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

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2023-04-20 14:43:17 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2023-04-20 14:43:17 +0300
commitdfc94207fec2d84314b1a5410cface22e8b369bd (patch)
treec54022f61ced104305889a64de080998a0dc773b /spec/frontend
parentb874efeff674f6bf0355d5d242ecf81c6f7155df (diff)
Add latest changes from gitlab-org/gitlab@15-11-stable-eev15.11.0-rc42
Diffstat (limited to 'spec/frontend')
-rw-r--r--spec/frontend/__helpers__/assert_props.js24
-rw-r--r--spec/frontend/__helpers__/wait_for_text.js2
-rw-r--r--spec/frontend/access_tokens/index_spec.js2
-rw-r--r--spec/frontend/add_context_commits_modal/components/__snapshots__/add_context_commits_modal_spec.js.snap11
-rw-r--r--spec/frontend/add_context_commits_modal/components/add_context_commits_modal_spec.js33
-rw-r--r--spec/frontend/admin/abuse_reports/components/abuse_report_actions_spec.js166
-rw-r--r--spec/frontend/admin/abuse_reports/components/abuse_report_details_spec.js53
-rw-r--r--spec/frontend/admin/abuse_reports/components/abuse_report_row_spec.js55
-rw-r--r--spec/frontend/admin/abuse_reports/components/abuse_reports_filtered_search_bar_spec.js33
-rw-r--r--spec/frontend/admin/abuse_reports/mock_data.js20
-rw-r--r--spec/frontend/admin/abuse_reports/utils_spec.js22
-rw-r--r--spec/frontend/admin/users/components/actions/actions_spec.js8
-rw-r--r--spec/frontend/admin/users/components/actions/delete_with_contributions_spec.js12
-rw-r--r--spec/frontend/admin/users/components/user_actions_spec.js12
-rw-r--r--spec/frontend/admin/users/new_spec.js7
-rw-r--r--spec/frontend/alerts_settings/components/__snapshots__/alerts_form_spec.js.snap1
-rw-r--r--spec/frontend/alerts_settings/components/alert_mapping_builder_spec.js2
-rw-r--r--spec/frontend/alerts_settings/components/alerts_settings_form_spec.js2
-rw-r--r--spec/frontend/alerts_settings/components/alerts_settings_wrapper_spec.js2
-rw-r--r--spec/frontend/analytics/cycle_analytics/base_spec.js1
-rw-r--r--spec/frontend/analytics/cycle_analytics/filter_bar_spec.js2
-rw-r--r--spec/frontend/analytics/cycle_analytics/mock_data.js2
-rw-r--r--spec/frontend/analytics/cycle_analytics/utils_spec.js9
-rw-r--r--spec/frontend/analytics/cycle_analytics/value_stream_filters_spec.js7
-rw-r--r--spec/frontend/analytics/shared/components/projects_dropdown_filter_spec.js26
-rw-r--r--spec/frontend/api/projects_api_spec.js22
-rw-r--r--spec/frontend/api/user_api_spec.js23
-rw-r--r--spec/frontend/artifacts/components/artifacts_bulk_delete_spec.js96
-rw-r--r--spec/frontend/artifacts_settings/components/keep_latest_artifact_checkbox_spec.js14
-rw-r--r--spec/frontend/authentication/password/components/password_input_spec.js49
-rw-r--r--spec/frontend/authentication/two_factor_auth/components/manage_two_factor_form_spec.js25
-rw-r--r--spec/frontend/authentication/webauthn/components/registration_spec.js2
-rw-r--r--spec/frontend/batch_comments/components/review_bar_spec.js4
-rw-r--r--spec/frontend/behaviors/gl_emoji_spec.js14
-rw-r--r--spec/frontend/behaviors/quick_submit_spec.js18
-rw-r--r--spec/frontend/behaviors/requires_input_spec.js5
-rw-r--r--spec/frontend/behaviors/shortcuts/shortcuts_issuable_spec.js7
-rw-r--r--spec/frontend/blob/components/blob_edit_header_spec.js20
-rw-r--r--spec/frontend/blob/components/blob_header_default_actions_spec.js5
-rw-r--r--spec/frontend/blob/components/mock_data.js2
-rw-r--r--spec/frontend/blob/file_template_selector_spec.js2
-rw-r--r--spec/frontend/blob/sketch/index_spec.js5
-rw-r--r--spec/frontend/boards/board_card_inner_spec.js4
-rw-r--r--spec/frontend/boards/board_list_spec.js120
-rw-r--r--spec/frontend/boards/components/board_app_spec.js38
-rw-r--r--spec/frontend/boards/components/board_card_spec.js45
-rw-r--r--spec/frontend/boards/components/board_column_spec.js2
-rw-r--r--spec/frontend/boards/components/board_content_sidebar_spec.js89
-rw-r--r--spec/frontend/boards/components/board_content_spec.js6
-rw-r--r--spec/frontend/boards/components/board_filtered_search_spec.js2
-rw-r--r--spec/frontend/boards/components/board_form_spec.js2
-rw-r--r--spec/frontend/boards/components/board_list_header_spec.js4
-rw-r--r--spec/frontend/boards/components/boards_selector_spec.js6
-rw-r--r--spec/frontend/boards/components/sidebar/board_sidebar_title_spec.js70
-rw-r--r--spec/frontend/boards/mock_data.js109
-rw-r--r--spec/frontend/boards/stores/actions_spec.js2
-rw-r--r--spec/frontend/branches/components/delete_branch_modal_spec.js94
-rw-r--r--spec/frontend/branches/components/delete_merged_branches_spec.js16
-rw-r--r--spec/frontend/captcha/captcha_modal_spec.js6
-rw-r--r--spec/frontend/captcha/init_recaptcha_script_spec.js2
-rw-r--r--spec/frontend/ci/artifacts/components/app_spec.js (renamed from spec/frontend/artifacts/components/app_spec.js)8
-rw-r--r--spec/frontend/ci/artifacts/components/artifact_row_spec.js (renamed from spec/frontend/artifacts/components/artifact_row_spec.js)12
-rw-r--r--spec/frontend/ci/artifacts/components/artifacts_bulk_delete_spec.js48
-rw-r--r--spec/frontend/ci/artifacts/components/artifacts_table_row_details_spec.js (renamed from spec/frontend/artifacts/components/artifacts_table_row_details_spec.js)12
-rw-r--r--spec/frontend/ci/artifacts/components/feedback_banner_spec.js (renamed from spec/frontend/artifacts/components/feedback_banner_spec.js)4
-rw-r--r--spec/frontend/ci/artifacts/components/job_artifacts_table_spec.js (renamed from spec/frontend/artifacts/components/job_artifacts_table_spec.js)244
-rw-r--r--spec/frontend/ci/artifacts/components/job_checkbox_spec.js (renamed from spec/frontend/artifacts/components/job_checkbox_spec.js)4
-rw-r--r--spec/frontend/ci/artifacts/graphql/cache_update_spec.js (renamed from spec/frontend/artifacts/graphql/cache_update_spec.js)4
-rw-r--r--spec/frontend/ci/ci_variable_list/ci_variable_list/ci_variable_list_spec.js10
-rw-r--r--spec/frontend/ci/ci_variable_list/components/ci_admin_variables_spec.js22
-rw-r--r--spec/frontend/ci/ci_variable_list/components/ci_environments_dropdown_spec.js149
-rw-r--r--spec/frontend/ci/ci_variable_list/components/ci_group_variables_spec.js22
-rw-r--r--spec/frontend/ci/ci_variable_list/components/ci_project_variables_spec.js27
-rw-r--r--spec/frontend/ci/ci_variable_list/components/ci_variable_modal_spec.js49
-rw-r--r--spec/frontend/ci/ci_variable_list/components/ci_variable_settings_spec.js47
-rw-r--r--spec/frontend/ci/ci_variable_list/components/ci_variable_shared_spec.js425
-rw-r--r--spec/frontend/ci/ci_variable_list/components/ci_variable_table_spec.js4
-rw-r--r--spec/frontend/ci/ci_variable_list/mocks.js5
-rw-r--r--spec/frontend/ci/pipeline_editor/components/commit/commit_form_spec.js4
-rw-r--r--spec/frontend/ci/pipeline_editor/components/drawer/cards/pipeline_config_reference_card_spec.js2
-rw-r--r--spec/frontend/ci/pipeline_editor/components/editor/ci_editor_header_spec.js4
-rw-r--r--spec/frontend/ci/pipeline_editor/components/file-nav/branch_switcher_spec.js2
-rw-r--r--spec/frontend/ci/pipeline_editor/components/file-tree/container_spec.js6
-rw-r--r--spec/frontend/ci/pipeline_editor/components/header/pipeline_editor_mini_graph_spec.js2
-rw-r--r--spec/frontend/ci/pipeline_editor/components/header/pipeline_status_spec.js2
-rw-r--r--spec/frontend/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/artifacts_and_cache_item_spec.js127
-rw-r--r--spec/frontend/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/job_setup_item_spec.js3
-rw-r--r--spec/frontend/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/rules_item_spec.js70
-rw-r--r--spec/frontend/ci/pipeline_editor/components/job_assistant_drawer/job_assistant_drawer_spec.js147
-rw-r--r--spec/frontend/ci/pipeline_editor/components/pipeline_editor_tabs_spec.js4
-rw-r--r--spec/frontend/ci/pipeline_editor/components/popovers/file_tree_popover_spec.js4
-rw-r--r--spec/frontend/ci/pipeline_editor/components/popovers/validate_pipeline_popover_spec.js2
-rw-r--r--spec/frontend/ci/pipeline_editor/components/popovers/walkthrough_popover_spec.js2
-rw-r--r--spec/frontend/ci/pipeline_editor/components/ui/editor_tab_spec.js4
-rw-r--r--spec/frontend/ci/pipeline_editor/graphql/resolvers_spec.js2
-rw-r--r--spec/frontend/ci/pipeline_editor/mock_data.js81
-rw-r--r--spec/frontend/ci/pipeline_editor/pipeline_editor_app_spec.js13
-rw-r--r--spec/frontend/ci/pipeline_new/components/pipeline_new_form_spec.js14
-rw-r--r--spec/frontend/ci/pipeline_new/components/refs_dropdown_spec.js6
-rw-r--r--spec/frontend/ci/pipeline_schedules/components/delete_pipeline_schedule_modal_spec.js4
-rw-r--r--spec/frontend/ci/pipeline_schedules/components/take_ownership_modal_spec.js4
-rw-r--r--spec/frontend/ci/runner/admin_new_runner_app/admin_new_runner_app_spec.js30
-rw-r--r--spec/frontend/ci/runner/admin_register_runner/admin_register_runner_app_spec.js4
-rw-r--r--spec/frontend/ci/runner/admin_runner_show/admin_runner_show_app_spec.js6
-rw-r--r--spec/frontend/ci/runner/admin_runners/admin_runners_app_spec.js31
-rw-r--r--spec/frontend/ci/runner/components/cells/runner_status_cell_spec.js8
-rw-r--r--spec/frontend/ci/runner/components/cells/runner_summary_cell_spec.js49
-rw-r--r--spec/frontend/ci/runner/components/registration/__snapshots__/utils_spec.js.snap9
-rw-r--r--spec/frontend/ci/runner/components/registration/registration_dropdown_spec.js50
-rw-r--r--spec/frontend/ci/runner/components/registration/registration_instructions_spec.js73
-rw-r--r--spec/frontend/ci/runner/components/registration/registration_token_reset_dropdown_item_spec.js6
-rw-r--r--spec/frontend/ci/runner/components/registration/registration_token_spec.js20
-rw-r--r--spec/frontend/ci/runner/components/registration/utils_spec.js34
-rw-r--r--spec/frontend/ci/runner/components/runner_create_form_spec.js31
-rw-r--r--spec/frontend/ci/runner/components/runner_delete_button_spec.js12
-rw-r--r--spec/frontend/ci/runner/components/runner_filtered_search_bar_spec.js14
-rw-r--r--spec/frontend/ci/runner/components/runner_list_empty_state_spec.js3
-rw-r--r--spec/frontend/ci/runner/components/runner_list_spec.js2
-rw-r--r--spec/frontend/ci/runner/components/runner_pause_button_spec.js2
-rw-r--r--spec/frontend/ci/runner/components/runner_platforms_radio_group_spec.js2
-rw-r--r--spec/frontend/ci/runner/components/runner_platforms_radio_spec.js4
-rw-r--r--spec/frontend/ci/runner/components/runner_projects_spec.js2
-rw-r--r--spec/frontend/ci/runner/components/runner_type_badge_spec.js3
-rw-r--r--spec/frontend/ci/runner/components/runner_type_tabs_spec.js3
-rw-r--r--spec/frontend/ci/runner/components/stat/runner_count_spec.js14
-rw-r--r--spec/frontend/ci/runner/group_new_runner_app/group_new_runner_app_spec.js132
-rw-r--r--spec/frontend/ci/runner/group_register_runner_app/group_register_runner_app_spec.js120
-rw-r--r--spec/frontend/ci/runner/group_runner_show/group_runner_show_app_spec.js4
-rw-r--r--spec/frontend/ci/runner/group_runners/group_runners_app_spec.js72
-rw-r--r--spec/frontend/ci/runner/mock_data.js130
-rw-r--r--spec/frontend/ci/runner/runner_search_utils_spec.js5
-rw-r--r--spec/frontend/ci/runner/sentry_utils_spec.js2
-rw-r--r--spec/frontend/clusters/agents/components/create_token_modal_spec.js2
-rw-r--r--spec/frontend/clusters/clusters_bundle_spec.js5
-rw-r--r--spec/frontend/clusters_list/components/agent_table_spec.js204
-rw-r--r--spec/frontend/clusters_list/components/install_agent_modal_spec.js2
-rw-r--r--spec/frontend/clusters_list/components/mock_data.js108
-rw-r--r--spec/frontend/comment_templates/components/__snapshots__/list_item_spec.js.snap140
-rw-r--r--spec/frontend/comment_templates/components/form_spec.js (renamed from spec/frontend/saved_replies/components/form_spec.js)27
-rw-r--r--spec/frontend/comment_templates/components/list_item_spec.js154
-rw-r--r--spec/frontend/comment_templates/components/list_spec.js (renamed from spec/frontend/saved_replies/components/list_spec.js)20
-rw-r--r--spec/frontend/comment_templates/pages/index_spec.js (renamed from spec/frontend/saved_replies/pages/index_spec.js)16
-rw-r--r--spec/frontend/commit/commit_box_pipeline_mini_graph_spec.js2
-rw-r--r--spec/frontend/commit/components/commit_box_pipeline_status_spec.js2
-rw-r--r--spec/frontend/content_editor/components/__snapshots__/toolbar_link_button_spec.js.snap33
-rw-r--r--spec/frontend/content_editor/components/bubble_menus/code_block_bubble_menu_spec.js6
-rw-r--r--spec/frontend/content_editor/components/bubble_menus/formatting_bubble_menu_spec.js2
-rw-r--r--spec/frontend/content_editor/components/bubble_menus/link_bubble_menu_spec.js63
-rw-r--r--spec/frontend/content_editor/components/bubble_menus/media_bubble_menu_spec.js8
-rw-r--r--spec/frontend/content_editor/components/content_editor_alert_spec.js2
-rw-r--r--spec/frontend/content_editor/components/formatting_toolbar_spec.js2
-rw-r--r--spec/frontend/content_editor/components/suggestions_dropdown_spec.js22
-rw-r--r--spec/frontend/content_editor/components/toolbar_attachment_button_spec.js57
-rw-r--r--spec/frontend/content_editor/components/toolbar_image_button_spec.js96
-rw-r--r--spec/frontend/content_editor/components/toolbar_link_button_spec.js223
-rw-r--r--spec/frontend/content_editor/components/toolbar_more_dropdown_spec.js2
-rw-r--r--spec/frontend/content_editor/components/wrappers/code_block_spec.js4
-rw-r--r--spec/frontend/content_editor/components/wrappers/details_spec.js2
-rw-r--r--spec/frontend/content_editor/components/wrappers/footnote_definition_spec.js2
-rw-r--r--spec/frontend/content_editor/components/wrappers/reference_label_spec.js (renamed from spec/frontend/content_editor/components/wrappers/label_spec.js)8
-rw-r--r--spec/frontend/content_editor/components/wrappers/reference_spec.js46
-rw-r--r--spec/frontend/content_editor/components/wrappers/table_cell_base_spec.js6
-rw-r--r--spec/frontend/content_editor/components/wrappers/table_cell_body_spec.js2
-rw-r--r--spec/frontend/content_editor/components/wrappers/table_cell_header_spec.js2
-rw-r--r--spec/frontend/content_editor/components/wrappers/table_of_contents_spec.js2
-rw-r--r--spec/frontend/content_editor/extensions/paste_markdown_spec.js52
-rw-r--r--spec/frontend/content_editor/markdown_snapshot_spec.js6
-rw-r--r--spec/frontend/content_editor/services/content_editor_spec.js4
-rw-r--r--spec/frontend/content_editor/services/create_content_editor_spec.js2
-rw-r--r--spec/frontend/content_editor/services/gl_api_markdown_deserializer_spec.js2
-rw-r--r--spec/frontend/content_editor/services/markdown_serializer_spec.js89
-rw-r--r--spec/frontend/content_editor/services/track_input_rules_and_shortcuts_spec.js4
-rw-r--r--spec/frontend/contributors/component/__snapshots__/contributors_spec.js.snap2
-rw-r--r--spec/frontend/custom_metrics/components/custom_metrics_form_fields_spec.js2
-rw-r--r--spec/frontend/deploy_keys/components/key_spec.js33
-rw-r--r--spec/frontend/design_management/components/design_notes/design_discussion_spec.js2
-rw-r--r--spec/frontend/design_management/components/design_notes/design_note_spec.js2
-rw-r--r--spec/frontend/design_management/components/design_notes/design_reply_form_spec.js67
-rw-r--r--spec/frontend/design_management/components/design_overlay_spec.js48
-rw-r--r--spec/frontend/design_management/components/design_scaler_spec.js2
-rw-r--r--spec/frontend/design_management/components/design_todo_button_spec.js4
-rw-r--r--spec/frontend/design_management/components/toolbar/__snapshots__/design_navigation_spec.js.snap2
-rw-r--r--spec/frontend/design_management/components/toolbar/design_navigation_spec.js64
-rw-r--r--spec/frontend/design_management/components/upload/design_version_dropdown_spec.js45
-rw-r--r--spec/frontend/design_management/components/upload/mock_data/all_versions.js20
-rw-r--r--spec/frontend/design_management/mock_data/all_versions.js8
-rw-r--r--spec/frontend/design_management/mock_data/apollo_mock.js197
-rw-r--r--spec/frontend/design_management/pages/design/__snapshots__/index_spec.js.snap4
-rw-r--r--spec/frontend/design_management/pages/design/index_spec.js2
-rw-r--r--spec/frontend/design_management/pages/index_spec.js41
-rw-r--r--spec/frontend/diffs/components/app_spec.js54
-rw-r--r--spec/frontend/diffs/components/commit_item_spec.js22
-rw-r--r--spec/frontend/diffs/components/diff_code_quality_item_spec.js66
-rw-r--r--spec/frontend/diffs/components/diff_code_quality_spec.js40
-rw-r--r--spec/frontend/diffs/components/diff_file_header_spec.js8
-rw-r--r--spec/frontend/diffs/components/diff_file_spec.js4
-rw-r--r--spec/frontend/diffs/components/diff_line_note_form_spec.js17
-rw-r--r--spec/frontend/diffs/components/diff_view_spec.js19
-rw-r--r--spec/frontend/diffs/components/hidden_files_warning_spec.js4
-rw-r--r--spec/frontend/diffs/components/shared/__snapshots__/findings_drawer_spec.js.snap126
-rw-r--r--spec/frontend/diffs/components/shared/findings_drawer_spec.js19
-rw-r--r--spec/frontend/diffs/create_diffs_store.js2
-rw-r--r--spec/frontend/diffs/mock_data/diff_code_quality.js5
-rw-r--r--spec/frontend/diffs/mock_data/findings_drawer.js21
-rw-r--r--spec/frontend/diffs/store/actions_spec.js331
-rw-r--r--spec/frontend/diffs/utils/merge_request_spec.js56
-rw-r--r--spec/frontend/drawio/drawio_editor_spec.js8
-rw-r--r--spec/frontend/dropzone_input_spec.js4
-rw-r--r--spec/frontend/editor/components/source_editor_toolbar_button_spec.js31
-rw-r--r--spec/frontend/editor/components/source_editor_toolbar_spec.js11
-rw-r--r--spec/frontend/editor/source_editor_markdown_livepreview_ext_spec.js102
-rw-r--r--spec/frontend/editor/utils_spec.js20
-rw-r--r--spec/frontend/emoji/awards_app/store/actions_spec.js4
-rw-r--r--spec/frontend/environment.js11
-rw-r--r--spec/frontend/environments/deploy_board_component_spec.js4
-rw-r--r--spec/frontend/environments/environment_actions_spec.js114
-rw-r--r--spec/frontend/environments/environment_details/components/deployment_actions_spec.js119
-rw-r--r--spec/frontend/environments/environment_details/page_spec.js35
-rw-r--r--spec/frontend/environments/environment_folder_spec.js2
-rw-r--r--spec/frontend/environments/environment_stop_spec.js2
-rw-r--r--spec/frontend/environments/environments_app_spec.js2
-rw-r--r--spec/frontend/environments/graphql/mock_data.js10
-rw-r--r--spec/frontend/environments/graphql/resolvers_spec.js57
-rw-r--r--spec/frontend/environments/helpers/__snapshots__/deployment_data_transformation_helper_spec.js.snap34
-rw-r--r--spec/frontend/environments/kubernetes_overview_spec.js56
-rw-r--r--spec/frontend/environments/kubernetes_pods_spec.js114
-rw-r--r--spec/frontend/environments/mock_data.js3
-rw-r--r--spec/frontend/environments/new_environment_item_spec.js27
-rw-r--r--spec/frontend/environments/stop_stale_environments_modal_spec.js4
-rw-r--r--spec/frontend/error_tracking/components/error_tracking_list_spec.js25
-rw-r--r--spec/frontend/feature_flags/components/configure_feature_flags_modal_spec.js8
-rw-r--r--spec/frontend/feature_flags/components/feature_flags_spec.js4
-rw-r--r--spec/frontend/filtered_search/dropdown_user_spec.js7
-rw-r--r--spec/frontend/filtered_search/dropdown_utils_spec.js7
-rw-r--r--spec/frontend/fixtures/api_projects.rb15
-rw-r--r--spec/frontend/fixtures/comment_templates.rb (renamed from spec/frontend/fixtures/saved_replies.rb)24
-rw-r--r--spec/frontend/fixtures/issues.rb9
-rw-r--r--spec/frontend/fixtures/job_artifacts.rb2
-rw-r--r--spec/frontend/fixtures/jobs.rb64
-rw-r--r--spec/frontend/fixtures/milestones.rb43
-rw-r--r--spec/frontend/fixtures/pipelines.rb25
-rw-r--r--spec/frontend/fixtures/projects.rb2
-rw-r--r--spec/frontend/fixtures/runner.rb28
-rw-r--r--spec/frontend/fixtures/startup_css.rb15
-rw-r--r--spec/frontend/fixtures/static/oauth_remember_me.html2
-rw-r--r--spec/frontend/fixtures/static/search_autocomplete.html15
-rw-r--r--spec/frontend/fixtures/timelogs.rb53
-rw-r--r--spec/frontend/frequent_items/components/app_spec.js1
-rw-r--r--spec/frontend/frequent_items/components/frequent_items_list_spec.js2
-rw-r--r--spec/frontend/frequent_items/store/actions_spec.js1
-rw-r--r--spec/frontend/gfm_auto_complete_spec.js5
-rw-r--r--spec/frontend/groups/components/app_spec.js6
-rw-r--r--spec/frontend/groups/components/empty_states/archived_projects_empty_state_spec.js4
-rw-r--r--spec/frontend/groups/components/empty_states/shared_projects_empty_state_spec.js4
-rw-r--r--spec/frontend/groups/components/empty_states/subgroups_and_projects_empty_state_spec.js1
-rw-r--r--spec/frontend/groups/components/groups_spec.js2
-rw-r--r--spec/frontend/groups/components/overview_tabs_spec.js1
-rw-r--r--spec/frontend/groups/components/transfer_group_form_spec.js2
-rw-r--r--spec/frontend/groups/settings/components/group_settings_readme_spec.js112
-rw-r--r--spec/frontend/groups/settings/mock_data.js6
-rw-r--r--spec/frontend/header_search/components/app_spec.js72
-rw-r--r--spec/frontend/header_search/init_spec.js18
-rw-r--r--spec/frontend/helpers/init_simple_app_helper_spec.js6
-rw-r--r--spec/frontend/ide/components/activity_bar_spec.js55
-rw-r--r--spec/frontend/ide/components/cannot_push_code_alert_spec.js2
-rw-r--r--spec/frontend/ide/components/commit_sidebar/form_spec.js21
-rw-r--r--spec/frontend/ide/components/commit_sidebar/list_spec.js2
-rw-r--r--spec/frontend/ide/components/ide_review_spec.js2
-rw-r--r--spec/frontend/ide/components/ide_spec.js4
-rw-r--r--spec/frontend/ide/components/ide_tree_spec.js76
-rw-r--r--spec/frontend/ide/components/new_dropdown/index_spec.js56
-rw-r--r--spec/frontend/ide/components/repo_commit_section_spec.js17
-rw-r--r--spec/frontend/ide/components/repo_editor_spec.js14
-rw-r--r--spec/frontend/ide/components/shared/commit_message_field_spec.js2
-rw-r--r--spec/frontend/ide/init_gitlab_web_ide_spec.js3
-rw-r--r--spec/frontend/ide/lib/languages/codeowners_spec.js85
-rw-r--r--spec/frontend/ide/stores/actions_spec.js4
-rw-r--r--spec/frontend/ide/stores/modules/commit/actions_spec.js66
-rw-r--r--spec/frontend/import/details/components/import_details_app_spec.js23
-rw-r--r--spec/frontend/import/details/components/import_details_table_spec.js33
-rw-r--r--spec/frontend/import/details/mock_data.js31
-rw-r--r--spec/frontend/import_entities/components/import_status_spec.js72
-rw-r--r--spec/frontend/import_entities/import_groups/components/import_actions_cell_spec.js4
-rw-r--r--spec/frontend/import_entities/import_groups/components/import_table_spec.js2
-rw-r--r--spec/frontend/import_entities/import_groups/graphql/client_factory_spec.js2
-rw-r--r--spec/frontend/import_entities/import_groups/services/status_poller_spec.js2
-rw-r--r--spec/frontend/incidents/components/incidents_list_spec.js2
-rw-r--r--spec/frontend/incidents_settings/components/incidents_settings_service_spec.js2
-rw-r--r--spec/frontend/integrations/edit/components/integration_form_spec.js2
-rw-r--r--spec/frontend/integrations/edit/components/jira_issues_fields_spec.js5
-rw-r--r--spec/frontend/invite_members/components/invite_group_notification_spec.js14
-rw-r--r--spec/frontend/invite_members/components/invite_groups_modal_spec.js26
-rw-r--r--spec/frontend/invite_members/components/invite_members_modal_spec.js278
-rw-r--r--spec/frontend/invite_members/mock_data/member_modal.js31
-rw-r--r--spec/frontend/invite_members/utils/member_utils_spec.js30
-rw-r--r--spec/frontend/invite_members/utils/trigger_successful_invite_alert_spec.js4
-rw-r--r--spec/frontend/issuable/issuable_form_spec.js69
-rw-r--r--spec/frontend/issuable/popover/components/mr_popover_spec.js4
-rw-r--r--spec/frontend/issuable/related_issues/components/add_issuable_form_spec.js2
-rw-r--r--spec/frontend/issues/issue_spec.js8
-rw-r--r--spec/frontend/issues/list/components/issues_list_app_spec.js4
-rw-r--r--spec/frontend/issues/list/components/jira_issues_import_status_app_spec.js2
-rw-r--r--spec/frontend/issues/new/components/type_select_spec.js141
-rw-r--r--spec/frontend/issues/show/components/app_spec.js19
-rw-r--r--spec/frontend/issues/show/components/description_spec.js82
-rw-r--r--spec/frontend/issues/show/components/edited_spec.js73
-rw-r--r--spec/frontend/issues/show/components/fields/description_spec.js7
-rw-r--r--spec/frontend/issues/show/components/header_actions_spec.js2
-rw-r--r--spec/frontend/issues/show/components/incidents/create_timeline_events_form_spec.js1
-rw-r--r--spec/frontend/issues/show/components/incidents/incident_tabs_spec.js2
-rw-r--r--spec/frontend/issues/show/components/incidents/timeline_events_form_spec.js14
-rw-r--r--spec/frontend/issues/show/components/locked_warning_spec.js4
-rw-r--r--spec/frontend/issues/show/mock_data/mock_data.js21
-rw-r--r--spec/frontend/jira_connect/branches/components/source_branch_dropdown_spec.js4
-rw-r--r--spec/frontend/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/index_spec.js10
-rw-r--r--spec/frontend/jira_import/components/jira_import_form_spec.js4
-rw-r--r--spec/frontend/jobs/components/job/manual_variables_form_spec.js4
-rw-r--r--spec/frontend/jobs/components/job/sidebar_header_spec.js2
-rw-r--r--spec/frontend/jobs/components/job/sidebar_spec.js4
-rw-r--r--spec/frontend/jobs/components/table/cells/actions_cell_spec.js2
-rw-r--r--spec/frontend/jobs/components/table/job_table_app_spec.js37
-rw-r--r--spec/frontend/jobs/mock_data.js12
-rw-r--r--spec/frontend/labels/components/delete_label_modal_spec.js87
-rw-r--r--spec/frontend/lib/apollo/indexed_db_persistent_storage_spec.js90
-rw-r--r--spec/frontend/lib/apollo/mock_data/cache_with_persist_directive_and_field.json151
-rw-r--r--spec/frontend/lib/apollo/persist_link_spec.js4
-rw-r--r--spec/frontend/lib/utils/chart_utils_spec.js55
-rw-r--r--spec/frontend/lib/utils/color_utils_spec.js2
-rw-r--r--spec/frontend/lib/utils/datetime/time_spent_utility_spec.js25
-rw-r--r--spec/frontend/lib/utils/error_message_spec.js101
-rw-r--r--spec/frontend/lib/utils/intersection_observer_spec.js2
-rw-r--r--spec/frontend/lib/utils/poll_spec.js2
-rw-r--r--spec/frontend/lib/utils/secret_detection_spec.js68
-rw-r--r--spec/frontend/lib/utils/web_ide_navigator_spec.js38
-rw-r--r--spec/frontend/members/components/table/expiration_datepicker_spec.js2
-rw-r--r--spec/frontend/members/components/table/role_dropdown_spec.js86
-rw-r--r--spec/frontend/members/utils_spec.js2
-rw-r--r--spec/frontend/ml/experiment_tracking/components/delete_button_spec.js68
-rw-r--r--spec/frontend/ml/experiment_tracking/routes/candidates/show/__snapshots__/ml_candidates_show_spec.js.snap113
-rw-r--r--spec/frontend/ml/experiment_tracking/routes/candidates/show/ml_candidates_show_spec.js25
-rw-r--r--spec/frontend/ml/experiment_tracking/routes/experiments/show/components/experiment_header_spec.js55
-rw-r--r--spec/frontend/ml/experiment_tracking/routes/experiments/show/ml_experiments_show_spec.js18
-rw-r--r--spec/frontend/ml/experiment_tracking/routes/experiments/show/mock_data.js2
-rw-r--r--spec/frontend/monitoring/components/variables/dropdown_field_spec.js2
-rw-r--r--spec/frontend/monitoring/pages/dashboard_page_spec.js3
-rw-r--r--spec/frontend/monitoring/store/actions_spec.js2
-rw-r--r--spec/frontend/nav/components/new_nav_toggle_spec.js52
-rw-r--r--spec/frontend/new_branch_spec.js2
-rw-r--r--spec/frontend/notebook/cells/output/dataframe_spec.js59
-rw-r--r--spec/frontend/notebook/cells/output/dataframe_util_spec.js113
-rw-r--r--spec/frontend/notebook/cells/output/index_spec.js18
-rw-r--r--spec/frontend/notebook/mock_data.js44
-rw-r--r--spec/frontend/notes/components/comment_form_spec.js34
-rw-r--r--spec/frontend/notes/components/discussion_filter_spec.js122
-rw-r--r--spec/frontend/notes/components/note_actions/timeline_event_button_spec.js2
-rw-r--r--spec/frontend/notes/components/note_awards_list_spec.js236
-rw-r--r--spec/frontend/notes/components/note_body_spec.js8
-rw-r--r--spec/frontend/notes/components/note_form_spec.js224
-rw-r--r--spec/frontend/notes/components/noteable_note_spec.js11
-rw-r--r--spec/frontend/notes/components/notes_app_spec.js2
-rw-r--r--spec/frontend/notes/deprecated_notes_spec.js11
-rw-r--r--spec/frontend/notes/stores/actions_spec.js6
-rw-r--r--spec/frontend/notifications/components/custom_notifications_modal_spec.js2
-rw-r--r--spec/frontend/oauth_application/components/oauth_secret_spec.js116
-rw-r--r--spec/frontend/oauth_remember_me_spec.js9
-rw-r--r--spec/frontend/observability/index_spec.js2
-rw-r--r--spec/frontend/operation_settings/components/metrics_settings_spec.js2
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/details_header_spec.js44
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/tags_list_spec.js289
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/registry_header_spec.js6
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/mock_data.js1
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/pages/details_spec.js119
-rw-r--r--spec/frontend/packages_and_registries/dependency_proxy/app_spec.js33
-rw-r--r--spec/frontend/packages_and_registries/dependency_proxy/components/manifest_list_spec.js19
-rw-r--r--spec/frontend/packages_and_registries/harbor_registry/pages/list_spec.js4
-rw-r--r--spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/infrastructure_title_spec.js2
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/delete_modal_spec.js73
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/package_versions_list_spec.js165
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/list/package_list_row_spec.js2
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/list/packages_list_spec.js49
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/list/packages_search_spec.js2
-rw-r--r--spec/frontend/packages_and_registries/package_registry/mock_data.js69
-rw-r--r--spec/frontend/packages_and_registries/package_registry/pages/details_spec.js101
-rw-r--r--spec/frontend/packages_and_registries/package_registry/pages/list_spec.js73
-rw-r--r--spec/frontend/packages_and_registries/settings/group/components/package_settings_spec.js4
-rw-r--r--spec/frontend/packages_and_registries/settings/group/components/packages_forwarding_settings_spec.js21
-rw-r--r--spec/frontend/packages_and_registries/settings/project/settings/components/cleanup_image_tags_spec.js2
-rw-r--r--spec/frontend/packages_and_registries/settings/project/settings/components/container_expiration_policy_form_spec.js2
-rw-r--r--spec/frontend/packages_and_registries/settings/project/settings/components/container_expiration_policy_spec.js2
-rw-r--r--spec/frontend/packages_and_registries/settings/project/settings/components/packages_cleanup_policy_form_spec.js2
-rw-r--r--spec/frontend/packages_and_registries/settings/project/settings/components/registry_settings_app_spec.js2
-rw-r--r--spec/frontend/packages_and_registries/shared/components/__snapshots__/registry_breadcrumb_spec.js.snap29
-rw-r--r--spec/frontend/packages_and_registries/shared/components/persisted_search_spec.js2
-rw-r--r--spec/frontend/packages_and_registries/shared/components/registry_list_spec.js2
-rw-r--r--spec/frontend/pages/admin/abuse_reports/abuse_reports_spec.js6
-rw-r--r--spec/frontend/pages/admin/application_settings/account_and_limits_spec.js7
-rw-r--r--spec/frontend/pages/admin/application_settings/metrics_and_profiling/usage_statistics_spec.js6
-rw-r--r--spec/frontend/pages/admin/jobs/components/cancel_jobs_modal_spec.js (renamed from spec/frontend/pages/admin/jobs/index/components/cancel_jobs_modal_spec.js)2
-rw-r--r--spec/frontend/pages/admin/jobs/components/cancel_jobs_spec.js (renamed from spec/frontend/pages/admin/jobs/index/components/cancel_jobs_spec.js)6
-rw-r--r--spec/frontend/pages/admin/jobs/components/table/admin_job_table_app_spec.js100
-rw-r--r--spec/frontend/pages/admin/jobs/components/table/graphql/cache_config_spec.js106
-rw-r--r--spec/frontend/pages/admin/projects/components/namespace_select_spec.js4
-rw-r--r--spec/frontend/pages/dashboard/todos/index/todos_spec.js5
-rw-r--r--spec/frontend/pages/groups/new/components/app_spec.js5
-rw-r--r--spec/frontend/pages/import/bitbucket_server/components/bitbucket_server_status_table_spec.js2
-rw-r--r--spec/frontend/pages/import/bulk_imports/history/components/bulk_imports_history_app_spec.js2
-rw-r--r--spec/frontend/pages/import/history/components/import_history_app_spec.js3
-rw-r--r--spec/frontend/pages/projects/forks/new/components/project_namespace_spec.js2
-rw-r--r--spec/frontend/pages/projects/shared/permissions/components/settings_panel_spec.js2
-rw-r--r--spec/frontend/pages/sessions/new/preserve_url_fragment_spec.js5
-rw-r--r--spec/frontend/pages/sessions/new/signin_tabs_memoizer_spec.js6
-rw-r--r--spec/frontend/pages/shared/wikis/components/wiki_form_spec.js9
-rw-r--r--spec/frontend/performance_bar/components/performance_bar_app_spec.js55
-rw-r--r--spec/frontend/performance_bar/index_spec.js10
-rw-r--r--spec/frontend/performance_bar/services/performance_bar_service_spec.js2
-rw-r--r--spec/frontend/performance_bar/stores/performance_bar_store_spec.js8
-rw-r--r--spec/frontend/pipeline_wizard/components/commit_spec.js8
-rw-r--r--spec/frontend/pipeline_wizard/components/step_nav_spec.js6
-rw-r--r--spec/frontend/pipeline_wizard/components/step_spec.js2
-rw-r--r--spec/frontend/pipeline_wizard/components/widgets/list_spec.js4
-rw-r--r--spec/frontend/pipeline_wizard/components/widgets/text_spec.js2
-rw-r--r--spec/frontend/pipeline_wizard/components/wrapper_spec.js8
-rw-r--r--spec/frontend/pipelines/components/dag/dag_spec.js12
-rw-r--r--spec/frontend/pipelines/components/jobs/jobs_app_spec.js2
-rw-r--r--spec/frontend/pipelines/components/pipeline_mini_graph/pipeline_stage_spec.js2
-rw-r--r--spec/frontend/pipelines/components/pipelines_list/empty_state/pipelines_ci_templates_spec.js104
-rw-r--r--spec/frontend/pipelines/graph/graph_component_wrapper_spec.js171
-rw-r--r--spec/frontend/pipelines/graph/graph_view_selector_spec.js15
-rw-r--r--spec/frontend/pipelines/graph/linked_pipeline_spec.js168
-rw-r--r--spec/frontend/pipelines/pipeline_operations_spec.js77
-rw-r--r--spec/frontend/pipelines/pipelines_actions_spec.js168
-rw-r--r--spec/frontend/pipelines/pipelines_manual_actions_spec.js216
-rw-r--r--spec/frontend/pipelines/pipelines_spec.js14
-rw-r--r--spec/frontend/pipelines/test_reports/stores/mutations_spec.js2
-rw-r--r--spec/frontend/profile/account/components/update_username_spec.js2
-rw-r--r--spec/frontend/profile/components/overview_tab_spec.js53
-rw-r--r--spec/frontend/profile/components/profile_tabs_spec.js57
-rw-r--r--spec/frontend/projects/commit/components/branches_dropdown_spec.js6
-rw-r--r--spec/frontend/projects/commit/components/commit_options_dropdown_spec.js2
-rw-r--r--spec/frontend/projects/commit/components/form_modal_spec.js74
-rw-r--r--spec/frontend/projects/commit/store/actions_spec.js2
-rw-r--r--spec/frontend/projects/commits/components/author_select_spec.js51
-rw-r--r--spec/frontend/projects/commits/store/actions_spec.js2
-rw-r--r--spec/frontend/projects/compare/components/revision_dropdown_legacy_spec.js2
-rw-r--r--spec/frontend/projects/compare/components/revision_dropdown_spec.js4
-rw-r--r--spec/frontend/projects/new/components/app_spec.js3
-rw-r--r--spec/frontend/projects/new/components/new_project_url_select_spec.js2
-rw-r--r--spec/frontend/projects/pipelines/charts/components/__snapshots__/ci_cd_analytics_area_chart_spec.js.snap1
-rw-r--r--spec/frontend/projects/settings/components/transfer_project_form_spec.js6
-rw-r--r--spec/frontend/projects/settings/topics/components/topics_token_selector_spec.js2
-rw-r--r--spec/frontend/projects/settings_service_desk/components/service_desk_root_spec.js4
-rw-r--r--spec/frontend/prometheus_metrics/custom_metrics_spec.js6
-rw-r--r--spec/frontend/prometheus_metrics/prometheus_metrics_spec.js7
-rw-r--r--spec/frontend/protected_branches/protected_branch_edit_spec.js2
-rw-r--r--spec/frontend/read_more_spec.js2
-rw-r--r--spec/frontend/ref/components/ref_selector_spec.js22
-rw-r--r--spec/frontend/ref/stores/actions_spec.js7
-rw-r--r--spec/frontend/ref/stores/mutations_spec.js10
-rw-r--r--spec/frontend/releases/components/app_edit_new_spec.js21
-rw-r--r--spec/frontend/releases/components/app_index_spec.js4
-rw-r--r--spec/frontend/releases/components/releases_sort_spec.js3
-rw-r--r--spec/frontend/releases/components/tag_create_spec.js107
-rw-r--r--spec/frontend/releases/components/tag_field_new_spec.js229
-rw-r--r--spec/frontend/releases/components/tag_search_spec.js144
-rw-r--r--spec/frontend/releases/stores/modules/detail/getters_spec.js27
-rw-r--r--spec/frontend/repository/components/blob_button_group_spec.js34
-rw-r--r--spec/frontend/repository/components/breadcrumbs_spec.js107
-rw-r--r--spec/frontend/repository/components/delete_blob_modal_spec.js8
-rw-r--r--spec/frontend/repository/components/fork_info_spec.js220
-rw-r--r--spec/frontend/repository/components/fork_sync_conflicts_modal_spec.js6
-rw-r--r--spec/frontend/repository/components/last_commit_spec.js24
-rw-r--r--spec/frontend/repository/components/new_directory_modal_spec.js2
-rw-r--r--spec/frontend/repository/components/preview/index_spec.js2
-rw-r--r--spec/frontend/repository/components/table/row_spec.js287
-rw-r--r--spec/frontend/repository/components/tree_content_spec.js2
-rw-r--r--spec/frontend/repository/mock_data.js4
-rw-r--r--spec/frontend/saved_replies/components/__snapshots__/list_item_spec.js.snap67
-rw-r--r--spec/frontend/saved_replies/components/list_item_spec.js50
-rw-r--r--spec/frontend/scripts/frontend/__fixtures__/locale/de/converted.json21
-rw-r--r--spec/frontend/scripts/frontend/__fixtures__/locale/de/gitlab.po13
-rw-r--r--spec/frontend/scripts/frontend/po_to_json_spec.js244
-rw-r--r--spec/frontend/search/highlight_blob_search_result_spec.js6
-rw-r--r--spec/frontend/search/mock_data.js300
-rw-r--r--spec/frontend/search/sidebar/components/app_spec.js2
-rw-r--r--spec/frontend/search/sidebar/components/checkbox_filter_spec.js52
-rw-r--r--spec/frontend/search/sidebar/components/confidentiality_filter_spec.js35
-rw-r--r--spec/frontend/search/sidebar/components/language_filter_spec.js53
-rw-r--r--spec/frontend/search/sidebar/components/scope_navigation_spec.js18
-rw-r--r--spec/frontend/search/sidebar/components/scope_new_navigation_spec.js83
-rw-r--r--spec/frontend/search/sidebar/components/status_filter_spec.js35
-rw-r--r--spec/frontend/search/store/getters_spec.js8
-rw-r--r--spec/frontend/search/store/utils_spec.js15
-rw-r--r--spec/frontend/search/topbar/components/searchable_dropdown_spec.js2
-rw-r--r--spec/frontend/search_autocomplete_spec.js292
-rw-r--r--spec/frontend/search_autocomplete_utils_spec.js114
-rw-r--r--spec/frontend/security_configuration/components/app_spec.js28
-rw-r--r--spec/frontend/security_configuration/components/feature_card_spec.js101
-rw-r--r--spec/frontend/security_configuration/components/training_provider_list_spec.js2
-rw-r--r--spec/frontend/sentry/sentry_browser_wrapper_spec.js2
-rw-r--r--spec/frontend/shortcuts_spec.js17
-rw-r--r--spec/frontend/sidebar/components/assignees/sidebar_participant_spec.js3
-rw-r--r--spec/frontend/sidebar/components/assignees/user_name_with_status_spec.js4
-rw-r--r--spec/frontend/sidebar/components/date/sidebar_date_widget_spec.js27
-rw-r--r--spec/frontend/sidebar/components/labels/labels_select_vue/dropdown_contents_create_view_spec.js85
-rw-r--r--spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_contents_create_view_spec.js30
-rw-r--r--spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_contents_labels_view_spec.js4
-rw-r--r--spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_header_spec.js4
-rw-r--r--spec/frontend/sidebar/components/labels/labels_select_widget/labels_select_root_spec.js2
-rw-r--r--spec/frontend/sidebar/components/labels/labels_select_widget/mock_data.js11
-rw-r--r--spec/frontend/sidebar/components/move/issuable_move_dropdown_spec.js4
-rw-r--r--spec/frontend/sidebar/components/move/move_issue_button_spec.js4
-rw-r--r--spec/frontend/sidebar/components/move/move_issues_button_spec.js2
-rw-r--r--spec/frontend/sidebar/components/reviewers/uncollapsed_reviewer_list_spec.js149
-rw-r--r--spec/frontend/sidebar/components/severity/sidebar_severity_widget_spec.js (renamed from spec/frontend/sidebar/components/severity/sidebar_severity_spec.js)86
-rw-r--r--spec/frontend/sidebar/components/sidebar_dropdown_widget_spec.js26
-rw-r--r--spec/frontend/sidebar/components/subscriptions/subscriptions_spec.js61
-rw-r--r--spec/frontend/sidebar/components/time_tracking/time_tracker_spec.js2
-rw-r--r--spec/frontend/sidebar/components/toggle/toggle_sidebar_spec.js2
-rw-r--r--spec/frontend/sidebar/mock_data.js11
-rw-r--r--spec/frontend/snippets/components/__snapshots__/snippet_visibility_edit_spec.js.snap2
-rw-r--r--spec/frontend/snippets/components/edit_spec.js2
-rw-r--r--spec/frontend/snippets/components/snippet_blob_view_spec.js220
-rw-r--r--spec/frontend/snippets/components/snippet_header_spec.js2
-rw-r--r--spec/frontend/super_sidebar/components/context_switcher_spec.js52
-rw-r--r--spec/frontend/super_sidebar/components/create_menu_spec.js24
-rw-r--r--spec/frontend/super_sidebar/components/frequent_items_list_spec.js35
-rw-r--r--spec/frontend/super_sidebar/components/global_search/components/global_search_autocomplete_items_spec.js196
-rw-r--r--spec/frontend/super_sidebar/components/global_search/components/global_search_default_items_spec.js61
-rw-r--r--spec/frontend/super_sidebar/components/global_search/components/global_search_scoped_items_spec.js107
-rw-r--r--spec/frontend/super_sidebar/components/global_search/components/global_search_spec.js324
-rw-r--r--spec/frontend/super_sidebar/components/global_search/mock_data.js202
-rw-r--r--spec/frontend/super_sidebar/components/global_search/store/actions_spec.js4
-rw-r--r--spec/frontend/super_sidebar/components/global_search/store/getters_spec.js7
-rw-r--r--spec/frontend/super_sidebar/components/global_search/store/mutations_spec.js2
-rw-r--r--spec/frontend/super_sidebar/components/global_search/utils_spec.js60
-rw-r--r--spec/frontend/super_sidebar/components/groups_list_spec.js9
-rw-r--r--spec/frontend/super_sidebar/components/help_center_spec.js103
-rw-r--r--spec/frontend/super_sidebar/components/items_list_spec.js45
-rw-r--r--spec/frontend/super_sidebar/components/merge_request_menu_spec.js19
-rw-r--r--spec/frontend/super_sidebar/components/nav_item_spec.js38
-rw-r--r--spec/frontend/super_sidebar/components/pinned_section_spec.js75
-rw-r--r--spec/frontend/super_sidebar/components/projects_list_spec.js9
-rw-r--r--spec/frontend/super_sidebar/components/search_results_spec.js16
-rw-r--r--spec/frontend/super_sidebar/components/sidebar_menu_spec.js151
-rw-r--r--spec/frontend/super_sidebar/components/super_sidebar_spec.js154
-rw-r--r--spec/frontend/super_sidebar/components/super_sidebar_toggle_spec.js106
-rw-r--r--spec/frontend/super_sidebar/components/user_bar_spec.js142
-rw-r--r--spec/frontend/super_sidebar/components/user_menu_spec.js189
-rw-r--r--spec/frontend/super_sidebar/components/user_name_group_spec.js22
-rw-r--r--spec/frontend/super_sidebar/mock_data.js28
-rw-r--r--spec/frontend/super_sidebar/super_sidebar_collapsed_state_manager_spec.js67
-rw-r--r--spec/frontend/surveys/merge_request_performance/app_spec.js28
-rw-r--r--spec/frontend/tags/components/delete_tag_modal_spec.js2
-rw-r--r--spec/frontend/terraform/components/states_table_actions_spec.js2
-rw-r--r--spec/frontend/test_setup.js13
-rw-r--r--spec/frontend/time_tracking/components/timelog_source_cell_spec.js136
-rw-r--r--spec/frontend/time_tracking/components/timelogs_app_spec.js238
-rw-r--r--spec/frontend/time_tracking/components/timelogs_table_spec.js223
-rw-r--r--spec/frontend/toggles/index_spec.js2
-rw-r--r--spec/frontend/tracking/tracking_initialization_spec.js21
-rw-r--r--spec/frontend/tracking/tracking_spec.js91
-rw-r--r--spec/frontend/usage_quotas/storage/components/project_storage_detail_spec.js49
-rw-r--r--spec/frontend/usage_quotas/storage/components/storage_type_icon_spec.js1
-rw-r--r--spec/frontend/usage_quotas/storage/components/usage_graph_spec.js24
-rw-r--r--spec/frontend/usage_quotas/storage/mock_data.js26
-rw-r--r--spec/frontend/user_lists/components/edit_user_list_spec.js2
-rw-r--r--spec/frontend/user_lists/components/user_lists_spec.js2
-rw-r--r--spec/frontend/user_popovers_spec.js8
-rw-r--r--spec/frontend/vue_merge_request_widget/components/mr_widget_memory_usage_spec.js245
-rw-r--r--spec/frontend/vue_merge_request_widget/components/mr_widget_rebase_spec.js343
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/mr_widget_merging_spec.js12
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/mr_widget_ready_to_merge_spec.js7
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/mr_widget_unresolved_discussions_spec.js36
-rw-r--r--spec/frontend/vue_merge_request_widget/components/widget/__snapshots__/dynamic_content_spec.js.snap82
-rw-r--r--spec/frontend/vue_merge_request_widget/components/widget/dynamic_content_spec.js2
-rw-r--r--spec/frontend/vue_merge_request_widget/components/widget/widget_spec.js22
-rw-r--r--spec/frontend/vue_merge_request_widget/extentions/accessibility/index_spec.js2
-rw-r--r--spec/frontend/vue_merge_request_widget/extentions/code_quality/index_spec.js2
-rw-r--r--spec/frontend/vue_merge_request_widget/extentions/terraform/index_spec.js2
-rw-r--r--spec/frontend/vue_merge_request_widget/mr_widget_options_spec.js39
-rw-r--r--spec/frontend/vue_shared/alert_details/alert_details_spec.js10
-rw-r--r--spec/frontend/vue_shared/alert_details/alert_metrics_spec.js63
-rw-r--r--spec/frontend/vue_shared/alert_details/sidebar/alert_sidebar_assignees_spec.js157
-rw-r--r--spec/frontend/vue_shared/components/color_picker/color_picker_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/color_select_dropdown/color_select_root_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/color_select_dropdown/dropdown_contents_color_view_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/color_select_dropdown/dropdown_contents_spec.js19
-rw-r--r--spec/frontend/vue_shared/components/color_select_dropdown/dropdown_value_spec.js11
-rw-r--r--spec/frontend/vue_shared/components/diff_viewer/utils_spec.js33
-rw-r--r--spec/frontend/vue_shared/components/diff_viewer/viewers/renamed_spec.js250
-rw-r--r--spec/frontend/vue_shared/components/dropdown/dropdown_widget_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/dropdown_keyboard_navigation_spec.js21
-rw-r--r--spec/frontend/vue_shared/components/entity_select/entity_select_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/entity_select/project_select_spec.js13
-rw-r--r--spec/frontend/vue_shared/components/file_finder/index_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/file_finder/item_spec.js10
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_bar_root_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/base_token_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/crm_organization_token_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/emoji_token_spec.js6
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/label_token_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/milestone_token_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/user_token_spec.js6
-rw-r--r--spec/frontend/vue_shared/components/gl_countdown_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/listbox_input/listbox_input_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/markdown/comment_templates_dropdown_spec.js76
-rw-r--r--spec/frontend/vue_shared/components/markdown/editor_mode_dropdown_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/markdown/markdown_editor_spec.js50
-rw-r--r--spec/frontend/vue_shared/components/markdown/saved_replies_dropdown_spec.js62
-rw-r--r--spec/frontend/vue_shared/components/markdown/suggestions_spec.js54
-rw-r--r--spec/frontend/vue_shared/components/markdown_drawer/markdown_drawer_spec.js8
-rw-r--r--spec/frontend/vue_shared/components/notes/system_note_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/projects_list/projects_list_item_spec.js169
-rw-r--r--spec/frontend/vue_shared/components/projects_list/projects_list_spec.js34
-rw-r--r--spec/frontend/vue_shared/components/registry/__snapshots__/code_instruction_spec.js.snap32
-rw-r--r--spec/frontend/vue_shared/components/registry/__snapshots__/history_item_spec.js.snap2
-rw-r--r--spec/frontend/vue_shared/components/registry/code_instruction_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/resizable_chart/__snapshots__/skeleton_loader_spec.js.snap4
-rw-r--r--spec/frontend/vue_shared/components/runner_instructions/instructions/runner_cli_instructions_spec.js6
-rw-r--r--spec/frontend/vue_shared/components/runner_instructions/runner_instructions_modal_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/slot_switch_spec.js5
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/components/chunk_line_spec.js6
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/components/chunk_spec.js1
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/source_viewer_deprecated_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/split_button_spec.js5
-rw-r--r--spec/frontend/vue_shared/components/tooltip_on_truncate_spec.js6
-rw-r--r--spec/frontend/vue_shared/components/user_callout_dismisser_spec.js14
-rw-r--r--spec/frontend/vue_shared/components/user_deletion_obstacles/user_deletion_obstacles_list_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/user_popover/user_popover_spec.js11
-rw-r--r--spec/frontend/vue_shared/components/vuex_module_provider_spec.js11
-rw-r--r--spec/frontend/vue_shared/components/web_ide_link_spec.js4
-rw-r--r--spec/frontend/vue_shared/directives/track_event_spec.js61
-rw-r--r--spec/frontend/vue_shared/issuable/issuable_blocked_icon_spec.js12
-rw-r--r--spec/frontend/vue_shared/issuable/list/components/issuable_item_spec.js2
-rw-r--r--spec/frontend/vue_shared/issuable/list/components/issuable_list_root_spec.js4
-rw-r--r--spec/frontend/vue_shared/issuable/list/mock_data.js6
-rw-r--r--spec/frontend/vue_shared/issuable/show/components/issuable_body_spec.js143
-rw-r--r--spec/frontend/vue_shared/issuable/show/components/issuable_edit_form_spec.js4
-rw-r--r--spec/frontend/vue_shared/issuable/show/components/issuable_header_spec.js23
-rw-r--r--spec/frontend/vue_shared/security_reports/components/manage_via_mr_spec.js36
-rw-r--r--spec/frontend/webhooks/components/push_events_spec.js2
-rw-r--r--spec/frontend/webhooks/components/test_dropdown_spec.js13
-rw-r--r--spec/frontend/whats_new/utils/notification_spec.js5
-rw-r--r--spec/frontend/work_items/components/item_title_spec.js2
-rw-r--r--spec/frontend/work_items/components/notes/work_item_add_note_spec.js45
-rw-r--r--spec/frontend/work_items/components/notes/work_item_comment_form_spec.js100
-rw-r--r--spec/frontend/work_items/components/notes/work_item_discussion_spec.js6
-rw-r--r--spec/frontend/work_items/components/notes/work_item_note_actions_spec.js84
-rw-r--r--spec/frontend/work_items/components/notes/work_item_note_spec.js117
-rw-r--r--spec/frontend/work_items/components/widget_wrapper_spec.js2
-rw-r--r--spec/frontend/work_items/components/work_item_actions_spec.js149
-rw-r--r--spec/frontend/work_items/components/work_item_assignees_spec.js2
-rw-r--r--spec/frontend/work_items/components/work_item_description_spec.js7
-rw-r--r--spec/frontend/work_items/components/work_item_detail_spec.js10
-rw-r--r--spec/frontend/work_items/components/work_item_links/work_item_link_child_metadata_spec.js19
-rw-r--r--spec/frontend/work_items/components/work_item_links/work_item_link_child_spec.js123
-rw-r--r--spec/frontend/work_items/components/work_item_links/work_item_links_menu_spec.js2
-rw-r--r--spec/frontend/work_items/components/work_item_links/work_item_links_spec.js2
-rw-r--r--spec/frontend/work_items/components/work_item_notes_spec.js14
-rw-r--r--spec/frontend/work_items/mock_data.js89
-rw-r--r--spec/frontend/work_items/pages/work_item_root_spec.js2
-rw-r--r--spec/frontend/work_items/router_spec.js2
662 files changed, 15923 insertions, 8244 deletions
diff --git a/spec/frontend/__helpers__/assert_props.js b/spec/frontend/__helpers__/assert_props.js
new file mode 100644
index 00000000000..3e372454bf5
--- /dev/null
+++ b/spec/frontend/__helpers__/assert_props.js
@@ -0,0 +1,24 @@
+import { mount } from '@vue/test-utils';
+import { ErrorWithStack } from 'jest-util';
+
+export function assertProps(Component, props, extraMountArgs = {}) {
+ const originalConsoleError = global.console.error;
+ global.console.error = function error(...args) {
+ throw new ErrorWithStack(
+ `Unexpected call of console.error() with:\n\n${args.join(', ')}`,
+ this.error,
+ );
+ };
+ const ComponentWithoutRenderFn = {
+ ...Component,
+ render() {
+ return '';
+ },
+ };
+
+ try {
+ mount(ComponentWithoutRenderFn, { propsData: props, ...extraMountArgs });
+ } finally {
+ global.console.error = originalConsoleError;
+ }
+}
diff --git a/spec/frontend/__helpers__/wait_for_text.js b/spec/frontend/__helpers__/wait_for_text.js
index 6bed8a90a98..991adc5d6c0 100644
--- a/spec/frontend/__helpers__/wait_for_text.js
+++ b/spec/frontend/__helpers__/wait_for_text.js
@@ -1,3 +1,3 @@
import { findByText } from '@testing-library/dom';
-export const waitForText = async (text, container = document) => findByText(container, text);
+export const waitForText = (text, container = document) => findByText(container, text);
diff --git a/spec/frontend/access_tokens/index_spec.js b/spec/frontend/access_tokens/index_spec.js
index 1157e44f41a..c1158e0d124 100644
--- a/spec/frontend/access_tokens/index_spec.js
+++ b/spec/frontend/access_tokens/index_spec.js
@@ -112,7 +112,7 @@ describe('access tokens', () => {
);
});
- it('mounts component and sets `inputAttrs` prop', async () => {
+ it('mounts component and sets `inputAttrs` prop', () => {
wrapper = createWrapper(initExpiresAtField());
const component = wrapper.findComponent(ExpiresAtField);
diff --git a/spec/frontend/add_context_commits_modal/components/__snapshots__/add_context_commits_modal_spec.js.snap b/spec/frontend/add_context_commits_modal/components/__snapshots__/add_context_commits_modal_spec.js.snap
index 2c2151bfb41..e379aba094c 100644
--- a/spec/frontend/add_context_commits_modal/components/__snapshots__/add_context_commits_modal_spec.js.snap
+++ b/spec/frontend/add_context_commits_modal/components/__snapshots__/add_context_commits_modal_spec.js.snap
@@ -6,8 +6,9 @@ exports[`AddContextCommitsModal renders modal with 2 tabs 1`] = `
body-class="add-review-item pt-0"
cancel-variant="light"
dismisslabel="Close"
- modalclass=""
+ modalclass="add-review-item-modal"
modalid="add-review-item"
+ nofocusonshow="true"
ok-disabled="true"
ok-title="Save changes"
scrollable="true"
@@ -27,9 +28,13 @@ exports[`AddContextCommitsModal renders modal with 2 tabs 1`] = `
<div
class="gl-mt-3"
>
- <gl-search-box-by-type-stub
+ <gl-filtered-search-stub
+ availabletokens="[object Object],[object Object],[object Object]"
+ class="flex-grow-1"
clearbuttontitle="Clear"
- placeholder="Search by commit title or SHA"
+ placeholder="Search or filter commits"
+ searchbuttonattributes="[object Object]"
+ searchinputattributes="[object Object]"
value=""
/>
diff --git a/spec/frontend/add_context_commits_modal/components/add_context_commits_modal_spec.js b/spec/frontend/add_context_commits_modal/components/add_context_commits_modal_spec.js
index 5e96da9af7e..27fe010c354 100644
--- a/spec/frontend/add_context_commits_modal/components/add_context_commits_modal_spec.js
+++ b/spec/frontend/add_context_commits_modal/components/add_context_commits_modal_spec.js
@@ -1,4 +1,4 @@
-import { GlModal, GlSearchBoxByType } from '@gitlab/ui';
+import { GlModal, GlFilteredSearch } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
@@ -49,7 +49,7 @@ describe('AddContextCommitsModal', () => {
};
const findModal = () => wrapper.findComponent(GlModal);
- const findSearch = () => wrapper.findComponent(GlSearchBoxByType);
+ const findSearch = () => wrapper.findComponent(GlFilteredSearch);
beforeEach(() => {
wrapper = createWrapper();
@@ -68,12 +68,29 @@ describe('AddContextCommitsModal', () => {
expect(findSearch().exists()).toBe(true);
});
- it('when user starts entering text in search box, it calls action "searchCommits" after waiting for 500s', () => {
- const searchText = 'abcd';
- findSearch().vm.$emit('input', searchText);
- expect(searchCommits).not.toHaveBeenCalled();
- jest.advanceTimersByTime(500);
- expect(searchCommits).toHaveBeenCalledWith(expect.anything(), searchText);
+ it('when user submits after entering filters in search box, then it calls action "searchCommits"', () => {
+ const search = [
+ 'abcd',
+ {
+ type: 'author',
+ value: { operator: '=', data: 'abhi' },
+ },
+ {
+ type: 'committed-before-date',
+ value: { operator: '=', data: '2022-10-31' },
+ },
+ {
+ type: 'committed-after-date',
+ value: { operator: '=', data: '2022-10-28' },
+ },
+ ];
+ findSearch().vm.$emit('submit', search);
+ expect(searchCommits).toHaveBeenCalledWith(expect.anything(), {
+ searchText: 'abcd',
+ author: 'abhi',
+ committed_before: '2022-10-31',
+ committed_after: '2022-10-28',
+ });
});
it('disabled ok button when no row is selected', () => {
diff --git a/spec/frontend/admin/abuse_reports/components/abuse_report_actions_spec.js b/spec/frontend/admin/abuse_reports/components/abuse_report_actions_spec.js
new file mode 100644
index 00000000000..e72d0c24d5e
--- /dev/null
+++ b/spec/frontend/admin/abuse_reports/components/abuse_report_actions_spec.js
@@ -0,0 +1,166 @@
+import { mount, shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
+import axios from 'axios';
+import MockAdapter from 'axios-mock-adapter';
+import { GlButton, GlModal } from '@gitlab/ui';
+import AbuseReportActions from '~/admin/abuse_reports/components/abuse_report_actions.vue';
+import { useMockLocationHelper } from 'helpers/mock_window_location_helper';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
+import { createAlert, VARIANT_SUCCESS } from '~/alert';
+import { sprintf } from '~/locale';
+import { ACTIONS_I18N } from '~/admin/abuse_reports/constants';
+import { mockAbuseReports } from '../mock_data';
+
+jest.mock('~/alert');
+
+describe('AbuseReportActions', () => {
+ let wrapper;
+
+ const findRemoveUserAndReportButton = () => wrapper.findAllComponents(GlButton).at(0);
+ const findBlockUserButton = () => wrapper.findAllComponents(GlButton).at(1);
+ const findRemoveReportButton = () => wrapper.findAllComponents(GlButton).at(2);
+ const findConfirmationModal = () => wrapper.findComponent(GlModal);
+
+ const report = mockAbuseReports[0];
+
+ const createComponent = ({ props, mountFn } = { props: {}, mountFn: mount }) => {
+ wrapper = mountFn(AbuseReportActions, {
+ propsData: {
+ report,
+ ...props,
+ },
+ });
+ };
+ const createShallowComponent = (props) => createComponent({ props, mountFn: shallowMount });
+
+ describe('default', () => {
+ beforeEach(() => {
+ createShallowComponent();
+ });
+
+ it('displays "Block user", "Remove user & report", and "Remove report" buttons', () => {
+ expect(findRemoveUserAndReportButton().text()).toBe(ACTIONS_I18N.removeUserAndReport);
+
+ const blockButton = findBlockUserButton();
+ expect(blockButton.text()).toBe(ACTIONS_I18N.blockUser);
+ expect(blockButton.attributes('disabled')).toBeUndefined();
+
+ expect(findRemoveReportButton().text()).toBe(ACTIONS_I18N.removeReport);
+ });
+
+ it('does not show the confirmation modal initially', () => {
+ expect(findConfirmationModal().props('visible')).toBe(false);
+ });
+ });
+
+ describe('block button when user is already blocked', () => {
+ it('is disabled and has the correct text', () => {
+ createShallowComponent({ report: { ...report, userBlocked: true } });
+
+ const button = findBlockUserButton();
+ expect(button.text()).toBe(ACTIONS_I18N.alreadyBlocked);
+ expect(button.attributes('disabled')).toBe('true');
+ });
+ });
+
+ describe('actions', () => {
+ let axiosMock;
+
+ useMockLocationHelper();
+
+ beforeEach(() => {
+ axiosMock = new MockAdapter(axios);
+
+ createComponent();
+ });
+
+ afterEach(() => {
+ axiosMock.restore();
+ createAlert.mockClear();
+ });
+
+ describe('on remove user and report', () => {
+ it('shows confirmation modal and reloads the page on success', async () => {
+ findRemoveUserAndReportButton().trigger('click');
+ await nextTick();
+
+ expect(findConfirmationModal().props()).toMatchObject({
+ visible: true,
+ title: sprintf(ACTIONS_I18N.removeUserAndReportConfirm, {
+ user: report.reportedUser.name,
+ }),
+ });
+
+ axiosMock.onDelete(report.removeUserAndReportPath).reply(HTTP_STATUS_OK);
+
+ findConfirmationModal().vm.$emit('primary');
+ await axios.waitForAll();
+
+ expect(window.location.reload).toHaveBeenCalled();
+ });
+ });
+
+ describe('on block user', () => {
+ beforeEach(async () => {
+ findBlockUserButton().trigger('click');
+ await nextTick();
+ });
+
+ it('shows confirmation modal', () => {
+ expect(findConfirmationModal().props()).toMatchObject({
+ visible: true,
+ title: ACTIONS_I18N.blockUserConfirm,
+ });
+ });
+
+ describe.each([
+ {
+ responseData: { notice: 'Notice' },
+ createAlertArgs: { message: 'Notice', variant: VARIANT_SUCCESS },
+ blockButtonText: ACTIONS_I18N.alreadyBlocked,
+ blockButtonDisabled: 'disabled',
+ },
+ {
+ responseData: { error: 'Error' },
+ createAlertArgs: { message: 'Error' },
+ blockButtonText: ACTIONS_I18N.blockUser,
+ blockButtonDisabled: undefined,
+ },
+ ])(
+ 'when reponse JSON is $responseData',
+ ({ responseData, createAlertArgs, blockButtonText, blockButtonDisabled }) => {
+ beforeEach(async () => {
+ axiosMock.onPut(report.blockUserPath).reply(HTTP_STATUS_OK, responseData);
+
+ findConfirmationModal().vm.$emit('primary');
+ await axios.waitForAll();
+ });
+
+ it('updates the block button correctly', () => {
+ const button = findBlockUserButton();
+ expect(button.text()).toBe(blockButtonText);
+ expect(button.attributes('disabled')).toBe(blockButtonDisabled);
+ });
+
+ it('displays the returned message', () => {
+ expect(createAlert).toHaveBeenCalledWith(createAlertArgs);
+ });
+ },
+ );
+ });
+
+ describe('on remove report', () => {
+ it('reloads the page on success', async () => {
+ axiosMock.onDelete(report.removeReportPath).reply(HTTP_STATUS_OK);
+
+ findRemoveReportButton().trigger('click');
+
+ expect(findConfirmationModal().props('visible')).toBe(false);
+
+ await axios.waitForAll();
+
+ expect(window.location.reload).toHaveBeenCalled();
+ });
+ });
+ });
+});
diff --git a/spec/frontend/admin/abuse_reports/components/abuse_report_details_spec.js b/spec/frontend/admin/abuse_reports/components/abuse_report_details_spec.js
new file mode 100644
index 00000000000..b89bbac0196
--- /dev/null
+++ b/spec/frontend/admin/abuse_reports/components/abuse_report_details_spec.js
@@ -0,0 +1,53 @@
+import { GlButton, GlCollapse } from '@gitlab/ui';
+import { nextTick } from 'vue';
+import { shallowMount } from '@vue/test-utils';
+import AbuseReportDetails from '~/admin/abuse_reports/components/abuse_report_details.vue';
+import { getTimeago } from '~/lib/utils/datetime_utility';
+import { mockAbuseReports } from '../mock_data';
+
+describe('AbuseReportDetails', () => {
+ let wrapper;
+ const report = mockAbuseReports[0];
+
+ const findToggleButton = () => wrapper.findComponent(GlButton);
+ const findCollapsible = () => wrapper.findComponent(GlCollapse);
+
+ const createComponent = () => {
+ wrapper = shallowMount(AbuseReportDetails, {
+ propsData: {
+ report,
+ },
+ });
+ };
+
+ describe('default', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders toggle button with the correct text', () => {
+ expect(findToggleButton().text()).toEqual('Show details');
+ });
+
+ it('renders collapsed GlCollapse containing the report details', () => {
+ const collapsible = findCollapsible();
+ expect(collapsible.attributes('visible')).toBeUndefined();
+
+ const userJoinedText = `User joined ${getTimeago().format(report.reportedUser.createdAt)}`;
+ expect(collapsible.text()).toMatch(userJoinedText);
+ expect(collapsible.text()).toMatch(report.message);
+ });
+ });
+
+ describe('when toggled', () => {
+ it('expands GlCollapse and updates toggle text', async () => {
+ createComponent();
+
+ findToggleButton().vm.$emit('click');
+ await nextTick();
+
+ expect(findToggleButton().text()).toEqual('Hide details');
+ expect(findCollapsible().attributes('visible')).toBe('true');
+ });
+ });
+});
diff --git a/spec/frontend/admin/abuse_reports/components/abuse_report_row_spec.js b/spec/frontend/admin/abuse_reports/components/abuse_report_row_spec.js
index d32fa25d238..9876ee70e5e 100644
--- a/spec/frontend/admin/abuse_reports/components/abuse_report_row_spec.js
+++ b/spec/frontend/admin/abuse_reports/components/abuse_report_row_spec.js
@@ -1,22 +1,31 @@
+import { GlSprintf, GlLink } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import setWindowLocation from 'helpers/set_window_location_helper';
+import AbuseReportDetails from '~/admin/abuse_reports/components/abuse_report_details.vue';
import AbuseReportRow from '~/admin/abuse_reports/components/abuse_report_row.vue';
+import AbuseReportActions from '~/admin/abuse_reports/components/abuse_report_actions.vue';
import ListItem from '~/vue_shared/components/registry/list_item.vue';
import { getTimeago } from '~/lib/utils/datetime_utility';
+import { SORT_UPDATED_AT } from '~/admin/abuse_reports/constants';
import { mockAbuseReports } from '../mock_data';
describe('AbuseReportRow', () => {
let wrapper;
const mockAbuseReport = mockAbuseReports[0];
+ const findLinks = () => wrapper.findAllComponents(GlLink);
+ const findAbuseReportActions = () => wrapper.findComponent(AbuseReportActions);
const findListItem = () => wrapper.findComponent(ListItem);
const findTitle = () => wrapper.findByTestId('title');
- const findUpdatedAt = () => wrapper.findByTestId('updated-at');
+ const findDisplayedDate = () => wrapper.findByTestId('abuse-report-date');
+ const findAbuseReportDetails = () => wrapper.findComponent(AbuseReportDetails);
const createComponent = () => {
wrapper = shallowMountExtended(AbuseReportRow, {
propsData: {
report: mockAbuseReport,
},
+ stubs: { GlSprintf },
});
};
@@ -29,15 +38,49 @@ describe('AbuseReportRow', () => {
});
it('displays correctly formatted title', () => {
- const { reporter, reportedUser, category } = mockAbuseReport;
+ const { reporter, reportedUser, category, reportedUserPath, reporterPath } = mockAbuseReport;
expect(findTitle().text()).toMatchInterpolatedText(
`${reportedUser.name} reported for ${category} by ${reporter.name}`,
);
+
+ const userLink = findLinks().at(0);
+ expect(userLink.text()).toEqual(reportedUser.name);
+ expect(userLink.attributes('href')).toEqual(reportedUserPath);
+
+ const reporterLink = findLinks().at(1);
+ expect(reporterLink.text()).toEqual(reporter.name);
+ expect(reporterLink.attributes('href')).toEqual(reporterPath);
});
- it('displays correctly formatted updated at', () => {
- expect(findUpdatedAt().text()).toMatchInterpolatedText(
- `Updated ${getTimeago().format(mockAbuseReport.updatedAt)}`,
- );
+ describe('displayed date', () => {
+ it('displays correctly formatted created at', () => {
+ expect(findDisplayedDate().text()).toMatchInterpolatedText(
+ `Created ${getTimeago().format(mockAbuseReport.createdAt)}`,
+ );
+ });
+
+ describe('when sorted by updated_at', () => {
+ it('displays correctly formatted updated at', () => {
+ setWindowLocation(`?sort=${SORT_UPDATED_AT.sortDirection.ascending}`);
+
+ createComponent();
+
+ expect(findDisplayedDate().text()).toMatchInterpolatedText(
+ `Updated ${getTimeago().format(mockAbuseReport.updatedAt)}`,
+ );
+ });
+ });
+ });
+
+ it('renders AbuseReportDetails', () => {
+ expect(findAbuseReportDetails().exists()).toBe(true);
+ expect(findAbuseReportDetails().props('report')).toEqual(mockAbuseReport);
+ });
+
+ it('renders AbuseReportRowActions with the correct props', () => {
+ const actions = findAbuseReportActions();
+
+ expect(actions.exists()).toBe(true);
+ expect(actions.props('report')).toMatchObject(mockAbuseReport);
});
});
diff --git a/spec/frontend/admin/abuse_reports/components/abuse_reports_filtered_search_bar_spec.js b/spec/frontend/admin/abuse_reports/components/abuse_reports_filtered_search_bar_spec.js
index 9efab8403a0..990503c453d 100644
--- a/spec/frontend/admin/abuse_reports/components/abuse_reports_filtered_search_bar_spec.js
+++ b/spec/frontend/admin/abuse_reports/components/abuse_reports_filtered_search_bar_spec.js
@@ -58,21 +58,28 @@ describe('AbuseReportsFilteredSearchBar', () => {
});
});
- it('sets status=open query when there is no initial status query', () => {
- createComponent();
+ it.each([undefined, 'invalid'])(
+ 'sets status=open query when initial status query is %s',
+ (status) => {
+ if (status) {
+ setWindowLocation(`?status=${status}`);
+ }
- expect(updateHistory).toHaveBeenCalledWith({
- url: 'https://localhost/?status=open',
- replace: true,
- });
+ createComponent();
- expect(findFilteredSearchBar().props('initialFilterValue')).toMatchObject([
- {
- type: FILTERED_SEARCH_TOKEN_STATUS.type,
- value: { data: 'open', operator: '=' },
- },
- ]);
- });
+ expect(updateHistory).toHaveBeenCalledWith({
+ url: 'https://localhost/?status=open',
+ replace: true,
+ });
+
+ expect(findFilteredSearchBar().props('initialFilterValue')).toMatchObject([
+ {
+ type: FILTERED_SEARCH_TOKEN_STATUS.type,
+ value: { data: 'open', operator: '=' },
+ },
+ ]);
+ },
+ );
it('parses and passes search param to `FilteredSearchBar` component as `initialFilterValue` prop', () => {
setWindowLocation('?status=closed&user=mr_abuser&reporter=ms_nitch');
diff --git a/spec/frontend/admin/abuse_reports/mock_data.js b/spec/frontend/admin/abuse_reports/mock_data.js
index 778f055eb82..90289757a74 100644
--- a/spec/frontend/admin/abuse_reports/mock_data.js
+++ b/spec/frontend/admin/abuse_reports/mock_data.js
@@ -1,14 +1,30 @@
export const mockAbuseReports = [
{
category: 'spam',
+ createdAt: '2018-10-03T05:46:38.977Z',
updatedAt: '2022-12-07T06:45:39.977Z',
reporter: { name: 'Ms. Admin' },
- reportedUser: { name: 'Mr. Abuser' },
+ reportedUser: { name: 'Mr. Abuser', createdAt: '2017-09-01T05:46:38.977Z' },
+ reportedUserPath: '/mr_abuser',
+ reporterPath: '/admin',
+ userBlocked: false,
+ blockUserPath: '/block/user/mr_abuser/path',
+ removeUserAndReportPath: '/remove/user/mr_abuser/and/report/path',
+ removeReportPath: '/remove/report/path',
+ message: 'message 1',
},
{
category: 'phishing',
+ createdAt: '2018-10-03T05:46:38.977Z',
updatedAt: '2022-12-07T06:45:39.977Z',
reporter: { name: 'Ms. Reporter' },
- reportedUser: { name: 'Mr. Phisher' },
+ reportedUser: { name: 'Mr. Phisher', createdAt: '2016-09-01T05:46:38.977Z' },
+ reportedUserPath: '/mr_phisher',
+ reporterPath: '/admin',
+ userBlocked: false,
+ blockUserPath: '/block/user/mr_phisher/path',
+ removeUserAndReportPath: '/remove/user/mr_phisher/and/report/path',
+ removeReportPath: '/remove/report/path',
+ message: 'message 2',
},
];
diff --git a/spec/frontend/admin/abuse_reports/utils_spec.js b/spec/frontend/admin/abuse_reports/utils_spec.js
index 17f0b9acb26..3908edd3fdd 100644
--- a/spec/frontend/admin/abuse_reports/utils_spec.js
+++ b/spec/frontend/admin/abuse_reports/utils_spec.js
@@ -1,5 +1,8 @@
-import { FILTERED_SEARCH_TOKEN_CATEGORY } from '~/admin/abuse_reports/constants';
-import { buildFilteredSearchCategoryToken } from '~/admin/abuse_reports/utils';
+import {
+ FILTERED_SEARCH_TOKEN_CATEGORY,
+ FILTERED_SEARCH_TOKEN_STATUS,
+} from '~/admin/abuse_reports/constants';
+import { buildFilteredSearchCategoryToken, isValidStatus } from '~/admin/abuse_reports/utils';
describe('buildFilteredSearchCategoryToken', () => {
it('adds correctly formatted options to FILTERED_SEARCH_TOKEN_CATEGORY', () => {
@@ -11,3 +14,18 @@ describe('buildFilteredSearchCategoryToken', () => {
});
});
});
+
+describe('isValidStatus', () => {
+ const validStatuses = FILTERED_SEARCH_TOKEN_STATUS.options.map((o) => o.value);
+
+ it.each(validStatuses)(
+ 'returns true when status is an option value of FILTERED_SEARCH_TOKEN_STATUS',
+ (status) => {
+ expect(isValidStatus(status)).toBe(true);
+ },
+ );
+
+ it('return false when status is not an option value of FILTERED_SEARCH_TOKEN_STATUS', () => {
+ expect(isValidStatus('invalid')).toBe(false);
+ });
+});
diff --git a/spec/frontend/admin/users/components/actions/actions_spec.js b/spec/frontend/admin/users/components/actions/actions_spec.js
index 4aeaa5356b4..a5e7c6ebe21 100644
--- a/spec/frontend/admin/users/components/actions/actions_spec.js
+++ b/spec/frontend/admin/users/components/actions/actions_spec.js
@@ -1,4 +1,4 @@
-import { GlDropdownItem } from '@gitlab/ui';
+import { GlDisclosureDropdownItem } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import Actions from '~/admin/users/components/actions';
import Delete from '~/admin/users/components/actions/delete.vue';
@@ -12,7 +12,7 @@ import { paths, userDeletionObstacles } from '../../mock_data';
describe('Action components', () => {
let wrapper;
- const findDropdownItem = () => wrapper.findComponent(GlDropdownItem);
+ const findDisclosureDropdownItem = () => wrapper.findComponent(GlDisclosureDropdownItem);
const initComponent = ({ component, props } = {}) => {
wrapper = shallowMount(component, {
@@ -32,7 +32,7 @@ describe('Action components', () => {
},
});
- expect(findDropdownItem().exists()).toBe(true);
+ expect(findDisclosureDropdownItem().exists()).toBe(true);
});
});
@@ -52,7 +52,7 @@ describe('Action components', () => {
},
});
- await findDropdownItem().vm.$emit('click');
+ await findDisclosureDropdownItem().vm.$emit('action');
expect(eventHub.$emit).toHaveBeenCalledWith(
EVENT_OPEN_DELETE_USER_MODAL,
diff --git a/spec/frontend/admin/users/components/actions/delete_with_contributions_spec.js b/spec/frontend/admin/users/components/actions/delete_with_contributions_spec.js
index 64a88aab2c2..606a5c779fb 100644
--- a/spec/frontend/admin/users/components/actions/delete_with_contributions_spec.js
+++ b/spec/frontend/admin/users/components/actions/delete_with_contributions_spec.js
@@ -1,5 +1,5 @@
import { GlLoadingIcon } from '@gitlab/ui';
-import { mountExtended } from 'helpers/vue_test_utils_helper';
+import { mount } from '@vue/test-utils';
import waitForPromises from 'helpers/wait_for_promises';
import DeleteWithContributions from '~/admin/users/components/actions/delete_with_contributions.vue';
import eventHub, {
@@ -35,7 +35,7 @@ describe('DeleteWithContributions', () => {
};
const createComponent = () => {
- wrapper = mountExtended(DeleteWithContributions, { propsData: defaultPropsData });
+ wrapper = mount(DeleteWithContributions, { propsData: defaultPropsData });
};
describe('when action is clicked', () => {
@@ -47,10 +47,10 @@ describe('DeleteWithContributions', () => {
});
it('displays loading icon and disables button', async () => {
- await wrapper.trigger('click');
+ await wrapper.find('button').trigger('click');
expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
- expect(wrapper.findByRole('menuitem').attributes()).toMatchObject({
+ expect(wrapper.attributes()).toMatchObject({
disabled: 'disabled',
'aria-busy': 'true',
});
@@ -67,7 +67,7 @@ describe('DeleteWithContributions', () => {
});
it('emits event with association counts', async () => {
- await wrapper.trigger('click');
+ await wrapper.find('button').trigger('click');
await waitForPromises();
expect(associationsCount).toHaveBeenCalledWith(defaultPropsData.userId);
@@ -92,7 +92,7 @@ describe('DeleteWithContributions', () => {
});
it('emits event with error', async () => {
- await wrapper.trigger('click');
+ await wrapper.find('button').trigger('click');
await waitForPromises();
expect(eventHub.$emit).toHaveBeenCalledWith(
diff --git a/spec/frontend/admin/users/components/user_actions_spec.js b/spec/frontend/admin/users/components/user_actions_spec.js
index 1a2cc3e5c34..73d8c082bb9 100644
--- a/spec/frontend/admin/users/components/user_actions_spec.js
+++ b/spec/frontend/admin/users/components/user_actions_spec.js
@@ -1,4 +1,4 @@
-import { GlDropdownDivider } from '@gitlab/ui';
+import { GlDisclosureDropdownGroup } from '@gitlab/ui';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import Actions from '~/admin/users/components/actions';
@@ -19,7 +19,7 @@ describe('AdminUserActions component', () => {
const findEditButton = (id = user.id) => findUserActions(id).find('[data-testid="edit"]');
const findActionsDropdown = (id = user.id) =>
findUserActions(id).find('[data-testid="dropdown-toggle"]');
- const findDropdownDivider = () => wrapper.findComponent(GlDropdownDivider);
+ const findDisclosureGroup = () => wrapper.findComponent(GlDisclosureDropdownGroup);
const initComponent = ({ actions = [], showButtonLabels } = {}) => {
wrapper = shallowMountExtended(AdminUserActions, {
@@ -104,8 +104,8 @@ describe('AdminUserActions component', () => {
initComponent({ actions: [LDAP, ...DELETE_ACTIONS] });
});
- it('renders a dropdown divider', () => {
- expect(findDropdownDivider().exists()).toBe(true);
+ it('renders a disclosure group', () => {
+ expect(findDisclosureGroup().exists()).toBe(true);
});
it('only renders delete dropdown items for actions containing the word "delete"', () => {
@@ -126,8 +126,8 @@ describe('AdminUserActions component', () => {
});
describe('when there are no delete actions', () => {
- it('does not render a dropdown divider', () => {
- expect(findDropdownDivider().exists()).toBe(false);
+ it('does not render a disclosure group', () => {
+ expect(findDisclosureGroup().exists()).toBe(false);
});
it('does not render a delete dropdown item', () => {
diff --git a/spec/frontend/admin/users/new_spec.js b/spec/frontend/admin/users/new_spec.js
index 5e5763822a8..eba5c87f470 100644
--- a/spec/frontend/admin/users/new_spec.js
+++ b/spec/frontend/admin/users/new_spec.js
@@ -1,20 +1,19 @@
+import newWithInternalUserRegex from 'test_fixtures/admin/users/new_with_internal_user_regex.html';
import {
setupInternalUserRegexHandler,
ID_USER_EMAIL,
ID_USER_EXTERNAL,
ID_WARNING,
} from '~/admin/users/new';
-import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
describe('admin/users/new', () => {
- const FIXTURE = 'admin/users/new_with_internal_user_regex.html';
-
let elExternal;
let elUserEmail;
let elWarningMessage;
beforeEach(() => {
- loadHTMLFixture(FIXTURE);
+ setHTMLFixture(newWithInternalUserRegex);
setupInternalUserRegexHandler();
elExternal = document.getElementById(ID_USER_EXTERNAL);
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 4a60d605cae..202a0a04192 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
@@ -61,6 +61,7 @@ exports[`Alert integration settings form default state should match the default
items="[object Object]"
noresultstext="No results found"
placement="left"
+ popperoptions="[object Object]"
resetbuttonlabel=""
searchplaceholder="Search"
selected="selecte_tmpl"
diff --git a/spec/frontend/alerts_settings/components/alert_mapping_builder_spec.js b/spec/frontend/alerts_settings/components/alert_mapping_builder_spec.js
index 1e125bdfd3a..2b8479eab6d 100644
--- a/spec/frontend/alerts_settings/components/alert_mapping_builder_spec.js
+++ b/spec/frontend/alerts_settings/components/alert_mapping_builder_spec.js
@@ -49,7 +49,7 @@ describe('AlertMappingBuilder', () => {
const fallbackColumnIcon = findColumnInRow(0, 3).findComponent(GlIcon);
expect(fallbackColumnIcon.exists()).toBe(true);
- expect(fallbackColumnIcon.attributes('name')).toBe('question');
+ expect(fallbackColumnIcon.attributes('name')).toBe('question-o');
expect(fallbackColumnIcon.attributes('title')).toBe(i18n.fallbackTooltip);
});
diff --git a/spec/frontend/alerts_settings/components/alerts_settings_form_spec.js b/spec/frontend/alerts_settings/components/alerts_settings_form_spec.js
index e0075aa71d9..b8575d8ab26 100644
--- a/spec/frontend/alerts_settings/components/alerts_settings_form_spec.js
+++ b/spec/frontend/alerts_settings/components/alerts_settings_form_spec.js
@@ -97,7 +97,7 @@ describe('AlertsSettingsForm', () => {
expect(findFormFields().at(0).isVisible()).toBe(true);
});
- it('disables the dropdown and shows help text when multi integrations are not supported', async () => {
+ it('disables the dropdown and shows help text when multi integrations are not supported', () => {
createComponent({ props: { canAddIntegration: false } });
expect(findSelect().attributes('disabled')).toBe('disabled');
expect(findMultiSupportText().exists()).toBe(true);
diff --git a/spec/frontend/alerts_settings/components/alerts_settings_wrapper_spec.js b/spec/frontend/alerts_settings/components/alerts_settings_wrapper_spec.js
index 67d8619f157..8c5df06042c 100644
--- a/spec/frontend/alerts_settings/components/alerts_settings_wrapper_spec.js
+++ b/spec/frontend/alerts_settings/components/alerts_settings_wrapper_spec.js
@@ -429,7 +429,7 @@ describe('AlertsSettingsWrapper', () => {
});
describe('Test alert', () => {
- it('makes `updateTestAlert` service call', async () => {
+ it('makes `updateTestAlert` service call', () => {
jest.spyOn(alertsUpdateService, 'updateTestAlert').mockResolvedValueOnce();
const testPayload = '{"title":"test"}';
findAlertsSettingsForm().vm.$emit('test-alert-payload', testPayload);
diff --git a/spec/frontend/analytics/cycle_analytics/base_spec.js b/spec/frontend/analytics/cycle_analytics/base_spec.js
index 033916eabcd..1a1d22626ea 100644
--- a/spec/frontend/analytics/cycle_analytics/base_spec.js
+++ b/spec/frontend/analytics/cycle_analytics/base_spec.js
@@ -137,6 +137,7 @@ describe('Value stream analytics component', () => {
it('passes the paths to the filter bar', () => {
expect(findFilters().props()).toEqual({
groupPath,
+ namespacePath: groupPath,
endDate: createdBefore,
hasDateRangeFilter: true,
hasProjectFilter: false,
diff --git a/spec/frontend/analytics/cycle_analytics/filter_bar_spec.js b/spec/frontend/analytics/cycle_analytics/filter_bar_spec.js
index da7824adbf9..f1b3af39199 100644
--- a/spec/frontend/analytics/cycle_analytics/filter_bar_spec.js
+++ b/spec/frontend/analytics/cycle_analytics/filter_bar_spec.js
@@ -85,7 +85,7 @@ describe('Filter bar', () => {
return shallowMount(FilterBar, {
store: initialStore,
propsData: {
- groupPath: 'foo',
+ namespacePath: 'foo',
},
stubs: {
UrlSync,
diff --git a/spec/frontend/analytics/cycle_analytics/mock_data.js b/spec/frontend/analytics/cycle_analytics/mock_data.js
index 216e07844b8..f9587bf1967 100644
--- a/spec/frontend/analytics/cycle_analytics/mock_data.js
+++ b/spec/frontend/analytics/cycle_analytics/mock_data.js
@@ -214,7 +214,7 @@ export const group = {
id: 1,
name: 'foo',
path: 'foo',
- full_path: 'foo',
+ full_path: 'groups/foo',
avatar_url: `${TEST_HOST}/images/home/nasa.svg`,
};
diff --git a/spec/frontend/analytics/cycle_analytics/utils_spec.js b/spec/frontend/analytics/cycle_analytics/utils_spec.js
index e6d17edcadc..ab5d78bde51 100644
--- a/spec/frontend/analytics/cycle_analytics/utils_spec.js
+++ b/spec/frontend/analytics/cycle_analytics/utils_spec.js
@@ -91,7 +91,7 @@ describe('Value stream analytics utils', () => {
const projectId = '5';
const createdAfter = '2021-09-01';
const createdBefore = '2021-11-06';
- const groupPath = 'fake-group';
+ const groupPath = 'groups/fake-group';
const namespaceName = 'Fake project';
const namespaceFullPath = 'fake-group/fake-project';
const labelsPath = '/fake-group/fake-project/-/labels.json';
@@ -130,7 +130,7 @@ describe('Value stream analytics utils', () => {
});
it('sets the endpoints', () => {
- expect(res.groupPath).toBe(`groups/${groupPath}`);
+ expect(res.groupPath).toBe(groupPath);
});
it('returns null when there is no stage', () => {
@@ -158,10 +158,13 @@ describe('Value stream analytics utils', () => {
describe('with features set', () => {
const fakeFeatures = { cycleAnalyticsForGroups: true };
+ beforeEach(() => {
+ window.gon = { licensed_features: fakeFeatures };
+ });
+
it('sets the feature flags', () => {
res = buildCycleAnalyticsInitialData({
...rawData,
- gon: { licensed_features: fakeFeatures },
});
expect(res.features).toMatchObject(fakeFeatures);
});
diff --git a/spec/frontend/analytics/cycle_analytics/value_stream_filters_spec.js b/spec/frontend/analytics/cycle_analytics/value_stream_filters_spec.js
index 160f6ce0563..c6915c9054c 100644
--- a/spec/frontend/analytics/cycle_analytics/value_stream_filters_spec.js
+++ b/spec/frontend/analytics/cycle_analytics/value_stream_filters_spec.js
@@ -10,12 +10,15 @@ import {
selectedProjects,
} from './mock_data';
+const { path } = currentGroup;
+const groupPath = `groups/${path}`;
+
function createComponent(props = {}) {
return shallowMount(ValueStreamFilters, {
propsData: {
selectedProjects,
- groupId: currentGroup.id,
- groupPath: currentGroup.fullPath,
+ groupPath,
+ namespacePath: currentGroup.fullPath,
startDate,
endDate,
...props,
diff --git a/spec/frontend/analytics/shared/components/projects_dropdown_filter_spec.js b/spec/frontend/analytics/shared/components/projects_dropdown_filter_spec.js
index d2cbe0d39e4..33801fb8552 100644
--- a/spec/frontend/analytics/shared/components/projects_dropdown_filter_spec.js
+++ b/spec/frontend/analytics/shared/components/projects_dropdown_filter_spec.js
@@ -1,4 +1,4 @@
-import { GlDropdown, GlDropdownItem, GlTruncate } from '@gitlab/ui';
+import { GlDropdown, GlDropdownItem, GlTruncate, GlSearchBoxByType } from '@gitlab/ui';
import { nextTick } from 'vue';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import { stubComponent } from 'helpers/stub_component';
@@ -31,6 +31,7 @@ const projects = [
const MockGlDropdown = stubComponent(GlDropdown, {
template: `
<div>
+ <slot name="header"></slot>
<div data-testid="vsa-highlighted-items">
<slot name="highlighted-items"></slot>
</div>
@@ -112,6 +113,8 @@ describe('ProjectsDropdownFilter component', () => {
const selectedIds = () => wrapper.vm.selectedProjects.map(({ id }) => id);
+ const findSearchBoxByType = () => wrapper.findComponent(GlSearchBoxByType);
+
describe('queryParams are applied when fetching data', () => {
beforeEach(() => {
createComponent({
@@ -123,9 +126,7 @@ describe('ProjectsDropdownFilter component', () => {
});
it('applies the correct queryParams when making an api call', 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({ searchTerm: 'gitlab' });
+ findSearchBoxByType().vm.$emit('input', 'gitlab');
expect(spyQuery).toHaveBeenCalledTimes(1);
@@ -144,6 +145,7 @@ describe('ProjectsDropdownFilter component', () => {
describe('highlighted items', () => {
const blockDefaultProps = { multiSelect: true };
+
beforeEach(() => {
createComponent(blockDefaultProps);
});
@@ -151,6 +153,7 @@ describe('ProjectsDropdownFilter component', () => {
describe('with no project selected', () => {
it('does not render the highlighted items', async () => {
await createWithMockDropdown(blockDefaultProps);
+
expect(findSelectedDropdownItems().length).toBe(0);
});
@@ -188,8 +191,7 @@ describe('ProjectsDropdownFilter component', () => {
expect(findSelectedProjectsLabel().text()).toBe('2 projects selected');
- findClearAllButton().trigger('click');
- await nextTick();
+ await findClearAllButton().trigger('click');
expect(findSelectedProjectsLabel().text()).toBe('Select projects');
});
@@ -201,16 +203,14 @@ describe('ProjectsDropdownFilter component', () => {
await createWithMockDropdown({ multiSelect: true });
selectDropdownItemAtIndex(0);
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({ searchTerm: 'this is a very long search string' });
+ findSearchBoxByType().vm.$emit('input', 'this is a very long search string');
});
- it('renders the highlighted items', async () => {
+ it('renders the highlighted items', () => {
expect(findUnhighlightedItems().findAll('li').length).toBe(1);
});
- it('hides the unhighlighted items that do not match the string', async () => {
+ it('hides the unhighlighted items that do not match the string', () => {
expect(findUnhighlightedItems().findAll('li').length).toBe(1);
expect(findUnhighlightedItems().text()).toContain('No matching results');
});
@@ -351,17 +351,19 @@ describe('ProjectsDropdownFilter component', () => {
it('should remove from selection when clicked again', () => {
selectDropdownItemAtIndex(0);
+
expect(selectedIds()).toEqual([projects[0].id]);
selectDropdownItemAtIndex(0);
+
expect(selectedIds()).toEqual([]);
});
it('renders the correct placeholder text when multiple projects are selected', async () => {
selectDropdownItemAtIndex(0);
selectDropdownItemAtIndex(1);
-
await nextTick();
+
expect(findDropdownButton().text()).toBe('2 projects selected');
});
});
diff --git a/spec/frontend/api/projects_api_spec.js b/spec/frontend/api/projects_api_spec.js
index 2de56fae0c2..4ceed885e6e 100644
--- a/spec/frontend/api/projects_api_spec.js
+++ b/spec/frontend/api/projects_api_spec.js
@@ -9,14 +9,11 @@ describe('~/api/projects_api.js', () => {
let mock;
const projectId = 1;
- const setfullPathProjectSearch = (value) => {
- window.gon.features.fullPathProjectSearch = value;
- };
beforeEach(() => {
mock = new MockAdapter(axios);
- window.gon = { api_version: 'v7', features: { fullPathProjectSearch: true } };
+ window.gon = { api_version: 'v7' };
});
afterEach(() => {
@@ -68,17 +65,18 @@ describe('~/api/projects_api.js', () => {
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';
+ describe('createProject', () => {
+ it('posts to the correct URL and returns the data', () => {
+ const body = { name: 'test project' };
+ const expectedUrl = '/api/v7/projects.json';
+ const expectedRes = { id: 999, name: 'test project' };
- mock.onGet(expectedUrl).reply(HTTP_STATUS_OK, { data: expectedProjects });
+ mock.onPost(expectedUrl, body).replyOnce(HTTP_STATUS_OK, { data: expectedRes });
- return projectsApi.getProjects(query, options).then(({ data }) => {
- expect(axios.get).toHaveBeenCalledWith(expectedUrl, expectedParams);
- expect(data.data).toEqual(expectedProjects);
+ return projectsApi.createProject(body).then(({ data }) => {
+ expect(data).toStrictEqual(expectedRes);
});
});
});
diff --git a/spec/frontend/api/user_api_spec.js b/spec/frontend/api/user_api_spec.js
index 6636d77a09b..a879c229581 100644
--- a/spec/frontend/api/user_api_spec.js
+++ b/spec/frontend/api/user_api_spec.js
@@ -1,6 +1,13 @@
import MockAdapter from 'axios-mock-adapter';
-import { followUser, unfollowUser, associationsCount, updateUserStatus } from '~/api/user_api';
+import projects from 'test_fixtures/api/users/projects/get.json';
+import {
+ followUser,
+ unfollowUser,
+ associationsCount,
+ updateUserStatus,
+ getUserProjects,
+} from '~/api/user_api';
import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import {
@@ -91,4 +98,18 @@ describe('~/api/user_api', () => {
expect(JSON.parse(axiosMock.history.patch[0].data)).toEqual(expectedData);
});
});
+
+ describe('getUserProjects', () => {
+ it('calls correct URL and returns expected response', async () => {
+ const expectedUrl = '/api/v4/users/1/projects';
+ const expectedResponse = { data: projects };
+
+ axiosMock.onGet(expectedUrl).replyOnce(HTTP_STATUS_OK, expectedResponse);
+
+ await expect(getUserProjects(1)).resolves.toEqual(
+ expect.objectContaining({ data: expectedResponse }),
+ );
+ expect(axiosMock.history.get[0].url).toBe(expectedUrl);
+ });
+ });
});
diff --git a/spec/frontend/artifacts/components/artifacts_bulk_delete_spec.js b/spec/frontend/artifacts/components/artifacts_bulk_delete_spec.js
deleted file mode 100644
index 876906b2c3c..00000000000
--- a/spec/frontend/artifacts/components/artifacts_bulk_delete_spec.js
+++ /dev/null
@@ -1,96 +0,0 @@
-import { GlSprintf, GlModal } from '@gitlab/ui';
-import Vue from 'vue';
-import VueApollo from 'vue-apollo';
-import mockGetJobArtifactsResponse from 'test_fixtures/graphql/artifacts/graphql/queries/get_job_artifacts.query.graphql.json';
-import createMockApollo from 'helpers/mock_apollo_helper';
-import { mountExtended } from 'helpers/vue_test_utils_helper';
-import waitForPromises from 'helpers/wait_for_promises';
-import ArtifactsBulkDelete from '~/artifacts/components/artifacts_bulk_delete.vue';
-import bulkDestroyArtifactsMutation from '~/artifacts/graphql/mutations/bulk_destroy_job_artifacts.mutation.graphql';
-
-Vue.use(VueApollo);
-
-describe('ArtifactsBulkDelete component', () => {
- let wrapper;
- let requestHandlers;
-
- const projectId = '123';
- const selectedArtifacts = [
- mockGetJobArtifactsResponse.data.project.jobs.nodes[0].artifacts.nodes[0].id,
- mockGetJobArtifactsResponse.data.project.jobs.nodes[0].artifacts.nodes[1].id,
- ];
-
- const findText = () => wrapper.findComponent(GlSprintf).text();
- const findDeleteButton = () => wrapper.findByTestId('bulk-delete-delete-button');
- const findClearButton = () => wrapper.findByTestId('bulk-delete-clear-button');
- const findModal = () => wrapper.findComponent(GlModal);
-
- const createComponent = ({
- handlers = {
- bulkDestroyArtifactsMutation: jest.fn(),
- },
- } = {}) => {
- requestHandlers = handlers;
- wrapper = mountExtended(ArtifactsBulkDelete, {
- apolloProvider: createMockApollo([
- [bulkDestroyArtifactsMutation, requestHandlers.bulkDestroyArtifactsMutation],
- ]),
- propsData: {
- selectedArtifacts,
- queryVariables: {},
- isLoading: false,
- isLastRow: false,
- },
- provide: { projectId },
- });
- };
-
- describe('selected artifacts box', () => {
- beforeEach(async () => {
- createComponent();
- await waitForPromises();
- });
-
- it('displays selected artifacts count', () => {
- expect(findText()).toContain(String(selectedArtifacts.length));
- });
-
- it('opens the confirmation modal when the delete button is clicked', async () => {
- expect(findModal().props('visible')).toBe(false);
-
- findDeleteButton().trigger('click');
- await waitForPromises();
-
- expect(findModal().props('visible')).toBe(true);
- });
-
- it('emits clearSelectedArtifacts event when the clear button is clicked', () => {
- findClearButton().trigger('click');
-
- expect(wrapper.emitted('clearSelectedArtifacts')).toBeDefined();
- });
- });
-
- describe('bulk delete confirmation modal', () => {
- beforeEach(async () => {
- createComponent();
- findDeleteButton().trigger('click');
- await waitForPromises();
- });
-
- it('calls the bulk delete mutation with the selected artifacts on confirm', () => {
- findModal().vm.$emit('primary');
-
- expect(requestHandlers.bulkDestroyArtifactsMutation).toHaveBeenCalledWith({
- projectId: `gid://gitlab/Project/${projectId}`,
- ids: selectedArtifacts,
- });
- });
-
- it('does not call the bulk delete mutation on cancel', () => {
- findModal().vm.$emit('cancel');
-
- expect(requestHandlers.bulkDestroyArtifactsMutation).not.toHaveBeenCalled();
- });
- });
-});
diff --git a/spec/frontend/artifacts_settings/components/keep_latest_artifact_checkbox_spec.js b/spec/frontend/artifacts_settings/components/keep_latest_artifact_checkbox_spec.js
index efdebe5f3b0..50ac7be9ae3 100644
--- a/spec/frontend/artifacts_settings/components/keep_latest_artifact_checkbox_spec.js
+++ b/spec/frontend/artifacts_settings/components/keep_latest_artifact_checkbox_spec.js
@@ -1,7 +1,8 @@
import { GlFormCheckbox, GlLink } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import Vue, { nextTick } from 'vue';
+import Vue from 'vue';
import VueApollo from 'vue-apollo';
+import waitForPromises from 'helpers/wait_for_promises';
import createMockApollo from 'helpers/mock_apollo_helper';
import UpdateKeepLatestArtifactProjectSetting from '~/artifacts_settings/graphql/mutations/update_keep_latest_artifact_project_setting.mutation.graphql';
import GetKeepLatestArtifactApplicationSetting from '~/artifacts_settings/graphql/queries/get_keep_latest_artifact_application_setting.query.graphql';
@@ -102,19 +103,16 @@ describe('Keep latest artifact checkbox', () => {
});
describe('when application keep latest artifact setting is enabled', () => {
- beforeEach(() => {
+ beforeEach(async () => {
createComponent();
+ await waitForPromises();
});
- it('sets correct setting value in checkbox with query result', async () => {
- await nextTick();
-
+ it('sets correct setting value in checkbox with query result', () => {
expect(wrapper.element).toMatchSnapshot();
});
- it('checkbox is enabled when application setting is enabled', async () => {
- await nextTick();
-
+ it('checkbox is enabled when application setting is enabled', () => {
expect(findCheckbox().attributes('disabled')).toBeUndefined();
});
});
diff --git a/spec/frontend/authentication/password/components/password_input_spec.js b/spec/frontend/authentication/password/components/password_input_spec.js
new file mode 100644
index 00000000000..623d986b36e
--- /dev/null
+++ b/spec/frontend/authentication/password/components/password_input_spec.js
@@ -0,0 +1,49 @@
+import { GlFormInput, GlButton } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import PasswordInput from '~/authentication/password/components/password_input.vue';
+import { SHOW_PASSWORD, HIDE_PASSWORD } from '~/authentication/password/constants';
+
+describe('PasswordInput', () => {
+ let wrapper;
+
+ const findPasswordInput = () => wrapper.findComponent(GlFormInput);
+ const findToggleButton = () => wrapper.findComponent(GlButton);
+
+ const createComponent = () => {
+ return shallowMount(PasswordInput, {
+ propsData: {
+ resourceName: 'new_user',
+ minimumPasswordLength: '8',
+ qaSelector: 'new_user_password_field',
+ },
+ });
+ };
+
+ beforeEach(() => {
+ wrapper = createComponent();
+ });
+
+ describe('when the show password button is clicked', () => {
+ beforeEach(() => {
+ findToggleButton().vm.$emit('click');
+ });
+
+ it('displays hide password button', () => {
+ expect(findPasswordInput().attributes('type')).toBe('text');
+ expect(findToggleButton().attributes('icon')).toBe('eye-slash');
+ expect(findToggleButton().attributes('aria-label')).toBe(HIDE_PASSWORD);
+ });
+
+ describe('when the hide password button is clicked', () => {
+ beforeEach(() => {
+ findToggleButton().vm.$emit('click');
+ });
+
+ it('displays show password button', () => {
+ expect(findPasswordInput().attributes('type')).toBe('password');
+ expect(findToggleButton().attributes('icon')).toBe('eye');
+ expect(findToggleButton().attributes('aria-label')).toBe(SHOW_PASSWORD);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/authentication/two_factor_auth/components/manage_two_factor_form_spec.js b/spec/frontend/authentication/two_factor_auth/components/manage_two_factor_form_spec.js
index 694c16a85c4..8ecef710e03 100644
--- a/spec/frontend/authentication/two_factor_auth/components/manage_two_factor_form_spec.js
+++ b/spec/frontend/authentication/two_factor_auth/components/manage_two_factor_form_spec.js
@@ -19,7 +19,6 @@ describe('ManageTwoFactorForm', () => {
wrapper = mountExtended(ManageTwoFactorForm, {
provide: {
...defaultProvide,
- webauthnEnabled: options?.webauthnEnabled ?? false,
isCurrentPasswordRequired: options?.currentPasswordRequired ?? true,
},
stubs: {
@@ -41,16 +40,6 @@ describe('ManageTwoFactorForm', () => {
const findRegenerateCodesButton = () => wrapper.findByTestId('test-2fa-regenerate-codes-button');
const findConfirmationModal = () => wrapper.findComponent(GlModal);
- const itShowsConfirmationModal = (confirmText) => {
- it('shows confirmation modal', async () => {
- await wrapper.findByLabelText('Current password').setValue('foo bar');
- await findDisableButton().trigger('click');
-
- expect(findConfirmationModal().props('visible')).toBe(true);
- expect(findConfirmationModal().html()).toContain(confirmText);
- });
- };
-
const itShowsValidationMessageIfCurrentPasswordFieldIsEmpty = (findButtonFunction) => {
it('shows validation message if `Current password` is empty', async () => {
await findButtonFunction().trigger('click');
@@ -91,16 +80,12 @@ describe('ManageTwoFactorForm', () => {
describe('when clicked', () => {
itShowsValidationMessageIfCurrentPasswordFieldIsEmpty(findDisableButton);
- itShowsConfirmationModal(i18n.confirm);
-
- describe('when webauthnEnabled', () => {
- beforeEach(() => {
- createComponent({
- webauthnEnabled: true,
- });
- });
+ it('shows confirmation modal', async () => {
+ await wrapper.findByLabelText('Current password').setValue('foo bar');
+ await findDisableButton().trigger('click');
- itShowsConfirmationModal(i18n.confirmWebAuthn);
+ expect(findConfirmationModal().props('visible')).toBe(true);
+ expect(findConfirmationModal().html()).toContain(i18n.confirmWebAuthn);
});
it('modifies the form action and method when submitted through the button', async () => {
diff --git a/spec/frontend/authentication/webauthn/components/registration_spec.js b/spec/frontend/authentication/webauthn/components/registration_spec.js
index 1221626db7d..e4ca1ac8c38 100644
--- a/spec/frontend/authentication/webauthn/components/registration_spec.js
+++ b/spec/frontend/authentication/webauthn/components/registration_spec.js
@@ -211,7 +211,7 @@ describe('Registration', () => {
});
describe(`when ${STATE_ERROR} state`, () => {
- it('shows an initial error message and a retry button', async () => {
+ it('shows an initial error message and a retry button', () => {
const myError = 'my error';
createComponent({ initialError: myError });
diff --git a/spec/frontend/batch_comments/components/review_bar_spec.js b/spec/frontend/batch_comments/components/review_bar_spec.js
index 923e86a7e64..ea4b015ea39 100644
--- a/spec/frontend/batch_comments/components/review_bar_spec.js
+++ b/spec/frontend/batch_comments/components/review_bar_spec.js
@@ -20,7 +20,7 @@ describe('Batch comments review bar component', () => {
document.body.className = '';
});
- it('adds review-bar-visible class to body when review bar is mounted', async () => {
+ it('adds review-bar-visible class to body when review bar is mounted', () => {
expect(document.body.classList.contains(REVIEW_BAR_VISIBLE_CLASS_NAME)).toBe(false);
createComponent();
@@ -28,7 +28,7 @@ describe('Batch comments review bar component', () => {
expect(document.body.classList.contains(REVIEW_BAR_VISIBLE_CLASS_NAME)).toBe(true);
});
- it('removes review-bar-visible class to body when review bar is destroyed', async () => {
+ it('removes review-bar-visible class to body when review bar is destroyed', () => {
createComponent();
wrapper.destroy();
diff --git a/spec/frontend/behaviors/gl_emoji_spec.js b/spec/frontend/behaviors/gl_emoji_spec.js
index 722327e94ba..995e4219ae3 100644
--- a/spec/frontend/behaviors/gl_emoji_spec.js
+++ b/spec/frontend/behaviors/gl_emoji_spec.js
@@ -51,13 +51,13 @@ describe('gl_emoji', () => {
'bomb emoji just with name attribute',
'<gl-emoji data-name="bomb"></gl-emoji>',
'<gl-emoji data-name="bomb" data-unicode-version="6.0" title="bomb">💣</gl-emoji>',
- `<gl-emoji data-name="bomb" data-unicode-version="6.0" title="bomb"><img class="emoji" title=":bomb:" alt=":bomb:" src="/-/emojis/${EMOJI_VERSION}/bomb.png" width="20" height="20" align="absmiddle"></gl-emoji>`,
+ `<gl-emoji data-name="bomb" data-unicode-version="6.0" title="bomb"><img class="emoji" title=":bomb:" alt=":bomb:" src="/-/emojis/${EMOJI_VERSION}/bomb.png" width="16" height="16" align="absmiddle"></gl-emoji>`,
],
[
'bomb emoji with name attribute and unicode version',
'<gl-emoji data-name="bomb" data-unicode-version="6.0">💣</gl-emoji>',
'<gl-emoji data-name="bomb" data-unicode-version="6.0">💣</gl-emoji>',
- `<gl-emoji data-name="bomb" data-unicode-version="6.0"><img class="emoji" title=":bomb:" alt=":bomb:" src="/-/emojis/${EMOJI_VERSION}/bomb.png" width="20" height="20" align="absmiddle"></gl-emoji>`,
+ `<gl-emoji data-name="bomb" data-unicode-version="6.0"><img class="emoji" title=":bomb:" alt=":bomb:" src="/-/emojis/${EMOJI_VERSION}/bomb.png" width="16" height="16" align="absmiddle"></gl-emoji>`,
],
[
'bomb emoji with sprite fallback',
@@ -69,19 +69,19 @@ describe('gl_emoji', () => {
'bomb emoji with image fallback',
'<gl-emoji data-fallback-src="/bomb.png" data-name="bomb"></gl-emoji>',
'<gl-emoji data-fallback-src="/bomb.png" data-name="bomb" data-unicode-version="6.0" title="bomb">💣</gl-emoji>',
- '<gl-emoji data-fallback-src="/bomb.png" data-name="bomb" data-unicode-version="6.0" title="bomb"><img class="emoji" title=":bomb:" alt=":bomb:" src="/bomb.png" width="20" height="20" align="absmiddle"></gl-emoji>',
+ '<gl-emoji data-fallback-src="/bomb.png" data-name="bomb" data-unicode-version="6.0" title="bomb"><img class="emoji" title=":bomb:" alt=":bomb:" src="/bomb.png" width="16" height="16" align="absmiddle"></gl-emoji>',
],
[
'invalid emoji',
'<gl-emoji data-name="invalid_emoji"></gl-emoji>',
'<gl-emoji data-name="grey_question" data-unicode-version="6.0" title="white question mark ornament">❔</gl-emoji>',
- `<gl-emoji data-name="grey_question" data-unicode-version="6.0" title="white question mark ornament"><img class="emoji" title=":grey_question:" alt=":grey_question:" src="/-/emojis/${EMOJI_VERSION}/grey_question.png" width="20" height="20" align="absmiddle"></gl-emoji>`,
+ `<gl-emoji data-name="grey_question" data-unicode-version="6.0" title="white question mark ornament"><img class="emoji" title=":grey_question:" alt=":grey_question:" src="/-/emojis/${EMOJI_VERSION}/grey_question.png" width="16" height="16" align="absmiddle"></gl-emoji>`,
],
[
'custom emoji with image fallback',
'<gl-emoji data-fallback-src="https://cultofthepartyparrot.com/parrots/hd/parrot.gif" data-name="party-parrot" data-unicode-version="custom"></gl-emoji>',
- '<gl-emoji data-fallback-src="https://cultofthepartyparrot.com/parrots/hd/parrot.gif" data-name="party-parrot" data-unicode-version="custom"><img class="emoji" title=":party-parrot:" alt=":party-parrot:" src="https://cultofthepartyparrot.com/parrots/hd/parrot.gif" width="20" height="20" align="absmiddle"></gl-emoji>',
- '<gl-emoji data-fallback-src="https://cultofthepartyparrot.com/parrots/hd/parrot.gif" data-name="party-parrot" data-unicode-version="custom"><img class="emoji" title=":party-parrot:" alt=":party-parrot:" src="https://cultofthepartyparrot.com/parrots/hd/parrot.gif" width="20" height="20" align="absmiddle"></gl-emoji>',
+ '<gl-emoji data-fallback-src="https://cultofthepartyparrot.com/parrots/hd/parrot.gif" data-name="party-parrot" data-unicode-version="custom"><img class="emoji" title=":party-parrot:" alt=":party-parrot:" src="https://cultofthepartyparrot.com/parrots/hd/parrot.gif" width="16" height="16" align="absmiddle"></gl-emoji>',
+ '<gl-emoji data-fallback-src="https://cultofthepartyparrot.com/parrots/hd/parrot.gif" data-name="party-parrot" data-unicode-version="custom"><img class="emoji" title=":party-parrot:" alt=":party-parrot:" src="https://cultofthepartyparrot.com/parrots/hd/parrot.gif" width="16" height="16" align="absmiddle"></gl-emoji>',
],
])('%s', (name, markup, withEmojiSupport, withoutEmojiSupport) => {
it(`renders correctly with emoji support`, async () => {
@@ -111,7 +111,7 @@ describe('gl_emoji', () => {
await waitForPromises();
expect(glEmojiElement.outerHTML).toBe(
- '<gl-emoji data-name="&quot;x=&quot;y&quot; onload=&quot;alert(document.location.href)&quot;" data-unicode-version="x"><img class="emoji" title=":&quot;x=&quot;y&quot; onload=&quot;alert(document.location.href)&quot;:" alt=":&quot;x=&quot;y&quot; onload=&quot;alert(document.location.href)&quot;:" src="/-/emojis/2/grey_question.png" width="20" height="20" align="absmiddle"></gl-emoji>',
+ '<gl-emoji data-name="&quot;x=&quot;y&quot; onload=&quot;alert(document.location.href)&quot;" data-unicode-version="x"><img class="emoji" title=":&quot;x=&quot;y&quot; onload=&quot;alert(document.location.href)&quot;:" alt=":&quot;x=&quot;y&quot; onload=&quot;alert(document.location.href)&quot;:" src="/-/emojis/2/grey_question.png" width="16" height="16" align="absmiddle"></gl-emoji>',
);
});
diff --git a/spec/frontend/behaviors/quick_submit_spec.js b/spec/frontend/behaviors/quick_submit_spec.js
index 317c671cd2b..81eeb3f153e 100644
--- a/spec/frontend/behaviors/quick_submit_spec.js
+++ b/spec/frontend/behaviors/quick_submit_spec.js
@@ -1,5 +1,6 @@
import $ from 'jquery';
-import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
+import htmlSnippetsShow from 'test_fixtures/snippets/show.html';
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import '~/behaviors/quick_submit';
describe('Quick Submit behavior', () => {
@@ -8,7 +9,7 @@ describe('Quick Submit behavior', () => {
const keydownEvent = (options = { keyCode: 13, metaKey: true }) => $.Event('keydown', options);
beforeEach(() => {
- loadHTMLFixture('snippets/show.html');
+ setHTMLFixture(htmlSnippetsShow);
testContext = {};
@@ -60,22 +61,15 @@ describe('Quick Submit behavior', () => {
expect(testContext.spies.submit).not.toHaveBeenCalled();
});
- it('disables input of type submit', () => {
- const submitButton = $('.js-quick-submit input[type=submit]');
- testContext.textarea.trigger(keydownEvent());
-
- expect(submitButton).toBeDisabled();
- });
-
- it('disables button of type submit', () => {
- const submitButton = $('.js-quick-submit input[type=submit]');
+ it('disables submit', () => {
+ const submitButton = $('.js-quick-submit [type=submit]');
testContext.textarea.trigger(keydownEvent());
expect(submitButton).toBeDisabled();
});
it('only clicks one submit', () => {
- const existingSubmit = $('.js-quick-submit input[type=submit]');
+ const existingSubmit = $('.js-quick-submit [type=submit]');
// Add an extra submit button
const newSubmit = $('<button type="submit">Submit it</button>');
newSubmit.insertAfter(testContext.textarea);
diff --git a/spec/frontend/behaviors/requires_input_spec.js b/spec/frontend/behaviors/requires_input_spec.js
index f2f68f17d1c..68fa980216a 100644
--- a/spec/frontend/behaviors/requires_input_spec.js
+++ b/spec/frontend/behaviors/requires_input_spec.js
@@ -1,12 +1,13 @@
import $ from 'jquery';
-import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
+import htmlNewBranch from 'test_fixtures/branches/new_branch.html';
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import '~/behaviors/requires_input';
describe('requiresInput', () => {
let submitButton;
beforeEach(() => {
- loadHTMLFixture('branches/new_branch.html');
+ setHTMLFixture(htmlNewBranch);
submitButton = $('button[type="submit"]');
});
diff --git a/spec/frontend/behaviors/shortcuts/shortcuts_issuable_spec.js b/spec/frontend/behaviors/shortcuts/shortcuts_issuable_spec.js
index e6e587ff44b..ae7f5416c0c 100644
--- a/spec/frontend/behaviors/shortcuts/shortcuts_issuable_spec.js
+++ b/spec/frontend/behaviors/shortcuts/shortcuts_issuable_spec.js
@@ -1,5 +1,6 @@
import $ from 'jquery';
-import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
+import htmlSnippetsShow from 'test_fixtures/snippets/show.html';
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import waitForPromises from 'helpers/wait_for_promises';
import initCopyAsGFM, { CopyAsGFM } from '~/behaviors/markdown/copy_as_gfm';
import ShortcutsIssuable from '~/behaviors/shortcuts/shortcuts_issuable';
@@ -11,8 +12,6 @@ jest.mock('~/lib/utils/common_utils', () => ({
}));
describe('ShortcutsIssuable', () => {
- const snippetShowFixtureName = 'snippets/show.html';
-
beforeAll(() => {
initCopyAsGFM();
@@ -24,7 +23,7 @@ describe('ShortcutsIssuable', () => {
const FORM_SELECTOR = '.js-main-target-form .js-vue-comment-form';
beforeEach(() => {
- loadHTMLFixture(snippetShowFixtureName);
+ setHTMLFixture(htmlSnippetsShow);
$('body').append(
`<div class="js-main-target-form">
<textarea class="js-vue-comment-form"></textarea>
diff --git a/spec/frontend/blob/components/blob_edit_header_spec.js b/spec/frontend/blob/components/blob_edit_header_spec.js
index 2b1bd1ac4ad..b0ce5f40d95 100644
--- a/spec/frontend/blob/components/blob_edit_header_spec.js
+++ b/spec/frontend/blob/components/blob_edit_header_spec.js
@@ -1,6 +1,5 @@
import { GlFormInput, GlButton } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import { nextTick } from 'vue';
import BlobEditHeader from '~/blob/components/blob_edit_header.vue';
describe('Blob Header Editing', () => {
@@ -15,8 +14,10 @@ describe('Blob Header Editing', () => {
},
});
};
+
const findDeleteButton = () =>
wrapper.findAllComponents(GlButton).wrappers.find((x) => x.text() === 'Delete file');
+ const findFormInput = () => wrapper.findComponent(GlFormInput);
beforeEach(() => {
createComponent();
@@ -28,7 +29,7 @@ describe('Blob Header Editing', () => {
});
it('contains a form input field', () => {
- expect(wrapper.findComponent(GlFormInput).exists()).toBe(true);
+ expect(findFormInput().exists()).toBe(true);
});
it('does not show delete button', () => {
@@ -37,19 +38,16 @@ describe('Blob Header Editing', () => {
});
describe('functionality', () => {
- it('emits input event when the blob name is changed', async () => {
- const inputComponent = wrapper.findComponent(GlFormInput);
+ it('emits input event when the blob name is changed', () => {
+ const inputComponent = findFormInput();
const newValue = 'bar.txt';
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({
- name: newValue,
- });
+ // update `name` with `newValue`
+ inputComponent.vm.$emit('input', newValue);
+ // trigger change event which emits input event on wrapper
inputComponent.vm.$emit('change');
- await nextTick();
- expect(wrapper.emitted().input[0]).toEqual([newValue]);
+ expect(wrapper.emitted().input).toEqual([[newValue]]);
});
});
diff --git a/spec/frontend/blob/components/blob_header_default_actions_spec.js b/spec/frontend/blob/components/blob_header_default_actions_spec.js
index e12021a48d2..6e001846bb6 100644
--- a/spec/frontend/blob/components/blob_header_default_actions_spec.js
+++ b/spec/frontend/blob/components/blob_header_default_actions_spec.js
@@ -45,7 +45,7 @@ describe('Blob Header Default Actions', () => {
it('exactly 3 buttons with predefined actions', () => {
expect(buttons.length).toBe(3);
[BTN_COPY_CONTENTS_TITLE, BTN_RAW_TITLE, BTN_DOWNLOAD_TITLE].forEach((title, i) => {
- expect(buttons.at(i).vm.$el.title).toBe(title);
+ expect(buttons.at(i).attributes('title')).toBe(title);
});
});
@@ -87,10 +87,9 @@ describe('Blob Header Default Actions', () => {
it('emits a copy event if overrideCopy is set to true', () => {
createComponent({ overrideCopy: true });
- jest.spyOn(wrapper.vm, '$emit');
findCopyButton().vm.$emit('click');
- expect(wrapper.vm.$emit).toHaveBeenCalledWith('copy');
+ expect(wrapper.emitted('copy')).toHaveLength(1);
});
});
diff --git a/spec/frontend/blob/components/mock_data.js b/spec/frontend/blob/components/mock_data.js
index b5803bf0cbc..6ecf5091591 100644
--- a/spec/frontend/blob/components/mock_data.js
+++ b/spec/frontend/blob/components/mock_data.js
@@ -47,11 +47,13 @@ export const BinaryBlob = {
};
export const RichBlobContentMock = {
+ __typename: 'Blob',
path: 'foo.md',
richData: '<h1>Rich</h1>',
};
export const SimpleBlobContentMock = {
+ __typename: 'Blob',
path: 'foo.js',
plainData: 'Plain',
};
diff --git a/spec/frontend/blob/file_template_selector_spec.js b/spec/frontend/blob/file_template_selector_spec.js
index 65444e86efd..123475f8d62 100644
--- a/spec/frontend/blob/file_template_selector_spec.js
+++ b/spec/frontend/blob/file_template_selector_spec.js
@@ -53,7 +53,7 @@ describe('FileTemplateSelector', () => {
expect(subject.wrapper.classList.contains('hidden')).toBe(false);
});
- it('sets the focus on the dropdown', async () => {
+ it('sets the focus on the dropdown', () => {
subject.show();
jest.spyOn(subject.dropdown, 'focus');
jest.runAllTimers();
diff --git a/spec/frontend/blob/sketch/index_spec.js b/spec/frontend/blob/sketch/index_spec.js
index 4b6cb79791c..64b6152a07d 100644
--- a/spec/frontend/blob/sketch/index_spec.js
+++ b/spec/frontend/blob/sketch/index_spec.js
@@ -1,10 +1,11 @@
import SketchLoader from '~/blob/sketch';
-import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import waitForPromises from 'helpers/wait_for_promises';
+import htmlSketchViewer from 'test_fixtures_static/sketch_viewer.html';
describe('Sketch viewer', () => {
beforeEach(() => {
- loadHTMLFixture('static/sketch_viewer.html');
+ setHTMLFixture(htmlSketchViewer);
});
afterEach(() => {
diff --git a/spec/frontend/boards/board_card_inner_spec.js b/spec/frontend/boards/board_card_inner_spec.js
index a612e863d46..a925f752f5e 100644
--- a/spec/frontend/boards/board_card_inner_spec.js
+++ b/spec/frontend/boards/board_card_inner_spec.js
@@ -168,7 +168,7 @@ describe('Board card component', () => {
});
describe('blocked', () => {
- it('renders blocked icon if issue is blocked', async () => {
+ it('renders blocked icon if issue is blocked', () => {
createWrapper({
props: {
item: {
@@ -487,7 +487,7 @@ describe('Board card component', () => {
});
describe('loading', () => {
- it('renders loading icon', async () => {
+ it('renders loading icon', () => {
createWrapper({
props: {
item: {
diff --git a/spec/frontend/boards/board_list_spec.js b/spec/frontend/boards/board_list_spec.js
index 9ec43c6e892..e0a110678b1 100644
--- a/spec/frontend/boards/board_list_spec.js
+++ b/spec/frontend/boards/board_list_spec.js
@@ -1,3 +1,4 @@
+import { GlIntersectionObserver } from '@gitlab/ui';
import Draggable from 'vuedraggable';
import { nextTick } from 'vue';
import { DraggableItemTypes, ListType } from 'ee_else_ce/boards/constants';
@@ -8,15 +9,18 @@ import BoardCard from '~/boards/components/board_card.vue';
import eventHub from '~/boards/eventhub';
import BoardCardMoveToPosition from '~/boards/components/board_card_move_to_position.vue';
-import { mockIssues } from './mock_data';
+import { mockIssues, mockList, mockIssuesMore } from './mock_data';
describe('Board list component', () => {
let wrapper;
const findByTestId = (testId) => wrapper.find(`[data-testid="${testId}"]`);
- const findIssueCountLoadingIcon = () => wrapper.find('[data-testid="count-loading-icon"]');
const findDraggable = () => wrapper.findComponent(Draggable);
const findMoveToPositionComponent = () => wrapper.findComponent(BoardCardMoveToPosition);
+ const findIntersectionObserver = () => wrapper.findComponent(GlIntersectionObserver);
+ const findBoardListCount = () => wrapper.find('.board-list-count');
+
+ const triggerInfiniteScroll = () => findIntersectionObserver().vm.$emit('appear');
const startDrag = (
params = {
@@ -61,41 +65,25 @@ describe('Board list component', () => {
expect(wrapper.find('.board-card').attributes('data-item-id')).toBe('1');
});
- it('shows new issue form', async () => {
- wrapper.vm.toggleForm();
-
- await nextTick();
- expect(wrapper.find('.board-new-issue-form').exists()).toBe(true);
- });
-
it('shows new issue form after eventhub event', async () => {
- eventHub.$emit(`toggle-issue-form-${wrapper.vm.list.id}`);
+ eventHub.$emit(`toggle-issue-form-${mockList.id}`);
await nextTick();
expect(wrapper.find('.board-new-issue-form').exists()).toBe(true);
});
- it('does not show new issue form for closed list', () => {
- wrapper.setProps({ list: { type: 'closed' } });
- wrapper.vm.toggleForm();
-
- expect(wrapper.find('.board-new-issue-form').exists()).toBe(false);
- });
-
- it('shows count list item', async () => {
- wrapper.vm.showCount = true;
-
- await nextTick();
- expect(wrapper.find('.board-list-count').exists()).toBe(true);
-
- expect(wrapper.find('.board-list-count').text()).toBe('Showing all issues');
- });
+ it('does not show new issue form for closed list', async () => {
+ wrapper = createComponent({
+ listProps: {
+ listType: ListType.closed,
+ },
+ });
+ await waitForPromises();
- it('sets data attribute with invalid id', async () => {
- wrapper.vm.showCount = true;
+ eventHub.$emit(`toggle-issue-form-${mockList.id}`);
await nextTick();
- expect(wrapper.find('.board-list-count').attributes('data-issue-id')).toBe('-1');
+ expect(wrapper.find('.board-new-issue-form').exists()).toBe(false);
});
it('renders the move to position icon', () => {
@@ -118,61 +106,41 @@ describe('Board list component', () => {
});
describe('load more issues', () => {
- const actions = {
- fetchItemsForList: jest.fn(),
- };
-
- it('does not load issues if already loading', () => {
- wrapper = createComponent({
- actions,
- state: { listsFlags: { 'gid://gitlab/List/1': { isLoadingMore: true } } },
+ describe('when loading is not in progress', () => {
+ beforeEach(() => {
+ wrapper = createComponent({
+ listProps: {
+ id: 'gid://gitlab/List/1',
+ },
+ componentProps: {
+ boardItems: mockIssuesMore,
+ },
+ actions: {
+ fetchItemsForList: jest.fn(),
+ },
+ state: { listsFlags: { 'gid://gitlab/List/1': { isLoadingMore: false } } },
+ });
});
- wrapper.vm.listRef.dispatchEvent(new Event('scroll'));
- expect(actions.fetchItemsForList).not.toHaveBeenCalled();
- });
-
- it('shows loading more spinner', async () => {
- wrapper = createComponent({
- state: { listsFlags: { 'gid://gitlab/List/1': { isLoadingMore: true } } },
- data: {
- showCount: true,
- },
+ it('has intersection observer when the number of board list items are more than 5', () => {
+ expect(findIntersectionObserver().exists()).toBe(true);
});
- await nextTick();
-
- expect(findIssueCountLoadingIcon().exists()).toBe(true);
- });
-
- it('shows how many more issues to load', async () => {
- wrapper = createComponent({
- data: {
- showCount: true,
- },
+ it('shows count when loaded more items and correct data attribute', async () => {
+ triggerInfiniteScroll();
+ await waitForPromises();
+ expect(findBoardListCount().exists()).toBe(true);
+ expect(findBoardListCount().attributes('data-issue-id')).toBe('-1');
});
-
- await nextTick();
- await waitForPromises();
- await nextTick();
- await nextTick();
-
- expect(wrapper.find('.board-list-count').text()).toBe('Showing 1 of 20 issues');
});
});
describe('max issue count warning', () => {
- beforeEach(() => {
- wrapper = createComponent({
- listProps: { issuesCount: 50 },
- });
- });
-
describe('when issue count exceeds max issue count', () => {
it('sets background to gl-bg-red-100', async () => {
- wrapper.setProps({ list: { issuesCount: 4, maxIssueCount: 3 } });
+ wrapper = createComponent({ listProps: { issuesCount: 4, maxIssueCount: 3 } });
- await nextTick();
+ await waitForPromises();
const block = wrapper.find('.gl-bg-red-100');
expect(block.exists()).toBe(true);
@@ -183,16 +151,18 @@ describe('Board list component', () => {
});
describe('when list issue count does NOT exceed list max issue count', () => {
- it('does not sets background to gl-bg-red-100', () => {
- wrapper.setProps({ list: { issuesCount: 2, maxIssueCount: 3 } });
+ it('does not sets background to gl-bg-red-100', async () => {
+ wrapper = createComponent({ list: { issuesCount: 2, maxIssueCount: 3 } });
+ await waitForPromises();
expect(wrapper.find('.gl-bg-red-100').exists()).toBe(false);
});
});
describe('when list max issue count is 0', () => {
- it('does not sets background to gl-bg-red-100', () => {
- wrapper.setProps({ list: { maxIssueCount: 0 } });
+ it('does not sets background to gl-bg-red-100', async () => {
+ wrapper = createComponent({ list: { maxIssueCount: 0 } });
+ await waitForPromises();
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 148e696b57b..77ba6cdc9c0 100644
--- a/spec/frontend/boards/components/board_app_spec.js
+++ b/spec/frontend/boards/components/board_app_spec.js
@@ -1,14 +1,20 @@
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 BoardApp from '~/boards/components/board_app.vue';
+import activeBoardItemQuery from 'ee_else_ce/boards/graphql/client/active_board_item.query.graphql';
+import { rawIssue } from '../mock_data';
describe('BoardApp', () => {
let wrapper;
let store;
+ const mockApollo = createMockApollo();
Vue.use(Vuex);
+ Vue.use(VueApollo);
const createStore = ({ mockGetters = {} } = {}) => {
store = new Vuex.Store({
@@ -23,12 +29,22 @@ describe('BoardApp', () => {
});
};
- const createComponent = () => {
+ const createComponent = ({ isApolloBoard = false, issue = rawIssue } = {}) => {
+ mockApollo.clients.defaultClient.cache.writeQuery({
+ query: activeBoardItemQuery,
+ data: {
+ activeBoardItem: issue,
+ },
+ });
+
wrapper = shallowMount(BoardApp, {
+ apolloProvider: mockApollo,
store,
provide: {
initialBoardId: 'gid://gitlab/Board/1',
initialFilterParams: {},
+ isIssueBoard: true,
+ isApolloBoard,
},
});
};
@@ -50,4 +66,22 @@ describe('BoardApp', () => {
expect(wrapper.classes()).not.toContain('is-compact');
});
+
+ describe('Apollo boards', () => {
+ beforeEach(async () => {
+ createComponent({ isApolloBoard: true });
+ await nextTick();
+ });
+
+ it('should have is-compact class when a card is selected', () => {
+ expect(wrapper.classes()).toContain('is-compact');
+ });
+
+ it('should not have is-compact class when no card is selected', async () => {
+ createComponent({ isApolloBoard: true, issue: {} });
+ await nextTick();
+
+ expect(wrapper.classes()).not.toContain('is-compact');
+ });
+ });
});
diff --git a/spec/frontend/boards/components/board_card_spec.js b/spec/frontend/boards/components/board_card_spec.js
index 46116bed4cf..897219303b5 100644
--- a/spec/frontend/boards/components/board_card_spec.js
+++ b/spec/frontend/boards/components/board_card_spec.js
@@ -1,8 +1,10 @@
import { GlLabel } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
+import VueApollo from 'vue-apollo';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import BoardCard from '~/boards/components/board_card.vue';
import BoardCardInner from '~/boards/components/board_card_inner.vue';
import { inactiveId } from '~/boards/constants';
@@ -14,6 +16,14 @@ describe('Board card', () => {
let mockActions;
Vue.use(Vuex);
+ Vue.use(VueApollo);
+
+ const mockSetActiveBoardItemResolver = jest.fn();
+ const mockApollo = createMockApollo([], {
+ Mutation: {
+ setActiveBoardItem: mockSetActiveBoardItemResolver,
+ },
+ });
const createStore = ({ initialState = {} } = {}) => {
mockActions = {
@@ -36,11 +46,11 @@ describe('Board card', () => {
const mountComponent = ({
propsData = {},
provide = {},
- mountFn = shallowMount,
stubs = { BoardCardInner },
item = mockIssue,
} = {}) => {
- wrapper = mountFn(BoardCard, {
+ wrapper = shallowMountExtended(BoardCard, {
+ apolloProvider: mockApollo,
stubs: {
...stubs,
BoardCardInner,
@@ -56,9 +66,9 @@ describe('Board card', () => {
groupId: null,
rootPath: '/',
scopedLabelsAvailable: false,
+ isIssueBoard: true,
isEpicBoard: false,
issuableType: 'issue',
- isProjectBoard: false,
isGroupBoard: true,
disabled: false,
isApolloBoard: false,
@@ -96,7 +106,7 @@ describe('Board card', () => {
});
});
- it('should not highlight the card by default', async () => {
+ it('should not highlight the card by default', () => {
createStore();
mountComponent();
@@ -104,7 +114,7 @@ describe('Board card', () => {
expect(wrapper.classes()).not.toContain('multi-select');
});
- it('should highlight the card with a correct style when selected', async () => {
+ it('should highlight the card with a correct style when selected', () => {
createStore({
initialState: {
activeId: mockIssue.id,
@@ -116,7 +126,7 @@ describe('Board card', () => {
expect(wrapper.classes()).not.toContain('multi-select');
});
- it('should highlight the card with a correct style when multi-selected', async () => {
+ it('should highlight the card with a correct style when multi-selected', () => {
createStore({
initialState: {
activeId: inactiveId,
@@ -218,4 +228,25 @@ describe('Board card', () => {
expect(wrapper.attributes('style')).toBeUndefined();
});
});
+
+ describe('Apollo boards', () => {
+ beforeEach(async () => {
+ createStore();
+ mountComponent({ provide: { isApolloBoard: true } });
+ await nextTick();
+ });
+
+ it('set active board item on client when clicking on card', async () => {
+ await selectCard();
+
+ expect(mockSetActiveBoardItemResolver).toHaveBeenCalledWith(
+ {},
+ {
+ boardItem: mockIssue,
+ },
+ expect.anything(),
+ expect.anything(),
+ );
+ });
+ });
});
diff --git a/spec/frontend/boards/components/board_column_spec.js b/spec/frontend/boards/components/board_column_spec.js
index 011665eee68..5717031be20 100644
--- a/spec/frontend/boards/components/board_column_spec.js
+++ b/spec/frontend/boards/components/board_column_spec.js
@@ -81,7 +81,7 @@ describe('Board Column Component', () => {
});
describe('on mount', () => {
- beforeEach(async () => {
+ beforeEach(() => {
initStore();
jest.spyOn(store, 'dispatch').mockImplementation();
});
diff --git a/spec/frontend/boards/components/board_content_sidebar_spec.js b/spec/frontend/boards/components/board_content_sidebar_spec.js
index 90376a4a553..9be2696de56 100644
--- a/spec/frontend/boards/components/board_content_sidebar_spec.js
+++ b/spec/frontend/boards/components/board_content_sidebar_spec.js
@@ -1,10 +1,15 @@
import { GlDrawer } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
import { MountingPortal } from 'portal-vue';
-import Vue from 'vue';
+import Vue, { nextTick } from 'vue';
+import VueApollo from 'vue-apollo';
import Vuex from 'vuex';
import SidebarDropdownWidget from 'ee_else_ce/sidebar/components/sidebar_dropdown_widget.vue';
+import createMockApollo from 'helpers/mock_apollo_helper';
import { stubComponent } from 'helpers/stub_component';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+
+import activeBoardItemQuery from 'ee_else_ce/boards/graphql/client/active_board_item.query.graphql';
import BoardContentSidebar from '~/boards/components/board_content_sidebar.vue';
import BoardSidebarTitle from '~/boards/components/sidebar/board_sidebar_title.vue';
import { ISSUABLE } from '~/boards/constants';
@@ -14,13 +19,21 @@ import SidebarSeverityWidget from '~/sidebar/components/severity/sidebar_severit
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';
-import { mockActiveIssue, mockIssue, mockIssueGroupPath, mockIssueProjectPath } from '../mock_data';
+import { mockActiveIssue, mockIssue, rawIssue } from '../mock_data';
Vue.use(Vuex);
+Vue.use(VueApollo);
describe('BoardContentSidebar', () => {
let wrapper;
let store;
+ const mockSetActiveBoardItemResolver = jest.fn();
+ const mockApollo = createMockApollo([], {
+ Mutation: {
+ setActiveBoardItem: mockSetActiveBoardItemResolver,
+ },
+ });
+
const createStore = ({ mockGetters = {}, mockActions = {} } = {}) => {
store = new Vuex.Store({
state: {
@@ -32,30 +45,29 @@ describe('BoardContentSidebar', () => {
activeBoardItem: () => {
return { ...mockActiveIssue, epic: null };
},
- groupPathForActiveIssue: () => mockIssueGroupPath,
- projectPathForActiveIssue: () => mockIssueProjectPath,
- isSidebarOpen: () => true,
...mockGetters,
},
actions: mockActions,
});
};
- const createComponent = () => {
- /*
- Dynamically imported components (in our case ee imports)
- aren't stubbed automatically in VTU v1:
- https://github.com/vuejs/vue-test-utils/issues/1279.
+ const createComponent = ({ isApolloBoard = false } = {}) => {
+ mockApollo.clients.defaultClient.cache.writeQuery({
+ query: activeBoardItemQuery,
+ data: {
+ activeBoardItem: rawIssue,
+ },
+ });
- This requires us to additionally mock apollo or vuex stores.
- */
- wrapper = shallowMount(BoardContentSidebar, {
+ wrapper = shallowMountExtended(BoardContentSidebar, {
+ apolloProvider: mockApollo,
provide: {
canUpdate: true,
rootPath: '/',
groupId: 1,
issuableType: TYPE_ISSUE,
isGroupBoard: false,
+ isApolloBoard,
},
store,
stubs: {
@@ -63,24 +75,6 @@ describe('BoardContentSidebar', () => {
template: '<div><slot name="header"></slot><slot></slot></div>',
}),
},
- mocks: {
- $apollo: {
- queries: {
- participants: {
- loading: false,
- },
- currentIteration: {
- loading: false,
- },
- iterations: {
- loading: false,
- },
- attributesList: {
- loading: false,
- },
- },
- },
- },
});
};
@@ -101,10 +95,12 @@ describe('BoardContentSidebar', () => {
});
});
- it('does not render GlDrawer when isSidebarOpen is false', () => {
- createStore({ mockGetters: { isSidebarOpen: () => false } });
+ it('does not render GlDrawer when no active item is set', async () => {
+ createStore({ mockGetters: { activeBoardItem: () => ({ id: '', iid: '' }) } });
createComponent();
+ await nextTick();
+
expect(wrapper.findComponent(GlDrawer).props('open')).toBe(false);
});
@@ -166,7 +162,7 @@ describe('BoardContentSidebar', () => {
createComponent();
});
- it('calls toggleBoardItem with correct parameters', async () => {
+ it('calls toggleBoardItem with correct parameters', () => {
wrapper.findComponent(GlDrawer).vm.$emit('close');
expect(toggleBoardItem).toHaveBeenCalledTimes(1);
@@ -189,4 +185,27 @@ describe('BoardContentSidebar', () => {
expect(wrapper.findComponent(SidebarSeverityWidget).exists()).toBe(true);
});
});
+
+ describe('Apollo boards', () => {
+ beforeEach(async () => {
+ createStore();
+ createComponent({ isApolloBoard: true });
+ await nextTick();
+ });
+
+ it('calls setActiveBoardItemMutation on close', async () => {
+ wrapper.findComponent(GlDrawer).vm.$emit('close');
+
+ await waitForPromises();
+
+ expect(mockSetActiveBoardItemResolver).toHaveBeenCalledWith(
+ {},
+ {
+ boardItem: null,
+ },
+ expect.anything(),
+ expect.anything(),
+ );
+ });
+ });
});
diff --git a/spec/frontend/boards/components/board_content_spec.js b/spec/frontend/boards/components/board_content_spec.js
index 33351bf8efd..ab51f477966 100644
--- a/spec/frontend/boards/components/board_content_spec.js
+++ b/spec/frontend/boards/components/board_content_spec.js
@@ -6,6 +6,7 @@ import Draggable from 'vuedraggable';
import Vuex from 'vuex';
import eventHub from '~/boards/eventhub';
+import { stubComponent } from 'helpers/stub_component';
import waitForPromises from 'helpers/wait_for_promises';
import createMockApollo from 'helpers/mock_apollo_helper';
import EpicsSwimlanes from 'ee_component/boards/components/epics_swimlanes.vue';
@@ -78,6 +79,11 @@ describe('BoardContent', () => {
isApolloBoard,
},
store,
+ stubs: {
+ BoardContentSidebar: stubComponent(BoardContentSidebar, {
+ template: '<div></div>',
+ }),
+ },
});
};
diff --git a/spec/frontend/boards/components/board_filtered_search_spec.js b/spec/frontend/boards/components/board_filtered_search_spec.js
index d8bc7f95f18..64111cfb01a 100644
--- a/spec/frontend/boards/components/board_filtered_search_spec.js
+++ b/spec/frontend/boards/components/board_filtered_search_spec.js
@@ -123,7 +123,7 @@ describe('BoardFilteredSearch', () => {
jest.spyOn(wrapper.vm, 'performSearch').mockImplementation();
});
- it('sets the url params to the correct results', async () => {
+ it('sets the url params to the correct results', () => {
const mockFilters = [
{ type: TOKEN_TYPE_AUTHOR, value: { data: 'root', operator: '=' } },
{ type: TOKEN_TYPE_ASSIGNEE, value: { data: 'root', operator: '=' } },
diff --git a/spec/frontend/boards/components/board_form_spec.js b/spec/frontend/boards/components/board_form_spec.js
index 62db59f8f57..f340dfab359 100644
--- a/spec/frontend/boards/components/board_form_spec.js
+++ b/spec/frontend/boards/components/board_form_spec.js
@@ -115,7 +115,7 @@ describe('BoardForm', () => {
expect(findForm().exists()).toBe(true);
});
- it('focuses an input field', async () => {
+ it('focuses an input field', () => {
expect(document.activeElement).toBe(wrapper.vm.$refs.name);
});
});
diff --git a/spec/frontend/boards/components/board_list_header_spec.js b/spec/frontend/boards/components/board_list_header_spec.js
index 466321cf1cc..0f91d2315cf 100644
--- a/spec/frontend/boards/components/board_list_header_spec.js
+++ b/spec/frontend/boards/components/board_list_header_spec.js
@@ -147,7 +147,7 @@ describe('Board List Header Component', () => {
});
describe('expanding / collapsing the column', () => {
- it('should display collapse icon when column is expanded', async () => {
+ it('should display collapse icon when column is expanded', () => {
createComponent();
const icon = findCaret();
@@ -155,7 +155,7 @@ describe('Board List Header Component', () => {
expect(icon.props('icon')).toBe('chevron-lg-down');
});
- it('should display expand icon when column is collapsed', async () => {
+ it('should display expand icon when column is collapsed', () => {
createComponent({ collapsed: true });
const icon = findCaret();
diff --git a/spec/frontend/boards/components/boards_selector_spec.js b/spec/frontend/boards/components/boards_selector_spec.js
index aa146eb4609..13c017706ef 100644
--- a/spec/frontend/boards/components/boards_selector_spec.js
+++ b/spec/frontend/boards/components/boards_selector_spec.js
@@ -250,7 +250,7 @@ describe('BoardsSelector', () => {
describe('dropdown visibility', () => {
describe('when multipleIssueBoardsAvailable is enabled', () => {
- it('show dropdown', async () => {
+ it('show dropdown', () => {
createStore();
createComponent({ provide: { multipleIssueBoardsAvailable: true } });
expect(findDropdown().exists()).toBe(true);
@@ -258,7 +258,7 @@ describe('BoardsSelector', () => {
});
describe('when multipleIssueBoardsAvailable is disabled but it hasMissingBoards', () => {
- it('show dropdown', async () => {
+ it('show dropdown', () => {
createStore();
createComponent({
provide: { multipleIssueBoardsAvailable: false, hasMissingBoards: true },
@@ -268,7 +268,7 @@ describe('BoardsSelector', () => {
});
describe("when multipleIssueBoardsAvailable is disabled and it dosn't hasMissingBoards", () => {
- it('hide dropdown', async () => {
+ it('hide dropdown', () => {
createStore();
createComponent({
provide: { multipleIssueBoardsAvailable: false, hasMissingBoards: 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 a20884baf3b..fae3b0c5d1a 100644
--- a/spec/frontend/boards/components/sidebar/board_sidebar_title_spec.js
+++ b/spec/frontend/boards/components/sidebar/board_sidebar_title_spec.js
@@ -1,9 +1,17 @@
import { GlAlert, GlFormInput, GlForm, GlLink } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import { nextTick } from 'vue';
+import Vue, { nextTick } from 'vue';
+import VueApollo from 'vue-apollo';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
import BoardEditableItem from '~/boards/components/sidebar/board_editable_item.vue';
import BoardSidebarTitle from '~/boards/components/sidebar/board_sidebar_title.vue';
import { createStore } from '~/boards/stores';
+import issueSetTitleMutation from '~/boards/graphql/issue_set_title.mutation.graphql';
+import updateEpicTitleMutation from '~/sidebar/queries/update_epic_title.mutation.graphql';
+import { updateIssueTitleResponse, updateEpicTitleResponse } from '../../mock_data';
+
+Vue.use(VueApollo);
const TEST_TITLE = 'New item title';
const TEST_ISSUE_A = {
@@ -21,24 +29,43 @@ const TEST_ISSUE_B = {
webUrl: 'webUrl',
};
-describe('~/boards/components/sidebar/board_sidebar_title.vue', () => {
+describe('BoardSidebarTitle', () => {
let wrapper;
let store;
+ let mockApollo;
+
+ const issueSetTitleMutationHandlerSuccess = jest.fn().mockResolvedValue(updateIssueTitleResponse);
+ const updateEpicTitleMutationHandlerSuccess = jest
+ .fn()
+ .mockResolvedValue(updateEpicTitleResponse);
afterEach(() => {
localStorage.clear();
store = null;
});
- const createWrapper = (item = TEST_ISSUE_A) => {
+ const createWrapper = ({ item = TEST_ISSUE_A, provide = {} } = {}) => {
store = createStore();
store.state.boardItems = { [item.id]: { ...item } };
store.dispatch('setActiveId', { id: item.id });
+ mockApollo = createMockApollo([
+ [issueSetTitleMutation, issueSetTitleMutationHandlerSuccess],
+ [updateEpicTitleMutation, updateEpicTitleMutationHandlerSuccess],
+ ]);
wrapper = shallowMount(BoardSidebarTitle, {
store,
+ apolloProvider: mockApollo,
provide: {
canUpdate: true,
+ fullPath: 'gitlab-org',
+ issuableType: 'issue',
+ isEpicBoard: false,
+ isApolloBoard: false,
+ ...provide,
+ },
+ propsData: {
+ activeItem: item,
},
stubs: {
'board-editable-item': BoardEditableItem,
@@ -86,7 +113,8 @@ describe('~/boards/components/sidebar/board_sidebar_title.vue', () => {
await nextTick();
});
- it('collapses sidebar and renders new title', () => {
+ it('collapses sidebar and renders new title', async () => {
+ await waitForPromises();
expect(findCollapsed().isVisible()).toBe(true);
expect(findTitle().text()).toContain(TEST_TITLE);
});
@@ -140,7 +168,7 @@ describe('~/boards/components/sidebar/board_sidebar_title.vue', () => {
createWrapper();
});
- it('sets title, expands item and shows alert', async () => {
+ it('sets title, expands item and shows alert', () => {
expect(wrapper.vm.title).toBe(TEST_TITLE);
expect(findCollapsed().isVisible()).toBe(false);
expect(findAlert().exists()).toBe(true);
@@ -149,7 +177,7 @@ describe('~/boards/components/sidebar/board_sidebar_title.vue', () => {
describe('when cancel button is clicked', () => {
beforeEach(async () => {
- createWrapper(TEST_ISSUE_B);
+ createWrapper({ item: TEST_ISSUE_B });
jest.spyOn(wrapper.vm, 'setActiveItemTitle').mockImplementation(() => {
store.state.boardItems[TEST_ISSUE_B.id].title = TEST_TITLE;
@@ -168,7 +196,7 @@ describe('~/boards/components/sidebar/board_sidebar_title.vue', () => {
describe('when the mutation fails', () => {
beforeEach(async () => {
- createWrapper(TEST_ISSUE_B);
+ createWrapper({ item: TEST_ISSUE_B });
jest.spyOn(wrapper.vm, 'setActiveItemTitle').mockImplementation(() => {
throw new Error(['failed mutation']);
@@ -185,4 +213,32 @@ describe('~/boards/components/sidebar/board_sidebar_title.vue', () => {
expect(wrapper.vm.setError).toHaveBeenCalled();
});
});
+
+ describe('Apollo boards', () => {
+ it.each`
+ issuableType | isEpicBoard | queryHandler | notCalledHandler
+ ${'issue'} | ${false} | ${issueSetTitleMutationHandlerSuccess} | ${updateEpicTitleMutationHandlerSuccess}
+ ${'epic'} | ${true} | ${updateEpicTitleMutationHandlerSuccess} | ${issueSetTitleMutationHandlerSuccess}
+ `(
+ 'updates $issuableType title',
+ async ({ issuableType, isEpicBoard, queryHandler, notCalledHandler }) => {
+ createWrapper({
+ provide: {
+ issuableType,
+ isEpicBoard,
+ isApolloBoard: true,
+ },
+ });
+
+ await nextTick();
+
+ findFormInput().vm.$emit('input', TEST_TITLE);
+ findForm().vm.$emit('submit', { preventDefault: () => {} });
+ await nextTick();
+
+ expect(queryHandler).toHaveBeenCalled();
+ expect(notCalledHandler).not.toHaveBeenCalled();
+ },
+ );
+ });
});
diff --git a/spec/frontend/boards/mock_data.js b/spec/frontend/boards/mock_data.js
index e5167120542..74733d1fd95 100644
--- a/spec/frontend/boards/mock_data.js
+++ b/spec/frontend/boards/mock_data.js
@@ -277,6 +277,9 @@ export const labels = [
},
];
+export const mockIssueFullPath = 'gitlab-org/test-subgroup/gitlab-test';
+export const mockEpicFullPath = 'gitlab-org/test-subgroup';
+
export const rawIssue = {
title: 'Issue 1',
id: 'gid://gitlab/Issue/436',
@@ -302,12 +305,24 @@ export const rawIssue = {
epic: {
id: 'gid://gitlab/Epic/41',
},
+ totalTimeSpent: 0,
+ humanTimeEstimate: null,
+ humanTotalTimeSpent: null,
+ emailsDisabled: false,
+ hidden: false,
+ webUrl: `${mockIssueFullPath}/-/issue/27`,
+ relativePosition: null,
+ severity: null,
+ milestone: null,
+ weight: null,
+ blocked: false,
+ blockedByCount: 0,
+ iteration: null,
+ healthStatus: null,
type: 'ISSUE',
+ __typename: 'Issue',
};
-export const mockIssueFullPath = 'gitlab-org/test-subgroup/gitlab-test';
-export const mockEpicFullPath = 'gitlab-org/test-subgroup';
-
export const mockIssue = {
id: 'gid://gitlab/Issue/436',
iid: '27',
@@ -329,7 +344,22 @@ export const mockIssue = {
epic: {
id: 'gid://gitlab/Epic/41',
},
+ totalTimeSpent: 0,
+ humanTimeEstimate: null,
+ humanTotalTimeSpent: null,
+ emailsDisabled: false,
+ hidden: false,
+ webUrl: `${mockIssueFullPath}/-/issue/27`,
+ relativePosition: null,
+ severity: null,
+ milestone: null,
+ weight: null,
+ blocked: false,
+ blockedByCount: 0,
+ iteration: null,
+ healthStatus: null,
type: 'ISSUE',
+ __typename: 'Issue',
};
export const mockEpic = {
@@ -425,7 +455,58 @@ export const mockIssue4 = {
epic: null,
};
+export const mockIssue5 = {
+ id: 'gid://gitlab/Issue/440',
+ iid: 40,
+ title: 'Issue 5',
+ referencePath: '#40',
+ dueDate: null,
+ timeEstimate: 0,
+ confidential: false,
+ path: '/gitlab-org/gitlab-test/-/issues/40',
+ assignees,
+ labels,
+ epic: null,
+};
+
+export const mockIssue6 = {
+ id: 'gid://gitlab/Issue/441',
+ iid: 41,
+ title: 'Issue 6',
+ referencePath: '#41',
+ dueDate: null,
+ timeEstimate: 0,
+ confidential: false,
+ path: '/gitlab-org/gitlab-test/-/issues/41',
+ assignees,
+ labels,
+ epic: null,
+};
+
+export const mockIssue7 = {
+ id: 'gid://gitlab/Issue/442',
+ iid: 42,
+ title: 'Issue 6',
+ referencePath: '#42',
+ dueDate: null,
+ timeEstimate: 0,
+ confidential: false,
+ path: '/gitlab-org/gitlab-test/-/issues/42',
+ assignees,
+ labels,
+ epic: null,
+};
+
export const mockIssues = [mockIssue, mockIssue2];
+export const mockIssuesMore = [
+ mockIssue,
+ mockIssue2,
+ mockIssue3,
+ mockIssue4,
+ mockIssue5,
+ mockIssue6,
+ mockIssue7,
+];
export const BoardsMockData = {
GET: {
@@ -925,4 +1006,26 @@ export const epicBoardListQueryResponse = (totalWeight = 5) => ({
},
});
+export const updateIssueTitleResponse = {
+ data: {
+ updateIssuableTitle: {
+ issue: {
+ id: 'gid://gitlab/Issue/436',
+ title: 'Issue 1 edit',
+ },
+ },
+ },
+};
+
+export const updateEpicTitleResponse = {
+ data: {
+ updateIssuableTitle: {
+ epic: {
+ id: 'gid://gitlab/Epic/426',
+ title: 'Epic 1 edit',
+ },
+ },
+ },
+};
+
export const DEFAULT_COLOR = '#1068bf';
diff --git a/spec/frontend/boards/stores/actions_spec.js b/spec/frontend/boards/stores/actions_spec.js
index f430062bb73..b8d3be28ca6 100644
--- a/spec/frontend/boards/stores/actions_spec.js
+++ b/spec/frontend/boards/stores/actions_spec.js
@@ -401,7 +401,7 @@ describe('fetchMilestones', () => {
},
);
- it('sets milestonesLoading to true', async () => {
+ it('sets milestonesLoading to true', () => {
jest.spyOn(gqlClient, 'query').mockResolvedValue(queryResponse);
const store = createStore();
diff --git a/spec/frontend/branches/components/delete_branch_modal_spec.js b/spec/frontend/branches/components/delete_branch_modal_spec.js
index dd5b7fca564..7851d86466f 100644
--- a/spec/frontend/branches/components/delete_branch_modal_spec.js
+++ b/spec/frontend/branches/components/delete_branch_modal_spec.js
@@ -7,6 +7,8 @@ import DeleteBranchModal from '~/branches/components/delete_branch_modal.vue';
import eventHub from '~/branches/event_hub';
let wrapper;
+let showMock;
+let hideMock;
const branchName = 'test_modal';
const defaultBranchName = 'default';
@@ -14,23 +16,20 @@ const deletePath = '/path/to/branch';
const merged = false;
const isProtectedBranch = false;
-const createComponent = (data = {}) => {
+const createComponent = () => {
+ showMock = jest.fn();
+ hideMock = jest.fn();
+
wrapper = extendedWrapper(
shallowMount(DeleteBranchModal, {
- data() {
- return {
- branchName,
- deletePath,
- defaultBranchName,
- merged,
- isProtectedBranch,
- ...data,
- };
- },
stubs: {
GlModal: stubComponent(GlModal, {
template:
'<div><slot name="modal-title"></slot><slot></slot><slot name="modal-footer"></slot></div>',
+ methods: {
+ show: showMock,
+ hide: hideMock,
+ },
}),
GlButton,
GlFormInput,
@@ -46,21 +45,36 @@ const findDeleteButton = () => wrapper.findByTestId('delete-branch-confirmation-
const findCancelButton = () => wrapper.findByTestId('delete-branch-cancel-button');
const findFormInput = () => wrapper.findComponent(GlFormInput);
const findForm = () => wrapper.find('form');
-const submitFormSpy = () => jest.spyOn(wrapper.vm.$refs.form, 'submit');
+const createSubmitFormSpy = () => jest.spyOn(findForm().element, 'submit');
+
+const emitOpenModal = (data = {}) =>
+ eventHub.$emit('openModal', {
+ isProtectedBranch,
+ branchName,
+ defaultBranchName,
+ deletePath,
+ merged,
+ ...data,
+ });
describe('Delete branch modal', () => {
const expectedUnmergedWarning =
"This branch hasn't been merged into default. To avoid data loss, consider merging this branch before deleting it.";
+ beforeEach(() => {
+ createComponent();
+
+ emitOpenModal();
+
+ showMock.mockClear();
+ hideMock.mockClear();
+ });
+
describe('Deleting a regular branch', () => {
const expectedTitle = 'Delete branch. Are you ABSOLUTELY SURE?';
const expectedWarning = "You're about to permanently delete the branch test_modal.";
const expectedMessage = `${expectedWarning} ${expectedUnmergedWarning}`;
- beforeEach(() => {
- createComponent();
- });
-
it('renders the modal correctly', () => {
expect(findModal().props('title')).toBe(expectedTitle);
expect(findModalMessage().text()).toMatchInterpolatedText(expectedMessage);
@@ -70,32 +84,30 @@ describe('Delete branch modal', () => {
});
it('submits the form when the delete button is clicked', () => {
+ const submitSpy = createSubmitFormSpy();
+
+ expect(submitSpy).not.toHaveBeenCalled();
+
findDeleteButton().trigger('click');
expect(findForm().attributes('action')).toBe(deletePath);
- expect(submitFormSpy()).toHaveBeenCalled();
+ expect(submitSpy).toHaveBeenCalled();
});
- it('calls show on the modal when a `openModal` event is received through the event hub', async () => {
- const showSpy = jest.spyOn(wrapper.vm.$refs.modal, 'show');
+ it('calls show on the modal when a `openModal` event is received through the event hub', () => {
+ expect(showMock).not.toHaveBeenCalled();
- eventHub.$emit('openModal', {
- isProtectedBranch,
- branchName,
- defaultBranchName,
- deletePath,
- merged,
- });
+ emitOpenModal();
- expect(showSpy).toHaveBeenCalled();
+ expect(showMock).toHaveBeenCalled();
});
it('calls hide on the modal when cancel button is clicked', () => {
- const closeModalSpy = jest.spyOn(wrapper.vm.$refs.modal, 'hide');
+ expect(hideMock).not.toHaveBeenCalled();
findCancelButton().trigger('click');
- expect(closeModalSpy).toHaveBeenCalled();
+ expect(hideMock).toHaveBeenCalled();
});
});
@@ -108,7 +120,9 @@ describe('Delete branch modal', () => {
'After you confirm and select Yes, delete protected branch, you cannot recover this branch. Please type the following to confirm: test_modal';
beforeEach(() => {
- createComponent({ isProtectedBranch: true });
+ emitOpenModal({
+ isProtectedBranch: true,
+ });
});
describe('rendering the modal correctly for a protected branch', () => {
@@ -138,8 +152,11 @@ describe('Delete branch modal', () => {
await waitForPromises();
+ const submitSpy = createSubmitFormSpy();
+
findDeleteButton().trigger('click');
- expect(submitFormSpy()).not.toHaveBeenCalled();
+
+ expect(submitSpy).not.toHaveBeenCalled();
});
it('opens with the delete button disabled and enables it when branch name is confirmed and fires submit', async () => {
@@ -151,16 +168,23 @@ describe('Delete branch modal', () => {
expect(findDeleteButton().props('disabled')).not.toBe(true);
+ const submitSpy = createSubmitFormSpy();
+
+ expect(submitSpy).not.toHaveBeenCalled();
+
findDeleteButton().trigger('click');
- expect(submitFormSpy()).toHaveBeenCalled();
+
+ expect(submitSpy).toHaveBeenCalled();
});
});
describe('Deleting a merged branch', () => {
- it('does not include the unmerged branch warning when merged is true', () => {
- createComponent({ merged: true });
+ beforeEach(() => {
+ emitOpenModal({ merged: true });
+ });
- expect(findModalMessage().html()).not.toContain(expectedUnmergedWarning);
+ it('does not include the unmerged branch warning when merged is true', () => {
+ expect(findModalMessage().text()).not.toContain(expectedUnmergedWarning);
});
});
});
diff --git a/spec/frontend/branches/components/delete_merged_branches_spec.js b/spec/frontend/branches/components/delete_merged_branches_spec.js
index 75a669c78f2..1e4d7082ccd 100644
--- a/spec/frontend/branches/components/delete_merged_branches_spec.js
+++ b/spec/frontend/branches/components/delete_merged_branches_spec.js
@@ -1,4 +1,4 @@
-import { GlButton, GlModal, GlFormInput, GlSprintf } from '@gitlab/ui';
+import { GlButton, GlFormInput, GlModal, GlSprintf } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { stubComponent } from 'helpers/stub_component';
@@ -10,11 +10,17 @@ import { formPath, propsDataMock } from '../mock_data';
jest.mock('~/lib/utils/csrf', () => ({ token: 'mock-csrf-token' }));
let wrapper;
+const modalShowSpy = jest.fn();
+const modalHideSpy = jest.fn();
const stubsData = {
GlModal: stubComponent(GlModal, {
template:
'<div><slot name="modal-title"></slot><slot></slot><slot name="modal-footer"></slot></div>',
+ methods: {
+ show: modalShowSpy,
+ hide: modalHideSpy,
+ },
}),
GlButton,
GlFormInput,
@@ -65,11 +71,10 @@ describe('Delete merged branches component', () => {
});
it('opens modal when clicked', () => {
- createComponent(mount);
- jest.spyOn(wrapper.vm.$refs.modal, 'show');
+ createComponent(mount, stubsData);
findDeleteButton().trigger('click');
- expect(wrapper.vm.$refs.modal.show).toHaveBeenCalled();
+ expect(modalShowSpy).toHaveBeenCalled();
});
});
@@ -131,9 +136,8 @@ describe('Delete merged branches component', () => {
});
it('calls hide on the modal when cancel button is clicked', () => {
- const closeModalSpy = jest.spyOn(wrapper.vm.$refs.modal, 'hide');
findCancelButton().trigger('click');
- expect(closeModalSpy).toHaveBeenCalled();
+ expect(modalHideSpy).toHaveBeenCalled();
});
});
});
diff --git a/spec/frontend/captcha/captcha_modal_spec.js b/spec/frontend/captcha/captcha_modal_spec.js
index 6d6d8043797..4bbed8ab3bb 100644
--- a/spec/frontend/captcha/captcha_modal_spec.js
+++ b/spec/frontend/captcha/captcha_modal_spec.js
@@ -61,12 +61,12 @@ describe('Captcha Modal', () => {
describe('functionality', () => {
describe('when modal is shown', () => {
describe('when initRecaptchaScript promise resolves successfully', () => {
- beforeEach(async () => {
+ beforeEach(() => {
createComponent({ props: { needsCaptchaResponse: true } });
findGlModal().vm.$emit('shown');
});
- it('shows modal', async () => {
+ it('shows modal', () => {
expect(showSpy).toHaveBeenCalled();
});
@@ -90,7 +90,7 @@ describe('Captcha Modal', () => {
expect(wrapper.emitted('receivedCaptchaResponse')).toEqual([[captchaResponse]]);
});
- it('hides modal with null trigger', async () => {
+ it('hides modal with null trigger', () => {
// Assert that hide is called with zero args, so that we don't trigger the logic
// for hiding the modal via cancel, esc, headerclose, etc, without a captcha response
expect(hideSpy).toHaveBeenCalledWith();
diff --git a/spec/frontend/captcha/init_recaptcha_script_spec.js b/spec/frontend/captcha/init_recaptcha_script_spec.js
index 78480821d95..3e2d7ba00ee 100644
--- a/spec/frontend/captcha/init_recaptcha_script_spec.js
+++ b/spec/frontend/captcha/init_recaptcha_script_spec.js
@@ -50,7 +50,7 @@ describe('initRecaptchaScript', () => {
await expect(result).resolves.toBe(window.grecaptcha);
});
- it('sets window[RECAPTCHA_ONLOAD_CALLBACK_NAME] to undefined', async () => {
+ it('sets window[RECAPTCHA_ONLOAD_CALLBACK_NAME] to undefined', () => {
expect(getScriptOnload()).toBeUndefined();
});
});
diff --git a/spec/frontend/artifacts/components/app_spec.js b/spec/frontend/ci/artifacts/components/app_spec.js
index 931c4703e95..435b03e82ab 100644
--- a/spec/frontend/artifacts/components/app_spec.js
+++ b/spec/frontend/ci/artifacts/components/app_spec.js
@@ -2,13 +2,13 @@ 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 ArtifactsApp from '~/ci/artifacts/components/app.vue';
+import JobArtifactsTable from '~/ci/artifacts/components/job_artifacts_table.vue';
+import getBuildArtifactsSizeQuery from '~/ci/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';
+import { PAGE_TITLE, TOTAL_ARTIFACTS_SIZE, SIZE_UNKNOWN } from '~/ci/artifacts/constants';
const TEST_BUILD_ARTIFACTS_SIZE = 1024;
const TEST_PROJECT_PATH = 'project/path';
diff --git a/spec/frontend/artifacts/components/artifact_row_spec.js b/spec/frontend/ci/artifacts/components/artifact_row_spec.js
index 268772ed4c0..f64d32410ed 100644
--- a/spec/frontend/artifacts/components/artifact_row_spec.js
+++ b/spec/frontend/ci/artifacts/components/artifact_row_spec.js
@@ -1,10 +1,10 @@
-import { GlBadge, GlButton, GlFriendlyWrap, GlFormCheckbox } from '@gitlab/ui';
-import mockGetJobArtifactsResponse from 'test_fixtures/graphql/artifacts/graphql/queries/get_job_artifacts.query.graphql.json';
+import { GlBadge, GlFriendlyWrap, GlFormCheckbox } from '@gitlab/ui';
+import mockGetJobArtifactsResponse from 'test_fixtures/graphql/ci/artifacts/graphql/queries/get_job_artifacts.query.graphql.json';
import { numberToHumanSize } from '~/lib/utils/number_utils';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import ArtifactRow from '~/artifacts/components/artifact_row.vue';
-import { BULK_DELETE_FEATURE_FLAG } from '~/artifacts/constants';
+import ArtifactRow from '~/ci/artifacts/components/artifact_row.vue';
+import { BULK_DELETE_FEATURE_FLAG } from '~/ci/artifacts/constants';
describe('ArtifactRow component', () => {
let wrapper;
@@ -27,7 +27,7 @@ describe('ArtifactRow component', () => {
isLastRow: false,
},
provide: { canDestroyArtifacts, glFeatures },
- stubs: { GlBadge, GlButton, GlFriendlyWrap },
+ stubs: { GlBadge, GlFriendlyWrap },
});
};
@@ -70,7 +70,7 @@ describe('ArtifactRow component', () => {
expect(wrapper.emitted('delete')).toBeUndefined();
- findDeleteButton().trigger('click');
+ findDeleteButton().vm.$emit('click');
await waitForPromises();
expect(wrapper.emitted('delete')).toBeDefined();
diff --git a/spec/frontend/ci/artifacts/components/artifacts_bulk_delete_spec.js b/spec/frontend/ci/artifacts/components/artifacts_bulk_delete_spec.js
new file mode 100644
index 00000000000..9e4fa6b9c6f
--- /dev/null
+++ b/spec/frontend/ci/artifacts/components/artifacts_bulk_delete_spec.js
@@ -0,0 +1,48 @@
+import { GlSprintf } from '@gitlab/ui';
+import mockGetJobArtifactsResponse from 'test_fixtures/graphql/ci/artifacts/graphql/queries/get_job_artifacts.query.graphql.json';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import ArtifactsBulkDelete from '~/ci/artifacts/components/artifacts_bulk_delete.vue';
+
+describe('ArtifactsBulkDelete component', () => {
+ let wrapper;
+
+ const selectedArtifacts = [
+ mockGetJobArtifactsResponse.data.project.jobs.nodes[0].artifacts.nodes[0].id,
+ mockGetJobArtifactsResponse.data.project.jobs.nodes[0].artifacts.nodes[1].id,
+ ];
+
+ const findText = () => wrapper.findComponent(GlSprintf).text();
+ const findDeleteButton = () => wrapper.findByTestId('bulk-delete-delete-button');
+ const findClearButton = () => wrapper.findByTestId('bulk-delete-clear-button');
+
+ const createComponent = () => {
+ wrapper = shallowMountExtended(ArtifactsBulkDelete, {
+ propsData: {
+ selectedArtifacts,
+ },
+ stubs: { GlSprintf },
+ });
+ };
+
+ describe('selected artifacts box', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('displays selected artifacts count', () => {
+ expect(findText()).toContain(String(selectedArtifacts.length));
+ });
+
+ it('emits showBulkDeleteModal event when the delete button is clicked', () => {
+ findDeleteButton().vm.$emit('click');
+
+ expect(wrapper.emitted('showBulkDeleteModal')).toBeDefined();
+ });
+
+ it('emits clearSelectedArtifacts event when the clear button is clicked', () => {
+ findClearButton().vm.$emit('click');
+
+ expect(wrapper.emitted('clearSelectedArtifacts')).toBeDefined();
+ });
+ });
+});
diff --git a/spec/frontend/artifacts/components/artifacts_table_row_details_spec.js b/spec/frontend/ci/artifacts/components/artifacts_table_row_details_spec.js
index 6bf3498f9b0..ebdb7e25c45 100644
--- a/spec/frontend/artifacts/components/artifacts_table_row_details_spec.js
+++ b/spec/frontend/ci/artifacts/components/artifacts_table_row_details_spec.js
@@ -1,15 +1,15 @@
import { GlModal } from '@gitlab/ui';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
-import getJobArtifactsResponse from 'test_fixtures/graphql/artifacts/graphql/queries/get_job_artifacts.query.graphql.json';
+import getJobArtifactsResponse from 'test_fixtures/graphql/ci/artifacts/graphql/queries/get_job_artifacts.query.graphql.json';
import waitForPromises from 'helpers/wait_for_promises';
-import ArtifactsTableRowDetails from '~/artifacts/components/artifacts_table_row_details.vue';
-import ArtifactRow from '~/artifacts/components/artifact_row.vue';
-import ArtifactDeleteModal from '~/artifacts/components/artifact_delete_modal.vue';
+import ArtifactsTableRowDetails from '~/ci/artifacts/components/artifacts_table_row_details.vue';
+import ArtifactRow from '~/ci/artifacts/components/artifact_row.vue';
+import ArtifactDeleteModal from '~/ci/artifacts/components/artifact_delete_modal.vue';
import createMockApollo from 'helpers/mock_apollo_helper';
import { mountExtended } from 'helpers/vue_test_utils_helper';
-import destroyArtifactMutation from '~/artifacts/graphql/mutations/destroy_artifact.mutation.graphql';
-import { I18N_DESTROY_ERROR, I18N_MODAL_TITLE } from '~/artifacts/constants';
+import destroyArtifactMutation from '~/ci/artifacts/graphql/mutations/destroy_artifact.mutation.graphql';
+import { I18N_DESTROY_ERROR, I18N_MODAL_TITLE } from '~/ci/artifacts/constants';
import { createAlert } from '~/alert';
jest.mock('~/alert');
diff --git a/spec/frontend/artifacts/components/feedback_banner_spec.js b/spec/frontend/ci/artifacts/components/feedback_banner_spec.js
index af9599daefa..53e0fdac6f6 100644
--- a/spec/frontend/artifacts/components/feedback_banner_spec.js
+++ b/spec/frontend/ci/artifacts/components/feedback_banner_spec.js
@@ -1,12 +1,12 @@
import { GlBanner } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import FeedbackBanner from '~/artifacts/components/feedback_banner.vue';
+import FeedbackBanner from '~/ci/artifacts/components/feedback_banner.vue';
import { makeMockUserCalloutDismisser } from 'helpers/mock_user_callout_dismisser';
import {
I18N_FEEDBACK_BANNER_TITLE,
I18N_FEEDBACK_BANNER_BUTTON,
FEEDBACK_URL,
-} from '~/artifacts/constants';
+} from '~/ci/artifacts/constants';
const mockBannerImagePath = 'banner/image/path';
diff --git a/spec/frontend/artifacts/components/job_artifacts_table_spec.js b/spec/frontend/ci/artifacts/components/job_artifacts_table_spec.js
index 40f3c9633ab..74d0d683662 100644
--- a/spec/frontend/artifacts/components/job_artifacts_table_spec.js
+++ b/spec/frontend/ci/artifacts/components/job_artifacts_table_spec.js
@@ -9,26 +9,30 @@ import {
} from '@gitlab/ui';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
-import getJobArtifactsResponse from 'test_fixtures/graphql/artifacts/graphql/queries/get_job_artifacts.query.graphql.json';
+import getJobArtifactsResponse from 'test_fixtures/graphql/ci/artifacts/graphql/queries/get_job_artifacts.query.graphql.json';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
import waitForPromises from 'helpers/wait_for_promises';
-import JobArtifactsTable from '~/artifacts/components/job_artifacts_table.vue';
-import FeedbackBanner from '~/artifacts/components/feedback_banner.vue';
-import ArtifactsTableRowDetails from '~/artifacts/components/artifacts_table_row_details.vue';
-import ArtifactDeleteModal from '~/artifacts/components/artifact_delete_modal.vue';
-import ArtifactsBulkDelete from '~/artifacts/components/artifacts_bulk_delete.vue';
+import JobArtifactsTable from '~/ci/artifacts/components/job_artifacts_table.vue';
+import FeedbackBanner from '~/ci/artifacts/components/feedback_banner.vue';
+import ArtifactsTableRowDetails from '~/ci/artifacts/components/artifacts_table_row_details.vue';
+import ArtifactDeleteModal from '~/ci/artifacts/components/artifact_delete_modal.vue';
+import ArtifactsBulkDelete from '~/ci/artifacts/components/artifacts_bulk_delete.vue';
+import BulkDeleteModal from '~/ci/artifacts/components/bulk_delete_modal.vue';
import createMockApollo from 'helpers/mock_apollo_helper';
import { mountExtended } from 'helpers/vue_test_utils_helper';
-import getJobArtifactsQuery from '~/artifacts/graphql/queries/get_job_artifacts.query.graphql';
-import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import getJobArtifactsQuery from '~/ci/artifacts/graphql/queries/get_job_artifacts.query.graphql';
+import bulkDestroyArtifactsMutation from '~/ci/artifacts/graphql/mutations/bulk_destroy_job_artifacts.mutation.graphql';
+import { getIdFromGraphQLId, convertToGraphQLId } from '~/graphql_shared/utils';
+import { TYPENAME_PROJECT } from '~/graphql_shared/constants';
import {
ARCHIVE_FILE_TYPE,
JOBS_PER_PAGE,
I18N_FETCH_ERROR,
INITIAL_CURRENT_PAGE,
BULK_DELETE_FEATURE_FLAG,
-} from '~/artifacts/constants';
-import { totalArtifactsSizeForJob } from '~/artifacts/utils';
+ I18N_BULK_DELETE_ERROR,
+} from '~/ci/artifacts/constants';
+import { totalArtifactsSizeForJob } from '~/ci/artifacts/utils';
import { createAlert } from '~/alert';
jest.mock('~/alert');
@@ -52,7 +56,8 @@ describe('JobArtifactsTable component', () => {
const findCount = () => wrapper.findByTestId('job-artifacts-count');
const findCountAt = (i) => wrapper.findAllByTestId('job-artifacts-count').at(i);
- const findModal = () => wrapper.findComponent(GlModal);
+ const findDeleteModal = () => wrapper.findComponent(ArtifactDeleteModal);
+ const findBulkDeleteModal = () => wrapper.findComponent(BulkDeleteModal);
const findStatuses = () => wrapper.findAllByTestId('job-artifacts-job-status');
const findSuccessfulJobStatus = () => findStatuses().at(0);
@@ -73,9 +78,10 @@ describe('JobArtifactsTable component', () => {
const findArtifactDeleteButton = () => wrapper.findByTestId('job-artifact-row-delete-button');
// first checkbox is a "select all", this finder should get the first job checkbox
- const findJobCheckbox = () => wrapper.findAllComponents(GlFormCheckbox).at(1);
+ const findJobCheckbox = (i = 1) => wrapper.findAllComponents(GlFormCheckbox).at(i);
const findAnyCheckbox = () => wrapper.findComponent(GlFormCheckbox);
const findBulkDelete = () => wrapper.findComponent(ArtifactsBulkDelete);
+ const findBulkDeleteContainer = () => wrapper.findByTestId('bulk-delete-container');
const findPagination = () => wrapper.findComponent(GlPagination);
const setPage = async (page) => {
@@ -83,6 +89,8 @@ describe('JobArtifactsTable component', () => {
await waitForPromises();
};
+ const projectId = 'some/project/id';
+
let enoughJobsToPaginate = [...getJobArtifactsResponse.data.project.jobs.nodes];
while (enoughJobsToPaginate.length <= JOBS_PER_PAGE) {
enoughJobsToPaginate = [
@@ -105,10 +113,20 @@ describe('JobArtifactsTable component', () => {
const archiveArtifact = job.artifacts.nodes.find(
(artifact) => artifact.fileType === ARCHIVE_FILE_TYPE,
);
+ const job2 = getJobArtifactsResponse.data.project.jobs.nodes[1];
+
+ const destroyedCount = job.artifacts.nodes.length;
+ const destroyedIds = job.artifacts.nodes.map((node) => node.id);
+ const bulkDestroyMutationHandler = jest.fn().mockResolvedValue({
+ data: {
+ bulkDestroyJobArtifacts: { errors: [], destroyedCount, destroyedIds },
+ },
+ });
const createComponent = ({
handlers = {
getJobArtifactsQuery: jest.fn().mockResolvedValue(getJobArtifactsResponse),
+ bulkDestroyArtifactsMutation: bulkDestroyMutationHandler,
},
data = {},
canDestroyArtifacts = true,
@@ -118,10 +136,11 @@ describe('JobArtifactsTable component', () => {
wrapper = mountExtended(JobArtifactsTable, {
apolloProvider: createMockApollo([
[getJobArtifactsQuery, requestHandlers.getJobArtifactsQuery],
+ [bulkDestroyArtifactsMutation, requestHandlers.bulkDestroyArtifactsMutation],
]),
provide: {
projectPath: 'project/path',
- projectId: 'gid://projects/id',
+ projectId,
canDestroyArtifacts,
artifactsManagementFeedbackImagePath: 'banner/image/path',
glFeatures,
@@ -265,12 +284,12 @@ describe('JobArtifactsTable component', () => {
expect(findDetailsInRow(0).exists()).toBe(false);
expect(findDetailsInRow(1).exists()).toBe(true);
- findArtifactDeleteButton().trigger('click');
+ findArtifactDeleteButton().vm.$emit('click');
await waitForPromises();
- expect(findModal().props('visible')).toBe(true);
+ expect(findDeleteModal().findComponent(GlModal).props('visible')).toBe(true);
- wrapper.findComponent(ArtifactDeleteModal).vm.$emit('primary');
+ findDeleteModal().vm.$emit('primary');
await waitForPromises();
expect(findDetailsInRow(0).exists()).toBe(false);
@@ -332,24 +351,132 @@ describe('JobArtifactsTable component', () => {
});
describe('delete button', () => {
- it('does not show when user does not have permission', async () => {
- createComponent({ canDestroyArtifacts: false });
+ const artifactsFromJob = job.artifacts.nodes.map((node) => node.id);
+
+ describe('with delete permission and bulk delete feature flag enabled', () => {
+ beforeEach(async () => {
+ createComponent({
+ canDestroyArtifacts: true,
+ glFeatures: { [BULK_DELETE_FEATURE_FLAG]: true },
+ });
+
+ await waitForPromises();
+ });
+
+ it('opens the confirmation modal with the artifacts from the job', async () => {
+ await findDeleteButton().vm.$emit('click');
+
+ expect(findBulkDeleteModal().props()).toMatchObject({
+ visible: true,
+ artifactsToDelete: artifactsFromJob,
+ });
+ });
+
+ it('on confirm, deletes the artifacts from the job and shows a toast', async () => {
+ findDeleteButton().vm.$emit('click');
+ findBulkDeleteModal().vm.$emit('primary');
+
+ expect(bulkDestroyMutationHandler).toHaveBeenCalledWith({
+ projectId: convertToGraphQLId(TYPENAME_PROJECT, projectId),
+ ids: artifactsFromJob,
+ });
+
+ await waitForPromises();
+
+ expect(mockToastShow).toHaveBeenCalledWith(
+ `${artifactsFromJob.length} selected artifacts deleted`,
+ );
+ });
+ it('does not clear selected artifacts on success', async () => {
+ // select job 2 via checkbox
+ findJobCheckbox(2).vm.$emit('input', true);
+
+ // click delete button job 1
+ findDeleteButton().vm.$emit('click');
+
+ // job 2's artifacts should still be selected
+ expect(findBulkDelete().props('selectedArtifacts')).toStrictEqual(
+ job2.artifacts.nodes.map((node) => node.id),
+ );
+
+ // confirm delete
+ findBulkDeleteModal().vm.$emit('primary');
+
+ // job 1's artifacts should be deleted
+ expect(bulkDestroyMutationHandler).toHaveBeenCalledWith({
+ projectId: convertToGraphQLId(TYPENAME_PROJECT, projectId),
+ ids: artifactsFromJob,
+ });
+
+ await waitForPromises();
+
+ // job 2's artifacts should still be selected
+ expect(findBulkDelete().props('selectedArtifacts')).toStrictEqual(
+ job2.artifacts.nodes.map((node) => node.id),
+ );
+ });
+ });
+
+ it('shows an alert and does not clear selected artifacts on error', async () => {
+ createComponent({
+ canDestroyArtifacts: true,
+ glFeatures: { [BULK_DELETE_FEATURE_FLAG]: true },
+ handlers: {
+ getJobArtifactsQuery: jest.fn().mockResolvedValue(getJobArtifactsResponse),
+ bulkDestroyArtifactsMutation: jest.fn().mockRejectedValue(),
+ },
+ });
await waitForPromises();
- expect(findDeleteButton().exists()).toBe(false);
+ // select job 2 via checkbox
+ findJobCheckbox(2).vm.$emit('input', true);
+
+ // click delete button job 1
+ findDeleteButton().vm.$emit('click');
+
+ // confirm delete
+ findBulkDeleteModal().vm.$emit('primary');
+
+ await waitForPromises();
+
+ // job 2's artifacts should still be selected
+ expect(findBulkDelete().props('selectedArtifacts')).toStrictEqual(
+ job2.artifacts.nodes.map((node) => node.id),
+ );
+ expect(createAlert).toHaveBeenCalledWith({
+ captureError: true,
+ error: expect.any(Error),
+ message: I18N_BULK_DELETE_ERROR,
+ });
});
- it('shows a disabled delete button for now (coming soon)', async () => {
- createComponent();
+ it('is disabled when bulk delete feature flag is disabled', async () => {
+ createComponent({
+ canDestroyArtifacts: true,
+ glFeatures: { [BULK_DELETE_FEATURE_FLAG]: false },
+ });
await waitForPromises();
expect(findDeleteButton().attributes('disabled')).toBe('disabled');
});
+
+ it('is hidden when user does not have delete permission', async () => {
+ createComponent({
+ canDestroyArtifacts: false,
+ glFeatures: { [BULK_DELETE_FEATURE_FLAG]: false },
+ });
+
+ await waitForPromises();
+
+ expect(findDeleteButton().exists()).toBe(false);
+ });
});
describe('bulk delete', () => {
+ const selectedArtifacts = job.artifacts.nodes.map((node) => node.id);
+
describe('with permission and feature flag enabled', () => {
beforeEach(async () => {
createComponent({
@@ -361,33 +488,84 @@ describe('JobArtifactsTable component', () => {
});
it('shows selected artifacts when a job is checked', async () => {
- expect(findBulkDelete().exists()).toBe(false);
+ expect(findBulkDeleteContainer().exists()).toBe(false);
await findJobCheckbox().vm.$emit('input', true);
- expect(findBulkDelete().exists()).toBe(true);
- expect(findBulkDelete().props('selectedArtifacts')).toStrictEqual(
- job.artifacts.nodes.map((node) => node.id),
- );
+ expect(findBulkDeleteContainer().exists()).toBe(true);
+ expect(findBulkDelete().props('selectedArtifacts')).toStrictEqual(selectedArtifacts);
});
it('disappears when selected artifacts are cleared', async () => {
await findJobCheckbox().vm.$emit('input', true);
- expect(findBulkDelete().exists()).toBe(true);
+ expect(findBulkDeleteContainer().exists()).toBe(true);
await findBulkDelete().vm.$emit('clearSelectedArtifacts');
- expect(findBulkDelete().exists()).toBe(false);
+ expect(findBulkDeleteContainer().exists()).toBe(false);
});
- it('shows a toast when artifacts are deleted', async () => {
- const count = job.artifacts.nodes.length;
+ it('shows a modal to confirm bulk delete', async () => {
+ findJobCheckbox().vm.$emit('input', true);
+ findBulkDelete().vm.$emit('showBulkDeleteModal');
- await findJobCheckbox().vm.$emit('input', true);
- findBulkDelete().vm.$emit('deleted', count);
+ await waitForPromises();
+
+ expect(findBulkDeleteModal().props('visible')).toBe(true);
+ });
+
+ it('deletes the selected artifacts and shows a toast', async () => {
+ findJobCheckbox().vm.$emit('input', true);
+ findBulkDelete().vm.$emit('showBulkDeleteModal');
+ findBulkDeleteModal().vm.$emit('primary');
+
+ expect(bulkDestroyMutationHandler).toHaveBeenCalledWith({
+ projectId: convertToGraphQLId(TYPENAME_PROJECT, projectId),
+ ids: selectedArtifacts,
+ });
+
+ await waitForPromises();
+
+ expect(mockToastShow).toHaveBeenCalledWith(
+ `${selectedArtifacts.length} selected artifacts deleted`,
+ );
+ });
+
+ it('clears selected artifacts on success', async () => {
+ findJobCheckbox().vm.$emit('input', true);
+ findBulkDelete().vm.$emit('showBulkDeleteModal');
+ findBulkDeleteModal().vm.$emit('primary');
+
+ await waitForPromises();
+
+ expect(findBulkDelete().props('selectedArtifacts')).toStrictEqual([]);
+ });
+ });
+
+ it('shows an alert and does not clear selected artifacts on error', async () => {
+ createComponent({
+ canDestroyArtifacts: true,
+ glFeatures: { [BULK_DELETE_FEATURE_FLAG]: true },
+ handlers: {
+ getJobArtifactsQuery: jest.fn().mockResolvedValue(getJobArtifactsResponse),
+ bulkDestroyArtifactsMutation: jest.fn().mockRejectedValue(),
+ },
+ });
+
+ await waitForPromises();
+
+ findJobCheckbox().vm.$emit('input', true);
+ findBulkDelete().vm.$emit('showBulkDeleteModal');
+ findBulkDeleteModal().vm.$emit('primary');
+
+ await waitForPromises();
- expect(mockToastShow).toHaveBeenCalledWith(`${count} selected artifacts deleted`);
+ expect(findBulkDelete().props('selectedArtifacts')).toStrictEqual(selectedArtifacts);
+ expect(createAlert).toHaveBeenCalledWith({
+ captureError: true,
+ error: expect.any(Error),
+ message: I18N_BULK_DELETE_ERROR,
});
});
diff --git a/spec/frontend/artifacts/components/job_checkbox_spec.js b/spec/frontend/ci/artifacts/components/job_checkbox_spec.js
index 95cc548b8c8..ae70bb4b17b 100644
--- a/spec/frontend/artifacts/components/job_checkbox_spec.js
+++ b/spec/frontend/ci/artifacts/components/job_checkbox_spec.js
@@ -1,7 +1,7 @@
import { GlFormCheckbox } from '@gitlab/ui';
-import mockGetJobArtifactsResponse from 'test_fixtures/graphql/artifacts/graphql/queries/get_job_artifacts.query.graphql.json';
+import mockGetJobArtifactsResponse from 'test_fixtures/graphql/ci/artifacts/graphql/queries/get_job_artifacts.query.graphql.json';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import JobCheckbox from '~/artifacts/components/job_checkbox.vue';
+import JobCheckbox from '~/ci/artifacts/components/job_checkbox.vue';
describe('JobCheckbox component', () => {
let wrapper;
diff --git a/spec/frontend/artifacts/graphql/cache_update_spec.js b/spec/frontend/ci/artifacts/graphql/cache_update_spec.js
index 4d610328298..3c415534c7c 100644
--- a/spec/frontend/artifacts/graphql/cache_update_spec.js
+++ b/spec/frontend/ci/artifacts/graphql/cache_update_spec.js
@@ -1,5 +1,5 @@
-import getJobArtifactsQuery from '~/artifacts/graphql/queries/get_job_artifacts.query.graphql';
-import { removeArtifactFromStore } from '~/artifacts/graphql/cache_update';
+import getJobArtifactsQuery from '~/ci/artifacts/graphql/queries/get_job_artifacts.query.graphql';
+import { removeArtifactFromStore } from '~/ci/artifacts/graphql/cache_update';
describe('Artifact table cache updates', () => {
let store;
diff --git a/spec/frontend/ci/ci_variable_list/ci_variable_list/ci_variable_list_spec.js b/spec/frontend/ci/ci_variable_list/ci_variable_list/ci_variable_list_spec.js
index e4abedb412f..8990a70d4ef 100644
--- a/spec/frontend/ci/ci_variable_list/ci_variable_list/ci_variable_list_spec.js
+++ b/spec/frontend/ci/ci_variable_list/ci_variable_list/ci_variable_list_spec.js
@@ -1,5 +1,7 @@
import $ from 'jquery';
-import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
+import htmlPipelineSchedulesEdit from 'test_fixtures/pipeline_schedules/edit.html';
+import htmlPipelineSchedulesEditWithVariables from 'test_fixtures/pipeline_schedules/edit_with_variables.html';
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import VariableList from '~/ci/ci_variable_list/ci_variable_list';
const HIDE_CLASS = 'hide';
@@ -11,7 +13,7 @@ describe('VariableList', () => {
describe('with only key/value inputs', () => {
describe('with no variables', () => {
beforeEach(() => {
- loadHTMLFixture('pipeline_schedules/edit.html');
+ setHTMLFixture(htmlPipelineSchedulesEdit);
$wrapper = $('.js-ci-variable-list-section');
variableList = new VariableList({
@@ -69,7 +71,7 @@ describe('VariableList', () => {
describe('with persisted variables', () => {
beforeEach(() => {
- loadHTMLFixture('pipeline_schedules/edit_with_variables.html');
+ setHTMLFixture(htmlPipelineSchedulesEditWithVariables);
$wrapper = $('.js-ci-variable-list-section');
variableList = new VariableList({
@@ -106,7 +108,7 @@ describe('VariableList', () => {
describe('toggleEnableRow method', () => {
beforeEach(() => {
- loadHTMLFixture('pipeline_schedules/edit_with_variables.html');
+ setHTMLFixture(htmlPipelineSchedulesEditWithVariables);
$wrapper = $('.js-ci-variable-list-section');
variableList = new VariableList({
diff --git a/spec/frontend/ci/ci_variable_list/components/ci_admin_variables_spec.js b/spec/frontend/ci/ci_variable_list/components/ci_admin_variables_spec.js
index 8e012883f09..1d0dcf242a4 100644
--- a/spec/frontend/ci/ci_variable_list/components/ci_admin_variables_spec.js
+++ b/spec/frontend/ci/ci_variable_list/components/ci_admin_variables_spec.js
@@ -1,7 +1,16 @@
import { shallowMount } from '@vue/test-utils';
+import {
+ ADD_MUTATION_ACTION,
+ DELETE_MUTATION_ACTION,
+ UPDATE_MUTATION_ACTION,
+} from '~/ci/ci_variable_list/constants';
import ciAdminVariables from '~/ci/ci_variable_list/components/ci_admin_variables.vue';
import ciVariableShared from '~/ci/ci_variable_list/components/ci_variable_shared.vue';
+import addAdminVariable from '~/ci/ci_variable_list/graphql/mutations/admin_add_variable.mutation.graphql';
+import deleteAdminVariable from '~/ci/ci_variable_list/graphql/mutations/admin_delete_variable.mutation.graphql';
+import updateAdminVariable from '~/ci/ci_variable_list/graphql/mutations/admin_update_variable.mutation.graphql';
+import getAdminVariables from '~/ci/ci_variable_list/graphql/queries/variables.query.graphql';
describe('Ci Project Variable wrapper', () => {
let wrapper;
@@ -22,8 +31,17 @@ describe('Ci Project Variable wrapper', () => {
componentName: 'InstanceVariables',
entity: '',
hideEnvironmentScope: true,
- mutationData: wrapper.vm.$options.mutationData,
- queryData: wrapper.vm.$options.queryData,
+ mutationData: {
+ [ADD_MUTATION_ACTION]: addAdminVariable,
+ [UPDATE_MUTATION_ACTION]: updateAdminVariable,
+ [DELETE_MUTATION_ACTION]: deleteAdminVariable,
+ },
+ queryData: {
+ ciVariables: {
+ lookup: expect.any(Function),
+ query: getAdminVariables,
+ },
+ },
refetchAfterMutation: true,
fullPath: null,
id: null,
diff --git a/spec/frontend/ci/ci_variable_list/components/ci_environments_dropdown_spec.js b/spec/frontend/ci/ci_variable_list/components/ci_environments_dropdown_spec.js
index 7181398c2a6..1937e3b34b7 100644
--- a/spec/frontend/ci/ci_variable_list/components/ci_environments_dropdown_spec.js
+++ b/spec/frontend/ci/ci_variable_list/components/ci_environments_dropdown_spec.js
@@ -1,13 +1,17 @@
import { GlListboxItem, GlCollapsibleListbox, GlDropdownItem, GlIcon } from '@gitlab/ui';
-import { mount } from '@vue/test-utils';
-import { allEnvironments } from '~/ci/ci_variable_list/constants';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import { allEnvironments, ENVIRONMENT_QUERY_LIMIT } from '~/ci/ci_variable_list/constants';
import CiEnvironmentsDropdown from '~/ci/ci_variable_list/components/ci_environments_dropdown.vue';
describe('Ci environments dropdown', () => {
let wrapper;
const envs = ['dev', 'prod', 'staging'];
- const defaultProps = { environments: envs, selectedEnvironmentScope: '' };
+ const defaultProps = {
+ areEnvironmentsLoading: false,
+ environments: envs,
+ selectedEnvironmentScope: '',
+ };
const findAllListboxItems = () => wrapper.findAllComponents(GlListboxItem);
const findListboxItemByIndex = (index) => wrapper.findAllComponents(GlListboxItem).at(index);
@@ -15,13 +19,19 @@ describe('Ci environments dropdown', () => {
const findListbox = () => wrapper.findComponent(GlCollapsibleListbox);
const findListboxText = () => findListbox().props('toggleText');
const findCreateWildcardButton = () => wrapper.findComponent(GlDropdownItem);
+ const findMaxEnvNote = () => wrapper.findByTestId('max-envs-notice');
- const createComponent = ({ props = {}, searchTerm = '' } = {}) => {
- wrapper = mount(CiEnvironmentsDropdown, {
+ const createComponent = ({ props = {}, searchTerm = '', enableFeatureFlag = false } = {}) => {
+ wrapper = mountExtended(CiEnvironmentsDropdown, {
propsData: {
...defaultProps,
...props,
},
+ provide: {
+ glFeatures: {
+ ciLimitEnvironmentScope: enableFeatureFlag,
+ },
+ },
});
findListbox().vm.$emit('search', searchTerm);
@@ -40,19 +50,32 @@ describe('Ci environments dropdown', () => {
});
describe('Search term is empty', () => {
- beforeEach(() => {
- createComponent({ props: { environments: envs } });
- });
+ describe.each`
+ featureFlag | flagStatus | defaultEnvStatus | firstItemValue | envIndices
+ ${true} | ${'enabled'} | ${'prepends'} | ${'*'} | ${[1, 2, 3]}
+ ${false} | ${'disabled'} | ${'does not prepend'} | ${envs[0]} | ${[0, 1, 2]}
+ `(
+ 'when ciLimitEnvironmentScope feature flag is $flagStatus',
+ ({ featureFlag, defaultEnvStatus, firstItemValue, envIndices }) => {
+ beforeEach(() => {
+ createComponent({ props: { environments: envs }, enableFeatureFlag: featureFlag });
+ });
- it('renders all environments when search term is empty', () => {
- expect(findListboxItemByIndex(0).text()).toBe(envs[0]);
- expect(findListboxItemByIndex(1).text()).toBe(envs[1]);
- expect(findListboxItemByIndex(2).text()).toBe(envs[2]);
- });
+ it(`${defaultEnvStatus} * in listbox`, () => {
+ expect(findListboxItemByIndex(0).text()).toBe(firstItemValue);
+ });
- it('does not display active checkmark on the inactive stage', () => {
- expect(findActiveIconByIndex(0).classes('gl-visibility-hidden')).toBe(true);
- });
+ it('renders all environments', () => {
+ expect(findListboxItemByIndex(envIndices[0]).text()).toBe(envs[0]);
+ expect(findListboxItemByIndex(envIndices[1]).text()).toBe(envs[1]);
+ expect(findListboxItemByIndex(envIndices[2]).text()).toBe(envs[2]);
+ });
+
+ it('does not display active checkmark', () => {
+ expect(findActiveIconByIndex(0).classes('gl-visibility-hidden')).toBe(true);
+ });
+ },
+ );
});
describe('when `*` is the value of selectedEnvironmentScope props', () => {
@@ -68,46 +91,92 @@ describe('Ci environments dropdown', () => {
});
});
- describe('Environments found', () => {
+ describe('When ciLimitEnvironmentScope feature flag is disabled', () => {
const currentEnv = envs[2];
beforeEach(() => {
- createComponent({ searchTerm: currentEnv });
+ createComponent();
});
- it('renders only the environment searched for', () => {
+ it('filters on the frontend and renders only the environment searched for', async () => {
+ await findListbox().vm.$emit('search', currentEnv);
+
expect(findAllListboxItems()).toHaveLength(1);
expect(findListboxItemByIndex(0).text()).toBe(currentEnv);
});
- it('does not display create button', () => {
- expect(findCreateWildcardButton().exists()).toBe(false);
+ it('does not emit event when searching', async () => {
+ expect(wrapper.emitted('search-environment-scope')).toBeUndefined();
+
+ await findListbox().vm.$emit('search', currentEnv);
+
+ expect(wrapper.emitted('search-environment-scope')).toBeUndefined();
});
- describe('Custom events', () => {
- describe('when selecting an environment', () => {
- const itemIndex = 0;
+ it('does not display note about max environments shown', () => {
+ expect(findMaxEnvNote().exists()).toBe(false);
+ });
+ });
- beforeEach(() => {
- createComponent();
- });
+ describe('When ciLimitEnvironmentScope feature flag is enabled', () => {
+ const currentEnv = envs[2];
- it('emits `select-environment` when an environment is clicked', () => {
- findListbox().vm.$emit('select', envs[itemIndex]);
- expect(wrapper.emitted('select-environment')).toEqual([[envs[itemIndex]]]);
- });
+ beforeEach(() => {
+ createComponent({ enableFeatureFlag: true });
+ });
+
+ it('renders environments passed down to it', async () => {
+ await findListbox().vm.$emit('search', currentEnv);
+
+ expect(findAllListboxItems()).toHaveLength(envs.length);
+ });
+
+ it('emits event when searching', async () => {
+ expect(wrapper.emitted('search-environment-scope')).toHaveLength(1);
+
+ await findListbox().vm.$emit('search', currentEnv);
+
+ expect(wrapper.emitted('search-environment-scope')).toHaveLength(2);
+ expect(wrapper.emitted('search-environment-scope')[1]).toEqual([currentEnv]);
+ });
+
+ it('renders loading icon while search query is loading', () => {
+ createComponent({ enableFeatureFlag: true, props: { areEnvironmentsLoading: true } });
+
+ expect(findListbox().props('searching')).toBe(true);
+ });
+
+ it('displays note about max environments shown', () => {
+ expect(findMaxEnvNote().exists()).toBe(true);
+ expect(findMaxEnvNote().text()).toContain(String(ENVIRONMENT_QUERY_LIMIT));
+ });
+ });
+
+ describe('Custom events', () => {
+ describe('when selecting an environment', () => {
+ const itemIndex = 0;
+
+ beforeEach(() => {
+ createComponent();
});
- describe('when creating a new environment from a search term', () => {
- const search = 'new-env';
- beforeEach(() => {
- createComponent({ searchTerm: search });
- });
+ it('emits `select-environment` when an environment is clicked', () => {
+ findListbox().vm.$emit('select', envs[itemIndex]);
- it('emits create-environment-scope', () => {
- findCreateWildcardButton().vm.$emit('click');
- expect(wrapper.emitted('create-environment-scope')).toEqual([[search]]);
- });
+ expect(wrapper.emitted('select-environment')).toEqual([[envs[itemIndex]]]);
+ });
+ });
+
+ describe('when creating a new environment from a search term', () => {
+ const search = 'new-env';
+ beforeEach(() => {
+ createComponent({ searchTerm: search });
+ });
+
+ it('emits create-environment-scope', () => {
+ findCreateWildcardButton().vm.$emit('click');
+
+ expect(wrapper.emitted('create-environment-scope')).toEqual([[search]]);
});
});
});
diff --git a/spec/frontend/ci/ci_variable_list/components/ci_group_variables_spec.js b/spec/frontend/ci/ci_variable_list/components/ci_group_variables_spec.js
index 77d90a7667d..7436210fe70 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
@@ -4,6 +4,15 @@ 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 {
+ ADD_MUTATION_ACTION,
+ DELETE_MUTATION_ACTION,
+ UPDATE_MUTATION_ACTION,
+} from '~/ci/ci_variable_list/constants';
+import getGroupVariables from '~/ci/ci_variable_list/graphql/queries/group_variables.query.graphql';
+import addGroupVariable from '~/ci/ci_variable_list/graphql/mutations/group_add_variable.mutation.graphql';
+import deleteGroupVariable from '~/ci/ci_variable_list/graphql/mutations/group_delete_variable.mutation.graphql';
+import updateGroupVariable from '~/ci/ci_variable_list/graphql/mutations/group_update_variable.mutation.graphql';
const mockProvide = {
glFeatures: {
@@ -37,8 +46,17 @@ describe('Ci Group Variable wrapper', () => {
entity: 'group',
fullPath: mockProvide.groupPath,
hideEnvironmentScope: false,
- mutationData: wrapper.vm.$options.mutationData,
- queryData: wrapper.vm.$options.queryData,
+ mutationData: {
+ [ADD_MUTATION_ACTION]: addGroupVariable,
+ [UPDATE_MUTATION_ACTION]: updateGroupVariable,
+ [DELETE_MUTATION_ACTION]: deleteGroupVariable,
+ },
+ queryData: {
+ ciVariables: {
+ lookup: expect.any(Function),
+ query: getGroupVariables,
+ },
+ },
refetchAfterMutation: false,
});
});
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 ce5237a84f7..69b0d4261b2 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
@@ -4,6 +4,16 @@ 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 {
+ ADD_MUTATION_ACTION,
+ DELETE_MUTATION_ACTION,
+ UPDATE_MUTATION_ACTION,
+} from '~/ci/ci_variable_list/constants';
+import getProjectEnvironments from '~/ci/ci_variable_list/graphql/queries/project_environments.query.graphql';
+import getProjectVariables from '~/ci/ci_variable_list/graphql/queries/project_variables.query.graphql';
+import addProjectVariable from '~/ci/ci_variable_list/graphql/mutations/project_add_variable.mutation.graphql';
+import deleteProjectVariable from '~/ci/ci_variable_list/graphql/mutations/project_delete_variable.mutation.graphql';
+import updateProjectVariable from '~/ci/ci_variable_list/graphql/mutations/project_update_variable.mutation.graphql';
const mockProvide = {
projectFullPath: '/namespace/project',
@@ -33,8 +43,21 @@ describe('Ci Project Variable wrapper', () => {
entity: 'project',
fullPath: mockProvide.projectFullPath,
hideEnvironmentScope: false,
- mutationData: wrapper.vm.$options.mutationData,
- queryData: wrapper.vm.$options.queryData,
+ mutationData: {
+ [ADD_MUTATION_ACTION]: addProjectVariable,
+ [UPDATE_MUTATION_ACTION]: updateProjectVariable,
+ [DELETE_MUTATION_ACTION]: deleteProjectVariable,
+ },
+ queryData: {
+ ciVariables: {
+ lookup: expect.any(Function),
+ query: getProjectVariables,
+ },
+ environments: {
+ lookup: expect.any(Function),
+ query: getProjectEnvironments,
+ },
+ },
refetchAfterMutation: false,
});
});
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 8f3fccc2804..e8bfb370fb4 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
@@ -10,10 +10,12 @@ import {
EVENT_LABEL,
EVENT_ACTION,
ENVIRONMENT_SCOPE_LINK_TITLE,
+ groupString,
instanceString,
+ projectString,
variableOptions,
} from '~/ci/ci_variable_list/constants';
-import { mockVariablesWithScopes } from '../mocks';
+import { mockEnvs, mockVariablesWithScopes, mockVariablesWithUniqueScopes } from '../mocks';
import ModalStub from '../stubs';
describe('Ci variable modal', () => {
@@ -42,12 +44,13 @@ describe('Ci variable modal', () => {
};
const defaultProps = {
+ areEnvironmentsLoading: false,
areScopedVariablesAvailable: true,
environments: [],
hideEnvironmentScope: false,
mode: ADD_VARIABLE_ACTION,
selectedVariable: {},
- variable: [],
+ variables: [],
};
const createComponent = ({ mountFn = shallowMountExtended, props = {}, provide = {} } = {}) => {
@@ -111,7 +114,6 @@ describe('Ci variable modal', () => {
beforeEach(() => {
createComponent({ props: { selectedVariable: currentVariable } });
- jest.spyOn(wrapper.vm, '$emit');
});
it('Dispatches `add-variable` action on submit', () => {
@@ -152,7 +154,7 @@ describe('Ci variable modal', () => {
findModal().vm.$emit('shown');
});
- it('keeps the value as false', async () => {
+ it('keeps the value as false', () => {
expect(
findProtectedVariableCheckbox().attributes('data-is-protected-checked'),
).toBeUndefined();
@@ -237,7 +239,6 @@ describe('Ci variable modal', () => {
it('defaults to expanded and raw:false when adding a variable', () => {
createComponent({ props: { selectedVariable: variable } });
- jest.spyOn(wrapper.vm, '$emit');
findModal().vm.$emit('shown');
@@ -262,7 +263,6 @@ describe('Ci variable modal', () => {
mode: EDIT_VARIABLE_ACTION,
},
});
- jest.spyOn(wrapper.vm, '$emit');
findModal().vm.$emit('shown');
await findExpandedVariableCheckbox().vm.$emit('change');
@@ -301,7 +301,6 @@ describe('Ci variable modal', () => {
beforeEach(() => {
createComponent({ props: { selectedVariable: variable, mode: EDIT_VARIABLE_ACTION } });
- jest.spyOn(wrapper.vm, '$emit');
});
it('button text is Update variable when updating', () => {
@@ -349,6 +348,42 @@ describe('Ci variable modal', () => {
expect(link.attributes('title')).toBe(ENVIRONMENT_SCOPE_LINK_TITLE);
expect(link.attributes('href')).toBe(defaultProvide.environmentScopeLink);
});
+
+ describe('when feature flag is enabled', () => {
+ beforeEach(() => {
+ createComponent({
+ props: {
+ environments: mockEnvs,
+ variables: mockVariablesWithUniqueScopes(projectString),
+ },
+ provide: { glFeatures: { ciLimitEnvironmentScope: true } },
+ });
+ });
+
+ it('does not merge environment scope sources', () => {
+ const expectedLength = mockEnvs.length;
+
+ expect(findCiEnvironmentsDropdown().props('environments')).toHaveLength(expectedLength);
+ });
+ });
+
+ describe('when feature flag is disabled', () => {
+ const mockGroupVariables = mockVariablesWithUniqueScopes(groupString);
+ beforeEach(() => {
+ createComponent({
+ props: {
+ environments: mockEnvs,
+ variables: mockGroupVariables,
+ },
+ });
+ });
+
+ it('merges environment scope sources', () => {
+ const expectedLength = mockGroupVariables.length + mockEnvs.length;
+
+ expect(findCiEnvironmentsDropdown().props('environments')).toHaveLength(expectedLength);
+ });
+ });
});
describe('and section is hidden', () => {
diff --git a/spec/frontend/ci/ci_variable_list/components/ci_variable_settings_spec.js b/spec/frontend/ci/ci_variable_list/components/ci_variable_settings_spec.js
index 0141232a299..12ca9a78369 100644
--- a/spec/frontend/ci/ci_variable_list/components/ci_variable_settings_spec.js
+++ b/spec/frontend/ci/ci_variable_list/components/ci_variable_settings_spec.js
@@ -1,4 +1,3 @@
-import { nextTick } from 'vue';
import { shallowMount } from '@vue/test-utils';
import CiVariableSettings from '~/ci/ci_variable_list/components/ci_variable_settings.vue';
import ciVariableModal from '~/ci/ci_variable_list/components/ci_variable_modal.vue';
@@ -16,6 +15,7 @@ describe('Ci variable table', () => {
let wrapper;
const defaultProps = {
+ areEnvironmentsLoading: false,
areScopedVariablesAvailable: true,
entity: 'project',
environments: mapEnvironmentNames(mockEnvs),
@@ -54,10 +54,10 @@ describe('Ci variable table', () => {
it('passes props down correctly to the ci modal', async () => {
createComponent();
- findCiVariableTable().vm.$emit('set-selected-variable');
- await nextTick();
+ await findCiVariableTable().vm.$emit('set-selected-variable');
expect(findCiVariableModal().props()).toEqual({
+ areEnvironmentsLoading: defaultProps.areEnvironmentsLoading,
areScopedVariablesAvailable: defaultProps.areScopedVariablesAvailable,
environments: defaultProps.environments,
hideEnvironmentScope: defaultProps.hideEnvironmentScope,
@@ -74,15 +74,13 @@ describe('Ci variable table', () => {
});
it('passes down ADD mode when receiving an empty variable', async () => {
- findCiVariableTable().vm.$emit('set-selected-variable');
- await nextTick();
+ await findCiVariableTable().vm.$emit('set-selected-variable');
expect(findCiVariableModal().props('mode')).toBe(ADD_VARIABLE_ACTION);
});
it('passes down EDIT mode when receiving a variable', async () => {
- findCiVariableTable().vm.$emit('set-selected-variable', newVariable);
- await nextTick();
+ await findCiVariableTable().vm.$emit('set-selected-variable', newVariable);
expect(findCiVariableModal().props('mode')).toBe(EDIT_VARIABLE_ACTION);
});
@@ -98,25 +96,21 @@ describe('Ci variable table', () => {
});
it('shows modal when adding a new variable', async () => {
- findCiVariableTable().vm.$emit('set-selected-variable');
- await nextTick();
+ await findCiVariableTable().vm.$emit('set-selected-variable');
expect(findCiVariableModal().exists()).toBe(true);
});
it('shows modal when updating a variable', async () => {
- findCiVariableTable().vm.$emit('set-selected-variable', newVariable);
- await nextTick();
+ await findCiVariableTable().vm.$emit('set-selected-variable', newVariable);
expect(findCiVariableModal().exists()).toBe(true);
});
it('hides modal when receiving the event from the modal', async () => {
- findCiVariableTable().vm.$emit('set-selected-variable');
- await nextTick();
+ await findCiVariableTable().vm.$emit('set-selected-variable');
- findCiVariableModal().vm.$emit('hideModal');
- await nextTick();
+ await findCiVariableModal().vm.$emit('hideModal');
expect(findCiVariableModal().exists()).toBe(false);
});
@@ -133,11 +127,9 @@ describe('Ci variable table', () => {
${'update-variable'}
${'delete-variable'}
`('bubbles up the $eventName event', async ({ eventName }) => {
- findCiVariableTable().vm.$emit('set-selected-variable');
- await nextTick();
+ await findCiVariableTable().vm.$emit('set-selected-variable');
- findCiVariableModal().vm.$emit(eventName, newVariable);
- await nextTick();
+ await findCiVariableModal().vm.$emit(eventName, newVariable);
expect(wrapper.emitted(eventName)).toEqual([[newVariable]]);
});
@@ -154,10 +146,23 @@ describe('Ci variable table', () => {
${'handle-next-page'} | ${undefined}
${'sort-changed'} | ${{ sortDesc: true }}
`('bubbles up the $eventName event', async ({ args, eventName }) => {
- findCiVariableTable().vm.$emit(eventName, args);
- await nextTick();
+ await findCiVariableTable().vm.$emit(eventName, args);
expect(wrapper.emitted(eventName)).toEqual([[args]]);
});
});
+
+ describe('environment events', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('bubbles up the search event', async () => {
+ await findCiVariableTable().vm.$emit('set-selected-variable');
+
+ await findCiVariableModal().vm.$emit('search-environment-scope', 'staging');
+
+ expect(wrapper.emitted('search-environment-scope')).toEqual([['staging']]);
+ });
+ });
});
diff --git a/spec/frontend/ci/ci_variable_list/components/ci_variable_shared_spec.js b/spec/frontend/ci/ci_variable_list/components/ci_variable_shared_spec.js
index 87192006efc..a25d325f7a1 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
@@ -1,13 +1,12 @@
-import Vue, { nextTick } from 'vue';
+import Vue from 'vue';
import VueApollo from 'vue-apollo';
import { GlLoadingIcon, GlTable } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
+import { assertProps } from 'helpers/assert_props';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { createAlert } from '~/alert';
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';
import ciVariableSettings from '~/ci/ci_variable_list/components/ci_variable_settings.vue';
@@ -18,12 +17,11 @@ import getGroupVariables from '~/ci/ci_variable_list/graphql/queries/group_varia
import getProjectVariables from '~/ci/ci_variable_list/graphql/queries/project_variables.query.graphql';
import {
- ADD_MUTATION_ACTION,
- DELETE_MUTATION_ACTION,
- UPDATE_MUTATION_ACTION,
+ ENVIRONMENT_QUERY_LIMIT,
environmentFetchErrorText,
genericMutationErrorText,
variableFetchErrorText,
+ mapMutationActionToToast,
} from '~/ci/ci_variable_list/constants';
import {
@@ -63,15 +61,22 @@ describe('Ci Variable Shared Component', () => {
let mockApollo;
let mockEnvironments;
+ let mockMutation;
+ let mockAddMutation;
+ let mockUpdateMutation;
+ let mockDeleteMutation;
let mockVariables;
+ const mockToastShow = jest.fn();
+
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findCiTable = () => wrapper.findComponent(GlTable);
const findCiSettings = () => wrapper.findComponent(ciVariableSettings);
// eslint-disable-next-line consistent-return
- async function createComponentWithApollo({
+ function createComponentWithApollo({
customHandlers = null,
+ customResolvers = null,
isLoading = false,
props = { ...createProjectProps() },
provide = {},
@@ -81,7 +86,9 @@ describe('Ci Variable Shared Component', () => {
[getProjectVariables, mockVariables],
];
- mockApollo = createMockApollo(handlers, resolvers);
+ const mutationResolvers = customResolvers || resolvers;
+
+ mockApollo = createMockApollo(handlers, mutationResolvers);
wrapper = shallowMount(ciVariableShared, {
propsData: {
@@ -94,6 +101,11 @@ describe('Ci Variable Shared Component', () => {
},
apolloProvider: mockApollo,
stubs: { ciVariableSettings, ciVariableTable },
+ mocks: {
+ $toast: {
+ show: mockToastShow,
+ },
+ },
});
if (!isLoading) {
@@ -104,6 +116,10 @@ describe('Ci Variable Shared Component', () => {
beforeEach(() => {
mockEnvironments = jest.fn();
mockVariables = jest.fn();
+ mockMutation = jest.fn();
+ mockAddMutation = jest.fn();
+ mockUpdateMutation = jest.fn();
+ mockDeleteMutation = jest.fn();
});
describe.each`
@@ -111,11 +127,11 @@ describe('Ci Variable Shared Component', () => {
${true} | ${'enabled'}
${false} | ${'disabled'}
`('When Pages FF is $text', ({ isVariablePagesEnabled }) => {
- const featureFlagProvide = isVariablePagesEnabled
+ const pagesFeatureFlagProvide = isVariablePagesEnabled
? { glFeatures: { ciVariablesPages: true } }
: {};
- describe('while queries are being fetch', () => {
+ describe('while queries are being fetched', () => {
beforeEach(() => {
createComponentWithApollo({ isLoading: true });
});
@@ -133,7 +149,7 @@ describe('Ci Variable Shared Component', () => {
mockVariables.mockResolvedValue(mockProjectVariables);
await createComponentWithApollo({
- provide: { ...createProjectProvide(), ...featureFlagProvide },
+ provide: { ...createProjectProvide(), ...pagesFeatureFlagProvide },
});
});
@@ -163,7 +179,7 @@ describe('Ci Variable Shared Component', () => {
mockEnvironments.mockResolvedValue(mockProjectEnvironments);
mockVariables.mockRejectedValue();
- await createComponentWithApollo({ provide: featureFlagProvide });
+ await createComponentWithApollo({ provide: pagesFeatureFlagProvide });
});
it('calls createAlert with the expected error message', () => {
@@ -176,7 +192,7 @@ describe('Ci Variable Shared Component', () => {
mockEnvironments.mockRejectedValue();
mockVariables.mockResolvedValue(mockProjectVariables);
- await createComponentWithApollo({ provide: featureFlagProvide });
+ await createComponentWithApollo({ provide: pagesFeatureFlagProvide });
});
it('calls createAlert with the expected error message', () => {
@@ -187,134 +203,283 @@ describe('Ci Variable Shared Component', () => {
describe('environment query', () => {
describe('when there is an environment key in queryData', () => {
- beforeEach(async () => {
+ beforeEach(() => {
mockEnvironments.mockResolvedValue(mockProjectEnvironments);
+
mockVariables.mockResolvedValue(mockProjectVariables);
+ });
+ it('environments are fetched', async () => {
await createComponentWithApollo({
props: { ...createProjectProps() },
- provide: featureFlagProvide,
+ provide: pagesFeatureFlagProvide,
});
+
+ expect(mockEnvironments).toHaveBeenCalled();
});
- it('is executed', () => {
- expect(mockVariables).toHaveBeenCalled();
+ describe('when Limit Environment Scope FF is enabled', () => {
+ beforeEach(async () => {
+ await createComponentWithApollo({
+ props: { ...createProjectProps() },
+ provide: {
+ glFeatures: {
+ ciLimitEnvironmentScope: true,
+ ciVariablesPages: isVariablePagesEnabled,
+ },
+ },
+ });
+ });
+
+ it('initial query is called with the correct variables', () => {
+ expect(mockEnvironments).toHaveBeenCalledWith({
+ first: ENVIRONMENT_QUERY_LIMIT,
+ fullPath: '/namespace/project/',
+ search: '',
+ });
+ });
+
+ it(`refetches environments when search term is present`, async () => {
+ expect(mockEnvironments).toHaveBeenCalledTimes(1);
+ expect(mockEnvironments).toHaveBeenCalledWith(expect.objectContaining({ search: '' }));
+
+ await findCiSettings().vm.$emit('search-environment-scope', 'staging');
+
+ expect(mockEnvironments).toHaveBeenCalledTimes(2);
+ expect(mockEnvironments).toHaveBeenCalledWith(
+ expect.objectContaining({ search: 'staging' }),
+ );
+ });
+ });
+
+ describe('when Limit Environment Scope FF is disabled', () => {
+ beforeEach(async () => {
+ await createComponentWithApollo({
+ props: { ...createProjectProps() },
+ provide: pagesFeatureFlagProvide,
+ });
+ });
+
+ it('initial query is called with the correct variables', () => {
+ expect(mockEnvironments).toHaveBeenCalledWith({ fullPath: '/namespace/project/' });
+ });
+
+ it(`does not refetch environments when search term is present`, async () => {
+ expect(mockEnvironments).toHaveBeenCalledTimes(1);
+
+ await findCiSettings().vm.$emit('search-environment-scope', 'staging');
+
+ expect(mockEnvironments).toHaveBeenCalledTimes(1);
+ });
});
});
- describe('when there isnt an environment key in queryData', () => {
+ describe("when there isn't an environment key in queryData", () => {
beforeEach(async () => {
mockVariables.mockResolvedValue(mockGroupVariables);
await createComponentWithApollo({
props: { ...createGroupProps() },
- provide: featureFlagProvide,
+ provide: pagesFeatureFlagProvide,
});
});
- it('is skipped', () => {
- expect(mockVariables).not.toHaveBeenCalled();
+ it('fetching environments is skipped', () => {
+ expect(mockEnvironments).not.toHaveBeenCalled();
});
});
});
describe('mutations', () => {
const groupProps = createGroupProps();
+ const instanceProps = createInstanceProps();
+ const projectProps = createProjectProps();
- beforeEach(async () => {
- mockVariables.mockResolvedValue(mockGroupVariables);
+ let mockMutationMap;
- await createComponentWithApollo({
- customHandlers: [[getGroupVariables, mockVariables]],
- props: groupProps,
- provide: featureFlagProvide,
- });
- });
- it.each`
- actionName | mutation | event
- ${'add'} | ${groupProps.mutationData[ADD_MUTATION_ACTION]} | ${'add-variable'}
- ${'update'} | ${groupProps.mutationData[UPDATE_MUTATION_ACTION]} | ${'update-variable'}
- ${'delete'} | ${groupProps.mutationData[DELETE_MUTATION_ACTION]} | ${'delete-variable'}
- `(
- 'calls the right mutation from propsData when user performs $actionName variable',
- async ({ event, mutation }) => {
- jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue();
-
- await findCiSettings().vm.$emit(event, newVariable);
+ describe('error handling and feedback', () => {
+ beforeEach(async () => {
+ mockVariables.mockResolvedValue(mockGroupVariables);
+ mockMutation.mockResolvedValue({ ...mockGroupVariables.data, errors: [] });
- expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({
- mutation,
- variables: {
- endpoint: mockProvide.endpoint,
- fullPath: groupProps.fullPath,
- id: convertToGraphQLId(TYPENAME_GROUP, groupProps.id),
- variable: newVariable,
+ await createComponentWithApollo({
+ customHandlers: [[getGroupVariables, mockVariables]],
+ customResolvers: {
+ Mutation: {
+ ...resolvers.Mutation,
+ addGroupVariable: mockMutation,
+ updateGroupVariable: mockMutation,
+ deleteGroupVariable: mockMutation,
+ },
},
+ props: groupProps,
+ provide: pagesFeatureFlagProvide,
});
- },
- );
+ });
- it.each`
- actionName | event
- ${'add'} | ${'add-variable'}
- ${'update'} | ${'update-variable'}
- ${'delete'} | ${'delete-variable'}
- `(
- 'throws with the specific graphql error if present when user performs $actionName variable',
- async ({ event }) => {
- const graphQLErrorMessage = 'There is a problem with this graphQL action';
- jest
- .spyOn(wrapper.vm.$apollo, 'mutate')
- .mockResolvedValue({ data: { ciVariableMutation: { errors: [graphQLErrorMessage] } } });
- await findCiSettings().vm.$emit(event, newVariable);
- await nextTick();
-
- expect(wrapper.vm.$apollo.mutate).toHaveBeenCalled();
- expect(createAlert).toHaveBeenCalledWith({ message: graphQLErrorMessage });
- },
- );
+ it.each`
+ actionName | event
+ ${'add'} | ${'add-variable'}
+ ${'update'} | ${'update-variable'}
+ ${'delete'} | ${'delete-variable'}
+ `(
+ 'throws the specific graphql error if present when user performs $actionName variable',
+ async ({ event }) => {
+ const graphQLErrorMessage = 'There is a problem with this graphQL action';
+ mockMutation.mockResolvedValue({
+ ...mockGroupVariables.data,
+ errors: [graphQLErrorMessage],
+ });
+
+ await findCiSettings().vm.$emit(event, newVariable);
+ await waitForPromises();
- it.each`
- actionName | event
- ${'add'} | ${'add-variable'}
- ${'update'} | ${'update-variable'}
- ${'delete'} | ${'delete-variable'}
+ expect(mockMutation).toHaveBeenCalled();
+ expect(createAlert).toHaveBeenCalledWith({ message: graphQLErrorMessage });
+ },
+ );
+
+ it.each`
+ actionName | event
+ ${'add'} | ${'add-variable'}
+ ${'update'} | ${'update-variable'}
+ ${'delete'} | ${'delete-variable'}
+ `(
+ 'throws generic error on failure with no graphql errors and user performs $actionName variable',
+ async ({ event }) => {
+ mockMutation.mockRejectedValue();
+
+ await findCiSettings().vm.$emit(event, newVariable);
+ await waitForPromises();
+
+ expect(mockMutation).toHaveBeenCalled();
+ expect(createAlert).toHaveBeenCalledWith({ message: genericMutationErrorText });
+ },
+ );
+
+ it.each`
+ actionName | event
+ ${'add'} | ${'add-variable'}
+ ${'update'} | ${'update-variable'}
+ ${'delete'} | ${'delete-variable'}
+ `(
+ 'displays toast message after user performs $actionName variable',
+ async ({ actionName, event }) => {
+ await findCiSettings().vm.$emit(event, newVariable);
+ await waitForPromises();
+
+ expect(mockMutation).toHaveBeenCalled();
+ expect(mockToastShow).toHaveBeenCalledWith(
+ mapMutationActionToToast[actionName](newVariable.key),
+ );
+ },
+ );
+ });
+
+ const setupMockMutations = (mockResolvedMutation) => {
+ mockAddMutation.mockResolvedValue(mockResolvedMutation);
+ mockUpdateMutation.mockResolvedValue(mockResolvedMutation);
+ mockDeleteMutation.mockResolvedValue(mockResolvedMutation);
+
+ return {
+ add: mockAddMutation,
+ update: mockUpdateMutation,
+ delete: mockDeleteMutation,
+ };
+ };
+
+ describe.each`
+ scope | mockVariablesResolvedValue | getVariablesHandler | addMutationName | updateMutationName | deleteMutationName | props
+ ${'instance'} | ${mockVariables} | ${getAdminVariables} | ${'addAdminVariable'} | ${'updateAdminVariable'} | ${'deleteAdminVariable'} | ${instanceProps}
+ ${'group'} | ${mockGroupVariables} | ${getGroupVariables} | ${'addGroupVariable'} | ${'updateGroupVariable'} | ${'deleteGroupVariable'} | ${groupProps}
+ ${'project'} | ${mockProjectVariables} | ${getProjectVariables} | ${'addProjectVariable'} | ${'updateProjectVariable'} | ${'deleteProjectVariable'} | ${projectProps}
`(
- 'throws generic error on failure with no graphql errors and user performs $actionName variable',
- async ({ event }) => {
- jest.spyOn(wrapper.vm.$apollo, 'mutate').mockImplementationOnce(() => {
- throw new Error();
+ '$scope variable mutations',
+ ({
+ addMutationName,
+ deleteMutationName,
+ getVariablesHandler,
+ mockVariablesResolvedValue,
+ updateMutationName,
+ props,
+ }) => {
+ beforeEach(async () => {
+ mockVariables.mockResolvedValue(mockVariablesResolvedValue);
+ mockMutationMap = setupMockMutations({ ...mockVariables.data, errors: [] });
+
+ await createComponentWithApollo({
+ customHandlers: [[getVariablesHandler, mockVariables]],
+ customResolvers: {
+ Mutation: {
+ ...resolvers.Mutation,
+ [addMutationName]: mockAddMutation,
+ [updateMutationName]: mockUpdateMutation,
+ [deleteMutationName]: mockDeleteMutation,
+ },
+ },
+ props,
+ provide: pagesFeatureFlagProvide,
+ });
});
- await findCiSettings().vm.$emit(event, newVariable);
- expect(wrapper.vm.$apollo.mutate).toHaveBeenCalled();
- expect(createAlert).toHaveBeenCalledWith({ message: genericMutationErrorText });
+ it.each`
+ actionName | event
+ ${'add'} | ${'add-variable'}
+ ${'update'} | ${'update-variable'}
+ ${'delete'} | ${'delete-variable'}
+ `(
+ 'calls the right mutation when user performs $actionName variable',
+ async ({ event, actionName }) => {
+ await findCiSettings().vm.$emit(event, newVariable);
+ await waitForPromises();
+
+ expect(mockMutationMap[actionName]).toHaveBeenCalledWith(
+ expect.anything(),
+ {
+ endpoint: mockProvide.endpoint,
+ fullPath: props.fullPath,
+ id: props.id,
+ variable: newVariable,
+ },
+ expect.anything(),
+ expect.anything(),
+ );
+ },
+ );
},
);
describe('without fullpath and ID props', () => {
beforeEach(async () => {
+ mockMutation.mockResolvedValue({ ...mockAdminVariables.data, errors: [] });
mockVariables.mockResolvedValue(mockAdminVariables);
await createComponentWithApollo({
customHandlers: [[getAdminVariables, mockVariables]],
+ customResolvers: {
+ Mutation: {
+ ...resolvers.Mutation,
+ addAdminVariable: mockMutation,
+ },
+ },
props: createInstanceProps(),
- provide: featureFlagProvide,
+ provide: pagesFeatureFlagProvide,
});
});
it('does not pass fullPath and ID to the mutation', async () => {
- jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue();
-
await findCiSettings().vm.$emit('add-variable', newVariable);
+ await waitForPromises();
- expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({
- mutation: wrapper.props().mutationData[ADD_MUTATION_ACTION],
- variables: {
+ expect(mockMutation).toHaveBeenCalledWith(
+ expect.anything(),
+ {
endpoint: mockProvide.endpoint,
variable: newVariable,
},
- });
+ expect.anything(),
+ expect.anything(),
+ );
});
});
});
@@ -359,10 +524,11 @@ describe('Ci Variable Shared Component', () => {
await createComponentWithApollo({
customHandlers,
props,
- provide: { ...provide, ...featureFlagProvide },
+ provide: { ...provide, ...pagesFeatureFlagProvide },
});
expect(findCiSettings().props()).toEqual({
+ areEnvironmentsLoading: false,
areScopedVariablesAvailable: wrapper.props().areScopedVariablesAvailable,
hideEnvironmentScope: defaultProps.hideEnvironmentScope,
pageInfo: defaultProps.pageInfo,
@@ -379,29 +545,29 @@ describe('Ci Variable Shared Component', () => {
describe('refetchAfterMutation', () => {
it.each`
- bool | text
- ${true} | ${'refetches the variables'}
- ${false} | ${'does not refetch the variables'}
- `('when $bool it $text', async ({ bool }) => {
+ bool | text | timesQueryCalled
+ ${true} | ${'refetches the variables'} | ${2}
+ ${false} | ${'does not refetch the variables'} | ${1}
+ `('when $bool it $text', async ({ bool, timesQueryCalled }) => {
+ mockMutation.mockResolvedValue({ ...mockAdminVariables.data, errors: [] });
+ mockVariables.mockResolvedValue(mockAdminVariables);
+
await createComponentWithApollo({
+ customHandlers: [[getAdminVariables, mockVariables]],
+ customResolvers: {
+ Mutation: {
+ ...resolvers.Mutation,
+ addAdminVariable: mockMutation,
+ },
+ },
props: { ...createInstanceProps(), refetchAfterMutation: bool },
- provide: featureFlagProvide,
+ provide: pagesFeatureFlagProvide,
});
- jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue({ data: {} });
- jest
- .spyOn(wrapper.vm.$apollo.queries.ciVariables, 'refetch')
- .mockImplementation(jest.fn());
-
await findCiSettings().vm.$emit('add-variable', newVariable);
+ await waitForPromises();
- await nextTick();
-
- if (bool) {
- expect(wrapper.vm.$apollo.queries.ciVariables.refetch).toHaveBeenCalled();
- } else {
- expect(wrapper.vm.$apollo.queries.ciVariables.refetch).not.toHaveBeenCalled();
- }
+ expect(mockVariables).toHaveBeenCalledTimes(timesQueryCalled);
});
});
@@ -409,7 +575,7 @@ describe('Ci Variable Shared Component', () => {
describe('queryData', () => {
let error;
- beforeEach(async () => {
+ beforeEach(() => {
mockVariables.mockResolvedValue(mockGroupVariables);
});
@@ -418,7 +584,7 @@ describe('Ci Variable Shared Component', () => {
await createComponentWithApollo({
customHandlers: [[getGroupVariables, mockVariables]],
props: { ...createGroupProps() },
- provide: featureFlagProvide,
+ provide: pagesFeatureFlagProvide,
});
} catch (e) {
error = e;
@@ -428,26 +594,21 @@ describe('Ci Variable Shared Component', () => {
}
});
- it('will not mount component with wrong data', async () => {
- try {
- await createComponentWithApollo({
- customHandlers: [[getGroupVariables, mockVariables]],
- props: { ...createGroupProps(), queryData: { wrongKey: {} } },
- provide: featureFlagProvide,
- });
- } catch (e) {
- error = e;
- } finally {
- expect(wrapper.exists()).toBe(false);
- expect(error.toString()).toContain('custom validator check failed for prop');
- }
+ it('report custom validator error on wrong data', () => {
+ expect(() =>
+ assertProps(
+ ciVariableShared,
+ { ...defaultProps, ...createGroupProps(), queryData: { wrongKey: {} } },
+ { provide: mockProvide },
+ ),
+ ).toThrow('custom validator check failed for prop');
});
});
describe('mutationData', () => {
let error;
- beforeEach(async () => {
+ beforeEach(() => {
mockVariables.mockResolvedValue(mockGroupVariables);
});
@@ -455,7 +616,7 @@ describe('Ci Variable Shared Component', () => {
try {
await createComponentWithApollo({
props: { ...createGroupProps() },
- provide: featureFlagProvide,
+ provide: pagesFeatureFlagProvide,
});
} catch (e) {
error = e;
@@ -465,18 +626,14 @@ describe('Ci Variable Shared Component', () => {
}
});
- it('will not mount component with wrong data', async () => {
- try {
- await createComponentWithApollo({
- props: { ...createGroupProps(), mutationData: { wrongKey: {} } },
- provide: featureFlagProvide,
- });
- } catch (e) {
- error = e;
- } finally {
- expect(wrapper.exists()).toBe(false);
- expect(error.toString()).toContain('custom validator check failed for prop');
- }
+ it('report custom validator error on wrong data', () => {
+ expect(() =>
+ assertProps(
+ ciVariableShared,
+ { ...defaultProps, ...createGroupProps(), mutationData: { wrongKey: {} } },
+ { provide: { ...mockProvide, ...pagesFeatureFlagProvide } },
+ ),
+ ).toThrow('custom validator check failed for prop');
});
});
});
diff --git a/spec/frontend/ci/ci_variable_list/components/ci_variable_table_spec.js b/spec/frontend/ci/ci_variable_list/components/ci_variable_table_spec.js
index 2ef789e89c3..0b28cb06cec 100644
--- a/spec/frontend/ci/ci_variable_list/components/ci_variable_table_spec.js
+++ b/spec/frontend/ci/ci_variable_list/components/ci_variable_table_spec.js
@@ -82,11 +82,11 @@ describe('Ci variable table', () => {
expect(findRevealButton().exists()).toBe(true);
});
- it('displays the correct amount of variables', async () => {
+ it('displays the correct amount of variables', () => {
expect(wrapper.findAll('.js-ci-variable-row')).toHaveLength(defaultProps.variables.length);
});
- it('displays the correct variable options', async () => {
+ it('displays the correct variable options', () => {
expect(findOptionsValues(0)).toBe('Protected, Expanded');
expect(findOptionsValues(1)).toBe('Masked');
});
diff --git a/spec/frontend/ci/ci_variable_list/mocks.js b/spec/frontend/ci/ci_variable_list/mocks.js
index 4da4f53f69f..f9450803308 100644
--- a/spec/frontend/ci/ci_variable_list/mocks.js
+++ b/spec/frontend/ci/ci_variable_list/mocks.js
@@ -56,6 +56,11 @@ export const mockVariablesWithScopes = (kind) =>
return { ...variable, environmentScope: '*' };
});
+export const mockVariablesWithUniqueScopes = (kind) =>
+ mockVariables(kind).map((variable) => {
+ return { ...variable, environmentScope: variable.value };
+ });
+
const createDefaultVars = ({ withScope = true, kind } = {}) => {
let base = mockVariables(kind);
diff --git a/spec/frontend/ci/pipeline_editor/components/commit/commit_form_spec.js b/spec/frontend/ci/pipeline_editor/components/commit/commit_form_spec.js
index b2dfa900b1d..03f346181e4 100644
--- a/spec/frontend/ci/pipeline_editor/components/commit/commit_form_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/commit/commit_form_spec.js
@@ -34,7 +34,7 @@ describe('Pipeline Editor | Commit Form', () => {
const findCancelBtn = () => wrapper.find('[type="reset"]');
describe('when the form is displayed', () => {
- beforeEach(async () => {
+ beforeEach(() => {
createComponent();
});
@@ -57,7 +57,7 @@ describe('Pipeline Editor | Commit Form', () => {
});
describe('when buttons are clicked', () => {
- beforeEach(async () => {
+ beforeEach(() => {
createComponent({}, mount);
});
diff --git a/spec/frontend/ci/pipeline_editor/components/drawer/cards/pipeline_config_reference_card_spec.js b/spec/frontend/ci/pipeline_editor/components/drawer/cards/pipeline_config_reference_card_spec.js
index 5399924b462..0296ab5a65c 100644
--- a/spec/frontend/ci/pipeline_editor/components/drawer/cards/pipeline_config_reference_card_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/drawer/cards/pipeline_config_reference_card_spec.js
@@ -68,7 +68,7 @@ describe('Pipeline config reference card', () => {
});
};
- it('tracks help page links', async () => {
+ it('tracks help page links', () => {
const {
CI_EXAMPLES_LINK,
CI_HELP_LINK,
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 560e8840d57..2861fc35342 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
@@ -58,7 +58,7 @@ describe('CI Editor Header', () => {
expect(findLinkBtn().props('icon')).toBe('external-link');
});
- it('tracks the click on the browse button', async () => {
+ it('tracks the click on the browse button', () => {
const { browseTemplates } = pipelineEditorTrackingOptions.actions;
testTracker(findLinkBtn(), browseTemplates);
@@ -91,7 +91,7 @@ describe('CI Editor Header', () => {
expect(wrapper.emitted('open-drawer')).toHaveLength(1);
});
- it('tracks open help drawer action', async () => {
+ it('tracks open help drawer action', () => {
const { actions } = pipelineEditorTrackingOptions;
testTracker(findHelpBtn(), actions.openHelpDrawer);
diff --git a/spec/frontend/ci/pipeline_editor/components/file-nav/branch_switcher_spec.js b/spec/frontend/ci/pipeline_editor/components/file-nav/branch_switcher_spec.js
index bf14f4c4cd6..3a99949413b 100644
--- a/spec/frontend/ci/pipeline_editor/components/file-nav/branch_switcher_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/file-nav/branch_switcher_spec.js
@@ -288,7 +288,7 @@ describe('Pipeline editor branch switcher', () => {
});
describe('with a search term', () => {
- beforeEach(async () => {
+ beforeEach(() => {
mockAvailableBranchQuery.mockResolvedValue(mockSearchBranches);
});
diff --git a/spec/frontend/ci/pipeline_editor/components/file-tree/container_spec.js b/spec/frontend/ci/pipeline_editor/components/file-tree/container_spec.js
index 306dd78d395..f2effcb2966 100644
--- a/spec/frontend/ci/pipeline_editor/components/file-tree/container_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/file-tree/container_spec.js
@@ -60,11 +60,11 @@ describe('Pipeline editor file nav', () => {
expect(fileTreeItems().exists()).toBe(false);
});
- it('renders alert tip', async () => {
+ it('renders alert tip', () => {
expect(findTip().exists()).toBe(true);
});
- it('renders learn more link', async () => {
+ it('renders learn more link', () => {
expect(findTip().props('secondaryButtonLink')).toBe(mockIncludesHelpPagePath);
});
@@ -87,7 +87,7 @@ describe('Pipeline editor file nav', () => {
});
});
- it('does not render alert tip', async () => {
+ it('does not render alert tip', () => {
expect(findTip().exists()).toBe(false);
});
});
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 7bf955012c7..b8526e569ec 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
@@ -96,7 +96,7 @@ describe('Pipeline Status', () => {
await waitForPromises();
});
- it('should emit an error event when query fails', async () => {
+ it('should emit an error event when query fails', () => {
expect(wrapper.emitted('showError')).toHaveLength(1);
expect(wrapper.emitted('showError')[0]).toEqual([
{
diff --git a/spec/frontend/ci/pipeline_editor/components/header/pipeline_status_spec.js b/spec/frontend/ci/pipeline_editor/components/header/pipeline_status_spec.js
index 3faa2890254..8ca88472bf1 100644
--- a/spec/frontend/ci/pipeline_editor/components/header/pipeline_status_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/header/pipeline_status_spec.js
@@ -77,7 +77,7 @@ describe('Pipeline Status', () => {
await waitForPromises();
});
- it('query is called with correct variables', async () => {
+ it('query is called with correct variables', () => {
expect(mockPipelineQuery).toHaveBeenCalledTimes(1);
expect(mockPipelineQuery).toHaveBeenCalledWith({
fullPath: mockProjectFullPath,
diff --git a/spec/frontend/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/artifacts_and_cache_item_spec.js b/spec/frontend/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/artifacts_and_cache_item_spec.js
new file mode 100644
index 00000000000..9046be4a45e
--- /dev/null
+++ b/spec/frontend/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/artifacts_and_cache_item_spec.js
@@ -0,0 +1,127 @@
+import ArtifactsAndCacheItem from '~/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/artifacts_and_cache_item.vue';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { JOB_TEMPLATE } from '~/ci/pipeline_editor/components/job_assistant_drawer/constants';
+
+describe('Artifacts and cache item', () => {
+ let wrapper;
+
+ const findArtifactsPathsInputByIndex = (index) =>
+ wrapper.findByTestId(`artifacts-paths-input-${index}`);
+ const findArtifactsExcludeInputByIndex = (index) =>
+ wrapper.findByTestId(`artifacts-exclude-input-${index}`);
+ const findCachePathsInputByIndex = (index) => wrapper.findByTestId(`cache-paths-input-${index}`);
+ const findCacheKeyInput = () => wrapper.findByTestId('cache-key-input');
+ const findDeleteArtifactsPathsButtonByIndex = (index) =>
+ wrapper.findByTestId(`delete-artifacts-paths-button-${index}`);
+ const findDeleteArtifactsExcludeButtonByIndex = (index) =>
+ wrapper.findByTestId(`delete-artifacts-exclude-button-${index}`);
+ const findDeleteCachePathsButtonByIndex = (index) =>
+ wrapper.findByTestId(`delete-cache-paths-button-${index}`);
+ const findAddArtifactsPathsButton = () => wrapper.findByTestId('add-artifacts-paths-button');
+ const findAddArtifactsExcludeButton = () => wrapper.findByTestId('add-artifacts-exclude-button');
+ const findAddCachePathsButton = () => wrapper.findByTestId('add-cache-paths-button');
+
+ const dummyArtifactsPath = 'dummyArtifactsPath';
+ const dummyArtifactsExclude = 'dummyArtifactsExclude';
+ const dummyCachePath = 'dummyCachePath';
+ const dummyCacheKey = 'dummyCacheKey';
+
+ const createComponent = ({ job = JSON.parse(JSON.stringify(JOB_TEMPLATE)) } = {}) => {
+ wrapper = shallowMountExtended(ArtifactsAndCacheItem, {
+ propsData: {
+ job,
+ },
+ });
+ };
+
+ it('should emit update job event when filling inputs', () => {
+ createComponent();
+
+ expect(wrapper.emitted('update-job')).toBeUndefined();
+
+ findArtifactsPathsInputByIndex(0).vm.$emit('input', dummyArtifactsPath);
+
+ expect(wrapper.emitted('update-job')).toHaveLength(1);
+ expect(wrapper.emitted('update-job')[0]).toStrictEqual([
+ 'artifacts.paths[0]',
+ dummyArtifactsPath,
+ ]);
+
+ findArtifactsExcludeInputByIndex(0).vm.$emit('input', dummyArtifactsExclude);
+
+ expect(wrapper.emitted('update-job')).toHaveLength(2);
+ expect(wrapper.emitted('update-job')[1]).toStrictEqual([
+ 'artifacts.exclude[0]',
+ dummyArtifactsExclude,
+ ]);
+
+ findCachePathsInputByIndex(0).vm.$emit('input', dummyCachePath);
+
+ expect(wrapper.emitted('update-job')).toHaveLength(3);
+ expect(wrapper.emitted('update-job')[2]).toStrictEqual(['cache.paths[0]', dummyCachePath]);
+
+ findCacheKeyInput().vm.$emit('input', dummyCacheKey);
+
+ expect(wrapper.emitted('update-job')).toHaveLength(4);
+ expect(wrapper.emitted('update-job')[3]).toStrictEqual(['cache.key', dummyCacheKey]);
+ });
+
+ it('should emit update job event when click add item button', () => {
+ createComponent();
+
+ findAddArtifactsPathsButton().vm.$emit('click');
+
+ expect(wrapper.emitted('update-job')).toHaveLength(1);
+ expect(wrapper.emitted('update-job')[0]).toStrictEqual(['artifacts.paths[1]', '']);
+
+ findAddArtifactsExcludeButton().vm.$emit('click');
+
+ expect(wrapper.emitted('update-job')).toHaveLength(2);
+ expect(wrapper.emitted('update-job')[1]).toStrictEqual(['artifacts.exclude[1]', '']);
+
+ findAddCachePathsButton().vm.$emit('click');
+
+ expect(wrapper.emitted('update-job')).toHaveLength(3);
+ expect(wrapper.emitted('update-job')[2]).toStrictEqual(['cache.paths[1]', '']);
+ });
+
+ it('should emit update job event when click delete item button', () => {
+ createComponent({
+ job: {
+ artifacts: {
+ paths: ['0', '1'],
+ exclude: ['0', '1'],
+ },
+ cache: {
+ paths: ['0', '1'],
+ key: '',
+ },
+ },
+ });
+
+ findDeleteArtifactsPathsButtonByIndex(0).vm.$emit('click');
+
+ expect(wrapper.emitted('update-job')).toHaveLength(1);
+ expect(wrapper.emitted('update-job')[0]).toStrictEqual(['artifacts.paths[0]']);
+
+ findDeleteArtifactsExcludeButtonByIndex(0).vm.$emit('click');
+
+ expect(wrapper.emitted('update-job')).toHaveLength(2);
+ expect(wrapper.emitted('update-job')[1]).toStrictEqual(['artifacts.exclude[0]']);
+
+ findDeleteCachePathsButtonByIndex(0).vm.$emit('click');
+
+ expect(wrapper.emitted('update-job')).toHaveLength(3);
+ expect(wrapper.emitted('update-job')[2]).toStrictEqual(['cache.paths[0]']);
+ });
+
+ it('should not emit update job event when click the only one delete item button', () => {
+ createComponent();
+
+ findDeleteArtifactsPathsButtonByIndex(0).vm.$emit('click');
+ findDeleteArtifactsExcludeButtonByIndex(0).vm.$emit('click');
+ findDeleteCachePathsButtonByIndex(0).vm.$emit('click');
+
+ expect(wrapper.emitted('update-job')).toBeUndefined();
+ });
+});
diff --git a/spec/frontend/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/job_setup_item_spec.js b/spec/frontend/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/job_setup_item_spec.js
index eaad0dae90d..373fb1b70c7 100644
--- a/spec/frontend/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/job_setup_item_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/job_setup_item_spec.js
@@ -1,4 +1,3 @@
-import createStore from '~/ci/pipeline_editor/store';
import JobSetupItem from '~/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/job_setup_item.vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { JOB_TEMPLATE } from '~/ci/pipeline_editor/components/job_assistant_drawer/constants';
@@ -18,8 +17,8 @@ describe('Job setup item', () => {
const createComponent = () => {
wrapper = shallowMountExtended(JobSetupItem, {
- store: createStore(),
propsData: {
+ availableStages: ['.pre', dummyJobStage, '.post'],
tagOptions: [
{ id: 'tag1', name: 'tag1' },
{ id: 'tag2', name: 'tag2' },
diff --git a/spec/frontend/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/rules_item_spec.js b/spec/frontend/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/rules_item_spec.js
new file mode 100644
index 00000000000..659ccb25996
--- /dev/null
+++ b/spec/frontend/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/rules_item_spec.js
@@ -0,0 +1,70 @@
+import RulesItem from '~/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/rules_item.vue';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import {
+ JOB_TEMPLATE,
+ JOB_RULES_WHEN,
+ JOB_RULES_START_IN,
+} from '~/ci/pipeline_editor/components/job_assistant_drawer/constants';
+
+describe('Rules item', () => {
+ let wrapper;
+
+ const findRulesWhenSelect = () => wrapper.findByTestId('rules-when-select');
+ const findRulesStartInNumberInput = () => wrapper.findByTestId('rules-start-in-number-input');
+ const findRulesStartInUnitSelect = () => wrapper.findByTestId('rules-start-in-unit-select');
+ const findRulesAllowFailureCheckBox = () => wrapper.findByTestId('rules-allow-failure-checkbox');
+
+ const dummyRulesWhen = JOB_RULES_WHEN.delayed.value;
+ const dummyRulesStartInNumber = 2;
+ const dummyRulesStartInUnit = JOB_RULES_START_IN.week.value;
+ const dummyRulesAllowFailure = true;
+
+ const createComponent = () => {
+ wrapper = shallowMountExtended(RulesItem, {
+ propsData: {
+ isStartValid: true,
+ job: JSON.parse(JSON.stringify(JOB_TEMPLATE)),
+ },
+ });
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('should emit update job event when filling inputs', () => {
+ expect(wrapper.emitted('update-job')).toBeUndefined();
+
+ findRulesWhenSelect().vm.$emit('input', dummyRulesWhen);
+
+ expect(wrapper.emitted('update-job')).toHaveLength(1);
+ expect(wrapper.emitted('update-job')[0]).toEqual([
+ 'rules[0].when',
+ JOB_RULES_WHEN.delayed.value,
+ ]);
+
+ findRulesStartInNumberInput().vm.$emit('input', dummyRulesStartInNumber);
+
+ expect(wrapper.emitted('update-job')).toHaveLength(2);
+ expect(wrapper.emitted('update-job')[1]).toEqual([
+ 'rules[0].start_in',
+ `2 ${JOB_RULES_START_IN.second.value}s`,
+ ]);
+
+ findRulesStartInUnitSelect().vm.$emit('input', dummyRulesStartInUnit);
+
+ expect(wrapper.emitted('update-job')).toHaveLength(3);
+ expect(wrapper.emitted('update-job')[2]).toEqual([
+ 'rules[0].start_in',
+ `2 ${dummyRulesStartInUnit}s`,
+ ]);
+
+ findRulesAllowFailureCheckBox().vm.$emit('input', dummyRulesAllowFailure);
+
+ expect(wrapper.emitted('update-job')).toHaveLength(4);
+ expect(wrapper.emitted('update-job')[3]).toEqual([
+ 'rules[0].allow_failure',
+ dummyRulesAllowFailure,
+ ]);
+ });
+});
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
index b293805d653..08aa7e3a11a 100644
--- 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
@@ -5,13 +5,16 @@ import { stringify } from 'yaml';
import JobAssistantDrawer from '~/ci/pipeline_editor/components/job_assistant_drawer/job_assistant_drawer.vue';
import JobSetupItem from '~/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/job_setup_item.vue';
import ImageItem from '~/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/image_item.vue';
-import getAllRunners from '~/ci/runner/graphql/list/all_runners.query.graphql';
+import ArtifactsAndCacheItem from '~/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/artifacts_and_cache_item.vue';
+import RulesItem from '~/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/rules_item.vue';
+import { JOB_RULES_WHEN } from '~/ci/pipeline_editor/components/job_assistant_drawer/constants';
+import getRunnerTags from '~/ci/pipeline_editor/graphql/queries/runner_tags.query.graphql';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import createStore from '~/ci/pipeline_editor/store';
-import { mockAllRunnersQueryResponse } from 'jest/ci/pipeline_editor/mock_data';
+
import { mountExtended } from 'helpers/vue_test_utils_helper';
import eventHub, { SCROLL_EDITOR_TO_BOTTOM } from '~/ci/pipeline_editor/event_hub';
+import { mockRunnersTagsQueryResponse, mockLintResponse, mockCiYml } from '../../mock_data';
Vue.use(VueApollo);
@@ -23,22 +26,32 @@ describe('Job assistant drawer', () => {
const dummyJobScript = 'b';
const dummyImageName = 'c';
const dummyImageEntrypoint = 'd';
+ const dummyArtifactsPath = 'e';
+ const dummyArtifactsExclude = 'f';
+ const dummyCachePath = 'g';
+ const dummyCacheKey = 'h';
+ const dummyRulesWhen = JOB_RULES_WHEN.delayed.value;
+ const dummyRulesStartIn = '1 second';
+ const dummyRulesAllowFailure = true;
const findDrawer = () => wrapper.findComponent(GlDrawer);
const findJobSetupItem = () => wrapper.findComponent(JobSetupItem);
const findImageItem = () => wrapper.findComponent(ImageItem);
+ const findArtifactsAndCacheItem = () => wrapper.findComponent(ArtifactsAndCacheItem);
+ const findRulesItem = () => wrapper.findComponent(RulesItem);
const findConfirmButton = () => wrapper.findByTestId('confirm-button');
const findCancelButton = () => wrapper.findByTestId('cancel-button');
const createComponent = () => {
mockApollo = createMockApollo([
- [getAllRunners, jest.fn().mockResolvedValue(mockAllRunnersQueryResponse)],
+ [getRunnerTags, jest.fn().mockResolvedValue(mockRunnersTagsQueryResponse)],
]);
wrapper = mountExtended(JobAssistantDrawer, {
- store: createStore(),
propsData: {
+ ciConfigData: mockLintResponse,
+ ciFileContent: mockCiYml,
isVisible: true,
},
apolloProvider: mockApollo,
@@ -54,10 +67,27 @@ describe('Job assistant drawer', () => {
expect(findJobSetupItem().exists()).toBe(true);
});
+ it('job setup item should have tag options', () => {
+ expect(findJobSetupItem().props('tagOptions')).toEqual([
+ { id: 'tag1', name: 'tag1' },
+ { id: 'tag2', name: 'tag2' },
+ { id: 'tag3', name: 'tag3' },
+ { id: 'tag4', name: 'tag4' },
+ ]);
+ });
+
it('should contain image accordion', () => {
expect(findImageItem().exists()).toBe(true);
});
+ it('should contain artifacts and cache item accordion', () => {
+ expect(findArtifactsAndCacheItem().exists()).toBe(true);
+ });
+
+ it('should contain rules accordion', () => {
+ expect(findRulesItem().exists()).toBe(true);
+ });
+
it('should emit close job assistant drawer event when closing the drawer', () => {
expect(wrapper.emitted('close-job-assistant-drawer')).toBeUndefined();
@@ -74,8 +104,7 @@ describe('Job assistant drawer', () => {
expect(wrapper.emitted('close-job-assistant-drawer')).toHaveLength(1);
});
- it('trigger validate if job name is empty', async () => {
- const updateCiConfigSpy = jest.spyOn(wrapper.vm, 'updateCiConfig');
+ it('should block submit if job name is empty', async () => {
findJobSetupItem().vm.$emit('update-job', 'script', 'b');
findConfirmButton().trigger('click');
@@ -83,7 +112,17 @@ describe('Job assistant drawer', () => {
expect(findJobSetupItem().props('isNameValid')).toBe(false);
expect(findJobSetupItem().props('isScriptValid')).toBe(true);
- expect(updateCiConfigSpy).toHaveBeenCalledTimes(0);
+ expect(wrapper.emitted('updateCiConfig')).toBeUndefined();
+ });
+
+ it('should block submit if rules when is delayed and start in is out of range', async () => {
+ findRulesItem().vm.$emit('update-job', 'rules[0].when', JOB_RULES_WHEN.delayed.value);
+ findRulesItem().vm.$emit('update-job', 'rules[0].start_in', '2 weeks');
+ findConfirmButton().trigger('click');
+
+ await nextTick();
+
+ expect(wrapper.emitted('updateCiConfig')).toBeUndefined();
});
describe('when enter valid input', () => {
@@ -92,10 +131,24 @@ describe('Job assistant drawer', () => {
findJobSetupItem().vm.$emit('update-job', 'script', dummyJobScript);
findImageItem().vm.$emit('update-job', 'image.name', dummyImageName);
findImageItem().vm.$emit('update-job', 'image.entrypoint', [dummyImageEntrypoint]);
+ findArtifactsAndCacheItem().vm.$emit('update-job', 'artifacts.paths', [dummyArtifactsPath]);
+ findArtifactsAndCacheItem().vm.$emit('update-job', 'artifacts.exclude', [
+ dummyArtifactsExclude,
+ ]);
+ findArtifactsAndCacheItem().vm.$emit('update-job', 'cache.paths', [dummyCachePath]);
+ findArtifactsAndCacheItem().vm.$emit('update-job', 'cache.key', dummyCacheKey);
+ findRulesItem().vm.$emit('update-job', 'rules[0].allow_failure', dummyRulesAllowFailure);
+ findRulesItem().vm.$emit('update-job', 'rules[0].when', dummyRulesWhen);
+ findRulesItem().vm.$emit('update-job', 'rules[0].start_in', dummyRulesStartIn);
});
it('passes correct prop to accordions', () => {
- const accordions = [findJobSetupItem(), findImageItem()];
+ const accordions = [
+ findJobSetupItem(),
+ findImageItem(),
+ findArtifactsAndCacheItem(),
+ findRulesItem(),
+ ];
accordions.forEach((accordion) => {
expect(accordion.props('job')).toMatchObject({
name: dummyJobName,
@@ -104,6 +157,21 @@ describe('Job assistant drawer', () => {
name: dummyImageName,
entrypoint: [dummyImageEntrypoint],
},
+ artifacts: {
+ paths: [dummyArtifactsPath],
+ exclude: [dummyArtifactsExclude],
+ },
+ cache: {
+ paths: [dummyCachePath],
+ key: dummyCacheKey,
+ },
+ rules: [
+ {
+ allow_failure: dummyRulesAllowFailure,
+ when: dummyRulesWhen,
+ start_in: dummyRulesStartIn,
+ },
+ ],
});
});
});
@@ -129,19 +197,60 @@ describe('Job assistant drawer', () => {
expect(findJobSetupItem().props('job')).toMatchObject({ name: '', script: '' });
});
- it('should update correct ci content when click add button', () => {
- const updateCiConfigSpy = jest.spyOn(wrapper.vm, 'updateCiConfig');
+ it('should omit keys with default value when click add button', () => {
+ findRulesItem().vm.$emit('update-job', 'rules[0].allow_failure', false);
+ findRulesItem().vm.$emit('update-job', 'rules[0].when', JOB_RULES_WHEN.onSuccess.value);
+ findRulesItem().vm.$emit('update-job', 'rules[0].start_in', dummyRulesStartIn);
+ findConfirmButton().trigger('click');
+
+ expect(wrapper.emitted('updateCiConfig')).toStrictEqual([
+ [
+ `${wrapper.props('ciFileContent')}\n${stringify({
+ [dummyJobName]: {
+ script: dummyJobScript,
+ image: { name: dummyImageName, entrypoint: [dummyImageEntrypoint] },
+ artifacts: {
+ paths: [dummyArtifactsPath],
+ exclude: [dummyArtifactsExclude],
+ },
+ cache: {
+ paths: [dummyCachePath],
+ key: dummyCacheKey,
+ },
+ },
+ })}`,
+ ],
+ ]);
+ });
+ it('should update correct ci content when click add button', () => {
findConfirmButton().trigger('click');
- expect(updateCiConfigSpy).toHaveBeenCalledWith(
- `\n${stringify({
- [dummyJobName]: {
- script: dummyJobScript,
- image: { name: dummyImageName, entrypoint: [dummyImageEntrypoint] },
- },
- })}`,
- );
+ expect(wrapper.emitted('updateCiConfig')).toStrictEqual([
+ [
+ `${wrapper.props('ciFileContent')}\n${stringify({
+ [dummyJobName]: {
+ script: dummyJobScript,
+ image: { name: dummyImageName, entrypoint: [dummyImageEntrypoint] },
+ artifacts: {
+ paths: [dummyArtifactsPath],
+ exclude: [dummyArtifactsExclude],
+ },
+ cache: {
+ paths: [dummyCachePath],
+ key: dummyCacheKey,
+ },
+ rules: [
+ {
+ allow_failure: dummyRulesAllowFailure,
+ when: dummyRulesWhen,
+ start_in: dummyRulesStartIn,
+ },
+ ],
+ },
+ })}`,
+ ],
+ ]);
});
it('should emit scroll editor to button event when click add button', () => {
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 52a543c7686..cbdf01105c7 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
@@ -314,13 +314,13 @@ describe('Pipeline editor tabs component', () => {
createComponent();
});
- it('shows walkthrough popover', async () => {
+ it('shows walkthrough popover', () => {
expect(findWalkthroughPopover().exists()).toBe(true);
});
});
describe('when isNewCiConfigFile prop is false', () => {
- it('does not show walkthrough popover', async () => {
+ it('does not show walkthrough popover', () => {
createComponent({ props: { isNewCiConfigFile: false } });
expect(findWalkthroughPopover().exists()).toBe(false);
});
diff --git a/spec/frontend/ci/pipeline_editor/components/popovers/file_tree_popover_spec.js b/spec/frontend/ci/pipeline_editor/components/popovers/file_tree_popover_spec.js
index a9aabb103f2..3d84f06967a 100644
--- a/spec/frontend/ci/pipeline_editor/components/popovers/file_tree_popover_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/popovers/file_tree_popover_spec.js
@@ -25,7 +25,7 @@ describe('FileTreePopover component', () => {
});
describe('default', () => {
- beforeEach(async () => {
+ beforeEach(() => {
createComponent({ stubs: { GlSprintf } });
});
@@ -45,7 +45,7 @@ describe('FileTreePopover component', () => {
});
describe('when popover has already been dismissed before', () => {
- it('does not render popover', async () => {
+ it('does not render popover', () => {
localStorage.setItem(FILE_TREE_POPOVER_DISMISSED_KEY, 'true');
createComponent();
diff --git a/spec/frontend/ci/pipeline_editor/components/popovers/validate_pipeline_popover_spec.js b/spec/frontend/ci/pipeline_editor/components/popovers/validate_pipeline_popover_spec.js
index 23f9c7a87ee..18eec48ad83 100644
--- a/spec/frontend/ci/pipeline_editor/components/popovers/validate_pipeline_popover_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/popovers/validate_pipeline_popover_spec.js
@@ -20,7 +20,7 @@ describe('ValidatePopover component', () => {
const findFeedbackLink = () => wrapper.findByTestId('feedback-link');
describe('template', () => {
- beforeEach(async () => {
+ beforeEach(() => {
createComponent({
stubs: { GlLink, GlSprintf },
});
diff --git a/spec/frontend/ci/pipeline_editor/components/popovers/walkthrough_popover_spec.js b/spec/frontend/ci/pipeline_editor/components/popovers/walkthrough_popover_spec.js
index 186fd803d47..37339b1c422 100644
--- a/spec/frontend/ci/pipeline_editor/components/popovers/walkthrough_popover_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/popovers/walkthrough_popover_spec.js
@@ -18,7 +18,7 @@ describe('WalkthroughPopover component', () => {
await wrapper.findByTestId('ctaBtn').trigger('click');
});
- it('emits "walkthrough-popover-cta-clicked" event', async () => {
+ it('emits "walkthrough-popover-cta-clicked" event', () => {
expect(wrapper.emitted()['walkthrough-popover-cta-clicked']).toHaveLength(1);
});
});
diff --git a/spec/frontend/ci/pipeline_editor/components/ui/editor_tab_spec.js b/spec/frontend/ci/pipeline_editor/components/ui/editor_tab_spec.js
index a4e7abba7b0..f02b1f5efbc 100644
--- a/spec/frontend/ci/pipeline_editor/components/ui/editor_tab_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/ui/editor_tab_spec.js
@@ -64,7 +64,7 @@ describe('~/ci/pipeline_editor/components/ui/editor_tab.vue', () => {
mockChildMounted = jest.fn();
});
- it('tabs are mounted lazily', async () => {
+ it('tabs are mounted lazily', () => {
createMockedWrapper();
expect(mockChildMounted).toHaveBeenCalledTimes(0);
@@ -192,7 +192,7 @@ describe('~/ci/pipeline_editor/components/ui/editor_tab.vue', () => {
createMockedWrapper();
});
- it('renders correct number of badges', async () => {
+ it('renders correct number of badges', () => {
expect(findBadges()).toHaveLength(1);
expect(findBadges().at(0).text()).toBe('NEW');
});
diff --git a/spec/frontend/ci/pipeline_editor/graphql/resolvers_spec.js b/spec/frontend/ci/pipeline_editor/graphql/resolvers_spec.js
index 6a6cc3a14de..893f6775ac5 100644
--- a/spec/frontend/ci/pipeline_editor/graphql/resolvers_spec.js
+++ b/spec/frontend/ci/pipeline_editor/graphql/resolvers_spec.js
@@ -34,7 +34,7 @@ describe('~/ci/pipeline_editor/graphql/resolvers', () => {
});
/* eslint-disable no-underscore-dangle */
- it('lint data has correct type names', async () => {
+ it('lint data has correct type names', () => {
expect(result.__typename).toBe('CiLintContent');
expect(result.jobs[0].__typename).toBe('CiLintJob');
diff --git a/spec/frontend/ci/pipeline_editor/mock_data.js b/spec/frontend/ci/pipeline_editor/mock_data.js
index ecfc477184b..865dd34fbfe 100644
--- a/spec/frontend/ci/pipeline_editor/mock_data.js
+++ b/spec/frontend/ci/pipeline_editor/mock_data.js
@@ -583,86 +583,31 @@ export const mockCommitCreateResponse = {
},
};
-export const mockAllRunnersQueryResponse = {
+export const mockRunnersTagsQueryResponse = {
data: {
runners: {
nodes: [
{
id: 'gid://gitlab/Ci::Runner/1',
- description: 'test',
- runnerType: 'PROJECT_TYPE',
- shortSha: 'DdTYMQGS',
- version: '15.6.1',
- ipAddress: '127.0.0.1',
- active: true,
- locked: true,
- jobCount: 0,
- jobExecutionStatus: 'IDLE',
- tagList: ['tag1', 'tag2', 'tag3'],
- createdAt: '2022-11-29T09:37:43Z',
- contactedAt: null,
- status: 'NEVER_CONTACTED',
- userPermissions: {
- updateRunner: true,
- deleteRunner: true,
- __typename: 'RunnerPermissions',
- },
- groups: null,
- ownerProject: {
- id: 'gid://gitlab/Project/1',
- name: '123',
- nameWithNamespace: 'Administrator / 123',
- webUrl: 'http://127.0.0.1:3000/root/test',
- __typename: 'Project',
- },
+ tagList: ['tag1', 'tag2'],
__typename: 'CiRunner',
- upgradeStatus: 'NOT_AVAILABLE',
- adminUrl: 'http://127.0.0.1:3000/admin/runners/1',
- editAdminUrl: 'http://127.0.0.1:3000/admin/runners/1/edit',
},
{
id: 'gid://gitlab/Ci::Runner/2',
- description: 'test',
- runnerType: 'PROJECT_TYPE',
- shortSha: 'DdTYMQGA',
- version: '15.6.1',
- ipAddress: '127.0.0.1',
- active: true,
- locked: true,
- jobCount: 0,
- jobExecutionStatus: 'IDLE',
- tagList: ['tag3', 'tag4'],
- createdAt: '2022-11-29T09:37:43Z',
- contactedAt: null,
- status: 'NEVER_CONTACTED',
- userPermissions: {
- updateRunner: true,
- deleteRunner: true,
- __typename: 'RunnerPermissions',
- },
- groups: null,
- ownerProject: {
- id: 'gid://gitlab/Project/1',
- name: '123',
- nameWithNamespace: 'Administrator / 123',
- webUrl: 'http://127.0.0.1:3000/root/test',
- __typename: 'Project',
- },
+ tagList: ['tag2', 'tag3'],
+ __typename: 'CiRunner',
+ },
+ {
+ id: 'gid://gitlab/Ci::Runner/3',
+ tagList: ['tag2', 'tag4'],
+ __typename: 'CiRunner',
+ },
+ {
+ id: 'gid://gitlab/Ci::Runner/4',
+ tagList: [],
__typename: 'CiRunner',
- upgradeStatus: 'NOT_AVAILABLE',
- adminUrl: 'http://127.0.0.1:3000/admin/runners/2',
- editAdminUrl: 'http://127.0.0.1:3000/admin/runners/2/edit',
},
],
- pageInfo: {
- hasNextPage: false,
- hasPreviousPage: false,
- startCursor:
- 'eyJjcmVhdGVkX2F0IjoiMjAyMi0xMS0yOSAwOTozNzo0My40OTEwNTEwMDAgKzAwMDAiLCJpZCI6IjIifQ',
- endCursor:
- 'eyJjcmVhdGVkX2F0IjoiMjAyMi0xMS0yOSAwOTozNzo0My40OTEwNTEwMDAgKzAwMDAiLCJpZCI6IjIifQ',
- __typename: 'PageInfo',
- },
__typename: 'CiRunnerConnection',
},
},
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 7a13bfbd1ab..8bac46a3e9c 100644
--- a/spec/frontend/ci/pipeline_editor/pipeline_editor_app_spec.js
+++ b/spec/frontend/ci/pipeline_editor/pipeline_editor_app_spec.js
@@ -8,7 +8,6 @@ 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 createStore from '~/ci/pipeline_editor/store';
import PipelineEditorTabs from '~/ci/pipeline_editor/components/pipeline_editor_tabs.vue';
import PipelineEditorEmptyState from '~/ci/pipeline_editor/components/ui/pipeline_editor_empty_state.vue';
import PipelineEditorMessages from '~/ci/pipeline_editor/components/ui/pipeline_editor_messages.vue';
@@ -81,9 +80,7 @@ describe('Pipeline editor app component', () => {
provide = {},
stubs = {},
} = {}) => {
- const store = createStore();
wrapper = shallowMount(PipelineEditorApp, {
- store,
provide: { ...defaultProvide, ...provide },
stubs,
mocks: {
@@ -99,7 +96,7 @@ describe('Pipeline editor app component', () => {
});
};
- const createComponentWithApollo = async ({
+ const createComponentWithApollo = ({
provide = {},
stubs = {},
withUndefinedBranch = false,
@@ -255,10 +252,6 @@ describe('Pipeline editor app component', () => {
.mockImplementation(jest.fn());
});
- it('available stages is updated', () => {
- expect(wrapper.vm.$store.state.availableStages).toStrictEqual(['test', 'build']);
- });
-
it('shows pipeline editor home component', () => {
expect(findEditorHome().exists()).toBe(true);
});
@@ -267,7 +260,7 @@ describe('Pipeline editor app component', () => {
expect(findAlert().exists()).toBe(false);
});
- it('ci config query is called with correct variables', async () => {
+ it('ci config query is called with correct variables', () => {
expect(mockCiConfigData).toHaveBeenCalledWith({
content: mockCiYml,
projectPath: mockProjectFullPath,
@@ -294,7 +287,7 @@ describe('Pipeline editor app component', () => {
.mockImplementation(jest.fn());
});
- it('shows an empty state and does not show editor home component', async () => {
+ it('shows an empty state and does not show editor home component', () => {
expect(findEmptyState().exists()).toBe(true);
expect(findAlert().exists()).toBe(false);
expect(findEditorHome().exists()).toBe(false);
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 1349461d8bc..9015031b6c8 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
@@ -142,7 +142,7 @@ describe('Pipeline New Form', () => {
await waitForPromises();
});
- it('displays the correct values for the provided query params', async () => {
+ it('displays the correct values for the provided query params', () => {
expect(findVariableTypes().at(0).props('text')).toBe('Variable');
expect(findVariableTypes().at(1).props('text')).toBe('File');
expect(findRefsDropdown().props('value')).toEqual({ shortName: 'tag-1' });
@@ -154,7 +154,7 @@ describe('Pipeline New Form', () => {
expect(findValueInputs().at(0).element.value).toBe('test_var_val');
});
- it('displays an empty variable for the user to fill out', async () => {
+ it('displays an empty variable for the user to fill out', () => {
expect(findKeyInputs().at(2).element.value).toBe('');
expect(findValueInputs().at(2).element.value).toBe('');
expect(findVariableTypes().at(2).props('text')).toBe('Variable');
@@ -186,12 +186,12 @@ describe('Pipeline New Form', () => {
});
describe('Pipeline creation', () => {
- beforeEach(async () => {
+ beforeEach(() => {
mockCiConfigVariables.mockResolvedValue(mockEmptyCiConfigVariablesResponse);
mock.onPost(pipelinesPath).reply(HTTP_STATUS_OK, newPipelinePostResponse);
});
- it('does not submit the native HTML form', async () => {
+ it('does not submit the native HTML form', () => {
createComponentWithApollo();
findForm().vm.$emit('submit', dummySubmitEvent);
@@ -328,7 +328,7 @@ describe('Pipeline New Form', () => {
});
const testBehaviorWhenCacheIsPopulated = (queryResponse) => {
- beforeEach(async () => {
+ beforeEach(() => {
mockCiConfigVariables.mockResolvedValue(queryResponse);
createComponentWithApollo({ method: mountExtended });
});
@@ -406,7 +406,7 @@ describe('Pipeline New Form', () => {
await waitForPromises();
});
- it('displays all the variables', async () => {
+ it('displays all the variables', () => {
expect(findVariableRows()).toHaveLength(6);
});
@@ -445,7 +445,7 @@ describe('Pipeline New Form', () => {
await waitForPromises();
});
- it('displays variables with description only', async () => {
+ it('displays variables with description only', () => {
expect(findVariableRows()).toHaveLength(2); // extra empty variable is added at the end
});
});
diff --git a/spec/frontend/ci/pipeline_new/components/refs_dropdown_spec.js b/spec/frontend/ci/pipeline_new/components/refs_dropdown_spec.js
index 60ace483712..82dac1358c5 100644
--- a/spec/frontend/ci/pipeline_new/components/refs_dropdown_spec.js
+++ b/spec/frontend/ci/pipeline_new/components/refs_dropdown_spec.js
@@ -54,7 +54,7 @@ describe('Pipeline New Form', () => {
expect(findRefsDropdownItems()).toHaveLength(0);
});
- it('does not make requests immediately', async () => {
+ it('does not make requests immediately', () => {
expect(mock.history.get).toHaveLength(0);
});
@@ -117,14 +117,14 @@ describe('Pipeline New Form', () => {
await waitForPromises();
});
- it('requests filtered tags and branches', async () => {
+ it('requests filtered tags and branches', () => {
expect(mock.history.get).toHaveLength(2);
expect(mock.history.get[1].params).toEqual({
search: mockSearchTerm,
});
});
- it('displays dropdown with branches and tags', async () => {
+ it('displays dropdown with branches and tags', () => {
const filteredRefLength = mockFilteredRefs.Tags.length + mockFilteredRefs.Branches.length;
expect(findRefsDropdownItems()).toHaveLength(filteredRefLength);
diff --git a/spec/frontend/ci/pipeline_schedules/components/delete_pipeline_schedule_modal_spec.js b/spec/frontend/ci/pipeline_schedules/components/delete_pipeline_schedule_modal_spec.js
index c45267e5a47..e48f556c246 100644
--- a/spec/frontend/ci/pipeline_schedules/components/delete_pipeline_schedule_modal_spec.js
+++ b/spec/frontend/ci/pipeline_schedules/components/delete_pipeline_schedule_modal_spec.js
@@ -20,13 +20,13 @@ describe('Delete pipeline schedule modal', () => {
createComponent();
});
- it('emits the deleteSchedule event', async () => {
+ it('emits the deleteSchedule event', () => {
findModal().vm.$emit('primary');
expect(wrapper.emitted()).toEqual({ deleteSchedule: [[]] });
});
- it('emits the hideModal event', async () => {
+ it('emits the hideModal event', () => {
findModal().vm.$emit('hide');
expect(wrapper.emitted()).toEqual({ hideModal: [[]] });
diff --git a/spec/frontend/ci/pipeline_schedules/components/take_ownership_modal_spec.js b/spec/frontend/ci/pipeline_schedules/components/take_ownership_modal_spec.js
index e3965d13c19..7cc254b7653 100644
--- a/spec/frontend/ci/pipeline_schedules/components/take_ownership_modal_spec.js
+++ b/spec/frontend/ci/pipeline_schedules/components/take_ownership_modal_spec.js
@@ -26,13 +26,13 @@ describe('Take ownership modal', () => {
);
});
- it('emits the takeOwnership event', async () => {
+ it('emits the takeOwnership event', () => {
findModal().vm.$emit('primary');
expect(wrapper.emitted()).toEqual({ takeOwnership: [[]] });
});
- it('emits the hideModal event', async () => {
+ it('emits the hideModal event', () => {
findModal().vm.$emit('hide');
expect(wrapper.emitted()).toEqual({ hideModal: [[]] });
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
index 85b1d3b1b2f..58a1c0bc18d 100644
--- 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
@@ -1,5 +1,3 @@
-import Vue from 'vue';
-import VueApollo from 'vue-apollo';
import { GlSprintf } from '@gitlab/ui';
import { s__ } from '~/locale';
@@ -11,14 +9,15 @@ import AdminNewRunnerApp from '~/ci/runner/admin_new_runner/admin_new_runner_app
import { saveAlertToLocalStorage } from '~/ci/runner/local_storage_alert/save_alert_to_local_storage';
import RunnerInstructionsModal from '~/vue_shared/components/runner_instructions/runner_instructions_modal.vue';
import RunnerPlatformsRadioGroup from '~/ci/runner/components/runner_platforms_radio_group.vue';
-import { PARAM_KEY_PLATFORM, DEFAULT_PLATFORM, WINDOWS_PLATFORM } from '~/ci/runner/constants';
+import {
+ PARAM_KEY_PLATFORM,
+ INSTANCE_TYPE,
+ DEFAULT_PLATFORM,
+ WINDOWS_PLATFORM,
+} from '~/ci/runner/constants';
import RunnerCreateForm from '~/ci/runner/components/runner_create_form.vue';
import { redirectTo } from '~/lib/utils/url_utility';
-import { runnerCreateResult } from '../mock_data';
-
-const mockLegacyRegistrationToken = 'LEGACY_REGISTRATION_TOKEN';
-
-Vue.use(VueApollo);
+import { runnerCreateResult, mockRegistrationToken } from '../mock_data';
jest.mock('~/ci/runner/local_storage_alert/save_alert_to_local_storage');
jest.mock('~/alert');
@@ -40,7 +39,7 @@ describe('AdminNewRunnerApp', () => {
const createComponent = () => {
wrapper = shallowMountExtended(AdminNewRunnerApp, {
propsData: {
- legacyRegistrationToken: mockLegacyRegistrationToken,
+ legacyRegistrationToken: mockRegistrationToken,
},
directives: {
GlModal: createMockDirective('gl-modal'),
@@ -58,7 +57,7 @@ describe('AdminNewRunnerApp', () => {
describe('Shows legacy modal', () => {
it('passes legacy registration to modal', () => {
expect(findRunnerInstructionsModal().props('registrationToken')).toEqual(
- mockLegacyRegistrationToken,
+ mockRegistrationToken,
);
});
@@ -76,8 +75,11 @@ describe('AdminNewRunnerApp', () => {
});
describe('Runner form', () => {
- it('shows the runner create form', () => {
- expect(findRunnerCreateForm().exists()).toBe(true);
+ it('shows the runner create form for an instance runner', () => {
+ expect(findRunnerCreateForm().props()).toEqual({
+ runnerType: INSTANCE_TYPE,
+ groupId: null,
+ });
});
describe('When a runner is saved', () => {
@@ -93,7 +95,7 @@ describe('AdminNewRunnerApp', () => {
});
it('redirects to the registration page', () => {
- const url = `${mockCreatedRunner.registerAdminUrl}?${PARAM_KEY_PLATFORM}=${DEFAULT_PLATFORM}`;
+ const url = `${mockCreatedRunner.ephemeralRegisterUrl}?${PARAM_KEY_PLATFORM}=${DEFAULT_PLATFORM}`;
expect(redirectTo).toHaveBeenCalledWith(url);
});
@@ -106,7 +108,7 @@ describe('AdminNewRunnerApp', () => {
});
it('redirects to the registration page with the platform', () => {
- const url = `${mockCreatedRunner.registerAdminUrl}?${PARAM_KEY_PLATFORM}=${WINDOWS_PLATFORM}`;
+ const url = `${mockCreatedRunner.ephemeralRegisterUrl}?${PARAM_KEY_PLATFORM}=${WINDOWS_PLATFORM}`;
expect(redirectTo).toHaveBeenCalledWith(url);
});
diff --git a/spec/frontend/ci/runner/admin_register_runner/admin_register_runner_app_spec.js b/spec/frontend/ci/runner/admin_register_runner/admin_register_runner_app_spec.js
index d04df85d58f..60244ba5bc2 100644
--- a/spec/frontend/ci/runner/admin_register_runner/admin_register_runner_app_spec.js
+++ b/spec/frontend/ci/runner/admin_register_runner/admin_register_runner_app_spec.js
@@ -37,7 +37,7 @@ describe('AdminRegisterRunnerApp', () => {
};
describe('When showing runner details', () => {
- beforeEach(async () => {
+ beforeEach(() => {
createComponent();
});
@@ -64,7 +64,7 @@ describe('AdminRegisterRunnerApp', () => {
});
describe('When another platform has been selected', () => {
- beforeEach(async () => {
+ beforeEach(() => {
setWindowLocation(`?${PARAM_KEY_PLATFORM}=${WINDOWS_PLATFORM}`);
createComponent();
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 9d9142f2c68..d1f95aef349 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
@@ -81,7 +81,7 @@ describe('AdminRunnerShowApp', () => {
await createComponent({ mountFn: mountExtended });
});
- it('expect GraphQL ID to be requested', async () => {
+ it('expect GraphQL ID to be requested', () => {
expect(mockRunnerQuery).toHaveBeenCalledWith({ id: mockRunnerGraphqlId });
});
@@ -89,7 +89,7 @@ describe('AdminRunnerShowApp', () => {
expect(findRunnerHeader().text()).toContain(`Runner #${mockRunnerId}`);
});
- it('displays the runner edit and pause buttons', async () => {
+ it('displays the runner edit and pause buttons', () => {
expect(findRunnerEditButton().attributes('href')).toBe(mockRunner.editAdminUrl);
expect(findRunnerPauseButton().exists()).toBe(true);
expect(findRunnerDeleteButton().exists()).toBe(true);
@@ -99,7 +99,7 @@ describe('AdminRunnerShowApp', () => {
expect(findRunnerDetailsTabs().props('runner')).toEqual(mockRunner);
});
- it('shows basic runner details', async () => {
+ it('shows basic runner details', () => {
const expected = `Description My Runner
Last contact Never contacted
Version 1.0.0
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 0cf6241c24f..2cd1bc0b2f8 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
@@ -57,13 +57,13 @@ import {
allRunnersDataPaginated,
onlineContactTimeoutSecs,
staleTimeoutSecs,
+ mockRegistrationToken,
newRunnerPath,
emptyPageInfo,
emptyStateSvgPath,
emptyStateFilteredSvgPath,
} from '../mock_data';
-const mockRegistrationToken = 'MOCK_REGISTRATION_TOKEN';
const mockRunners = allRunnersData.data.runners.nodes;
const mockRunnersCount = runnersCountData.data.runners.count;
@@ -208,13 +208,13 @@ describe('AdminRunnersApp', () => {
it('runner item links to the runner admin page', async () => {
await createComponent({ mountFn: mountExtended });
- const { id, shortSha } = mockRunners[0];
+ const { id, shortSha, adminUrl } = mockRunners[0];
const numericId = getIdFromGraphQLId(id);
const runnerLink = wrapper.find('tr [data-testid="td-summary"]').findComponent(GlLink);
expect(runnerLink.text()).toBe(`#${numericId} (${shortSha})`);
- expect(runnerLink.attributes('href')).toBe(`http://localhost/admin/runners/${numericId}`);
+ expect(runnerLink.attributes('href')).toBe(adminUrl);
});
it('renders runner actions for each runner', async () => {
@@ -264,7 +264,7 @@ describe('AdminRunnersApp', () => {
});
describe('Single runner row', () => {
- const { id: graphqlId, shortSha } = mockRunners[0];
+ const { id: graphqlId, shortSha, adminUrl } = mockRunners[0];
const id = getIdFromGraphQLId(graphqlId);
beforeEach(async () => {
@@ -273,11 +273,11 @@ describe('AdminRunnersApp', () => {
await createComponent({ mountFn: mountExtended });
});
- it('Links to the runner page', async () => {
+ it('Links to the runner page', () => {
const runnerLink = wrapper.find('tr [data-testid="td-summary"]').findComponent(GlLink);
expect(runnerLink.text()).toBe(`#${id} (${shortSha})`);
- expect(runnerLink.attributes('href')).toBe(`http://localhost/admin/runners/${id}`);
+ expect(runnerLink.attributes('href')).toBe(adminUrl);
});
it('Shows job status and links to jobs', () => {
@@ -286,13 +286,10 @@ describe('AdminRunnersApp', () => {
.findComponent(RunnerJobStatusBadge);
expect(badge.props('jobStatus')).toBe(mockRunners[0].jobExecutionStatus);
-
- const badgeHref = new URL(badge.attributes('href'));
- expect(badgeHref.pathname).toBe(`/admin/runners/${id}`);
- expect(badgeHref.hash).toBe(`#${JOBS_ROUTE_PATH}`);
+ expect(badge.attributes('href')).toBe(`${adminUrl}#${JOBS_ROUTE_PATH}`);
});
- it('When runner is paused or unpaused, some data is refetched', async () => {
+ it('When runner is paused or unpaused, some data is refetched', () => {
expect(mockRunnersCountHandler).toHaveBeenCalledTimes(COUNT_QUERIES);
findRunnerActionsCell().vm.$emit('toggledPaused');
@@ -301,7 +298,7 @@ describe('AdminRunnersApp', () => {
expect(showToast).toHaveBeenCalledTimes(0);
});
- it('When runner is deleted, data is refetched and a toast message is shown', async () => {
+ it('When runner is deleted, data is refetched and a toast message is shown', () => {
findRunnerActionsCell().vm.$emit('deleted', { message: 'Runner deleted' });
expect(showToast).toHaveBeenCalledTimes(1);
@@ -324,7 +321,7 @@ describe('AdminRunnersApp', () => {
{ type: PARAM_KEY_STATUS, value: { data: STATUS_ONLINE, operator: '=' } },
{ type: PARAM_KEY_PAUSED, value: { data: 'true', operator: '=' } },
],
- sort: 'CREATED_DESC',
+ sort: DEFAULT_SORT,
pagination: {},
});
});
@@ -410,7 +407,7 @@ describe('AdminRunnersApp', () => {
await createComponent({ mountFn: mountExtended });
});
- it('count data is refetched', async () => {
+ it('count data is refetched', () => {
expect(mockRunnersCountHandler).toHaveBeenCalledTimes(COUNT_QUERIES);
findRunnerList().vm.$emit('deleted', { message: 'Runners deleted' });
@@ -418,7 +415,7 @@ describe('AdminRunnersApp', () => {
expect(mockRunnersCountHandler).toHaveBeenCalledTimes(COUNT_QUERIES * 2);
});
- it('toast is shown', async () => {
+ it('toast is shown', () => {
expect(showToast).toHaveBeenCalledTimes(0);
findRunnerList().vm.$emit('deleted', { message: 'Runners deleted' });
@@ -480,11 +477,11 @@ describe('AdminRunnersApp', () => {
await createComponent();
});
- it('error is shown to the user', async () => {
+ it('error is shown to the user', () => {
expect(createAlert).toHaveBeenCalledTimes(1);
});
- it('error is reported to sentry', async () => {
+ it('error is reported to sentry', () => {
expect(captureException).toHaveBeenCalledWith({
error: new Error('Error!'),
component: 'AdminRunnersApp',
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 ec23d8415e8..c435dd57de2 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
@@ -1,4 +1,4 @@
-import { mount } from '@vue/test-utils';
+import { shallowMount } from '@vue/test-utils';
import RunnerStatusCell from '~/ci/runner/components/cells/runner_status_cell.vue';
import RunnerStatusBadge from '~/ci/runner/components/runner_status_badge.vue';
@@ -20,7 +20,7 @@ describe('RunnerStatusCell', () => {
const findPausedBadge = () => wrapper.findComponent(RunnerPausedBadge);
const createComponent = ({ runner = {}, ...options } = {}) => {
- wrapper = mount(RunnerStatusCell, {
+ wrapper = shallowMount(RunnerStatusCell, {
propsData: {
runner: {
runnerType: INSTANCE_TYPE,
@@ -30,6 +30,10 @@ describe('RunnerStatusCell', () => {
...runner,
},
},
+ stubs: {
+ RunnerStatusBadge,
+ RunnerPausedBadge,
+ },
...options,
});
};
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 585a03c0811..23ec170961a 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
@@ -1,5 +1,6 @@
-import { __ } from '~/locale';
+import { __, sprintf } from '~/locale';
import { mountExtended } from 'helpers/vue_test_utils_helper';
+import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
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';
@@ -11,11 +12,13 @@ import {
I18N_INSTANCE_TYPE,
PROJECT_TYPE,
I18N_NO_DESCRIPTION,
+ I18N_CREATED_AT_LABEL,
+ I18N_CREATED_AT_BY_LABEL,
} from '~/ci/runner/constants';
-import { allRunnersData } from '../../mock_data';
+import { allRunnersWithCreatorData } from '../../mock_data';
-const mockRunner = allRunnersData.data.runners.nodes[0];
+const mockRunner = allRunnersWithCreatorData.data.runners.nodes[0];
describe('RunnerTypeCell', () => {
let wrapper;
@@ -142,10 +145,42 @@ describe('RunnerTypeCell', () => {
expect(findRunnerSummaryField('pipeline').text()).toContain('1,000+');
});
- it('Displays created at', () => {
- expect(findRunnerSummaryField('calendar').findComponent(TimeAgo).props('time')).toBe(
- mockRunner.createdAt,
- );
+ describe('Displays creation info', () => {
+ const findCreatedTime = () => findRunnerSummaryField('calendar').findComponent(TimeAgo);
+
+ it('Displays created at ...', () => {
+ createComponent({
+ createdBy: null,
+ });
+
+ expect(findRunnerSummaryField('calendar').text()).toMatchInterpolatedText(
+ sprintf(I18N_CREATED_AT_LABEL, {
+ timeAgo: findCreatedTime().text(),
+ }),
+ );
+ expect(findCreatedTime().props('time')).toBe(mockRunner.createdAt);
+ });
+
+ it('Displays created at ... by ...', () => {
+ expect(findRunnerSummaryField('calendar').text()).toMatchInterpolatedText(
+ sprintf(I18N_CREATED_AT_BY_LABEL, {
+ timeAgo: findCreatedTime().text(),
+ avatar: mockRunner.createdBy.username,
+ }),
+ );
+ expect(findCreatedTime().props('time')).toBe(mockRunner.createdAt);
+ });
+
+ it('Displays creator avatar', () => {
+ const { name, avatarUrl, webUrl, username } = mockRunner.createdBy;
+
+ expect(wrapper.findComponent(UserAvatarLink).props()).toMatchObject({
+ imgAlt: expect.stringContaining(name),
+ imgSrc: avatarUrl,
+ linkHref: webUrl,
+ tooltipText: username,
+ });
+ });
});
it('Displays tag list', () => {
diff --git a/spec/frontend/ci/runner/components/registration/__snapshots__/utils_spec.js.snap b/spec/frontend/ci/runner/components/registration/__snapshots__/utils_spec.js.snap
index 09d032fd32d..5eb7ffaacd6 100644
--- a/spec/frontend/ci/runner/components/registration/__snapshots__/utils_spec.js.snap
+++ b/spec/frontend/ci/runner/components/registration/__snapshots__/utils_spec.js.snap
@@ -75,8 +75,7 @@ exports[`registration utils for "linux" platform registerCommand is correct 1`]
Array [
"gitlab-runner register",
" --url http://test.host",
- " --registration-token REGISTRATION_TOKEN",
- " --description 'RUNNER'",
+ " --token MOCK_AUTHENTICATION_TOKEN",
]
`;
@@ -130,8 +129,7 @@ exports[`registration utils for "osx" platform registerCommand is correct 1`] =
Array [
"gitlab-runner register",
" --url http://test.host",
- " --registration-token REGISTRATION_TOKEN",
- " --description 'RUNNER'",
+ " --token MOCK_AUTHENTICATION_TOKEN",
]
`;
@@ -189,8 +187,7 @@ exports[`registration utils for "windows" platform registerCommand is correct 1`
Array [
".\\\\gitlab-runner.exe register",
" --url http://test.host",
- " --registration-token REGISTRATION_TOKEN",
- " --description 'RUNNER'",
+ " --token MOCK_AUTHENTICATION_TOKEN",
]
`;
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 9ed59b0a57d..d23723807b1 100644
--- a/spec/frontend/ci/runner/components/registration/registration_dropdown_spec.js
+++ b/spec/frontend/ci/runner/components/registration/registration_dropdown_spec.js
@@ -1,10 +1,10 @@
import { GlModal, GlDropdown, GlDropdownItem, GlDropdownForm } from '@gitlab/ui';
-import { mount, shallowMount, createWrapper } from '@vue/test-utils';
+import { 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 { shallowMountExtended, mountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
@@ -21,9 +21,7 @@ import {
mockRunnerPlatforms,
mockInstructions,
} from 'jest/vue_shared/components/runner_instructions/mock_data';
-
-const mockToken = '0123456789';
-const maskToken = '**********';
+import { mockRegistrationToken } from '../../mock_data';
Vue.use(VueApollo);
@@ -53,17 +51,15 @@ describe('RegistrationDropdown', () => {
await waitForPromises();
};
- const createComponent = ({ props = {}, ...options } = {}, mountFn = shallowMount) => {
- wrapper = extendedWrapper(
- mountFn(RegistrationDropdown, {
- propsData: {
- registrationToken: mockToken,
- type: INSTANCE_TYPE,
- ...props,
- },
- ...options,
- }),
- );
+ const createComponent = ({ props = {}, ...options } = {}, mountFn = shallowMountExtended) => {
+ wrapper = mountFn(RegistrationDropdown, {
+ propsData: {
+ registrationToken: mockRegistrationToken,
+ type: INSTANCE_TYPE,
+ ...props,
+ },
+ ...options,
+ });
};
const createComponentWithModal = () => {
@@ -79,7 +75,7 @@ describe('RegistrationDropdown', () => {
// Use `attachTo` to find the modal
attachTo: document.body,
},
- mount,
+ mountExtended,
);
};
@@ -89,7 +85,7 @@ describe('RegistrationDropdown', () => {
${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);
+ createComponent({ props: { type: INSTANCE_TYPE } }, mountExtended);
expect(wrapper.text()).toContain('Register an instance runner');
});
@@ -111,7 +107,7 @@ describe('RegistrationDropdown', () => {
describe('When the dropdown item is clicked', () => {
beforeEach(async () => {
- createComponentWithModal({}, mount);
+ createComponentWithModal({}, mountExtended);
await openModal();
});
@@ -142,7 +138,15 @@ describe('RegistrationDropdown', () => {
});
it('Displays masked value by default', () => {
- createComponent({}, mount);
+ const mockToken = '0123456789';
+ const maskToken = '**********';
+
+ createComponent(
+ {
+ props: { registrationToken: mockToken },
+ },
+ mountExtended,
+ );
expect(findRegistrationTokenInput().element.value).toBe(maskToken);
});
@@ -171,7 +175,7 @@ describe('RegistrationDropdown', () => {
};
it('Updates token input', async () => {
- createComponent({}, mount);
+ createComponent({}, mountExtended);
expect(findRegistrationToken().props('value')).not.toBe(newToken);
@@ -181,11 +185,11 @@ describe('RegistrationDropdown', () => {
});
it('Updates token in modal', async () => {
- createComponentWithModal({}, mount);
+ createComponentWithModal({}, mountExtended);
await openModal();
- expect(findModalContent()).toContain(mockToken);
+ expect(findModalContent()).toContain(mockRegistrationToken);
await resetToken();
diff --git a/spec/frontend/ci/runner/components/registration/registration_instructions_spec.js b/spec/frontend/ci/runner/components/registration/registration_instructions_spec.js
index eb4b659091d..8c196d7b5e3 100644
--- a/spec/frontend/ci/runner/components/registration/registration_instructions_spec.js
+++ b/spec/frontend/ci/runner/components/registration/registration_instructions_spec.js
@@ -22,16 +22,13 @@ import {
RUNNER_REGISTRATION_POLLING_INTERVAL_MS,
I18N_REGISTRATION_SUCCESS,
} from '~/ci/runner/constants';
-import { runnerForRegistration } from '../../mock_data';
+import { runnerForRegistration, mockAuthenticationToken } from '../../mock_data';
Vue.use(VueApollo);
-const MOCK_TOKEN = 'MOCK_TOKEN';
-const mockDescription = runnerForRegistration.data.runner.description;
-
const mockRunner = {
...runnerForRegistration.data.runner,
- ephemeralAuthenticationToken: MOCK_TOKEN,
+ ephemeralAuthenticationToken: mockAuthenticationToken,
};
const mockRunnerWithoutToken = {
...runnerForRegistration.data.runner,
@@ -53,6 +50,18 @@ describe('RegistrationInstructions', () => {
await waitForPromises();
};
+ const mockBeforeunload = () => {
+ const event = new Event('beforeunload');
+ const preventDefault = jest.spyOn(event, 'preventDefault');
+ const returnValueSetter = jest.spyOn(event, 'returnValue', 'set');
+
+ return {
+ event,
+ preventDefault,
+ returnValueSetter,
+ };
+ };
+
const mockResolvedRunner = (runner = mockRunner) => {
mockRunnerQuery.mockResolvedValue({
data: {
@@ -84,7 +93,7 @@ describe('RegistrationInstructions', () => {
window.gon.gitlab_url = TEST_HOST;
});
- it('loads runner with id', async () => {
+ it('loads runner with id', () => {
createComponent();
expect(mockRunnerQuery).toHaveBeenCalledWith({ id: mockRunner.id });
@@ -139,13 +148,12 @@ describe('RegistrationInstructions', () => {
command: [
'gitlab-runner register',
` --url ${TEST_HOST}`,
- ` --registration-token ${MOCK_TOKEN}`,
- ` --description '${mockDescription}'`,
+ ` --token ${mockAuthenticationToken}`,
],
prompt: '$',
});
- expect(step1.find('[data-testid="runner-token"]').text()).toBe(MOCK_TOKEN);
- expect(step1.findComponent(ClipboardButton).props('text')).toBe(MOCK_TOKEN);
+ expect(step1.findByTestId('runner-token').text()).toBe(mockAuthenticationToken);
+ expect(step1.findComponent(ClipboardButton).props('text')).toBe(mockAuthenticationToken);
});
it('renders step 1 in loading state', () => {
@@ -169,9 +177,8 @@ describe('RegistrationInstructions', () => {
expect(step1.findComponent(CliCommand).props('command')).toEqual([
'gitlab-runner register',
` --url ${TEST_HOST}`,
- ` --description '${mockDescription}'`,
]);
- expect(step1.find('[data-testid="runner-token"]').exists()).toBe(false);
+ expect(step1.findByTestId('runner-token').exists()).toBe(false);
expect(step1.findComponent(ClipboardButton).exists()).toBe(false);
});
@@ -211,11 +218,10 @@ describe('RegistrationInstructions', () => {
expect(step1.findComponent(CliCommand).props('command')).toEqual([
'gitlab-runner register',
` --url ${TEST_HOST}`,
- ` --registration-token ${MOCK_TOKEN}`,
- ` --description '${mockDescription}'`,
+ ` --token ${mockAuthenticationToken}`,
]);
- expect(step1.find('[data-testid="runner-token"]').text()).toBe(MOCK_TOKEN);
- expect(step1.findComponent(ClipboardButton).props('text')).toBe(MOCK_TOKEN);
+ expect(step1.findByTestId('runner-token').text()).toBe(mockAuthenticationToken);
+ expect(step1.findComponent(ClipboardButton).props('text')).toBe(mockAuthenticationToken);
});
it('when runner is not available (e.g. deleted), the UI does not update', async () => {
@@ -226,11 +232,10 @@ describe('RegistrationInstructions', () => {
expect(step1.findComponent(CliCommand).props('command')).toEqual([
'gitlab-runner register',
` --url ${TEST_HOST}`,
- ` --registration-token ${MOCK_TOKEN}`,
- ` --description '${mockDescription}'`,
+ ` --token ${mockAuthenticationToken}`,
]);
- expect(step1.find('[data-testid="runner-token"]').text()).toBe(MOCK_TOKEN);
- expect(step1.findComponent(ClipboardButton).props('text')).toBe(MOCK_TOKEN);
+ expect(step1.findByTestId('runner-token').text()).toBe(mockAuthenticationToken);
+ expect(step1.findComponent(ClipboardButton).props('text')).toBe(mockAuthenticationToken);
});
});
});
@@ -273,6 +278,20 @@ describe('RegistrationInstructions', () => {
it('does not show success message', () => {
expect(wrapper.text()).not.toContain(I18N_REGISTRATION_SUCCESS);
});
+
+ describe('when the page is closing', () => {
+ it('warns the user against closing', () => {
+ const { event, preventDefault, returnValueSetter } = mockBeforeunload();
+
+ expect(preventDefault).not.toHaveBeenCalled();
+ expect(returnValueSetter).not.toHaveBeenCalled();
+
+ window.dispatchEvent(event);
+
+ expect(preventDefault).toHaveBeenCalledWith();
+ expect(returnValueSetter).toHaveBeenCalledWith(expect.any(String));
+ });
+ });
});
describe('when the runner has been registered', () => {
@@ -288,6 +307,20 @@ describe('RegistrationInstructions', () => {
expect(wrapper.text()).toContain('🎉');
expect(wrapper.text()).toContain(I18N_REGISTRATION_SUCCESS);
});
+
+ describe('when the page is closing', () => {
+ it('does not warn the user against closing', () => {
+ const { event, preventDefault, returnValueSetter } = mockBeforeunload();
+
+ expect(preventDefault).not.toHaveBeenCalled();
+ expect(returnValueSetter).not.toHaveBeenCalled();
+
+ window.dispatchEvent(event);
+
+ expect(preventDefault).not.toHaveBeenCalled();
+ expect(returnValueSetter).not.toHaveBeenCalled();
+ });
+ });
});
});
});
diff --git a/spec/frontend/ci/runner/components/registration/registration_token_reset_dropdown_item_spec.js b/spec/frontend/ci/runner/components/registration/registration_token_reset_dropdown_item_spec.js
index ff69fd6d3d6..bfdde922e17 100644
--- a/spec/frontend/ci/runner/components/registration/registration_token_reset_dropdown_item_spec.js
+++ b/spec/frontend/ci/runner/components/registration/registration_token_reset_dropdown_item_spec.js
@@ -18,7 +18,7 @@ jest.mock('~/ci/runner/sentry_utils');
Vue.use(VueApollo);
Vue.use(GlToast);
-const mockNewToken = 'NEW_TOKEN';
+const mockNewRegistrationToken = 'MOCK_NEW_REGISTRATION_TOKEN';
const modalID = 'token-reset-modal';
describe('RegistrationTokenResetDropdownItem', () => {
@@ -54,7 +54,7 @@ describe('RegistrationTokenResetDropdownItem', () => {
runnersRegistrationTokenResetMutationHandler = jest.fn().mockResolvedValue({
data: {
runnersRegistrationTokenReset: {
- token: mockNewToken,
+ token: mockNewRegistrationToken,
errors: [],
},
},
@@ -109,7 +109,7 @@ describe('RegistrationTokenResetDropdownItem', () => {
it('emits result', () => {
expect(wrapper.emitted('tokenReset')).toHaveLength(1);
- expect(wrapper.emitted('tokenReset')[0]).toEqual([mockNewToken]);
+ expect(wrapper.emitted('tokenReset')[0]).toEqual([mockNewRegistrationToken]);
});
it('does not show a loading state', () => {
diff --git a/spec/frontend/ci/runner/components/registration/registration_token_spec.js b/spec/frontend/ci/runner/components/registration/registration_token_spec.js
index 4f44e6e10b2..fc659f7974f 100644
--- a/spec/frontend/ci/runner/components/registration/registration_token_spec.js
+++ b/spec/frontend/ci/runner/components/registration/registration_token_spec.js
@@ -3,9 +3,7 @@ import Vue from 'vue';
import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
import RegistrationToken from '~/ci/runner/components/registration/registration_token.vue';
import InputCopyToggleVisibility from '~/vue_shared/components/form/input_copy_toggle_visibility.vue';
-
-const mockToken = '01234567890';
-const mockMasked = '***********';
+import { mockRegistrationToken } from '../../mock_data';
describe('RegistrationToken', () => {
let wrapper;
@@ -18,7 +16,7 @@ describe('RegistrationToken', () => {
const createComponent = ({ props = {}, mountFn = shallowMountExtended } = {}) => {
wrapper = mountFn(RegistrationToken, {
propsData: {
- value: mockToken,
+ value: mockRegistrationToken,
inputId: 'token-value',
...props,
},
@@ -30,7 +28,7 @@ describe('RegistrationToken', () => {
it('Displays value and copy button', () => {
createComponent();
- expect(findInputCopyToggleVisibility().props('value')).toBe(mockToken);
+ expect(findInputCopyToggleVisibility().props('value')).toBe(mockRegistrationToken);
expect(findInputCopyToggleVisibility().props('copyButtonTitle')).toBe(
'Copy registration token',
);
@@ -38,9 +36,17 @@ describe('RegistrationToken', () => {
// Component integration test to ensure secure masking
it('Displays masked value by default', () => {
- createComponent({ mountFn: mountExtended });
+ const mockToken = '0123456789';
+ const maskToken = '**********';
+
+ createComponent({
+ props: {
+ value: mockToken,
+ },
+ mountFn: mountExtended,
+ });
- expect(wrapper.find('input').element.value).toBe(mockMasked);
+ expect(wrapper.find('input').element.value).toBe(maskToken);
});
describe('When the copy to clipboard button is clicked', () => {
diff --git a/spec/frontend/ci/runner/components/registration/utils_spec.js b/spec/frontend/ci/runner/components/registration/utils_spec.js
index acf5993b15b..997cc5769ee 100644
--- a/spec/frontend/ci/runner/components/registration/utils_spec.js
+++ b/spec/frontend/ci/runner/components/registration/utils_spec.js
@@ -14,8 +14,7 @@ import {
platformArchitectures,
} from '~/ci/runner/components/registration/utils';
-const REGISTRATION_TOKEN = 'REGISTRATION_TOKEN';
-const DESCRIPTION = 'RUNNER';
+import { mockAuthenticationToken } from '../../mock_data';
describe('registration utils', () => {
beforeEach(() => {
@@ -33,8 +32,7 @@ describe('registration utils', () => {
expect(
registerCommand({
platform,
- registrationToken: REGISTRATION_TOKEN,
- description: DESCRIPTION,
+ token: mockAuthenticationToken,
}),
).toMatchSnapshot();
@@ -47,26 +45,6 @@ describe('registration utils', () => {
},
);
- describe.each([LINUX_PLATFORM, MACOS_PLATFORM])('for "%s" platform', (platform) => {
- it.each`
- description | parameter
- ${'my runner'} | ${"'my runner'"}
- ${"bob's runner"} | ${"'bob'\\''s runner'"}
- `('registerCommand escapes description `$description`', ({ description, parameter }) => {
- expect(registerCommand({ platform, description })[2]).toBe(` --description ${parameter}`);
- });
- });
-
- describe.each([WINDOWS_PLATFORM])('for "%s" platform', (platform) => {
- it.each`
- description | parameter
- ${'my runner'} | ${"'my runner'"}
- ${"bob's runner"} | ${"'bob''s runner'"}
- `('registerCommand escapes description `$description`', ({ description, parameter }) => {
- expect(registerCommand({ platform, description })[2]).toBe(` --description ${parameter}`);
- });
- });
-
describe('for missing platform', () => {
it('commandPrompt uses the default', () => {
const expected = commandPrompt({ platform: DEFAULT_PLATFORM });
@@ -78,15 +56,13 @@ describe('registration utils', () => {
it('registerCommand uses the default', () => {
const expected = registerCommand({
platform: DEFAULT_PLATFORM,
- registrationToken: REGISTRATION_TOKEN,
+ token: mockAuthenticationToken,
});
- expect(registerCommand({ platform: null, registrationToken: REGISTRATION_TOKEN })).toEqual(
+ expect(registerCommand({ platform: null, token: mockAuthenticationToken })).toEqual(expected);
+ expect(registerCommand({ platform: undefined, token: mockAuthenticationToken })).toEqual(
expected,
);
- expect(
- registerCommand({ platform: undefined, registrationToken: REGISTRATION_TOKEN }),
- ).toEqual(expected);
});
it('runCommand uses the default', () => {
diff --git a/spec/frontend/ci/runner/components/runner_create_form_spec.js b/spec/frontend/ci/runner/components/runner_create_form_spec.js
index 1123a026a4d..a13a19db067 100644
--- a/spec/frontend/ci/runner/components/runner_create_form_spec.js
+++ b/spec/frontend/ci/runner/components/runner_create_form_spec.js
@@ -6,7 +6,7 @@ import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import RunnerCreateForm from '~/ci/runner/components/runner_create_form.vue';
import RunnerFormFields from '~/ci/runner/components/runner_form_fields.vue';
-import { DEFAULT_ACCESS_LEVEL } from '~/ci/runner/constants';
+import { DEFAULT_ACCESS_LEVEL, INSTANCE_TYPE, GROUP_TYPE } from '~/ci/runner/constants';
import runnerCreateMutation from '~/ci/runner/graphql/new/runner_create.mutation.graphql';
import { captureException } from '~/ci/runner/sentry_utils';
import { runnerCreateResult } from '../mock_data';
@@ -35,30 +35,42 @@ describe('RunnerCreateForm', () => {
const findRunnerFormFields = () => wrapper.findComponent(RunnerFormFields);
const findSubmitBtn = () => wrapper.find('[type="submit"]');
- const createComponent = () => {
+ const createComponent = ({ props } = {}) => {
wrapper = shallowMountExtended(RunnerCreateForm, {
+ propsData: {
+ runnerType: INSTANCE_TYPE,
+ ...props,
+ },
apolloProvider: createMockApollo([[runnerCreateMutation, runnerCreateHandler]]),
});
};
beforeEach(() => {
runnerCreateHandler = jest.fn().mockResolvedValue(runnerCreateResult);
-
- createComponent();
});
it('shows default runner values', () => {
+ createComponent();
+
expect(findRunnerFormFields().props('value')).toEqual(defaultRunnerModel);
});
it('shows a submit button', () => {
+ createComponent();
+
expect(findSubmitBtn().exists()).toBe(true);
});
- describe('when user submits', () => {
+ describe.each`
+ typeName | props | scopeData
+ ${'an instance runner'} | ${{ runnerType: INSTANCE_TYPE }} | ${{ runnerType: INSTANCE_TYPE }}
+ ${'a group runner'} | ${{ runnerType: GROUP_TYPE, groupId: 'gid://gitlab/Group/72' }} | ${{ runnerType: GROUP_TYPE, groupId: 'gid://gitlab/Group/72' }}
+ `('when user submits $typeName', ({ props, scopeData }) => {
let preventDefault;
beforeEach(() => {
+ createComponent({ props });
+
preventDefault = jest.fn();
findRunnerFormFields().vm.$emit('input', {
@@ -82,10 +94,11 @@ describe('RunnerCreateForm', () => {
expect(findSubmitBtn().props('loading')).toBe(true);
});
- it('saves runner', async () => {
+ it('saves runner', () => {
expect(runnerCreateHandler).toHaveBeenCalledWith({
input: {
...defaultRunnerModel,
+ ...scopeData,
description: 'My runner',
maximumTimeout: 0,
tagList: ['tag1', 'tag2'],
@@ -100,7 +113,7 @@ describe('RunnerCreateForm', () => {
await waitForPromises();
});
- it('emits "saved" result', async () => {
+ it('emits "saved" result', () => {
expect(wrapper.emitted('saved')[0]).toEqual([mockCreatedRunner]);
});
@@ -119,7 +132,7 @@ describe('RunnerCreateForm', () => {
await waitForPromises();
});
- it('emits "error" result', async () => {
+ it('emits "error" result', () => {
expect(wrapper.emitted('error')[0]).toEqual([error]);
});
@@ -154,7 +167,7 @@ describe('RunnerCreateForm', () => {
await waitForPromises();
});
- it('emits "error" results', async () => {
+ it('emits "error" results', () => {
expect(wrapper.emitted('error')[0]).toEqual([new Error(`${errorMsg1} ${errorMsg2}`)]);
});
diff --git a/spec/frontend/ci/runner/components/runner_delete_button_spec.js b/spec/frontend/ci/runner/components/runner_delete_button_spec.js
index f9bea318d84..3123f2894fb 100644
--- a/spec/frontend/ci/runner/components/runner_delete_button_spec.js
+++ b/spec/frontend/ci/runner/components/runner_delete_button_spec.js
@@ -124,15 +124,15 @@ describe('RunnerDeleteButton', () => {
});
describe('Immediately after the delete button is clicked', () => {
- beforeEach(async () => {
+ beforeEach(() => {
findModal().vm.$emit('primary');
});
- it('The button has a loading state', async () => {
+ it('The button has a loading state', () => {
expect(findBtn().props('loading')).toBe(true);
});
- it('The stale tooltip is removed', async () => {
+ it('The stale tooltip is removed', () => {
expect(getTooltip()).toBe('');
});
});
@@ -255,15 +255,15 @@ describe('RunnerDeleteButton', () => {
});
describe('Immediately after the button is clicked', () => {
- beforeEach(async () => {
+ beforeEach(() => {
findModal().vm.$emit('primary');
});
- it('The button has a loading state', async () => {
+ it('The button has a loading state', () => {
expect(findBtn().props('loading')).toBe(true);
});
- it('The stale tooltip is removed', async () => {
+ it('The stale tooltip is removed', () => {
expect(getTooltip()).toBe('');
});
});
diff --git a/spec/frontend/ci/runner/components/runner_filtered_search_bar_spec.js b/spec/frontend/ci/runner/components/runner_filtered_search_bar_spec.js
index ac84c7898bf..7572122a5f3 100644
--- a/spec/frontend/ci/runner/components/runner_filtered_search_bar_spec.js
+++ b/spec/frontend/ci/runner/components/runner_filtered_search_bar_spec.js
@@ -1,5 +1,6 @@
import { GlFilteredSearch, GlDropdown, GlDropdownItem } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { assertProps } from 'helpers/assert_props';
import RunnerFilteredSearchBar from '~/ci/runner/components/runner_filtered_search_bar.vue';
import { statusTokenConfig } from '~/ci/runner/components/search_tokens/status_token_config';
import TagToken from '~/ci/runner/components/search_tokens/tag_token.vue';
@@ -43,12 +44,12 @@ describe('RunnerList', () => {
expect(inputs[inputs.length - 1][0]).toEqual(value);
};
+ const defaultProps = { namespace: 'runners', tokens: [], value: mockSearch };
+
const createComponent = ({ props = {}, options = {} } = {}) => {
wrapper = shallowMountExtended(RunnerFilteredSearchBar, {
propsData: {
- namespace: 'runners',
- tokens: [],
- value: mockSearch,
+ ...defaultProps,
...props,
},
stubs: {
@@ -109,11 +110,14 @@ describe('RunnerList', () => {
it('fails validation for v-model with the wrong shape', () => {
expect(() => {
- createComponent({ props: { value: { filters: 'wrong_filters', sort: 'sort' } } });
+ assertProps(RunnerFilteredSearchBar, {
+ ...defaultProps,
+ value: { filters: 'wrong_filters', sort: 'sort' },
+ });
}).toThrow('Invalid prop: custom validator check failed');
expect(() => {
- createComponent({ props: { value: { sort: 'sort' } } });
+ assertProps(RunnerFilteredSearchBar, { ...defaultProps, value: { sort: 'sort' } });
}).toThrow('Invalid prop: custom validator check failed');
});
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 3e813723b5b..e4ca84853c3 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
@@ -5,6 +5,7 @@ import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import RunnerInstructionsModal from '~/vue_shared/components/runner_instructions/runner_instructions_modal.vue';
import {
+ mockRegistrationToken,
newRunnerPath,
emptyStateSvgPath,
emptyStateFilteredSvgPath,
@@ -12,8 +13,6 @@ import {
import RunnerListEmptyState from '~/ci/runner/components/runner_list_empty_state.vue';
-const mockRegistrationToken = 'REGISTRATION_TOKEN';
-
describe('RunnerListEmptyState', () => {
let wrapper;
diff --git a/spec/frontend/ci/runner/components/runner_list_spec.js b/spec/frontend/ci/runner/components/runner_list_spec.js
index 6f4913dca3e..0f4ec717c3e 100644
--- a/spec/frontend/ci/runner/components/runner_list_spec.js
+++ b/spec/frontend/ci/runner/components/runner_list_spec.js
@@ -164,7 +164,7 @@ describe('RunnerList', () => {
});
});
- it('Emits a deleted event', async () => {
+ it('Emits a deleted event', () => {
const event = { message: 'Deleted!' };
findRunnerBulkDelete().vm.$emit('deleted', event);
diff --git a/spec/frontend/ci/runner/components/runner_pause_button_spec.js b/spec/frontend/ci/runner/components/runner_pause_button_spec.js
index 62e6cc902b7..350d029f3fc 100644
--- a/spec/frontend/ci/runner/components/runner_pause_button_spec.js
+++ b/spec/frontend/ci/runner/components/runner_pause_button_spec.js
@@ -134,7 +134,7 @@ describe('RunnerPauseButton', () => {
await clickAndWait();
});
- it(`The mutation to that sets active to ${newActiveValue} is called`, async () => {
+ it(`The mutation to that sets active to ${newActiveValue} is called`, () => {
expect(runnerToggleActiveHandler).toHaveBeenCalledTimes(1);
expect(runnerToggleActiveHandler).toHaveBeenCalledWith({
input: {
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
index db6fd2c369b..d419b34df1b 100644
--- a/spec/frontend/ci/runner/components/runner_platforms_radio_group_spec.js
+++ b/spec/frontend/ci/runner/components/runner_platforms_radio_group_spec.js
@@ -84,7 +84,7 @@ describe('RunnerPlatformsRadioGroup', () => {
text | href
${'Docker'} | ${DOCKER_HELP_URL}
${'Kubernetes'} | ${KUBERNETES_HELP_URL}
- `('provides link to "$text" docs', async ({ text, href }) => {
+ `('provides link to "$text" docs', ({ text, href }) => {
const radio = findFormRadioByText(text);
expect(radio.findComponent(GlLink).attributes()).toEqual({
diff --git a/spec/frontend/ci/runner/components/runner_platforms_radio_spec.js b/spec/frontend/ci/runner/components/runner_platforms_radio_spec.js
index fb81edd1ae2..340b04637f8 100644
--- a/spec/frontend/ci/runner/components/runner_platforms_radio_spec.js
+++ b/spec/frontend/ci/runner/components/runner_platforms_radio_spec.js
@@ -41,7 +41,7 @@ describe('RunnerPlatformsRadio', () => {
expect(findFormRadio().attributes('value')).toBe(mockValue);
});
- it('emits when item is clicked', async () => {
+ it('emits when item is clicked', () => {
findDiv().trigger('click');
expect(wrapper.emitted('input')).toEqual([[mockValue]]);
@@ -94,7 +94,7 @@ describe('RunnerPlatformsRadio', () => {
expect(wrapper.classes('gl-cursor-pointer')).toBe(false);
});
- it('does not emit when item is clicked', async () => {
+ it('does not emit when item is clicked', () => {
findDiv().trigger('click');
expect(wrapper.emitted('input')).toBe(undefined);
diff --git a/spec/frontend/ci/runner/components/runner_projects_spec.js b/spec/frontend/ci/runner/components/runner_projects_spec.js
index ccc1bc18675..afdc54d8ebc 100644
--- a/spec/frontend/ci/runner/components/runner_projects_spec.js
+++ b/spec/frontend/ci/runner/components/runner_projects_spec.js
@@ -89,7 +89,7 @@ describe('RunnerProjects', () => {
await waitForPromises();
});
- it('Shows a heading', async () => {
+ it('Shows a heading', () => {
const expected = sprintf(I18N_ASSIGNED_PROJECTS, { projectCount: mockProjects.length });
expect(findHeading().text()).toBe(expected);
diff --git a/spec/frontend/ci/runner/components/runner_type_badge_spec.js b/spec/frontend/ci/runner/components/runner_type_badge_spec.js
index 7a0fb6f69ea..f7ecd108967 100644
--- a/spec/frontend/ci/runner/components/runner_type_badge_spec.js
+++ b/spec/frontend/ci/runner/components/runner_type_badge_spec.js
@@ -2,6 +2,7 @@ import { GlBadge } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import RunnerTypeBadge from '~/ci/runner/components/runner_type_badge.vue';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
+import { assertProps } from 'helpers/assert_props';
import {
INSTANCE_TYPE,
GROUP_TYPE,
@@ -50,7 +51,7 @@ describe('RunnerTypeBadge', () => {
it('validation fails for an incorrect type', () => {
expect(() => {
- createComponent({ props: { type: 'AN_UNKNOWN_VALUE' } });
+ assertProps(RunnerTypeBadge, { type: 'AN_UNKNOWN_VALUE' });
}).toThrow();
});
diff --git a/spec/frontend/ci/runner/components/runner_type_tabs_spec.js b/spec/frontend/ci/runner/components/runner_type_tabs_spec.js
index 6e15c84ad7e..71dcc5b4226 100644
--- a/spec/frontend/ci/runner/components/runner_type_tabs_spec.js
+++ b/spec/frontend/ci/runner/components/runner_type_tabs_spec.js
@@ -8,6 +8,7 @@ import {
PROJECT_TYPE,
DEFAULT_MEMBERSHIP,
DEFAULT_SORT,
+ STATUS_ONLINE,
} from '~/ci/runner/constants';
const mockSearch = {
@@ -111,7 +112,7 @@ describe('RunnerTypeTabs', () => {
it('Renders a count next to each tab', () => {
const mockVariables = {
paused: true,
- status: 'ONLINE',
+ status: STATUS_ONLINE,
};
createComponent({
diff --git a/spec/frontend/ci/runner/components/stat/runner_count_spec.js b/spec/frontend/ci/runner/components/stat/runner_count_spec.js
index 42d8c9a1080..df774ba3e57 100644
--- a/spec/frontend/ci/runner/components/stat/runner_count_spec.js
+++ b/spec/frontend/ci/runner/components/stat/runner_count_spec.js
@@ -2,7 +2,7 @@ import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import { shallowMount } from '@vue/test-utils';
import RunnerCount from '~/ci/runner/components/stat/runner_count.vue';
-import { INSTANCE_TYPE, GROUP_TYPE } from '~/ci/runner/constants';
+import { INSTANCE_TYPE, GROUP_TYPE, STATUS_ONLINE } from '~/ci/runner/constants';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { captureException } from '~/ci/runner/sentry_utils';
@@ -47,7 +47,7 @@ describe('RunnerCount', () => {
});
describe('in admin scope', () => {
- const mockVariables = { status: 'ONLINE' };
+ const mockVariables = { status: STATUS_ONLINE };
beforeEach(async () => {
await createComponent({ props: { scope: INSTANCE_TYPE } });
@@ -67,7 +67,7 @@ describe('RunnerCount', () => {
expect(wrapper.html()).toBe(`<strong>${runnersCountData.data.runners.count}</strong>`);
});
- it('does not fetch from the group query', async () => {
+ it('does not fetch from the group query', () => {
expect(mockGroupRunnersCountHandler).not.toHaveBeenCalled();
});
@@ -89,7 +89,7 @@ describe('RunnerCount', () => {
await createComponent({ props: { scope: INSTANCE_TYPE, skip: true } });
});
- it('does not fetch data', async () => {
+ it('does not fetch data', () => {
expect(mockRunnersCountHandler).not.toHaveBeenCalled();
expect(mockGroupRunnersCountHandler).not.toHaveBeenCalled();
@@ -106,7 +106,7 @@ describe('RunnerCount', () => {
await createComponent({ props: { scope: INSTANCE_TYPE } });
});
- it('data is not shown and error is reported', async () => {
+ it('data is not shown and error is reported', () => {
expect(wrapper.html()).toBe('<strong></strong>');
expect(captureException).toHaveBeenCalledWith({
@@ -121,7 +121,7 @@ describe('RunnerCount', () => {
await createComponent({ props: { scope: GROUP_TYPE } });
});
- it('fetches data from the group query', async () => {
+ it('fetches data from the group query', () => {
expect(mockGroupRunnersCountHandler).toHaveBeenCalledTimes(1);
expect(mockGroupRunnersCountHandler).toHaveBeenCalledWith({});
@@ -141,7 +141,7 @@ describe('RunnerCount', () => {
wrapper.vm.refetch();
});
- it('data is not shown and error is reported', async () => {
+ it('data is not shown and error is reported', () => {
expect(mockRunnersCountHandler).toHaveBeenCalledTimes(2);
});
});
diff --git a/spec/frontend/ci/runner/group_new_runner_app/group_new_runner_app_spec.js b/spec/frontend/ci/runner/group_new_runner_app/group_new_runner_app_spec.js
new file mode 100644
index 00000000000..027196ab004
--- /dev/null
+++ b/spec/frontend/ci/runner/group_new_runner_app/group_new_runner_app_spec.js
@@ -0,0 +1,132 @@
+import { GlSprintf } from '@gitlab/ui';
+import { s__ } from '~/locale';
+
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
+import { createAlert, VARIANT_SUCCESS } from '~/alert';
+
+import GroupRunnerRunnerApp from '~/ci/runner/group_new_runner/group_new_runner_app.vue';
+import { saveAlertToLocalStorage } from '~/ci/runner/local_storage_alert/save_alert_to_local_storage';
+import RunnerInstructionsModal from '~/vue_shared/components/runner_instructions/runner_instructions_modal.vue';
+import RunnerPlatformsRadioGroup from '~/ci/runner/components/runner_platforms_radio_group.vue';
+import {
+ PARAM_KEY_PLATFORM,
+ GROUP_TYPE,
+ DEFAULT_PLATFORM,
+ WINDOWS_PLATFORM,
+} from '~/ci/runner/constants';
+import RunnerCreateForm from '~/ci/runner/components/runner_create_form.vue';
+import { redirectTo } from '~/lib/utils/url_utility';
+import { runnerCreateResult, mockRegistrationToken } from '../mock_data';
+
+const mockGroupId = 'gid://gitlab/Group/72';
+
+jest.mock('~/ci/runner/local_storage_alert/save_alert_to_local_storage');
+jest.mock('~/alert');
+jest.mock('~/lib/utils/url_utility', () => ({
+ ...jest.requireActual('~/lib/utils/url_utility'),
+ redirectTo: jest.fn(),
+}));
+
+const mockCreatedRunner = runnerCreateResult.data.runnerCreate.runner;
+
+describe('GroupRunnerRunnerApp', () => {
+ let wrapper;
+
+ const findLegacyInstructionsLink = () => wrapper.findByTestId('legacy-instructions-link');
+ const findRunnerInstructionsModal = () => wrapper.findComponent(RunnerInstructionsModal);
+ const findRunnerPlatformsRadioGroup = () => wrapper.findComponent(RunnerPlatformsRadioGroup);
+ const findRunnerCreateForm = () => wrapper.findComponent(RunnerCreateForm);
+
+ const createComponent = () => {
+ wrapper = shallowMountExtended(GroupRunnerRunnerApp, {
+ propsData: {
+ groupId: mockGroupId,
+ legacyRegistrationToken: mockRegistrationToken,
+ },
+ directives: {
+ GlModal: createMockDirective('gl-modal'),
+ },
+ stubs: {
+ GlSprintf,
+ },
+ });
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ describe('Shows legacy modal', () => {
+ it('passes legacy registration to modal', () => {
+ expect(findRunnerInstructionsModal().props('registrationToken')).toEqual(
+ mockRegistrationToken,
+ );
+ });
+
+ it('opens a modal with the legacy instructions', () => {
+ const modalId = getBinding(findLegacyInstructionsLink().element, 'gl-modal').value;
+
+ expect(findRunnerInstructionsModal().props('modalId')).toBe(modalId);
+ });
+ });
+
+ describe('Platform', () => {
+ it('shows the platforms radio group', () => {
+ expect(findRunnerPlatformsRadioGroup().props('value')).toBe(DEFAULT_PLATFORM);
+ });
+ });
+
+ describe('Runner form', () => {
+ it('shows the runner create form for an instance runner', () => {
+ expect(findRunnerCreateForm().props()).toEqual({
+ runnerType: GROUP_TYPE,
+ groupId: mockGroupId,
+ });
+ });
+
+ describe('When a runner is saved', () => {
+ beforeEach(() => {
+ findRunnerCreateForm().vm.$emit('saved', mockCreatedRunner);
+ });
+
+ it('pushes an alert to be shown after redirection', () => {
+ expect(saveAlertToLocalStorage).toHaveBeenCalledWith({
+ message: s__('Runners|Runner created.'),
+ variant: VARIANT_SUCCESS,
+ });
+ });
+
+ it('redirects to the registration page', () => {
+ const url = `${mockCreatedRunner.ephemeralRegisterUrl}?${PARAM_KEY_PLATFORM}=${DEFAULT_PLATFORM}`;
+
+ expect(redirectTo).toHaveBeenCalledWith(url);
+ });
+ });
+
+ describe('When another platform is selected and a runner is saved', () => {
+ beforeEach(() => {
+ findRunnerPlatformsRadioGroup().vm.$emit('input', WINDOWS_PLATFORM);
+ findRunnerCreateForm().vm.$emit('saved', mockCreatedRunner);
+ });
+
+ it('redirects to the registration page with the platform', () => {
+ const url = `${mockCreatedRunner.ephemeralRegisterUrl}?${PARAM_KEY_PLATFORM}=${WINDOWS_PLATFORM}`;
+
+ expect(redirectTo).toHaveBeenCalledWith(url);
+ });
+ });
+
+ describe('When runner fails to save', () => {
+ const ERROR_MSG = 'Cannot save!';
+
+ beforeEach(() => {
+ findRunnerCreateForm().vm.$emit('error', new Error(ERROR_MSG));
+ });
+
+ it('shows an error message', () => {
+ expect(createAlert).toHaveBeenCalledWith({ message: ERROR_MSG });
+ });
+ });
+ });
+});
diff --git a/spec/frontend/ci/runner/group_register_runner_app/group_register_runner_app_spec.js b/spec/frontend/ci/runner/group_register_runner_app/group_register_runner_app_spec.js
new file mode 100644
index 00000000000..2f0807c700c
--- /dev/null
+++ b/spec/frontend/ci/runner/group_register_runner_app/group_register_runner_app_spec.js
@@ -0,0 +1,120 @@
+import { nextTick } from 'vue';
+import { GlButton } from '@gitlab/ui';
+
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import setWindowLocation from 'helpers/set_window_location_helper';
+import { TEST_HOST } from 'helpers/test_constants';
+
+import { updateHistory } from '~/lib/utils/url_utility';
+import { PARAM_KEY_PLATFORM, DEFAULT_PLATFORM, WINDOWS_PLATFORM } from '~/ci/runner/constants';
+import GroupRegisterRunnerApp from '~/ci/runner/group_register_runner/group_register_runner_app.vue';
+import RegistrationInstructions from '~/ci/runner/components/registration/registration_instructions.vue';
+import PlatformsDrawer from '~/ci/runner/components/registration/platforms_drawer.vue';
+import { runnerForRegistration } from '../mock_data';
+
+const mockRunnerId = runnerForRegistration.data.runner.id;
+const mockRunnersPath = '/groups/group1/-/runners';
+
+jest.mock('~/lib/utils/url_utility', () => ({
+ ...jest.requireActual('~/lib/utils/url_utility'),
+ updateHistory: jest.fn(),
+}));
+
+describe('GroupRegisterRunnerApp', () => {
+ let wrapper;
+
+ const findRegistrationInstructions = () => wrapper.findComponent(RegistrationInstructions);
+ const findPlatformsDrawer = () => wrapper.findComponent(PlatformsDrawer);
+ const findBtn = () => wrapper.findComponent(GlButton);
+
+ const createComponent = () => {
+ wrapper = shallowMountExtended(GroupRegisterRunnerApp, {
+ propsData: {
+ runnerId: mockRunnerId,
+ runnersPath: mockRunnersPath,
+ },
+ });
+ };
+
+ describe('When showing runner details', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ describe('when runner token is available', () => {
+ it('shows registration instructions', () => {
+ expect(findRegistrationInstructions().props()).toEqual({
+ platform: DEFAULT_PLATFORM,
+ runnerId: mockRunnerId,
+ });
+ });
+
+ it('configures platform drawer', () => {
+ expect(findPlatformsDrawer().props()).toEqual({
+ open: false,
+ platform: DEFAULT_PLATFORM,
+ });
+ });
+
+ it('shows runner list button', () => {
+ expect(findBtn().attributes('href')).toBe(mockRunnersPath);
+ expect(findBtn().props('variant')).toBe('confirm');
+ });
+ });
+ });
+
+ describe('When another platform has been selected', () => {
+ beforeEach(() => {
+ setWindowLocation(`?${PARAM_KEY_PLATFORM}=${WINDOWS_PLATFORM}`);
+
+ createComponent();
+ });
+
+ it('shows registration instructions for the platform', () => {
+ expect(findRegistrationInstructions().props('platform')).toBe(WINDOWS_PLATFORM);
+ });
+ });
+
+ describe('When opening install instructions', () => {
+ beforeEach(() => {
+ createComponent();
+
+ findRegistrationInstructions().vm.$emit('toggleDrawer');
+ });
+
+ it('opens platform drawer', () => {
+ expect(findPlatformsDrawer().props('open')).toBe(true);
+ });
+
+ it('closes platform drawer', async () => {
+ findRegistrationInstructions().vm.$emit('toggleDrawer');
+ await nextTick();
+
+ expect(findPlatformsDrawer().props('open')).toBe(false);
+ });
+
+ it('closes platform drawer from drawer', async () => {
+ findPlatformsDrawer().vm.$emit('close');
+ await nextTick();
+
+ expect(findPlatformsDrawer().props('open')).toBe(false);
+ });
+
+ describe('when selecting a platform', () => {
+ beforeEach(() => {
+ findPlatformsDrawer().vm.$emit('selectPlatform', WINDOWS_PLATFORM);
+ });
+
+ it('updates the url', () => {
+ expect(updateHistory).toHaveBeenCalledTimes(1);
+ expect(updateHistory).toHaveBeenCalledWith({
+ url: `${TEST_HOST}/?${PARAM_KEY_PLATFORM}=${WINDOWS_PLATFORM}`,
+ });
+ });
+
+ it('updates the registration instructions', () => {
+ expect(findRegistrationInstructions().props('platform')).toBe(WINDOWS_PLATFORM);
+ });
+ });
+ });
+});
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 fadc6e5ebc5..60f51704c0e 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
@@ -83,7 +83,7 @@ describe('GroupRunnerShowApp', () => {
await createComponent({ mountFn: mountExtended });
});
- it('expect GraphQL ID to be requested', async () => {
+ it('expect GraphQL ID to be requested', () => {
expect(mockRunnerQuery).toHaveBeenCalledWith({ id: mockRunnerGraphqlId });
});
@@ -91,7 +91,7 @@ describe('GroupRunnerShowApp', () => {
expect(findRunnerHeader().text()).toContain(`Runner #${mockRunnerId}`);
});
- it('displays the runner edit and pause buttons', async () => {
+ it('displays the runner edit and pause buttons', () => {
expect(findRunnerEditButton().attributes('href')).toBe(mockEditGroupRunnerPath);
expect(findRunnerPauseButton().exists()).toBe(true);
expect(findRunnerDeleteButton().exists()).toBe(true);
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 00c7262e38b..6824242cba9 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
@@ -58,6 +58,8 @@ import {
groupRunnersCountData,
onlineContactTimeoutSecs,
staleTimeoutSecs,
+ mockRegistrationToken,
+ newRunnerPath,
emptyPageInfo,
emptyStateSvgPath,
emptyStateFilteredSvgPath,
@@ -67,7 +69,6 @@ Vue.use(VueApollo);
Vue.use(GlToast);
const mockGroupFullPath = 'group1';
-const mockRegistrationToken = 'AABBCC';
const mockGroupRunnersEdges = groupRunnersData.data.group.runners.edges;
const mockGroupRunnersCount = mockGroupRunnersEdges.length;
@@ -87,6 +88,7 @@ describe('GroupRunnersApp', () => {
const findRunnerStats = () => wrapper.findComponent(RunnerStats);
const findRunnerActionsCell = () => wrapper.findComponent(RunnerActionsCell);
const findRegistrationDropdown = () => wrapper.findComponent(RegistrationDropdown);
+ const findNewRunnerBtn = () => wrapper.findByText(s__('Runners|New group runner'));
const findRunnerTypeTabs = () => wrapper.findComponent(RunnerTypeTabs);
const findRunnerList = () => wrapper.findComponent(RunnerList);
const findRunnerListEmptyState = () => wrapper.findComponent(RunnerListEmptyState);
@@ -114,6 +116,7 @@ describe('GroupRunnersApp', () => {
propsData: {
registrationToken: mockRegistrationToken,
groupFullPath: mockGroupFullPath,
+ newRunnerPath,
...props,
},
provide: {
@@ -287,7 +290,7 @@ describe('GroupRunnersApp', () => {
});
});
- it('When runner is paused or unpaused, some data is refetched', async () => {
+ it('When runner is paused or unpaused, some data is refetched', () => {
expect(mockGroupRunnersCountHandler).toHaveBeenCalledTimes(COUNT_QUERIES);
findRunnerActionsCell().vm.$emit('toggledPaused');
@@ -299,7 +302,7 @@ describe('GroupRunnersApp', () => {
expect(showToast).toHaveBeenCalledTimes(0);
});
- it('When runner is deleted, data is refetched and a toast message is shown', async () => {
+ it('When runner is deleted, data is refetched and a toast message is shown', () => {
findRunnerActionsCell().vm.$emit('deleted', { message: 'Runner deleted' });
expect(showToast).toHaveBeenCalledTimes(1);
@@ -416,7 +419,7 @@ describe('GroupRunnersApp', () => {
expect(createAlert).not.toHaveBeenCalled();
});
- it('shows an empty state', async () => {
+ it('shows an empty state', () => {
expect(findRunnerListEmptyState().exists()).toBe(true);
});
});
@@ -427,11 +430,11 @@ describe('GroupRunnersApp', () => {
await createComponent();
});
- it('error is shown to the user', async () => {
+ it('error is shown to the user', () => {
expect(createAlert).toHaveBeenCalledTimes(1);
});
- it('error is reported to sentry', async () => {
+ it('error is reported to sentry', () => {
expect(captureException).toHaveBeenCalledWith({
error: new Error('Error!'),
component: 'GroupRunnersApp',
@@ -468,32 +471,69 @@ describe('GroupRunnersApp', () => {
});
describe('when user has permission to register group runner', () => {
- beforeEach(() => {
+ it('shows the register group runner button', () => {
createComponent({
- propsData: {
+ props: {
registrationToken: mockRegistrationToken,
- groupFullPath: mockGroupFullPath,
},
});
+ expect(findRegistrationDropdown().exists()).toBe(true);
});
- it('shows the register group runner button', () => {
- expect(findRegistrationDropdown().exists()).toBe(true);
+ it('when create_runner_workflow_for_namespace is enabled', () => {
+ createComponent({
+ props: {
+ newRunnerPath,
+ },
+ provide: {
+ glFeatures: {
+ createRunnerWorkflowForNamespace: true,
+ },
+ },
+ });
+
+ expect(findNewRunnerBtn().attributes('href')).toBe(newRunnerPath);
+ });
+
+ it('when create_runner_workflow_for_namespace is disabled', () => {
+ createComponent({
+ props: {
+ newRunnerPath,
+ },
+ provide: {
+ glFeatures: {
+ createRunnerWorkflowForNamespace: false,
+ },
+ },
+ });
+
+ expect(findNewRunnerBtn().exists()).toBe(false);
});
});
describe('when user has no permission to register group runner', () => {
- beforeEach(() => {
+ it('does not show the register group runner button', () => {
createComponent({
- propsData: {
+ props: {
registrationToken: null,
- groupFullPath: mockGroupFullPath,
},
});
+ expect(findRegistrationDropdown().exists()).toBe(false);
});
- it('does not show the register group runner button', () => {
- expect(findRegistrationDropdown().exists()).toBe(false);
+ it('when create_runner_workflow_for_namespace is enabled', () => {
+ createComponent({
+ props: {
+ newRunnerPath: null,
+ },
+ provide: {
+ glFeatures: {
+ createRunnerWorkflowForNamespace: true,
+ },
+ },
+ });
+
+ expect(findNewRunnerBtn().exists()).toBe(false);
});
});
});
diff --git a/spec/frontend/ci/runner/mock_data.js b/spec/frontend/ci/runner/mock_data.js
index 092a419c1fe..196005c9882 100644
--- a/spec/frontend/ci/runner/mock_data.js
+++ b/spec/frontend/ci/runner/mock_data.js
@@ -1,5 +1,14 @@
// Fixtures generated by: spec/frontend/fixtures/runner.rb
+// List queries
+import allRunnersData from 'test_fixtures/graphql/ci/runner/list/all_runners.query.graphql.json';
+import allRunnersDataPaginated from 'test_fixtures/graphql/ci/runner/list/all_runners.query.graphql.paginated.json';
+import allRunnersWithCreatorData from 'test_fixtures/graphql/ci/runner/list/all_runners.query.graphql.with_creator.json';
+import runnersCountData from 'test_fixtures/graphql/ci/runner/list/all_runners_count.query.graphql.json';
+import groupRunnersData from 'test_fixtures/graphql/ci/runner/list/group_runners.query.graphql.json';
+import groupRunnersDataPaginated from 'test_fixtures/graphql/ci/runner/list/group_runners.query.graphql.paginated.json';
+import groupRunnersCountData from 'test_fixtures/graphql/ci/runner/list/group_runners_count.query.graphql.json';
+
// Register runner queries
import runnerForRegistration from 'test_fixtures/graphql/ci/runner/register/runner_for_registration.query.graphql.json';
@@ -14,16 +23,15 @@ import runnerJobsData from 'test_fixtures/graphql/ci/runner/show/runner_jobs.que
import runnerFormData from 'test_fixtures/graphql/ci/runner/edit/runner_form.query.graphql.json';
// New runner queries
-
-// List queries
-import allRunnersData from 'test_fixtures/graphql/ci/runner/list/all_runners.query.graphql.json';
-import allRunnersDataPaginated from 'test_fixtures/graphql/ci/runner/list/all_runners.query.graphql.paginated.json';
-import runnersCountData from 'test_fixtures/graphql/ci/runner/list/all_runners_count.query.graphql.json';
-import groupRunnersData from 'test_fixtures/graphql/ci/runner/list/group_runners.query.graphql.json';
-import groupRunnersDataPaginated from 'test_fixtures/graphql/ci/runner/list/group_runners.query.graphql.paginated.json';
-import groupRunnersCountData from 'test_fixtures/graphql/ci/runner/list/group_runners_count.query.graphql.json';
-
-import { DEFAULT_MEMBERSHIP, RUNNER_PAGE_SIZE } from '~/ci/runner/constants';
+import {
+ DEFAULT_MEMBERSHIP,
+ INSTANCE_TYPE,
+ CREATED_DESC,
+ CREATED_ASC,
+ STATUS_ONLINE,
+ STATUS_STALE,
+ RUNNER_PAGE_SIZE,
+} from '~/ci/runner/constants';
import { FILTERED_SEARCH_TERM } from '~/vue_shared/components/filtered_search_bar/constants';
const emptyPageInfo = {
@@ -46,29 +54,29 @@ export const mockSearchExamples = [
membership: DEFAULT_MEMBERSHIP,
filters: [],
pagination: {},
- sort: 'CREATED_DESC',
+ sort: CREATED_DESC,
},
graphqlVariables: {
membership: DEFAULT_MEMBERSHIP,
- sort: 'CREATED_DESC',
+ sort: CREATED_DESC,
first: RUNNER_PAGE_SIZE,
},
isDefault: true,
},
{
name: 'a single status',
- urlQuery: '?status[]=ACTIVE',
+ urlQuery: '?status[]=ONLINE',
search: {
runnerType: null,
membership: DEFAULT_MEMBERSHIP,
- filters: [{ type: 'status', value: { data: 'ACTIVE', operator: '=' } }],
+ filters: [{ type: 'status', value: { data: STATUS_ONLINE, operator: '=' } }],
pagination: {},
- sort: 'CREATED_DESC',
+ sort: CREATED_DESC,
},
graphqlVariables: {
membership: DEFAULT_MEMBERSHIP,
- status: 'ACTIVE',
- sort: 'CREATED_DESC',
+ status: STATUS_ONLINE,
+ sort: CREATED_DESC,
first: RUNNER_PAGE_SIZE,
},
},
@@ -85,12 +93,12 @@ export const mockSearchExamples = [
},
],
pagination: {},
- sort: 'CREATED_DESC',
+ sort: CREATED_DESC,
},
graphqlVariables: {
membership: DEFAULT_MEMBERSHIP,
search: 'something',
- sort: 'CREATED_DESC',
+ sort: CREATED_DESC,
first: RUNNER_PAGE_SIZE,
},
},
@@ -111,12 +119,12 @@ export const mockSearchExamples = [
},
],
pagination: {},
- sort: 'CREATED_DESC',
+ sort: CREATED_DESC,
},
graphqlVariables: {
membership: DEFAULT_MEMBERSHIP,
search: 'something else',
- sort: 'CREATED_DESC',
+ sort: CREATED_DESC,
first: RUNNER_PAGE_SIZE,
},
},
@@ -124,54 +132,54 @@ export const mockSearchExamples = [
name: 'single instance type',
urlQuery: '?runner_type[]=INSTANCE_TYPE',
search: {
- runnerType: 'INSTANCE_TYPE',
+ runnerType: INSTANCE_TYPE,
membership: DEFAULT_MEMBERSHIP,
filters: [],
pagination: {},
- sort: 'CREATED_DESC',
+ sort: CREATED_DESC,
},
graphqlVariables: {
- type: 'INSTANCE_TYPE',
+ type: INSTANCE_TYPE,
membership: DEFAULT_MEMBERSHIP,
- sort: 'CREATED_DESC',
+ sort: CREATED_DESC,
first: RUNNER_PAGE_SIZE,
},
},
{
name: 'multiple runner status',
- urlQuery: '?status[]=ACTIVE&status[]=PAUSED',
+ urlQuery: '?status[]=ONLINE&status[]=STALE',
search: {
runnerType: null,
membership: DEFAULT_MEMBERSHIP,
filters: [
- { type: 'status', value: { data: 'ACTIVE', operator: '=' } },
- { type: 'status', value: { data: 'PAUSED', operator: '=' } },
+ { type: 'status', value: { data: STATUS_ONLINE, operator: '=' } },
+ { type: 'status', value: { data: STATUS_STALE, operator: '=' } },
],
pagination: {},
- sort: 'CREATED_DESC',
+ sort: CREATED_DESC,
},
graphqlVariables: {
- status: 'ACTIVE',
+ status: STATUS_ONLINE,
membership: DEFAULT_MEMBERSHIP,
- sort: 'CREATED_DESC',
+ sort: CREATED_DESC,
first: RUNNER_PAGE_SIZE,
},
},
{
name: 'multiple status, a single instance type and a non default sort',
- urlQuery: '?status[]=ACTIVE&runner_type[]=INSTANCE_TYPE&sort=CREATED_ASC',
+ urlQuery: '?status[]=ONLINE&runner_type[]=INSTANCE_TYPE&sort=CREATED_ASC',
search: {
- runnerType: 'INSTANCE_TYPE',
+ runnerType: INSTANCE_TYPE,
membership: DEFAULT_MEMBERSHIP,
- filters: [{ type: 'status', value: { data: 'ACTIVE', operator: '=' } }],
+ filters: [{ type: 'status', value: { data: STATUS_ONLINE, operator: '=' } }],
pagination: {},
- sort: 'CREATED_ASC',
+ sort: CREATED_ASC,
},
graphqlVariables: {
- status: 'ACTIVE',
- type: 'INSTANCE_TYPE',
+ status: STATUS_ONLINE,
+ type: INSTANCE_TYPE,
membership: DEFAULT_MEMBERSHIP,
- sort: 'CREATED_ASC',
+ sort: CREATED_ASC,
first: RUNNER_PAGE_SIZE,
},
},
@@ -183,13 +191,13 @@ export const mockSearchExamples = [
membership: DEFAULT_MEMBERSHIP,
filters: [{ type: 'tag', value: { data: 'tag-1', operator: '=' } }],
pagination: {},
- sort: 'CREATED_DESC',
+ sort: CREATED_DESC,
},
graphqlVariables: {
membership: DEFAULT_MEMBERSHIP,
tagList: ['tag-1'],
- first: 20,
- sort: 'CREATED_DESC',
+ first: RUNNER_PAGE_SIZE,
+ sort: CREATED_DESC,
},
},
{
@@ -203,13 +211,13 @@ export const mockSearchExamples = [
{ type: 'tag', value: { data: 'tag-2', operator: '=' } },
],
pagination: {},
- sort: 'CREATED_DESC',
+ sort: CREATED_DESC,
},
graphqlVariables: {
membership: DEFAULT_MEMBERSHIP,
tagList: ['tag-1', 'tag-2'],
- first: 20,
- sort: 'CREATED_DESC',
+ first: RUNNER_PAGE_SIZE,
+ sort: CREATED_DESC,
},
},
{
@@ -220,11 +228,11 @@ export const mockSearchExamples = [
membership: DEFAULT_MEMBERSHIP,
filters: [],
pagination: { after: 'AFTER_CURSOR' },
- sort: 'CREATED_DESC',
+ sort: CREATED_DESC,
},
graphqlVariables: {
membership: DEFAULT_MEMBERSHIP,
- sort: 'CREATED_DESC',
+ sort: CREATED_DESC,
after: 'AFTER_CURSOR',
first: RUNNER_PAGE_SIZE,
},
@@ -237,11 +245,11 @@ export const mockSearchExamples = [
membership: DEFAULT_MEMBERSHIP,
filters: [],
pagination: { before: 'BEFORE_CURSOR' },
- sort: 'CREATED_DESC',
+ sort: CREATED_DESC,
},
graphqlVariables: {
membership: DEFAULT_MEMBERSHIP,
- sort: 'CREATED_DESC',
+ sort: CREATED_DESC,
before: 'BEFORE_CURSOR',
last: RUNNER_PAGE_SIZE,
},
@@ -249,24 +257,24 @@ export const mockSearchExamples = [
{
name: 'the next page filtered by a status, an instance type, tags and a non default sort',
urlQuery:
- '?status[]=ACTIVE&runner_type[]=INSTANCE_TYPE&tag[]=tag-1&tag[]=tag-2&sort=CREATED_ASC&after=AFTER_CURSOR',
+ '?status[]=ONLINE&runner_type[]=INSTANCE_TYPE&tag[]=tag-1&tag[]=tag-2&sort=CREATED_ASC&after=AFTER_CURSOR',
search: {
- runnerType: 'INSTANCE_TYPE',
+ runnerType: INSTANCE_TYPE,
membership: DEFAULT_MEMBERSHIP,
filters: [
- { type: 'status', value: { data: 'ACTIVE', operator: '=' } },
+ { type: 'status', value: { data: STATUS_ONLINE, operator: '=' } },
{ type: 'tag', value: { data: 'tag-1', operator: '=' } },
{ type: 'tag', value: { data: 'tag-2', operator: '=' } },
],
pagination: { after: 'AFTER_CURSOR' },
- sort: 'CREATED_ASC',
+ sort: CREATED_ASC,
},
graphqlVariables: {
- status: 'ACTIVE',
- type: 'INSTANCE_TYPE',
+ status: STATUS_ONLINE,
+ type: INSTANCE_TYPE,
membership: DEFAULT_MEMBERSHIP,
tagList: ['tag-1', 'tag-2'],
- sort: 'CREATED_ASC',
+ sort: CREATED_ASC,
after: 'AFTER_CURSOR',
first: RUNNER_PAGE_SIZE,
},
@@ -279,12 +287,12 @@ export const mockSearchExamples = [
membership: DEFAULT_MEMBERSHIP,
filters: [{ type: 'paused', value: { data: 'true', operator: '=' } }],
pagination: {},
- sort: 'CREATED_DESC',
+ sort: CREATED_DESC,
},
graphqlVariables: {
paused: true,
membership: DEFAULT_MEMBERSHIP,
- sort: 'CREATED_DESC',
+ sort: CREATED_DESC,
first: RUNNER_PAGE_SIZE,
},
},
@@ -296,12 +304,12 @@ export const mockSearchExamples = [
membership: DEFAULT_MEMBERSHIP,
filters: [{ type: 'paused', value: { data: 'false', operator: '=' } }],
pagination: {},
- sort: 'CREATED_DESC',
+ sort: CREATED_DESC,
},
graphqlVariables: {
paused: false,
membership: DEFAULT_MEMBERSHIP,
- sort: 'CREATED_DESC',
+ sort: CREATED_DESC,
first: RUNNER_PAGE_SIZE,
},
},
@@ -310,12 +318,16 @@ export const mockSearchExamples = [
export const onlineContactTimeoutSecs = 2 * 60 * 60;
export const staleTimeoutSecs = 7889238; // Ruby's `3.months`
+export const mockRegistrationToken = 'MOCK_REGISTRATION_TOKEN';
+export const mockAuthenticationToken = 'MOCK_AUTHENTICATION_TOKEN';
+
export const newRunnerPath = '/runners/new';
export const emptyStateSvgPath = 'emptyStateSvgPath.svg';
export const emptyStateFilteredSvgPath = 'emptyStateFilteredSvgPath.svg';
export {
allRunnersData,
+ allRunnersWithCreatorData,
allRunnersDataPaginated,
runnersCountData,
groupRunnersData,
diff --git a/spec/frontend/ci/runner/runner_search_utils_spec.js b/spec/frontend/ci/runner/runner_search_utils_spec.js
index f64b89d47fd..9a4a6139198 100644
--- a/spec/frontend/ci/runner/runner_search_utils_spec.js
+++ b/spec/frontend/ci/runner/runner_search_utils_spec.js
@@ -7,6 +7,7 @@ import {
isSearchFiltered,
} from 'ee_else_ce/ci/runner/runner_search_utils';
import { FILTERED_SEARCH_TERM } from '~/vue_shared/components/filtered_search_bar/constants';
+import { DEFAULT_SORT } from '~/ci/runner/constants';
import { mockSearchExamples } from './mock_data';
describe('search_params.js', () => {
@@ -68,7 +69,7 @@ describe('search_params.js', () => {
'http://test.host/?paused[]=true',
'http://test.host/?search=my_text',
])('When a filter is removed, it is removed from the URL', (initialUrl) => {
- const search = { filters: [], sort: 'CREATED_DESC' };
+ const search = { filters: [], sort: DEFAULT_SORT };
const expectedUrl = `http://test.host/`;
expect(fromSearchToUrl(search, initialUrl)).toBe(expectedUrl);
@@ -76,7 +77,7 @@ describe('search_params.js', () => {
it('When unrelated search parameter is present, it does not get removed', () => {
const initialUrl = `http://test.host/?unrelated=UNRELATED&status[]=ACTIVE`;
- const search = { filters: [], sort: 'CREATED_DESC' };
+ const search = { filters: [], sort: DEFAULT_SORT };
const expectedUrl = `http://test.host/?unrelated=UNRELATED`;
expect(fromSearchToUrl(search, initialUrl)).toBe(expectedUrl);
diff --git a/spec/frontend/ci/runner/sentry_utils_spec.js b/spec/frontend/ci/runner/sentry_utils_spec.js
index f7b689272ce..2f17cc43ac5 100644
--- a/spec/frontend/ci/runner/sentry_utils_spec.js
+++ b/spec/frontend/ci/runner/sentry_utils_spec.js
@@ -6,7 +6,7 @@ jest.mock('@sentry/browser');
describe('~/ci/runner/sentry_utils', () => {
let mockSetTag;
- beforeEach(async () => {
+ beforeEach(() => {
mockSetTag = jest.fn();
Sentry.withScope.mockImplementation((fn) => {
diff --git a/spec/frontend/clusters/agents/components/create_token_modal_spec.js b/spec/frontend/clusters/agents/components/create_token_modal_spec.js
index ff698952c6b..42e6a70ee26 100644
--- a/spec/frontend/clusters/agents/components/create_token_modal_spec.js
+++ b/spec/frontend/clusters/agents/components/create_token_modal_spec.js
@@ -213,7 +213,7 @@ describe('CreateTokenModal', () => {
await mockCreatedResponse(createAgentTokenErrorResponse);
});
- it('displays the error message', async () => {
+ it('displays the error message', () => {
expect(findAlert().text()).toBe(
createAgentTokenErrorResponse.data.clusterAgentTokenCreate.errors[0],
);
diff --git a/spec/frontend/clusters/clusters_bundle_spec.js b/spec/frontend/clusters/clusters_bundle_spec.js
index a2ec19c5b4a..d657566713f 100644
--- a/spec/frontend/clusters/clusters_bundle_spec.js
+++ b/spec/frontend/clusters/clusters_bundle_spec.js
@@ -1,5 +1,6 @@
import MockAdapter from 'axios-mock-adapter';
-import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
+import htmlShowCluster from 'test_fixtures/clusters/show_cluster.html';
+import { setHTMLFixture, 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';
@@ -24,7 +25,7 @@ describe('Clusters', () => {
};
beforeEach(() => {
- loadHTMLFixture('clusters/show_cluster.html');
+ setHTMLFixture(htmlShowCluster);
mockGetClusterStatusRequest();
diff --git a/spec/frontend/clusters_list/components/agent_table_spec.js b/spec/frontend/clusters_list/components/agent_table_spec.js
index 9cbb83eedd2..0f68a69458e 100644
--- a/spec/frontend/clusters_list/components/agent_table_spec.js
+++ b/spec/frontend/clusters_list/components/agent_table_spec.js
@@ -13,9 +13,9 @@ const defaultConfigHelpUrl =
const provideData = {
gitlabVersion: '14.8',
- kasVersion: '14.8',
+ kasVersion: '14.8.0',
};
-const propsData = {
+const defaultProps = {
agents: clusterAgents,
};
@@ -26,9 +26,6 @@ const DeleteAgentButtonStub = stubComponent(DeleteAgentButton, {
const outdatedTitle = I18N_AGENT_TABLE.versionOutdatedTitle;
const mismatchTitle = I18N_AGENT_TABLE.versionMismatchTitle;
const mismatchOutdatedTitle = I18N_AGENT_TABLE.versionMismatchOutdatedTitle;
-const outdatedText = sprintf(I18N_AGENT_TABLE.versionOutdatedText, {
- version: provideData.kasVersion,
-});
const mismatchText = I18N_AGENT_TABLE.versionMismatchText;
describe('AgentTable', () => {
@@ -39,127 +36,150 @@ describe('AgentTable', () => {
const findStatusIcon = (at) => findStatusText(at).findComponent(GlIcon);
const findLastContactText = (at) => wrapper.findAllByTestId('cluster-agent-last-contact').at(at);
const findVersionText = (at) => wrapper.findAllByTestId('cluster-agent-version').at(at);
+ const findAgentId = (at) => wrapper.findAllByTestId('cluster-agent-id').at(at);
const findConfiguration = (at) =>
wrapper.findAllByTestId('cluster-agent-configuration-link').at(at);
const findDeleteAgentButton = () => wrapper.findAllComponents(DeleteAgentButton);
- beforeEach(() => {
+ const createWrapper = ({ provide = provideData, propsData = defaultProps } = {}) => {
wrapper = mountExtended(AgentTable, {
propsData,
- provide: provideData,
+ provide,
stubs: {
DeleteAgentButton: DeleteAgentButtonStub,
},
});
- });
-
- afterEach(() => {
- if (wrapper) {
- wrapper.destroy();
- }
- });
+ };
describe('agent table', () => {
- it.each`
- agentName | link | lineNumber
- ${'agent-1'} | ${'/agent-1'} | ${0}
- ${'agent-2'} | ${'/agent-2'} | ${1}
- `('displays agent link for $agentName', ({ agentName, link, lineNumber }) => {
- expect(findAgentLink(lineNumber).text()).toBe(agentName);
- expect(findAgentLink(lineNumber).attributes('href')).toBe(link);
+ describe('default', () => {
+ beforeEach(() => {
+ createWrapper();
+ });
+
+ it.each`
+ agentName | link | lineNumber
+ ${'agent-1'} | ${'/agent-1'} | ${0}
+ ${'agent-2'} | ${'/agent-2'} | ${1}
+ `('displays agent link for $agentName', ({ agentName, link, lineNumber }) => {
+ expect(findAgentLink(lineNumber).text()).toBe(agentName);
+ expect(findAgentLink(lineNumber).attributes('href')).toBe(link);
+ });
+
+ it.each`
+ agentGraphQLId | agentId | lineNumber
+ ${'gid://gitlab/Clusters::Agent/1'} | ${'1'} | ${0}
+ ${'gid://gitlab/Clusters::Agent/2'} | ${'2'} | ${1}
+ `(
+ 'displays agent id as "$agentId" for "$agentGraphQLId" at line $lineNumber',
+ ({ agentId, lineNumber }) => {
+ expect(findAgentId(lineNumber).text()).toBe(agentId);
+ },
+ );
+
+ it.each`
+ status | iconName | lineNumber
+ ${'Never connected'} | ${'status-neutral'} | ${0}
+ ${'Connected'} | ${'status-success'} | ${1}
+ ${'Not connected'} | ${'status-alert'} | ${2}
+ `(
+ 'displays agent connection status as "$status" at line $lineNumber',
+ ({ status, iconName, lineNumber }) => {
+ expect(findStatusText(lineNumber).text()).toBe(status);
+ expect(findStatusIcon(lineNumber).props('name')).toBe(iconName);
+ },
+ );
+
+ it.each`
+ lastContact | lineNumber
+ ${'Never'} | ${0}
+ ${timeagoMixin.methods.timeFormatted(connectedTimeNow)} | ${1}
+ ${timeagoMixin.methods.timeFormatted(connectedTimeInactive)} | ${2}
+ `(
+ 'displays agent last contact time as "$lastContact" at line $lineNumber',
+ ({ lastContact, lineNumber }) => {
+ expect(findLastContactText(lineNumber).text()).toBe(lastContact);
+ },
+ );
+
+ it.each`
+ agentConfig | link | lineNumber
+ ${'.gitlab/agents/agent-1'} | ${'/agent/full/path'} | ${0}
+ ${'Default configuration'} | ${defaultConfigHelpUrl} | ${1}
+ `(
+ 'displays config file path as "$agentPath" at line $lineNumber',
+ ({ agentConfig, link, lineNumber }) => {
+ const findLink = findConfiguration(lineNumber).findComponent(GlLink);
+
+ expect(findLink.attributes('href')).toBe(link);
+ expect(findConfiguration(lineNumber).text()).toBe(agentConfig);
+ },
+ );
+
+ it('displays actions menu for each agent', () => {
+ expect(findDeleteAgentButton()).toHaveLength(clusterAgents.length);
+ });
});
- it.each`
- status | iconName | lineNumber
- ${'Never connected'} | ${'status-neutral'} | ${0}
- ${'Connected'} | ${'status-success'} | ${1}
- ${'Not connected'} | ${'status-alert'} | ${2}
+ describe.each`
+ agentMockIdx | agentVersion | kasVersion | versionMismatch | versionOutdated | title
+ ${0} | ${''} | ${'14.8.0'} | ${false} | ${false} | ${''}
+ ${1} | ${'14.8.0'} | ${'14.8.0'} | ${false} | ${false} | ${''}
+ ${2} | ${'14.6.0'} | ${'14.8.0'} | ${false} | ${true} | ${outdatedTitle}
+ ${3} | ${'14.7.0'} | ${'14.8.0'} | ${true} | ${false} | ${mismatchTitle}
+ ${4} | ${'14.3.0'} | ${'14.8.0'} | ${true} | ${true} | ${mismatchOutdatedTitle}
+ ${5} | ${'14.6.0'} | ${'14.8.0-rc1'} | ${false} | ${false} | ${''}
+ ${6} | ${'14.8.0'} | ${'15.0.0'} | ${false} | ${true} | ${outdatedTitle}
+ ${7} | ${'14.8.0'} | ${'15.0.0-rc1'} | ${false} | ${true} | ${outdatedTitle}
+ ${8} | ${'14.8.0'} | ${'14.8.10'} | ${false} | ${false} | ${''}
`(
- 'displays agent connection status as "$status" at line $lineNumber',
- ({ status, iconName, lineNumber }) => {
- expect(findStatusText(lineNumber).text()).toBe(status);
- expect(findStatusIcon(lineNumber).props('name')).toBe(iconName);
- },
- );
+ 'when agent version is "$agentVersion", KAS version is "$kasVersion" and version mismatch is "$versionMismatch"',
+ ({ agentMockIdx, agentVersion, kasVersion, versionMismatch, versionOutdated, title }) => {
+ const currentAgent = clusterAgents[agentMockIdx];
- it.each`
- lastContact | lineNumber
- ${'Never'} | ${0}
- ${timeagoMixin.methods.timeFormatted(connectedTimeNow)} | ${1}
- ${timeagoMixin.methods.timeFormatted(connectedTimeInactive)} | ${2}
- `(
- 'displays agent last contact time as "$lastContact" at line $lineNumber',
- ({ lastContact, lineNumber }) => {
- expect(findLastContactText(lineNumber).text()).toBe(lastContact);
- },
- );
+ const findIcon = () => findVersionText(0).findComponent(GlIcon);
+ const findPopover = () => wrapper.findByTestId(`popover-${currentAgent.name}`);
- describe.each`
- agent | version | podsNumber | versionMismatch | versionOutdated | title | texts | lineNumber
- ${'agent-1'} | ${''} | ${1} | ${false} | ${false} | ${''} | ${''} | ${0}
- ${'agent-2'} | ${'14.8'} | ${2} | ${false} | ${false} | ${''} | ${''} | ${1}
- ${'agent-3'} | ${'14.5'} | ${1} | ${false} | ${true} | ${outdatedTitle} | ${[outdatedText]} | ${2}
- ${'agent-4'} | ${'14.7'} | ${2} | ${true} | ${false} | ${mismatchTitle} | ${[mismatchText]} | ${3}
- ${'agent-5'} | ${'14.3'} | ${2} | ${true} | ${true} | ${mismatchOutdatedTitle} | ${[mismatchText, outdatedText]} | ${4}
- `(
- 'agent version column at line $lineNumber',
- ({
- agent,
- version,
- podsNumber,
- versionMismatch,
- versionOutdated,
- title,
- texts,
- lineNumber,
- }) => {
- const findIcon = () => findVersionText(lineNumber).findComponent(GlIcon);
- const findPopover = () => wrapper.findByTestId(`popover-${agent}`);
const versionWarning = versionMismatch || versionOutdated;
+ const outdatedText = sprintf(I18N_AGENT_TABLE.versionOutdatedText, {
+ version: kasVersion,
+ });
- it('shows the correct agent version', () => {
- expect(findVersionText(lineNumber).text()).toBe(version);
+ beforeEach(() => {
+ createWrapper({
+ provide: { gitlabVersion: '14.8', kasVersion },
+ propsData: { agents: [currentAgent] },
+ });
+ });
+
+ it('shows the correct agent version text', () => {
+ expect(findVersionText(0).text()).toBe(agentVersion);
});
if (versionWarning) {
- it(`shows a warning icon when agent versions mismatch is ${versionMismatch} and outdated is ${versionOutdated} and the number of pods is ${podsNumber}`, () => {
+ it('shows a warning icon', () => {
expect(findIcon().props('name')).toBe('warning');
});
-
it(`renders correct title for the popover when agent versions mismatch is ${versionMismatch} and outdated is ${versionOutdated}`, () => {
expect(findPopover().props('title')).toBe(title);
});
-
- it(`renders correct text for the popover when agent versions mismatch is ${versionMismatch} and outdated is ${versionOutdated}`, () => {
- texts.forEach((text) => {
- expect(findPopover().text()).toContain(text);
+ if (versionMismatch) {
+ it(`renders correct text for the popover when agent versions mismatch is ${versionMismatch}`, () => {
+ expect(findPopover().text()).toContain(mismatchText);
});
- });
+ }
+ if (versionOutdated) {
+ it(`renders correct text for the popover when agent versions outdated is ${versionOutdated}`, () => {
+ expect(findPopover().text()).toContain(outdatedText);
+ });
+ }
} else {
- it(`doesn't show a warning icon with a popover when agent versions mismatch is ${versionMismatch} and outdated is ${versionOutdated} and the number of pods is ${podsNumber}`, () => {
+ it(`doesn't show a warning icon with a popover when agent versions mismatch is ${versionMismatch} and outdated is ${versionOutdated}`, () => {
expect(findIcon().exists()).toBe(false);
expect(findPopover().exists()).toBe(false);
});
}
},
);
-
- it.each`
- agentConfig | link | lineNumber
- ${'.gitlab/agents/agent-1'} | ${'/agent/full/path'} | ${0}
- ${'Default configuration'} | ${defaultConfigHelpUrl} | ${1}
- `(
- 'displays config file path as "$agentPath" at line $lineNumber',
- ({ agentConfig, link, lineNumber }) => {
- const findLink = findConfiguration(lineNumber).findComponent(GlLink);
-
- expect(findLink.attributes('href')).toBe(link);
- expect(findConfiguration(lineNumber).text()).toBe(agentConfig);
- },
- );
-
- it('displays actions menu for each agent', () => {
- expect(findDeleteAgentButton()).toHaveLength(5);
- });
});
});
diff --git a/spec/frontend/clusters_list/components/install_agent_modal_spec.js b/spec/frontend/clusters_list/components/install_agent_modal_spec.js
index 3156eaaecfc..f9009696c7b 100644
--- a/spec/frontend/clusters_list/components/install_agent_modal_spec.js
+++ b/spec/frontend/clusters_list/components/install_agent_modal_spec.js
@@ -256,7 +256,7 @@ describe('InstallAgentModal', () => {
return mockSelectedAgentResponse();
});
- it('displays the error message', async () => {
+ it('displays the error message', () => {
expect(findAlert().text()).toBe(
createAgentTokenErrorResponse.data.clusterAgentTokenCreate.errors[0],
);
diff --git a/spec/frontend/clusters_list/components/mock_data.js b/spec/frontend/clusters_list/components/mock_data.js
index 3d18b22d727..af1fb496118 100644
--- a/spec/frontend/clusters_list/components/mock_data.js
+++ b/spec/frontend/clusters_list/components/mock_data.js
@@ -19,7 +19,7 @@ export const connectedTimeInactive = new Date(connectedTimeNow.getTime() - ACTIV
export const clusterAgents = [
{
name: 'agent-1',
- id: 'agent-1-id',
+ id: 'gid://gitlab/Clusters::Agent/1',
configFolder: {
webPath: '/agent/full/path',
},
@@ -30,17 +30,17 @@ export const clusterAgents = [
},
{
name: 'agent-2',
- id: 'agent-2-id',
+ id: 'gid://gitlab/Clusters::Agent/2',
webPath: '/agent-2',
status: 'active',
lastContact: connectedTimeNow.getTime(),
connections: {
nodes: [
{
- metadata: { version: 'v14.8' },
+ metadata: { version: 'v14.8.0' },
},
{
- metadata: { version: 'v14.8' },
+ metadata: { version: 'v14.8.0' },
},
],
},
@@ -54,14 +54,14 @@ export const clusterAgents = [
},
{
name: 'agent-3',
- id: 'agent-3-id',
+ id: 'gid://gitlab/Clusters::Agent/3',
webPath: '/agent-3',
status: 'inactive',
lastContact: connectedTimeInactive.getTime(),
connections: {
nodes: [
{
- metadata: { version: 'v14.5' },
+ metadata: { version: 'v14.6.0' },
},
],
},
@@ -75,17 +75,17 @@ export const clusterAgents = [
},
{
name: 'agent-4',
- id: 'agent-4-id',
+ id: 'gid://gitlab/Clusters::Agent/4',
webPath: '/agent-4',
status: 'inactive',
lastContact: connectedTimeInactive.getTime(),
connections: {
nodes: [
{
- metadata: { version: 'v14.7' },
+ metadata: { version: 'v14.7.0' },
},
{
- metadata: { version: 'v14.8' },
+ metadata: { version: 'v14.8.0' },
},
],
},
@@ -99,17 +99,101 @@ export const clusterAgents = [
},
{
name: 'agent-5',
- id: 'agent-5-id',
+ id: 'gid://gitlab/Clusters::Agent/5',
webPath: '/agent-5',
status: 'inactive',
lastContact: connectedTimeInactive.getTime(),
connections: {
nodes: [
{
- metadata: { version: 'v14.5' },
+ metadata: { version: 'v14.5.0' },
},
{
- metadata: { version: 'v14.3' },
+ metadata: { version: 'v14.3.0' },
+ },
+ ],
+ },
+ tokens: {
+ nodes: [
+ {
+ lastUsedAt: connectedTimeInactive,
+ },
+ ],
+ },
+ },
+ {
+ name: 'agent-6',
+ id: 'gid://gitlab/Clusters::Agent/6',
+ webPath: '/agent-6',
+ status: 'inactive',
+ lastContact: connectedTimeInactive.getTime(),
+ connections: {
+ nodes: [
+ {
+ metadata: { version: 'v14.6.0' },
+ },
+ ],
+ },
+ tokens: {
+ nodes: [
+ {
+ lastUsedAt: connectedTimeInactive,
+ },
+ ],
+ },
+ },
+ {
+ name: 'agent-7',
+ id: 'gid://gitlab/Clusters::Agent/7',
+ webPath: '/agent-7',
+ status: 'inactive',
+ lastContact: connectedTimeInactive.getTime(),
+ connections: {
+ nodes: [
+ {
+ metadata: { version: 'v14.8.0' },
+ },
+ ],
+ },
+ tokens: {
+ nodes: [
+ {
+ lastUsedAt: connectedTimeInactive,
+ },
+ ],
+ },
+ },
+ {
+ name: 'agent-8',
+ id: 'gid://gitlab/Clusters::Agent/8',
+ webPath: '/agent-8',
+ status: 'inactive',
+ lastContact: connectedTimeInactive.getTime(),
+ connections: {
+ nodes: [
+ {
+ metadata: { version: 'v14.8.0' },
+ },
+ ],
+ },
+ tokens: {
+ nodes: [
+ {
+ lastUsedAt: connectedTimeInactive,
+ },
+ ],
+ },
+ },
+ {
+ name: 'agent-9',
+ id: 'gid://gitlab/Clusters::Agent/9',
+ webPath: '/agent-9',
+ status: 'inactive',
+ lastContact: connectedTimeInactive.getTime(),
+ connections: {
+ nodes: [
+ {
+ metadata: { version: 'v14.8.0' },
},
],
},
diff --git a/spec/frontend/comment_templates/components/__snapshots__/list_item_spec.js.snap b/spec/frontend/comment_templates/components/__snapshots__/list_item_spec.js.snap
new file mode 100644
index 00000000000..c979ee5a1d2
--- /dev/null
+++ b/spec/frontend/comment_templates/components/__snapshots__/list_item_spec.js.snap
@@ -0,0 +1,140 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Comment templates list item component renders list item 1`] = `
+<li
+ class="gl-pt-4 gl-pb-5 gl-border-b"
+>
+ <div
+ class="gl-display-flex gl-align-items-center"
+ >
+ <h6
+ class="gl-mr-3 gl-my-0"
+ data-testid="comment-template-name"
+ >
+ test
+ </h6>
+
+ <div
+ class="gl-ml-auto"
+ >
+ <div
+ class="gl-new-dropdown gl-disclosure-dropdown"
+ >
+ <button
+ aria-controls="base-dropdown-5"
+ aria-labelledby="actions-toggle-3"
+ class="btn btn-default btn-md gl-button btn-default-tertiary gl-new-dropdown-toggle gl-new-dropdown-icon-only gl-new-dropdown-toggle-no-caret"
+ data-testid="base-dropdown-toggle"
+ id="actions-toggle-3"
+ listeners="[object Object]"
+ type="button"
+ >
+ <!---->
+
+ <svg
+ aria-hidden="true"
+ class="gl-button-icon gl-icon s16"
+ data-testid="ellipsis_v-icon"
+ role="img"
+ >
+ <use
+ href="#ellipsis_v"
+ />
+ </svg>
+
+ <span
+ class="gl-button-text"
+ >
+ <span
+ class="gl-new-dropdown-button-text gl-sr-only"
+ >
+
+ Comment template actions
+
+ </span>
+
+ <!---->
+ </span>
+ </button>
+
+ <div
+ class="gl-new-dropdown-panel"
+ data-testid="base-dropdown-menu"
+ id="base-dropdown-5"
+ >
+ <div
+ class="gl-new-dropdown-inner"
+ >
+
+ <ul
+ aria-labelledby="actions-toggle-3"
+ class="gl-new-dropdown-contents"
+ data-testid="disclosure-content"
+ id="disclosure-4"
+ tabindex="-1"
+ >
+ <li
+ class="gl-new-dropdown-item"
+ data-testid="disclosure-dropdown-item"
+ tabindex="0"
+ >
+ <button
+ class="gl-new-dropdown-item-content"
+ data-testid="comment-template-edit-btn"
+ tabindex="-1"
+ type="button"
+ >
+ <span
+ class="gl-new-dropdown-item-text-wrapper"
+ >
+
+ Edit
+
+ </span>
+ </button>
+ </li>
+ <li
+ class="gl-new-dropdown-item"
+ data-testid="disclosure-dropdown-item"
+ tabindex="0"
+ >
+ <button
+ class="gl-new-dropdown-item-content gl-text-red-500!"
+ data-testid="comment-template-delete-btn"
+ tabindex="-1"
+ type="button"
+ >
+ <span
+ class="gl-new-dropdown-item-text-wrapper"
+ >
+
+ Delete
+
+ </span>
+ </button>
+ </li>
+ </ul>
+
+ </div>
+ </div>
+ </div>
+
+ <div
+ class="gl-tooltip"
+ >
+
+ Comment template actions
+
+ </div>
+ </div>
+ </div>
+
+ <div
+ class="gl-mt-3 gl-font-monospace"
+ >
+ /assign_reviewer
+ </div>
+
+ <!---->
+</li>
+`;
diff --git a/spec/frontend/saved_replies/components/form_spec.js b/spec/frontend/comment_templates/components/form_spec.js
index adeda498e6f..053a5099c37 100644
--- a/spec/frontend/saved_replies/components/form_spec.js
+++ b/spec/frontend/comment_templates/components/form_spec.js
@@ -2,13 +2,13 @@ import Vue, { nextTick } from 'vue';
import { mount } from '@vue/test-utils';
import { GlAlert } from '@gitlab/ui';
import VueApollo from 'vue-apollo';
-import createdSavedReplyResponse from 'test_fixtures/graphql/saved_replies/create_saved_reply.mutation.graphql.json';
-import createdSavedReplyErrorResponse from 'test_fixtures/graphql/saved_replies/create_saved_reply_with_errors.mutation.graphql.json';
+import createdSavedReplyResponse from 'test_fixtures/graphql/comment_templates/create_saved_reply.mutation.graphql.json';
+import createdSavedReplyErrorResponse from 'test_fixtures/graphql/comment_templates/create_saved_reply_with_errors.mutation.graphql.json';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import Form from '~/saved_replies/components/form.vue';
-import createSavedReplyMutation from '~/saved_replies/queries/create_saved_reply.mutation.graphql';
-import updateSavedReplyMutation from '~/saved_replies/queries/update_saved_reply.mutation.graphql';
+import Form from '~/comment_templates/components/form.vue';
+import createSavedReplyMutation from '~/comment_templates/queries/create_saved_reply.mutation.graphql';
+import updateSavedReplyMutation from '~/comment_templates/queries/update_saved_reply.mutation.graphql';
let wrapper;
let createSavedReplyResponseSpy;
@@ -39,18 +39,19 @@ function createComponent(id = null, response = createdSavedReplyResponse) {
});
}
-const findSavedReplyNameInput = () => wrapper.find('[data-testid="saved-reply-name-input"]');
+const findSavedReplyNameInput = () => wrapper.find('[data-testid="comment-template-name-input"]');
const findSavedReplyNameFormGroup = () =>
- wrapper.find('[data-testid="saved-reply-name-form-group"]');
-const findSavedReplyContentInput = () => wrapper.find('[data-testid="saved-reply-content-input"]');
+ wrapper.find('[data-testid="comment-template-name-form-group"]');
+const findSavedReplyContentInput = () =>
+ wrapper.find('[data-testid="comment-template-content-input"]');
const findSavedReplyContentFormGroup = () =>
- wrapper.find('[data-testid="saved-reply-content-form-group"]');
-const findSavedReplyFrom = () => wrapper.find('[data-testid="saved-reply-form"]');
+ wrapper.find('[data-testid="comment-template-content-form-group"]');
+const findSavedReplyFrom = () => wrapper.find('[data-testid="comment-template-form"]');
const findAlerts = () => wrapper.findAllComponents(GlAlert);
-const findSubmitBtn = () => wrapper.find('[data-testid="saved-reply-form-submit-btn"]');
+const findSubmitBtn = () => wrapper.find('[data-testid="comment-template-form-submit-btn"]');
-describe('Saved replies form component', () => {
- describe('create saved reply', () => {
+describe('Comment templates form component', () => {
+ describe('creates comment template', () => {
it('calls apollo mutation', async () => {
wrapper = createComponent();
diff --git a/spec/frontend/comment_templates/components/list_item_spec.js b/spec/frontend/comment_templates/components/list_item_spec.js
new file mode 100644
index 00000000000..925d78da4ad
--- /dev/null
+++ b/spec/frontend/comment_templates/components/list_item_spec.js
@@ -0,0 +1,154 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import { mount } from '@vue/test-utils';
+import { GlDisclosureDropdown, GlDisclosureDropdownItem, GlModal } from '@gitlab/ui';
+import { __ } from '~/locale';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import { createMockDirective } from 'helpers/vue_mock_directive';
+import ListItem from '~/comment_templates/components/list_item.vue';
+import deleteSavedReplyMutation from '~/comment_templates/queries/delete_saved_reply.mutation.graphql';
+
+function createMockApolloProvider(requestHandlers = [deleteSavedReplyMutation]) {
+ Vue.use(VueApollo);
+
+ return createMockApollo([requestHandlers]);
+}
+
+describe('Comment templates list item component', () => {
+ let wrapper;
+ let $router;
+
+ function createComponent(propsData = {}, apolloProvider = createMockApolloProvider) {
+ $router = {
+ push: jest.fn(),
+ };
+
+ return mount(ListItem, {
+ propsData,
+ directives: {
+ GlModal: createMockDirective('gl-modal'),
+ },
+ apolloProvider,
+ mocks: {
+ $router,
+ },
+ });
+ }
+
+ const findDropdown = () => wrapper.findComponent(GlDisclosureDropdown);
+ const findDropdownItems = () => wrapper.findAllComponents(GlDisclosureDropdownItem);
+ const findModal = () => wrapper.findComponent(GlModal);
+
+ it('renders list item', () => {
+ wrapper = createComponent({ template: { name: 'test', content: '/assign_reviewer' } });
+
+ expect(wrapper.element).toMatchSnapshot();
+ });
+
+ describe('comment template actions dropdown', () => {
+ beforeEach(() => {
+ wrapper = createComponent({ template: { name: 'test', content: '/assign_reviewer' } });
+ });
+
+ it('exists', () => {
+ expect(findDropdown().exists()).toBe(true);
+ });
+
+ it('has correct toggle text', () => {
+ expect(findDropdown().props('toggleText')).toBe(__('Comment template actions'));
+ });
+
+ it('has correct amount of dropdown items', () => {
+ const items = findDropdownItems();
+
+ expect(items.exists()).toBe(true);
+ expect(items).toHaveLength(2);
+ });
+
+ describe('edit option', () => {
+ it('exists', () => {
+ const items = findDropdownItems();
+
+ const editItem = items.filter((item) => item.text() === __('Edit'));
+
+ expect(editItem.exists()).toBe(true);
+ });
+
+ it('shows as first dropdown item', () => {
+ const items = findDropdownItems();
+
+ expect(items.at(0).text()).toBe(__('Edit'));
+ });
+ });
+
+ describe('delete option', () => {
+ it('exists', () => {
+ const items = findDropdownItems();
+
+ const deleteItem = items.filter((item) => item.text() === __('Delete'));
+
+ expect(deleteItem.exists()).toBe(true);
+ });
+
+ it('shows as first dropdown item', () => {
+ const items = findDropdownItems();
+
+ expect(items.at(1).text()).toBe(__('Delete'));
+ });
+ });
+ });
+
+ describe('Delete modal', () => {
+ let deleteSavedReplyMutationResponse;
+
+ beforeEach(() => {
+ deleteSavedReplyMutationResponse = jest
+ .fn()
+ .mockResolvedValue({ data: { savedReplyDestroy: { errors: [] } } });
+
+ const apolloProvider = createMockApolloProvider([
+ deleteSavedReplyMutation,
+ deleteSavedReplyMutationResponse,
+ ]);
+
+ wrapper = createComponent(
+ { template: { name: 'test', content: '/assign_reviewer', id: 1 } },
+ apolloProvider,
+ );
+ });
+
+ it('exists', () => {
+ expect(findModal().exists()).toBe(true);
+ });
+
+ it('has correct title', () => {
+ expect(findModal().props('title')).toBe(__('Delete comment template'));
+ });
+
+ it('delete button calls Apollo mutate', async () => {
+ await findModal().vm.$emit('primary');
+
+ expect(deleteSavedReplyMutationResponse).toHaveBeenCalledWith({ id: 1 });
+ });
+
+ it('cancel button does not trigger Apollo mutation', async () => {
+ await findModal().vm.$emit('secondary');
+
+ expect(deleteSavedReplyMutationResponse).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('Dropdown Edit', () => {
+ beforeEach(() => {
+ wrapper = createComponent({ template: { name: 'test', content: '/assign_reviewer' } });
+ });
+
+ it('click triggers router push', async () => {
+ const editComponent = findDropdownItems().at(0);
+
+ await editComponent.find('button').trigger('click');
+
+ expect($router.push).toHaveBeenCalled();
+ });
+ });
+});
diff --git a/spec/frontend/saved_replies/components/list_spec.js b/spec/frontend/comment_templates/components/list_spec.js
index bc69cb852e0..8b0daf2fe2f 100644
--- a/spec/frontend/saved_replies/components/list_spec.js
+++ b/spec/frontend/comment_templates/components/list_spec.js
@@ -1,8 +1,8 @@
import { mount } from '@vue/test-utils';
-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 List from '~/saved_replies/components/list.vue';
-import ListItem from '~/saved_replies/components/list_item.vue';
+import noSavedRepliesResponse from 'test_fixtures/graphql/comment_templates/saved_replies_empty.query.graphql.json';
+import savedRepliesResponse from 'test_fixtures/graphql/comment_templates/saved_replies.query.graphql.json';
+import List from '~/comment_templates/components/list.vue';
+import ListItem from '~/comment_templates/components/list_item.vue';
let wrapper;
@@ -18,28 +18,28 @@ function createComponent(res = {}) {
});
}
-describe('Saved replies list component', () => {
+describe('Comment templates list component', () => {
it('does not render any list items when response is empty', () => {
wrapper = createComponent(noSavedRepliesResponse);
expect(wrapper.findAllComponents(ListItem).length).toBe(0);
});
- it('render saved replies count', () => {
+ it('render comment templates count', () => {
wrapper = createComponent(savedRepliesResponse);
- expect(wrapper.find('[data-testid="title"]').text()).toEqual('My saved replies (2)');
+ expect(wrapper.find('[data-testid="title"]').text()).toEqual('My comment templates (2)');
});
- it('renders list of saved replies', () => {
+ it('renders list of comment templates', () => {
const savedReplies = savedRepliesResponse.data.currentUser.savedReplies.nodes;
wrapper = createComponent(savedRepliesResponse);
expect(wrapper.findAllComponents(ListItem).length).toBe(2);
- expect(wrapper.findAllComponents(ListItem).at(0).props('reply')).toEqual(
+ expect(wrapper.findAllComponents(ListItem).at(0).props('template')).toEqual(
expect.objectContaining(savedReplies[0]),
);
- expect(wrapper.findAllComponents(ListItem).at(1).props('reply')).toEqual(
+ expect(wrapper.findAllComponents(ListItem).at(1).props('template')).toEqual(
expect.objectContaining(savedReplies[1]),
);
});
diff --git a/spec/frontend/saved_replies/pages/index_spec.js b/spec/frontend/comment_templates/pages/index_spec.js
index 771025d64ec..6dbec3ef4a4 100644
--- a/spec/frontend/saved_replies/pages/index_spec.js
+++ b/spec/frontend/comment_templates/pages/index_spec.js
@@ -1,12 +1,12 @@
import Vue from 'vue';
import { mount } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
-import savedRepliesResponse from 'test_fixtures/graphql/saved_replies/saved_replies.query.graphql.json';
+import savedRepliesResponse from 'test_fixtures/graphql/comment_templates/saved_replies.query.graphql.json';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import IndexPage from '~/saved_replies/pages/index.vue';
-import ListItem from '~/saved_replies/components/list_item.vue';
-import savedRepliesQuery from '~/saved_replies/queries/saved_replies.query.graphql';
+import IndexPage from '~/comment_templates/pages/index.vue';
+import ListItem from '~/comment_templates/components/list_item.vue';
+import savedRepliesQuery from '~/comment_templates/queries/saved_replies.query.graphql';
let wrapper;
@@ -26,8 +26,8 @@ function createComponent(options = {}) {
});
}
-describe('Saved replies index page component', () => {
- it('renders list of saved replies', async () => {
+describe('Comment templates index page component', () => {
+ it('renders list of comment templates', async () => {
const mockApollo = createMockApolloProvider(savedRepliesResponse);
const savedReplies = savedRepliesResponse.data.currentUser.savedReplies.nodes;
wrapper = createComponent({ mockApollo });
@@ -35,10 +35,10 @@ describe('Saved replies index page component', () => {
await waitForPromises();
expect(wrapper.findAllComponents(ListItem).length).toBe(2);
- expect(wrapper.findAllComponents(ListItem).at(0).props('reply')).toEqual(
+ expect(wrapper.findAllComponents(ListItem).at(0).props('template')).toEqual(
expect.objectContaining(savedReplies[0]),
);
- expect(wrapper.findAllComponents(ListItem).at(1).props('reply')).toEqual(
+ expect(wrapper.findAllComponents(ListItem).at(1).props('template')).toEqual(
expect.objectContaining(savedReplies[1]),
);
});
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 64623968aa0..cc251104811 100644
--- a/spec/frontend/commit/commit_box_pipeline_mini_graph_spec.js
+++ b/spec/frontend/commit/commit_box_pipeline_mini_graph_spec.js
@@ -83,7 +83,7 @@ describe('Commit box pipeline mini graph', () => {
await createComponent();
});
- it('should not display loading state after the query is resolved', async () => {
+ it('should not display loading state after the query is resolved', () => {
expect(findLoadingIcon().exists()).toBe(false);
expect(findPipelineMiniGraph().exists()).toBe(true);
});
diff --git a/spec/frontend/commit/components/commit_box_pipeline_status_spec.js b/spec/frontend/commit/components/commit_box_pipeline_status_spec.js
index 9c7a41b3506..5df35cc6dda 100644
--- a/spec/frontend/commit/components/commit_box_pipeline_status_spec.js
+++ b/spec/frontend/commit/components/commit_box_pipeline_status_spec.js
@@ -70,7 +70,7 @@ describe('Commit box pipeline status', () => {
await waitForPromises();
});
- it('should display pipeline status after the query is resolved successfully', async () => {
+ it('should display pipeline status after the query is resolved successfully', () => {
expect(findStatusIcon().exists()).toBe(true);
expect(findLoadingIcon().exists()).toBe(false);
diff --git a/spec/frontend/content_editor/components/__snapshots__/toolbar_link_button_spec.js.snap b/spec/frontend/content_editor/components/__snapshots__/toolbar_link_button_spec.js.snap
deleted file mode 100644
index 331a0a474a3..00000000000
--- a/spec/frontend/content_editor/components/__snapshots__/toolbar_link_button_spec.js.snap
+++ /dev/null
@@ -1,33 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`content_editor/components/toolbar_link_button renders dropdown component 1`] = `
-"<div title=\\"Insert link\\" lazy=\\"\\">
- <li role=\\"presentation\\" class=\\"gl-px-3!\\">
- <form tabindex=\\"-1\\" class=\\"b-dropdown-form gl-p-0\\">
- <div role=\\"group\\" class=\\"input-group\\" placeholder=\\"Link URL\\">
- <!---->
- <!----> <input type=\\"text\\" placeholder=\\"Link URL\\" class=\\"form-control gl-form-input\\">
- <div class=\\"input-group-append\\"><button type=\\"button\\" class=\\"btn btn-confirm btn-md gl-button\\">
- <!---->
- <!----> <span class=\\"gl-button-text\\">Apply</span></button></div>
- <!---->
- </div>
- </form>
- </li>
- <li role=\\"presentation\\" class=\\"gl-dropdown-divider\\">
- <hr role=\\"separator\\" aria-orientation=\\"horizontal\\" class=\\"dropdown-divider\\">
- </li>
- <li role=\\"presentation\\" class=\\"gl-dropdown-item\\"><button role=\\"menuitem\\" type=\\"button\\" class=\\"dropdown-item\\">
- <!---->
- <!---->
- <!---->
- <div class=\\"gl-dropdown-item-text-wrapper\\">
- <p class=\\"gl-dropdown-item-text-primary\\">
- Upload file
- </p>
- <!---->
- </div>
- <!---->
- </button></li>
-</div>"
-`;
diff --git a/spec/frontend/content_editor/components/bubble_menus/code_block_bubble_menu_spec.js b/spec/frontend/content_editor/components/bubble_menus/code_block_bubble_menu_spec.js
index 085a6d3a28d..2a6ab75227c 100644
--- a/spec/frontend/content_editor/components/bubble_menus/code_block_bubble_menu_spec.js
+++ b/spec/frontend/content_editor/components/bubble_menus/code_block_bubble_menu_spec.js
@@ -59,7 +59,7 @@ describe('content_editor/components/bubble_menus/code_block_bubble_menu', () =>
checked: x.props('isChecked'),
}));
- beforeEach(async () => {
+ beforeEach(() => {
buildEditor();
buildWrapper();
});
@@ -133,7 +133,7 @@ describe('content_editor/components/bubble_menus/code_block_bubble_menu', () =>
});
describe('preview button', () => {
- it('does not appear for a regular code block', async () => {
+ it('does not appear for a regular code block', () => {
tiptapEditor.commands.insertContent('<pre lang="javascript">var a = 2;</pre>');
expect(wrapper.findByTestId('preview-diagram').exists()).toBe(false);
@@ -269,7 +269,7 @@ describe('content_editor/components/bubble_menus/code_block_bubble_menu', () =>
await emitEditorEvent({ event: 'transaction', tiptapEditor });
});
- it('hides the custom language input form and shows dropdown items', async () => {
+ it('hides the custom language input form and shows dropdown items', () => {
expect(wrapper.findComponent(GlDropdownItem).exists()).toBe(true);
expect(wrapper.findComponent(GlSearchBoxByType).exists()).toBe(true);
expect(wrapper.findComponent(GlDropdownForm).exists()).toBe(false);
diff --git a/spec/frontend/content_editor/components/bubble_menus/formatting_bubble_menu_spec.js b/spec/frontend/content_editor/components/bubble_menus/formatting_bubble_menu_spec.js
index 7bab473529f..c4bc29adb52 100644
--- a/spec/frontend/content_editor/components/bubble_menus/formatting_bubble_menu_spec.js
+++ b/spec/frontend/content_editor/components/bubble_menus/formatting_bubble_menu_spec.js
@@ -53,7 +53,7 @@ describe('content_editor/components/bubble_menus/formatting_bubble_menu', () =>
${'superscript'} | ${{ contentType: 'superscript', iconName: 'superscript', label: 'Superscript', editorCommand: 'toggleSuperscript' }}
${'subscript'} | ${{ contentType: 'subscript', iconName: 'subscript', label: 'Subscript', editorCommand: 'toggleSubscript' }}
${'highlight'} | ${{ contentType: 'highlight', iconName: 'highlight', label: 'Highlight', editorCommand: 'toggleHighlight' }}
- ${'link'} | ${{ contentType: 'link', iconName: 'link', label: 'Insert link', editorCommand: 'toggleLink', editorCommandParams: { href: '' } }}
+ ${'link'} | ${{ contentType: 'link', iconName: 'link', label: 'Insert link', editorCommand: 'editLink' }}
`('given a $testId toolbar control', ({ testId, controlProps }) => {
beforeEach(() => {
buildWrapper();
diff --git a/spec/frontend/content_editor/components/bubble_menus/link_bubble_menu_spec.js b/spec/frontend/content_editor/components/bubble_menus/link_bubble_menu_spec.js
index eb5a3b61591..2a8a1b00692 100644
--- a/spec/frontend/content_editor/components/bubble_menus/link_bubble_menu_spec.js
+++ b/spec/frontend/content_editor/components/bubble_menus/link_bubble_menu_spec.js
@@ -59,13 +59,13 @@ describe('content_editor/components/bubble_menus/link_bubble_menu', () => {
expect(wrapper.findByTestId('remove-link').exists()).toBe(exist);
};
- beforeEach(async () => {
+ beforeEach(() => {
buildEditor();
tiptapEditor
.chain()
.insertContent(
- 'Download <a href="/path/to/project/-/wikis/uploads/my_file.pdf" data-canonical-src="uploads/my_file.pdf" title="Click here to download">PDF File</a>',
+ 'Download <a href="/path/to/project/-/wikis/uploads/my_file.pdf" data-canonical-src="uploads/my_file.pdf">PDF File</a>',
)
.setTextSelection(14) // put cursor in the middle of the link
.run();
@@ -84,7 +84,6 @@ describe('content_editor/components/bubble_menus/link_bubble_menu', () => {
expect.objectContaining({
href: '/path/to/project/-/wikis/uploads/my_file.pdf',
'aria-label': 'uploads/my_file.pdf',
- title: 'uploads/my_file.pdf',
target: '_blank',
}),
);
@@ -181,52 +180,17 @@ describe('content_editor/components/bubble_menus/link_bubble_menu', () => {
});
});
- describe('for a placeholder link', () => {
- beforeEach(async () => {
- tiptapEditor
- .chain()
- .clearContent()
- .insertContent('Dummy link')
- .selectAll()
- .setLink({ href: '' })
- .setTextSelection(4)
- .run();
-
- await buildWrapperAndDisplayMenu();
- });
-
- it('directly opens the edit form for a placeholder link', async () => {
- expectLinkButtonsToExist(false);
-
- expect(wrapper.findComponent(GlForm).exists()).toBe(true);
- });
-
- it('removes the link on clicking apply (if no change)', async () => {
- await wrapper.findComponent(GlForm).vm.$emit('submit', createFakeEvent());
-
- expect(tiptapEditor.getHTML()).toBe('<p>Dummy link</p>');
- });
-
- it('removes the link on clicking cancel', async () => {
- await wrapper.findByTestId('cancel-link').vm.$emit('click');
-
- expect(tiptapEditor.getHTML()).toBe('<p>Dummy link</p>');
- });
- });
-
describe('edit button', () => {
let linkHrefInput;
- let linkTitleInput;
beforeEach(async () => {
await buildWrapperAndDisplayMenu();
await wrapper.findByTestId('edit-link').vm.$emit('click');
linkHrefInput = wrapper.findByTestId('link-href');
- linkTitleInput = wrapper.findByTestId('link-title');
});
- it('hides the link and copy/edit/remove link buttons', async () => {
+ it('hides the link and copy/edit/remove link buttons', () => {
expectLinkButtonsToExist(false);
});
@@ -234,7 +198,6 @@ describe('content_editor/components/bubble_menus/link_bubble_menu', () => {
expect(wrapper.findComponent(GlForm).exists()).toBe(true);
expect(linkHrefInput.element.value).toBe('uploads/my_file.pdf');
- expect(linkTitleInput.element.value).toBe('Click here to download');
});
it('extends selection to select the entire link', () => {
@@ -247,26 +210,18 @@ describe('content_editor/components/bubble_menus/link_bubble_menu', () => {
describe('after making changes in the form and clicking apply', () => {
beforeEach(async () => {
linkHrefInput.setValue('https://google.com');
- linkTitleInput.setValue('Search Google');
contentEditor.resolveUrl.mockResolvedValue('https://google.com');
await wrapper.findComponent(GlForm).vm.$emit('submit', createFakeEvent());
});
- it('updates prosemirror doc with new link', async () => {
- expect(tiptapEditor.getHTML()).toBe(
- '<p>Download <a target="_blank" rel="noopener noreferrer nofollow" href="https://google.com" title="Search Google">PDF File</a></p>',
- );
- });
-
it('updates the link in the bubble menu', () => {
const link = wrapper.findComponent(GlLink);
expect(link.attributes()).toEqual(
expect.objectContaining({
href: 'https://google.com',
'aria-label': 'https://google.com',
- title: 'https://google.com',
target: '_blank',
}),
);
@@ -277,7 +232,6 @@ describe('content_editor/components/bubble_menus/link_bubble_menu', () => {
describe('after making changes in the form and clicking cancel', () => {
beforeEach(async () => {
linkHrefInput.setValue('https://google.com');
- linkTitleInput.setValue('Search Google');
await wrapper.findByTestId('cancel-link').vm.$emit('click');
});
@@ -285,17 +239,6 @@ describe('content_editor/components/bubble_menus/link_bubble_menu', () => {
it('hides the form and shows the copy/edit/remove link buttons', () => {
expectLinkButtonsToExist();
});
-
- it('resets the form with old values of the link from prosemirror', async () => {
- // click edit once again to show the form back
- await wrapper.findByTestId('edit-link').vm.$emit('click');
-
- linkHrefInput = wrapper.findByTestId('link-href');
- linkTitleInput = wrapper.findByTestId('link-title');
-
- expect(linkHrefInput.element.value).toBe('uploads/my_file.pdf');
- expect(linkTitleInput.element.value).toBe('Click here to download');
- });
});
});
});
diff --git a/spec/frontend/content_editor/components/bubble_menus/media_bubble_menu_spec.js b/spec/frontend/content_editor/components/bubble_menus/media_bubble_menu_spec.js
index c918f068c07..e02b36fb8e9 100644
--- a/spec/frontend/content_editor/components/bubble_menus/media_bubble_menu_spec.js
+++ b/spec/frontend/content_editor/components/bubble_menus/media_bubble_menu_spec.js
@@ -100,11 +100,11 @@ describe.each`
bubbleMenu = wrapper.findComponent(BubbleMenu);
});
- it('renders bubble menu component', async () => {
+ it('renders bubble menu component', () => {
expect(bubbleMenu.classes()).toEqual(['gl-shadow', 'gl-rounded-base', 'gl-bg-white']);
});
- it('shows a clickable link to the image', async () => {
+ it('shows a clickable link to the image', () => {
const link = wrapper.findComponent(GlLink);
expect(link.attributes()).toEqual(
expect.objectContaining({
@@ -202,7 +202,7 @@ describe.each`
mediaAltInput = wrapper.findByTestId('media-alt');
});
- it('hides the link and copy/edit/remove link buttons', async () => {
+ it('hides the link and copy/edit/remove link buttons', () => {
expectLinkButtonsToExist(false);
});
@@ -225,7 +225,7 @@ describe.each`
await wrapper.findComponent(GlForm).vm.$emit('submit', createFakeEvent());
});
- it(`updates prosemirror doc with new src to the ${mediaType}`, async () => {
+ it(`updates prosemirror doc with new src to the ${mediaType}`, () => {
expect(tiptapEditor.getHTML()).toBe(mediaOutputHTML);
});
diff --git a/spec/frontend/content_editor/components/content_editor_alert_spec.js b/spec/frontend/content_editor/components/content_editor_alert_spec.js
index e62e2331d25..e6873e2cf96 100644
--- a/spec/frontend/content_editor/components/content_editor_alert_spec.js
+++ b/spec/frontend/content_editor/components/content_editor_alert_spec.js
@@ -14,7 +14,7 @@ describe('content_editor/components/content_editor_alert', () => {
const findErrorAlert = () => wrapper.findComponent(GlAlert);
- const createWrapper = async () => {
+ const createWrapper = () => {
tiptapEditor = createTestEditor();
eventHub = eventHubFactory();
diff --git a/spec/frontend/content_editor/components/formatting_toolbar_spec.js b/spec/frontend/content_editor/components/formatting_toolbar_spec.js
index 4a7b7cedf19..5d2a9e493e5 100644
--- a/spec/frontend/content_editor/components/formatting_toolbar_spec.js
+++ b/spec/frontend/content_editor/components/formatting_toolbar_spec.js
@@ -35,7 +35,7 @@ describe('content_editor/components/formatting_toolbar', () => {
${'bullet-list'} | ${{ contentType: 'bulletList', iconName: 'list-bulleted', label: 'Add a bullet list', editorCommand: 'toggleBulletList' }}
${'ordered-list'} | ${{ contentType: 'orderedList', iconName: 'list-numbered', label: 'Add a numbered list', editorCommand: 'toggleOrderedList' }}
${'task-list'} | ${{ contentType: 'taskList', iconName: 'list-task', label: 'Add a checklist', editorCommand: 'toggleTaskList' }}
- ${'image'} | ${{}}
+ ${'attachment'} | ${{}}
${'table'} | ${{}}
${'more'} | ${{}}
`('given a $testId toolbar control', ({ testId, controlProps }) => {
diff --git a/spec/frontend/content_editor/components/suggestions_dropdown_spec.js b/spec/frontend/content_editor/components/suggestions_dropdown_spec.js
index e72eb892e74..9d34d9d0e9e 100644
--- a/spec/frontend/content_editor/components/suggestions_dropdown_spec.js
+++ b/spec/frontend/content_editor/components/suggestions_dropdown_spec.js
@@ -1,4 +1,4 @@
-import { GlAvatarLabeled, GlDropdownItem } from '@gitlab/ui';
+import { GlDropdownItem, GlAvatarLabeled, GlLoadingIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import SuggestionsDropdown from '~/content_editor/components/suggestions_dropdown.vue';
@@ -75,6 +75,26 @@ describe('~/content_editor/components/suggestions_dropdown', () => {
unicodeVersion: '6.0',
};
+ it.each`
+ loading | description
+ ${false} | ${'does not show a loading indicator'}
+ ${true} | ${'shows a loading indicator'}
+ `('$description if loading=$loading', ({ loading }) => {
+ buildWrapper({
+ propsData: {
+ loading,
+ char: '@',
+ nodeType: 'reference',
+ nodeProps: {
+ referenceType: 'member',
+ },
+ items: [exampleUser],
+ },
+ });
+
+ expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(loading);
+ });
+
describe('on item select', () => {
it.each`
nodeType | referenceType | char | reference | insertedText | insertedProps
diff --git a/spec/frontend/content_editor/components/toolbar_attachment_button_spec.js b/spec/frontend/content_editor/components/toolbar_attachment_button_spec.js
new file mode 100644
index 00000000000..06ea863dbfa
--- /dev/null
+++ b/spec/frontend/content_editor/components/toolbar_attachment_button_spec.js
@@ -0,0 +1,57 @@
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import ToolbarAttachmentButton from '~/content_editor/components/toolbar_attachment_button.vue';
+import Attachment from '~/content_editor/extensions/attachment';
+import Link from '~/content_editor/extensions/link';
+import { createTestEditor, mockChainedCommands } from '../test_utils';
+
+describe('content_editor/components/toolbar_attachment_button', () => {
+ let wrapper;
+ let editor;
+
+ const buildWrapper = () => {
+ wrapper = mountExtended(ToolbarAttachmentButton, {
+ provide: {
+ tiptapEditor: editor,
+ },
+ });
+ };
+
+ const selectFile = async (file) => {
+ const input = wrapper.findComponent({ ref: 'fileSelector' });
+
+ // override the property definition because `input.files` isn't directly modifyable
+ Object.defineProperty(input.element, 'files', { value: [file], writable: true });
+ await input.trigger('change');
+ };
+
+ beforeEach(() => {
+ editor = createTestEditor({
+ extensions: [
+ Link,
+ Attachment.configure({
+ renderMarkdown: jest.fn(),
+ uploadsPath: '/uploads/',
+ }),
+ ],
+ });
+
+ buildWrapper();
+ });
+
+ afterEach(() => {
+ editor.destroy();
+ });
+
+ it('uploads the selected attachment when file input changes', async () => {
+ const commands = mockChainedCommands(editor, ['focus', 'uploadAttachment', 'run']);
+ const file = new File(['foo'], 'foo.png', { type: 'image/png' });
+
+ await selectFile(file);
+
+ expect(commands.focus).toHaveBeenCalled();
+ expect(commands.uploadAttachment).toHaveBeenCalledWith({ file });
+ expect(commands.run).toHaveBeenCalled();
+
+ expect(wrapper.emitted().execute[0]).toEqual([{ contentType: 'link', value: 'upload' }]);
+ });
+});
diff --git a/spec/frontend/content_editor/components/toolbar_image_button_spec.js b/spec/frontend/content_editor/components/toolbar_image_button_spec.js
deleted file mode 100644
index 0ec950137fc..00000000000
--- a/spec/frontend/content_editor/components/toolbar_image_button_spec.js
+++ /dev/null
@@ -1,96 +0,0 @@
-import { GlButton, GlFormInputGroup, GlDropdown } from '@gitlab/ui';
-import { mountExtended } from 'helpers/vue_test_utils_helper';
-import ToolbarImageButton from '~/content_editor/components/toolbar_image_button.vue';
-import Attachment from '~/content_editor/extensions/attachment';
-import Image from '~/content_editor/extensions/image';
-import { stubComponent } from 'helpers/stub_component';
-import { createTestEditor, mockChainedCommands } from '../test_utils';
-
-describe('content_editor/components/toolbar_image_button', () => {
- let wrapper;
- let editor;
-
- const buildWrapper = () => {
- wrapper = mountExtended(ToolbarImageButton, {
- provide: {
- tiptapEditor: editor,
- },
- stubs: {
- GlDropdown: stubComponent(GlDropdown),
- },
- });
- };
-
- const findImageURLInput = () =>
- wrapper.findComponent(GlFormInputGroup).find('input[type="text"]');
- const findApplyImageButton = () => wrapper.findComponent(GlButton);
- const findDropdown = () => wrapper.findComponent(GlDropdown);
-
- const selectFile = async (file) => {
- const input = wrapper.findComponent({ ref: 'fileSelector' });
-
- // override the property definition because `input.files` isn't directly modifyable
- Object.defineProperty(input.element, 'files', { value: [file], writable: true });
- await input.trigger('change');
- };
-
- beforeEach(() => {
- editor = createTestEditor({
- extensions: [
- Image,
- Attachment.configure({
- renderMarkdown: jest.fn(),
- uploadsPath: '/uploads/',
- }),
- ],
- });
-
- buildWrapper();
- });
-
- afterEach(() => {
- editor.destroy();
- });
-
- it('sets the image to the value in the URL input when "Insert" button is clicked', async () => {
- const commands = mockChainedCommands(editor, ['focus', 'setImage', 'run']);
-
- await findImageURLInput().setValue('https://example.com/img.jpg');
- await findApplyImageButton().trigger('click');
-
- expect(commands.focus).toHaveBeenCalled();
- expect(commands.setImage).toHaveBeenCalledWith({
- alt: 'img',
- src: 'https://example.com/img.jpg',
- canonicalSrc: 'https://example.com/img.jpg',
- });
- expect(commands.run).toHaveBeenCalled();
-
- expect(wrapper.emitted().execute[0]).toEqual([{ contentType: 'image', value: 'url' }]);
- });
-
- it('uploads the selected image when file input changes', async () => {
- const commands = mockChainedCommands(editor, ['focus', 'uploadAttachment', 'run']);
- const file = new File(['foo'], 'foo.png', { type: 'image/png' });
-
- await selectFile(file);
-
- expect(commands.focus).toHaveBeenCalled();
- expect(commands.uploadAttachment).toHaveBeenCalledWith({ file });
- expect(commands.run).toHaveBeenCalled();
-
- expect(wrapper.emitted().execute[0]).toEqual([{ contentType: 'image', value: 'upload' }]);
- });
-
- describe('a11y tests', () => {
- it('sets text, title, and text-sr-only properties to the table button dropdown', () => {
- buildWrapper();
-
- expect(findDropdown().props()).toMatchObject({
- text: 'Insert image',
- textSrOnly: true,
- });
- expect(findDropdown().attributes('title')).toBe('Insert image');
- });
- });
-});
diff --git a/spec/frontend/content_editor/components/toolbar_link_button_spec.js b/spec/frontend/content_editor/components/toolbar_link_button_spec.js
deleted file mode 100644
index 80090c0278f..00000000000
--- a/spec/frontend/content_editor/components/toolbar_link_button_spec.js
+++ /dev/null
@@ -1,223 +0,0 @@
-import { GlDropdown, GlButton, GlFormInputGroup } from '@gitlab/ui';
-import { mountExtended } from 'helpers/vue_test_utils_helper';
-import ToolbarLinkButton from '~/content_editor/components/toolbar_link_button.vue';
-import eventHubFactory from '~/helpers/event_hub_factory';
-import Link from '~/content_editor/extensions/link';
-import { hasSelection } from '~/content_editor/services/utils';
-import { stubComponent } from 'helpers/stub_component';
-import { createTestEditor, mockChainedCommands, emitEditorEvent } from '../test_utils';
-
-jest.mock('~/content_editor/services/utils');
-
-describe('content_editor/components/toolbar_link_button', () => {
- let wrapper;
- let editor;
-
- const buildWrapper = () => {
- wrapper = mountExtended(ToolbarLinkButton, {
- provide: {
- tiptapEditor: editor,
- eventHub: eventHubFactory(),
- },
- stubs: {
- GlDropdown: stubComponent(GlDropdown),
- },
- });
- };
- const findDropdown = () => wrapper.findComponent(GlDropdown);
- const findLinkURLInput = () => wrapper.findComponent(GlFormInputGroup).find('input[type="text"]');
- const findApplyLinkButton = () => wrapper.findComponent(GlButton);
- const findRemoveLinkButton = () => wrapper.findByText('Remove link');
-
- const selectFile = async (file) => {
- const input = wrapper.findComponent({ ref: 'fileSelector' });
-
- // override the property definition because `input.files` isn't directly modifyable
- Object.defineProperty(input.element, 'files', { value: [file], writable: true });
- await input.trigger('change');
- };
-
- beforeEach(() => {
- editor = createTestEditor();
- });
-
- afterEach(() => {
- editor.destroy();
- });
-
- it('renders dropdown component', () => {
- buildWrapper();
-
- expect(findDropdown().html()).toMatchSnapshot();
- });
-
- describe('when there is an active link', () => {
- beforeEach(async () => {
- jest.spyOn(editor, 'isActive').mockReturnValueOnce(true);
- buildWrapper();
-
- await emitEditorEvent({ event: 'transaction', tiptapEditor: editor });
- });
-
- it('sets dropdown as active when link extension is active', () => {
- expect(findDropdown().props('toggleClass')).toEqual({ active: true });
- });
-
- it('does not display the upload file option', () => {
- expect(wrapper.findByText('Upload file').exists()).toBe(false);
- });
-
- it('displays a remove link dropdown option', () => {
- expect(wrapper.findByText('Remove link').exists()).toBe(true);
- });
-
- it('executes removeLink command when the remove link option is clicked', async () => {
- const commands = mockChainedCommands(editor, ['focus', 'unsetLink', 'run']);
-
- await findRemoveLinkButton().trigger('click');
-
- expect(commands.unsetLink).toHaveBeenCalled();
- expect(commands.focus).toHaveBeenCalled();
- expect(commands.run).toHaveBeenCalled();
- });
-
- it('updates the link with a new link when "Apply" button is clicked', async () => {
- const commands = mockChainedCommands(editor, ['focus', 'unsetLink', 'setLink', 'run']);
-
- await findLinkURLInput().setValue('https://example');
- await findApplyLinkButton().trigger('click');
-
- expect(commands.focus).toHaveBeenCalled();
- expect(commands.unsetLink).toHaveBeenCalled();
- expect(commands.setLink).toHaveBeenCalledWith({
- href: 'https://example',
- canonicalSrc: 'https://example',
- });
- expect(commands.run).toHaveBeenCalled();
-
- expect(wrapper.emitted().execute[0]).toEqual([{ contentType: 'link' }]);
- });
-
- describe('on selection update', () => {
- it('updates link input box with canonical-src if present', async () => {
- jest.spyOn(editor, 'getAttributes').mockReturnValueOnce({
- canonicalSrc: 'uploads/my-file.zip',
- href: '/username/my-project/uploads/abcdefgh133535/my-file.zip',
- });
-
- await emitEditorEvent({ event: 'transaction', tiptapEditor: editor });
-
- expect(findLinkURLInput().element.value).toEqual('uploads/my-file.zip');
- });
-
- it('updates link input box with link href otherwise', async () => {
- jest.spyOn(editor, 'getAttributes').mockReturnValueOnce({
- href: 'https://gitlab.com',
- });
-
- await emitEditorEvent({ event: 'transaction', tiptapEditor: editor });
-
- expect(findLinkURLInput().element.value).toEqual('https://gitlab.com');
- });
- });
- });
-
- describe('when there is no active link', () => {
- beforeEach(() => {
- jest.spyOn(editor, 'isActive');
- editor.isActive.mockReturnValueOnce(false);
- buildWrapper();
- });
-
- it('does not set dropdown as active', () => {
- expect(findDropdown().props('toggleClass')).toEqual({ active: false });
- });
-
- it('displays the upload file option', () => {
- expect(wrapper.findByText('Upload file').exists()).toBe(true);
- });
-
- it('does not display a remove link dropdown option', () => {
- expect(wrapper.findByText('Remove link').exists()).toBe(false);
- });
-
- it('sets the link to the value in the URL input when "Apply" button is clicked', async () => {
- const commands = mockChainedCommands(editor, ['focus', 'unsetLink', 'setLink', 'run']);
-
- await findLinkURLInput().setValue('https://example');
- await findApplyLinkButton().trigger('click');
-
- expect(commands.focus).toHaveBeenCalled();
- expect(commands.setLink).toHaveBeenCalledWith({
- href: 'https://example',
- canonicalSrc: 'https://example',
- });
- expect(commands.run).toHaveBeenCalled();
-
- expect(wrapper.emitted().execute[0]).toEqual([{ contentType: 'link' }]);
- });
-
- it('uploads the selected image when file input changes', async () => {
- const commands = mockChainedCommands(editor, ['focus', 'uploadAttachment', 'run']);
- const file = new File(['foo'], 'foo.png', { type: 'image/png' });
-
- await selectFile(file);
-
- expect(commands.focus).toHaveBeenCalled();
- expect(commands.uploadAttachment).toHaveBeenCalledWith({ file });
- expect(commands.run).toHaveBeenCalled();
-
- expect(wrapper.emitted().execute[0]).toEqual([{ contentType: 'link' }]);
- });
- });
-
- describe('when the user displays the dropdown', () => {
- let commands;
-
- beforeEach(() => {
- commands = mockChainedCommands(editor, ['focus', 'extendMarkRange', 'run']);
- });
-
- describe('given the user has not selected text', () => {
- beforeEach(() => {
- hasSelection.mockReturnValueOnce(false);
- });
-
- it('the editor selection is extended to the current mark extent', () => {
- buildWrapper();
-
- findDropdown().vm.$emit('show');
- expect(commands.extendMarkRange).toHaveBeenCalledWith(Link.name);
- expect(commands.focus).toHaveBeenCalled();
- expect(commands.run).toHaveBeenCalled();
- });
- });
-
- describe('given the user has selected text', () => {
- beforeEach(() => {
- hasSelection.mockReturnValueOnce(true);
- });
-
- it('the editor does not modify the current selection', () => {
- buildWrapper();
-
- findDropdown().vm.$emit('show');
- expect(commands.extendMarkRange).not.toHaveBeenCalled();
- expect(commands.focus).not.toHaveBeenCalled();
- expect(commands.run).not.toHaveBeenCalled();
- });
- });
- });
-
- describe('a11y tests', () => {
- it('sets text, title, and text-sr-only properties to the table button dropdown', () => {
- buildWrapper();
-
- expect(findDropdown().props()).toMatchObject({
- text: 'Insert link',
- textSrOnly: true,
- });
- expect(findDropdown().attributes('title')).toBe('Insert link');
- });
- });
-});
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 5af4784f358..78b02744d51 100644
--- a/spec/frontend/content_editor/components/toolbar_more_dropdown_spec.js
+++ b/spec/frontend/content_editor/components/toolbar_more_dropdown_spec.js
@@ -53,7 +53,7 @@ describe('content_editor/components/toolbar_more_dropdown', () => {
let commands;
let btn;
- beforeEach(async () => {
+ beforeEach(() => {
buildWrapper();
commands = mockChainedCommands(tiptapEditor, [command, 'focus', 'run']);
diff --git a/spec/frontend/content_editor/components/wrappers/code_block_spec.js b/spec/frontend/content_editor/components/wrappers/code_block_spec.js
index 057e50cd0e2..cbeea90dcb4 100644
--- a/spec/frontend/content_editor/components/wrappers/code_block_spec.js
+++ b/spec/frontend/content_editor/components/wrappers/code_block_spec.js
@@ -26,7 +26,7 @@ describe('content/components/wrappers/code_block', () => {
eventHub = eventHubFactory();
};
- const createWrapper = async (nodeAttrs = { language }) => {
+ const createWrapper = (nodeAttrs = { language }) => {
updateAttributesFn = jest.fn();
wrapper = mountExtended(CodeBlockWrapper, {
@@ -97,7 +97,7 @@ describe('content/components/wrappers/code_block', () => {
jest.spyOn(tiptapEditor, 'isActive').mockReturnValue(true);
});
- it('does not render a preview if showPreview: false', async () => {
+ it('does not render a preview if showPreview: false', () => {
createWrapper({ language: 'plantuml', isDiagram: true, showPreview: false });
expect(wrapper.findComponent({ ref: 'diagramContainer' }).exists()).toBe(false);
diff --git a/spec/frontend/content_editor/components/wrappers/details_spec.js b/spec/frontend/content_editor/components/wrappers/details_spec.js
index 232c1e9aede..e35b04636f7 100644
--- a/spec/frontend/content_editor/components/wrappers/details_spec.js
+++ b/spec/frontend/content_editor/components/wrappers/details_spec.js
@@ -5,7 +5,7 @@ import DetailsWrapper from '~/content_editor/components/wrappers/details.vue';
describe('content/components/wrappers/details', () => {
let wrapper;
- const createWrapper = async () => {
+ const createWrapper = () => {
wrapper = shallowMountExtended(DetailsWrapper, {
propsData: {
node: {},
diff --git a/spec/frontend/content_editor/components/wrappers/footnote_definition_spec.js b/spec/frontend/content_editor/components/wrappers/footnote_definition_spec.js
index 91c6799478e..b5b118a2d9a 100644
--- a/spec/frontend/content_editor/components/wrappers/footnote_definition_spec.js
+++ b/spec/frontend/content_editor/components/wrappers/footnote_definition_spec.js
@@ -4,7 +4,7 @@ import FootnoteDefinitionWrapper from '~/content_editor/components/wrappers/foot
describe('content/components/wrappers/footnote_definition', () => {
let wrapper;
- const createWrapper = async (node = {}) => {
+ const createWrapper = (node = {}) => {
wrapper = shallowMountExtended(FootnoteDefinitionWrapper, {
propsData: {
node,
diff --git a/spec/frontend/content_editor/components/wrappers/label_spec.js b/spec/frontend/content_editor/components/wrappers/reference_label_spec.js
index fa32b746142..f57caee911b 100644
--- a/spec/frontend/content_editor/components/wrappers/label_spec.js
+++ b/spec/frontend/content_editor/components/wrappers/reference_label_spec.js
@@ -1,12 +1,12 @@
import { GlLabel } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import LabelWrapper from '~/content_editor/components/wrappers/label.vue';
+import ReferenceLabelWrapper from '~/content_editor/components/wrappers/reference_label.vue';
-describe('content/components/wrappers/label', () => {
+describe('content/components/wrappers/reference_label', () => {
let wrapper;
- const createWrapper = async (node = {}) => {
- wrapper = shallowMountExtended(LabelWrapper, {
+ const createWrapper = (node = {}) => {
+ wrapper = shallowMountExtended(ReferenceLabelWrapper, {
propsData: { node },
});
};
diff --git a/spec/frontend/content_editor/components/wrappers/reference_spec.js b/spec/frontend/content_editor/components/wrappers/reference_spec.js
new file mode 100644
index 00000000000..828b92a6b1e
--- /dev/null
+++ b/spec/frontend/content_editor/components/wrappers/reference_spec.js
@@ -0,0 +1,46 @@
+import { GlLink } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import ReferenceWrapper from '~/content_editor/components/wrappers/reference.vue';
+
+describe('content/components/wrappers/reference', () => {
+ let wrapper;
+
+ const createWrapper = (node = {}) => {
+ wrapper = shallowMountExtended(ReferenceWrapper, {
+ propsData: { node },
+ });
+ };
+
+ it('renders a span for commands', () => {
+ createWrapper({ attrs: { referenceType: 'command', text: '/assign' } });
+
+ const span = wrapper.find('span');
+ expect(span.text()).toBe('/assign');
+ });
+
+ it('renders an anchor for everything else', () => {
+ createWrapper({ attrs: { referenceType: 'issue', text: '#252522' } });
+
+ const link = wrapper.findComponent(GlLink);
+ expect(link.text()).toBe('#252522');
+ });
+
+ it('adds gfm-project_member class for project members', () => {
+ createWrapper({ attrs: { referenceType: 'user', text: '@root' } });
+
+ const link = wrapper.findComponent(GlLink);
+ expect(link.text()).toBe('@root');
+ expect(link.classes('gfm-project_member')).toBe(true);
+ expect(link.classes('current-user')).toBe(false);
+ });
+
+ it('adds a current-user class if the project member is current user', () => {
+ window.gon = { current_username: 'root' };
+
+ createWrapper({ attrs: { referenceType: 'user', text: '@root' } });
+
+ const link = wrapper.findComponent(GlLink);
+ expect(link.text()).toBe('@root');
+ expect(link.classes('current-user')).toBe(true);
+ });
+});
diff --git a/spec/frontend/content_editor/components/wrappers/table_cell_base_spec.js b/spec/frontend/content_editor/components/wrappers/table_cell_base_spec.js
index d8f34565705..71ffbd3f93c 100644
--- a/spec/frontend/content_editor/components/wrappers/table_cell_base_spec.js
+++ b/spec/frontend/content_editor/components/wrappers/table_cell_base_spec.js
@@ -13,7 +13,7 @@ describe('content/components/wrappers/table_cell_base', () => {
let editor;
let node;
- const createWrapper = async (propsData = { cellType: 'td' }) => {
+ const createWrapper = (propsData = { cellType: 'td' }) => {
wrapper = shallowMountExtended(TableCellBaseWrapper, {
propsData: {
editor,
@@ -118,7 +118,7 @@ describe('content/components/wrappers/table_cell_base', () => {
},
);
- it('does not allow deleting rows and columns', async () => {
+ it('does not allow deleting rows and columns', () => {
expect(findDropdownItemWithLabelExists('Delete row')).toBe(false);
expect(findDropdownItemWithLabelExists('Delete column')).toBe(false);
});
@@ -173,7 +173,7 @@ describe('content/components/wrappers/table_cell_base', () => {
await nextTick();
});
- it('does not allow adding a row before the header', async () => {
+ it('does not allow adding a row before the header', () => {
expect(findDropdownItemWithLabelExists('Insert row before')).toBe(false);
});
diff --git a/spec/frontend/content_editor/components/wrappers/table_cell_body_spec.js b/spec/frontend/content_editor/components/wrappers/table_cell_body_spec.js
index 506f442bcc7..4c91573e0c7 100644
--- a/spec/frontend/content_editor/components/wrappers/table_cell_body_spec.js
+++ b/spec/frontend/content_editor/components/wrappers/table_cell_body_spec.js
@@ -8,7 +8,7 @@ describe('content/components/wrappers/table_cell_body', () => {
let editor;
let node;
- const createWrapper = async () => {
+ const createWrapper = () => {
wrapper = shallowMount(TableCellBodyWrapper, {
propsData: {
editor,
diff --git a/spec/frontend/content_editor/components/wrappers/table_cell_header_spec.js b/spec/frontend/content_editor/components/wrappers/table_cell_header_spec.js
index bebe7fb4124..689a8bc32bb 100644
--- a/spec/frontend/content_editor/components/wrappers/table_cell_header_spec.js
+++ b/spec/frontend/content_editor/components/wrappers/table_cell_header_spec.js
@@ -8,7 +8,7 @@ describe('content/components/wrappers/table_cell_header', () => {
let editor;
let node;
- const createWrapper = async () => {
+ const createWrapper = () => {
wrapper = shallowMount(TableCellHeaderWrapper, {
propsData: {
editor,
diff --git a/spec/frontend/content_editor/components/wrappers/table_of_contents_spec.js b/spec/frontend/content_editor/components/wrappers/table_of_contents_spec.js
index 4d5911dda0c..037da7678bb 100644
--- a/spec/frontend/content_editor/components/wrappers/table_of_contents_spec.js
+++ b/spec/frontend/content_editor/components/wrappers/table_of_contents_spec.js
@@ -20,7 +20,7 @@ describe('content/components/wrappers/table_of_contents', () => {
eventHub = eventHubFactory();
};
- const createWrapper = async () => {
+ const createWrapper = () => {
wrapper = mountExtended(TableOfContentsWrapper, {
propsData: {
editor: tiptapEditor,
diff --git a/spec/frontend/content_editor/extensions/paste_markdown_spec.js b/spec/frontend/content_editor/extensions/paste_markdown_spec.js
index 8f3a4934e77..c9997e3c58f 100644
--- a/spec/frontend/content_editor/extensions/paste_markdown_spec.js
+++ b/spec/frontend/content_editor/extensions/paste_markdown_spec.js
@@ -2,6 +2,7 @@ import PasteMarkdown from '~/content_editor/extensions/paste_markdown';
import CodeBlockHighlight from '~/content_editor/extensions/code_block_highlight';
import Diagram from '~/content_editor/extensions/diagram';
import Frontmatter from '~/content_editor/extensions/frontmatter';
+import Heading from '~/content_editor/extensions/heading';
import Bold from '~/content_editor/extensions/bold';
import { VARIANT_DANGER } from '~/alert';
import eventHubFactory from '~/helpers/event_hub_factory';
@@ -20,6 +21,7 @@ describe('content_editor/extensions/paste_markdown', () => {
let doc;
let p;
let bold;
+ let heading;
let renderMarkdown;
let eventHub;
const defaultData = { 'text/plain': '**bold text**' };
@@ -36,16 +38,18 @@ describe('content_editor/extensions/paste_markdown', () => {
CodeBlockHighlight,
Diagram,
Frontmatter,
+ Heading,
PasteMarkdown.configure({ renderMarkdown, eventHub }),
],
});
({
- builders: { doc, p, bold },
+ builders: { doc, p, bold, heading },
} = createDocBuilder({
tiptapEditor,
names: {
bold: { markType: Bold.name },
+ heading: { nodeType: Heading.name },
},
}));
});
@@ -110,6 +114,52 @@ describe('content_editor/extensions/paste_markdown', () => {
expect(tiptapEditor.state.doc.toJSON()).toEqual(expectedDoc.toJSON());
});
+
+ describe('when pasting inline content in an existing paragraph', () => {
+ it('inserts the inline content next to the existing paragraph content', async () => {
+ const expectedDoc = doc(p('Initial text and', bold('bold text')));
+
+ tiptapEditor.commands.setContent('Initial text and ');
+
+ await triggerPasteEventHandlerAndWaitForTransaction(buildClipboardEvent());
+
+ expect(tiptapEditor.state.doc.toJSON()).toEqual(expectedDoc.toJSON());
+ });
+ });
+
+ describe('when pasting inline content and there is text selected', () => {
+ it('inserts the block content after the existing paragraph', async () => {
+ const expectedDoc = doc(p('Initial text', bold('bold text')));
+
+ tiptapEditor.commands.setContent('Initial text and ');
+ tiptapEditor.commands.setTextSelection({ from: 13, to: 17 });
+
+ await triggerPasteEventHandlerAndWaitForTransaction(buildClipboardEvent());
+
+ expect(tiptapEditor.state.doc.toJSON()).toEqual(expectedDoc.toJSON());
+ });
+ });
+
+ describe('when pasting block content in an existing paragraph', () => {
+ beforeEach(() => {
+ renderMarkdown.mockReset();
+ renderMarkdown.mockResolvedValueOnce('<h1>Heading</h1><p><strong>bold text</strong></p>');
+ });
+
+ it('inserts the block content after the existing paragraph', async () => {
+ const expectedDoc = doc(
+ p('Initial text and'),
+ heading({ level: 1 }, 'Heading'),
+ p(bold('bold text')),
+ );
+
+ tiptapEditor.commands.setContent('Initial text and ');
+
+ await triggerPasteEventHandlerAndWaitForTransaction(buildClipboardEvent());
+
+ expect(tiptapEditor.state.doc.toJSON()).toEqual(expectedDoc.toJSON());
+ });
+ });
});
describe('when rendering markdown fails', () => {
diff --git a/spec/frontend/content_editor/markdown_snapshot_spec.js b/spec/frontend/content_editor/markdown_snapshot_spec.js
index fd64003420e..49b466fd7f5 100644
--- a/spec/frontend/content_editor/markdown_snapshot_spec.js
+++ b/spec/frontend/content_editor/markdown_snapshot_spec.js
@@ -42,7 +42,7 @@ describe('markdown example snapshots in ContentEditor', () => {
const expectedProseMirrorJsonExamples = loadExamples(prosemirrorJsonYml);
const exampleNames = Object.keys(markdownExamples);
- beforeAll(async () => {
+ beforeAll(() => {
return renderHtmlAndJsonForAllExamples(markdownExamples).then((examples) => {
actualHtmlAndJsonExamples = examples;
});
@@ -60,7 +60,7 @@ describe('markdown example snapshots in ContentEditor', () => {
if (skipRunningSnapshotWysiwygHtmlTests) {
it.todo(`${exampleNamePrefix} HTML: ${skipRunningSnapshotWysiwygHtmlTests}`);
} else {
- it(`${exampleNamePrefix} HTML`, async () => {
+ it(`${exampleNamePrefix} HTML`, () => {
const expectedHtml = expectedHtmlExamples[name].wysiwyg;
const { html: actualHtml } = actualHtmlAndJsonExamples[name];
@@ -78,7 +78,7 @@ describe('markdown example snapshots in ContentEditor', () => {
if (skipRunningSnapshotProsemirrorJsonTests) {
it.todo(`${exampleNamePrefix} ProseMirror JSON: ${skipRunningSnapshotProsemirrorJsonTests}`);
} else {
- it(`${exampleNamePrefix} ProseMirror JSON`, async () => {
+ it(`${exampleNamePrefix} ProseMirror JSON`, () => {
const expectedJson = expectedProseMirrorJsonExamples[name];
const { json: actualJson } = actualHtmlAndJsonExamples[name];
diff --git a/spec/frontend/content_editor/services/content_editor_spec.js b/spec/frontend/content_editor/services/content_editor_spec.js
index 6175cbdd3d4..5dfe9c06923 100644
--- a/spec/frontend/content_editor/services/content_editor_spec.js
+++ b/spec/frontend/content_editor/services/content_editor_spec.js
@@ -64,13 +64,13 @@ describe('content_editor/services/content_editor', () => {
});
describe('editable', () => {
- it('returns true when tiptapEditor is editable', async () => {
+ it('returns true when tiptapEditor is editable', () => {
contentEditor.setEditable(true);
expect(contentEditor.editable).toBe(true);
});
- it('returns false when tiptapEditor is readonly', async () => {
+ it('returns false when tiptapEditor is readonly', () => {
contentEditor.setEditable(false);
expect(contentEditor.editable).toBe(false);
diff --git a/spec/frontend/content_editor/services/create_content_editor_spec.js b/spec/frontend/content_editor/services/create_content_editor_spec.js
index 00cc628ca72..53cd51b8c5f 100644
--- a/spec/frontend/content_editor/services/create_content_editor_spec.js
+++ b/spec/frontend/content_editor/services/create_content_editor_spec.js
@@ -53,7 +53,7 @@ describe('content_editor/services/create_content_editor', () => {
});
});
- it('allows providing external content editor extensions', async () => {
+ it('allows providing external content editor extensions', () => {
const labelReference = 'this is a ~group::editor';
const { tiptapExtension, serializer } = createTestContentEditorExtension();
diff --git a/spec/frontend/content_editor/services/gl_api_markdown_deserializer_spec.js b/spec/frontend/content_editor/services/gl_api_markdown_deserializer_spec.js
index 8ee37282ee9..a9960918e62 100644
--- a/spec/frontend/content_editor/services/gl_api_markdown_deserializer_spec.js
+++ b/spec/frontend/content_editor/services/gl_api_markdown_deserializer_spec.js
@@ -43,7 +43,7 @@ describe('content_editor/services/gl_api_markdown_deserializer', () => {
});
});
- it('transforms HTML returned by render function to a ProseMirror document', async () => {
+ it('transforms HTML returned by render function to a ProseMirror document', () => {
const document = doc(p(bold(text)), comment(' some comment '));
expect(result.document.toJSON()).toEqual(document.toJSON());
diff --git a/spec/frontend/content_editor/services/markdown_serializer_spec.js b/spec/frontend/content_editor/services/markdown_serializer_spec.js
index c4d302547a5..a28d5a278e6 100644
--- a/spec/frontend/content_editor/services/markdown_serializer_spec.js
+++ b/spec/frontend/content_editor/services/markdown_serializer_spec.js
@@ -268,6 +268,19 @@ comment -->
).toBe('![GitLab][gitlab-url]');
});
+ it('omits image data urls when serializing', () => {
+ expect(
+ serialize(
+ paragraph(
+ image({
+ src: 'data:image/png;base64,iVBORw0KGgoAAAAN',
+ alt: 'image',
+ }),
+ ),
+ ),
+ ).toBe('![image]()');
+ });
+
it('correctly serializes strikethrough', () => {
expect(serialize(paragraph(strike('deleted content')))).toBe('~~deleted content~~');
});
@@ -885,6 +898,59 @@ _An elephant at sunset_
);
});
+ it('correctly renders a table with checkboxes', () => {
+ expect(
+ serialize(
+ table(
+ // each table cell must contain at least one paragraph
+ tableRow(
+ tableHeader(paragraph('')),
+ tableHeader(paragraph('Item')),
+ tableHeader(paragraph('Description')),
+ ),
+ tableRow(
+ tableCell(taskList(taskItem(paragraph('')))),
+ tableCell(paragraph('Item 1')),
+ tableCell(paragraph('Description 1')),
+ ),
+ tableRow(
+ tableCell(taskList(taskItem(paragraph('some text')))),
+ tableCell(paragraph('Item 2')),
+ tableCell(paragraph('Description 2')),
+ ),
+ ),
+ ).trim(),
+ ).toBe(
+ `
+<table>
+<tr>
+<th>
+
+</th>
+<th>Item</th>
+<th>Description</th>
+</tr>
+<tr>
+<td>
+
+* [ ] &nbsp;
+</td>
+<td>Item 1</td>
+<td>Description 1</td>
+</tr>
+<tr>
+<td>
+
+* [ ] some text
+</td>
+<td>Item 2</td>
+<td>Description 2</td>
+</tr>
+</table>
+ `.trim(),
+ );
+ });
+
it('correctly serializes a table with line breaks', () => {
expect(
serialize(
@@ -1309,6 +1375,25 @@ paragraph
.run();
};
+ const editNonInclusiveMarkAction = (initialContent) => {
+ tiptapEditor.commands.setContent(initialContent.toJSON());
+ tiptapEditor.commands.selectTextblockEnd();
+
+ let { from } = tiptapEditor.state.selection;
+ tiptapEditor.commands.setTextSelection({
+ from: from - 1,
+ to: from - 1,
+ });
+
+ const sel = tiptapEditor.state.doc.textBetween(from - 1, from, ' ');
+ tiptapEditor.commands.insertContent(`${sel} modified`);
+
+ tiptapEditor.commands.selectTextblockEnd();
+ from = tiptapEditor.state.selection.from;
+
+ tiptapEditor.commands.deleteRange({ from: from - 1, to: from });
+ };
+
it.each`
mark | markdown | modifiedMarkdown | editAction
${'bold'} | ${'**bold**'} | ${'**bold modified**'} | ${defaultEditAction}
@@ -1319,8 +1404,8 @@ paragraph
${'italic'} | ${'*italic*'} | ${'*italic modified*'} | ${defaultEditAction}
${'italic'} | ${'<em>italic</em>'} | ${'<em>italic modified</em>'} | ${defaultEditAction}
${'italic'} | ${'<i>italic</i>'} | ${'<i>italic modified</i>'} | ${defaultEditAction}
- ${'link'} | ${'[gitlab](https://gitlab.com)'} | ${'[gitlab modified](https://gitlab.com)'} | ${defaultEditAction}
- ${'link'} | ${'<a href="https://gitlab.com">link</a>'} | ${'<a href="https://gitlab.com">link modified</a>'} | ${defaultEditAction}
+ ${'link'} | ${'[gitlab](https://gitlab.com)'} | ${'[gitlab modified](https://gitlab.com)'} | ${editNonInclusiveMarkAction}
+ ${'link'} | ${'<a href="https://gitlab.com">link</a>'} | ${'<a href="https://gitlab.com">link modified</a>'} | ${editNonInclusiveMarkAction}
${'link'} | ${'link www.gitlab.com'} | ${'modified link www.gitlab.com'} | ${prependContentEditAction}
${'link'} | ${'link https://www.gitlab.com'} | ${'modified link https://www.gitlab.com'} | ${prependContentEditAction}
${'link'} | ${'link(https://www.gitlab.com)'} | ${'modified link(https://www.gitlab.com)'} | ${prependContentEditAction}
diff --git a/spec/frontend/content_editor/services/track_input_rules_and_shortcuts_spec.js b/spec/frontend/content_editor/services/track_input_rules_and_shortcuts_spec.js
index 8c1a3831a74..1459988cf8f 100644
--- a/spec/frontend/content_editor/services/track_input_rules_and_shortcuts_spec.js
+++ b/spec/frontend/content_editor/services/track_input_rules_and_shortcuts_spec.js
@@ -43,7 +43,7 @@ describe('content_editor/services/track_input_rules_and_shortcuts', () => {
});
describe('when creating a heading using an keyboard shortcut', () => {
- it('sends a tracking event indicating that a heading was created using an input rule', async () => {
+ it('sends a tracking event indicating that a heading was created using an input rule', () => {
const shortcuts = Heading.parent.config.addKeyboardShortcuts.call(Heading);
const [firstShortcut] = Object.keys(shortcuts);
const nodeName = Heading.name;
@@ -68,7 +68,7 @@ describe('content_editor/services/track_input_rules_and_shortcuts', () => {
});
describe('when creating a heading using an input rule', () => {
- it('sends a tracking event indicating that a heading was created using an input rule', async () => {
+ it('sends a tracking event indicating that a heading was created using an input rule', () => {
const nodeName = Heading.name;
triggerNodeInputRule({ tiptapEditor: editor, inputRuleText: '## ' });
expect(trackingSpy).toHaveBeenCalledWith(undefined, INPUT_RULE_TRACKING_ACTION, {
diff --git a/spec/frontend/contributors/component/__snapshots__/contributors_spec.js.snap b/spec/frontend/contributors/component/__snapshots__/contributors_spec.js.snap
index 4b7439f6fd2..5cfb4702be7 100644
--- a/spec/frontend/contributors/component/__snapshots__/contributors_spec.js.snap
+++ b/spec/frontend/contributors/component/__snapshots__/contributors_spec.js.snap
@@ -64,6 +64,7 @@ exports[`Contributors charts should render charts and a RefSelector when loading
legendlayout="inline"
legendmaxtext="Max"
legendmintext="Min"
+ legendseriesinfo=""
option="[object Object]"
responsive=""
thresholds=""
@@ -100,6 +101,7 @@ exports[`Contributors charts should render charts and a RefSelector when loading
legendlayout="inline"
legendmaxtext="Max"
legendmintext="Min"
+ legendseriesinfo=""
option="[object Object]"
responsive=""
thresholds=""
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 12fef9d5ddf..d3cdd0d16ef 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
@@ -173,7 +173,7 @@ describe('custom metrics form fields component', () => {
return axios.waitForAll();
});
- it('shows invalid query message', async () => {
+ it('shows invalid query message', () => {
expect(wrapper.text()).toContain(errorMessage);
});
});
diff --git a/spec/frontend/deploy_keys/components/key_spec.js b/spec/frontend/deploy_keys/components/key_spec.js
index 5f20d4ad542..3c4fa2a6de6 100644
--- a/spec/frontend/deploy_keys/components/key_spec.js
+++ b/spec/frontend/deploy_keys/components/key_spec.js
@@ -1,9 +1,10 @@
import { mount } from '@vue/test-utils';
import { nextTick } from 'vue';
import data from 'test_fixtures/deploy_keys/keys.json';
+import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import key from '~/deploy_keys/components/key.vue';
import DeployKeysStore from '~/deploy_keys/store';
-import { getTimeago } from '~/lib/utils/datetime_utility';
+import { getTimeago, formatDate } from '~/lib/utils/datetime_utility';
describe('Deploy keys key', () => {
let wrapper;
@@ -18,6 +19,9 @@ describe('Deploy keys key', () => {
endpoint: 'https://test.host/dummy/endpoint',
...propsData,
},
+ directives: {
+ GlTooltip: createMockDirective('gl-tooltip'),
+ },
});
};
@@ -43,6 +47,33 @@ describe('Deploy keys key', () => {
);
});
+ it('renders human friendly expiration date', () => {
+ const expiresAt = new Date();
+ createComponent({
+ deployKey: { ...deployKey, expires_at: expiresAt },
+ });
+
+ expect(findTextAndTrim('.key-expires-at')).toBe(`${getTimeago().format(expiresAt)}`);
+ });
+ it('shows tooltip for expiration date', () => {
+ const expiresAt = new Date();
+ createComponent({
+ deployKey: { ...deployKey, expires_at: expiresAt },
+ });
+
+ const expiryComponent = wrapper.find('[data-testid="expires-at-tooltip"]');
+ const tooltip = getBinding(expiryComponent.element, 'gl-tooltip');
+ expect(tooltip).toBeDefined();
+ expect(expiryComponent.attributes('title')).toBe(`${formatDate(expiresAt)}`);
+ });
+ it('renders never when no expiration date', () => {
+ createComponent({
+ deployKey: { ...deployKey, expires_at: null },
+ });
+
+ expect(wrapper.find('[data-testid="expires-never"]').exists()).toBe(true);
+ });
+
it('shows pencil button for editing', () => {
createComponent({ deployKey });
diff --git a/spec/frontend/design_management/components/design_notes/design_discussion_spec.js b/spec/frontend/design_management/components/design_notes/design_discussion_spec.js
index 56bf0fa60a7..a6ab147884f 100644
--- a/spec/frontend/design_management/components/design_notes/design_discussion_spec.js
+++ b/spec/frontend/design_management/components/design_notes/design_discussion_spec.js
@@ -381,7 +381,7 @@ describe('Design discussions component', () => {
});
});
- it('should open confirmation modal when the note emits `delete-note` event', async () => {
+ it('should open confirmation modal when the note emits `delete-note` event', () => {
createComponent();
findDesignNotes().at(0).vm.$emit('delete-note', { id: '1' });
diff --git a/spec/frontend/design_management/components/design_notes/design_note_spec.js b/spec/frontend/design_management/components/design_notes/design_note_spec.js
index 82848bd1a19..6f5b282fa3b 100644
--- a/spec/frontend/design_management/components/design_notes/design_note_spec.js
+++ b/spec/frontend/design_management/components/design_notes/design_note_spec.js
@@ -189,7 +189,7 @@ describe('Design note component', () => {
});
});
- it('should emit `delete-note` event with proper payload when delete note button is clicked', async () => {
+ it('should emit `delete-note` event with proper payload when delete note button is clicked', () => {
const payload = {
...note,
userPermissions: {
diff --git a/spec/frontend/design_management/components/design_notes/design_reply_form_spec.js b/spec/frontend/design_management/components/design_notes/design_reply_form_spec.js
index db1cfb4f504..f08efc0c685 100644
--- a/spec/frontend/design_management/components/design_notes/design_reply_form_spec.js
+++ b/spec/frontend/design_management/components/design_notes/design_reply_form_spec.js
@@ -1,7 +1,9 @@
import { GlAlert } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
-import { nextTick } from 'vue';
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
import Autosave from '~/autosave';
+import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
import createNoteMutation from '~/design_management/graphql/mutations/create_note.mutation.graphql';
@@ -17,11 +19,14 @@ import {
mockNoteSubmitFailureMutationResponse,
} from '../../mock_data/apollo_mock';
+Vue.use(VueApollo);
+
jest.mock('~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal');
jest.mock('~/autosave');
describe('Design reply form component', () => {
let wrapper;
+ let mockApollo;
const findTextarea = () => wrapper.find('textarea');
const findSubmitButton = () => wrapper.findComponent({ ref: 'submitButton' });
@@ -32,14 +37,10 @@ describe('Design reply form component', () => {
const mockComment = 'New comment';
const mockDiscussionId = 'gid://gitlab/Discussion/6466a72f35b163f3c3e52d7976a09387f2c573e8';
const createNoteMutationData = {
- mutation: createNoteMutation,
- update: expect.anything(),
- variables: {
- input: {
- noteableId: mockNoteableId,
- discussionId: mockDiscussionId,
- body: mockComment,
- },
+ input: {
+ noteableId: mockNoteableId,
+ discussionId: mockDiscussionId,
+ body: mockComment,
},
};
@@ -49,14 +50,15 @@ describe('Design reply form component', () => {
const metaKey = {
metaKey: true,
};
- const mutationHandler = jest.fn().mockResolvedValue();
+ const mockMutationHandler = jest.fn().mockResolvedValue(mockNoteSubmitSuccessMutationResponse);
function createComponent({
props = {},
mountOptions = {},
data = {},
- mutation = mutationHandler,
+ mutationHandler = mockMutationHandler,
} = {}) {
+ mockApollo = createMockApollo([[createNoteMutation, mutationHandler]]);
wrapper = mount(DesignReplyForm, {
propsData: {
designNoteMutation: createNoteMutation,
@@ -67,11 +69,7 @@ describe('Design reply form component', () => {
...props,
},
...mountOptions,
- mocks: {
- $apollo: {
- mutate: mutation,
- },
- },
+ apolloProvider: mockApollo,
data() {
return {
...data,
@@ -85,6 +83,7 @@ describe('Design reply form component', () => {
});
afterEach(() => {
+ mockApollo = null;
confirmAction.mockReset();
});
@@ -125,9 +124,8 @@ describe('Design reply form component', () => {
${'gid://gitlab/DiffDiscussion/123'} | ${123}
`(
'initializes autosave support on discussion with proper key',
- async ({ discussionId, shortDiscussionId }) => {
+ ({ discussionId, shortDiscussionId }) => {
createComponent({ props: { discussionId } });
- await nextTick();
expect(Autosave).toHaveBeenCalledWith(expect.any(Element), [
'Discussion',
@@ -138,9 +136,8 @@ describe('Design reply form component', () => {
);
describe('when form has no text', () => {
- beforeEach(async () => {
+ beforeEach(() => {
createComponent();
- await nextTick();
});
it('submit button is disabled', () => {
@@ -151,11 +148,10 @@ describe('Design reply form component', () => {
key | keyData
${'ctrl'} | ${ctrlKey}
${'meta'} | ${metaKey}
- `('does not perform mutation on textarea $key+enter keydown', async ({ keyData }) => {
+ `('does not perform mutation on textarea $key+enter keydown', ({ keyData }) => {
findTextarea().trigger('keydown.enter', keyData);
- await nextTick();
- expect(mutationHandler).not.toHaveBeenCalled();
+ expect(mockMutationHandler).not.toHaveBeenCalled();
});
it('emits cancelForm event on pressing escape button on textarea', () => {
@@ -182,22 +178,20 @@ describe('Design reply form component', () => {
noteableId: mockNoteableId,
discussionId: mockDiscussionId,
};
- const successfulMutation = jest.fn().mockResolvedValue(mockNoteSubmitSuccessMutationResponse);
+
createComponent({
props: {
- designNoteMutation: createNoteMutation,
mutationVariables: mockMutationVariables,
value: mockComment,
},
- mutation: successfulMutation,
});
findSubmitButton().vm.$emit('click');
- await nextTick();
- expect(successfulMutation).toHaveBeenCalledWith(createNoteMutationData);
+ expect(mockMutationHandler).toHaveBeenCalledWith(createNoteMutationData);
await waitForPromises();
+
expect(wrapper.emitted('note-submit-complete')).toEqual([
[mockNoteSubmitSuccessMutationResponse],
]);
@@ -212,20 +206,17 @@ describe('Design reply form component', () => {
noteableId: mockNoteableId,
discussionId: mockDiscussionId,
};
- const successfulMutation = jest.fn().mockResolvedValue(mockNoteSubmitSuccessMutationResponse);
+
createComponent({
props: {
- designNoteMutation: createNoteMutation,
mutationVariables: mockMutationVariables,
value: mockComment,
},
- mutation: successfulMutation,
});
findTextarea().trigger('keydown.enter', keyData);
- await nextTick();
- expect(successfulMutation).toHaveBeenCalledWith(createNoteMutationData);
+ expect(mockMutationHandler).toHaveBeenCalledWith(createNoteMutationData);
await waitForPromises();
expect(wrapper.emitted('note-submit-complete')).toEqual([
@@ -240,7 +231,7 @@ describe('Design reply form component', () => {
designNoteMutation: createNoteMutation,
value: mockComment,
},
- mutation: failedMutation,
+ mutationHandler: failedMutation,
data: {
errorMessage: 'error',
},
@@ -260,7 +251,7 @@ describe('Design reply form component', () => {
${false} | ${false} | ${UPDATE_NOTE_ERROR}
`(
'return proper error message on error in case of isDiscussion is $isDiscussion and isNewComment is $isNewComment',
- async ({ isDiscussion, isNewComment, errorMessage }) => {
+ ({ isDiscussion, isNewComment, errorMessage }) => {
createComponent({ props: { isDiscussion, isNewComment } });
expect(wrapper.vm.getErrorMessage()).toBe(errorMessage);
@@ -275,12 +266,11 @@ describe('Design reply form component', () => {
expect(wrapper.emitted('cancel-form')).toHaveLength(1);
});
- it('opens confirmation modal on Escape key when text has changed', async () => {
+ it('opens confirmation modal on Escape key when text has changed', () => {
createComponent();
findTextarea().setValue(mockComment);
- await nextTick();
findTextarea().trigger('keyup.esc');
expect(confirmAction).toHaveBeenCalled();
@@ -292,7 +282,6 @@ describe('Design reply form component', () => {
createComponent({ props: { value: mockComment } });
findTextarea().setValue('Comment changed');
- await nextTick();
findTextarea().trigger('keyup.esc');
expect(confirmAction).toHaveBeenCalled();
@@ -306,10 +295,8 @@ describe('Design reply form component', () => {
createComponent({ props: { value: mockComment } });
findTextarea().setValue('Comment changed');
- await nextTick();
findTextarea().trigger('keyup.esc');
- await nextTick();
expect(confirmAction).toHaveBeenCalled();
await waitForPromises();
diff --git a/spec/frontend/design_management/components/design_overlay_spec.js b/spec/frontend/design_management/components/design_overlay_spec.js
index 2807fe7727f..3eb47fdb97e 100644
--- a/spec/frontend/design_management/components/design_overlay_spec.js
+++ b/spec/frontend/design_management/components/design_overlay_spec.js
@@ -1,6 +1,6 @@
-import { shallowMount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import DesignOverlay from '~/design_management/components/design_overlay.vue';
@@ -16,22 +16,20 @@ describe('Design overlay component', () => {
const mockDimensions = { width: 100, height: 100 };
- 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 findOverlay = () => wrapper.findByTestId('design-overlay');
+ const findAllNotes = () => wrapper.findAllByTestId('note-pin');
+ const findCommentBadge = () => wrapper.findByTestId('comment-badge');
const findBadgeAtIndex = (noteIndex) => findAllNotes().at(noteIndex);
const findFirstBadge = () => findBadgeAtIndex(0);
const findSecondBadge = () => findBadgeAtIndex(1);
- const clickAndDragBadge = async (elem, fromPoint, toPoint) => {
+ const clickAndDragBadge = (elem, fromPoint, toPoint) => {
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.vm.$emit('mouseup', new MouseEvent('click', { clientX: toPoint.x, clientY: toPoint.y }));
- await nextTick();
};
function createComponent(props = {}, data = {}) {
@@ -47,7 +45,7 @@ describe('Design overlay component', () => {
},
});
- wrapper = shallowMount(DesignOverlay, {
+ wrapper = shallowMountExtended(DesignOverlay, {
apolloProvider,
propsData: {
dimensions: mockDimensions,
@@ -80,7 +78,7 @@ describe('Design overlay component', () => {
expect(wrapper.attributes().style).toBe('width: 100px; height: 100px; top: 0px; left: 0px;');
});
- it('should emit `openCommentForm` when clicking on overlay', async () => {
+ it('should emit `openCommentForm` when clicking on overlay', () => {
createComponent();
const newCoordinates = {
x: 10,
@@ -90,7 +88,7 @@ describe('Design overlay component', () => {
wrapper
.find('[data-qa-selector="design_image_button"]')
.trigger('mouseup', { offsetX: newCoordinates.x, offsetY: newCoordinates.y });
- await nextTick();
+
expect(wrapper.emitted('openCommentForm')).toEqual([
[{ x: newCoordinates.x, y: newCoordinates.y }],
]);
@@ -175,25 +173,15 @@ describe('Design overlay component', () => {
});
});
- it('should recalculate badges positions on window resize', async () => {
+ it('should calculate badges positions based on dimensions', () => {
createComponent({
notes,
dimensions: {
- width: 400,
- height: 400,
- },
- });
-
- expect(findFirstBadge().props('position')).toEqual({ left: '40px', top: '60px' });
-
- wrapper.setProps({
- dimensions: {
width: 200,
height: 200,
},
});
- await nextTick();
expect(findFirstBadge().props('position')).toEqual({ left: '20px', top: '30px' });
});
@@ -216,7 +204,6 @@ describe('Design overlay component', () => {
new MouseEvent('click', { clientX: position.x, clientY: position.y }),
);
- await nextTick();
findFirstBadge().vm.$emit(
'mouseup',
new MouseEvent('click', { clientX: position.x, clientY: position.y }),
@@ -290,7 +277,7 @@ describe('Design overlay component', () => {
});
describe('when moving the comment badge', () => {
- it('should update badge style when note-moving action ends', async () => {
+ it('should update badge style when note-moving action ends', () => {
const { position } = notes[0];
createComponent({
currentCommentForm: {
@@ -298,19 +285,15 @@ describe('Design overlay component', () => {
},
});
- const commentBadge = findCommentBadge();
+ expect(findCommentBadge().props('position')).toEqual({ left: '10px', top: '15px' });
+
const toPoint = { x: 20, y: 20 };
- await clickAndDragBadge(commentBadge, { x: position.x, y: position.y }, toPoint);
- 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({
+ createComponent({
currentCommentForm: { height: position.height, width: position.width, ...toPoint },
});
- await nextTick();
- expect(commentBadge.props('position')).toEqual({ left: '20px', top: '20px' });
+ expect(findCommentBadge().props('position')).toEqual({ left: '20px', top: '20px' });
});
it('should emit `openCommentForm` event when mouseleave fired on overlay element', async () => {
@@ -330,8 +313,7 @@ describe('Design overlay component', () => {
newCoordinates,
);
- wrapper.trigger('mouseleave');
- await nextTick();
+ findOverlay().vm.$emit('mouseleave');
expect(wrapper.emitted('openCommentForm')).toEqual([[newCoordinates]]);
});
diff --git a/spec/frontend/design_management/components/design_scaler_spec.js b/spec/frontend/design_management/components/design_scaler_spec.js
index 62a26a8f5dd..b29448b4471 100644
--- a/spec/frontend/design_management/components/design_scaler_spec.js
+++ b/spec/frontend/design_management/components/design_scaler_spec.js
@@ -36,7 +36,7 @@ describe('Design management design scaler component', () => {
expect(wrapper.emitted('scale')[1]).toEqual([1]);
});
- it('emits @scale event when "decrement" button clicked', async () => {
+ it('emits @scale event when "decrement" button clicked', () => {
getDecreaseScaleButton().vm.$emit('click');
expect(wrapper.emitted('scale')[1]).toEqual([1.4]);
});
diff --git a/spec/frontend/design_management/components/design_todo_button_spec.js b/spec/frontend/design_management/components/design_todo_button_spec.js
index f713203c0ee..698535d8937 100644
--- a/spec/frontend/design_management/components/design_todo_button_spec.js
+++ b/spec/frontend/design_management/components/design_todo_button_spec.js
@@ -81,7 +81,7 @@ describe('Design management design todo button', () => {
await nextTick();
});
- it('calls `$apollo.mutate` with the `todoMarkDone` mutation and variables containing `id`', async () => {
+ it('calls `$apollo.mutate` with the `todoMarkDone` mutation and variables containing `id`', () => {
const todoMarkDoneMutationVariables = {
mutation: todoMarkDoneMutation,
update: expect.anything(),
@@ -127,7 +127,7 @@ describe('Design management design todo button', () => {
await nextTick();
});
- it('calls `$apollo.mutate` with the `createDesignTodoMutation` mutation and variables containing `issuable_id`, `issue_id`, & `projectPath`', async () => {
+ it('calls `$apollo.mutate` with the `createDesignTodoMutation` mutation and variables containing `issuable_id`, `issue_id`, & `projectPath`', () => {
const createDesignTodoMutationVariables = {
mutation: createDesignTodoMutation,
update: expect.anything(),
diff --git a/spec/frontend/design_management/components/toolbar/__snapshots__/design_navigation_spec.js.snap b/spec/frontend/design_management/components/toolbar/__snapshots__/design_navigation_spec.js.snap
index b5a69b28a88..934bda570d4 100644
--- a/spec/frontend/design_management/components/toolbar/__snapshots__/design_navigation_spec.js.snap
+++ b/spec/frontend/design_management/components/toolbar/__snapshots__/design_navigation_spec.js.snap
@@ -7,7 +7,7 @@ exports[`Design management pagination component renders navigation buttons 1`] =
class="gl-display-flex gl-align-items-center"
>
- 0 of 2
+ 0 of 3
<gl-button-group-stub
class="gl-mx-5"
diff --git a/spec/frontend/design_management/components/toolbar/design_navigation_spec.js b/spec/frontend/design_management/components/toolbar/design_navigation_spec.js
index 8427d83ceee..28d6b7118be 100644
--- a/spec/frontend/design_management/components/toolbar/design_navigation_spec.js
+++ b/spec/frontend/design_management/components/toolbar/design_navigation_spec.js
@@ -1,9 +1,18 @@
/* global Mousetrap */
import 'mousetrap';
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import { GlButtonGroup } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import { nextTick } from 'vue';
import DesignNavigation from '~/design_management/components/toolbar/design_navigation.vue';
import { DESIGN_ROUTE_NAME } from '~/design_management/router/constants';
+import getDesignListQuery from 'shared_queries/design_management/get_design_list.query.graphql';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import {
+ getDesignListQueryResponse,
+ designListQueryResponseNodes,
+} from '../../mock_data/apollo_mock';
const push = jest.fn();
const $router = {
@@ -18,11 +27,23 @@ const $route = {
describe('Design management pagination component', () => {
let wrapper;
- function createComponent() {
+ const buildMockHandler = (nodes = designListQueryResponseNodes) => {
+ return jest.fn().mockResolvedValue(getDesignListQueryResponse({ designs: nodes }));
+ };
+
+ const createMockApolloProvider = (handler) => {
+ Vue.use(VueApollo);
+
+ return createMockApollo([[getDesignListQuery, handler]]);
+ };
+
+ function createComponent({ propsData = {}, handler = buildMockHandler() } = {}) {
wrapper = shallowMount(DesignNavigation, {
propsData: {
id: '2',
+ ...propsData,
},
+ apolloProvider: createMockApolloProvider(handler),
mocks: {
$router,
$route,
@@ -30,48 +51,45 @@ describe('Design management pagination component', () => {
});
}
- beforeEach(() => {
- createComponent();
- });
+ const findGlButtonGroup = () => wrapper.findComponent(GlButtonGroup);
+
+ it('hides components when designs are empty', async () => {
+ createComponent({ handler: buildMockHandler([]) });
+ await waitForPromises();
- it('hides components when designs are empty', () => {
+ expect(findGlButtonGroup().exists()).toBe(false);
expect(wrapper.element).toMatchSnapshot();
});
it('renders navigation buttons', 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({
- designCollection: { designs: [{ id: '1' }, { id: '2' }] },
- });
+ createComponent({ handler: buildMockHandler() });
+ await waitForPromises();
- await nextTick();
+ expect(findGlButtonGroup().exists()).toBe(true);
expect(wrapper.element).toMatchSnapshot();
});
describe('keyboard buttons navigation', () => {
- beforeEach(() => {
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({
- designCollection: { designs: [{ filename: '1' }, { filename: '2' }, { filename: '3' }] },
- });
- });
+ it('routes to previous design on Left button', async () => {
+ createComponent({ propsData: { id: designListQueryResponseNodes[1].filename } });
+ await waitForPromises();
- it('routes to previous design on Left button', () => {
Mousetrap.trigger('left');
expect(push).toHaveBeenCalledWith({
name: DESIGN_ROUTE_NAME,
- params: { id: '1' },
+ params: { id: designListQueryResponseNodes[0].filename },
query: {},
});
});
- it('routes to next design on Right button', () => {
+ it('routes to next design on Right button', async () => {
+ createComponent({ propsData: { id: designListQueryResponseNodes[1].filename } });
+ await waitForPromises();
+
Mousetrap.trigger('right');
expect(push).toHaveBeenCalledWith({
name: DESIGN_ROUTE_NAME,
- params: { id: '3' },
+ params: { id: designListQueryResponseNodes[2].filename },
query: {},
});
});
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 cdfff61ba4f..3ee68f80538 100644
--- a/spec/frontend/design_management/components/upload/design_version_dropdown_spec.js
+++ b/spec/frontend/design_management/components/upload/design_version_dropdown_spec.js
@@ -1,9 +1,14 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
import { GlAvatar, GlCollapsibleListbox, GlListboxItem } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import { nextTick } from 'vue';
+import getDesignListQuery from 'shared_queries/design_management/get_design_list.query.graphql';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
import DesignVersionDropdown from '~/design_management/components/upload/design_version_dropdown.vue';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
-import mockAllVersions from './mock_data/all_versions';
+import mockAllVersions from '../../mock_data/all_versions';
+import { getDesignListQueryResponse } from '../../mock_data/apollo_mock';
const LATEST_VERSION_ID = 1;
const PREVIOUS_VERSION_ID = 2;
@@ -20,11 +25,20 @@ const MOCK_ROUTE = {
query: {},
};
+Vue.use(VueApollo);
+
describe('Design management design version dropdown component', () => {
let wrapper;
function createComponent({ maxVersions = -1, $route = MOCK_ROUTE } = {}) {
+ const designVersions =
+ maxVersions > -1 ? mockAllVersions.slice(0, maxVersions) : mockAllVersions;
+ const designListHandler = jest
+ .fn()
+ .mockResolvedValue(getDesignListQueryResponse({ versions: designVersions }));
+
wrapper = shallowMount(DesignVersionDropdown, {
+ apolloProvider: createMockApollo([[getDesignListQuery, designListHandler]]),
propsData: {
projectPath: '',
issueIid: '',
@@ -34,12 +48,6 @@ describe('Design management design version dropdown component', () => {
},
stubs: { GlAvatar: true, GlCollapsibleListbox },
});
-
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({
- allVersions: maxVersions > -1 ? mockAllVersions.slice(0, maxVersions) : mockAllVersions,
- });
}
const findListbox = () => wrapper.findComponent(GlCollapsibleListbox);
@@ -52,7 +60,7 @@ describe('Design management design version dropdown component', () => {
beforeEach(async () => {
createComponent();
- await nextTick();
+ await waitForPromises();
listItem = findAllListboxItems().at(0);
});
@@ -74,7 +82,8 @@ describe('Design management design version dropdown component', () => {
it('has "latest" on most recent version item', async () => {
createComponent();
- await nextTick();
+ await waitForPromises();
+
expect(findVersionLink(0).text()).toContain('latest');
});
});
@@ -83,7 +92,7 @@ describe('Design management design version dropdown component', () => {
it('displays latest version text by default', async () => {
createComponent();
- await nextTick();
+ await waitForPromises();
expect(findListbox().props('toggleText')).toBe('Showing latest version');
});
@@ -91,35 +100,39 @@ describe('Design management design version dropdown component', () => {
it('displays latest version text when only 1 version is present', async () => {
createComponent({ maxVersions: 1 });
- await nextTick();
+ await waitForPromises();
+
expect(findListbox().props('toggleText')).toBe('Showing latest version');
});
it('displays version text when the current version is not the latest', async () => {
createComponent({ $route: designRouteFactory(PREVIOUS_VERSION_ID) });
- await nextTick();
+ await waitForPromises();
+
expect(findListbox().props('toggleText')).toBe(`Showing version #1`);
});
it('displays latest version text when the current version is the latest', async () => {
createComponent({ $route: designRouteFactory(LATEST_VERSION_ID) });
- await nextTick();
+ await waitForPromises();
+
expect(findListbox().props('toggleText')).toBe('Showing latest version');
});
it('should have the same length as apollo query', async () => {
createComponent();
- await nextTick();
+ await waitForPromises();
+
expect(findAllListboxItems()).toHaveLength(wrapper.vm.allVersions.length);
});
it('should render TimeAgo', async () => {
createComponent();
- await nextTick();
+ await waitForPromises();
expect(wrapper.findAllComponents(TimeAgo)).toHaveLength(wrapper.vm.allVersions.length);
});
diff --git a/spec/frontend/design_management/components/upload/mock_data/all_versions.js b/spec/frontend/design_management/components/upload/mock_data/all_versions.js
deleted file mode 100644
index 24c59ce1a75..00000000000
--- a/spec/frontend/design_management/components/upload/mock_data/all_versions.js
+++ /dev/null
@@ -1,20 +0,0 @@
-export default [
- {
- id: 'gid://gitlab/DesignManagement::Version/1',
- sha: 'b389071a06c153509e11da1f582005b316667001',
- createdAt: '2021-08-09T06:05:00Z',
- author: {
- id: 'gid://gitlab/User/1',
- name: 'Adminstrator',
- },
- },
- {
- id: 'gid://gitlab/DesignManagement::Version/2',
- sha: 'b389071a06c153509e11da1f582005b316667021',
- createdAt: '2021-08-09T06:05:00Z',
- author: {
- id: 'gid://gitlab/User/1',
- name: 'Adminstrator',
- },
- },
-];
diff --git a/spec/frontend/design_management/mock_data/all_versions.js b/spec/frontend/design_management/mock_data/all_versions.js
index f4026da7dfd..36f611247a9 100644
--- a/spec/frontend/design_management/mock_data/all_versions.js
+++ b/spec/frontend/design_management/mock_data/all_versions.js
@@ -1,20 +1,26 @@
export default [
{
+ __typename: 'DesignVersion',
id: 'gid://gitlab/DesignManagement::Version/1',
sha: 'b389071a06c153509e11da1f582005b316667001',
createdAt: '2021-08-09T06:05:00Z',
author: {
+ __typename: 'UserCore',
id: 'gid://gitlab/User/1',
name: 'Adminstrator',
+ avatarUrl: 'avatar.png',
},
},
{
- id: 'gid://gitlab/DesignManagement::Version/1',
+ __typename: 'DesignVersion',
+ id: 'gid://gitlab/DesignManagement::Version/2',
sha: 'b389071a06c153509e11da1f582005b316667021',
createdAt: '2021-08-09T06:05:00Z',
author: {
+ __typename: 'UserCore',
id: 'gid://gitlab/User/1',
name: 'Adminstrator',
+ avatarUrl: 'avatar.png',
},
},
];
diff --git a/spec/frontend/design_management/mock_data/apollo_mock.js b/spec/frontend/design_management/mock_data/apollo_mock.js
index 2b99dcf14da..18e08ecd729 100644
--- a/spec/frontend/design_management/mock_data/apollo_mock.js
+++ b/spec/frontend/design_management/mock_data/apollo_mock.js
@@ -1,4 +1,49 @@
-export const designListQueryResponse = {
+export const designListQueryResponseNodes = [
+ {
+ __typename: 'Design',
+ id: '1',
+ event: 'NONE',
+ filename: 'fox_1.jpg',
+ notesCount: 3,
+ image: 'image-1',
+ imageV432x230: 'image-1',
+ currentUserTodos: {
+ __typename: 'ToDo',
+ nodes: [],
+ },
+ },
+ {
+ __typename: 'Design',
+ id: '2',
+ event: 'NONE',
+ filename: 'fox_2.jpg',
+ notesCount: 2,
+ image: 'image-2',
+ imageV432x230: 'image-2',
+ currentUserTodos: {
+ __typename: 'ToDo',
+ nodes: [],
+ },
+ },
+ {
+ __typename: 'Design',
+ id: '3',
+ event: 'NONE',
+ filename: 'fox_3.jpg',
+ notesCount: 1,
+ image: 'image-3',
+ imageV432x230: 'image-3',
+ currentUserTodos: {
+ __typename: 'ToDo',
+ nodes: [],
+ },
+ },
+];
+
+export const getDesignListQueryResponse = ({
+ versions = [],
+ designs = designListQueryResponseNodes,
+} = {}) => ({
data: {
project: {
__typename: 'Project',
@@ -11,57 +56,17 @@ export const designListQueryResponse = {
copyState: 'READY',
designs: {
__typename: 'DesignConnection',
- nodes: [
- {
- __typename: 'Design',
- id: '1',
- event: 'NONE',
- filename: 'fox_1.jpg',
- notesCount: 3,
- image: 'image-1',
- imageV432x230: 'image-1',
- currentUserTodos: {
- __typename: 'ToDo',
- nodes: [],
- },
- },
- {
- __typename: 'Design',
- id: '2',
- event: 'NONE',
- filename: 'fox_2.jpg',
- notesCount: 2,
- image: 'image-2',
- imageV432x230: 'image-2',
- currentUserTodos: {
- __typename: 'ToDo',
- nodes: [],
- },
- },
- {
- __typename: 'Design',
- id: '3',
- event: 'NONE',
- filename: 'fox_3.jpg',
- notesCount: 1,
- image: 'image-3',
- imageV432x230: 'image-3',
- currentUserTodos: {
- __typename: 'ToDo',
- nodes: [],
- },
- },
- ],
+ nodes: designs,
},
versions: {
- __typename: 'DesignVersion',
- nodes: [],
+ __typename: 'DesignVersionConnection',
+ nodes: versions,
},
},
},
},
},
-};
+});
export const designUploadMutationCreatedResponse = {
data: {
@@ -212,64 +217,62 @@ export const getDesignQueryResponse = {
},
};
-export const mockNoteSubmitSuccessMutationResponse = [
- {
- data: {
- createNote: {
- note: {
- id: 'gid://gitlab/DiffNote/468',
- 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',
- },
- body: 'New comment',
- bodyHtml: "<p data-sourcepos='1:1-1:4' dir='auto'>asdd</p>",
- createdAt: '2023-02-24T06:49:20Z',
- resolved: false,
- position: {
- diffRefs: {
- baseSha: 'f63ae53ed82d8765477c191383e1e6a000c10375',
- startSha: 'f63ae53ed82d8765477c191383e1e6a000c10375',
- headSha: 'f348c652f1a737151fc79047895e695fbe81464c',
- __typename: 'DiffRefs',
- },
- x: 441,
- y: 128,
- height: 152,
- width: 695,
- __typename: 'DiffPosition',
- },
- userPermissions: {
- adminNote: true,
- repositionNote: true,
- __typename: 'NotePermissions',
+export const mockNoteSubmitSuccessMutationResponse = {
+ data: {
+ createNote: {
+ note: {
+ id: 'gid://gitlab/DiffNote/468',
+ 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',
+ },
+ body: 'New comment',
+ bodyHtml: "<p data-sourcepos='1:1-1:4' dir='auto'>asdd</p>",
+ createdAt: '2023-02-24T06:49:20Z',
+ resolved: false,
+ position: {
+ diffRefs: {
+ baseSha: 'f63ae53ed82d8765477c191383e1e6a000c10375',
+ startSha: 'f63ae53ed82d8765477c191383e1e6a000c10375',
+ headSha: 'f348c652f1a737151fc79047895e695fbe81464c',
+ __typename: 'DiffRefs',
},
- discussion: {
- id: 'gid://gitlab/Discussion/6466a72f35b163f3c3e52d7976a09387f2c573e8',
- notes: {
- nodes: [
- {
- id: 'gid://gitlab/DiffNote/459',
- __typename: 'Note',
- },
- ],
- __typename: 'NoteConnection',
- },
- __typename: 'Discussion',
+ x: 441,
+ y: 128,
+ height: 152,
+ width: 695,
+ __typename: 'DiffPosition',
+ },
+ userPermissions: {
+ adminNote: true,
+ repositionNote: true,
+ __typename: 'NotePermissions',
+ },
+ discussion: {
+ id: 'gid://gitlab/Discussion/6466a72f35b163f3c3e52d7976a09387f2c573e8',
+ notes: {
+ nodes: [
+ {
+ id: 'gid://gitlab/DiffNote/459',
+ __typename: 'Note',
+ },
+ ],
+ __typename: 'NoteConnection',
},
- __typename: 'Note',
+ __typename: 'Discussion',
},
- errors: [],
- __typename: 'CreateNotePayload',
+ __typename: 'Note',
},
+ errors: [],
+ __typename: 'CreateNotePayload',
},
},
-];
+};
export const mockNoteSubmitFailureMutationResponse = [
{
diff --git a/spec/frontend/design_management/pages/design/__snapshots__/index_spec.js.snap b/spec/frontend/design_management/pages/design/__snapshots__/index_spec.js.snap
index d86fbf81d20..18b63082e4a 100644
--- a/spec/frontend/design_management/pages/design/__snapshots__/index_spec.js.snap
+++ b/spec/frontend/design_management/pages/design/__snapshots__/index_spec.js.snap
@@ -2,7 +2,7 @@
exports[`Design management design index page renders design index 1`] = `
<div
- class="design-detail js-design-detail fixed-top gl-w-full gl-bottom-0 gl-display-flex gl-justify-content-center gl-flex-direction-column gl-lg-flex-direction-row"
+ class="design-detail js-design-detail fixed-top gl-w-full gl-display-flex gl-justify-content-center gl-flex-direction-column gl-lg-flex-direction-row"
>
<div
class="gl-display-flex gl-overflow-hidden gl-flex-grow-1 gl-flex-direction-column gl-relative"
@@ -115,7 +115,7 @@ exports[`Design management design index page renders design index 1`] = `
exports[`Design management design index page with error GlAlert is rendered in correct position with correct content 1`] = `
<div
- class="design-detail js-design-detail fixed-top gl-w-full gl-bottom-0 gl-display-flex gl-justify-content-center gl-flex-direction-column gl-lg-flex-direction-row"
+ class="design-detail js-design-detail fixed-top gl-w-full gl-display-flex gl-justify-content-center gl-flex-direction-column gl-lg-flex-direction-row"
>
<div
class="gl-display-flex gl-overflow-hidden gl-flex-grow-1 gl-flex-direction-column gl-relative"
diff --git a/spec/frontend/design_management/pages/design/index_spec.js b/spec/frontend/design_management/pages/design/index_spec.js
index 6cec4036d40..fcb03ea3700 100644
--- a/spec/frontend/design_management/pages/design/index_spec.js
+++ b/spec/frontend/design_management/pages/design/index_spec.js
@@ -153,7 +153,7 @@ describe('Design management design index page', () => {
});
describe('when navigating away from component', () => {
- it('removes fullscreen layout class', async () => {
+ it('removes fullscreen layout class', () => {
jest.spyOn(utils, 'getPageLayoutElement').mockReturnValue(mockPageLayoutElement);
createComponent({ loading: true });
diff --git a/spec/frontend/design_management/pages/index_spec.js b/spec/frontend/design_management/pages/index_spec.js
index 1ddf757eb19..1a6403d3b87 100644
--- a/spec/frontend/design_management/pages/index_spec.js
+++ b/spec/frontend/design_management/pages/index_spec.js
@@ -32,7 +32,7 @@ import {
import { createAlert } from '~/alert';
import DesignDropzone from '~/vue_shared/components/upload_dropzone/upload_dropzone.vue';
import {
- designListQueryResponse,
+ getDesignListQueryResponse,
designUploadMutationCreatedResponse,
designUploadMutationUpdatedResponse,
getPermissionsQueryResponse,
@@ -100,6 +100,7 @@ describe('Design management index page', () => {
let wrapper;
let fakeApollo;
let moveDesignHandler;
+ let permissionsQueryHandler;
const findDesignCheckboxes = () => wrapper.findAll('.design-checkbox');
const findSelectAllButton = () => wrapper.findByTestId('select-all-designs-button');
@@ -174,14 +175,16 @@ describe('Design management index page', () => {
}
function createComponentWithApollo({
+ permissionsHandler = jest.fn().mockResolvedValue(getPermissionsQueryResponse()),
moveHandler = jest.fn().mockResolvedValue(moveDesignMutationResponse),
}) {
Vue.use(VueApollo);
+ permissionsQueryHandler = permissionsHandler;
moveDesignHandler = moveHandler;
const requestHandlers = [
- [getDesignListQuery, jest.fn().mockResolvedValue(designListQueryResponse)],
- [permissionsQuery, jest.fn().mockResolvedValue(getPermissionsQueryResponse())],
+ [getDesignListQuery, jest.fn().mockResolvedValue(getDesignListQueryResponse())],
+ [permissionsQuery, permissionsQueryHandler],
[moveDesignMutation, moveDesignHandler],
];
@@ -230,13 +233,6 @@ describe('Design management index page', () => {
expect(findDesignUploadButton().exists()).toBe(true);
});
- it('does not render toolbar when there is no permission', () => {
- createComponent({ designs: mockDesigns, allVersions: [mockVersion], createDesign: false });
-
- expect(findDesignToolbarWrapper().exists()).toBe(false);
- expect(findDesignUploadButton().exists()).toBe(false);
- });
-
it('has correct classes applied to design dropzone', () => {
createComponent({ designs: mockDesigns, allVersions: [mockVersion] });
expect(dropzoneClasses()).toContain('design-list-item');
@@ -723,7 +719,7 @@ describe('Design management index page', () => {
expect(mockMutate).not.toHaveBeenCalled();
});
- it('removes onPaste listener after mouseleave event', async () => {
+ it('removes onPaste listener after mouseleave event', () => {
findDesignsWrapper().trigger('mouseleave');
document.dispatchEvent(event);
@@ -744,6 +740,17 @@ describe('Design management index page', () => {
});
});
+ describe('when there is no permission to create a design', () => {
+ beforeEach(() => {
+ createComponent({ designs: mockDesigns, allVersions: [mockVersion], createDesign: false });
+ });
+
+ it("doesn't render the design toolbar and dropzone", () => {
+ expect(findToolbar().exists()).toBe(false);
+ expect(findDropzoneWrapper().exists()).toBe(false);
+ });
+ });
+
describe('with mocked Apollo client', () => {
it('has a design with id 1 as a first one', async () => {
createComponentWithApollo({});
@@ -819,5 +826,17 @@ describe('Design management index page', () => {
'Something went wrong when reordering designs. Please try again',
);
});
+
+ it("doesn't render the design toolbar and dropzone if the user can't edit", async () => {
+ createComponentWithApollo({
+ permissionsHandler: jest.fn().mockResolvedValue(getPermissionsQueryResponse(false)),
+ });
+
+ await waitForPromises();
+
+ expect(permissionsQueryHandler).toHaveBeenCalled();
+ expect(findToolbar().exists()).toBe(false);
+ expect(findDropzoneWrapper().exists()).toBe(false);
+ });
});
});
diff --git a/spec/frontend/diffs/components/app_spec.js b/spec/frontend/diffs/components/app_spec.js
index 06995706a2b..f24ce8ba4ce 100644
--- a/spec/frontend/diffs/components/app_spec.js
+++ b/spec/frontend/diffs/components/app_spec.js
@@ -11,6 +11,7 @@ import CommitWidget from '~/diffs/components/commit_widget.vue';
import CompareVersions from '~/diffs/components/compare_versions.vue';
import DiffFile from '~/diffs/components/diff_file.vue';
import NoChanges from '~/diffs/components/no_changes.vue';
+import findingsDrawer from '~/diffs/components/shared/findings_drawer.vue';
import TreeList from '~/diffs/components/tree_list.vue';
import CollapsedFilesWarning from '~/diffs/components/collapsed_files_warning.vue';
@@ -174,7 +175,7 @@ describe('diffs/components/app', () => {
});
describe('codequality diff', () => {
- it('does not fetch code quality data on FOSS', async () => {
+ it('does not fetch code quality data on FOSS', () => {
createComponent();
jest.spyOn(wrapper.vm, 'fetchCodequality');
wrapper.vm.fetchData(false);
@@ -714,19 +715,27 @@ describe('diffs/components/app', () => {
});
it.each`
- currentDiffFileId | targetFile
- ${'123'} | ${2}
- ${'312'} | ${1}
+ currentDiffFileId | targetFile | newFileByFile
+ ${'123'} | ${2} | ${false}
+ ${'312'} | ${1} | ${true}
`(
'calls navigateToDiffFileIndex with $index when $link is clicked',
- async ({ currentDiffFileId, targetFile }) => {
- createComponent({ fileByFileUserPreference: true }, ({ state }) => {
- state.diffs.treeEntries = {
- 123: { type: 'blob', fileHash: '123' },
- 312: { type: 'blob', fileHash: '312' },
- };
- state.diffs.currentDiffFileId = currentDiffFileId;
- });
+ async ({ currentDiffFileId, targetFile, newFileByFile }) => {
+ createComponent(
+ { fileByFileUserPreference: true },
+ ({ state }) => {
+ state.diffs.treeEntries = {
+ 123: { type: 'blob', fileHash: '123', filePaths: { old: '1234', new: '123' } },
+ 312: { type: 'blob', fileHash: '312', filePaths: { old: '3124', new: '312' } },
+ };
+ state.diffs.currentDiffFileId = currentDiffFileId;
+ },
+ {
+ glFeatures: {
+ singleFileFileByFile: newFileByFile,
+ },
+ },
+ );
await nextTick();
@@ -736,9 +745,28 @@ describe('diffs/components/app', () => {
await nextTick();
- expect(wrapper.vm.navigateToDiffFileIndex).toHaveBeenCalledWith(targetFile - 1);
+ expect(wrapper.vm.navigateToDiffFileIndex).toHaveBeenCalledWith({
+ index: targetFile - 1,
+ singleFile: newFileByFile,
+ });
},
);
});
});
+
+ describe('findings-drawer', () => {
+ it('does not render findings-drawer when codeQualityInlineDrawer flag is off', () => {
+ createComponent();
+ expect(wrapper.findComponent(findingsDrawer).exists()).toBe(false);
+ });
+
+ it('does render findings-drawer when codeQualityInlineDrawer flag is on', () => {
+ createComponent({}, () => {}, {
+ glFeatures: {
+ codeQualityInlineDrawer: true,
+ },
+ });
+ expect(wrapper.findComponent(findingsDrawer).exists()).toBe(true);
+ });
+ });
});
diff --git a/spec/frontend/diffs/components/commit_item_spec.js b/spec/frontend/diffs/components/commit_item_spec.js
index 4b4b6351d3f..3c092296130 100644
--- a/spec/frontend/diffs/components/commit_item_spec.js
+++ b/spec/frontend/diffs/components/commit_item_spec.js
@@ -1,4 +1,5 @@
import { mount } from '@vue/test-utils';
+import { GlFormCheckbox } from '@gitlab/ui';
import getDiffWithCommit from 'test_fixtures/merge_request_diffs/with_commit.json';
import { TEST_HOST } from 'helpers/test_constants';
import { trimText } from 'helpers/text_helper';
@@ -28,6 +29,7 @@ describe('diffs/components/commit_item', () => {
const getCommitterElement = () => wrapper.find('.committer');
const getCommitActionsElement = () => wrapper.find('.commit-actions');
const getCommitPipelineStatus = () => wrapper.findComponent(CommitPipelineStatus);
+ const getCommitCheckbox = () => wrapper.findComponent(GlFormCheckbox);
const mountComponent = (propsData) => {
wrapper = mount(Component, {
@@ -168,4 +170,24 @@ describe('diffs/components/commit_item', () => {
expect(getCommitPipelineStatus().exists()).toBe(true);
});
});
+
+ describe('when commit is selectable', () => {
+ beforeEach(() => {
+ mountComponent({
+ commit: { ...commit },
+ isSelectable: true,
+ });
+ });
+
+ it('renders checkbox', () => {
+ expect(getCommitCheckbox().exists()).toBe(true);
+ });
+
+ it('emits "handleCheckboxChange" event on change', () => {
+ expect(wrapper.emitted('handleCheckboxChange')).toBeUndefined();
+ getCommitCheckbox().vm.$emit('change');
+
+ expect(wrapper.emitted('handleCheckboxChange')[0]).toEqual([true]);
+ });
+ });
});
diff --git a/spec/frontend/diffs/components/diff_code_quality_item_spec.js b/spec/frontend/diffs/components/diff_code_quality_item_spec.js
new file mode 100644
index 00000000000..be9fb61a77d
--- /dev/null
+++ b/spec/frontend/diffs/components/diff_code_quality_item_spec.js
@@ -0,0 +1,66 @@
+import { GlIcon, GlLink } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import DiffCodeQualityItem from '~/diffs/components/diff_code_quality_item.vue';
+import { SEVERITY_CLASSES, SEVERITY_ICONS } from '~/ci/reports/codequality_report/constants';
+import { multipleFindingsArr } from '../mock_data/diff_code_quality';
+
+let wrapper;
+
+const findIcon = () => wrapper.findComponent(GlIcon);
+const findButton = () => wrapper.findComponent(GlLink);
+const findDescriptionPlainText = () => wrapper.findByTestId('description-plain-text');
+const findDescriptionLinkSection = () => wrapper.findByTestId('description-button-section');
+
+describe('DiffCodeQuality', () => {
+ const createWrapper = ({ glFeatures = {} } = {}) => {
+ return shallowMountExtended(DiffCodeQualityItem, {
+ propsData: {
+ finding: multipleFindingsArr[0],
+ },
+ provide: {
+ glFeatures,
+ },
+ });
+ };
+
+ it('shows icon for given degradation', () => {
+ wrapper = createWrapper();
+ expect(findIcon().exists()).toBe(true);
+
+ expect(findIcon().attributes()).toMatchObject({
+ class: `codequality-severity-icon ${SEVERITY_CLASSES[multipleFindingsArr[0].severity]}`,
+ name: SEVERITY_ICONS[multipleFindingsArr[0].severity],
+ size: '12',
+ });
+ });
+
+ describe('with codeQualityInlineDrawer flag false', () => {
+ it('should render severity + description in plain text', () => {
+ wrapper = createWrapper({
+ glFeatures: {
+ codeQualityInlineDrawer: false,
+ },
+ });
+ expect(findDescriptionPlainText().text()).toContain(multipleFindingsArr[0].severity);
+ expect(findDescriptionPlainText().text()).toContain(multipleFindingsArr[0].description);
+ });
+ });
+
+ describe('with codeQualityInlineDrawer flag true', () => {
+ beforeEach(() => {
+ wrapper = createWrapper({
+ glFeatures: {
+ codeQualityInlineDrawer: true,
+ },
+ });
+ });
+
+ it('should render severity as plain text', () => {
+ expect(findDescriptionLinkSection().text()).toContain(multipleFindingsArr[0].severity);
+ });
+
+ it('should render button with description text', () => {
+ expect(findButton().text()).toContain(multipleFindingsArr[0].description);
+ });
+ });
+});
diff --git a/spec/frontend/diffs/components/diff_code_quality_spec.js b/spec/frontend/diffs/components/diff_code_quality_spec.js
index e5ca90eb7c8..9ecfb62e1c5 100644
--- a/spec/frontend/diffs/components/diff_code_quality_spec.js
+++ b/spec/frontend/diffs/components/diff_code_quality_spec.js
@@ -1,13 +1,12 @@
-import { GlIcon } from '@gitlab/ui';
import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
import DiffCodeQuality from '~/diffs/components/diff_code_quality.vue';
-import { SEVERITY_CLASSES, SEVERITY_ICONS } from '~/ci/reports/codequality_report/constants';
+import DiffCodeQualityItem from '~/diffs/components/diff_code_quality_item.vue';
import { NEW_CODE_QUALITY_FINDINGS } from '~/diffs/i18n';
import { multipleFindingsArr } from '../mock_data/diff_code_quality';
let wrapper;
-const findIcon = () => wrapper.findComponent(GlIcon);
+const diffItems = () => wrapper.findAllComponents(DiffCodeQualityItem);
const findHeading = () => wrapper.findByTestId(`diff-codequality-findings-heading`);
describe('DiffCodeQuality', () => {
@@ -28,37 +27,12 @@ describe('DiffCodeQuality', () => {
expect(wrapper.emitted('hideCodeQualityFindings').length).toBe(1);
});
- it('renders heading and correct amount of list items for codequality array and their description', async () => {
- wrapper = createWrapper(multipleFindingsArr);
- expect(findHeading().text()).toEqual(NEW_CODE_QUALITY_FINDINGS);
-
- const listItems = wrapper.findAll('li');
- expect(wrapper.findAll('li').length).toBe(5);
+ it('renders heading and correct amount of list items for codequality array and their description', () => {
+ wrapper = createWrapper(multipleFindingsArr, shallowMountExtended);
- listItems.wrappers.map((e, i) => {
- return expect(e.text()).toContain(
- `${multipleFindingsArr[i].severity} - ${multipleFindingsArr[i].description}`,
- );
- });
- });
-
- it.each`
- severity
- ${'info'}
- ${'minor'}
- ${'major'}
- ${'critical'}
- ${'blocker'}
- ${'unknown'}
- `('shows icon for $severity degradation', ({ severity }) => {
- wrapper = createWrapper([{ severity }], shallowMountExtended);
-
- expect(findIcon().exists()).toBe(true);
+ expect(findHeading().text()).toEqual(NEW_CODE_QUALITY_FINDINGS);
- expect(findIcon().attributes()).toMatchObject({
- class: `codequality-severity-icon ${SEVERITY_CLASSES[severity]}`,
- name: SEVERITY_ICONS[severity],
- size: '12',
- });
+ expect(diffItems()).toHaveLength(multipleFindingsArr.length);
+ expect(diffItems().at(0).props().finding).toEqual(multipleFindingsArr[0]);
});
});
diff --git a/spec/frontend/diffs/components/diff_file_header_spec.js b/spec/frontend/diffs/components/diff_file_header_spec.js
index 4515a8e8926..900aa8d1469 100644
--- a/spec/frontend/diffs/components/diff_file_header_spec.js
+++ b/spec/frontend/diffs/components/diff_file_header_spec.js
@@ -85,7 +85,7 @@ describe('DiffFileHeader component', () => {
const findExternalLink = () => wrapper.findComponent({ ref: 'externalLink' });
const findReplacedFileButton = () => wrapper.findComponent({ ref: 'replacedFileButton' });
const findViewFileButton = () => wrapper.findComponent({ ref: 'viewButton' });
- const findCollapseIcon = () => wrapper.findComponent({ ref: 'collapseIcon' });
+ const findCollapseButton = () => wrapper.findComponent({ ref: 'collapseButton' });
const findEditButton = () => wrapper.findComponent({ ref: 'editButton' });
const findReviewFileCheckbox = () => wrapper.find("[data-testid='fileReviewCheckbox']");
@@ -111,7 +111,7 @@ describe('DiffFileHeader component', () => {
${'hidden'} | ${false}
`('collapse toggle is $visibility if collapsible is $collapsible', ({ collapsible }) => {
createComponent({ props: { collapsible } });
- expect(findCollapseIcon().exists()).toBe(collapsible);
+ expect(findCollapseButton().exists()).toBe(collapsible);
});
it.each`
@@ -120,7 +120,7 @@ describe('DiffFileHeader component', () => {
${false} | ${'chevron-right'}
`('collapse icon is $icon if expanded is $expanded', ({ icon, expanded }) => {
createComponent({ props: { expanded, collapsible: true } });
- expect(findCollapseIcon().props('name')).toBe(icon);
+ expect(findCollapseButton().props('icon')).toBe(icon);
});
it('when header is clicked emits toggleFile', async () => {
@@ -133,7 +133,7 @@ describe('DiffFileHeader component', () => {
it('when collapseIcon is clicked emits toggleFile', async () => {
createComponent({ props: { collapsible: true } });
- findCollapseIcon().vm.$emit('click', new Event('click'));
+ findCollapseButton().vm.$emit('click', new Event('click'));
await nextTick();
expect(wrapper.emitted().toggleFile).toBeDefined();
});
diff --git a/spec/frontend/diffs/components/diff_file_spec.js b/spec/frontend/diffs/components/diff_file_spec.js
index 93698396450..79cd2508757 100644
--- a/spec/frontend/diffs/components/diff_file_spec.js
+++ b/spec/frontend/diffs/components/diff_file_spec.js
@@ -306,7 +306,7 @@ describe('DiffFile', () => {
markFileToBeRendered(store);
});
- it('should have the file content', async () => {
+ it('should have the file content', () => {
expect(wrapper.findComponent(DiffContentComponent).exists()).toBe(true);
});
@@ -316,7 +316,7 @@ describe('DiffFile', () => {
});
describe('toggle', () => {
- it('should update store state', async () => {
+ it('should update store state', () => {
jest.spyOn(wrapper.vm.$store, 'dispatch').mockImplementation(() => {});
toggleFile(wrapper);
diff --git a/spec/frontend/diffs/components/diff_line_note_form_spec.js b/spec/frontend/diffs/components/diff_line_note_form_spec.js
index bd0e3455872..eb895bd9057 100644
--- a/spec/frontend/diffs/components/diff_line_note_form_spec.js
+++ b/spec/frontend/diffs/components/diff_line_note_form_spec.js
@@ -1,7 +1,6 @@
import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
import Vuex from 'vuex';
-import Autosave from '~/autosave';
import DiffLineNoteForm from '~/diffs/components/diff_line_note_form.vue';
import { createModules } from '~/mr_notes/stores';
import NoteForm from '~/notes/components/note_form.vue';
@@ -11,7 +10,6 @@ import { noteableDataMock } from 'jest/notes/mock_data';
import { getDiffFileMock } from '../mock_data/diff_file';
jest.mock('~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal');
-jest.mock('~/autosave');
describe('DiffLineNoteForm', () => {
let wrapper;
@@ -77,7 +75,6 @@ describe('DiffLineNoteForm', () => {
const findCommentForm = () => wrapper.findComponent(MultilineCommentForm);
beforeEach(() => {
- Autosave.mockClear();
createComponent();
});
@@ -100,19 +97,6 @@ describe('DiffLineNoteForm', () => {
});
});
- it('should init autosave', () => {
- // we're using shallow mount here so there's no element to pass to Autosave
- expect(Autosave).toHaveBeenCalledWith(undefined, [
- 'Note',
- 'Issue',
- 98,
- undefined,
- 'DiffNote',
- undefined,
- '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_1_2',
- ]);
- });
-
describe('when cancelling form', () => {
afterEach(() => {
confirmAction.mockReset();
@@ -146,7 +130,6 @@ describe('DiffLineNoteForm', () => {
await nextTick();
expect(getSelectedLine().hasForm).toBe(false);
- expect(Autosave.mock.instances[0].reset).toHaveBeenCalled();
});
});
diff --git a/spec/frontend/diffs/components/diff_view_spec.js b/spec/frontend/diffs/components/diff_view_spec.js
index 9bff6bd14f1..cfc80e61b30 100644
--- a/spec/frontend/diffs/components/diff_view_spec.js
+++ b/spec/frontend/diffs/components/diff_view_spec.js
@@ -14,7 +14,7 @@ describe('DiffView', () => {
const setSelectedCommentPosition = jest.fn();
const getDiffRow = (wrapper) => wrapper.findComponent(DiffRow).vm;
- const createWrapper = (props, provide = {}) => {
+ const createWrapper = (props) => {
Vue.use(Vuex);
const batchComments = {
@@ -48,7 +48,7 @@ describe('DiffView', () => {
...props,
};
const stubs = { DiffExpansionCell, DiffRow, DiffCommentCell, DraftNote };
- return shallowMount(DiffView, { propsData, store, stubs, provide });
+ return shallowMount(DiffView, { propsData, store, stubs });
};
it('does not render a diff-line component when there is no finding', () => {
@@ -56,24 +56,13 @@ describe('DiffView', () => {
expect(wrapper.findComponent(DiffLine).exists()).toBe(false);
});
- it('does render a diff-line component with the correct props when there is a finding & refactorCodeQualityInlineFindings flag is true', async () => {
- const wrapper = createWrapper(diffCodeQuality, {
- glFeatures: { refactorCodeQualityInlineFindings: true },
- });
+ it('does render a diff-line component with the correct props when there is a finding', async () => {
+ const wrapper = createWrapper(diffCodeQuality);
wrapper.findComponent(DiffRow).vm.$emit('toggleCodeQualityFindings', 2);
await nextTick();
expect(wrapper.findComponent(DiffLine).props('line')).toBe(diffCodeQuality.diffLines[2]);
});
- it('does not render a diff-line component when there is a finding & refactorCodeQualityInlineFindings flag is false', async () => {
- const wrapper = createWrapper(diffCodeQuality, {
- glFeatures: { refactorCodeQualityInlineFindings: false },
- });
- wrapper.findComponent(DiffRow).vm.$emit('toggleCodeQualityFindings', 2);
- await nextTick();
- expect(wrapper.findComponent(DiffLine).exists()).toBe(false);
- });
-
it.each`
type | side | container | sides | total
${'parallel'} | ${'left'} | ${'.old'} | ${{ left: { lineDrafts: [], renderDiscussion: true }, right: { lineDrafts: [], renderDiscussion: true } }} | ${2}
diff --git a/spec/frontend/diffs/components/hidden_files_warning_spec.js b/spec/frontend/diffs/components/hidden_files_warning_spec.js
index d9359fb3c7b..9b748a3ed6f 100644
--- a/spec/frontend/diffs/components/hidden_files_warning_spec.js
+++ b/spec/frontend/diffs/components/hidden_files_warning_spec.js
@@ -37,7 +37,9 @@ describe('HiddenFilesWarning', () => {
it('has a correct visible/total files text', () => {
expect(wrapper.text()).toContain(
- __('To preserve performance only 5 of 10 files are displayed.'),
+ __(
+ 'For a faster browsing experience, only 5 of 10 files are shown. Download one of the files below to see all changes',
+ ),
);
});
});
diff --git a/spec/frontend/diffs/components/shared/__snapshots__/findings_drawer_spec.js.snap b/spec/frontend/diffs/components/shared/__snapshots__/findings_drawer_spec.js.snap
new file mode 100644
index 00000000000..ab330ffbb38
--- /dev/null
+++ b/spec/frontend/diffs/components/shared/__snapshots__/findings_drawer_spec.js.snap
@@ -0,0 +1,126 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`FindingsDrawer matches the snapshot 1`] = `
+<gl-drawer-stub
+ class="findings-drawer"
+ headerheight=""
+ open="true"
+ variant="default"
+ zindex="252"
+>
+ <h2
+ class="gl-font-size-h2 gl-mt-0 gl-mb-0"
+ data-testid="findings-drawer-heading"
+ >
+
+ Unused method argument - \`c\`. If it's necessary, use \`_\` or \`_c\` as an argument name to indicate that it won't be used.
+
+ </h2>
+ <ul
+ class="gl-list-style-none gl-border-b-initial gl-mb-0 gl-pb-0!"
+ >
+ <li
+ class="gl-mb-4"
+ data-testid="findings-drawer-severity"
+ >
+ <span
+ class="gl-font-weight-bold"
+ >
+ Severity:
+ </span>
+
+ <gl-icon-stub
+ class="codequality-severity-icon gl-text-orange-200"
+ data-testid="findings-drawer-severity-icon"
+ name="severity-low"
+ size="12"
+ />
+
+
+ minor
+
+ </li>
+
+ <li
+ class="gl-mb-4"
+ data-testid="findings-drawer-engine"
+ >
+ <span
+ class="gl-font-weight-bold"
+ >
+ Engine:
+ </span>
+
+ testengine name
+
+ </li>
+
+ <li
+ class="gl-mb-4"
+ data-testid="findings-drawer-category"
+ >
+ <span
+ class="gl-font-weight-bold"
+ >
+ Category:
+ </span>
+
+ testcategory 1
+
+ </li>
+
+ <li
+ class="gl-mb-4"
+ data-testid="findings-drawer-other-locations"
+ >
+ <span
+ class="gl-font-weight-bold gl-mb-3 gl-display-block"
+ >
+ Other locations:
+ </span>
+
+ <ul
+ class="gl-pl-6"
+ >
+ <li
+ class="gl-mb-1"
+ >
+ <gl-link-stub
+ data-testid="findings-drawer-other-locations-link"
+ href="http://testlink.com"
+ >
+ testpath
+ </gl-link-stub>
+ </li>
+ <li
+ class="gl-mb-1"
+ >
+ <gl-link-stub
+ data-testid="findings-drawer-other-locations-link"
+ href="http://testlink.com"
+ >
+ testpath 1
+ </gl-link-stub>
+ </li>
+ <li
+ class="gl-mb-1"
+ >
+ <gl-link-stub
+ data-testid="findings-drawer-other-locations-link"
+ href="http://testlink.com"
+ >
+ testpath2
+ </gl-link-stub>
+ </li>
+ </ul>
+ </li>
+ </ul>
+
+ <span
+ class="drawer-body gl-display-block gl-px-3 gl-py-0!"
+ data-testid="findings-drawer-body"
+ >
+ Duplicated Code Duplicated code
+ </span>
+</gl-drawer-stub>
+`;
diff --git a/spec/frontend/diffs/components/shared/findings_drawer_spec.js b/spec/frontend/diffs/components/shared/findings_drawer_spec.js
new file mode 100644
index 00000000000..0af6e0f0e96
--- /dev/null
+++ b/spec/frontend/diffs/components/shared/findings_drawer_spec.js
@@ -0,0 +1,19 @@
+import FindingsDrawer from '~/diffs/components/shared/findings_drawer.vue';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import mockFinding from '../../mock_data/findings_drawer';
+
+let wrapper;
+describe('FindingsDrawer', () => {
+ const createWrapper = () => {
+ return shallowMountExtended(FindingsDrawer, {
+ propsData: {
+ drawer: mockFinding,
+ },
+ });
+ };
+
+ it('matches the snapshot', () => {
+ wrapper = createWrapper();
+ expect(wrapper.element).toMatchSnapshot();
+ });
+});
diff --git a/spec/frontend/diffs/create_diffs_store.js b/spec/frontend/diffs/create_diffs_store.js
index 307ebdaa4ac..92f38858ca5 100644
--- a/spec/frontend/diffs/create_diffs_store.js
+++ b/spec/frontend/diffs/create_diffs_store.js
@@ -3,6 +3,7 @@ import Vuex from 'vuex';
import batchCommentsModule from '~/batch_comments/stores/modules/batch_comments';
import diffsModule from '~/diffs/store/modules';
import notesModule from '~/notes/stores/modules';
+import findingsDrawer from '~/mr_notes/stores/drawer';
Vue.use(Vuex);
@@ -18,6 +19,7 @@ export default function createDiffsStore() {
diffs: diffsModule(),
notes: notesModule(),
batchComments: batchCommentsModule(),
+ findingsDrawer: findingsDrawer(),
},
});
}
diff --git a/spec/frontend/diffs/mock_data/diff_code_quality.js b/spec/frontend/diffs/mock_data/diff_code_quality.js
index 7558592f6a4..29f16da8d89 100644
--- a/spec/frontend/diffs/mock_data/diff_code_quality.js
+++ b/spec/frontend/diffs/mock_data/diff_code_quality.js
@@ -24,6 +24,11 @@ export const multipleFindingsArr = [
description: 'mocked blocker Issue',
line: 3,
},
+ {
+ severity: 'unknown',
+ description: 'mocked unknown Issue',
+ line: 3,
+ },
];
export const fiveFindings = {
diff --git a/spec/frontend/diffs/mock_data/findings_drawer.js b/spec/frontend/diffs/mock_data/findings_drawer.js
new file mode 100644
index 00000000000..d7e7e957c83
--- /dev/null
+++ b/spec/frontend/diffs/mock_data/findings_drawer.js
@@ -0,0 +1,21 @@
+export default {
+ line: 7,
+ description:
+ "Unused method argument - `c`. If it's necessary, use `_` or `_c` as an argument name to indicate that it won't be used.",
+ severity: 'minor',
+ engineName: 'testengine name',
+ categories: ['testcategory 1', 'testcategory 2'],
+ content: {
+ body: 'Duplicated Code Duplicated code',
+ },
+ location: {
+ path: 'workhorse/config_test.go',
+ lines: { begin: 221, end: 284 },
+ },
+ otherLocations: [
+ { path: 'testpath', href: 'http://testlink.com' },
+ { path: 'testpath 1', href: 'http://testlink.com' },
+ { path: 'testpath2', href: 'http://testlink.com' },
+ ],
+ type: 'issue',
+};
diff --git a/spec/frontend/diffs/store/actions_spec.js b/spec/frontend/diffs/store/actions_spec.js
index b00076504e3..f3581c3dd74 100644
--- a/spec/frontend/diffs/store/actions_spec.js
+++ b/spec/frontend/diffs/store/actions_spec.js
@@ -1,5 +1,6 @@
import MockAdapter from 'axios-mock-adapter';
import Cookies from '~/lib/utils/cookies';
+import waitForPromises from 'helpers/wait_for_promises';
import { useLocalStorageSpy } from 'helpers/local_storage_helper';
import { TEST_HOST } from 'helpers/test_constants';
import testAction from 'helpers/vuex_action_helper';
@@ -9,6 +10,7 @@ import {
INLINE_DIFF_VIEW_TYPE,
PARALLEL_DIFF_VIEW_TYPE,
} from '~/diffs/constants';
+import { LOAD_SINGLE_DIFF_FAILED } from '~/diffs/i18n';
import * as diffActions from '~/diffs/store/actions';
import * as types from '~/diffs/store/mutation_types';
import * as utils from '~/diffs/store/utils';
@@ -24,10 +26,16 @@ import {
} from '~/lib/utils/http_status';
import { mergeUrlParams } from '~/lib/utils/url_utility';
import eventHub from '~/notes/event_hub';
+import diffsEventHub from '~/diffs/event_hub';
import { diffMetadata } from '../mock_data/diff_metadata';
jest.mock('~/alert');
+jest.mock('~/lib/utils/secret_detection', () => ({
+ confirmSensitiveAction: jest.fn(() => Promise.resolve(false)),
+ containsSensitiveToken: jest.requireActual('~/lib/utils/secret_detection').containsSensitiveToken,
+}));
+
describe('DiffsStoreActions', () => {
let mock;
@@ -135,6 +143,112 @@ describe('DiffsStoreActions', () => {
});
});
+ describe('fetchFileByFile', () => {
+ beforeEach(() => {
+ window.location.hash = 'e334a2a10f036c00151a04cea7938a5d4213a818';
+ });
+
+ it('should do nothing if there is no tree entry for the file ID', () => {
+ return testAction(diffActions.fetchFileByFile, {}, { flatBlobsList: [] }, [], []);
+ });
+
+ it('should do nothing if the tree entry for the file ID has already been marked as loaded', () => {
+ return testAction(
+ diffActions.fetchFileByFile,
+ {},
+ {
+ flatBlobsList: [
+ { fileHash: 'e334a2a10f036c00151a04cea7938a5d4213a818', diffLoaded: true },
+ ],
+ },
+ [],
+ [],
+ );
+ });
+
+ describe('when a tree entry exists for the file, but it has not been marked as loaded', () => {
+ let state;
+ let commit;
+ let hubSpy;
+ const endpointDiffForPath = '/diffs/set/endpoint/path';
+ const diffForPath = mergeUrlParams(
+ {
+ old_path: 'old/123',
+ new_path: 'new/123',
+ w: '1',
+ view: 'inline',
+ },
+ endpointDiffForPath,
+ );
+ const treeEntry = {
+ fileHash: 'e334a2a10f036c00151a04cea7938a5d4213a818',
+ filePaths: { old: 'old/123', new: 'new/123' },
+ };
+ const fileResult = {
+ diff_files: [{ file_hash: 'e334a2a10f036c00151a04cea7938a5d4213a818' }],
+ };
+ const getters = {
+ flatBlobsList: [treeEntry],
+ getDiffFileByHash(hash) {
+ return state.diffFiles?.find((entry) => entry.file_hash === hash);
+ },
+ };
+
+ beforeEach(() => {
+ commit = jest.fn();
+ state = {
+ endpointDiffForPath,
+ diffFiles: [],
+ };
+ getters.flatBlobsList = [treeEntry];
+ hubSpy = jest.spyOn(diffsEventHub, '$emit');
+ });
+
+ it('does nothing if the file already exists in the loaded diff files', () => {
+ state.diffFiles = fileResult.diff_files;
+
+ return testAction(diffActions.fetchFileByFile, state, getters, [], []);
+ });
+
+ it('does some standard work every time', async () => {
+ mock.onGet(diffForPath).reply(HTTP_STATUS_OK, fileResult);
+
+ await diffActions.fetchFileByFile({ state, getters, commit });
+
+ expect(commit).toHaveBeenCalledWith(types.SET_BATCH_LOADING_STATE, 'loading');
+ expect(commit).toHaveBeenCalledWith(types.SET_RETRIEVING_BATCHES, true);
+
+ // wait for the mocked network request to return and start processing the .then
+ await waitForPromises();
+
+ expect(commit).toHaveBeenCalledWith(types.SET_DIFF_DATA_BATCH, fileResult);
+ expect(commit).toHaveBeenCalledWith(types.SET_BATCH_LOADING_STATE, 'loaded');
+
+ expect(hubSpy).toHaveBeenCalledWith('diffFilesModified');
+ });
+
+ it.each`
+ urlHash | diffFiles | expected
+ ${treeEntry.fileHash} | ${[]} | ${''}
+ ${'abcdef1234567890'} | ${fileResult.diff_files} | ${'e334a2a10f036c00151a04cea7938a5d4213a818'}
+ `(
+ "sets the current file to the first diff file ('$id') if it's not a note hash and there isn't a current ID set",
+ async ({ urlHash, diffFiles, expected }) => {
+ window.location.hash = urlHash;
+ mock.onGet(diffForPath).reply(HTTP_STATUS_OK, fileResult);
+ state.diffFiles = diffFiles;
+
+ await diffActions.fetchFileByFile({ state, getters, commit });
+
+ // wait for the mocked network request to return and start processing the .then
+ await waitForPromises();
+
+ expect(commit).toHaveBeenCalledWith(types.SET_CURRENT_DIFF_FILE, expected);
+ },
+ );
+ });
+ });
+
describe('fetchDiffFilesBatch', () => {
it('should fetch batch diff files', () => {
const endpointBatch = '/fetch/diffs_batch';
@@ -818,31 +932,32 @@ describe('DiffsStoreActions', () => {
});
describe('saveDiffDiscussion', () => {
- it('dispatches actions', () => {
- const commitId = 'something';
- const formData = {
- diffFile: getDiffFileMock(),
- noteableData: {},
- };
- const note = {};
- const state = {
- commit: {
- id: commitId,
- },
- };
- const dispatch = jest.fn((name) => {
- switch (name) {
- case 'saveNote':
- return Promise.resolve({
- discussion: 'test',
- });
- case 'updateDiscussion':
- return Promise.resolve('discussion');
- default:
- return Promise.resolve({});
- }
- });
+ const dispatch = jest.fn((name) => {
+ switch (name) {
+ case 'saveNote':
+ return Promise.resolve({
+ discussion: 'test',
+ });
+ case 'updateDiscussion':
+ return Promise.resolve('discussion');
+ default:
+ return Promise.resolve({});
+ }
+ });
+ const commitId = 'something';
+ const formData = {
+ diffFile: getDiffFileMock(),
+ noteableData: {},
+ };
+ const note = {};
+ const state = {
+ commit: {
+ id: commitId,
+ },
+ };
+
+ it('dispatches actions', () => {
return diffActions.saveDiffDiscussion({ state, dispatch }, { note, formData }).then(() => {
expect(dispatch).toHaveBeenCalledTimes(5);
expect(dispatch).toHaveBeenNthCalledWith(1, 'saveNote', expect.any(Object), {
@@ -856,6 +971,16 @@ describe('DiffsStoreActions', () => {
expect(dispatch).toHaveBeenNthCalledWith(3, 'assignDiscussionsToDiff', ['discussion']);
});
});
+
+ it('should not add note with sensitive token', async () => {
+ const sensitiveMessage = 'token: glpat-1234567890abcdefghij';
+
+ await diffActions.saveDiffDiscussion(
+ { state, dispatch },
+ { note: sensitiveMessage, formData },
+ );
+ expect(dispatch).not.toHaveBeenCalled();
+ });
});
describe('toggleTreeOpen', () => {
@@ -870,6 +995,104 @@ describe('DiffsStoreActions', () => {
});
});
+ describe('goToFile', () => {
+ const getters = {};
+ const file = { path: 'path' };
+ const fileHash = 'test';
+ let state;
+ let dispatch;
+ let commit;
+
+ beforeEach(() => {
+ getters.isTreePathLoaded = () => false;
+ state = {
+ viewDiffsFileByFile: true,
+ treeEntries: {
+ path: {
+ fileHash,
+ },
+ },
+ };
+ commit = jest.fn();
+ dispatch = jest.fn().mockResolvedValue();
+ });
+
+ it('immediately defers to scrollToFile if the app is not in file-by-file mode', () => {
+ state.viewDiffsFileByFile = false;
+
+ diffActions.goToFile({ state, dispatch }, file);
+
+ expect(dispatch).toHaveBeenCalledWith('scrollToFile', file);
+ });
+
+ describe('when the app is in fileByFile mode', () => {
+ describe('when the singleFileFileByFile feature flag is enabled', () => {
+ it('commits SET_CURRENT_DIFF_FILE', () => {
+ diffActions.goToFile(
+ { state, commit, dispatch, getters },
+ { path: file.path, singleFile: true },
+ );
+
+ expect(commit).toHaveBeenCalledWith(types.SET_CURRENT_DIFF_FILE, fileHash);
+ });
+
+ it('does nothing more if the path has already been loaded', () => {
+ getters.isTreePathLoaded = () => true;
+
+ diffActions.goToFile(
+ { state, dispatch, getters, commit },
+ { path: file.path, singleFile: true },
+ );
+
+ expect(commit).toHaveBeenCalledWith(types.SET_CURRENT_DIFF_FILE, fileHash);
+ expect(dispatch).toHaveBeenCalledTimes(0);
+ });
+
+ describe('when the tree entry has not been loaded', () => {
+ it('updates location hash', () => {
+ diffActions.goToFile(
+ { state, commit, getters, dispatch },
+ { path: file.path, singleFile: true },
+ );
+
+ expect(document.location.hash).toBe('#test');
+ });
+
+ it('loads the file and then scrolls to it', async () => {
+ diffActions.goToFile(
+ { state, commit, getters, dispatch },
+ { path: file.path, singleFile: true },
+ );
+
+ // Wait for the fetchFileByFile dispatch to return, to trigger scrollToFile
+ await waitForPromises();
+
+ expect(dispatch).toHaveBeenCalledWith('fetchFileByFile');
+ expect(dispatch).toHaveBeenCalledWith('scrollToFile', file);
+ expect(dispatch).toHaveBeenCalledTimes(2);
+ });
+
+ it('shows an alert when there was an error fetching the file', async () => {
+ dispatch = jest.fn().mockRejectedValue();
+
+ diffActions.goToFile(
+ { state, commit, getters, dispatch },
+ { path: file.path, singleFile: true },
+ );
+
+ // Wait for the fetchFileByFile dispatch to return, to trigger the catch
+ await waitForPromises();
+
+ expect(createAlert).toHaveBeenCalledTimes(1);
+ expect(createAlert).toHaveBeenCalledWith({
+ message: expect.stringMatching(LOAD_SINGLE_DIFF_FAILED),
+ });
+ });
+ });
+ });
+ });
+ });
+
describe('scrollToFile', () => {
let commit;
const getters = { isVirtualScrollingEnabled: false };
@@ -1392,6 +1615,54 @@ describe('DiffsStoreActions', () => {
);
});
+ describe('rereadNoteHash', () => {
+ beforeEach(() => {
+ window.location.hash = 'note_123';
+ });
+
+ it('dispatches setCurrentDiffFileIdFromNote if the hash is a note URL', () => {
+ window.location.hash = 'note_123';
+
+ return testAction(
+ diffActions.rereadNoteHash,
+ {},
+ {},
+ [],
+ [{ type: 'setCurrentDiffFileIdFromNote', payload: '123' }],
+ );
+ });
+
+ it('dispatches fetchFileByFile if the app is in fileByFile mode', () => {
+ window.location.hash = 'note_123';
+
+ return testAction(
+ diffActions.rereadNoteHash,
+ {},
+ { viewDiffsFileByFile: true },
+ [],
+ [{ type: 'setCurrentDiffFileIdFromNote', payload: '123' }, { type: 'fetchFileByFile' }],
+ );
+ });
+
+ it('does not try to fetch the diff file if the app is not in fileByFile mode', () => {
+ window.location.hash = 'note_123';
+
+ return testAction(
+ diffActions.rereadNoteHash,
+ {},
+ { viewDiffsFileByFile: false },
+ [],
+ [{ type: 'setCurrentDiffFileIdFromNote', payload: '123' }],
+ );
+ });
+
+ it('does nothing if the hash is not a note URL', () => {
+ window.location.hash = 'abcdef1234567890';
+
+ return testAction(diffActions.rereadNoteHash, {}, {}, [], []);
+ });
+ });
+
describe('setCurrentDiffFileIdFromNote', () => {
it('commits SET_CURRENT_DIFF_FILE', () => {
const commit = jest.fn();
@@ -1436,12 +1707,22 @@ describe('DiffsStoreActions', () => {
it('commits SET_CURRENT_DIFF_FILE', () => {
return testAction(
diffActions.navigateToDiffFileIndex,
- 0,
+ { index: 0, singleFile: false },
{ flatBlobsList: [{ fileHash: '123' }] },
[{ type: types.SET_CURRENT_DIFF_FILE, payload: '123' }],
[],
);
});
+
+ it('dispatches the fetchFileByFile action when the state value viewDiffsFileByFile is true and the single-file file-by-file feature flag is enabled', () => {
+ return testAction(
+ diffActions.navigateToDiffFileIndex,
+ { index: 0, singleFile: true },
+ { viewDiffsFileByFile: true, flatBlobsList: [{ fileHash: '123' }] },
+ [{ type: types.SET_CURRENT_DIFF_FILE, payload: '123' }],
+ [{ type: 'fetchFileByFile' }],
+ );
+ });
});
describe('setFileByFile', () => {
diff --git a/spec/frontend/diffs/utils/merge_request_spec.js b/spec/frontend/diffs/utils/merge_request_spec.js
index c070e8c004d..21599a3be45 100644
--- a/spec/frontend/diffs/utils/merge_request_spec.js
+++ b/spec/frontend/diffs/utils/merge_request_spec.js
@@ -1,4 +1,8 @@
-import { getDerivedMergeRequestInformation } from '~/diffs/utils/merge_request';
+import {
+ updateChangesTabCount,
+ getDerivedMergeRequestInformation,
+} from '~/diffs/utils/merge_request';
+import { ZERO_CHANGES_ALT_DISPLAY } from '~/diffs/constants';
import { diffMetadata } from '../mock_data/diff_metadata';
describe('Merge Request utilities', () => {
@@ -24,6 +28,56 @@ describe('Merge Request utilities', () => {
...noVersion,
};
+ describe('updateChangesTabCount', () => {
+ let dummyTab;
+ let badge;
+
+ beforeEach(() => {
+ dummyTab = document.createElement('div');
+ dummyTab.classList.add('js-diffs-tab');
+ dummyTab.insertAdjacentHTML('afterbegin', '<span class="gl-badge">ERROR</span>');
+ badge = dummyTab.querySelector('.gl-badge');
+ });
+
+ afterEach(() => {
+ dummyTab.remove();
+ dummyTab = null;
+ badge = null;
+ });
+
+ it('uses the alt hyphen display when the new changes are falsey', () => {
+ updateChangesTabCount({ count: 0, badge });
+
+ expect(dummyTab.textContent).toBe(ZERO_CHANGES_ALT_DISPLAY);
+
+ updateChangesTabCount({ badge });
+
+ expect(dummyTab.textContent).toBe(ZERO_CHANGES_ALT_DISPLAY);
+
+ updateChangesTabCount({ count: false, badge });
+
+ expect(dummyTab.textContent).toBe(ZERO_CHANGES_ALT_DISPLAY);
+ });
+
+ it('uses the actual value for display when the value is truthy', () => {
+ updateChangesTabCount({ count: 42, badge });
+
+ expect(dummyTab.textContent).toBe('42');
+
+ updateChangesTabCount({ count: '999+', badge });
+
+ expect(dummyTab.textContent).toBe('999+');
+ });
+
+ it('selects the proper element to modify by default', () => {
+ document.body.insertAdjacentElement('afterbegin', dummyTab);
+
+ updateChangesTabCount({ count: 42 });
+
+ expect(dummyTab.textContent).toBe('42');
+ });
+ });
+
describe('getDerivedMergeRequestInformation', () => {
let endpoint = `${diffMetadata.latest_version_path}.json?searchParam=irrelevant`;
diff --git a/spec/frontend/drawio/drawio_editor_spec.js b/spec/frontend/drawio/drawio_editor_spec.js
index 5ef26c04204..d7d75922e1e 100644
--- a/spec/frontend/drawio/drawio_editor_spec.js
+++ b/spec/frontend/drawio/drawio_editor_spec.js
@@ -108,7 +108,7 @@ describe('drawio/drawio_editor', () => {
await waitForDrawioIFrameMessage();
});
- it('sends configure action to the draw.io iframe', async () => {
+ it('sends configure action to the draw.io iframe', () => {
expectDrawioIframeMessage({
expectation: {
action: 'configure',
@@ -121,7 +121,7 @@ describe('drawio/drawio_editor', () => {
});
});
- it('does not remove the iframe after the load error timeouts run', async () => {
+ it('does not remove the iframe after the load error timeouts run', () => {
jest.runAllTimers();
expect(findDrawioIframe()).not.toBe(null);
@@ -227,7 +227,7 @@ describe('drawio/drawio_editor', () => {
postMessageToParentWindow({ event: 'init' });
});
- it('displays an error alert indicating that the image is not a diagram', async () => {
+ it('displays an error alert indicating that the image is not a diagram', () => {
expect(createAlert).toHaveBeenCalledWith({
message: errorMessage,
error: expect.any(Error),
@@ -248,7 +248,7 @@ describe('drawio/drawio_editor', () => {
postMessageToParentWindow({ event: 'init' });
});
- it('displays an error alert indicating the failure', async () => {
+ it('displays an error alert indicating the failure', () => {
expect(createAlert).toHaveBeenCalledWith({
message: 'Cannot load the diagram into the diagrams.net editor',
error: expect.any(Error),
diff --git a/spec/frontend/dropzone_input_spec.js b/spec/frontend/dropzone_input_spec.js
index fdd157dd09f..179ba917e7f 100644
--- a/spec/frontend/dropzone_input_spec.js
+++ b/spec/frontend/dropzone_input_spec.js
@@ -48,9 +48,9 @@ describe('dropzone_input', () => {
};
beforeEach(() => {
- loadHTMLFixture('issues/new-issue.html');
+ loadHTMLFixture('milestones/new-milestone.html');
- form = $('#new_issue');
+ form = $('#new_milestone');
form.data('uploads-path', TEST_UPLOAD_PATH);
dropzoneInput(form);
});
diff --git a/spec/frontend/editor/components/source_editor_toolbar_button_spec.js b/spec/frontend/editor/components/source_editor_toolbar_button_spec.js
index 79692ab4557..b5944a52af7 100644
--- a/spec/frontend/editor/components/source_editor_toolbar_button_spec.js
+++ b/spec/frontend/editor/components/source_editor_toolbar_button_spec.js
@@ -33,17 +33,17 @@ describe('Source Editor Toolbar button', () => {
it('does not render the button if the props have not been passed', () => {
createComponent({});
- expect(findButton().vm).toBeUndefined();
+ expect(findButton().exists()).toBe(false);
});
- it('renders a default button without props', async () => {
+ it('renders a default button without props', () => {
createComponent();
const btn = findButton();
expect(btn.exists()).toBe(true);
expect(btn.props()).toMatchObject(defaultProps);
});
- it('renders a button based on the props passed', async () => {
+ it('renders a button based on the props passed', () => {
createComponent({
button: customProps,
});
@@ -107,34 +107,31 @@ describe('Source Editor Toolbar button', () => {
});
describe('click handler', () => {
- let clickEvent;
-
- beforeEach(() => {
- clickEvent = new Event('click');
- });
-
it('fires the click handler on the button when available', async () => {
- const spy = jest.fn();
+ const clickSpy = jest.fn();
+ const clickEvent = new Event('click');
createComponent({
button: {
- onClick: spy,
+ onClick: clickSpy,
},
});
- expect(spy).not.toHaveBeenCalled();
+ expect(wrapper.emitted('click')).toEqual(undefined);
findButton().vm.$emit('click', clickEvent);
await nextTick();
- expect(spy).toHaveBeenCalledWith(clickEvent);
+
+ expect(wrapper.emitted('click')).toEqual([[clickEvent]]);
+ expect(clickSpy).toHaveBeenCalledWith(clickEvent);
});
+
it('emits the "click" event, passing the event itself', async () => {
createComponent();
- jest.spyOn(wrapper.vm, '$emit');
- expect(wrapper.vm.$emit).not.toHaveBeenCalled();
+ expect(wrapper.emitted('click')).toEqual(undefined);
- findButton().vm.$emit('click', clickEvent);
+ findButton().vm.$emit('click');
await nextTick();
- expect(wrapper.vm.$emit).toHaveBeenCalledWith('click', clickEvent);
+ expect(wrapper.emitted('click')).toHaveLength(1);
});
});
});
diff --git a/spec/frontend/editor/components/source_editor_toolbar_spec.js b/spec/frontend/editor/components/source_editor_toolbar_spec.js
index f737340a317..95dc29c7916 100644
--- a/spec/frontend/editor/components/source_editor_toolbar_spec.js
+++ b/spec/frontend/editor/components/source_editor_toolbar_spec.js
@@ -104,19 +104,16 @@ describe('Source Editor Toolbar', () => {
group: EDITOR_TOOLBAR_BUTTON_GROUPS.settings,
});
createComponentWithApollo([item1, item2, item3]);
- jest.spyOn(wrapper.vm, '$emit');
- expect(wrapper.vm.$emit).not.toHaveBeenCalled();
+ expect(wrapper.emitted('click')).toEqual(undefined);
findButtons().at(0).vm.$emit('click');
- expect(wrapper.vm.$emit).toHaveBeenCalledWith('click', item1);
+ expect(wrapper.emitted('click')).toEqual([[item1]]);
findButtons().at(1).vm.$emit('click');
- expect(wrapper.vm.$emit).toHaveBeenCalledWith('click', item2);
+ expect(wrapper.emitted('click')).toEqual([[item1], [item2]]);
findButtons().at(2).vm.$emit('click');
- expect(wrapper.vm.$emit).toHaveBeenCalledWith('click', item3);
-
- expect(wrapper.vm.$emit.mock.calls).toHaveLength(3);
+ expect(wrapper.emitted('click')).toEqual([[item1], [item2], [item3]]);
});
});
});
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 895eb87833d..fb5fce92482 100644
--- a/spec/frontend/editor/source_editor_markdown_livepreview_ext_spec.js
+++ b/spec/frontend/editor/source_editor_markdown_livepreview_ext_spec.js
@@ -1,6 +1,5 @@
import MockAdapter from 'axios-mock-adapter';
import { Emitter } from 'monaco-editor';
-import { useFakeRequestAnimationFrame } from 'helpers/fake_request_animation_frame';
import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import waitForPromises from 'helpers/wait_for_promises';
import {
@@ -28,6 +27,7 @@ describe('Markdown Live Preview Extension for Source Editor', () => {
let panelSpy;
let mockAxios;
let extension;
+ let resizeCallback;
const previewMarkdownPath = '/gitlab/fooGroup/barProj/preview_markdown';
const firstLine = 'This is a';
const secondLine = 'multiline';
@@ -35,6 +35,8 @@ describe('Markdown Live Preview Extension for Source Editor', () => {
const text = `${firstLine}\n${secondLine}\n${thirdLine}`;
const markdownPath = 'foo.md';
const responseData = '<div>FooBar</div>';
+ const observeSpy = jest.fn();
+ const disconnectSpy = jest.fn();
const togglePreview = async () => {
instance.togglePreview();
@@ -43,8 +45,22 @@ describe('Markdown Live Preview Extension for Source Editor', () => {
beforeEach(() => {
mockAxios = new MockAdapter(axios);
- setHTMLFixture('<div id="editor" data-editor-loading></div>');
+ setHTMLFixture(
+ '<div style="width: 500px; height: 500px"><div id="editor" data-editor-loading></div></div>',
+ );
editorEl = document.getElementById('editor');
+ global.ResizeObserver = class {
+ constructor(callback) {
+ resizeCallback = callback;
+ this.observe = (node) => {
+ return observeSpy(node);
+ };
+ this.disconnect = () => {
+ return disconnectSpy();
+ };
+ }
+ };
+
editor = new SourceEditor();
instance = editor.createInstance({
el: editorEl,
@@ -77,9 +93,6 @@ describe('Markdown Live Preview Extension for Source Editor', () => {
actions: expect.any(Object),
shown: false,
modelChangeListener: undefined,
- layoutChangeListener: {
- dispose: expect.anything(),
- },
path: previewMarkdownPath,
actionShowPreviewCondition: expect.any(Object),
eventEmitter: expect.any(Object),
@@ -94,36 +107,64 @@ describe('Markdown Live Preview Extension for Source Editor', () => {
expect(panelSpy).toHaveBeenCalled();
});
- describe('onDidLayoutChange', () => {
- const emitter = new Emitter();
- let layoutSpy;
+ describe('ResizeObserver handler', () => {
+ it('sets a ResizeObserver to observe the container DOM node', () => {
+ observeSpy.mockClear();
+ instance.togglePreview();
+ expect(observeSpy).toHaveBeenCalledWith(instance.getContainerDomNode());
+ });
- useFakeRequestAnimationFrame();
+ describe('disconnects the ResizeObserver when…', () => {
+ beforeEach(() => {
+ instance.togglePreview();
+ instance.markdownPreview.modelChangeListener = {
+ dispose: jest.fn(),
+ };
+ });
- beforeEach(() => {
- instance.unuse(extension);
- instance.onDidLayoutChange = emitter.event;
- extension = instance.use({
- definition: EditorMarkdownPreviewExtension,
- setupOptions: { previewMarkdownPath },
+ it('the preview gets closed', () => {
+ expect(disconnectSpy).not.toHaveBeenCalled();
+ instance.togglePreview();
+ expect(disconnectSpy).toHaveBeenCalled();
});
- layoutSpy = jest.spyOn(instance, 'layout');
- });
- it('does not trigger the layout when the preview is not active [default]', async () => {
- expect(instance.markdownPreview.shown).toBe(false);
- expect(layoutSpy).not.toHaveBeenCalled();
- await emitter.fire();
- expect(layoutSpy).not.toHaveBeenCalled();
+ it('the extension is unused', () => {
+ expect(disconnectSpy).not.toHaveBeenCalled();
+ instance.unuse(extension);
+ expect(disconnectSpy).toHaveBeenCalled();
+ });
});
- it('triggers the layout if the preview panel is opened', async () => {
- expect(layoutSpy).not.toHaveBeenCalled();
- instance.togglePreview();
- layoutSpy.mockReset();
+ describe('layout behavior', () => {
+ let layoutSpy;
+ let instanceDimensions;
+ let newInstanceWidth;
- await emitter.fire();
- expect(layoutSpy).toHaveBeenCalledTimes(1);
+ beforeEach(() => {
+ instanceDimensions = instance.getLayoutInfo();
+ });
+
+ it('does not trigger the layout if the preview panel is closed', () => {
+ layoutSpy = jest.spyOn(instance, 'layout');
+ newInstanceWidth = instanceDimensions.width + 100;
+
+ // Manually trigger the resize event
+ resizeCallback([{ contentRect: { width: newInstanceWidth } }]);
+ expect(layoutSpy).not.toHaveBeenCalled();
+ });
+
+ it('triggers the layout if the preview panel is opened, and width of the editor has changed', () => {
+ instance.togglePreview();
+ layoutSpy = jest.spyOn(instance, 'layout');
+ newInstanceWidth = instanceDimensions.width + 100;
+
+ // Manually trigger the resize event
+ resizeCallback([{ contentRect: { width: newInstanceWidth } }]);
+ expect(layoutSpy).toHaveBeenCalledWith({
+ width: newInstanceWidth * EXTENSION_MARKDOWN_PREVIEW_PANEL_WIDTH,
+ height: instanceDimensions.height,
+ });
+ });
});
});
@@ -226,11 +267,10 @@ describe('Markdown Live Preview Extension for Source Editor', () => {
expect(newWidth === width / EXTENSION_MARKDOWN_PREVIEW_PANEL_WIDTH).toBe(true);
});
- it('disposes the layoutChange listener and does not re-layout on layout changes', () => {
- expect(instance.markdownPreview.layoutChangeListener).toBeDefined();
+ it('disconnects the ResizeObserver', () => {
instance.unuse(extension);
- expect(instance.markdownPreview?.layoutChangeListener).toBeUndefined();
+ expect(disconnectSpy).toHaveBeenCalled();
});
it('does not trigger the re-layout after instance is unused', async () => {
diff --git a/spec/frontend/editor/utils_spec.js b/spec/frontend/editor/utils_spec.js
index 13b8a9804b0..c9d6cbcaaa6 100644
--- a/spec/frontend/editor/utils_spec.js
+++ b/spec/frontend/editor/utils_spec.js
@@ -1,6 +1,8 @@
import { editor as monacoEditor } from 'monaco-editor';
import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import * as utils from '~/editor/utils';
+import languages from '~/ide/lib/languages';
+import { registerLanguages } from '~/ide/utils';
import { DEFAULT_THEME } from '~/ide/lib/themes';
describe('Source Editor utils', () => {
@@ -53,13 +55,19 @@ describe('Source Editor utils', () => {
});
describe('getBlobLanguage', () => {
+ beforeEach(() => {
+ registerLanguages(...languages);
+ });
+
it.each`
- path | expectedLanguage
- ${'foo.js'} | ${'javascript'}
- ${'foo.js.rb'} | ${'ruby'}
- ${'foo.bar'} | ${'plaintext'}
- ${undefined} | ${'plaintext'}
- ${'foo/bar/foo.js'} | ${'javascript'}
+ path | expectedLanguage
+ ${'foo.js'} | ${'javascript'}
+ ${'foo.js.rb'} | ${'ruby'}
+ ${'foo.bar'} | ${'plaintext'}
+ ${undefined} | ${'plaintext'}
+ ${'foo/bar/foo.js'} | ${'javascript'}
+ ${'CODEOWNERS'} | ${'codeowners'}
+ ${'.gitlab/CODEOWNERS'} | ${'codeowners'}
`(`returns '$expectedLanguage' for '$path' path`, ({ path, expectedLanguage }) => {
const language = utils.getBlobLanguage(path);
diff --git a/spec/frontend/emoji/awards_app/store/actions_spec.js b/spec/frontend/emoji/awards_app/store/actions_spec.js
index 3e9b49707ed..65f2e813f19 100644
--- a/spec/frontend/emoji/awards_app/store/actions_spec.js
+++ b/spec/frontend/emoji/awards_app/store/actions_spec.js
@@ -119,7 +119,7 @@ describe('Awards app actions', () => {
mock.onPost(`${relativeRootUrl || ''}/awards`).reply(HTTP_STATUS_OK, { id: 1 });
});
- it('adds an optimistic award, removes it, and then commits ADD_NEW_AWARD', async () => {
+ it('adds an optimistic award, removes it, and then commits ADD_NEW_AWARD', () => {
testAction(actions.toggleAward, null, { path: '/awards', awards: [] }, [
makeOptimisticAddMutation(),
makeOptimisticRemoveMutation(),
@@ -156,7 +156,7 @@ describe('Awards app actions', () => {
mock.onDelete(`${relativeRootUrl || ''}/awards/1`).reply(HTTP_STATUS_OK);
});
- it('commits REMOVE_AWARD', async () => {
+ it('commits REMOVE_AWARD', () => {
testAction(
actions.toggleAward,
'thumbsup',
diff --git a/spec/frontend/environment.js b/spec/frontend/environment.js
index 7b160c48501..4e341b2bb2f 100644
--- a/spec/frontend/environment.js
+++ b/spec/frontend/environment.js
@@ -21,8 +21,17 @@ class CustomEnvironment extends TestEnvironment {
// https://gitlab.com/gitlab-org/gitlab/-/merge_requests/39496#note_503084332
setGlobalDateToFakeDate();
+ const { error: originalErrorFn } = context.console;
Object.assign(context.console, {
error(...args) {
+ if (
+ args?.[0]?.includes('[Vue warn]: Missing required prop') ||
+ args?.[0]?.includes('[Vue warn]: Invalid prop')
+ ) {
+ originalErrorFn.apply(context.console, args);
+ return;
+ }
+
throw new ErrorWithStack(
`Unexpected call of console.error() with:\n\n${args.join(', ')}`,
this.error,
@@ -30,7 +39,7 @@ class CustomEnvironment extends TestEnvironment {
},
warn(...args) {
- if (args[0].includes('The updateQuery callback for fetchMore is deprecated')) {
+ if (args?.[0]?.includes('The updateQuery callback for fetchMore is deprecated')) {
return;
}
throw new ErrorWithStack(
diff --git a/spec/frontend/environments/deploy_board_component_spec.js b/spec/frontend/environments/deploy_board_component_spec.js
index 73a366457fb..f50efada91a 100644
--- a/spec/frontend/environments/deploy_board_component_spec.js
+++ b/spec/frontend/environments/deploy_board_component_spec.js
@@ -61,7 +61,7 @@ describe('Deploy Board', () => {
const icon = iconSpan.findComponent(GlIcon);
expect(tooltip.props('target')()).toBe(iconSpan.element);
- expect(icon.props('name')).toBe('question');
+ expect(icon.props('name')).toBe('question-o');
});
it('renders the canary weight selector', () => {
@@ -116,7 +116,7 @@ describe('Deploy Board', () => {
const icon = iconSpan.findComponent(GlIcon);
expect(tooltip.props('target')()).toBe(iconSpan.element);
- expect(icon.props('name')).toBe('question');
+ expect(icon.props('name')).toBe('question-o');
});
it('renders the canary weight selector', () => {
diff --git a/spec/frontend/environments/environment_actions_spec.js b/spec/frontend/environments/environment_actions_spec.js
index 3c9b4144e45..dcfefbb2072 100644
--- a/spec/frontend/environments/environment_actions_spec.js
+++ b/spec/frontend/environments/environment_actions_spec.js
@@ -1,14 +1,8 @@
-import { GlDropdown, GlDropdownItem, GlLoadingIcon, GlIcon } from '@gitlab/ui';
-import { shallowMount, mount } from '@vue/test-utils';
-import Vue, { nextTick } from 'vue';
-import VueApollo from 'vue-apollo';
+import { GlDisclosureDropdown, GlDisclosureDropdownItem, GlIcon } from '@gitlab/ui';
+import { mount } from '@vue/test-utils';
import { TEST_HOST } from 'helpers/test_constants';
-import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import EnvironmentActions from '~/environments/components/environment_actions.vue';
-import eventHub from '~/environments/event_hub';
-import actionMutation from '~/environments/graphql/mutations/action.mutation.graphql';
import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
-import createMockApollo from 'helpers/mock_apollo_helper';
jest.mock('~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal');
@@ -29,15 +23,9 @@ const expiredJobAction = {
describe('EnvironmentActions Component', () => {
let wrapper;
- const findEnvironmentActionsButton = () =>
- wrapper.find('[data-testid="environment-actions-button"]');
-
- function createComponent(props, { mountFn = shallowMount, options = {} } = {}) {
- wrapper = mountFn(EnvironmentActions, {
+ function createComponent(props, { options = {} } = {}) {
+ wrapper = mount(EnvironmentActions, {
propsData: { actions: [], ...props },
- directives: {
- GlTooltip: createMockDirective('gl-tooltip'),
- },
...options,
});
}
@@ -46,9 +34,10 @@ describe('EnvironmentActions Component', () => {
return createComponent({ actions: [scheduledJobAction, expiredJobAction] }, opts);
}
+ const findDropdownItems = () => wrapper.findAllComponents(GlDisclosureDropdownItem);
const findDropdownItem = (action) => {
- const buttons = wrapper.findAllComponents(GlDropdownItem);
- return buttons.filter((button) => button.text().startsWith(action.name)).at(0);
+ const items = findDropdownItems();
+ return items.filter((item) => item.text().startsWith(action.name)).at(0);
};
afterEach(() => {
@@ -56,19 +45,15 @@ describe('EnvironmentActions Component', () => {
});
it('should render a dropdown button with 2 icons', () => {
- createComponent({}, { mountFn: mount });
- expect(wrapper.findComponent(GlDropdown).findAllComponents(GlIcon).length).toBe(2);
- });
-
- it('should render a dropdown button with aria-label description', () => {
createComponent();
- expect(wrapper.findComponent(GlDropdown).attributes('aria-label')).toBe('Deploy to...');
+ expect(wrapper.findComponent(GlDisclosureDropdown).findAllComponents(GlIcon).length).toBe(2);
});
- it('should render a tooltip', () => {
+ it('should render a dropdown button with aria-label description', () => {
createComponent();
- const tooltip = getBinding(findEnvironmentActionsButton().element, 'gl-tooltip');
- expect(tooltip).toBeDefined();
+ expect(wrapper.findComponent(GlDisclosureDropdown).attributes('aria-label')).toBe(
+ 'Deploy to...',
+ );
});
describe('manual actions', () => {
@@ -93,96 +78,31 @@ describe('EnvironmentActions Component', () => {
});
it('should render a dropdown with the provided list of actions', () => {
- expect(wrapper.findAllComponents(GlDropdownItem)).toHaveLength(actions.length);
+ expect(findDropdownItems()).toHaveLength(actions.length);
});
it("should render a disabled action when it's not playable", () => {
- const dropdownItems = wrapper.findAllComponents(GlDropdownItem);
+ const dropdownItems = findDropdownItems();
const lastDropdownItem = dropdownItems.at(dropdownItems.length - 1);
- expect(lastDropdownItem.attributes('disabled')).toBe('true');
+ expect(lastDropdownItem.find('button').attributes('disabled')).toBe('disabled');
});
});
describe('scheduled jobs', () => {
- let emitSpy;
-
- const clickAndConfirm = async ({ confirm = true } = {}) => {
- confirmAction.mockResolvedValueOnce(confirm);
-
- findDropdownItem(scheduledJobAction).vm.$emit('click');
- await nextTick();
- };
-
beforeEach(() => {
- emitSpy = jest.fn();
- eventHub.$on('postAction', emitSpy);
jest.spyOn(Date, 'now').mockImplementation(() => new Date('2063-04-04T00:42:00Z').getTime());
});
- describe('when postAction event is confirmed', () => {
- beforeEach(async () => {
- createComponentWithScheduledJobs({ mountFn: mount });
- clickAndConfirm();
- });
-
- it('emits postAction event', () => {
- expect(confirmAction).toHaveBeenCalled();
- expect(emitSpy).toHaveBeenCalledWith({ endpoint: scheduledJobAction.playPath });
- });
-
- it('should render a dropdown button with a loading icon', () => {
- expect(wrapper.findComponent(GlLoadingIcon).isVisible()).toBe(true);
- });
- });
-
- describe('when postAction event is denied', () => {
- beforeEach(async () => {
- createComponentWithScheduledJobs({ mountFn: mount });
- clickAndConfirm({ confirm: false });
- });
-
- it('does not emit postAction event if confirmation is cancelled', () => {
- expect(confirmAction).toHaveBeenCalled();
- expect(emitSpy).not.toHaveBeenCalled();
- });
- });
-
it('displays the remaining time in the dropdown', () => {
+ confirmAction.mockResolvedValueOnce(true);
createComponentWithScheduledJobs();
expect(findDropdownItem(scheduledJobAction).text()).toContain('24:00:00');
});
it('displays 00:00:00 for expired jobs in the dropdown', () => {
+ confirmAction.mockResolvedValueOnce(true);
createComponentWithScheduledJobs();
expect(findDropdownItem(expiredJobAction).text()).toContain('00:00:00');
});
});
-
- describe('graphql', () => {
- Vue.use(VueApollo);
-
- const action = {
- name: 'bar',
- play_path: 'https://gitlab.com/play',
- };
-
- let mockApollo;
-
- beforeEach(() => {
- mockApollo = createMockApollo();
- createComponent(
- { actions: [action], graphql: true },
- { options: { apolloProvider: mockApollo } },
- );
- });
-
- it('should trigger a graphql mutation on click', () => {
- jest.spyOn(mockApollo.defaultClient, 'mutate');
- findDropdownItem(action).vm.$emit('click');
- expect(mockApollo.defaultClient.mutate).toHaveBeenCalledWith({
- mutation: actionMutation,
- variables: { action },
- });
- });
- });
});
diff --git a/spec/frontend/environments/environment_details/components/deployment_actions_spec.js b/spec/frontend/environments/environment_details/components/deployment_actions_spec.js
index 725c8c6479e..a0eb4c494e6 100644
--- a/spec/frontend/environments/environment_details/components/deployment_actions_spec.js
+++ b/spec/frontend/environments/environment_details/components/deployment_actions_spec.js
@@ -1,8 +1,15 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import { GlButton } from '@gitlab/ui';
import DeploymentActions from '~/environments/environment_details/components/deployment_actions.vue';
import { mountExtended } from 'helpers/vue_test_utils_helper';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import { translations } from '~/environments/environment_details/constants';
import ActionsComponent from '~/environments/components/environment_actions.vue';
describe('~/environments/environment_details/components/deployment_actions.vue', () => {
+ Vue.use(VueApollo);
let wrapper;
const actionsData = [
@@ -14,34 +21,116 @@ describe('~/environments/environment_details/components/deployment_actions.vue',
},
];
- const createWrapper = ({ actions }) => {
+ const rollbackData = {
+ id: '123',
+ name: 'enironment-name',
+ lastDeployment: {
+ commit: {
+ shortSha: 'abcd1234',
+ },
+ isLast: true,
+ },
+ retryUrl: 'deployment/retry',
+ };
+
+ const mockSetEnvironmentToRollback = jest.fn();
+ const mockResolvers = {
+ Mutation: {
+ setEnvironmentToRollback: mockSetEnvironmentToRollback,
+ },
+ };
+ const createWrapper = ({ actions, rollback, approvalEnvironment }) => {
+ const mockApollo = createMockApollo([], mockResolvers);
return mountExtended(DeploymentActions, {
+ apolloProvider: mockApollo,
+ provide: {
+ projectPath: 'fullProjectPath',
+ },
propsData: {
actions,
+ rollback,
+ approvalEnvironment,
},
});
};
- describe('when there is no actions provided', () => {
- beforeEach(() => {
- wrapper = createWrapper({ actions: [] });
+ const findRollbackButton = () => wrapper.findComponent(GlButton);
+
+ describe('deployment 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);
+ });
});
- 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);
+ });
});
});
- describe('when there are actions provided', () => {
- beforeEach(() => {
- wrapper = createWrapper({ actions: actionsData });
+ describe('rollback action', () => {
+ describe('when there is no rollback data available', () => {
+ it('should not show a rollback button', () => {
+ wrapper = createWrapper({ actions: [] });
+ const button = findRollbackButton();
+ expect(button.exists()).toBe(false);
+ });
});
- it('should render actions component', () => {
- const actionsComponent = wrapper.findComponent(ActionsComponent);
- expect(actionsComponent.exists()).toBe(true);
- expect(actionsComponent.props().actions).toBe(actionsData);
- });
+ describe.each([
+ { isLast: true, buttonTitle: translations.redeployButtonTitle, icon: 'repeat' },
+ { isLast: false, buttonTitle: translations.rollbackButtonTitle, icon: 'redo' },
+ ])(
+ `when there is a rollback data available and the deployment isLast=$isLast`,
+ ({ isLast, buttonTitle, icon }) => {
+ let rollback;
+ beforeEach(() => {
+ const lastDeployment = { ...rollbackData.lastDeployment, isLast };
+ rollback = { ...rollbackData };
+ rollback.lastDeployment = lastDeployment;
+ wrapper = createWrapper({ actions: [], rollback });
+ });
+
+ it('should show the rollback button', () => {
+ const button = findRollbackButton();
+ expect(button.exists()).toBe(true);
+ });
+
+ it(`the rollback button should have "${icon}" icon`, () => {
+ const button = findRollbackButton();
+ expect(button.props().icon).toBe(icon);
+ });
+
+ it(`the rollback button should have "${buttonTitle}" title`, () => {
+ const button = findRollbackButton();
+ expect(button.attributes().title).toBe(buttonTitle);
+ });
+
+ it(`the rollback button click should send correct mutation`, async () => {
+ const button = findRollbackButton();
+ button.vm.$emit('click');
+ await waitForPromises();
+ expect(mockSetEnvironmentToRollback).toHaveBeenCalledWith(
+ expect.anything(),
+ { environment: rollback },
+ expect.anything(),
+ expect.anything(),
+ );
+ });
+ },
+ );
});
});
diff --git a/spec/frontend/environments/environment_details/page_spec.js b/spec/frontend/environments/environment_details/page_spec.js
index 3a1a3238abe..ed7e0feb6ed 100644
--- a/spec/frontend/environments/environment_details/page_spec.js
+++ b/spec/frontend/environments/environment_details/page_spec.js
@@ -15,19 +15,40 @@ describe('~/environments/environment_details/page.vue', () => {
let wrapper;
+ const emptyEnvironmentToRollbackData = { id: '', name: '', lastDeployment: null, retryUrl: '' };
+ const environmentToRollbackMock = jest.fn();
+
+ const mockResolvers = {
+ Query: {
+ environmentToRollback: environmentToRollbackMock,
+ },
+ };
+
const defaultWrapperParameters = {
resolvedData: resolvedEnvironmentDetails,
+ environmentToRollbackData: emptyEnvironmentToRollbackData,
};
- const createWrapper = ({ resolvedData } = defaultWrapperParameters) => {
- const mockApollo = createMockApollo([
- [getEnvironmentDetails, jest.fn().mockResolvedValue(resolvedData)],
- ]);
+ const createWrapper = ({
+ resolvedData,
+ environmentToRollbackData,
+ } = defaultWrapperParameters) => {
+ const mockApollo = createMockApollo(
+ [[getEnvironmentDetails, jest.fn().mockResolvedValue(resolvedData)]],
+ mockResolvers,
+ );
+ environmentToRollbackMock.mockReturnValue(
+ environmentToRollbackData || emptyEnvironmentToRollbackData,
+ );
+ const projectFullPath = 'gitlab-group/test-project';
return mountExtended(EnvironmentsDetailPage, {
apolloProvider: mockApollo,
+ provide: {
+ projectPath: projectFullPath,
+ },
propsData: {
- projectFullPath: 'gitlab-group/test-project',
+ projectFullPath,
environmentName: 'test-environment-name',
},
});
@@ -48,7 +69,7 @@ describe('~/environments/environment_details/page.vue', () => {
wrapper = createWrapper();
await waitForPromises();
});
- it('should render a table when query is loaded', async () => {
+ it('should render a table when query is loaded', () => {
expect(wrapper.findComponent(GlLoadingIcon).exists()).not.toBe(true);
expect(wrapper.findComponent(GlTableLite).exists()).toBe(true);
});
@@ -60,7 +81,7 @@ describe('~/environments/environment_details/page.vue', () => {
await waitForPromises();
});
- it('should render empty state component', async () => {
+ it('should render empty state component', () => {
expect(wrapper.findComponent(GlTableLite).exists()).toBe(false);
expect(wrapper.findComponent(EmptyState).exists()).toBe(true);
});
diff --git a/spec/frontend/environments/environment_folder_spec.js b/spec/frontend/environments/environment_folder_spec.js
index 279ff32a13d..4716f807657 100644
--- a/spec/frontend/environments/environment_folder_spec.js
+++ b/spec/frontend/environments/environment_folder_spec.js
@@ -38,7 +38,7 @@ describe('~/environments/components/environments_folder.vue', () => {
provide: { helpPagePath: '/help', projectId: '1' },
});
- beforeEach(async () => {
+ beforeEach(() => {
environmentFolderMock = jest.fn();
[nestedEnvironment] = resolvedEnvironmentsApp.environments;
environmentFolderMock.mockReturnValue(resolvedFolder);
diff --git a/spec/frontend/environments/environment_stop_spec.js b/spec/frontend/environments/environment_stop_spec.js
index 851e24c22cc..3e27b8822e1 100644
--- a/spec/frontend/environments/environment_stop_spec.js
+++ b/spec/frontend/environments/environment_stop_spec.js
@@ -73,7 +73,7 @@ describe('Stop Component', () => {
});
});
- it('should show a loading icon if the environment is currently stopping', async () => {
+ it('should show a loading icon if the environment is currently stopping', () => {
expect(findButton().props('loading')).toBe(true);
});
});
diff --git a/spec/frontend/environments/environments_app_spec.js b/spec/frontend/environments/environments_app_spec.js
index a843f801da5..6f2ee6f06cd 100644
--- a/spec/frontend/environments/environments_app_spec.js
+++ b/spec/frontend/environments/environments_app_spec.js
@@ -422,7 +422,7 @@ describe('~/environments/components/environments_app.vue', () => {
);
});
- it('should sync search term from query params on load', async () => {
+ it('should sync search term from query params on load', () => {
expect(searchBox.element.value).toBe('prod');
});
});
diff --git a/spec/frontend/environments/graphql/mock_data.js b/spec/frontend/environments/graphql/mock_data.js
index b5435990042..8d91ffe5ffc 100644
--- a/spec/frontend/environments/graphql/mock_data.js
+++ b/spec/frontend/environments/graphql/mock_data.js
@@ -801,6 +801,14 @@ export const resolvedDeploymentDetails = {
export const agent = {
project: 'agent-project',
- id: '1',
+ id: 'gid://gitlab/ClusterAgent/1',
name: 'agent-name',
+ kubernetesNamespace: 'agent-namespace',
};
+
+const runningPod = { status: { phase: 'Running' } };
+const pendingPod = { status: { phase: 'Pending' } };
+const succeededPod = { status: { phase: 'Succeeded' } };
+const failedPod = { status: { phase: 'Failed' } };
+
+export const k8sPodsMock = [runningPod, runningPod, pendingPod, succeededPod, failedPod, failedPod];
diff --git a/spec/frontend/environments/graphql/resolvers_spec.js b/spec/frontend/environments/graphql/resolvers_spec.js
index 2c223d3a1a7..c66844f5f24 100644
--- a/spec/frontend/environments/graphql/resolvers_spec.js
+++ b/spec/frontend/environments/graphql/resolvers_spec.js
@@ -1,4 +1,5 @@
import MockAdapter from 'axios-mock-adapter';
+import { CoreV1Api } from '@gitlab/cluster-client';
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';
@@ -17,6 +18,7 @@ import {
resolvedEnvironment,
folder,
resolvedFolder,
+ k8sPodsMock,
} from './mock_data';
const ENDPOINT = `${TEST_HOST}/environments`;
@@ -143,6 +145,61 @@ describe('~/frontend/environments/graphql/resolvers', () => {
expect(environmentFolder).toEqual(resolvedFolder);
});
});
+ describe('k8sPods', () => {
+ const namespace = 'default';
+ const configuration = {
+ basePath: 'kas-proxy/',
+ baseOptions: {
+ headers: { 'GitLab-Agent-Id': '1' },
+ },
+ };
+
+ const mockPodsListFn = jest.fn().mockImplementation(() => {
+ return Promise.resolve({
+ data: {
+ items: k8sPodsMock,
+ },
+ });
+ });
+
+ const mockNamespacedPodsListFn = jest.fn().mockImplementation(mockPodsListFn);
+ const mockAllPodsListFn = jest.fn().mockImplementation(mockPodsListFn);
+
+ beforeEach(() => {
+ jest
+ .spyOn(CoreV1Api.prototype, 'listCoreV1NamespacedPod')
+ .mockImplementation(mockNamespacedPodsListFn);
+ jest
+ .spyOn(CoreV1Api.prototype, 'listCoreV1PodForAllNamespaces')
+ .mockImplementation(mockAllPodsListFn);
+ });
+
+ it('should request namespaced pods from the cluster_client library if namespace is specified', async () => {
+ const pods = await mockResolvers.Query.k8sPods(null, { configuration, namespace });
+
+ expect(mockNamespacedPodsListFn).toHaveBeenCalledWith(namespace);
+ expect(mockAllPodsListFn).not.toHaveBeenCalled();
+
+ expect(pods).toEqual(k8sPodsMock);
+ });
+ it('should request all pods from the cluster_client library if namespace is not specified', async () => {
+ const pods = await mockResolvers.Query.k8sPods(null, { configuration, namespace: '' });
+
+ expect(mockAllPodsListFn).toHaveBeenCalled();
+ expect(mockNamespacedPodsListFn).not.toHaveBeenCalled();
+
+ expect(pods).toEqual(k8sPodsMock);
+ });
+ it('should throw an error if the API call fails', async () => {
+ jest
+ .spyOn(CoreV1Api.prototype, 'listCoreV1PodForAllNamespaces')
+ .mockRejectedValue(new Error('API error'));
+
+ await expect(mockResolvers.Query.k8sPods(null, { configuration })).rejects.toThrow(
+ 'API error',
+ );
+ });
+ });
describe('stopEnvironment', () => {
it('should post to the stop environment path', async () => {
mock.onPost(ENDPOINT).reply(HTTP_STATUS_OK);
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 326a28bd769..ec0fe0c5541 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
@@ -26,11 +26,37 @@ Object {
},
"created": "2022-10-17T07:44:17Z",
"deployed": "2022-10-17T07:44:43Z",
+ "deploymentApproval": Object {
+ "isApprovalActionAvailable": false,
+ },
"id": "31",
"job": Object {
"label": "deploy-prod (#860)",
"webPath": "/gitlab-org/pipelinestest/-/jobs/860",
},
+ "rollback": Object {
+ "id": "gid://gitlab/Deployment/76",
+ "lastDeployment": Object {
+ "commit": Object {
+ "author": Object {
+ "avatarUrl": "/uploads/-/system/user/avatar/1/avatar.png",
+ "id": "gid://gitlab/User/1",
+ "name": "Administrator",
+ "webUrl": "http://gdk.test:3000/root",
+ },
+ "authorEmail": "admin@example.com",
+ "authorGravatar": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
+ "authorName": "Administrator",
+ "id": "gid://gitlab/CommitPresenter/0cb48dd5deddb7632fd7c3defb16075fc6c3ca74",
+ "message": "Update .gitlab-ci.yml file",
+ "shortId": "0cb48dd5",
+ "webUrl": "http://gdk.test:3000/gitlab-org/pipelinestest/-/commit/0cb48dd5deddb7632fd7c3defb16075fc6c3ca74",
+ },
+ "isLast": false,
+ },
+ "name": undefined,
+ "retryUrl": "/gitlab-org/pipelinestest/-/jobs/860/retry",
+ },
"status": "success",
"triggerer": Object {
"avatarUrl": "/uploads/-/system/user/avatar/1/avatar.png",
@@ -60,8 +86,12 @@ Object {
},
"created": "2022-10-17T07:44:17Z",
"deployed": "2022-10-17T07:44:43Z",
+ "deploymentApproval": Object {
+ "isApprovalActionAvailable": false,
+ },
"id": "31",
"job": undefined,
+ "rollback": null,
"status": "success",
"triggerer": Object {
"avatarUrl": "/uploads/-/system/user/avatar/1/avatar.png",
@@ -91,8 +121,12 @@ Object {
},
"created": "2022-10-17T07:44:17Z",
"deployed": "",
+ "deploymentApproval": Object {
+ "isApprovalActionAvailable": false,
+ },
"id": "31",
"job": null,
+ "rollback": null,
"status": "success",
"triggerer": Object {
"avatarUrl": "/uploads/-/system/user/avatar/1/avatar.png",
diff --git a/spec/frontend/environments/kubernetes_overview_spec.js b/spec/frontend/environments/kubernetes_overview_spec.js
index 8673c657760..1912fd4a82b 100644
--- a/spec/frontend/environments/kubernetes_overview_spec.js
+++ b/spec/frontend/environments/kubernetes_overview_spec.js
@@ -1,19 +1,28 @@
import { nextTick } from 'vue';
import { shallowMount } from '@vue/test-utils';
-import { GlCollapse, GlButton } from '@gitlab/ui';
+import { GlCollapse, GlButton, GlAlert } from '@gitlab/ui';
import KubernetesOverview from '~/environments/components/kubernetes_overview.vue';
import KubernetesAgentInfo from '~/environments/components/kubernetes_agent_info.vue';
-
-const agent = {
- project: 'agent-project',
- id: '1',
- name: 'agent-name',
-};
+import KubernetesPods from '~/environments/components/kubernetes_pods.vue';
+import { agent } from './graphql/mock_data';
+import { mockKasTunnelUrl } from './mock_data';
const propsData = {
agentId: agent.id,
agentName: agent.name,
agentProjectPath: agent.project,
+ namespace: agent.kubernetesNamespace,
+};
+
+const provide = {
+ kasTunnelUrl: mockKasTunnelUrl,
+};
+
+const configuration = {
+ basePath: provide.kasTunnelUrl.replace(/\/$/, ''),
+ baseOptions: {
+ headers: { 'GitLab-Agent-Id': '1' },
+ },
};
describe('~/environments/components/kubernetes_overview.vue', () => {
@@ -22,10 +31,13 @@ describe('~/environments/components/kubernetes_overview.vue', () => {
const findCollapse = () => wrapper.findComponent(GlCollapse);
const findCollapseButton = () => wrapper.findComponent(GlButton);
const findAgentInfo = () => wrapper.findComponent(KubernetesAgentInfo);
+ const findKubernetesPods = () => wrapper.findComponent(KubernetesPods);
+ const findAlert = () => wrapper.findComponent(GlAlert);
const createWrapper = () => {
wrapper = shallowMount(KubernetesOverview, {
propsData,
+ provide,
});
};
@@ -57,6 +69,7 @@ describe('~/environments/components/kubernetes_overview.vue', () => {
it("doesn't render components when the collapse is not visible", () => {
expect(findAgentInfo().exists()).toBe(false);
+ expect(findKubernetesPods().exists()).toBe(false);
});
it('opens on click', async () => {
@@ -70,15 +83,40 @@ describe('~/environments/components/kubernetes_overview.vue', () => {
});
describe('when section is expanded', () => {
- it('renders kubernetes agent info', async () => {
+ beforeEach(() => {
createWrapper();
- await toggleCollapse();
+ toggleCollapse();
+ });
+ it('renders kubernetes agent info', () => {
expect(findAgentInfo().props()).toEqual({
agentName: agent.name,
agentId: agent.id,
agentProjectPath: agent.project,
});
});
+
+ it('renders kubernetes pods', () => {
+ expect(findKubernetesPods().props()).toEqual({
+ namespace: agent.kubernetesNamespace,
+ configuration,
+ });
+ });
+ });
+
+ describe('on cluster error', () => {
+ beforeEach(() => {
+ createWrapper();
+ toggleCollapse();
+ });
+
+ it('shows alert with the error message', async () => {
+ const error = 'Error message from pods';
+
+ findKubernetesPods().vm.$emit('cluster-error', error);
+ await nextTick();
+
+ expect(findAlert().text()).toBe(error);
+ });
});
});
diff --git a/spec/frontend/environments/kubernetes_pods_spec.js b/spec/frontend/environments/kubernetes_pods_spec.js
new file mode 100644
index 00000000000..137309d7853
--- /dev/null
+++ b/spec/frontend/environments/kubernetes_pods_spec.js
@@ -0,0 +1,114 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import { shallowMount } from '@vue/test-utils';
+import { GlLoadingIcon } from '@gitlab/ui';
+import { GlSingleStat } from '@gitlab/ui/dist/charts';
+import waitForPromises from 'helpers/wait_for_promises';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import KubernetesPods from '~/environments/components/kubernetes_pods.vue';
+import { mockKasTunnelUrl } from './mock_data';
+import { k8sPodsMock } from './graphql/mock_data';
+
+Vue.use(VueApollo);
+
+describe('~/environments/components/kubernetes_pods.vue', () => {
+ let wrapper;
+
+ const namespace = 'my-kubernetes-namespace';
+ const configuration = {
+ basePath: mockKasTunnelUrl,
+ baseOptions: {
+ headers: { 'GitLab-Agent-Id': '1' },
+ },
+ };
+
+ const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
+ const findAllStats = () => wrapper.findAllComponents(GlSingleStat);
+ const findSingleStat = (at) => findAllStats().at(at);
+
+ const createApolloProvider = () => {
+ const mockResolvers = {
+ Query: {
+ k8sPods: jest.fn().mockReturnValue(k8sPodsMock),
+ },
+ };
+
+ return createMockApollo([], mockResolvers);
+ };
+
+ const createWrapper = (apolloProvider = createApolloProvider()) => {
+ wrapper = shallowMount(KubernetesPods, {
+ propsData: { namespace, configuration },
+ apolloProvider,
+ });
+ };
+
+ describe('mounted', () => {
+ it('shows the loading icon', () => {
+ createWrapper();
+
+ expect(findLoadingIcon().exists()).toBe(true);
+ });
+
+ it('hides the loading icon when the list of pods loaded', async () => {
+ createWrapper();
+ await waitForPromises();
+
+ expect(findLoadingIcon().exists()).toBe(false);
+ });
+ });
+
+ describe('when gets pods data', () => {
+ it('renders stats', async () => {
+ createWrapper();
+ await waitForPromises();
+
+ expect(findAllStats()).toHaveLength(4);
+ });
+
+ it.each`
+ count | title | index
+ ${2} | ${KubernetesPods.i18n.runningPods} | ${0}
+ ${1} | ${KubernetesPods.i18n.pendingPods} | ${1}
+ ${1} | ${KubernetesPods.i18n.succeededPods} | ${2}
+ ${2} | ${KubernetesPods.i18n.failedPods} | ${3}
+ `(
+ 'renders stat with title "$title" and count "$count" at index $index',
+ async ({ count, title, index }) => {
+ createWrapper();
+ await waitForPromises();
+
+ expect(findSingleStat(index).props()).toMatchObject({
+ value: count,
+ title,
+ });
+ },
+ );
+ });
+
+ describe('when gets an error from the cluster_client API', () => {
+ const error = new Error('Error from the cluster_client API');
+ const createErroredApolloProvider = () => {
+ const mockResolvers = {
+ Query: {
+ k8sPods: jest.fn().mockRejectedValueOnce(error),
+ },
+ };
+
+ return createMockApollo([], mockResolvers);
+ };
+
+ beforeEach(async () => {
+ createWrapper(createErroredApolloProvider());
+ await waitForPromises();
+ });
+
+ it("doesn't show pods stats", () => {
+ expect(findAllStats()).toHaveLength(0);
+ });
+
+ it('emits an error message', () => {
+ expect(wrapper.emitted('cluster-error')).toMatchObject([[error]]);
+ });
+ });
+});
diff --git a/spec/frontend/environments/mock_data.js b/spec/frontend/environments/mock_data.js
index a6d67c26304..bd2c6b7c892 100644
--- a/spec/frontend/environments/mock_data.js
+++ b/spec/frontend/environments/mock_data.js
@@ -313,6 +313,8 @@ const createEnvironment = (data = {}) => ({
...data,
});
+const mockKasTunnelUrl = 'https://kas.gitlab.com/k8s-proxy';
+
export {
environment,
environmentsList,
@@ -321,4 +323,5 @@ export {
tableData,
deployBoardMockData,
createEnvironment,
+ mockKasTunnelUrl,
};
diff --git a/spec/frontend/environments/new_environment_item_spec.js b/spec/frontend/environments/new_environment_item_spec.js
index c04ff896794..b4f5263a151 100644
--- a/spec/frontend/environments/new_environment_item_spec.js
+++ b/spec/frontend/environments/new_environment_item_spec.js
@@ -7,10 +7,12 @@ import { stubTransition } from 'helpers/stub_transition';
import { formatDate, getTimeago } from '~/lib/utils/datetime_utility';
import { __, s__, sprintf } from '~/locale';
import EnvironmentItem from '~/environments/components/new_environment_item.vue';
+import EnvironmentActions from '~/environments/components/environment_actions.vue';
import Deployment from '~/environments/components/deployment.vue';
import DeployBoardWrapper from '~/environments/components/deploy_board_wrapper.vue';
import KubernetesOverview from '~/environments/components/kubernetes_overview.vue';
import { resolvedEnvironment, rolloutStatus, agent } from './graphql/mock_data';
+import { mockKasTunnelUrl } from './mock_data';
Vue.use(VueApollo);
@@ -25,11 +27,18 @@ describe('~/environments/components/new_environment_item.vue', () => {
mountExtended(EnvironmentItem, {
apolloProvider,
propsData: { environment: resolvedEnvironment, ...propsData },
- provide: { helpPagePath: '/help', projectId: '1', projectPath: '/1', ...provideData },
+ provide: {
+ helpPagePath: '/help',
+ projectId: '1',
+ projectPath: '/1',
+ kasTunnelUrl: mockKasTunnelUrl,
+ ...provideData,
+ },
stubs: { transition: stubTransition() },
});
const findDeployment = () => wrapper.findComponent(Deployment);
+ const findActions = () => wrapper.findComponent(EnvironmentActions);
const findKubernetesOverview = () => wrapper.findComponent(KubernetesOverview);
const expandCollapsedSection = async () => {
@@ -124,9 +133,7 @@ describe('~/environments/components/new_environment_item.vue', () => {
it('shows a dropdown if there are actions to perform', () => {
wrapper = createWrapper({ apolloProvider: createApolloProvider() });
- const actions = wrapper.findByRole('button', { name: __('Deploy to...') });
-
- expect(actions.exists()).toBe(true);
+ expect(findActions().exists()).toBe(true);
});
it('does not show a dropdown if there are no actions to perform', () => {
@@ -140,17 +147,15 @@ describe('~/environments/components/new_environment_item.vue', () => {
},
});
- const actions = wrapper.findByRole('button', { name: __('Deploy to...') });
-
- expect(actions.exists()).toBe(false);
+ expect(findActions().exists()).toBe(false);
});
it('passes all the actions down to the action component', () => {
wrapper = createWrapper({ apolloProvider: createApolloProvider() });
- const action = wrapper.findByRole('menuitem', { name: 'deploy-staging' });
-
- expect(action.exists()).toBe(true);
+ expect(findActions().props('actions')).toMatchObject(
+ resolvedEnvironment.lastDeployment.manualActions,
+ );
});
});
@@ -382,6 +387,7 @@ describe('~/environments/components/new_environment_item.vue', () => {
const button = await expandCollapsedSection();
expect(button.attributes('aria-label')).toBe(__('Collapse'));
+ expect(button.props('category')).toBe('secondary');
expect(collapse.attributes('visible')).toBe('visible');
expect(icon.props('name')).toBe('chevron-lg-down');
expect(environmentName.classes('gl-font-weight-bold')).toBe(true);
@@ -537,6 +543,7 @@ describe('~/environments/components/new_environment_item.vue', () => {
agentProjectPath: agent.project,
agentName: agent.name,
agentId: agent.id,
+ namespace: agent.kubernetesNamespace,
});
});
diff --git a/spec/frontend/environments/stop_stale_environments_modal_spec.js b/spec/frontend/environments/stop_stale_environments_modal_spec.js
index ddf6670db12..3d28ceba318 100644
--- a/spec/frontend/environments/stop_stale_environments_modal_spec.js
+++ b/spec/frontend/environments/stop_stale_environments_modal_spec.js
@@ -40,12 +40,12 @@ describe('~/environments/components/stop_stale_environments_modal.vue', () => {
jest.resetAllMocks();
});
- it('sets the correct min and max dates', async () => {
+ it('sets the correct min and max dates', () => {
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 () => {
+ it('requests cleanup when submit is clicked', () => {
mock.onPost().replyOnce(HTTP_STATUS_OK);
wrapper.findComponent(GlModal).vm.$emit('primary');
const url = STOP_STALE_ENVIRONMENTS_PATH.replace(':id', 1).replace(':version', 'v4');
diff --git a/spec/frontend/error_tracking/components/error_tracking_list_spec.js b/spec/frontend/error_tracking/components/error_tracking_list_spec.js
index 31473899145..c9a2551d11c 100644
--- a/spec/frontend/error_tracking/components/error_tracking_list_spec.js
+++ b/spec/frontend/error_tracking/components/error_tracking_list_spec.js
@@ -98,12 +98,6 @@ describe('ErrorTrackingList', () => {
});
});
- afterEach(() => {
- if (wrapper) {
- wrapper.destroy();
- }
- });
-
describe('loading', () => {
beforeEach(() => {
store.state.list.loading = true;
@@ -452,32 +446,34 @@ describe('ErrorTrackingList', () => {
describe('When pagination is required', () => {
describe('and previous cursor is not available', () => {
- beforeEach(async () => {
+ beforeEach(() => {
store.state.list.loading = false;
delete store.state.list.pagination.previous;
mountComponent();
});
- it('disables Prev button in the pagination', async () => {
+ it('disables Prev button in the pagination', () => {
expect(findPagination().props('prevPage')).toBe(null);
expect(findPagination().props('nextPage')).not.toBe(null);
});
});
describe('and next cursor is not available', () => {
- beforeEach(async () => {
+ beforeEach(() => {
store.state.list.loading = false;
delete store.state.list.pagination.next;
mountComponent();
});
- it('disables Next button in the pagination', async () => {
+ it('disables Next button in the pagination', () => {
expect(findPagination().props('prevPage')).not.toBe(null);
expect(findPagination().props('nextPage')).toBe(null);
});
});
describe('and the user is not on the first page', () => {
describe('and the previous button is clicked', () => {
- beforeEach(async () => {
+ const currentPage = 2;
+
+ beforeEach(() => {
store.state.list.loading = false;
mountComponent({
stubs: {
@@ -485,15 +481,12 @@ describe('ErrorTrackingList', () => {
GlPagination: false,
},
});
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({ pageValue: 2 });
- await nextTick();
+ findPagination().vm.$emit('input', currentPage);
});
it('fetches the previous page of results', () => {
expect(wrapper.find('.prev-page-item').attributes('aria-disabled')).toBe(undefined);
- wrapper.vm.goToPrevPage();
+ findPagination().vm.$emit('input', currentPage - 1);
expect(actions.fetchPaginatedResults).toHaveBeenCalled();
expect(actions.fetchPaginatedResults).toHaveBeenLastCalledWith(
expect.anything(),
diff --git a/spec/frontend/feature_flags/components/configure_feature_flags_modal_spec.js b/spec/frontend/feature_flags/components/configure_feature_flags_modal_spec.js
index b06e0340991..a12c25c6897 100644
--- a/spec/frontend/feature_flags/components/configure_feature_flags_modal_spec.js
+++ b/spec/frontend/feature_flags/components/configure_feature_flags_modal_spec.js
@@ -128,11 +128,11 @@ describe('Configure Feature Flags Modal', () => {
expect(findSecondaryAction()).toBe(null);
});
- it('should not display regenerating instance ID', async () => {
+ it('should not display regenerating instance ID', () => {
expect(findDangerGlAlert().exists()).toBe(false);
});
- it('should disable the project name input', async () => {
+ it('should disable the project name input', () => {
expect(findProjectNameInput().exists()).toBe(false);
});
});
@@ -142,7 +142,7 @@ describe('Configure Feature Flags Modal', () => {
factory({ hasRotateError: true });
});
- it('should display an error', async () => {
+ it('should display an error', () => {
expect(wrapper.findByTestId('rotate-error').exists()).toBe(true);
expect(wrapper.find('[name="warning"]').exists()).toBe(true);
});
@@ -151,7 +151,7 @@ describe('Configure Feature Flags Modal', () => {
describe('is rotating', () => {
beforeEach(factory.bind(null, { isRotating: true }));
- it('should disable the project name input', async () => {
+ it('should disable the project name input', () => {
expect(findProjectNameInput().attributes('disabled')).toBe('true');
});
});
diff --git a/spec/frontend/feature_flags/components/feature_flags_spec.js b/spec/frontend/feature_flags/components/feature_flags_spec.js
index 23e86d0eb2f..8492fe7bdde 100644
--- a/spec/frontend/feature_flags/components/feature_flags_spec.js
+++ b/spec/frontend/feature_flags/components/feature_flags_spec.js
@@ -94,7 +94,7 @@ describe('Feature flags', () => {
await limitAlert().vm.$emit('dismiss');
});
- it('hides the alert', async () => {
+ it('hides the alert', () => {
expect(limitAlert().exists()).toBe(false);
});
@@ -176,7 +176,7 @@ describe('Feature flags', () => {
emptyState = wrapper.findComponent(GlEmptyState);
});
- it('should render the empty state', async () => {
+ it('should render the empty state', () => {
expect(emptyState.exists()).toBe(true);
});
diff --git a/spec/frontend/filtered_search/dropdown_user_spec.js b/spec/frontend/filtered_search/dropdown_user_spec.js
index 02ef813883f..8ddf8390431 100644
--- a/spec/frontend/filtered_search/dropdown_user_spec.js
+++ b/spec/frontend/filtered_search/dropdown_user_spec.js
@@ -1,4 +1,5 @@
-import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
+import htmlMergeRequestList from 'test_fixtures/merge_requests/merge_request_list.html';
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import DropdownUser from '~/filtered_search/dropdown_user';
import DropdownUtils from '~/filtered_search/dropdown_utils';
import FilteredSearchTokenizer from '~/filtered_search/filtered_search_tokenizer';
@@ -71,13 +72,11 @@ describe('Dropdown User', () => {
});
describe('hideCurrentUser', () => {
- const fixtureTemplate = 'merge_requests/merge_request_list.html';
-
let dropdown;
let authorFilterDropdownElement;
beforeEach(() => {
- loadHTMLFixture(fixtureTemplate);
+ setHTMLFixture(htmlMergeRequestList);
authorFilterDropdownElement = document.querySelector('#js-dropdown-author');
const dummyInput = document.createElement('div');
dropdown = new DropdownUser({
diff --git a/spec/frontend/filtered_search/dropdown_utils_spec.js b/spec/frontend/filtered_search/dropdown_utils_spec.js
index 2030b45b44c..d8a5b493b7a 100644
--- a/spec/frontend/filtered_search/dropdown_utils_spec.js
+++ b/spec/frontend/filtered_search/dropdown_utils_spec.js
@@ -1,12 +1,11 @@
-import { loadHTMLFixture, setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
+import htmlMergeRequestList from 'test_fixtures/merge_requests/merge_request_list.html';
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import FilteredSearchSpecHelper from 'helpers/filtered_search_spec_helper';
import DropdownUtils from '~/filtered_search/dropdown_utils';
import FilteredSearchDropdownManager from '~/filtered_search/filtered_search_dropdown_manager';
import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered_search_token_keys';
describe('Dropdown Utils', () => {
- const issuableListFixture = 'merge_requests/merge_request_list.html';
-
describe('getEscapedText', () => {
it('should return same word when it has no space', () => {
const escaped = DropdownUtils.getEscapedText('textWithoutSpace');
@@ -355,7 +354,7 @@ describe('Dropdown Utils', () => {
let authorToken;
beforeEach(() => {
- loadHTMLFixture(issuableListFixture);
+ setHTMLFixture(htmlMergeRequestList);
authorToken = FilteredSearchSpecHelper.createFilterVisualToken('author', '=', '@user');
const searchTermToken = FilteredSearchSpecHelper.createSearchVisualToken('search term');
diff --git a/spec/frontend/fixtures/api_projects.rb b/spec/frontend/fixtures/api_projects.rb
index d1dfd223419..24c47d8d139 100644
--- a/spec/frontend/fixtures/api_projects.rb
+++ b/spec/frontend/fixtures/api_projects.rb
@@ -6,10 +6,11 @@ RSpec.describe API::Projects, '(JavaScript fixtures)', type: :request do
include ApiHelpers
include JavaScriptFixturesHelpers
- let(:namespace) { create(:namespace, name: 'gitlab-test') }
- let(:project) { create(:project, :repository, namespace: namespace, path: 'lorem-ipsum') }
- let(:project_empty) { create(:project_empty_repo, namespace: namespace, path: 'lorem-ipsum-empty') }
- let(:user) { project.owner }
+ let_it_be(:namespace) { create(:namespace, name: 'gitlab-test') }
+ let_it_be(:project) { create(:project, :repository, namespace: namespace, path: 'lorem-ipsum') }
+ let_it_be(:project_empty) { create(:project_empty_repo, namespace: namespace, path: 'lorem-ipsum-empty') }
+ let_it_be(:user) { project.owner }
+ let_it_be(:personal_projects) { create_list(:project, 3, namespace: user.namespace, topics: create_list(:topic, 5)) }
it 'api/projects/get.json' do
get api("/projects/#{project.id}", user)
@@ -28,4 +29,10 @@ RSpec.describe API::Projects, '(JavaScript fixtures)', type: :request do
expect(response).to be_successful
end
+
+ it 'api/users/projects/get.json' do
+ get api("/users/#{user.id}/projects", user)
+
+ expect(response).to be_successful
+ end
end
diff --git a/spec/frontend/fixtures/saved_replies.rb b/spec/frontend/fixtures/comment_templates.rb
index 613e4a1b447..32f425d7ebd 100644
--- a/spec/frontend/fixtures/saved_replies.rb
+++ b/spec/frontend/fixtures/comment_templates.rb
@@ -13,9 +13,9 @@ RSpec.describe GraphQL::Query, type: :request, feature_category: :user_profile d
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/'
+ context 'when user has no comment templates' do
+ base_input_path = 'comment_templates/queries/'
+ base_output_path = 'graphql/comment_templates/'
query_name = 'saved_replies.query.graphql'
it "#{base_output_path}saved_replies_empty.query.graphql.json" do
@@ -27,9 +27,9 @@ RSpec.describe GraphQL::Query, type: :request, feature_category: :user_profile d
end
end
- context 'when user has saved replies' do
- base_input_path = 'saved_replies/queries/'
- base_output_path = 'graphql/saved_replies/'
+ context 'when user has comment templates' do
+ base_input_path = 'comment_templates/queries/'
+ base_output_path = 'graphql/comment_templates/'
query_name = 'saved_replies.query.graphql'
it "#{base_output_path}saved_replies.query.graphql.json" do
@@ -44,9 +44,9 @@ RSpec.describe GraphQL::Query, type: :request, feature_category: :user_profile d
end
end
- context 'when user creates saved reply' do
- base_input_path = 'saved_replies/queries/'
- base_output_path = 'graphql/saved_replies/'
+ context 'when user creates comment template' do
+ base_input_path = 'comment_templates/queries/'
+ base_output_path = 'graphql/comment_templates/'
query_name = 'create_saved_reply.mutation.graphql'
it "#{base_output_path}#{query_name}.json" do
@@ -58,9 +58,9 @@ RSpec.describe GraphQL::Query, type: :request, feature_category: :user_profile d
end
end
- context 'when user creates saved reply and it errors' do
- base_input_path = 'saved_replies/queries/'
- base_output_path = 'graphql/saved_replies/'
+ context 'when user creates comment template and it errors' do
+ base_input_path = 'comment_templates/queries/'
+ base_output_path = 'graphql/comment_templates/'
query_name = 'create_saved_reply.mutation.graphql'
it "#{base_output_path}create_saved_reply_with_errors.mutation.graphql.json" do
diff --git a/spec/frontend/fixtures/issues.rb b/spec/frontend/fixtures/issues.rb
index 1e6baf30a76..e85e683b599 100644
--- a/spec/frontend/fixtures/issues.rb
+++ b/spec/frontend/fixtures/issues.rb
@@ -20,15 +20,6 @@ RSpec.describe Projects::IssuesController, '(JavaScript fixtures)', :with_licens
remove_repository(project)
end
- it 'issues/new-issue.html' do
- get :new, params: {
- namespace_id: project.namespace.to_param,
- project_id: project
- }
-
- expect(response).to be_successful
- end
-
it 'issues/open-issue.html' do
render_issue(create(:issue, project: project))
end
diff --git a/spec/frontend/fixtures/job_artifacts.rb b/spec/frontend/fixtures/job_artifacts.rb
index e53cdbbaaa5..6dadd6750f1 100644
--- a/spec/frontend/fixtures/job_artifacts.rb
+++ b/spec/frontend/fixtures/job_artifacts.rb
@@ -12,7 +12,7 @@ RSpec.describe 'Job Artifacts (GraphQL fixtures)' do
let_it_be(:pipeline) { create(:ci_pipeline, project: project) }
let_it_be(:user) { create(:user) }
- job_artifacts_query_path = 'artifacts/graphql/queries/get_job_artifacts.query.graphql'
+ job_artifacts_query_path = 'ci/artifacts/graphql/queries/get_job_artifacts.query.graphql'
it "graphql/#{job_artifacts_query_path}.json" do
create(:ci_build, :failed, :artifacts, :trace_artifact, pipeline: pipeline)
diff --git a/spec/frontend/fixtures/jobs.rb b/spec/frontend/fixtures/jobs.rb
index 3583beb83c2..376c04cd629 100644
--- a/spec/frontend/fixtures/jobs.rb
+++ b/spec/frontend/fixtures/jobs.rb
@@ -48,49 +48,49 @@ RSpec.describe 'Jobs (JavaScript fixtures)' do
let!(:with_artifact) { create(:ci_build, :success, name: 'with_artifact', job_artifacts: [artifact], pipeline: pipeline) }
let!(:with_coverage) { create(:ci_build, :success, name: 'with_coverage', coverage: 40.0, pipeline: pipeline) }
- fixtures_path = 'graphql/jobs/'
- get_jobs_query = 'get_jobs.query.graphql'
- full_path = 'frontend-fixtures/builds-project'
+ shared_examples 'graphql queries' do |path, jobs_query|
+ let_it_be(:variables) { {} }
- let_it_be(:query) do
- get_graphql_query_as_string("jobs/components/table/graphql/queries/#{get_jobs_query}")
- end
+ let_it_be(:query) do
+ get_graphql_query_as_string("#{path}/#{jobs_query}")
+ end
- it "#{fixtures_path}#{get_jobs_query}.json" do
- post_graphql(query, current_user: user, variables: {
- fullPath: full_path
- })
+ fixtures_path = 'graphql/jobs/'
- expect_graphql_errors_to_be_empty
- end
+ it "#{fixtures_path}#{jobs_query}.json" do
+ post_graphql(query, current_user: user, variables: variables)
- it "#{fixtures_path}#{get_jobs_query}.as_guest.json" do
- guest = create(:user)
- project.add_guest(guest)
+ expect_graphql_errors_to_be_empty
+ end
- post_graphql(query, current_user: guest, variables: {
- fullPath: full_path
- })
+ it "#{fixtures_path}#{jobs_query}.as_guest.json" do
+ guest = create(:user)
+ project.add_guest(guest)
- expect_graphql_errors_to_be_empty
- end
+ post_graphql(query, current_user: guest, variables: variables)
- it "#{fixtures_path}#{get_jobs_query}.paginated.json" do
- post_graphql(query, current_user: user, variables: {
- fullPath: full_path,
- first: 2
- })
+ expect_graphql_errors_to_be_empty
+ end
- expect_graphql_errors_to_be_empty
+ it "#{fixtures_path}#{jobs_query}.paginated.json" do
+ post_graphql(query, current_user: user, variables: variables.merge({ first: 2 }))
+
+ expect_graphql_errors_to_be_empty
+ end
+
+ it "#{fixtures_path}#{jobs_query}.empty.json" do
+ post_graphql(query, current_user: user, variables: variables.merge({ first: 0 }))
+
+ expect_graphql_errors_to_be_empty
+ end
end
- it "#{fixtures_path}#{get_jobs_query}.empty.json" do
- post_graphql(query, current_user: user, variables: {
- fullPath: full_path,
- first: 0
- })
+ it_behaves_like 'graphql queries', 'jobs/components/table/graphql/queries', 'get_jobs.query.graphql' do
+ let(:variables) { { fullPath: 'frontend-fixtures/builds-project' } }
+ end
- expect_graphql_errors_to_be_empty
+ it_behaves_like 'graphql queries', 'pages/admin/jobs/components/table/graphql/queries', 'get_all_jobs.query.graphql' do
+ let(:user) { create(:admin) }
end
end
diff --git a/spec/frontend/fixtures/milestones.rb b/spec/frontend/fixtures/milestones.rb
new file mode 100644
index 00000000000..5e39dcf190a
--- /dev/null
+++ b/spec/frontend/fixtures/milestones.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Projects::MilestonesController, '(JavaScript fixtures)', :with_license, feature_category: :team_planning, type: :controller do
+ include JavaScriptFixturesHelpers
+
+ let_it_be(:user) { create(:user, feed_token: 'feedtoken:coldfeed') }
+ let_it_be(:namespace) { create(:namespace, name: 'frontend-fixtures') }
+ let_it_be(:project) { create(:project_empty_repo, namespace: namespace, path: 'milestones-project') }
+
+ render_views
+
+ before do
+ project.add_maintainer(user)
+ sign_in(user)
+ end
+
+ after do
+ remove_repository(project)
+ end
+
+ it 'milestones/new-milestone.html' do
+ get :new, params: {
+ namespace_id: project.namespace.to_param,
+ project_id: project
+ }
+
+ expect(response).to be_successful
+ end
+
+ private
+
+ def render_milestone(milestone)
+ get :show, params: {
+ namespace_id: project.namespace.to_param,
+ project_id: project,
+ id: milestone.to_param
+ }
+
+ expect(response).to be_successful
+ end
+end
diff --git a/spec/frontend/fixtures/pipelines.rb b/spec/frontend/fixtures/pipelines.rb
index 768934d6278..24a6f6f7de6 100644
--- a/spec/frontend/fixtures/pipelines.rb
+++ b/spec/frontend/fixtures/pipelines.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe Projects::PipelinesController, '(JavaScript fixtures)', type: :controller do
+ include ApiHelpers
+ include GraphqlHelpers
include JavaScriptFixturesHelpers
let_it_be(:namespace) { create(:namespace, name: 'frontend-fixtures') }
@@ -56,4 +58,27 @@ RSpec.describe Projects::PipelinesController, '(JavaScript fixtures)', type: :co
expect(response).to be_successful
end
+
+ describe GraphQL::Query, type: :request do
+ fixtures_path = 'graphql/pipelines/'
+ get_pipeline_actions_query = 'get_pipeline_actions.query.graphql'
+
+ let!(:pipeline_with_manual_actions) { create(:ci_pipeline, project: project, user: user) }
+ let!(:build_scheduled) { create(:ci_build, :scheduled, pipeline: pipeline_with_manual_actions, stage: 'test') }
+ let!(:build_manual) { create(:ci_build, :manual, pipeline: pipeline_with_manual_actions, stage: 'build') }
+ let!(:build_manual_cannot_play) do
+ create(:ci_build, :manual, :skipped, pipeline: pipeline_with_manual_actions, stage: 'build')
+ end
+
+ let_it_be(:query) do
+ get_graphql_query_as_string("pipelines/graphql/queries/#{get_pipeline_actions_query}")
+ end
+
+ it "#{fixtures_path}#{get_pipeline_actions_query}.json" do
+ post_graphql(query, current_user: user,
+ variables: { fullPath: project.full_path, iid: pipeline_with_manual_actions.iid })
+
+ expect_graphql_errors_to_be_empty
+ end
+ end
end
diff --git a/spec/frontend/fixtures/projects.rb b/spec/frontend/fixtures/projects.rb
index 2ccf2c0392f..8cd651c5b36 100644
--- a/spec/frontend/fixtures/projects.rb
+++ b/spec/frontend/fixtures/projects.rb
@@ -67,7 +67,7 @@ RSpec.describe 'Projects (JavaScript fixtures)', type: :controller do
end
end
- describe 'Storage', feature_category: :subscription_cost_management do
+ describe 'Storage', feature_category: :consumables_cost_management do
describe GraphQL::Query, type: :request do
include GraphqlHelpers
context 'project storage statistics query' do
diff --git a/spec/frontend/fixtures/runner.rb b/spec/frontend/fixtures/runner.rb
index 1581bc58289..099df607487 100644
--- a/spec/frontend/fixtures/runner.rb
+++ b/spec/frontend/fixtures/runner.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Runner (JavaScript fixtures)' do
+RSpec.describe 'Runner (JavaScript fixtures)', feature_category: :runner_fleet do
include AdminModeHelper
include ApiHelpers
include JavaScriptFixturesHelpers
@@ -13,7 +13,7 @@ RSpec.describe 'Runner (JavaScript fixtures)' do
let_it_be(:project) { create(:project, :repository, :public) }
let_it_be(:project_2) { create(:project, :repository, :public) }
- let_it_be(:runner) { create(:ci_runner, :instance, description: 'My Runner', version: '1.0.0') }
+ let_it_be(:runner) { create(:ci_runner, :instance, description: 'My Runner', creator: admin, version: '1.0.0') }
let_it_be(:group_runner) { create(:ci_runner, :group, groups: [group], version: '2.0.0') }
let_it_be(:group_runner_2) { create(:ci_runner, :group, groups: [group], version: '2.0.0') }
let_it_be(:project_runner) { create(:ci_runner, :project, projects: [project, project_2], version: '2.0.0') }
@@ -58,6 +58,13 @@ RSpec.describe 'Runner (JavaScript fixtures)' do
expect_graphql_errors_to_be_empty
end
+
+ it "#{fixtures_path}#{all_runners_query}.with_creator.json" do
+ # "last: 1" fetches the first runner created, with admin as "creator"
+ post_graphql(query, current_user: admin, variables: { last: 1 })
+
+ expect_graphql_errors_to_be_empty
+ end
end
describe 'all_runners_count.query.graphql', type: :request do
@@ -169,14 +176,17 @@ RSpec.describe 'Runner (JavaScript fixtures)' do
get_graphql_query_as_string("#{query_path}#{runner_create_mutation}")
end
- it "#{fixtures_path}#{runner_create_mutation}.json" do
- post_graphql(query, current_user: admin, variables: {
- input: {
- description: 'My dummy runner'
- }
- })
+ context 'with runnerType set to INSTANCE_TYPE' do
+ it "#{fixtures_path}#{runner_create_mutation}.json" do
+ post_graphql(query, current_user: admin, variables: {
+ input: {
+ runnerType: 'INSTANCE_TYPE',
+ description: 'My dummy runner'
+ }
+ })
- expect_graphql_errors_to_be_empty
+ expect_graphql_errors_to_be_empty
+ end
end
end
end
diff --git a/spec/frontend/fixtures/startup_css.rb b/spec/frontend/fixtures/startup_css.rb
index 18a4aa58c00..5b09e1c9495 100644
--- a/spec/frontend/fixtures/startup_css.rb
+++ b/spec/frontend/fixtures/startup_css.rb
@@ -40,21 +40,6 @@ RSpec.describe 'Startup CSS fixtures', type: :controller do
expect(response).to be_successful
end
- # This Feature Flag is on by default
- # This ensures that the correct css is generated
- # When the feature flag is on, the general startup will capture it
- # This will be removed as part of https://gitlab.com/gitlab-org/gitlab/-/issues/339348
- it "startup_css/project-#{type}-search-ff-off.html" do
- stub_feature_flags(new_header_search: false)
-
- get :show, params: {
- namespace_id: project.namespace.to_param,
- id: project
- }
-
- expect(response).to be_successful
- end
-
# This Feature Flag is off by default
# This ensures that the correct css is generated for super sidebar
# When the feature flag is off, the general startup will capture it
diff --git a/spec/frontend/fixtures/static/oauth_remember_me.html b/spec/frontend/fixtures/static/oauth_remember_me.html
index 0b4d482925d..60277ecf66e 100644
--- a/spec/frontend/fixtures/static/oauth_remember_me.html
+++ b/spec/frontend/fixtures/static/oauth_remember_me.html
@@ -1,5 +1,5 @@
<div id="oauth-container">
- <input id="remember_me" type="checkbox" />
+ <input id="remember_me_omniauth" type="checkbox" />
<form method="post" action="http://example.com/">
<button class="js-oauth-login twitter" type="submit">
diff --git a/spec/frontend/fixtures/static/search_autocomplete.html b/spec/frontend/fixtures/static/search_autocomplete.html
deleted file mode 100644
index 29db9020424..00000000000
--- a/spec/frontend/fixtures/static/search_autocomplete.html
+++ /dev/null
@@ -1,15 +0,0 @@
-<div class="search search-form">
-<form class="form-inline">
-<div class="search-input-container">
-<div class="search-input-wrap">
-<div class="dropdown">
-<input class="search-input dropdown-menu-toggle" id="search">
-<div class="dropdown-menu dropdown-select">
-<div class="dropdown-content"></div>
-</div>
-</div>
-</div>
-</div>
-<input class="js-search-project-options" type="hidden">
-</form>
-</div>
diff --git a/spec/frontend/fixtures/timelogs.rb b/spec/frontend/fixtures/timelogs.rb
new file mode 100644
index 00000000000..c66e2447ea6
--- /dev/null
+++ b/spec/frontend/fixtures/timelogs.rb
@@ -0,0 +1,53 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Timelogs (GraphQL fixtures)', feature_category: :team_planning do
+ describe GraphQL::Query, type: :request do
+ include ApiHelpers
+ include GraphqlHelpers
+ include JavaScriptFixturesHelpers
+
+ let_it_be(:guest) { create(:user) }
+ let_it_be(:developer) { create(:user) }
+
+ context 'for time tracking timelogs' do
+ let_it_be(:project) { create(:project_empty_repo, :public) }
+ let_it_be(:issue) { create(:issue, project: project) }
+
+ let(:query_path) { 'time_tracking/components/queries/get_timelogs.query.graphql' }
+ let(:query) { get_graphql_query_as_string(query_path) }
+
+ before_all do
+ project.add_guest(guest)
+ project.add_developer(developer)
+ end
+
+ it "graphql/get_timelogs_empty_response.json" do
+ post_graphql(query, current_user: guest, variables: { username: guest.username })
+
+ expect_graphql_errors_to_be_empty
+ end
+
+ context 'with 20 or less timelogs' do
+ let_it_be(:timelogs) { create_list(:timelog, 6, user: developer, issue: issue, time_spent: 4 * 60 * 60) }
+
+ it "graphql/get_non_paginated_timelogs_response.json" do
+ post_graphql(query, current_user: guest, variables: { username: developer.username })
+
+ expect_graphql_errors_to_be_empty
+ end
+ end
+
+ context 'with more than 20 timelogs' do
+ let_it_be(:timelogs) { create_list(:timelog, 30, user: developer, issue: issue, time_spent: 4 * 60 * 60) }
+
+ it "graphql/get_paginated_timelogs_response.json" do
+ post_graphql(query, current_user: guest, variables: { username: developer.username, first: 25 })
+
+ expect_graphql_errors_to_be_empty
+ end
+ end
+ end
+ end
+end
diff --git a/spec/frontend/frequent_items/components/app_spec.js b/spec/frontend/frequent_items/components/app_spec.js
index 4f5788dcb77..a8ae72eb4b3 100644
--- a/spec/frontend/frequent_items/components/app_spec.js
+++ b/spec/frontend/frequent_items/components/app_spec.js
@@ -33,7 +33,6 @@ 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,
diff --git a/spec/frontend/frequent_items/components/frequent_items_list_spec.js b/spec/frontend/frequent_items/components/frequent_items_list_spec.js
index 87f8e131b77..dd6dd80af4f 100644
--- a/spec/frontend/frequent_items/components/frequent_items_list_spec.js
+++ b/spec/frontend/frequent_items/components/frequent_items_list_spec.js
@@ -48,7 +48,7 @@ describe('FrequentItemsListComponent', () => {
});
describe('fetched item messages', () => {
- it('should show default empty list message', async () => {
+ it('should show default empty list message', () => {
createComponent({
items: [],
});
diff --git a/spec/frontend/frequent_items/store/actions_spec.js b/spec/frontend/frequent_items/store/actions_spec.js
index c228bca4973..2feb488da2c 100644
--- a/spec/frontend/frequent_items/store/actions_spec.js
+++ b/spec/frontend/frequent_items/store/actions_spec.js
@@ -25,7 +25,6 @@ describe('Frequent Items Dropdown Store Actions', () => {
mockedState.namespace = mockNamespace;
mockedState.storageKey = mockStorageKey;
- gon.features = { fullPathProjectSearch: true };
});
afterEach(() => {
diff --git a/spec/frontend/gfm_auto_complete_spec.js b/spec/frontend/gfm_auto_complete_spec.js
index e4fd8649263..73284fbe5e5 100644
--- a/spec/frontend/gfm_auto_complete_spec.js
+++ b/spec/frontend/gfm_auto_complete_spec.js
@@ -666,10 +666,11 @@ describe('GfmAutoComplete', () => {
username: 'my-group',
title: '',
icon: '<i class="icon"/>',
- availabilityStatus: '<span class="gl-text-gray-500"> (Busy)</span>',
+ availabilityStatus:
+ '<span class="badge badge-warning badge-pill gl-badge sm gl-ml-2">Busy</span>',
}),
).toBe(
- '<li>IMG my-group <small><span class="gl-text-gray-500"> (Busy)</span></small> <i class="icon"/></li>',
+ '<li>IMG my-group <small><span class="badge badge-warning badge-pill gl-badge sm gl-ml-2">Busy</span></small> <i class="icon"/></li>',
);
});
diff --git a/spec/frontend/groups/components/app_spec.js b/spec/frontend/groups/components/app_spec.js
index 98868de8475..7b42e50fee5 100644
--- a/spec/frontend/groups/components/app_spec.js
+++ b/spec/frontend/groups/components/app_spec.js
@@ -112,7 +112,7 @@ describe('AppComponent', () => {
});
});
- it('should show alert error when request fails', () => {
+ it('should show an alert when request fails', () => {
mock.onGet('/dashboard/groups.json').reply(HTTP_STATUS_BAD_REQUEST);
jest.spyOn(window, 'scrollTo').mockImplementation(() => {});
@@ -320,7 +320,7 @@ describe('AppComponent', () => {
});
});
- it('should show error alert message if request failed to leave group', () => {
+ it('should show error alert if request failed to leave group', () => {
const message = 'An error occurred. Please try again.';
jest
.spyOn(vm.service, 'leaveGroup')
@@ -337,7 +337,7 @@ describe('AppComponent', () => {
});
});
- it('should show appropriate error alert message if request forbids to leave group', () => {
+ it('shows appropriate error alert 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: HTTP_STATUS_FORBIDDEN });
jest.spyOn(vm.store, 'removeGroup');
diff --git a/spec/frontend/groups/components/empty_states/archived_projects_empty_state_spec.js b/spec/frontend/groups/components/empty_states/archived_projects_empty_state_spec.js
index be61ffa92b4..bb3c0bc1526 100644
--- a/spec/frontend/groups/components/empty_states/archived_projects_empty_state_spec.js
+++ b/spec/frontend/groups/components/empty_states/archived_projects_empty_state_spec.js
@@ -6,7 +6,7 @@ import ArchivedProjectsEmptyState from '~/groups/components/empty_states/archive
let wrapper;
const defaultProvide = {
- newProjectIllustration: '/assets/illustrations/project-create-new-sm.svg',
+ emptyProjectsIllustration: '/assets/llustrations/empty-state/empty-projects-md.svg',
};
const createComponent = () => {
@@ -21,7 +21,7 @@ describe('ArchivedProjectsEmptyState', () => {
expect(wrapper.findComponent(GlEmptyState).props()).toMatchObject({
title: ArchivedProjectsEmptyState.i18n.title,
- svgPath: defaultProvide.newProjectIllustration,
+ svgPath: defaultProvide.emptyProjectsIllustration,
});
});
});
diff --git a/spec/frontend/groups/components/empty_states/shared_projects_empty_state_spec.js b/spec/frontend/groups/components/empty_states/shared_projects_empty_state_spec.js
index c4ace1be1f3..8ba1c480d5e 100644
--- a/spec/frontend/groups/components/empty_states/shared_projects_empty_state_spec.js
+++ b/spec/frontend/groups/components/empty_states/shared_projects_empty_state_spec.js
@@ -6,7 +6,7 @@ import SharedProjectsEmptyState from '~/groups/components/empty_states/shared_pr
let wrapper;
const defaultProvide = {
- newProjectIllustration: '/assets/illustrations/project-create-new-sm.svg',
+ emptyProjectsIllustration: '/assets/illustrations/empty-state/empty-projects-md.svg',
};
const createComponent = () => {
@@ -21,7 +21,7 @@ describe('SharedProjectsEmptyState', () => {
expect(wrapper.findComponent(GlEmptyState).props()).toMatchObject({
title: SharedProjectsEmptyState.i18n.title,
- svgPath: defaultProvide.newProjectIllustration,
+ svgPath: defaultProvide.emptyProjectsIllustration,
});
});
});
diff --git a/spec/frontend/groups/components/empty_states/subgroups_and_projects_empty_state_spec.js b/spec/frontend/groups/components/empty_states/subgroups_and_projects_empty_state_spec.js
index dc4271b98ee..5ae4d0be7d6 100644
--- a/spec/frontend/groups/components/empty_states/subgroups_and_projects_empty_state_spec.js
+++ b/spec/frontend/groups/components/empty_states/subgroups_and_projects_empty_state_spec.js
@@ -10,6 +10,7 @@ const defaultProvide = {
newProjectPath: '/projects/new?namespace_id=231',
newSubgroupIllustration: '/assets/illustrations/group-new.svg',
newSubgroupPath: '/groups/new?parent_id=231',
+ emptyProjectsIllustration: '/assets/illustrations/empty-state/empty-projects-md.svg',
emptySubgroupIllustration: '/assets/illustrations/empty-state/empty-subgroup-md.svg',
canCreateSubgroups: true,
canCreateProjects: true,
diff --git a/spec/frontend/groups/components/groups_spec.js b/spec/frontend/groups/components/groups_spec.js
index 9ee785d688a..c04eaa501ba 100644
--- a/spec/frontend/groups/components/groups_spec.js
+++ b/spec/frontend/groups/components/groups_spec.js
@@ -32,7 +32,7 @@ describe('GroupsComponent', () => {
const findPaginationLinks = () => wrapper.findComponent(PaginationLinks);
- beforeEach(async () => {
+ beforeEach(() => {
Vue.component('GroupFolder', GroupFolderComponent);
Vue.component('GroupItem', GroupItemComponent);
});
diff --git a/spec/frontend/groups/components/overview_tabs_spec.js b/spec/frontend/groups/components/overview_tabs_spec.js
index 906609c97f9..101dd06d578 100644
--- a/spec/frontend/groups/components/overview_tabs_spec.js
+++ b/spec/frontend/groups/components/overview_tabs_spec.js
@@ -39,6 +39,7 @@ describe('OverviewTabs', () => {
newProjectPath: 'projects/new',
newSubgroupIllustration: '',
newProjectIllustration: '',
+ emptyProjectsIllustration: '',
emptySubgroupIllustration: '',
canCreateSubgroups: false,
canCreateProjects: false,
diff --git a/spec/frontend/groups/components/transfer_group_form_spec.js b/spec/frontend/groups/components/transfer_group_form_spec.js
index fd0c3907e04..4d4de1ae3d5 100644
--- a/spec/frontend/groups/components/transfer_group_form_spec.js
+++ b/spec/frontend/groups/components/transfer_group_form_spec.js
@@ -69,7 +69,7 @@ describe('Transfer group form', () => {
expect(findHiddenInput().attributes('value')).toBeUndefined();
});
- it('does not render the alert message', () => {
+ it('does not render the alert', () => {
expect(findAlert().exists()).toBe(false);
});
diff --git a/spec/frontend/groups/settings/components/group_settings_readme_spec.js b/spec/frontend/groups/settings/components/group_settings_readme_spec.js
new file mode 100644
index 00000000000..8d4da73934f
--- /dev/null
+++ b/spec/frontend/groups/settings/components/group_settings_readme_spec.js
@@ -0,0 +1,112 @@
+import { GlModal, GlSprintf } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import GroupSettingsReadme from '~/groups/settings/components/group_settings_readme.vue';
+import { GITLAB_README_PROJECT } from '~/groups/settings/constants';
+import {
+ MOCK_GROUP_PATH,
+ MOCK_GROUP_ID,
+ MOCK_PATH_TO_GROUP_README,
+ MOCK_PATH_TO_README_PROJECT,
+} from '../mock_data';
+
+describe('GroupSettingsReadme', () => {
+ let wrapper;
+
+ const defaultProps = {
+ groupPath: MOCK_GROUP_PATH,
+ groupId: MOCK_GROUP_ID,
+ };
+
+ const createComponent = (props = {}) => {
+ wrapper = shallowMountExtended(GroupSettingsReadme, {
+ propsData: {
+ ...defaultProps,
+ ...props,
+ },
+ stubs: {
+ GlModal,
+ GlSprintf,
+ },
+ });
+ };
+
+ const findHasReadmeButtonLink = () => wrapper.findByText('README');
+ const findAddReadmeButton = () => wrapper.findByTestId('group-settings-add-readme-button');
+ const findModalBody = () => wrapper.findByTestId('group-settings-modal-readme-body');
+ const findModalCreateReadmeButton = () =>
+ wrapper.findByTestId('group-settings-modal-create-readme-button');
+
+ describe('Group has existing README', () => {
+ beforeEach(() => {
+ createComponent({
+ groupReadmePath: MOCK_PATH_TO_GROUP_README,
+ readmeProjectPath: MOCK_PATH_TO_README_PROJECT,
+ });
+ });
+
+ describe('template', () => {
+ it('renders README Button Link with correct path and text', () => {
+ expect(findHasReadmeButtonLink().exists()).toBe(true);
+ expect(findHasReadmeButtonLink().attributes('href')).toBe(MOCK_PATH_TO_GROUP_README);
+ });
+
+ it('does not render Add README Button', () => {
+ expect(findAddReadmeButton().exists()).toBe(false);
+ });
+ });
+ });
+
+ describe('Group has README project without README file', () => {
+ beforeEach(() => {
+ createComponent({ readmeProjectPath: MOCK_PATH_TO_README_PROJECT });
+ });
+
+ describe('template', () => {
+ it('does not render README', () => {
+ expect(findHasReadmeButtonLink().exists()).toBe(false);
+ });
+
+ it('does render Add Readme Button with correct text', () => {
+ expect(findAddReadmeButton().exists()).toBe(true);
+ expect(findAddReadmeButton().text()).toBe('Add README');
+ });
+
+ it('generates a hidden modal with correct body text', () => {
+ expect(findModalBody().text()).toMatchInterpolatedText(
+ `This will create a README.md for project ${MOCK_PATH_TO_README_PROJECT}.`,
+ );
+ });
+
+ it('generates a hidden modal with correct button text', () => {
+ expect(findModalCreateReadmeButton().text()).toBe('Add README');
+ });
+ });
+ });
+
+ describe('Group does not have README project', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ describe('template', () => {
+ it('does not render README', () => {
+ expect(findHasReadmeButtonLink().exists()).toBe(false);
+ });
+
+ it('does render Add Readme Button with correct text', () => {
+ expect(findAddReadmeButton().exists()).toBe(true);
+ expect(findAddReadmeButton().text()).toBe('Add README');
+ });
+
+ it('generates a hidden modal with correct body text', () => {
+ expect(findModalBody().text()).toMatchInterpolatedText(
+ `This will create a project ${MOCK_GROUP_PATH}/${GITLAB_README_PROJECT} and add a README.md.`,
+ );
+ });
+
+ it('generates a hidden modal with correct button text', () => {
+ expect(findModalCreateReadmeButton().text()).toBe('Create and add README');
+ });
+ });
+ });
+});
diff --git a/spec/frontend/groups/settings/mock_data.js b/spec/frontend/groups/settings/mock_data.js
new file mode 100644
index 00000000000..4551ee3318b
--- /dev/null
+++ b/spec/frontend/groups/settings/mock_data.js
@@ -0,0 +1,6 @@
+export const MOCK_GROUP_PATH = 'test-group';
+export const MOCK_GROUP_ID = '999';
+
+export const MOCK_PATH_TO_GROUP_README = '/group/project/-/blob/main/README.md';
+
+export const MOCK_PATH_TO_README_PROJECT = 'group/project';
diff --git a/spec/frontend/header_search/components/app_spec.js b/spec/frontend/header_search/components/app_spec.js
index 8e84c672d90..ad56b2dde24 100644
--- a/spec/frontend/header_search/components/app_spec.js
+++ b/spec/frontend/header_search/components/app_spec.js
@@ -20,6 +20,7 @@ import {
IS_NOT_FOCUSED,
IS_FOCUSED,
SEARCH_SHORTCUTS_MIN_CHARACTERS,
+ DROPDOWN_CLOSE_TIMEOUT,
} from '~/header_search/constants';
import DropdownKeyboardNavigation from '~/vue_shared/components/dropdown_keyboard_navigation.vue';
import { ENTER_KEY } from '~/lib/utils/keys';
@@ -43,6 +44,9 @@ jest.mock('~/lib/utils/url_utility', () => ({
describe('HeaderSearchApp', () => {
let wrapper;
+ jest.useFakeTimers();
+ jest.spyOn(global, 'setTimeout');
+
const actionSpies = {
setSearch: jest.fn(),
fetchAutocompleteOptions: jest.fn(),
@@ -131,7 +135,7 @@ describe('HeaderSearchApp', () => {
beforeEach(() => {
window.gon.current_username = username;
createComponent();
- findHeaderSearchInput().vm.$emit(showDropdown ? 'click' : '');
+ findHeaderSearchInput().vm.$emit(showDropdown ? 'focusin' : '');
});
it(`should${showSearchDropdown ? '' : ' not'} render`, () => {
@@ -153,7 +157,7 @@ describe('HeaderSearchApp', () => {
beforeEach(() => {
window.gon.current_username = MOCK_USERNAME;
createComponent({ search }, {});
- findHeaderSearchInput().vm.$emit('click');
+ findHeaderSearchInput().vm.$emit('focusin');
});
it(`should${showDefault ? '' : ' not'} render the Default Dropdown Items`, () => {
@@ -174,6 +178,7 @@ describe('HeaderSearchApp', () => {
it(`should close the dropdown when press escape key`, async () => {
findHeaderSearchInput().vm.$emit('keydown', new KeyboardEvent({ key: 27 }));
+ jest.runAllTimers();
await nextTick();
expect(findHeaderSearchDropdown().exists()).toBe(false);
expect(wrapper.emitted().expandSearchBar.length).toBe(1);
@@ -192,7 +197,7 @@ describe('HeaderSearchApp', () => {
beforeEach(() => {
window.gon.current_username = username;
createComponent();
- findHeaderSearchInput().vm.$emit(showDropdown ? 'click' : '');
+ findHeaderSearchInput().vm.$emit(showDropdown ? 'focusin' : '');
});
it(`sets description to ${expectedDesc}`, () => {
@@ -224,7 +229,7 @@ describe('HeaderSearchApp', () => {
searchOptions: () => searchOptions,
},
);
- findHeaderSearchInput().vm.$emit(showDropdown ? 'click' : '');
+ findHeaderSearchInput().vm.$emit(showDropdown ? 'focusin' : '');
});
it(`sets description to ${expectedDesc}`, () => {
@@ -253,7 +258,7 @@ describe('HeaderSearchApp', () => {
searchOptions: () => searchOptions,
},
);
- findHeaderSearchInput().vm.$emit('click');
+ findHeaderSearchInput().vm.$emit('focusin');
});
it(`${hasToken ? 'is' : 'is NOT'} rendered when data set has type "${
@@ -287,7 +292,7 @@ describe('HeaderSearchApp', () => {
window.gon.current_username = MOCK_USERNAME;
createComponent({ search, searchContext }, { searchOptions: () => searchOptions });
if (isFocused) {
- findHeaderSearchInput().vm.$emit('click');
+ findHeaderSearchInput().vm.$emit('focusin');
}
});
@@ -328,7 +333,7 @@ describe('HeaderSearchApp', () => {
searchOptions: () => searchOptions,
},
);
- findHeaderSearchInput().vm.$emit('click');
+ findHeaderSearchInput().vm.$emit('focusin');
});
it(`icon for data set type "${searchOptions[0]?.html_id}" ${
@@ -349,12 +354,12 @@ describe('HeaderSearchApp', () => {
});
describe('events', () => {
- beforeEach(() => {
- window.gon.current_username = MOCK_USERNAME;
- createComponent();
- });
-
describe('Header Search Input', () => {
+ beforeEach(() => {
+ window.gon.current_username = MOCK_USERNAME;
+ createComponent();
+ });
+
describe('when dropdown is closed', () => {
let trackingSpy;
@@ -362,9 +367,9 @@ describe('HeaderSearchApp', () => {
trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
});
- it('onFocus opens dropdown and triggers snowplow event', async () => {
+ it('onFocusin opens dropdown and triggers snowplow event', async () => {
expect(findHeaderSearchDropdown().exists()).toBe(false);
- findHeaderSearchInput().vm.$emit('focus');
+ findHeaderSearchInput().vm.$emit('focusin');
await nextTick();
@@ -375,25 +380,19 @@ describe('HeaderSearchApp', () => {
});
});
- it('onClick opens dropdown and triggers snowplow event', async () => {
+ it('onFocusout closes dropdown and triggers snowplow event', async () => {
expect(findHeaderSearchDropdown().exists()).toBe(false);
- findHeaderSearchInput().vm.$emit('click');
+ findHeaderSearchInput().vm.$emit('focusout');
+ jest.runAllTimers();
await nextTick();
- expect(findHeaderSearchDropdown().exists()).toBe(true);
- expect(trackingSpy).toHaveBeenCalledWith(undefined, 'focus_input', {
+ expect(findHeaderSearchDropdown().exists()).toBe(false);
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, 'blur_input', {
label: 'global_search',
property: 'navigation_top',
});
});
-
- it('onClick followed by onFocus only triggers a single snowplow event', async () => {
- findHeaderSearchInput().vm.$emit('click');
- findHeaderSearchInput().vm.$emit('focus');
-
- expect(trackingSpy).toHaveBeenCalledTimes(1);
- });
});
describe('onInput', () => {
@@ -435,18 +434,18 @@ describe('HeaderSearchApp', () => {
});
});
- describe('Dropdown Keyboard Navigation', () => {
+ describe('onFocusout dropdown', () => {
beforeEach(() => {
- findHeaderSearchInput().vm.$emit('click');
+ window.gon.current_username = MOCK_USERNAME;
+ createComponent({ search: 'tes' }, {});
+ findHeaderSearchInput().vm.$emit('focusin');
});
- it('closes dropdown when @tab is emitted', async () => {
- expect(findHeaderSearchDropdown().exists()).toBe(true);
- findDropdownKeyboardNavigation().vm.$emit('tab');
-
- await nextTick();
+ it('closes with timeout so click event gets emited', () => {
+ findHeaderSearchInput().vm.$emit('focusout');
- expect(findHeaderSearchDropdown().exists()).toBe(false);
+ expect(setTimeout).toHaveBeenCalledTimes(1);
+ expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), DROPDOWN_CLOSE_TIMEOUT);
});
});
});
@@ -461,7 +460,7 @@ describe('HeaderSearchApp', () => {
beforeEach(() => {
window.gon.current_username = MOCK_USERNAME;
createComponent({ search });
- findHeaderSearchInput().vm.$emit('click');
+ findHeaderSearchInput().vm.$emit('focusin');
});
it(`when currentFocusIndex changes to ${MOCK_INDEX} updates the data to searchOptions[${MOCK_INDEX}]`, () => {
@@ -502,10 +501,11 @@ describe('HeaderSearchApp', () => {
beforeEach(() => {
window.gon.current_username = MOCK_USERNAME;
createComponent();
- findHeaderSearchInput().vm.$emit('click');
+ findHeaderSearchInput().vm.$emit('focusin');
});
- it('onKey-enter clicks the selected dropdown item rather than submitting a search', () => {
+ it('onKey-enter clicks the selected dropdown item rather than submitting a search', async () => {
+ await nextTick();
findDropdownKeyboardNavigation().vm.$emit('change', MOCK_INDEX);
findHeaderSearchInput().vm.$emit('keydown', new KeyboardEvent({ key: ENTER_KEY }));
diff --git a/spec/frontend/header_search/init_spec.js b/spec/frontend/header_search/init_spec.js
index 40c1843d461..9ccc6919b81 100644
--- a/spec/frontend/header_search/init_spec.js
+++ b/spec/frontend/header_search/init_spec.js
@@ -33,7 +33,6 @@ describe('Header Search EventListener', () => {
jest.mock('~/header_search', () => ({ initHeaderSearchApp: jest.fn() }));
await eventHandler.apply(
{
- newHeaderSearchFeatureFlag: true,
searchInputBox: document.querySelector('#search'),
},
[cleanEventListeners],
@@ -47,7 +46,6 @@ describe('Header Search EventListener', () => {
jest.mock('~/header_search', () => ({ initHeaderSearchApp: mockVueApp }));
await eventHandler.apply(
{
- newHeaderSearchFeatureFlag: true,
searchInputBox: document.querySelector('#search'),
},
() => {},
@@ -55,20 +53,4 @@ describe('Header Search EventListener', () => {
expect(mockVueApp).toHaveBeenCalled();
});
-
- it('attaches old vue dropdown when feature flag is disabled', async () => {
- const mockLegacyApp = jest.fn(() => ({
- onSearchInputFocus: jest.fn(),
- }));
- jest.mock('~/search_autocomplete', () => mockLegacyApp);
- await eventHandler.apply(
- {
- newHeaderSearchFeatureFlag: false,
- searchInputBox: document.querySelector('#search'),
- },
- () => {},
- );
-
- expect(mockLegacyApp).toHaveBeenCalled();
- });
});
diff --git a/spec/frontend/helpers/init_simple_app_helper_spec.js b/spec/frontend/helpers/init_simple_app_helper_spec.js
index 8dd3745e0ac..7938e3851d0 100644
--- a/spec/frontend/helpers/init_simple_app_helper_spec.js
+++ b/spec/frontend/helpers/init_simple_app_helper_spec.js
@@ -38,19 +38,19 @@ describe('helpers/init_simple_app_helper/initSimpleApp', () => {
resetHTMLFixture();
});
- it('mounts the component if the selector exists', async () => {
+ it('mounts the component if the selector exists', () => {
initMock('<div id="mount-here"></div>');
expect(findMock().exists()).toBe(true);
});
- it('does not mount the component if selector does not exist', async () => {
+ it('does not mount the component if selector does not exist', () => {
initMock('<div id="do-not-mount-here"></div>');
expect(didCreateApp()).toBe(false);
});
- it('passes the prop to the component if the prop exists', async () => {
+ it('passes the prop to the component if the prop exists', () => {
initMock(`<div id="mount-here" data-view-model={"someKey":"thing","count":123}></div>`);
expect(findMock().props()).toEqual({
diff --git a/spec/frontend/ide/components/activity_bar_spec.js b/spec/frontend/ide/components/activity_bar_spec.js
index ff04f9a84f1..95582aca8fd 100644
--- a/spec/frontend/ide/components/activity_bar_spec.js
+++ b/spec/frontend/ide/components/activity_bar_spec.js
@@ -1,14 +1,20 @@
+import { nextTick } from 'vue';
import { GlBadge } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import ActivityBar from '~/ide/components/activity_bar.vue';
import { leftSidebarViews } from '~/ide/constants';
import { createStore } from '~/ide/stores';
+const { edit, ...VIEW_OBJECTS_WITHOUT_EDIT } = leftSidebarViews;
+const MODES_WITHOUT_EDIT = Object.keys(VIEW_OBJECTS_WITHOUT_EDIT);
+const MODES = Object.keys(leftSidebarViews);
+
describe('IDE ActivityBar component', () => {
let wrapper;
let store;
const findChangesBadge = () => wrapper.findComponent(GlBadge);
+ const findModeButton = (mode) => wrapper.findByTestId(`${mode}-mode-button`);
const mountComponent = (state) => {
store = createStore();
@@ -19,45 +25,43 @@ describe('IDE ActivityBar component', () => {
...state,
});
- wrapper = shallowMount(ActivityBar, { store });
+ wrapper = shallowMountExtended(ActivityBar, { store });
};
- describe('updateActivityBarView', () => {
- beforeEach(() => {
- mountComponent();
- jest.spyOn(wrapper.vm, 'updateActivityBarView').mockImplementation(() => {});
- });
+ describe('active item', () => {
+ // Test that mode button does not have 'active' class before click,
+ // and does have 'active' class after click
+ const testSettingActiveItem = async (mode) => {
+ const button = findModeButton(mode);
- it('calls updateActivityBarView with edit value on click', () => {
- wrapper.find('.js-ide-edit-mode').trigger('click');
+ expect(button.classes('active')).toBe(false);
- expect(wrapper.vm.updateActivityBarView).toHaveBeenCalledWith(leftSidebarViews.edit.name);
- });
+ button.trigger('click');
+ await nextTick();
- it('calls updateActivityBarView with commit value on click', () => {
- wrapper.find('.js-ide-commit-mode').trigger('click');
+ expect(button.classes('active')).toBe(true);
+ };
- expect(wrapper.vm.updateActivityBarView).toHaveBeenCalledWith(leftSidebarViews.commit.name);
- });
+ it.each(MODES)('is initially set to %s mode', (mode) => {
+ mountComponent({ currentActivityView: leftSidebarViews[mode].name });
- it('calls updateActivityBarView with review value on click', () => {
- wrapper.find('.js-ide-review-mode').trigger('click');
+ const button = findModeButton(mode);
- expect(wrapper.vm.updateActivityBarView).toHaveBeenCalledWith(leftSidebarViews.review.name);
+ expect(button.classes('active')).toBe(true);
});
- });
- describe('active item', () => {
- it('sets edit item active', () => {
+ it.each(MODES_WITHOUT_EDIT)('is correctly set after clicking %s mode button', (mode) => {
mountComponent();
- expect(wrapper.find('.js-ide-edit-mode').classes()).toContain('active');
+ testSettingActiveItem(mode);
});
- it('sets commit item active', () => {
- mountComponent({ currentActivityView: leftSidebarViews.commit.name });
+ it('is correctly set after clicking edit mode button', () => {
+ // The default currentActivityView is leftSidebarViews.edit.name,
+ // so for the 'edit' mode, we pass a different currentActivityView.
+ mountComponent({ currentActivityView: leftSidebarViews.review.name });
- expect(wrapper.find('.js-ide-commit-mode').classes()).toContain('active');
+ testSettingActiveItem('edit');
});
});
@@ -65,7 +69,6 @@ describe('IDE ActivityBar component', () => {
it('is rendered when files are staged', () => {
mountComponent({ stagedFiles: [{ path: '/path/to/file' }] });
- expect(findChangesBadge().exists()).toBe(true);
expect(findChangesBadge().text()).toBe('1');
});
diff --git a/spec/frontend/ide/components/cannot_push_code_alert_spec.js b/spec/frontend/ide/components/cannot_push_code_alert_spec.js
index d4db2246008..c72d8c5fccd 100644
--- a/spec/frontend/ide/components/cannot_push_code_alert_spec.js
+++ b/spec/frontend/ide/components/cannot_push_code_alert_spec.js
@@ -45,7 +45,7 @@ describe('ide/components/cannot_push_code_alert', () => {
createComponent();
});
- it('shows alert with message', () => {
+ it('shows an alert with message', () => {
expect(findAlert().props()).toMatchObject({ dismissible: false });
expect(findAlert().text()).toBe(TEST_MESSAGE);
});
diff --git a/spec/frontend/ide/components/commit_sidebar/form_spec.js b/spec/frontend/ide/components/commit_sidebar/form_spec.js
index 0c0998c037a..04dd81d9fda 100644
--- a/spec/frontend/ide/components/commit_sidebar/form_spec.js
+++ b/spec/frontend/ide/components/commit_sidebar/form_spec.js
@@ -21,6 +21,7 @@ import { COMMIT_TO_NEW_BRANCH } from '~/ide/stores/modules/commit/constants';
describe('IDE commit form', () => {
let wrapper;
let store;
+ const showModalSpy = jest.fn();
const createComponent = () => {
wrapper = shallowMount(CommitForm, {
@@ -29,7 +30,11 @@ describe('IDE commit form', () => {
GlTooltip: createMockDirective('gl-tooltip'),
},
stubs: {
- GlModal: stubComponent(GlModal),
+ GlModal: stubComponent(GlModal, {
+ methods: {
+ show: showModalSpy,
+ },
+ }),
},
});
};
@@ -57,6 +62,7 @@ describe('IDE commit form', () => {
tooltip: getBinding(findCommitButtonTooltip().element, 'gl-tooltip').value.title,
});
const findForm = () => wrapper.find('form');
+ const findModal = () => wrapper.findComponent(GlModal);
const submitForm = () => findForm().trigger('submit');
const findCommitMessageInput = () => wrapper.findComponent(CommitMessageField);
const setCommitMessageInput = (val) => findCommitMessageInput().vm.$emit('input', val);
@@ -83,7 +89,7 @@ describe('IDE commit form', () => {
${'when user cannot push'} | ${['test']} | ${{ pushCode: false }} | ${goToEditView} | ${findBeginCommitButtonData} | ${true} | ${MSG_CANNOT_PUSH_CODE}
${'when user cannot push'} | ${['test']} | ${{ pushCode: false }} | ${goToCommitView} | ${findCommitButtonData} | ${true} | ${MSG_CANNOT_PUSH_CODE}
`('$desc', ({ stagedFiles, userPermissions, viewFn, buttonFn, disabled, tooltip }) => {
- beforeEach(async () => {
+ beforeEach(() => {
store.state.stagedFiles = stagedFiles;
store.state.projects.abcproject.userPermissions = userPermissions;
@@ -298,22 +304,19 @@ describe('IDE commit form', () => {
${() => createCodeownersCommitError('test message')} | ${{ actionPrimary: { text: 'Create new branch' } }}
${createUnexpectedCommitError} | ${{ actionPrimary: null }}
`('opens error modal if commitError with $error', async ({ createError, props }) => {
- const modal = wrapper.findComponent(GlModal);
- modal.vm.show = jest.fn();
-
const error = createError();
store.state.commit.commitError = error;
await nextTick();
- expect(modal.vm.show).toHaveBeenCalled();
- expect(modal.props()).toMatchObject({
+ expect(showModalSpy).toHaveBeenCalled();
+ expect(findModal().props()).toMatchObject({
actionCancel: { text: 'Cancel' },
...props,
});
// Because of the legacy 'mountComponent' approach here, the only way to
// test the text of the modal is by viewing the content of the modal added to the document.
- expect(modal.html()).toContain(error.messageHTML);
+ expect(findModal().html()).toContain(error.messageHTML);
});
});
@@ -339,7 +342,7 @@ describe('IDE commit form', () => {
await nextTick();
- wrapper.findComponent(GlModal).vm.$emit('ok');
+ findModal().vm.$emit('ok');
await waitForPromises();
diff --git a/spec/frontend/ide/components/commit_sidebar/list_spec.js b/spec/frontend/ide/components/commit_sidebar/list_spec.js
index 6b9ba939a87..c0b0cb0b732 100644
--- a/spec/frontend/ide/components/commit_sidebar/list_spec.js
+++ b/spec/frontend/ide/components/commit_sidebar/list_spec.js
@@ -20,7 +20,7 @@ describe('Multi-file editor commit sidebar list', () => {
});
describe('with a list of files', () => {
- beforeEach(async () => {
+ beforeEach(() => {
const f = file('file name');
f.changed = true;
wrapper = mountComponent({ fileList: [f] });
diff --git a/spec/frontend/ide/components/ide_review_spec.js b/spec/frontend/ide/components/ide_review_spec.js
index e6fd018969f..7ae8cfac935 100644
--- a/spec/frontend/ide/components/ide_review_spec.js
+++ b/spec/frontend/ide/components/ide_review_spec.js
@@ -63,7 +63,7 @@ describe('IDE review mode', () => {
await wrapper.vm.reactivate();
});
- it('updates viewer to "mrdiff"', async () => {
+ it('updates viewer to "mrdiff"', () => {
expect(store.state.viewer).toBe('mrdiff');
});
});
diff --git a/spec/frontend/ide/components/ide_spec.js b/spec/frontend/ide/components/ide_spec.js
index 1c8d570cdce..f2a684ab65e 100644
--- a/spec/frontend/ide/components/ide_spec.js
+++ b/spec/frontend/ide/components/ide_spec.js
@@ -64,7 +64,7 @@ describe('WebIDE', () => {
});
});
- it('renders "New file" button in empty repo', async () => {
+ it('renders "New file" button in empty repo', () => {
expect(wrapper.find('[title="New file"]').exists()).toBe(true);
});
});
@@ -169,7 +169,7 @@ describe('WebIDE', () => {
});
});
- it('when user cannot push code, shows alert', () => {
+ it('when user cannot push code, shows an alert', () => {
store.state.links = {
forkInfo: {
ide_path: TEST_FORK_IDE_PATH,
diff --git a/spec/frontend/ide/components/ide_tree_spec.js b/spec/frontend/ide/components/ide_tree_spec.js
index 9f452910496..bcfa6809eca 100644
--- a/spec/frontend/ide/components/ide_tree_spec.js
+++ b/spec/frontend/ide/components/ide_tree_spec.js
@@ -1,9 +1,9 @@
import { mount } from '@vue/test-utils';
import Vue from 'vue';
import Vuex from 'vuex';
-import { keepAlive } from 'helpers/keep_alive_component_helper';
+import { viewerTypes } from '~/ide/constants';
import IdeTree from '~/ide/components/ide_tree.vue';
-import { createStore } from '~/ide/stores';
+import { createStoreOptions } from '~/ide/stores';
import { file } from '../helpers';
import { projectData } from '../mock_data';
@@ -13,42 +13,72 @@ describe('IdeTree', () => {
let store;
let wrapper;
- beforeEach(() => {
- store = createStore();
-
- store.state.currentProjectId = 'abcproject';
- store.state.currentBranchId = 'main';
- store.state.projects.abcproject = { ...projectData };
- Vue.set(store.state.trees, 'abcproject/main', {
- tree: [file('fileName')],
- loading: false,
+ const actionSpies = {
+ updateViewer: jest.fn(),
+ };
+
+ const testState = {
+ currentProjectId: 'abcproject',
+ currentBranchId: 'main',
+ projects: {
+ abcproject: { ...projectData },
+ },
+ trees: {
+ 'abcproject/main': {
+ tree: [file('fileName')],
+ loading: false,
+ },
+ },
+ };
+
+ const createComponent = (replaceState) => {
+ const defaultStore = createStoreOptions();
+
+ store = new Vuex.Store({
+ ...defaultStore,
+ state: {
+ ...defaultStore.state,
+ ...testState,
+ replaceState,
+ },
+ actions: {
+ ...defaultStore.actions,
+ ...actionSpies,
+ },
});
- wrapper = mount(keepAlive(IdeTree), {
+ wrapper = mount(IdeTree, {
store,
});
- });
+ };
- it('renders list of files', () => {
- expect(wrapper.text()).toContain('fileName');
+ beforeEach(() => {
+ createComponent();
});
- describe('activated', () => {
- let inititializeSpy;
+ afterEach(() => {
+ actionSpies.updateViewer.mockClear();
+ });
- beforeEach(async () => {
- inititializeSpy = jest.spyOn(wrapper.findComponent(IdeTree).vm, 'initialize');
- store.state.viewer = 'diff';
+ describe('renders properly', () => {
+ it('renders list of files', () => {
+ expect(wrapper.text()).toContain('fileName');
+ });
+ });
- await wrapper.vm.reactivate();
+ describe('activated', () => {
+ beforeEach(() => {
+ createComponent({
+ viewer: viewerTypes.diff,
+ });
});
it('re initializes the component', () => {
- expect(inititializeSpy).toHaveBeenCalled();
+ expect(actionSpies.updateViewer).toHaveBeenCalled();
});
it('updates viewer to "editor" by default', () => {
- expect(store.state.viewer).toBe('editor');
+ expect(actionSpies.updateViewer).toHaveBeenCalledWith(expect.any(Object), viewerTypes.edit);
});
});
});
diff --git a/spec/frontend/ide/components/new_dropdown/index_spec.js b/spec/frontend/ide/components/new_dropdown/index_spec.js
index 01dcb174c41..a2371abe955 100644
--- a/spec/frontend/ide/components/new_dropdown/index_spec.js
+++ b/spec/frontend/ide/components/new_dropdown/index_spec.js
@@ -1,33 +1,50 @@
-import { mount } from '@vue/test-utils';
+import Vue from 'vue';
+import Vuex from 'vuex';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
import NewDropdown from '~/ide/components/new_dropdown/index.vue';
import Button from '~/ide/components/new_dropdown/button.vue';
-import { createStore } from '~/ide/stores';
+import Modal from '~/ide/components/new_dropdown/modal.vue';
+import { stubComponent } from 'helpers/stub_component';
+
+Vue.use(Vuex);
describe('new dropdown component', () => {
let wrapper;
+ const openMock = jest.fn();
+ const deleteEntryMock = jest.fn();
const findAllButtons = () => wrapper.findAllComponents(Button);
- const mountComponent = () => {
- const store = createStore();
- store.state.currentProjectId = 'abcproject';
- store.state.path = '';
- store.state.trees['abcproject/mybranch'] = { tree: [] };
+ const mountComponent = (props = {}) => {
+ const fakeStore = () => {
+ return new Vuex.Store({
+ actions: {
+ deleteEntry: deleteEntryMock,
+ },
+ });
+ };
- wrapper = mount(NewDropdown, {
- store,
+ wrapper = mountExtended(NewDropdown, {
+ store: fakeStore(),
propsData: {
branch: 'main',
path: '',
mouseOver: false,
type: 'tree',
+ ...props,
+ },
+ stubs: {
+ NewModal: stubComponent(Modal, {
+ methods: {
+ open: openMock,
+ },
+ }),
},
});
};
beforeEach(() => {
mountComponent();
- jest.spyOn(wrapper.vm.$refs.newModal, 'open').mockImplementation(() => {});
});
it('renders new file, upload and new directory links', () => {
@@ -38,37 +55,34 @@ describe('new dropdown component', () => {
describe('createNewItem', () => {
it('opens modal for a blob when new file is clicked', () => {
- findAllButtons().at(0).trigger('click');
+ findAllButtons().at(0).vm.$emit('click');
- expect(wrapper.vm.$refs.newModal.open).toHaveBeenCalledWith('blob', '');
+ expect(openMock).toHaveBeenCalledWith('blob', '');
});
it('opens modal for a tree when new directory is clicked', () => {
- findAllButtons().at(2).trigger('click');
+ findAllButtons().at(2).vm.$emit('click');
- expect(wrapper.vm.$refs.newModal.open).toHaveBeenCalledWith('tree', '');
+ expect(openMock).toHaveBeenCalledWith('tree', '');
});
});
describe('isOpen', () => {
it('scrolls dropdown into view', async () => {
- jest.spyOn(wrapper.vm.$refs.dropdownMenu, 'scrollIntoView').mockImplementation(() => {});
+ const dropdownMenu = wrapper.findByTestId('dropdown-menu');
+ const scrollIntoViewSpy = jest.spyOn(dropdownMenu.element, 'scrollIntoView');
await wrapper.setProps({ isOpen: true });
- expect(wrapper.vm.$refs.dropdownMenu.scrollIntoView).toHaveBeenCalledWith({
- block: 'nearest',
- });
+ expect(scrollIntoViewSpy).toHaveBeenCalledWith({ block: 'nearest' });
});
});
describe('delete entry', () => {
it('calls delete action', () => {
- jest.spyOn(wrapper.vm, 'deleteEntry').mockImplementation(() => {});
-
findAllButtons().at(4).trigger('click');
- expect(wrapper.vm.deleteEntry).toHaveBeenCalledWith('');
+ expect(deleteEntryMock).toHaveBeenCalledWith(expect.anything(), '');
});
});
});
diff --git a/spec/frontend/ide/components/repo_commit_section_spec.js b/spec/frontend/ide/components/repo_commit_section_spec.js
index 92bb645b1c0..ead609421b7 100644
--- a/spec/frontend/ide/components/repo_commit_section_spec.js
+++ b/spec/frontend/ide/components/repo_commit_section_spec.js
@@ -157,21 +157,4 @@ describe('RepoCommitSection', () => {
expect(wrapper.findComponent(EmptyState).exists()).toBe(false);
});
});
-
- describe('activated', () => {
- let inititializeSpy;
-
- beforeEach(async () => {
- createComponent();
-
- inititializeSpy = jest.spyOn(wrapper.findComponent(RepoCommitSection).vm, 'initialize');
- store.state.viewer = 'diff';
-
- await wrapper.vm.reactivate();
- });
-
- it('re initializes the component', () => {
- expect(inititializeSpy).toHaveBeenCalled();
- });
- });
});
diff --git a/spec/frontend/ide/components/repo_editor_spec.js b/spec/frontend/ide/components/repo_editor_spec.js
index 9253bfc7e71..6747ec97050 100644
--- a/spec/frontend/ide/components/repo_editor_spec.js
+++ b/spec/frontend/ide/components/repo_editor_spec.js
@@ -293,7 +293,7 @@ describe('RepoEditor', () => {
});
describe('when file changes to non-markdown file', () => {
- beforeEach(async () => {
+ beforeEach(() => {
wrapper.setProps({ file: dummyFile.empty });
});
@@ -601,7 +601,7 @@ describe('RepoEditor', () => {
const f = createRemoteFile('newFile');
Vue.set(vm.$store.state.entries, f.path, f);
- jest.spyOn(service, 'getRawFileData').mockImplementation(async () => {
+ jest.spyOn(service, 'getRawFileData').mockImplementation(() => {
expect(vm.file.loading).toBe(true);
// switching from edit to diff mode usually triggers editor initialization
@@ -609,7 +609,7 @@ describe('RepoEditor', () => {
jest.runOnlyPendingTimers();
- return 'rawFileData123\n';
+ return Promise.resolve('rawFileData123\n');
});
wrapper.setProps({
@@ -630,18 +630,18 @@ describe('RepoEditor', () => {
jest
.spyOn(service, 'getRawFileData')
- .mockImplementation(async () => {
+ .mockImplementation(() => {
// opening fileB while the content of fileA is still being fetched
wrapper.setProps({
file: fileB,
});
- return aContent;
+ return Promise.resolve(aContent);
})
- .mockImplementationOnce(async () => {
+ .mockImplementationOnce(() => {
// we delay returning fileB content
// to make sure the editor doesn't initialize prematurely
jest.advanceTimersByTime(30);
- return bContent;
+ return Promise.resolve(bContent);
});
wrapper.setProps({
diff --git a/spec/frontend/ide/components/shared/commit_message_field_spec.js b/spec/frontend/ide/components/shared/commit_message_field_spec.js
index 186b1997497..ccf544b27b7 100644
--- a/spec/frontend/ide/components/shared/commit_message_field_spec.js
+++ b/spec/frontend/ide/components/shared/commit_message_field_spec.js
@@ -50,7 +50,7 @@ describe('CommitMessageField', () => {
await nextTick();
});
- it('is added on textarea focus', async () => {
+ it('is added on textarea focus', () => {
expect(wrapper.attributes('class')).toEqual(
expect.stringContaining('gl-outline-none! gl-focus-ring-border-1-gray-900!'),
);
diff --git a/spec/frontend/ide/init_gitlab_web_ide_spec.js b/spec/frontend/ide/init_gitlab_web_ide_spec.js
index bfc87f17092..2666f06e8d8 100644
--- a/spec/frontend/ide/init_gitlab_web_ide_spec.js
+++ b/spec/frontend/ide/init_gitlab_web_ide_spec.js
@@ -26,6 +26,7 @@ const TEST_GITLAB_WEB_IDE_PUBLIC_PATH = 'test/webpack/assets/gitlab-web-ide/publ
const TEST_FILE_PATH = 'foo/README.md';
const TEST_MR_ID = '7';
const TEST_MR_TARGET_PROJECT = 'gitlab-org/the-real-gitlab';
+const TEST_SIGN_IN_PATH = 'sign-in';
const TEST_FORK_INFO = { fork_path: '/forky' };
const TEST_IDE_REMOTE_PATH = '/-/ide/remote/:remote_host/:remote_path';
const TEST_START_REMOTE_PARAMS = {
@@ -56,6 +57,7 @@ describe('ide/init_gitlab_web_ide', () => {
el.dataset.editorFontSrcUrl = TEST_EDITOR_FONT_SRC_URL;
el.dataset.editorFontFormat = TEST_EDITOR_FONT_FORMAT;
el.dataset.editorFontFamily = TEST_EDITOR_FONT_FAMILY;
+ el.dataset.signInPath = TEST_SIGN_IN_PATH;
document.body.append(el);
};
@@ -109,6 +111,7 @@ describe('ide/init_gitlab_web_ide', () => {
links: {
userPreferences: TEST_USER_PREFERENCES_PATH,
feedbackIssue: GITLAB_WEB_IDE_FEEDBACK_ISSUE,
+ signIn: TEST_SIGN_IN_PATH,
},
editorFont: {
srcUrl: TEST_EDITOR_FONT_SRC_URL,
diff --git a/spec/frontend/ide/lib/languages/codeowners_spec.js b/spec/frontend/ide/lib/languages/codeowners_spec.js
new file mode 100644
index 00000000000..aa0ab123c4b
--- /dev/null
+++ b/spec/frontend/ide/lib/languages/codeowners_spec.js
@@ -0,0 +1,85 @@
+import { editor } from 'monaco-editor';
+import codeowners from '~/ide/lib/languages/codeowners';
+import { registerLanguages } from '~/ide/utils';
+
+describe('tokenization for CODEOWNERS files', () => {
+ beforeEach(() => {
+ registerLanguages(codeowners);
+ });
+
+ it.each([
+ ['## Foo bar comment', [[{ language: 'codeowners', offset: 0, type: 'comment.codeowners' }]]],
+ [
+ '/foo/bar @gsamsa',
+ [
+ [
+ { language: 'codeowners', offset: 0, type: 'regexp.codeowners' },
+ { language: 'codeowners', offset: 8, type: 'source.codeowners' },
+ { language: 'codeowners', offset: 9, type: 'variable.value.codeowners' },
+ ],
+ ],
+ ],
+ [
+ '^[Section name]',
+ [
+ [
+ { language: 'codeowners', offset: 0, type: 'constant.numeric.codeowners' },
+ { language: 'codeowners', offset: 1, type: 'namespace.codeowners' },
+ ],
+ ],
+ ],
+ [
+ '[Section name][3]',
+ [
+ [
+ { language: 'codeowners', offset: 0, type: 'namespace.codeowners' },
+ { language: 'codeowners', offset: 14, type: 'constant.numeric.codeowners' },
+ ],
+ ],
+ ],
+ [
+ '[Section name][30]',
+ [
+ [
+ { language: 'codeowners', offset: 0, type: 'namespace.codeowners' },
+ { language: 'codeowners', offset: 14, type: 'constant.numeric.codeowners' },
+ ],
+ ],
+ ],
+ [
+ '^[Section name][3]',
+ [
+ [
+ { language: 'codeowners', offset: 0, type: 'constant.numeric.codeowners' },
+ { language: 'codeowners', offset: 1, type: 'namespace.codeowners' },
+ { language: 'codeowners', offset: 15, type: 'constant.numeric.codeowners' },
+ ],
+ ],
+ ],
+ [
+ '^[Section-name-test][3]',
+ [
+ [
+ { language: 'codeowners', offset: 0, type: 'constant.numeric.codeowners' },
+ { language: 'codeowners', offset: 1, type: 'namespace.codeowners' },
+ { language: 'codeowners', offset: 20, type: 'constant.numeric.codeowners' },
+ ],
+ ],
+ ],
+ [
+ '[Section-name_test]',
+ [[{ language: 'codeowners', offset: 0, type: 'namespace.codeowners' }]],
+ ],
+ [
+ '[2 Be or not 2 be][3]',
+ [
+ [
+ { language: 'codeowners', offset: 0, type: 'namespace.codeowners' },
+ { language: 'codeowners', offset: 18, type: 'constant.numeric.codeowners' },
+ ],
+ ],
+ ],
+ ])('%s', (string, tokens) => {
+ expect(editor.tokenize(string, 'codeowners')).toEqual(tokens);
+ });
+});
diff --git a/spec/frontend/ide/stores/actions_spec.js b/spec/frontend/ide/stores/actions_spec.js
index 63b63af667c..f6925e78b6a 100644
--- a/spec/frontend/ide/stores/actions_spec.js
+++ b/spec/frontend/ide/stores/actions_spec.js
@@ -210,7 +210,7 @@ describe('Multi-file store actions', () => {
expect(store.dispatch).toHaveBeenCalledWith('setFileActive', 'test');
});
- it('creates alert message if file already exists', async () => {
+ it('creates alert if file already exists', async () => {
const f = file('test', '1', 'blob');
store.state.trees['abcproject/mybranch'].tree = [f];
store.state.entries[f.path] = f;
@@ -440,7 +440,7 @@ describe('Multi-file store actions', () => {
});
describe('setErrorMessage', () => {
- it('commis error messsage', () => {
+ it('commis error message', () => {
return testAction(
setErrorMessage,
'error',
diff --git a/spec/frontend/ide/stores/modules/commit/actions_spec.js b/spec/frontend/ide/stores/modules/commit/actions_spec.js
index 872aa9b6e6b..3eaff92d321 100644
--- a/spec/frontend/ide/stores/modules/commit/actions_spec.js
+++ b/spec/frontend/ide/stores/modules/commit/actions_spec.js
@@ -1,7 +1,6 @@
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
-import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
import { file } from 'jest/ide/helpers';
import { commitActionTypes, PERMISSION_CREATE_MR } from '~/ide/constants';
import eventHub from '~/ide/eventhub';
@@ -40,14 +39,12 @@ describe('IDE commit module actions', () => {
let mock;
let store;
let router;
- let trackingSpy;
beforeEach(() => {
store = createStore();
router = createRouter(store);
gon.api_version = 'v1';
mock = new MockAdapter(axios);
- trackingSpy = mockTracking(undefined, undefined, jest.spyOn);
jest.spyOn(router, 'push').mockImplementation();
mock
@@ -56,7 +53,6 @@ describe('IDE commit module actions', () => {
});
afterEach(() => {
- unmockTracking();
mock.restore();
});
@@ -426,28 +422,6 @@ describe('IDE commit module actions', () => {
});
});
});
-
- describe('learnGitlabSource', () => {
- describe('learnGitlabSource is true', () => {
- it('tracks commit', async () => {
- store.state.learnGitlabSource = true;
-
- await store.dispatch('commit/commitChanges');
-
- expect(trackingSpy).toHaveBeenCalledWith(undefined, 'commit', {
- label: 'web_ide_learn_gitlab_source',
- });
- });
- });
-
- describe('learnGitlabSource is false', () => {
- it('does not track commit', async () => {
- await store.dispatch('commit/commitChanges');
-
- expect(trackingSpy).not.toHaveBeenCalled();
- });
- });
- });
});
describe('success response with failed message', () => {
@@ -465,26 +439,6 @@ describe('IDE commit module actions', () => {
expect(alert.textContent.trim()).toBe('failed message');
});
-
- describe('learnGitlabSource', () => {
- describe('learnGitlabSource is true', () => {
- it('does not track commit', async () => {
- store.state.learnGitlabSource = true;
-
- await store.dispatch('commit/commitChanges');
-
- expect(trackingSpy).not.toHaveBeenCalled();
- });
- });
-
- describe('learnGitlabSource is false', () => {
- it('does not track commit', async () => {
- await store.dispatch('commit/commitChanges');
-
- expect(trackingSpy).not.toHaveBeenCalled();
- });
- });
- });
});
describe('failed response', () => {
@@ -504,26 +458,6 @@ describe('IDE commit module actions', () => {
['commit/SET_ERROR', createUnexpectedCommitError(), undefined],
]);
});
-
- describe('learnGitlabSource', () => {
- describe('learnGitlabSource is true', () => {
- it('does not track commit', async () => {
- store.state.learnGitlabSource = true;
-
- await store.dispatch('commit/commitChanges').catch(() => {});
-
- expect(trackingSpy).not.toHaveBeenCalled();
- });
- });
-
- describe('learnGitlabSource is false', () => {
- it('does not track commit', async () => {
- await store.dispatch('commit/commitChanges').catch(() => {});
-
- expect(trackingSpy).not.toHaveBeenCalled();
- });
- });
- });
});
describe('first commit of a branch', () => {
diff --git a/spec/frontend/import/details/components/import_details_app_spec.js b/spec/frontend/import/details/components/import_details_app_spec.js
new file mode 100644
index 00000000000..178ce071de0
--- /dev/null
+++ b/spec/frontend/import/details/components/import_details_app_spec.js
@@ -0,0 +1,23 @@
+import { shallowMount } from '@vue/test-utils';
+import ImportDetailsApp from '~/import/details/components/import_details_app.vue';
+import { mockProject } from '../mock_data';
+
+describe('Import details app', () => {
+ let wrapper;
+
+ const createComponent = () => {
+ wrapper = shallowMount(ImportDetailsApp, {
+ propsData: {
+ project: mockProject,
+ },
+ });
+ };
+
+ describe('template', () => {
+ it('renders heading', () => {
+ createComponent();
+
+ expect(wrapper.find('h1').text()).toBe(ImportDetailsApp.i18n.pageTitle);
+ });
+ });
+});
diff --git a/spec/frontend/import/details/components/import_details_table_spec.js b/spec/frontend/import/details/components/import_details_table_spec.js
new file mode 100644
index 00000000000..43c9a66c00a
--- /dev/null
+++ b/spec/frontend/import/details/components/import_details_table_spec.js
@@ -0,0 +1,33 @@
+import { mount, shallowMount } from '@vue/test-utils';
+import { GlEmptyState, GlTable } from '@gitlab/ui';
+
+import PaginationBar from '~/vue_shared/components/pagination_bar/pagination_bar.vue';
+import ImportDetailsTable from '~/import/details/components/import_details_table.vue';
+
+describe('Import details table', () => {
+ let wrapper;
+
+ const createComponent = ({ mountFn = shallowMount } = {}) => {
+ wrapper = mountFn(ImportDetailsTable);
+ };
+
+ const findGlTable = () => wrapper.findComponent(GlTable);
+ const findGlEmptyState = () => findGlTable().findComponent(GlEmptyState);
+ const findPaginationBar = () => wrapper.findComponent(PaginationBar);
+
+ describe('template', () => {
+ describe('when no items are available', () => {
+ it('renders table with empty state', () => {
+ createComponent({ mountFn: mount });
+
+ expect(findGlEmptyState().exists()).toBe(true);
+ });
+
+ it('does not render pagination', () => {
+ createComponent();
+
+ expect(findPaginationBar().exists()).toBe(false);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/import/details/mock_data.js b/spec/frontend/import/details/mock_data.js
new file mode 100644
index 00000000000..514fb3a923d
--- /dev/null
+++ b/spec/frontend/import/details/mock_data.js
@@ -0,0 +1,31 @@
+export const mockProject = {
+ id: 26,
+ name: 'acl',
+ fullPath: '/root/acl',
+ fullName: 'Administrator / acl',
+ refsUrl: '/root/acl/refs',
+ importSource: 'namespace/acl',
+ importStatus: 'finished',
+ humanImportStatusName: 'finished',
+ providerLink: 'https://github.com/namespace/acl',
+ relationType: null,
+ stats: {
+ fetched: {
+ note: 1,
+ issue: 2,
+ label: 5,
+ collaborator: 2,
+ pullRequest: 1,
+ pullRequestMergedBy: 1,
+ },
+ imported: {
+ note: 1,
+ issue: 2,
+ label: 6,
+ collaborator: 3,
+ pullRequest: 1,
+ pullRequestMergedBy: 1,
+ pullRequestReviewRequest: 1,
+ },
+ },
+};
diff --git a/spec/frontend/import_entities/components/import_status_spec.js b/spec/frontend/import_entities/components/import_status_spec.js
index 3488d9f60c8..8e569e3ec85 100644
--- a/spec/frontend/import_entities/components/import_status_spec.js
+++ b/spec/frontend/import_entities/components/import_status_spec.js
@@ -1,4 +1,4 @@
-import { GlAccordionItem, GlBadge, GlIcon } from '@gitlab/ui';
+import { GlAccordionItem, GlBadge, GlIcon, GlLink } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import ImportStatus from '~/import_entities/components/import_status.vue';
import { STATUSES } from '~/import_entities/constants';
@@ -6,12 +6,17 @@ import { STATUSES } from '~/import_entities/constants';
describe('Import entities status component', () => {
let wrapper;
- const createComponent = (propsData) => {
+ const mockStatItems = { label: 100, note: 200 };
+
+ const createComponent = (propsData, { provide } = {}) => {
wrapper = shallowMount(ImportStatus, {
propsData,
+ provide,
});
};
+ const findGlLink = () => wrapper.findComponent(GlLink);
+
describe('success status', () => {
const getStatusText = () => wrapper.findComponent(GlBadge).text();
const getStatusIcon = () => wrapper.findComponent(GlBadge).props('icon');
@@ -24,13 +29,11 @@ describe('Import entities status component', () => {
});
it('displays finished status as complete when all stats items were processed', () => {
- const statItems = { label: 100, note: 200 };
-
createComponent({
status: STATUSES.FINISHED,
stats: {
- fetched: { ...statItems },
- imported: { ...statItems },
+ fetched: { ...mockStatItems },
+ imported: { ...mockStatItems },
},
});
@@ -39,17 +42,15 @@ describe('Import entities status component', () => {
});
it('displays finished status as partial when all stats items were processed', () => {
- const statItems = { label: 100, note: 200 };
-
createComponent({
status: STATUSES.FINISHED,
stats: {
- fetched: { ...statItems },
- imported: { ...statItems, label: 50 },
+ fetched: { ...mockStatItems },
+ imported: { ...mockStatItems, label: 50 },
},
});
- expect(getStatusText()).toBe('Partial import');
+ expect(getStatusText()).toBe('Partially completed');
expect(getStatusIcon()).toBe('status-alert');
});
});
@@ -151,4 +152,53 @@ describe('Import entities status component', () => {
expect(getStatusIcon()).toBe('status-success');
});
});
+
+ describe('show details link', () => {
+ const mockDetailsPath = 'details_path';
+ const mockCompleteStats = {
+ fetched: { ...mockStatItems },
+ imported: { ...mockStatItems },
+ };
+ const mockIncompleteStats = {
+ fetched: { ...mockStatItems },
+ imported: { ...mockStatItems, label: 50 },
+ };
+
+ describe.each`
+ detailsPath | importDetailsPage | partialImport | expectLink
+ ${undefined} | ${false} | ${false} | ${false}
+ ${undefined} | ${false} | ${true} | ${false}
+ ${undefined} | ${true} | ${false} | ${false}
+ ${undefined} | ${true} | ${true} | ${false}
+ ${mockDetailsPath} | ${false} | ${false} | ${false}
+ ${mockDetailsPath} | ${false} | ${true} | ${false}
+ ${mockDetailsPath} | ${true} | ${false} | ${false}
+ ${mockDetailsPath} | ${true} | ${true} | ${true}
+ `(
+ 'when detailsPath is $detailsPath, feature flag importDetailsPage is $importDetailsPage, partial import is $partialImport',
+ ({ detailsPath, importDetailsPage, partialImport, expectLink }) => {
+ beforeEach(() => {
+ createComponent(
+ {
+ status: STATUSES.FINISHED,
+ stats: partialImport ? mockIncompleteStats : mockCompleteStats,
+ },
+ {
+ provide: {
+ detailsPath,
+ glFeatures: { importDetailsPage },
+ },
+ },
+ );
+ });
+
+ it(`${expectLink ? 'renders' : 'does not render'} import details link`, () => {
+ expect(findGlLink().exists()).toBe(expectLink);
+ if (expectLink) {
+ expect(findGlLink().attributes('href')).toBe(mockDetailsPath);
+ }
+ });
+ },
+ );
+ });
});
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 1a52485f779..4c13ec555c2 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
@@ -94,14 +94,14 @@ describe('import actions cell', () => {
);
});
- it('request migrate projects by default', async () => {
+ it('request migrate projects by default', () => {
const dropdown = wrapper.findComponent(GlDropdown);
dropdown.vm.$emit('click');
expect(wrapper.emitted('import-group')[0]).toStrictEqual([{ migrateProjects: true }]);
});
- it('request not to migrate projects via dropdown option', async () => {
+ it('request not to migrate projects via dropdown option', () => {
const dropdown = wrapper.findComponent(GlDropdown);
dropdown.findComponent(GlDropdownItem).vm.$emit('click');
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 205218fdabd..b1aa94cf418 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
@@ -687,7 +687,7 @@ describe('import table', () => {
return waitForPromises();
});
- it('renders import all dropdown', async () => {
+ it('renders import all dropdown', () => {
expect(findImportSelectedDropdown().exists()).toBe(true);
});
diff --git a/spec/frontend/import_entities/import_groups/graphql/client_factory_spec.js b/spec/frontend/import_entities/import_groups/graphql/client_factory_spec.js
index 83566469176..540c42a2854 100644
--- a/spec/frontend/import_entities/import_groups/graphql/client_factory_spec.js
+++ b/spec/frontend/import_entities/import_groups/graphql/client_factory_spec.js
@@ -48,7 +48,7 @@ describe('Bulk import resolvers', () => {
};
let results;
- beforeEach(async () => {
+ beforeEach(() => {
axiosMockAdapter = new MockAdapter(axios);
client = createClient();
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 5ee2b2e698f..e1ed739a708 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
@@ -81,7 +81,7 @@ describe('Bulk import status poller', () => {
expect(pollInstance.makeRequest).toHaveBeenCalled();
});
- it('when error occurs shows alert with error', () => {
+ it('when error occurs shows an alert with error', () => {
const [[pollConfig]] = Poll.mock.calls;
pollConfig.errorCallback();
expect(createAlert).toHaveBeenCalled();
diff --git a/spec/frontend/incidents/components/incidents_list_spec.js b/spec/frontend/incidents/components/incidents_list_spec.js
index e8d222dc2e9..6e64eeaf295 100644
--- a/spec/frontend/incidents/components/incidents_list_spec.js
+++ b/spec/frontend/incidents/components/incidents_list_spec.js
@@ -212,7 +212,7 @@ describe('Incidents List', () => {
});
});
- it('contains a link to the incident details page', async () => {
+ it('contains a link to the incident details page', () => {
findTableRows().at(0).trigger('click');
expect(visitUrl).toHaveBeenCalledWith(
joinPaths(`/project/issues/incident`, mockIncidents[0].iid),
diff --git a/spec/frontend/incidents_settings/components/incidents_settings_service_spec.js b/spec/frontend/incidents_settings/components/incidents_settings_service_spec.js
index c5c29b4bb19..9b11fe2bff0 100644
--- a/spec/frontend/incidents_settings/components/incidents_settings_service_spec.js
+++ b/spec/frontend/incidents_settings/components/incidents_settings_service_spec.js
@@ -33,7 +33,7 @@ describe('IncidentsSettingsService', () => {
});
});
- it('should display an alert message on update error', () => {
+ it('should display an alert on update error', () => {
mock.onPatch().reply(HTTP_STATUS_BAD_REQUEST);
return service.updateSettings({}).then(() => {
diff --git a/spec/frontend/integrations/edit/components/integration_form_spec.js b/spec/frontend/integrations/edit/components/integration_form_spec.js
index 58fb456eb53..5aa3ee35379 100644
--- a/spec/frontend/integrations/edit/components/integration_form_spec.js
+++ b/spec/frontend/integrations/edit/components/integration_form_spec.js
@@ -495,7 +495,7 @@ describe('IntegrationForm', () => {
expect(refreshCurrentPage).toHaveBeenCalledTimes(1);
});
- it('resets `isResetting`', async () => {
+ it('resets `isResetting`', () => {
expect(findFormActions().props('isResetting')).toBe(false);
});
});
diff --git a/spec/frontend/integrations/edit/components/jira_issues_fields_spec.js b/spec/frontend/integrations/edit/components/jira_issues_fields_spec.js
index 90ee69ef2dc..82f70b8ede1 100644
--- a/spec/frontend/integrations/edit/components/jira_issues_fields_spec.js
+++ b/spec/frontend/integrations/edit/components/jira_issues_fields_spec.js
@@ -37,8 +37,7 @@ describe('JiraIssuesFields', () => {
const findProjectKey = () => wrapper.findComponent(GlFormInput);
const findProjectKeyFormGroup = () => wrapper.findByTestId('project-key-form-group');
const findJiraForVulnerabilities = () => wrapper.findByTestId('jira-for-vulnerabilities');
- const setEnableCheckbox = async (isEnabled = true) =>
- findEnableCheckbox().vm.$emit('input', isEnabled);
+ const setEnableCheckbox = (isEnabled = true) => findEnableCheckbox().vm.$emit('input', isEnabled);
const assertProjectKeyState = (expectedStateValue) => {
expect(findProjectKey().attributes('state')).toBe(expectedStateValue);
@@ -178,7 +177,7 @@ describe('JiraIssuesFields', () => {
});
describe('with no project key', () => {
- it('sets Project Key `state` attribute to `undefined`', async () => {
+ it('sets Project Key `state` attribute to `undefined`', () => {
assertProjectKeyState(undefined);
});
});
diff --git a/spec/frontend/invite_members/components/invite_group_notification_spec.js b/spec/frontend/invite_members/components/invite_group_notification_spec.js
index 3e6ba6da9f4..1da2e7b705d 100644
--- a/spec/frontend/invite_members/components/invite_group_notification_spec.js
+++ b/spec/frontend/invite_members/components/invite_group_notification_spec.js
@@ -2,7 +2,7 @@ import { GlAlert, GlLink, GlSprintf } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { sprintf } from '~/locale';
import InviteGroupNotification from '~/invite_members/components/invite_group_notification.vue';
-import { GROUP_MODAL_ALERT_BODY } from '~/invite_members/constants';
+import { GROUP_MODAL_TO_GROUP_ALERT_BODY } from '~/invite_members/constants';
describe('InviteGroupNotification', () => {
let wrapper;
@@ -13,7 +13,11 @@ describe('InviteGroupNotification', () => {
const createComponent = () => {
wrapper = shallowMountExtended(InviteGroupNotification, {
provide: { freeUsersLimit: 5 },
- propsData: { name: 'name' },
+ propsData: {
+ name: 'name',
+ notificationLink: '_notification_link_',
+ notificationText: GROUP_MODAL_TO_GROUP_ALERT_BODY,
+ },
stubs: { GlSprintf },
});
};
@@ -28,15 +32,13 @@ describe('InviteGroupNotification', () => {
});
it('shows the correct message', () => {
- const message = sprintf(GROUP_MODAL_ALERT_BODY, { count: 5 });
+ const message = sprintf(GROUP_MODAL_TO_GROUP_ALERT_BODY, { count: 5 });
expect(findAlert().text()).toMatchInterpolatedText(message);
});
it('has a help link', () => {
- expect(findLink().attributes('href')).toEqual(
- 'https://docs.gitlab.com/ee/user/group/manage.html#share-a-group-with-another-group',
- );
+ expect(findLink().attributes('href')).toEqual('_notification_link_');
});
});
});
diff --git a/spec/frontend/invite_members/components/invite_groups_modal_spec.js b/spec/frontend/invite_members/components/invite_groups_modal_spec.js
index 82b4717fbf1..4f082145562 100644
--- a/spec/frontend/invite_members/components/invite_groups_modal_spec.js
+++ b/spec/frontend/invite_members/components/invite_groups_modal_spec.js
@@ -12,6 +12,12 @@ import {
displaySuccessfulInvitationAlert,
reloadOnInvitationSuccess,
} from '~/invite_members/utils/trigger_successful_invite_alert';
+import {
+ GROUP_MODAL_TO_GROUP_ALERT_BODY,
+ GROUP_MODAL_TO_GROUP_ALERT_LINK,
+ GROUP_MODAL_TO_PROJECT_ALERT_BODY,
+ GROUP_MODAL_TO_PROJECT_ALERT_LINK,
+} from '~/invite_members/constants';
import { propsData, sharedGroup } from '../mock_data/group_modal';
jest.mock('~/invite_members/utils/trigger_successful_invite_alert');
@@ -91,6 +97,26 @@ describe('InviteGroupsModal', () => {
expect(findInviteGroupAlert().exists()).toBe(false);
});
+
+ it('shows the user limit notification alert with correct link and text for group', () => {
+ createComponent({ freeUserCapEnabled: true });
+
+ expect(findInviteGroupAlert().props()).toMatchObject({
+ name: propsData.name,
+ notificationText: GROUP_MODAL_TO_GROUP_ALERT_BODY,
+ notificationLink: GROUP_MODAL_TO_GROUP_ALERT_LINK,
+ });
+ });
+
+ it('shows the user limit notification alert with correct link and text for project', () => {
+ createComponent({ freeUserCapEnabled: true, isProject: true });
+
+ expect(findInviteGroupAlert().props()).toMatchObject({
+ name: propsData.name,
+ notificationText: GROUP_MODAL_TO_PROJECT_ALERT_BODY,
+ notificationLink: GROUP_MODAL_TO_PROJECT_ALERT_LINK,
+ });
+ });
});
describe('submitting the invite form', () => {
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 39d5ddee723..e080e665a3b 100644
--- a/spec/frontend/invite_members/components/invite_members_modal_spec.js
+++ b/spec/frontend/invite_members/components/invite_members_modal_spec.js
@@ -6,7 +6,6 @@ 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';
-import ExperimentTracking from '~/experimentation/experiment_tracking';
import InviteMembersModal from '~/invite_members/components/invite_members_modal.vue';
import InviteModalBase from '~/invite_members/components/invite_modal_base.vue';
import ModalConfetti from '~/invite_members/components/confetti.vue';
@@ -18,11 +17,11 @@ import {
MEMBERS_MODAL_CELEBRATE_TITLE,
MEMBERS_PLACEHOLDER,
MEMBERS_TO_PROJECT_CELEBRATE_INTRO_TEXT,
- LEARN_GITLAB,
EXPANDED_ERRORS,
EMPTY_INVITES_ALERT_TEXT,
ON_CELEBRATION_TRACK_LABEL,
INVITE_MEMBER_MODAL_TRACKING_CATEGORY,
+ INVALID_FEEDBACK_MESSAGE_DEFAULT,
} from '~/invite_members/constants';
import eventHub from '~/invite_members/event_hub';
import ContentTransition from '~/vue_shared/components/content_transition.vue';
@@ -40,7 +39,9 @@ import {
import { GROUPS_INVITATIONS_PATH, invitationsApiResponse } from '../mock_data/api_responses';
import {
propsData,
- inviteSource,
+ emailPostData,
+ postData,
+ singleUserPostData,
newProjectPath,
user1,
user2,
@@ -63,11 +64,12 @@ describe('InviteMembersModal', () => {
let mock;
let trackingSpy;
- const expectTracking = (
- action,
- label = undefined,
- category = INVITE_MEMBER_MODAL_TRACKING_CATEGORY,
- ) => expect(trackingSpy).toHaveBeenCalledWith(category, action, { label, category });
+ const expectTracking = (action, label = undefined, property = undefined) =>
+ expect(trackingSpy).toHaveBeenCalledWith(INVITE_MEMBER_MODAL_TRACKING_CATEGORY, action, {
+ label,
+ category: INVITE_MEMBER_MODAL_TRACKING_CATEGORY,
+ property,
+ });
const createComponent = (props = {}, stubs = {}) => {
wrapper = shallowMountExtended(InviteMembersModal, {
@@ -211,15 +213,6 @@ describe('InviteMembersModal', () => {
expect(findTasksToBeDone().exists()).toBe(false);
});
-
- describe('when opened from the Learn GitLab page', () => {
- it('does render the tasks to be done', async () => {
- await setupComponent({}, []);
- await triggerOpenModal({ source: LEARN_GITLAB });
-
- expect(findTasksToBeDone().exists()).toBe(true);
- });
- });
});
describe('rendering the tasks', () => {
@@ -278,38 +271,18 @@ describe('InviteMembersModal', () => {
});
describe('tracking events', () => {
- it('tracks the view for invite_members_for_task', async () => {
- await setupComponentWithTasks();
-
- expect(ExperimentTracking).toHaveBeenCalledWith(INVITE_MEMBERS_FOR_TASK.name);
- expect(ExperimentTracking.prototype.event).toHaveBeenCalledWith(
- INVITE_MEMBERS_FOR_TASK.view,
- );
- });
-
it('tracks the submit for invite_members_for_task', async () => {
await setupComponentWithTasks();
- await triggerMembersTokenSelect([user1]);
- clickInviteButton();
+ await triggerMembersTokenSelect([user1]);
- expect(ExperimentTracking).toHaveBeenCalledWith(INVITE_MEMBERS_FOR_TASK.name, {
- label: 'selected_tasks_to_be_done',
- property: 'ci,code',
- });
- expect(ExperimentTracking.prototype.event).toHaveBeenCalledWith(
- INVITE_MEMBERS_FOR_TASK.submit,
- );
- });
+ trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
- it('does not track the submit for invite_members_for_task when invites have not been entered', async () => {
- await setupComponentWithTasks();
clickInviteButton();
- expect(ExperimentTracking).not.toHaveBeenCalledWith(
- INVITE_MEMBERS_FOR_TASK.name,
- expect.any,
- );
+ expectTracking(INVITE_MEMBERS_FOR_TASK.submit, 'selected_tasks_to_be_done', 'ci,code');
+
+ unmockTracking();
});
});
});
@@ -472,7 +445,7 @@ describe('InviteMembersModal', () => {
const expectedSyntaxError = 'email contains an invalid email address';
describe('when no invites have been entered in the form and then some are entered', () => {
- beforeEach(async () => {
+ beforeEach(() => {
createInviteMembersToGroupWrapper();
});
@@ -492,16 +465,6 @@ describe('InviteMembersModal', () => {
});
describe('when inviting an existing user to group by user ID', () => {
- const postData = {
- user_id: '1,2',
- access_level: propsData.defaultAccessLevel,
- expires_at: undefined,
- invite_source: inviteSource,
- format: 'json',
- tasks_to_be_done: [],
- tasks_project_id: '',
- };
-
describe('when reloadOnSubmit is true', () => {
beforeEach(async () => {
createComponent({ reloadPageOnSubmit: true });
@@ -555,20 +518,6 @@ describe('InviteMembersModal', () => {
expect(reloadOnInvitationSuccess).not.toHaveBeenCalled();
});
});
-
- describe('when opened from a Learn GitLab page', () => {
- it('emits the `showSuccessfulInvitationsAlert` event', async () => {
- await triggerOpenModal({ source: LEARN_GITLAB });
-
- jest.spyOn(eventHub, '$emit').mockImplementation();
-
- clickInviteButton();
-
- await waitForPromises();
-
- expect(eventHub.$emit).toHaveBeenCalledWith('showSuccessfulInvitationsAlert');
- });
- });
});
describe('when member is not added successfully', () => {
@@ -675,16 +624,6 @@ describe('InviteMembersModal', () => {
});
describe('when inviting a new user by email address', () => {
- const postData = {
- access_level: propsData.defaultAccessLevel,
- expires_at: undefined,
- email: 'email@example.com',
- invite_source: inviteSource,
- tasks_to_be_done: [],
- tasks_project_id: '',
- format: 'json',
- };
-
describe('when invites are sent successfully', () => {
beforeEach(async () => {
createComponent();
@@ -692,7 +631,7 @@ describe('InviteMembersModal', () => {
trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
wrapper.vm.$toast = { show: jest.fn() };
- jest.spyOn(Api, 'inviteGroupMembers').mockResolvedValue({ data: postData });
+ jest.spyOn(Api, 'inviteGroupMembers').mockResolvedValue({ data: emailPostData });
});
describe('when triggered from regular mounting', () => {
@@ -701,7 +640,7 @@ describe('InviteMembersModal', () => {
});
it('calls Api inviteGroupMembers with the correct params', () => {
- expect(Api.inviteGroupMembers).toHaveBeenCalledWith(propsData.id, postData);
+ expect(Api.inviteGroupMembers).toHaveBeenCalledWith(propsData.id, emailPostData);
});
it('displays the successful toastMessage', () => {
@@ -719,96 +658,117 @@ describe('InviteMembersModal', () => {
});
describe('when invites are not sent successfully', () => {
- beforeEach(async () => {
- createInviteMembersToGroupWrapper();
+ describe('when api throws error', () => {
+ beforeEach(async () => {
+ jest.spyOn(axios, 'post').mockImplementation(() => {
+ throw new Error();
+ });
- await triggerMembersTokenSelect([user3]);
+ createInviteMembersToGroupWrapper();
+
+ await triggerMembersTokenSelect([user3]);
+ clickInviteButton();
+ });
+
+ it('displays the default error message', () => {
+ expect(membersFormGroupInvalidFeedback()).toBe(INVALID_FEEDBACK_MESSAGE_DEFAULT);
+ expect(findMembersSelect().props('exceptionState')).toBe(false);
+ expect(findActionButton().props('loading')).toBe(false);
+ });
});
- it('displays the api error for invalid email syntax', async () => {
- mockInvitationsApi(HTTP_STATUS_BAD_REQUEST, invitationsApiResponse.EMAIL_INVALID);
+ describe('when api rejects promise', () => {
+ beforeEach(async () => {
+ createInviteMembersToGroupWrapper();
- clickInviteButton();
+ await triggerMembersTokenSelect([user3]);
+ });
- await waitForPromises();
+ it('displays the api error for invalid email syntax', async () => {
+ mockInvitationsApi(HTTP_STATUS_BAD_REQUEST, invitationsApiResponse.EMAIL_INVALID);
- expect(membersFormGroupInvalidFeedback()).toBe(expectedSyntaxError);
- expect(findMembersSelect().props('exceptionState')).toBe(false);
- expect(findActionButton().props('loading')).toBe(false);
- });
+ clickInviteButton();
- it('clears the error when the modal is hidden', async () => {
- mockInvitationsApi(HTTP_STATUS_BAD_REQUEST, invitationsApiResponse.EMAIL_INVALID);
+ await waitForPromises();
- clickInviteButton();
+ expect(membersFormGroupInvalidFeedback()).toBe(expectedSyntaxError);
+ expect(findMembersSelect().props('exceptionState')).toBe(false);
+ expect(findActionButton().props('loading')).toBe(false);
+ });
- await waitForPromises();
+ it('clears the error when the modal is hidden', async () => {
+ mockInvitationsApi(HTTP_STATUS_BAD_REQUEST, invitationsApiResponse.EMAIL_INVALID);
- expect(membersFormGroupInvalidFeedback()).toBe(expectedSyntaxError);
- expect(findMembersSelect().props('exceptionState')).toBe(false);
- expect(findActionButton().props('loading')).toBe(false);
+ clickInviteButton();
- findModal().vm.$emit('hidden');
+ await waitForPromises();
- await nextTick();
+ expect(membersFormGroupInvalidFeedback()).toBe(expectedSyntaxError);
+ expect(findMembersSelect().props('exceptionState')).toBe(false);
+ expect(findActionButton().props('loading')).toBe(false);
- expect(findMemberErrorAlert().exists()).toBe(false);
- expect(membersFormGroupInvalidFeedback()).toBe('');
- expect(findMembersSelect().props('exceptionState')).not.toBe(false);
- });
+ findModal().vm.$emit('hidden');
- it('displays the restricted email error when restricted email is invited', async () => {
- mockInvitationsApi(HTTP_STATUS_CREATED, invitationsApiResponse.EMAIL_RESTRICTED);
+ await nextTick();
- clickInviteButton();
+ expect(findMemberErrorAlert().exists()).toBe(false);
+ expect(membersFormGroupInvalidFeedback()).toBe('');
+ expect(findMembersSelect().props('exceptionState')).not.toBe(false);
+ });
- await waitForPromises();
+ it('displays the restricted email error when restricted email is invited', async () => {
+ mockInvitationsApi(HTTP_STATUS_CREATED, invitationsApiResponse.EMAIL_RESTRICTED);
- expect(findMemberErrorAlert().exists()).toBe(true);
- expect(findMemberErrorAlert().text()).toContain(expectedEmailRestrictedError);
- expect(membersFormGroupInvalidFeedback()).toBe('');
- expect(findMembersSelect().props('exceptionState')).not.toBe(false);
- expect(findActionButton().props('loading')).toBe(false);
- });
+ clickInviteButton();
- it('displays all errors when there are multiple emails that return a restricted error message', async () => {
- mockInvitationsApi(HTTP_STATUS_CREATED, invitationsApiResponse.MULTIPLE_RESTRICTED);
+ await waitForPromises();
- clickInviteButton();
+ expect(findMemberErrorAlert().exists()).toBe(true);
+ expect(findMemberErrorAlert().text()).toContain(expectedEmailRestrictedError);
+ expect(membersFormGroupInvalidFeedback()).toBe('');
+ expect(findMembersSelect().props('exceptionState')).not.toBe(false);
+ expect(findActionButton().props('loading')).toBe(false);
+ });
- await waitForPromises();
+ it('displays all errors when there are multiple emails that return a restricted error message', async () => {
+ mockInvitationsApi(HTTP_STATUS_CREATED, invitationsApiResponse.MULTIPLE_RESTRICTED);
- expect(findMemberErrorAlert().exists()).toBe(true);
- expect(findMemberErrorAlert().text()).toContain(
- Object.values(invitationsApiResponse.MULTIPLE_RESTRICTED.message)[0],
- );
- expect(findMemberErrorAlert().text()).toContain(
- Object.values(invitationsApiResponse.MULTIPLE_RESTRICTED.message)[1],
- );
- expect(findMemberErrorAlert().text()).toContain(
- Object.values(invitationsApiResponse.MULTIPLE_RESTRICTED.message)[2],
- );
- expect(membersFormGroupInvalidFeedback()).toBe('');
- expect(findMembersSelect().props('exceptionState')).not.toBe(false);
- });
+ clickInviteButton();
- it('displays the invalid syntax error for bad request', async () => {
- mockInvitationsApi(HTTP_STATUS_BAD_REQUEST, invitationsApiResponse.ERROR_EMAIL_INVALID);
+ await waitForPromises();
- clickInviteButton();
+ expect(findMemberErrorAlert().exists()).toBe(true);
+ expect(findMemberErrorAlert().text()).toContain(
+ Object.values(invitationsApiResponse.MULTIPLE_RESTRICTED.message)[0],
+ );
+ expect(findMemberErrorAlert().text()).toContain(
+ Object.values(invitationsApiResponse.MULTIPLE_RESTRICTED.message)[1],
+ );
+ expect(findMemberErrorAlert().text()).toContain(
+ Object.values(invitationsApiResponse.MULTIPLE_RESTRICTED.message)[2],
+ );
+ expect(membersFormGroupInvalidFeedback()).toBe('');
+ expect(findMembersSelect().props('exceptionState')).not.toBe(false);
+ });
- await waitForPromises();
+ it('displays the invalid syntax error for bad request', async () => {
+ mockInvitationsApi(HTTP_STATUS_BAD_REQUEST, invitationsApiResponse.ERROR_EMAIL_INVALID);
- expect(membersFormGroupInvalidFeedback()).toBe(expectedSyntaxError);
- expect(findMembersSelect().props('exceptionState')).toBe(false);
- });
+ clickInviteButton();
- it('does not call displaySuccessfulInvitationAlert on mount', () => {
- expect(displaySuccessfulInvitationAlert).not.toHaveBeenCalled();
- });
+ await waitForPromises();
+
+ expect(membersFormGroupInvalidFeedback()).toBe(expectedSyntaxError);
+ expect(findMembersSelect().props('exceptionState')).toBe(false);
+ });
- it('does not call reloadOnInvitationSuccess', () => {
- expect(reloadOnInvitationSuccess).not.toHaveBeenCalled();
+ it('does not call displaySuccessfulInvitationAlert on mount', () => {
+ expect(displaySuccessfulInvitationAlert).not.toHaveBeenCalled();
+ });
+
+ it('does not call reloadOnInvitationSuccess', () => {
+ expect(reloadOnInvitationSuccess).not.toHaveBeenCalled();
+ });
});
});
@@ -892,17 +852,6 @@ describe('InviteMembersModal', () => {
});
describe('when inviting members and non-members in same click', () => {
- const postData = {
- access_level: propsData.defaultAccessLevel,
- expires_at: undefined,
- invite_source: inviteSource,
- format: 'json',
- tasks_to_be_done: [],
- tasks_project_id: '',
- user_id: '1',
- email: 'email@example.com',
- };
-
describe('when invites are sent successfully', () => {
beforeEach(async () => {
createComponent();
@@ -910,7 +859,7 @@ describe('InviteMembersModal', () => {
trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
wrapper.vm.$toast = { show: jest.fn() };
- jest.spyOn(Api, 'inviteGroupMembers').mockResolvedValue({ data: postData });
+ jest.spyOn(Api, 'inviteGroupMembers').mockResolvedValue({ data: singleUserPostData });
});
describe('when triggered from regular mounting', () => {
@@ -922,7 +871,7 @@ describe('InviteMembersModal', () => {
it('calls Api inviteGroupMembers with the correct params and invite source', () => {
expect(Api.inviteGroupMembers).toHaveBeenCalledWith(propsData.id, {
- ...postData,
+ ...singleUserPostData,
invite_source: '_invite_source_',
});
});
@@ -951,26 +900,9 @@ describe('InviteMembersModal', () => {
clickInviteButton();
- expect(Api.inviteGroupMembers).toHaveBeenCalledWith(propsData.id, postData);
+ expect(Api.inviteGroupMembers).toHaveBeenCalledWith(propsData.id, singleUserPostData);
});
});
});
-
- describe('tracking', () => {
- beforeEach(async () => {
- createComponent();
- await triggerMembersTokenSelect([user3]);
-
- wrapper.vm.$toast = { show: jest.fn() };
- jest.spyOn(Api, 'inviteGroupMembers').mockResolvedValue({});
- });
-
- it('tracks the view for learn_gitlab source', () => {
- eventHub.$emit('openModal', { source: LEARN_GITLAB });
-
- expect(ExperimentTracking).toHaveBeenCalledWith(INVITE_MEMBERS_FOR_TASK.name);
- expect(ExperimentTracking.prototype.event).toHaveBeenCalledWith(LEARN_GITLAB);
- });
- });
});
});
diff --git a/spec/frontend/invite_members/mock_data/member_modal.js b/spec/frontend/invite_members/mock_data/member_modal.js
index 59d58f21bb0..67fb1dcbfbd 100644
--- a/spec/frontend/invite_members/mock_data/member_modal.js
+++ b/spec/frontend/invite_members/mock_data/member_modal.js
@@ -45,4 +45,35 @@ export const user6 = {
avatar_url: '',
};
+export const postData = {
+ user_id: `${user1.id},${user2.id}`,
+ access_level: propsData.defaultAccessLevel,
+ expires_at: undefined,
+ invite_source: inviteSource,
+ format: 'json',
+ tasks_to_be_done: [],
+ tasks_project_id: '',
+};
+
+export const emailPostData = {
+ access_level: propsData.defaultAccessLevel,
+ expires_at: undefined,
+ email: `${user3.name}`,
+ invite_source: inviteSource,
+ tasks_to_be_done: [],
+ tasks_project_id: '',
+ format: 'json',
+};
+
+export const singleUserPostData = {
+ access_level: propsData.defaultAccessLevel,
+ expires_at: undefined,
+ user_id: `${user1.id}`,
+ email: `${user3.name}`,
+ invite_source: inviteSource,
+ tasks_to_be_done: [],
+ tasks_project_id: '',
+ format: 'json',
+};
+
export const GlEmoji = { template: '<img/>' };
diff --git a/spec/frontend/invite_members/utils/member_utils_spec.js b/spec/frontend/invite_members/utils/member_utils_spec.js
index eb76c9845d4..b6fc70038bb 100644
--- a/spec/frontend/invite_members/utils/member_utils_spec.js
+++ b/spec/frontend/invite_members/utils/member_utils_spec.js
@@ -1,4 +1,12 @@
-import { memberName } from '~/invite_members/utils/member_utils';
+import {
+ memberName,
+ triggerExternalAlert,
+ qualifiesForTasksToBeDone,
+} from '~/invite_members/utils/member_utils';
+import setWindowLocation from 'helpers/set_window_location_helper';
+import { getParameterValues } from '~/lib/utils/url_utility';
+
+jest.mock('~/lib/utils/url_utility');
describe('Member Name', () => {
it.each([
@@ -10,3 +18,23 @@ describe('Member Name', () => {
expect(memberName(member)).toBe(result);
});
});
+
+describe('Trigger External Alert', () => {
+ it('returns false', () => {
+ expect(triggerExternalAlert()).toBe(false);
+ });
+});
+
+describe('Qualifies For Tasks To Be Done', () => {
+ it.each([
+ ['invite_members_for_task', true],
+ ['blah', false],
+ ])(`returns name from supplied member token: %j`, (value, result) => {
+ setWindowLocation(`blah/blah?open_modal=${value}`);
+ getParameterValues.mockImplementation(() => {
+ return [value];
+ });
+
+ expect(qualifiesForTasksToBeDone()).toBe(result);
+ });
+});
diff --git a/spec/frontend/invite_members/utils/trigger_successful_invite_alert_spec.js b/spec/frontend/invite_members/utils/trigger_successful_invite_alert_spec.js
index fd011658f95..6192713f121 100644
--- a/spec/frontend/invite_members/utils/trigger_successful_invite_alert_spec.js
+++ b/spec/frontend/invite_members/utils/trigger_successful_invite_alert_spec.js
@@ -13,7 +13,7 @@ jest.mock('~/alert');
useLocalStorageSpy();
describe('Display Successful Invitation Alert', () => {
- it('does not show alert if localStorage key not present', () => {
+ it('does not show an alert if localStorage key not present', () => {
localStorage.removeItem(TOAST_MESSAGE_LOCALSTORAGE_KEY);
displaySuccessfulInvitationAlert();
@@ -21,7 +21,7 @@ describe('Display Successful Invitation Alert', () => {
expect(createAlert).not.toHaveBeenCalled();
});
- it('shows alert when localStorage key is present', () => {
+ it('shows an alert when localStorage key is present', () => {
localStorage.setItem(TOAST_MESSAGE_LOCALSTORAGE_KEY, 'true');
displaySuccessfulInvitationAlert();
diff --git a/spec/frontend/issuable/issuable_form_spec.js b/spec/frontend/issuable/issuable_form_spec.js
index 3e778e50fb8..d7e5f9083b0 100644
--- a/spec/frontend/issuable/issuable_form_spec.js
+++ b/spec/frontend/issuable/issuable_form_spec.js
@@ -3,11 +3,18 @@ import Autosave from '~/autosave';
import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import IssuableForm from '~/issuable/issuable_form';
import setWindowLocation from 'helpers/set_window_location_helper';
-
+import { confirmSensitiveAction, i18n } from '~/lib/utils/secret_detection';
import { getSaveableFormChildren } from './helpers';
jest.mock('~/autosave');
+jest.mock('~/lib/utils/secret_detection', () => {
+ return {
+ ...jest.requireActual('~/lib/utils/secret_detection'),
+ confirmSensitiveAction: jest.fn(() => Promise.resolve(false)),
+ };
+});
+
const createIssuable = (form) => {
return new IssuableForm(form);
};
@@ -35,16 +42,13 @@ describe('IssuableForm', () => {
describe('autosave', () => {
let $title;
- let $description;
beforeEach(() => {
$title = $form.find('input[name*="[title]"]').get(0);
- $description = $form.find('textarea[name*="[description]"]').get(0);
});
afterEach(() => {
$title = null;
- $description = null;
});
describe('initAutosave', () => {
@@ -64,11 +68,6 @@ describe('IssuableForm', () => {
['/foo', 'bar=true', 'title'],
'autosave//foo/bar=true=title',
);
- expect(Autosave).toHaveBeenCalledWith(
- $description,
- ['/foo', 'bar=true', 'description'],
- 'autosave//foo/bar=true=description',
- );
});
it("creates autosave fields without the searchTerm if it's an issue new form", () => {
@@ -81,11 +80,6 @@ describe('IssuableForm', () => {
['/issues/new', '', 'title'],
'autosave//issues/new/bar=true=title',
);
- expect(Autosave).toHaveBeenCalledWith(
- $description,
- ['/issues/new', '', 'description'],
- 'autosave//issues/new/bar=true=description',
- );
});
it.each([
@@ -106,7 +100,9 @@ describe('IssuableForm', () => {
const children = getSaveableFormChildren($form[0]);
- expect(Autosave).toHaveBeenCalledTimes(children.length);
+ // description autosave is being handled separately
+ // hence we're using children.length - 1
+ expect(Autosave).toHaveBeenCalledTimes(children.length - 1);
expect(Autosave).toHaveBeenLastCalledWith(
$input.get(0),
['/', '', id],
@@ -116,13 +112,12 @@ describe('IssuableForm', () => {
});
describe('resetAutosave', () => {
- it('calls reset on title and description', () => {
+ it('calls reset on title', () => {
instance = createIssuable($form);
instance.resetAutosave();
expect(instance.autosaves.get('title').reset).toHaveBeenCalledTimes(1);
- expect(instance.autosaves.get('description').reset).toHaveBeenCalledTimes(1);
});
it('resets autosave when submit', () => {
@@ -245,4 +240,44 @@ describe('IssuableForm', () => {
);
});
});
+
+ describe('Checks for sensitive token', () => {
+ let issueDescription;
+ const sensitiveMessage = 'token: glpat-1234567890abcdefghij';
+
+ beforeEach(() => {
+ issueDescription = $form.find('textarea[name*="[description]"]').get(0);
+ });
+
+ afterEach(() => {
+ issueDescription = null;
+ });
+
+ it('submits the form when no token is present', () => {
+ issueDescription.value = 'sample message';
+
+ const handleSubmit = jest.spyOn(IssuableForm.prototype, 'handleSubmit');
+ const resetAutosave = jest.spyOn(IssuableForm.prototype, 'resetAutosave');
+ createIssuable($form);
+
+ $form.submit();
+
+ expect(handleSubmit).toHaveBeenCalled();
+ expect(resetAutosave).toHaveBeenCalled();
+ });
+
+ it('prevents form submission when token is present', () => {
+ issueDescription.value = sensitiveMessage;
+
+ const handleSubmit = jest.spyOn(IssuableForm.prototype, 'handleSubmit');
+ const resetAutosave = jest.spyOn(IssuableForm.prototype, 'resetAutosave');
+ createIssuable($form);
+
+ $form.submit();
+
+ expect(handleSubmit).toHaveBeenCalled();
+ expect(confirmSensitiveAction).toHaveBeenCalledWith(i18n.descriptionPrompt);
+ expect(resetAutosave).not.toHaveBeenCalled();
+ });
+ });
});
diff --git a/spec/frontend/issuable/popover/components/mr_popover_spec.js b/spec/frontend/issuable/popover/components/mr_popover_spec.js
index d9e113eeaae..5b29ecfc0ba 100644
--- a/spec/frontend/issuable/popover/components/mr_popover_spec.js
+++ b/spec/frontend/issuable/popover/components/mr_popover_spec.js
@@ -95,7 +95,7 @@ describe('MR Popover', () => {
expect(wrapper.text()).toContain('foo/bar!1');
});
- it('shows CI Icon if there is pipeline data', async () => {
+ it('shows CI Icon if there is pipeline data', () => {
expect(wrapper.findComponent(CiIcon).exists()).toBe(true);
});
});
@@ -108,7 +108,7 @@ describe('MR Popover', () => {
return waitForPromises();
});
- it('does not show CI icon if there is no pipeline data', async () => {
+ it('does not show CI icon if there is no pipeline data', () => {
expect(wrapper.findComponent(CiIcon).exists()).toBe(false);
});
});
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 f8e47bc0a4b..f90b9117688 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
@@ -230,7 +230,7 @@ describe('AddIssuableForm', () => {
]);
});
- it('emits an event with a "is_blocked_by" link type when the "is blocked by" radio input selected', async () => {
+ it('emits an event with a "is_blocked_by" link type when the "is blocked by" radio input selected', () => {
findRadioGroup().vm.$emit('input', linkedIssueTypesMap.IS_BLOCKED_BY);
findAddIssuableForm().trigger('submit');
diff --git a/spec/frontend/issues/issue_spec.js b/spec/frontend/issues/issue_spec.js
index f04e766a78c..3b8a09714a7 100644
--- a/spec/frontend/issues/issue_spec.js
+++ b/spec/frontend/issues/issue_spec.js
@@ -1,6 +1,8 @@
import { getByText } from '@testing-library/dom';
+import htmlOpenIssue from 'test_fixtures/issues/open-issue.html';
+import htmlClosedIssue from 'test_fixtures/issues/closed-issue.html';
import MockAdapter from 'axios-mock-adapter';
-import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
+import { setHTMLFixture, 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';
@@ -40,9 +42,9 @@ describe('Issue', () => {
`('$desc', ({ isIssueInitiallyOpen, expectedCounterText }) => {
beforeEach(() => {
if (isIssueInitiallyOpen) {
- loadHTMLFixture('issues/open-issue.html');
+ setHTMLFixture(htmlOpenIssue);
} else {
- loadHTMLFixture('issues/closed-issue.html');
+ setHTMLFixture(htmlClosedIssue);
}
testContext.issueCounter = getIssueCounter();
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 b28a08e2fce..15dde76f49b 100644
--- a/spec/frontend/issues/list/components/issues_list_app_spec.js
+++ b/spec/frontend/issues/list/components/issues_list_app_spec.js
@@ -201,7 +201,7 @@ describe('CE IssuesListApp component', () => {
return waitForPromises();
});
- it('renders', async () => {
+ it('renders', () => {
expect(findIssuableList().props()).toMatchObject({
namespace: defaultProvide.fullPath,
recentSearchesStorageKey: 'issues',
@@ -803,7 +803,7 @@ describe('CE IssuesListApp component', () => {
describe('when "sort" event is emitted by IssuableList', () => {
it.each(Object.keys(urlSortParams))(
'updates to the new sort when payload is `%s`',
- async (sortKey) => {
+ (sortKey) => {
// Ensure initial sort key is different so we can trigger an update when emitting a sort key
wrapper =
sortKey === CREATED_DESC
diff --git a/spec/frontend/issues/list/components/jira_issues_import_status_app_spec.js b/spec/frontend/issues/list/components/jira_issues_import_status_app_spec.js
index 7bbb5a954ae..81739f6ef1d 100644
--- a/spec/frontend/issues/list/components/jira_issues_import_status_app_spec.js
+++ b/spec/frontend/issues/list/components/jira_issues_import_status_app_spec.js
@@ -94,7 +94,7 @@ describe('JiraIssuesImportStatus', () => {
});
});
- describe('alert message', () => {
+ describe('alert', () => {
it('is hidden when dismissed', async () => {
wrapper = mountComponent({
shouldShowInProgressAlert: true,
diff --git a/spec/frontend/issues/new/components/type_select_spec.js b/spec/frontend/issues/new/components/type_select_spec.js
new file mode 100644
index 00000000000..a25ace10fe7
--- /dev/null
+++ b/spec/frontend/issues/new/components/type_select_spec.js
@@ -0,0 +1,141 @@
+import { GlCollapsibleListbox, GlIcon } from '@gitlab/ui';
+import { mount } from '@vue/test-utils';
+import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
+import * as urlUtility from '~/lib/utils/url_utility';
+import TypeSelect from '~/issues/new/components/type_select.vue';
+import { TYPE_ISSUE, TYPE_INCIDENT } from '~/issues/constants';
+import { __ } from '~/locale';
+
+const issuePath = 'issues/new';
+const incidentPath = 'issues/new?issuable_template=incident';
+const tracking = {
+ action: 'select_issue_type_incident',
+ label: 'select_issue_type_incident_dropdown_option',
+};
+
+const defaultProps = {
+ selectedType: '',
+ isIssueAllowed: true,
+ isIncidentAllowed: true,
+ issuePath,
+ incidentPath,
+};
+
+const issue = {
+ value: TYPE_ISSUE,
+ text: __('Issue'),
+ icon: 'issue-type-issue',
+ href: issuePath,
+};
+const incident = {
+ value: TYPE_INCIDENT,
+ text: __('Incident'),
+ icon: 'issue-type-incident',
+ href: incidentPath,
+ tracking,
+};
+
+describe('Issue type select component', () => {
+ let wrapper;
+ let trackingSpy;
+ let navigationSpy;
+
+ const createComponent = (props = {}) => {
+ wrapper = mount(TypeSelect, {
+ propsData: { ...defaultProps, ...props },
+ });
+ };
+
+ const findListbox = () => wrapper.findComponent(GlCollapsibleListbox);
+ const findAllIcons = () => wrapper.findAllComponents(GlIcon);
+ const findListboxItemIcon = () => findAllIcons().at(2);
+
+ describe('initial state', () => {
+ it('renders listbox with the correct header text', () => {
+ createComponent();
+
+ expect(findListbox().props('headerText')).toBe(TypeSelect.i18n.selectType);
+ });
+
+ it.each`
+ selectedType | toggleText
+ ${''} | ${TypeSelect.i18n.selectType}
+ ${TYPE_ISSUE} | ${TypeSelect.i18n.issuableType[TYPE_ISSUE]}
+ ${TYPE_INCIDENT} | ${TypeSelect.i18n.issuableType[TYPE_INCIDENT]}
+ `(
+ 'renders listbox with the correct toggle text when selectedType is "$selectedType"',
+ ({ selectedType, toggleText }) => {
+ createComponent({ selectedType });
+
+ expect(findListbox().props('toggleText')).toBe(toggleText);
+ },
+ );
+
+ it.each`
+ isIssueAllowed | isIncidentAllowed | items
+ ${true} | ${true} | ${[issue, incident]}
+ ${true} | ${false} | ${[issue]}
+ ${false} | ${true} | ${[incident]}
+ `(
+ 'renders listbox with the correct items when isIssueAllowed is "$isIssueAllowed" and isIncidentAllowed is "$isIncidentAllowed"',
+ ({ isIssueAllowed, isIncidentAllowed, items }) => {
+ createComponent({ isIssueAllowed, isIncidentAllowed });
+
+ expect(findListbox().props('items')).toMatchObject(items);
+ },
+ );
+
+ it.each`
+ isIssueAllowed | isIncidentAllowed | icon
+ ${true} | ${false} | ${issue.icon}
+ ${false} | ${true} | ${incident.icon}
+ `(
+ 'renders listbox item with the correct $icon icon',
+ ({ isIssueAllowed, isIncidentAllowed, icon }) => {
+ createComponent({ isIssueAllowed, isIncidentAllowed });
+ findListbox().vm.$emit('shown');
+
+ expect(findListboxItemIcon().props('name')).toBe(icon);
+ },
+ );
+ });
+
+ describe('on type selected', () => {
+ beforeEach(() => {
+ navigationSpy = jest.spyOn(urlUtility, 'visitUrl').mockReturnValue({});
+ trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
+ });
+
+ afterEach(() => {
+ unmockTracking();
+ navigationSpy.mockRestore();
+ });
+
+ it.each`
+ selectedType | expectedUrl
+ ${TYPE_ISSUE} | ${issuePath}
+ ${TYPE_INCIDENT} | ${incidentPath}
+ `('navigates to the $selectedType issuable page', ({ selectedType, expectedUrl }) => {
+ createComponent();
+ findListbox().vm.$emit('select', selectedType);
+
+ expect(navigationSpy).toHaveBeenCalledWith(expectedUrl);
+ });
+
+ it("doesn't call tracking APIs when tracking is not available for the issuable type", () => {
+ createComponent();
+ findListbox().vm.$emit('select', TYPE_ISSUE);
+
+ expect(trackingSpy).not.toHaveBeenCalled();
+ });
+
+ it('calls tracking APIs when tracking is available for the issuable type', () => {
+ createComponent();
+ findListbox().vm.$emit('select', TYPE_INCIDENT);
+
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, tracking.action, {
+ label: tracking.label,
+ });
+ });
+ });
+});
diff --git a/spec/frontend/issues/show/components/app_spec.js b/spec/frontend/issues/show/components/app_spec.js
index 1006f54eeaf..a4ee9a62926 100644
--- a/spec/frontend/issues/show/components/app_spec.js
+++ b/spec/frontend/issues/show/components/app_spec.js
@@ -1,6 +1,5 @@
import { GlIcon, GlIntersectionObserver } from '@gitlab/ui';
import MockAdapter from 'axios-mock-adapter';
-import { setHTMLFixture } from 'helpers/fixtures';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
@@ -100,22 +99,6 @@ describe('Issuable output', () => {
};
beforeEach(() => {
- setHTMLFixture(`
- <div>
- <title>Title</title>
- <div class="detail-page-description content-block">
- <details open>
- <summary>One</summary>
- </details>
- <details>
- <summary>Two</summary>
- </details>
- </div>
- <div class="flash-container"></div>
- <span id="task_status"></span>
- </div>
- `);
-
jest.spyOn(eventHub, '$emit');
axiosMock = new MockAdapter(axios);
@@ -348,7 +331,7 @@ describe('Issuable output', () => {
});
describe('when title is not in view', () => {
- beforeEach(async () => {
+ beforeEach(() => {
wrapper.findComponent(GlIntersectionObserver).vm.$emit('disappear');
});
diff --git a/spec/frontend/issues/show/components/description_spec.js b/spec/frontend/issues/show/components/description_spec.js
index 740b2f782e4..c6869573b2e 100644
--- a/spec/frontend/issues/show/components/description_spec.js
+++ b/spec/frontend/issues/show/components/description_spec.js
@@ -1,8 +1,6 @@
-import $ from 'jquery';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import getIssueDetailsQuery from 'ee_else_ce/work_items/graphql/get_issue_details.query.graphql';
-import setWindowLocation from 'helpers/set_window_location_helper';
import { TEST_HOST } from 'helpers/test_constants';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
@@ -21,13 +19,13 @@ import {
getIssueDetailsResponse,
projectWorkItemTypesQueryResponse,
} from 'jest/work_items/mock_data';
-import { descriptionProps as initialProps, descriptionHtmlWithList } from '../mock_data/mock_data';
+import {
+ descriptionProps as initialProps,
+ descriptionHtmlWithList,
+ descriptionHtmlWithDetailsTag,
+} from '../mock_data/mock_data';
jest.mock('~/alert');
-jest.mock('~/lib/utils/url_utility', () => ({
- ...jest.requireActual('~/lib/utils/url_utility'),
- updateHistory: jest.fn(),
-}));
jest.mock('~/task_list');
jest.mock('~/behaviors/markdown/render_gfm');
@@ -87,21 +85,6 @@ describe('Description component', () => {
beforeEach(() => {
window.gon = { sprite_icons: mockSpriteIcons };
-
- setWindowLocation(TEST_HOST);
-
- if (!document.querySelector('.issuable-meta')) {
- const metaData = document.createElement('div');
- metaData.classList.add('issuable-meta');
- metaData.innerHTML =
- '<div class="flash-container"></div><span id="task_status"></span><span id="task_status_short"></span>';
-
- document.body.appendChild(metaData);
- }
- });
-
- afterAll(() => {
- $('.issuable-meta .flash-container').remove();
});
it('doesnt animate first description changes', async () => {
@@ -132,6 +115,19 @@ describe('Description component', () => {
expect(findGfmContent().classes()).toContain('issue-realtime-trigger-pulse');
});
+ it('doesnt animate expand/collapse of details elements', async () => {
+ createComponent();
+
+ await wrapper.setProps({ descriptionHtml: descriptionHtmlWithDetailsTag.collapsed });
+ expect(findGfmContent().classes()).not.toContain('issue-realtime-pre-pulse');
+
+ await wrapper.setProps({ descriptionHtml: descriptionHtmlWithDetailsTag.expanded });
+ expect(findGfmContent().classes()).not.toContain('issue-realtime-pre-pulse');
+
+ await wrapper.setProps({ descriptionHtml: descriptionHtmlWithDetailsTag.collapsed });
+ expect(findGfmContent().classes()).not.toContain('issue-realtime-pre-pulse');
+ });
+
it('applies syntax highlighting and math when description changed', async () => {
createComponent();
@@ -166,7 +162,7 @@ describe('Description component', () => {
expect(TaskList).toHaveBeenCalled();
});
- it('does not re-init the TaskList when canUpdate is false', async () => {
+ it('does not re-init the TaskList when canUpdate is false', () => {
createComponent({
props: {
issuableType: 'issuableType',
@@ -202,46 +198,6 @@ describe('Description component', () => {
});
});
- describe('taskStatus', () => {
- it('adds full taskStatus', async () => {
- createComponent({
- props: {
- taskStatus: '1 of 1',
- },
- });
- await nextTick();
-
- expect(document.querySelector('.issuable-meta #task_status').textContent.trim()).toBe(
- '1 of 1',
- );
- });
-
- it('adds short taskStatus', async () => {
- createComponent({
- props: {
- taskStatus: '1 of 1',
- },
- });
- await nextTick();
-
- expect(document.querySelector('.issuable-meta #task_status_short').textContent.trim()).toBe(
- '1/1 checklist item',
- );
- });
-
- it('clears task status text when no tasks are present', async () => {
- createComponent({
- props: {
- taskStatus: '0 of 0',
- },
- });
-
- await nextTick();
-
- expect(document.querySelector('.issuable-meta #task_status').textContent.trim()).toBe('');
- });
- });
-
describe('with list', () => {
beforeEach(async () => {
createComponent({
diff --git a/spec/frontend/issues/show/components/edited_spec.js b/spec/frontend/issues/show/components/edited_spec.js
index a509627c347..dc0c7f5be46 100644
--- a/spec/frontend/issues/show/components/edited_spec.js
+++ b/spec/frontend/issues/show/components/edited_spec.js
@@ -1,41 +1,84 @@
+import { GlLink } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import { getTimeago } from '~/lib/utils/datetime_utility';
import Edited from '~/issues/show/components/edited.vue';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
-const timeago = getTimeago();
-
describe('Edited component', () => {
let wrapper;
- const findAuthorLink = () => wrapper.find('a');
+ const timeago = getTimeago();
+ const updatedAt = '2017-05-15T12:31:04.428Z';
+
+ const findAuthorLink = () => wrapper.findComponent(GlLink);
const findTimeAgoTooltip = () => wrapper.findComponent(TimeAgoTooltip);
const formatText = (text) => text.trim().replace(/\s\s+/g, ' ');
const mountComponent = (propsData) => mount(Edited, { propsData });
- const updatedAt = '2017-05-15T12:31:04.428Z';
- it('renders an edited at+by string', () => {
- wrapper = mountComponent({
- updatedAt,
- updatedByName: 'Some User',
- updatedByPath: '/some_user',
+ describe('task status section', () => {
+ describe('task status text', () => {
+ it('renders when there is a task status', () => {
+ wrapper = mountComponent({ taskCompletionStatus: { completed_count: 1, count: 3 } });
+
+ expect(wrapper.text()).toContain('1 of 3 checklist items completed');
+ });
+
+ it('does not render when task count is 0', () => {
+ wrapper = mountComponent({ taskCompletionStatus: { completed_count: 0, count: 0 } });
+
+ expect(wrapper.text()).not.toContain('0 of 0 checklist items completed');
+ });
});
- expect(formatText(wrapper.text())).toBe(`Edited ${timeago.format(updatedAt)} by Some User`);
- expect(findAuthorLink().attributes('href')).toBe('/some_user');
- expect(findTimeAgoTooltip().exists()).toBe(true);
+ describe('checkmark', () => {
+ it('renders when all tasks are completed', () => {
+ wrapper = mountComponent({ taskCompletionStatus: { completed_count: 3, count: 3 } });
+
+ expect(wrapper.text()).toContain('✓');
+ });
+
+ it('does not render when tasks are incomplete', () => {
+ wrapper = mountComponent({ taskCompletionStatus: { completed_count: 2, count: 3 } });
+
+ expect(wrapper.text()).not.toContain('✓');
+ });
+
+ it('does not render when task count is 0', () => {
+ wrapper = mountComponent({ taskCompletionStatus: { completed_count: 0, count: 0 } });
+
+ expect(wrapper.text()).not.toContain('✓');
+ });
+ });
+
+ describe('middot', () => {
+ it('renders when there is also "Edited by" text', () => {
+ wrapper = mountComponent({
+ taskCompletionStatus: { completed_count: 3, count: 3 },
+ updatedAt,
+ });
+
+ expect(wrapper.text()).toContain('·');
+ });
+
+ it('does not render when there is no "Edited by" text', () => {
+ wrapper = mountComponent({ taskCompletionStatus: { completed_count: 3, count: 3 } });
+
+ expect(wrapper.text()).not.toContain('·');
+ });
+ });
});
- it('if no updatedAt is provided, no time element will be rendered', () => {
+ it('renders an edited at+by string', () => {
wrapper = mountComponent({
+ updatedAt,
updatedByName: 'Some User',
updatedByPath: '/some_user',
});
- expect(formatText(wrapper.text())).toBe('Edited by Some User');
+ expect(formatText(wrapper.text())).toBe(`Edited ${timeago.format(updatedAt)} by Some User`);
expect(findAuthorLink().attributes('href')).toBe('/some_user');
- expect(findTimeAgoTooltip().exists()).toBe(false);
+ expect(findTimeAgoTooltip().exists()).toBe(true);
});
it('if no updatedByName and updatedByPath is provided, no user element will be rendered', () => {
diff --git a/spec/frontend/issues/show/components/fields/description_spec.js b/spec/frontend/issues/show/components/fields/description_spec.js
index 5c145ed4707..c7116f380a1 100644
--- a/spec/frontend/issues/show/components/fields/description_spec.js
+++ b/spec/frontend/issues/show/components/fields/description_spec.js
@@ -81,11 +81,8 @@ describe('Description field component', () => {
autofocus: true,
supportsQuickActions: true,
quickActionsDocsPath: expect.any(String),
- });
-
- expect(findMarkdownEditor().vm.$attrs).toMatchObject({
- 'enable-autocomplete': true,
- 'markdown-docs-path': '/',
+ markdownDocsPath: '/',
+ enableAutocomplete: true,
});
});
diff --git a/spec/frontend/issues/show/components/header_actions_spec.js b/spec/frontend/issues/show/components/header_actions_spec.js
index 58ec7387851..db3435855f6 100644
--- a/spec/frontend/issues/show/components/header_actions_spec.js
+++ b/spec/frontend/issues/show/components/header_actions_spec.js
@@ -440,7 +440,7 @@ describe('HeaderActions component', () => {
wrapper = mountComponent({ props: { isIssueAuthor: false } });
});
- it("doesn't render", async () => {
+ it("doesn't render", () => {
expect(findAbuseCategorySelector().exists()).toEqual(false);
});
diff --git a/spec/frontend/issues/show/components/incidents/create_timeline_events_form_spec.js b/spec/frontend/issues/show/components/incidents/create_timeline_events_form_spec.js
index 6b68e7a0da6..b13a1041eda 100644
--- a/spec/frontend/issues/show/components/incidents/create_timeline_events_form_spec.js
+++ b/spec/frontend/issues/show/components/incidents/create_timeline_events_form_spec.js
@@ -86,7 +86,6 @@ describe('Create Timeline events', () => {
provide: {
fullPath: 'group/project',
issuableId: '1',
- glFeatures: { incidentEventTags: true },
},
apolloProvider,
});
diff --git a/spec/frontend/issues/show/components/incidents/incident_tabs_spec.js b/spec/frontend/issues/show/components/incidents/incident_tabs_spec.js
index 0f4fb02a40b..5a49b29c458 100644
--- a/spec/frontend/issues/show/components/incidents/incident_tabs_spec.js
+++ b/spec/frontend/issues/show/components/incidents/incident_tabs_spec.js
@@ -163,7 +163,7 @@ describe('Incident Tabs component', () => {
mountComponent({ mount: mountExtended });
});
- it('shows only the summary tab by default', async () => {
+ it('shows only the summary tab by default', () => {
expect(findActiveTabs()).toHaveLength(1);
expect(findActiveTabs().at(0).text()).toBe(incidentTabsI18n.summaryTitle);
});
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 af01fd34336..9c4662ce38f 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
@@ -113,17 +113,7 @@ describe('Timeline events form', () => {
]);
});
- describe('with incident_event_tag feature flag enabled', () => {
- beforeEach(() => {
- mountComponent(
- {},
- {},
- {
- incidentEventTags: true,
- },
- );
- });
-
+ describe('Event Tags', () => {
describe('event tags listbox', () => {
it('should render option list from provided array', () => {
expect(findTagsListbox().props('items')).toEqual(mockTags);
@@ -255,7 +245,7 @@ describe('Timeline events form', () => {
expect(findMinuteInput().element.value).toBe('0');
});
- it('should disable the save buttons when event content does not exist', async () => {
+ it('should disable the save buttons when event content does not exist', () => {
expect(findSubmitButton().props('disabled')).toBe(true);
expect(findSubmitAndAddButton().props('disabled')).toBe(true);
});
diff --git a/spec/frontend/issues/show/components/locked_warning_spec.js b/spec/frontend/issues/show/components/locked_warning_spec.js
index f8a8c999632..2e786b665d0 100644
--- a/spec/frontend/issues/show/components/locked_warning_spec.js
+++ b/spec/frontend/issues/show/components/locked_warning_spec.js
@@ -35,11 +35,11 @@ describe('LockedWarning component', () => {
expect(alert.props('dismissible')).toBe(false);
});
- it(`displays correct message`, async () => {
+ it(`displays correct message`, () => {
expect(alert.text()).toMatchInterpolatedText(sprintf(i18n.alertMessage, { issuableType }));
});
- it(`displays a link with correct text`, async () => {
+ it(`displays a link with correct text`, () => {
expect(link.exists()).toBe(true);
expect(link.text()).toBe(`the ${issuableType}`);
});
diff --git a/spec/frontend/issues/show/mock_data/mock_data.js b/spec/frontend/issues/show/mock_data/mock_data.js
index 86d09665947..ed969a08ac5 100644
--- a/spec/frontend/issues/show/mock_data/mock_data.js
+++ b/spec/frontend/issues/show/mock_data/mock_data.js
@@ -5,7 +5,7 @@ export const initialRequest = {
title_text: 'this is a title',
description: '<p>this is a description!</p>',
description_text: 'this is a description',
- task_status: '2 of 4 completed',
+ task_completion_status: { completed_count: 2, count: 4 },
updated_at: '2015-05-15T12:31:04.428Z',
updated_by_name: 'Some User',
updated_by_path: '/some_user',
@@ -17,7 +17,7 @@ export const secondRequest = {
title_text: '2',
description: '<p>42</p>',
description_text: '42',
- task_status: '0 of 0 completed',
+ task_completion_status: { completed_count: 0, count: 0 },
updated_at: '2016-05-15T12:31:04.428Z',
updated_by_name: 'Other User',
updated_by_path: '/other_user',
@@ -30,7 +30,7 @@ export const putRequest = {
title_text: 'PUT',
description: '<p>PUT_DESC</p>',
description_text: 'PUT_DESC',
- task_status: '0 of 0 completed',
+ task_completion_status: { completed_count: 0, count: 0 },
updated_at: '2016-05-15T12:31:04.428Z',
updated_by_name: 'Other User',
updated_by_path: '/other_user',
@@ -41,7 +41,6 @@ export const descriptionProps = {
canUpdate: true,
descriptionHtml: 'test',
descriptionText: 'test',
- taskStatus: '',
updateUrl: TEST_HOST,
};
@@ -60,6 +59,7 @@ export const appProps = {
initialTitleText: '',
initialDescriptionHtml: 'test',
initialDescriptionText: 'test',
+ initialTaskCompletionStatus: { completed_count: 2, count: 4 },
lockVersion: 1,
issueType: 'issue',
markdownPreviewPath: '/',
@@ -79,3 +79,16 @@ export const descriptionHtmlWithList = `
<li data-sourcepos="3:1-3:8">todo 3</li>
</ul>
`;
+
+export const descriptionHtmlWithDetailsTag = {
+ expanded: `
+ <details open="true">
+ <summary>Section 1</summary>
+ <p>Data</p>
+ </details>'`,
+ collapsed: `
+ <details>
+ <summary>Section 1</summary>
+ <p>Data</p>
+ </details>'`,
+};
diff --git a/spec/frontend/jira_connect/branches/components/source_branch_dropdown_spec.js b/spec/frontend/jira_connect/branches/components/source_branch_dropdown_spec.js
index 701512953df..a3bc8e861b2 100644
--- a/spec/frontend/jira_connect/branches/components/source_branch_dropdown_spec.js
+++ b/spec/frontend/jira_connect/branches/components/source_branch_dropdown_spec.js
@@ -147,7 +147,7 @@ describe('SourceBranchDropdown', () => {
});
describe('when selecting a listbox item', () => {
- it('emits `change` event with the selected branch name', async () => {
+ it('emits `change` event with the selected branch name', () => {
const mockBranchName = mockProject.repository.branchNames[1];
findListbox().vm.$emit('select', mockBranchName);
expect(wrapper.emitted('change')[1]).toEqual([mockBranchName]);
@@ -157,7 +157,7 @@ describe('SourceBranchDropdown', () => {
describe('when `selectedBranchName` prop is specified', () => {
const mockBranchName = mockProject.repository.branchNames[2];
- beforeEach(async () => {
+ beforeEach(() => {
wrapper.setProps({
selectedBranchName: mockBranchName,
});
diff --git a/spec/frontend/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/index_spec.js b/spec/frontend/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/index_spec.js
index d99d8986296..ce6861ff460 100644
--- a/spec/frontend/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/index_spec.js
+++ b/spec/frontend/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/index_spec.js
@@ -6,7 +6,7 @@ import SignInGitlabMultiversion from '~/jira_connect/subscriptions/pages/sign_in
import SignInOauthButton from '~/jira_connect/subscriptions/components/sign_in_oauth_button.vue';
import VersionSelectForm from '~/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/version_select_form.vue';
-import { updateInstallation } from '~/jira_connect/subscriptions/api';
+import { updateInstallation, setApiBaseURL } from '~/jira_connect/subscriptions/api';
import { reloadPage, persistBaseUrl, retrieveBaseUrl } from '~/jira_connect/subscriptions/utils';
import { GITLAB_COM_BASE_PATH } from '~/jira_connect/subscriptions/constants';
@@ -72,6 +72,10 @@ describe('SignInGitlabMultiversion', () => {
expect(findSetupInstructions().exists()).toBe(true);
});
+ it('calls setApiBaseURL with correct params', () => {
+ expect(setApiBaseURL).toHaveBeenCalledWith(mockBasePath);
+ });
+
describe('when SetupInstructions emits `next` event', () => {
beforeEach(async () => {
findSetupInstructions().vm.$emit('next');
@@ -102,6 +106,10 @@ describe('SignInGitlabMultiversion', () => {
expect(findSignInOauthButton().props('gitlabBasePath')).toBe(GITLAB_COM_BASE_PATH);
});
+ it('does not call setApiBaseURL', () => {
+ expect(setApiBaseURL).not.toHaveBeenCalled();
+ });
+
describe('when button emits `sign-in` event', () => {
it('emits `sign-in-oauth` event', () => {
const button = findSignInOauthButton();
diff --git a/spec/frontend/jira_import/components/jira_import_form_spec.js b/spec/frontend/jira_import/components/jira_import_form_spec.js
index c7db9f429de..7fd6398aaa4 100644
--- a/spec/frontend/jira_import/components/jira_import_form_spec.js
+++ b/spec/frontend/jira_import/components/jira_import_form_spec.js
@@ -304,7 +304,7 @@ describe('JiraImportForm', () => {
expect(getContinueButton().text()).toBe('Continue');
});
- it('is in loading state when the form is submitting', async () => {
+ it('is in loading state when the form is submitting', () => {
wrapper = mountComponent({ isSubmitting: true });
expect(getContinueButton().props('loading')).toBe(true);
@@ -416,7 +416,7 @@ describe('JiraImportForm', () => {
wrapper = mountComponent({ hasMoreUsers: true });
});
- it('calls the GraphQL user mapping mutation', async () => {
+ it('calls the GraphQL user mapping mutation', () => {
const mutationArguments = {
mutation: getJiraUserMappingMutation,
variables: {
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 98b9ca78a45..c8c865dd28e 100644
--- a/spec/frontend/jobs/components/job/manual_variables_form_spec.js
+++ b/spec/frontend/jobs/components/job/manual_variables_form_spec.js
@@ -54,7 +54,7 @@ describe('Manual Variables Form', () => {
});
};
- const createComponentWithApollo = async ({ props = {} } = {}) => {
+ const createComponentWithApollo = ({ props = {} } = {}) => {
const requestHandlers = [[getJobQuery, getJobQueryResponse]];
mockApollo = createMockApollo(requestHandlers);
@@ -309,7 +309,7 @@ describe('Manual Variables Form', () => {
await createComponentWithApollo();
});
- it('delete variable button placeholder should only exist when a user cannot remove', async () => {
+ it('delete variable button placeholder should only exist when a user cannot remove', () => {
expect(findDeleteVarBtnPlaceholder().exists()).toBe(true);
});
diff --git a/spec/frontend/jobs/components/job/sidebar_header_spec.js b/spec/frontend/jobs/components/job/sidebar_header_spec.js
index da97945f9bf..cf182330578 100644
--- a/spec/frontend/jobs/components/job/sidebar_header_spec.js
+++ b/spec/frontend/jobs/components/job/sidebar_header_spec.js
@@ -31,7 +31,7 @@ describe('Sidebar Header', () => {
});
};
- const createComponentWithApollo = async ({ props = {}, restJob = {} } = {}) => {
+ const createComponentWithApollo = ({ props = {}, restJob = {} } = {}) => {
const getJobQueryResponse = jest.fn().mockResolvedValue(mockJobResponse);
const requestHandlers = [[getJobQuery, getJobQueryResponse]];
diff --git a/spec/frontend/jobs/components/job/sidebar_spec.js b/spec/frontend/jobs/components/job/sidebar_spec.js
index cefa4582c15..fbff64b4d78 100644
--- a/spec/frontend/jobs/components/job/sidebar_spec.js
+++ b/spec/frontend/jobs/components/job/sidebar_spec.js
@@ -139,7 +139,7 @@ describe('Sidebar details block', () => {
return store.dispatch('receiveJobsForStageSuccess', jobsInStage.latest_statuses);
});
- it('renders list of jobs', async () => {
+ it('renders list of jobs', () => {
expect(findJobsContainer().exists()).toBe(true);
});
});
@@ -147,7 +147,7 @@ describe('Sidebar details block', () => {
describe('when job data changes', () => {
const stageArg = job.pipeline.details.stages.find((stage) => stage.name === job.stage);
- beforeEach(async () => {
+ beforeEach(() => {
jest.spyOn(store, 'dispatch');
});
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 55fe534aa3b..79bc765f181 100644
--- a/spec/frontend/jobs/components/table/cells/actions_cell_spec.js
+++ b/spec/frontend/jobs/components/table/cells/actions_cell_spec.js
@@ -122,7 +122,7 @@ describe('Job actions cell', () => {
${findPlayButton} | ${'play'} | ${playableJob} | ${JobPlayMutation} | ${playMutationHandler} | ${playableJob.id}
${findRetryButton} | ${'retry'} | ${retryableJob} | ${JobRetryMutation} | ${retryMutationHandler} | ${retryableJob.id}
${findCancelButton} | ${'cancel'} | ${cancelableJob} | ${JobCancelMutation} | ${cancelMutationHandler} | ${cancelableJob.id}
- `('performs the $action mutation', async ({ button, jobType, mutationFile, handler, jobId }) => {
+ `('performs the $action mutation', ({ button, jobType, mutationFile, handler, jobId }) => {
createComponent(jobType, [[mutationFile, handler]]);
button().vm.$emit('click');
diff --git a/spec/frontend/jobs/components/table/job_table_app_spec.js b/spec/frontend/jobs/components/table/job_table_app_spec.js
index 6247cfcc640..19033c227d9 100644
--- a/spec/frontend/jobs/components/table/job_table_app_spec.js
+++ b/spec/frontend/jobs/components/table/job_table_app_spec.js
@@ -275,5 +275,42 @@ describe('Job table app', () => {
url: `${TEST_HOST}/?statuses=FAILED`,
});
});
+
+ it('resets query param after clearing tokens', () => {
+ createComponent();
+
+ jest.spyOn(urlUtils, 'updateHistory');
+
+ findFilteredSearch().vm.$emit('filterJobsBySearch', [mockFailedSearchToken]);
+
+ expect(successHandler).toHaveBeenCalledWith({
+ first: 30,
+ fullPath: 'gitlab-org/gitlab',
+ statuses: 'FAILED',
+ });
+ expect(countSuccessHandler).toHaveBeenCalledWith({
+ fullPath: 'gitlab-org/gitlab',
+ statuses: 'FAILED',
+ });
+ expect(urlUtils.updateHistory).toHaveBeenCalledWith({
+ url: `${TEST_HOST}/?statuses=FAILED`,
+ });
+
+ findFilteredSearch().vm.$emit('filterJobsBySearch', []);
+
+ expect(urlUtils.updateHistory).toHaveBeenCalledWith({
+ url: `${TEST_HOST}/`,
+ });
+
+ expect(successHandler).toHaveBeenCalledWith({
+ first: 30,
+ fullPath: 'gitlab-org/gitlab',
+ statuses: null,
+ });
+ expect(countSuccessHandler).toHaveBeenCalledWith({
+ fullPath: 'gitlab-org/gitlab',
+ statuses: null,
+ });
+ });
});
});
diff --git a/spec/frontend/jobs/mock_data.js b/spec/frontend/jobs/mock_data.js
index 483b4ca711f..fb1ded7b4ef 100644
--- a/spec/frontend/jobs/mock_data.js
+++ b/spec/frontend/jobs/mock_data.js
@@ -1,7 +1,9 @@
import mockJobsCount from 'test_fixtures/graphql/jobs/get_jobs_count.query.graphql.json';
import mockJobsEmpty from 'test_fixtures/graphql/jobs/get_jobs.query.graphql.empty.json';
import mockJobsPaginated from 'test_fixtures/graphql/jobs/get_jobs.query.graphql.paginated.json';
+import mockAllJobsPaginated from 'test_fixtures/graphql/jobs/get_all_jobs.query.graphql.paginated.json';
import mockJobs from 'test_fixtures/graphql/jobs/get_jobs.query.graphql.json';
+import mockAllJobs from 'test_fixtures/graphql/jobs/get_all_jobs.query.graphql.json';
import mockJobsAsGuest from 'test_fixtures/graphql/jobs/get_jobs.query.graphql.as_guest.json';
import { TEST_HOST } from 'spec/test_constants';
import { TOKEN_TYPE_STATUS } from '~/vue_shared/components/filtered_search_bar/constants';
@@ -11,8 +13,10 @@ threeWeeksAgo.setDate(threeWeeksAgo.getDate() - 21);
// Fixtures generated at spec/frontend/fixtures/jobs.rb
export const mockJobsResponsePaginated = mockJobsPaginated;
+export const mockAllJobsResponsePaginated = mockAllJobsPaginated;
export const mockJobsResponseEmpty = mockJobsEmpty;
export const mockJobsNodes = mockJobs.data.project.jobs.nodes;
+export const mockAllJobsNodes = mockAllJobs.data.jobs.nodes;
export const mockJobsNodesAsGuest = mockJobsAsGuest.data.project.jobs.nodes;
export const mockJobsCountResponse = mockJobsCount;
@@ -922,6 +926,14 @@ export const stages = [
},
];
+export const statuses = {
+ success: 'SUCCESS',
+ failed: 'FAILED',
+ canceled: 'CANCELED',
+ pending: 'PENDING',
+ running: 'RUNNING',
+};
+
export default {
id: 4757,
artifact: {
diff --git a/spec/frontend/labels/components/delete_label_modal_spec.js b/spec/frontend/labels/components/delete_label_modal_spec.js
index 7654d218209..19aef42528a 100644
--- a/spec/frontend/labels/components/delete_label_modal_spec.js
+++ b/spec/frontend/labels/components/delete_label_modal_spec.js
@@ -1,60 +1,55 @@
import { GlModal } from '@gitlab/ui';
-import { mount } from '@vue/test-utils';
import { stubComponent } from 'helpers/stub_component';
import { TEST_HOST } from 'helpers/test_constants';
-import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
import DeleteLabelModal from '~/labels/components/delete_label_modal.vue';
-const MOCK_MODAL_DATA = {
- labelName: 'label 1',
- subjectName: 'GitLab Org',
- destroyPath: `${TEST_HOST}/1`,
-};
-
describe('~/labels/components/delete_label_modal', () => {
let wrapper;
- const createComponent = () => {
- wrapper = extendedWrapper(
- mount(DeleteLabelModal, {
- propsData: {
- selector: '.js-test-btn',
- },
- stubs: {
- GlModal: stubComponent(GlModal, {
- template:
- '<div><slot name="modal-title"></slot><slot></slot><slot name="modal-footer"></slot></div>',
- }),
- },
- }),
- );
+ const mountComponent = () => {
+ const button = document.createElement('button');
+ button.classList.add('js-test-btn');
+ button.dataset.destroyPath = `${TEST_HOST}/1`;
+ button.dataset.labelName = 'label 1';
+ button.dataset.subjectName = 'GitLab Org';
+ document.body.append(button);
+
+ wrapper = mountExtended(DeleteLabelModal, {
+ propsData: {
+ selector: '.js-test-btn',
+ },
+ stubs: {
+ GlModal: stubComponent(GlModal, {
+ template:
+ '<div><slot name="modal-title"></slot><slot></slot><slot name="modal-footer"></slot></div>',
+ }),
+ },
+ });
+
+ button.click();
};
const findModal = () => wrapper.findComponent(GlModal);
- const findPrimaryModalButton = () => wrapper.findByTestId('delete-button');
-
- describe('template', () => {
- describe('when modal data is set', () => {
- beforeEach(() => {
- createComponent();
- wrapper.vm.labelName = MOCK_MODAL_DATA.labelName;
- wrapper.vm.subjectName = MOCK_MODAL_DATA.subjectName;
- wrapper.vm.destroyPath = MOCK_MODAL_DATA.destroyPath;
- });
-
- it('renders GlModal', () => {
- expect(findModal().exists()).toBe(true);
- });
-
- it('displays the label name and subject name', () => {
- expect(findModal().text()).toContain(
- `${MOCK_MODAL_DATA.labelName} will be permanently deleted from ${MOCK_MODAL_DATA.subjectName}. This cannot be undone`,
- );
- });
-
- it('passes the destroyPath to the button', () => {
- expect(findPrimaryModalButton().attributes('href')).toBe(MOCK_MODAL_DATA.destroyPath);
- });
+ const findDeleteButton = () => wrapper.findByRole('link', { name: 'Delete label' });
+
+ describe('when modal data is set', () => {
+ beforeEach(() => {
+ mountComponent();
+ });
+
+ it('renders GlModal', () => {
+ expect(findModal().exists()).toBe(true);
+ });
+
+ it('displays the label name and subject name', () => {
+ expect(findModal().text()).toContain(
+ `label 1 will be permanently deleted from GitLab Org. This cannot be undone`,
+ );
+ });
+
+ it('passes the destroyPath to the button', () => {
+ expect(findDeleteButton().attributes('href')).toBe('http://test.host/1');
});
});
});
diff --git a/spec/frontend/lib/apollo/indexed_db_persistent_storage_spec.js b/spec/frontend/lib/apollo/indexed_db_persistent_storage_spec.js
new file mode 100644
index 00000000000..f96364a918e
--- /dev/null
+++ b/spec/frontend/lib/apollo/indexed_db_persistent_storage_spec.js
@@ -0,0 +1,90 @@
+import { IndexedDBPersistentStorage } from '~/lib/apollo/indexed_db_persistent_storage';
+import { db } from '~/lib/apollo/local_db';
+import CACHE_WITH_PERSIST_DIRECTIVE_AND_FIELDS from './mock_data/cache_with_persist_directive_and_field.json';
+
+describe('IndexedDBPersistentStorage', () => {
+ let subject;
+
+ const seedData = async (cacheKey, data = CACHE_WITH_PERSIST_DIRECTIVE_AND_FIELDS) => {
+ const { ROOT_QUERY, ...rest } = data;
+
+ await db.table('queries').put(ROOT_QUERY, cacheKey);
+
+ const asyncPuts = Object.entries(rest).map(async ([key, value]) => {
+ const {
+ groups: { type, gid },
+ } = /^(?<type>.+?):(?<gid>.+)$/.exec(key);
+ const tableName = type.toLowerCase();
+
+ if (tableName !== 'projectmember' && tableName !== 'groupmember') {
+ await db.table(tableName).put(value, gid);
+ }
+ });
+
+ await Promise.all(asyncPuts);
+ };
+
+ beforeEach(async () => {
+ subject = await IndexedDBPersistentStorage.create();
+ });
+
+ afterEach(() => {
+ db.close();
+ });
+
+ it('returns empty response if there is nothing stored in the DB', async () => {
+ const result = await subject.getItem('some-query');
+
+ expect(result).toEqual({});
+ });
+
+ it('returns stored cache if cache was persisted in IndexedDB', async () => {
+ await seedData('issues_list', CACHE_WITH_PERSIST_DIRECTIVE_AND_FIELDS);
+
+ const result = await subject.getItem('issues_list');
+ expect(result).toEqual(CACHE_WITH_PERSIST_DIRECTIVE_AND_FIELDS);
+ });
+
+ it('puts the results in database on `setItem` call', async () => {
+ await subject.setItem(
+ 'issues_list',
+ JSON.stringify({
+ ROOT_QUERY: 'ROOT_QUERY_KEY',
+ 'Project:gid://gitlab/Project/6': {
+ __typename: 'Project',
+ id: 'gid://gitlab/Project/6',
+ },
+ }),
+ );
+
+ await expect(db.table('queries').get('issues_list')).resolves.toEqual('ROOT_QUERY_KEY');
+ await expect(db.table('project').get('gid://gitlab/Project/6')).resolves.toEqual({
+ __typename: 'Project',
+ id: 'gid://gitlab/Project/6',
+ });
+ });
+
+ it('does not put results into non-existent table', async () => {
+ const queryId = 'issues_list';
+
+ await subject.setItem(
+ queryId,
+ JSON.stringify({
+ ROOT_QUERY: 'ROOT_QUERY_KEY',
+ 'DNE:gid://gitlab/DNE/1': {},
+ }),
+ );
+
+ expect(db.tables.map((x) => x.name)).not.toContain('dne');
+ });
+
+ it('when removeItem is called, clears all data', async () => {
+ await seedData('issues_list', CACHE_WITH_PERSIST_DIRECTIVE_AND_FIELDS);
+
+ await subject.removeItem();
+
+ const actual = await Promise.all(db.tables.map((x) => x.toArray()));
+
+ expect(actual).toEqual(db.tables.map(() => []));
+ });
+});
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
index c0651517986..0dfc2240cc3 100644
--- 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
@@ -171,38 +171,6 @@
}
]
},
- "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": [
@@ -1999,125 +1967,6 @@
"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",
diff --git a/spec/frontend/lib/apollo/persist_link_spec.js b/spec/frontend/lib/apollo/persist_link_spec.js
index ddb861bcee0..f3afc4ba8cd 100644
--- a/spec/frontend/lib/apollo/persist_link_spec.js
+++ b/spec/frontend/lib/apollo/persist_link_spec.js
@@ -56,7 +56,7 @@ describe('~/lib/apollo/persist_link', () => {
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 () => {
+ it('decorates the response with `__persist: true` is there is `__persist` field in the query', () => {
const link = getPersistLink().concat(terminatingLink);
subscription = execute(link, { query: QUERY_WITH_PERSIST_FIELD }).subscribe(({ data }) => {
@@ -64,7 +64,7 @@ describe('~/lib/apollo/persist_link', () => {
});
});
- it('does not decorate the response with `__persist: true` is there if query is not persistent', async () => {
+ it('does not decorate the response with `__persist: true` is there if query is not persistent', () => {
const link = getPersistLink().concat(terminatingLink);
subscription = execute(link, { query: DEFAULT_QUERY }).subscribe(({ data }) => {
diff --git a/spec/frontend/lib/utils/chart_utils_spec.js b/spec/frontend/lib/utils/chart_utils_spec.js
index 65bb68c5017..3b34b0ef672 100644
--- a/spec/frontend/lib/utils/chart_utils_spec.js
+++ b/spec/frontend/lib/utils/chart_utils_spec.js
@@ -1,4 +1,8 @@
-import { firstAndLastY } from '~/lib/utils/chart_utils';
+import { firstAndLastY, getToolboxOptions } from '~/lib/utils/chart_utils';
+import { __ } from '~/locale';
+import * as iconUtils from '~/lib/utils/icon_utils';
+
+jest.mock('~/lib/utils/icon_utils');
describe('Chart utils', () => {
describe('firstAndLastY', () => {
@@ -12,4 +16,53 @@ describe('Chart utils', () => {
expect(firstAndLastY(data)).toEqual([1, 3]);
});
});
+
+ describe('getToolboxOptions', () => {
+ describe('when icons are successfully fetched', () => {
+ beforeEach(() => {
+ iconUtils.getSvgIconPathContent.mockImplementation((name) =>
+ Promise.resolve(`${name}-svg-path-mock`),
+ );
+ });
+
+ it('returns toolbox config', async () => {
+ await expect(getToolboxOptions()).resolves.toEqual({
+ toolbox: {
+ feature: {
+ dataZoom: {
+ icon: {
+ zoom: 'path://marquee-selection-svg-path-mock',
+ back: 'path://redo-svg-path-mock',
+ },
+ },
+ restore: {
+ icon: 'path://repeat-svg-path-mock',
+ },
+ saveAsImage: {
+ icon: 'path://download-svg-path-mock',
+ },
+ },
+ },
+ });
+ });
+ });
+
+ describe('when icons are not successfully fetched', () => {
+ const error = new Error();
+
+ beforeEach(() => {
+ iconUtils.getSvgIconPathContent.mockRejectedValue(error);
+ jest.spyOn(console, 'warn').mockImplementation();
+ });
+
+ it('returns empty object and calls `console.warn`', async () => {
+ await expect(getToolboxOptions()).resolves.toEqual({});
+ // eslint-disable-next-line no-console
+ expect(console.warn).toHaveBeenCalledWith(
+ __('SVG could not be rendered correctly: '),
+ error,
+ );
+ });
+ });
+ });
});
diff --git a/spec/frontend/lib/utils/color_utils_spec.js b/spec/frontend/lib/utils/color_utils_spec.js
index 87966cf9fba..a5580a3d8d6 100644
--- a/spec/frontend/lib/utils/color_utils_spec.js
+++ b/spec/frontend/lib/utils/color_utils_spec.js
@@ -63,7 +63,7 @@ describe('Color utils', () => {
${'groups:issues:index'} | ${'gl-dark'} | ${'monokai-light'} | ${true}
`(
'is $expected on $page with $bodyClass body class and $ideTheme IDE theme',
- async ({ page, bodyClass, ideTheme, expected }) => {
+ ({ page, bodyClass, ideTheme, expected }) => {
document.body.outerHTML = `<body class="${bodyClass}" data-page="${page}"></body>`;
window.gon = {
user_color_scheme: ideTheme,
diff --git a/spec/frontend/lib/utils/datetime/time_spent_utility_spec.js b/spec/frontend/lib/utils/datetime/time_spent_utility_spec.js
new file mode 100644
index 00000000000..15e056e45d0
--- /dev/null
+++ b/spec/frontend/lib/utils/datetime/time_spent_utility_spec.js
@@ -0,0 +1,25 @@
+import { formatTimeSpent } from '~/lib/utils/datetime/time_spent_utility';
+
+describe('Time spent utils', () => {
+ describe('formatTimeSpent', () => {
+ describe('with limitToHours false', () => {
+ it('formats 34500 seconds to `1d 1h 35m`', () => {
+ expect(formatTimeSpent(34500)).toEqual('1d 1h 35m');
+ });
+
+ it('formats -34500 seconds to `- 1d 1h 35m`', () => {
+ expect(formatTimeSpent(-34500)).toEqual('- 1d 1h 35m');
+ });
+ });
+
+ describe('with limitToHours true', () => {
+ it('formats 34500 seconds to `9h 35m`', () => {
+ expect(formatTimeSpent(34500, true)).toEqual('9h 35m');
+ });
+
+ it('formats -34500 seconds to `- 9h 35m`', () => {
+ expect(formatTimeSpent(-34500, true)).toEqual('- 9h 35m');
+ });
+ });
+ });
+});
diff --git a/spec/frontend/lib/utils/error_message_spec.js b/spec/frontend/lib/utils/error_message_spec.js
index 17b5168c32f..d55a6de06c3 100644
--- a/spec/frontend/lib/utils/error_message_spec.js
+++ b/spec/frontend/lib/utils/error_message_spec.js
@@ -1,65 +1,48 @@
-import { parseErrorMessage, USER_FACING_ERROR_MESSAGE_PREFIX } from '~/lib/utils/error_message';
+import { parseErrorMessage } from '~/lib/utils/error_message';
-const defaultErrorMessage = 'Something caused this error';
-const userFacingErrorMessage = 'User facing error message';
-const nonUserFacingErrorMessage = 'NonUser facing error message';
-const genericErrorMessage = 'Some error message';
+const defaultErrorMessage = 'Default error message';
+const errorMessage = 'Returned error message';
-describe('error message', () => {
- describe('when given an errormessage object', () => {
- const errorMessageObject = {
- options: {
- cause: defaultErrorMessage,
- },
- filename: 'error.js',
- linenumber: 7,
- };
+const generateErrorWithMessage = (message) => {
+ return {
+ message,
+ };
+};
- it('returns the correct values for userfacing errors', () => {
- const userFacingObject = errorMessageObject;
- userFacingObject.message = `${USER_FACING_ERROR_MESSAGE_PREFIX} ${userFacingErrorMessage}`;
-
- expect(parseErrorMessage(userFacingObject)).toEqual({
- message: userFacingErrorMessage,
- userFacing: true,
- });
- });
-
- it('returns the correct values for non userfacing errors', () => {
- const nonUserFacingObject = errorMessageObject;
- nonUserFacingObject.message = nonUserFacingErrorMessage;
-
- expect(parseErrorMessage(nonUserFacingObject)).toEqual({
- message: nonUserFacingErrorMessage,
- userFacing: false,
- });
- });
+describe('parseErrorMessage', () => {
+ const ufErrorPrefix = 'Foo:';
+ beforeEach(() => {
+ gon.uf_error_prefix = ufErrorPrefix;
});
- describe('when given an errormessage string', () => {
- it('returns the correct values for userfacing errors', () => {
- expect(
- parseErrorMessage(`${USER_FACING_ERROR_MESSAGE_PREFIX} ${genericErrorMessage}`),
- ).toEqual({
- message: genericErrorMessage,
- userFacing: true,
- });
- });
-
- it('returns the correct values for non userfacing errors', () => {
- expect(parseErrorMessage(genericErrorMessage)).toEqual({
- message: genericErrorMessage,
- userFacing: false,
- });
- });
- });
-
- describe('when given nothing', () => {
- it('returns an empty error message', () => {
- expect(parseErrorMessage()).toEqual({
- message: '',
- userFacing: false,
- });
- });
- });
+ it.each`
+ error | expectedResult
+ ${`${ufErrorPrefix} ${errorMessage}`} | ${errorMessage}
+ ${`${errorMessage} ${ufErrorPrefix}`} | ${defaultErrorMessage}
+ ${errorMessage} | ${defaultErrorMessage}
+ ${undefined} | ${defaultErrorMessage}
+ ${''} | ${defaultErrorMessage}
+ `(
+ 'properly parses "$error" error object and returns "$expectedResult"',
+ ({ error, expectedResult }) => {
+ const errorObject = generateErrorWithMessage(error);
+ expect(parseErrorMessage(errorObject, defaultErrorMessage)).toEqual(expectedResult);
+ },
+ );
+
+ it.each`
+ error | defaultMessage | expectedResult
+ ${undefined} | ${defaultErrorMessage} | ${defaultErrorMessage}
+ ${''} | ${defaultErrorMessage} | ${defaultErrorMessage}
+ ${{}} | ${defaultErrorMessage} | ${defaultErrorMessage}
+ ${generateErrorWithMessage(errorMessage)} | ${undefined} | ${''}
+ ${generateErrorWithMessage(`${ufErrorPrefix} ${errorMessage}`)} | ${undefined} | ${errorMessage}
+ ${generateErrorWithMessage(errorMessage)} | ${''} | ${''}
+ ${generateErrorWithMessage(`${ufErrorPrefix} ${errorMessage}`)} | ${''} | ${errorMessage}
+ `(
+ 'properly handles the edge case of error="$error" and defaultMessage="$defaultMessage"',
+ ({ error, defaultMessage, expectedResult }) => {
+ expect(parseErrorMessage(error, defaultMessage)).toEqual(expectedResult);
+ },
+ );
});
diff --git a/spec/frontend/lib/utils/intersection_observer_spec.js b/spec/frontend/lib/utils/intersection_observer_spec.js
index 71b1daffe0d..8eef403f0ae 100644
--- a/spec/frontend/lib/utils/intersection_observer_spec.js
+++ b/spec/frontend/lib/utils/intersection_observer_spec.js
@@ -57,7 +57,7 @@ describe('IntersectionObserver Utility', () => {
${true} | ${'IntersectionAppear'}
`(
'should emit the correct event on the entry target based on the computed Intersection',
- async ({ isIntersecting, event }) => {
+ ({ isIntersecting, event }) => {
const target = document.createElement('div');
observer.addEntry({ target, isIntersecting });
diff --git a/spec/frontend/lib/utils/poll_spec.js b/spec/frontend/lib/utils/poll_spec.js
index 63eeb54e850..096a92305dc 100644
--- a/spec/frontend/lib/utils/poll_spec.js
+++ b/spec/frontend/lib/utils/poll_spec.js
@@ -121,7 +121,7 @@ describe('Poll', () => {
});
describe('with delayed initial request', () => {
- it('delays the first request', async () => {
+ it('delays the first request', () => {
mockServiceCall({ status: HTTP_STATUS_OK, headers: { 'poll-interval': 1 } });
const Polling = new Poll({
diff --git a/spec/frontend/lib/utils/secret_detection_spec.js b/spec/frontend/lib/utils/secret_detection_spec.js
new file mode 100644
index 00000000000..7bde6cc4a8e
--- /dev/null
+++ b/spec/frontend/lib/utils/secret_detection_spec.js
@@ -0,0 +1,68 @@
+import { containsSensitiveToken, confirmSensitiveAction, i18n } from '~/lib/utils/secret_detection';
+import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
+
+jest.mock('~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal');
+
+const mockConfirmAction = ({ confirmed }) => {
+ confirmAction.mockResolvedValueOnce(confirmed);
+};
+
+describe('containsSensitiveToken', () => {
+ describe('when message does not contain sensitive tokens', () => {
+ const nonSensitiveMessages = [
+ 'This is a normal message',
+ '1234567890',
+ '!@#$%^&*()_+',
+ 'https://example.com',
+ ];
+
+ it.each(nonSensitiveMessages)('returns false for message: %s', (message) => {
+ expect(containsSensitiveToken(message)).toBe(false);
+ });
+ });
+
+ describe('when message contains sensitive tokens', () => {
+ const sensitiveMessages = [
+ 'token: glpat-cgyKc1k_AsnEpmP-5fRL',
+ 'token: GlPat-abcdefghijklmnopqrstuvwxyz',
+ 'token: feed_token=ABCDEFGHIJKLMNOPQRSTUVWXYZ',
+ 'https://example.com/feed?feed_token=123456789_abcdefghij',
+ 'glpat-1234567890 and feed_token=ABCDEFGHIJKLMNOPQRSTUVWXYZ',
+ ];
+
+ it.each(sensitiveMessages)('returns true for message: %s', (message) => {
+ expect(containsSensitiveToken(message)).toBe(true);
+ });
+ });
+});
+
+describe('confirmSensitiveAction', () => {
+ afterEach(() => {
+ confirmAction.mockReset();
+ });
+
+ it('should call confirmAction with correct parameters', async () => {
+ const prompt = 'Are you sure you want to delete this item?';
+ const expectedParams = {
+ primaryBtnVariant: 'danger',
+ primaryBtnText: i18n.primaryBtnText,
+ };
+ await confirmSensitiveAction(prompt);
+
+ expect(confirmAction).toHaveBeenCalledWith(prompt, expectedParams);
+ });
+
+ it('should return true when confirmed is true', async () => {
+ mockConfirmAction({ confirmed: true });
+
+ const result = await confirmSensitiveAction();
+ expect(result).toBe(true);
+ });
+
+ it('should return false when confirmed is false', async () => {
+ mockConfirmAction({ confirmed: false });
+
+ const result = await confirmSensitiveAction();
+ expect(result).toBe(false);
+ });
+});
diff --git a/spec/frontend/lib/utils/web_ide_navigator_spec.js b/spec/frontend/lib/utils/web_ide_navigator_spec.js
new file mode 100644
index 00000000000..0f5cd09d50e
--- /dev/null
+++ b/spec/frontend/lib/utils/web_ide_navigator_spec.js
@@ -0,0 +1,38 @@
+import { visitUrl, webIDEUrl } from '~/lib/utils/url_utility';
+import { openWebIDE } from '~/lib/utils/web_ide_navigator';
+
+jest.mock('~/lib/utils/url_utility', () => ({
+ visitUrl: jest.fn(),
+ webIDEUrl: jest.fn().mockImplementation((path) => `/-/ide/projects${path}`),
+}));
+
+describe('openWebIDE', () => {
+ it('when called without projectPath throws TypeError and does not call visitUrl', () => {
+ expect(() => {
+ openWebIDE();
+ }).toThrow(new TypeError('projectPath parameter is required'));
+ expect(visitUrl).not.toHaveBeenCalled();
+ });
+
+ it('when called with projectPath and without fileName calls visitUrl with correct path', () => {
+ const params = { projectPath: 'project-path' };
+ const expectedNonIDEPath = `/${params.projectPath}/edit/main/-/`;
+ const expectedIDEPath = `/-/ide/projects${expectedNonIDEPath}`;
+
+ openWebIDE(params.projectPath);
+
+ expect(webIDEUrl).toHaveBeenCalledWith(expectedNonIDEPath);
+ expect(visitUrl).toHaveBeenCalledWith(expectedIDEPath);
+ });
+
+ it('when called with projectPath and fileName calls visitUrl with correct path', () => {
+ const params = { projectPath: 'project-path', fileName: 'README' };
+ const expectedNonIDEPath = `/${params.projectPath}/edit/main/-/${params.fileName}/`;
+ const expectedIDEPath = `/-/ide/projects${expectedNonIDEPath}`;
+
+ openWebIDE(params.projectPath, params.fileName);
+
+ expect(webIDEUrl).toHaveBeenCalledWith(expectedNonIDEPath);
+ expect(visitUrl).toHaveBeenCalledWith(expectedIDEPath);
+ });
+});
diff --git a/spec/frontend/members/components/table/expiration_datepicker_spec.js b/spec/frontend/members/components/table/expiration_datepicker_spec.js
index 15812ee6572..9176a02a447 100644
--- a/spec/frontend/members/components/table/expiration_datepicker_spec.js
+++ b/spec/frontend/members/components/table/expiration_datepicker_spec.js
@@ -93,7 +93,7 @@ describe('ExpirationDatepicker', () => {
});
describe('when datepicker is changed', () => {
- beforeEach(async () => {
+ beforeEach(() => {
createComponent();
findDatepicker().vm.$emit('input', new Date('2020-03-17'));
diff --git a/spec/frontend/members/components/table/role_dropdown_spec.js b/spec/frontend/members/components/table/role_dropdown_spec.js
index d6e63b1930f..1045e3f9849 100644
--- a/spec/frontend/members/components/table/role_dropdown_spec.js
+++ b/spec/frontend/members/components/table/role_dropdown_spec.js
@@ -1,5 +1,6 @@
import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
+import * as Sentry from '@sentry/browser';
import { within } from '@testing-library/dom';
import { mount, createWrapper } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
@@ -12,6 +13,7 @@ import { member } from '../../mock_data';
Vue.use(Vuex);
jest.mock('ee_else_ce/members/guest_overage_confirm_action');
+jest.mock('@sentry/browser');
describe('RoleDropdown', () => {
let wrapper;
@@ -20,9 +22,9 @@ describe('RoleDropdown', () => {
show: jest.fn(),
};
- const createStore = () => {
+ const createStore = ({ updateMemberRoleReturn = Promise.resolve() } = {}) => {
actions = {
- updateMemberRole: jest.fn(() => Promise.resolve()),
+ updateMemberRole: jest.fn(() => updateMemberRoleReturn),
};
return new Vuex.Store({
@@ -32,7 +34,7 @@ describe('RoleDropdown', () => {
});
};
- const createComponent = (propsData = {}) => {
+ const createComponent = (propsData = {}, store = createStore()) => {
wrapper = mount(RoleDropdown, {
provide: {
namespace: MEMBER_TYPES.user,
@@ -46,7 +48,7 @@ describe('RoleDropdown', () => {
permissions: {},
...propsData,
},
- store: createStore(),
+ store,
mocks: {
$toast,
},
@@ -75,11 +77,11 @@ describe('RoleDropdown', () => {
});
describe('when dropdown is open', () => {
- beforeEach(() => {
+ beforeEach(async () => {
guestOverageConfirmAction.mockReturnValue(true);
createComponent();
- return findDropdownToggle().trigger('click');
+ await findDropdownToggle().trigger('click');
});
it('renders all valid roles', () => {
@@ -113,26 +115,74 @@ describe('RoleDropdown', () => {
});
});
- it('displays toast when successful', async () => {
- await getDropdownItemByText('Developer').trigger('click');
+ describe('when updateMemberRole is successful', () => {
+ it('displays toast', async () => {
+ await getDropdownItemByText('Developer').trigger('click');
- await nextTick();
+ await nextTick();
- expect($toast.show).toHaveBeenCalledWith('Role updated successfully.');
- });
+ expect($toast.show).toHaveBeenCalledWith('Role updated successfully.');
+ });
- it('puts dropdown in loading state while waiting for `updateMemberRole` to resolve', async () => {
- await getDropdownItemByText('Developer').trigger('click');
+ it('puts dropdown in loading state while waiting for `updateMemberRole` to resolve', async () => {
+ await getDropdownItemByText('Developer').trigger('click');
+
+ expect(findDropdown().props('loading')).toBe(true);
+ });
+
+ it('enables dropdown after `updateMemberRole` resolves', async () => {
+ await getDropdownItemByText('Developer').trigger('click');
- expect(findDropdown().props('loading')).toBe(true);
+ await waitForPromises();
+
+ expect(findDropdown().props('disabled')).toBe(false);
+ });
+
+ it('does not log error to Sentry', async () => {
+ await getDropdownItemByText('Developer').trigger('click');
+
+ await waitForPromises();
+
+ expect(Sentry.captureException).not.toHaveBeenCalled();
+ });
});
- it('enables dropdown after `updateMemberRole` resolves', async () => {
- await getDropdownItemByText('Developer').trigger('click');
+ describe('when updateMemberRole is not successful', () => {
+ const reason = 'Rejected ☹️';
- await waitForPromises();
+ beforeEach(() => {
+ createComponent({}, createStore({ updateMemberRoleReturn: Promise.reject(reason) }));
+ });
+
+ it('does not display toast', async () => {
+ await getDropdownItemByText('Developer').trigger('click');
+
+ await nextTick();
+
+ expect($toast.show).not.toHaveBeenCalled();
+ });
+
+ it('puts dropdown in loading state while waiting for `updateMemberRole` to resolve', async () => {
+ await getDropdownItemByText('Developer').trigger('click');
+
+ expect(findDropdown().props('loading')).toBe(true);
+ });
- expect(findDropdown().props('disabled')).toBe(false);
+ it('enables dropdown after `updateMemberRole` resolves', async () => {
+ await getDropdownItemByText('Developer').trigger('click');
+
+ await waitForPromises();
+
+ expect(findDropdown().props('disabled')).toBe(false);
+ });
+
+ it('logs error to Sentry', async () => {
+ await getDropdownItemByText('Developer').trigger('click');
+
+ await waitForPromises();
+
+ expect(Sentry.captureException).toHaveBeenCalledWith(reason);
+ });
});
});
});
diff --git a/spec/frontend/members/utils_spec.js b/spec/frontend/members/utils_spec.js
index 4f276e8c9df..c4357e9c1f0 100644
--- a/spec/frontend/members/utils_spec.js
+++ b/spec/frontend/members/utils_spec.js
@@ -213,7 +213,7 @@ describe('Members Utils', () => {
${'recent_sign_in'} | ${{ sortByKey: 'lastSignIn', sortDesc: false }}
${'oldest_sign_in'} | ${{ sortByKey: 'lastSignIn', sortDesc: true }}
`('when `sort` query string param is `$sortParam`', ({ sortParam, expected }) => {
- it(`returns ${JSON.stringify(expected)}`, async () => {
+ it(`returns ${JSON.stringify(expected)}`, () => {
setWindowLocation(`?sort=${sortParam}`);
expect(parseSortParam(['account', 'granted', 'expires', 'maxRole', 'lastSignIn'])).toEqual(
diff --git a/spec/frontend/ml/experiment_tracking/components/delete_button_spec.js b/spec/frontend/ml/experiment_tracking/components/delete_button_spec.js
new file mode 100644
index 00000000000..0243cbeb7bf
--- /dev/null
+++ b/spec/frontend/ml/experiment_tracking/components/delete_button_spec.js
@@ -0,0 +1,68 @@
+import { GlModal, GlDropdown, GlDropdownItem } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import DeleteButton from '~/ml/experiment_tracking/components/delete_button.vue';
+
+const csrfToken = 'mock-csrf-token';
+jest.mock('~/lib/utils/csrf', () => ({ token: csrfToken }));
+
+const MODAL_BODY = 'MODAL_BODY';
+const MODAL_TITLE = 'MODAL_TITLE';
+
+describe('DeleteButton', () => {
+ let wrapper;
+
+ const findModal = () => wrapper.findComponent(GlModal);
+ const findDropdown = () => wrapper.findComponent(GlDropdown);
+ const findDeleteButton = () => wrapper.findComponent(GlDropdownItem);
+ const findForm = () => wrapper.find('form');
+ const findModalText = () => wrapper.findByText(MODAL_BODY);
+
+ beforeEach(() => {
+ wrapper = shallowMountExtended(DeleteButton, {
+ propsData: {
+ deletePath: '/delete',
+ deleteConfirmationText: MODAL_BODY,
+ actionPrimaryText: 'Delete!',
+ modalTitle: MODAL_TITLE,
+ },
+ });
+ });
+
+ it('mounts the modal', () => {
+ expect(findModal().exists()).toBe(true);
+ });
+
+ it('mounts the dropdown', () => {
+ expect(findDropdown().exists()).toBe(true);
+ });
+
+ it('mounts the button', () => {
+ expect(findDeleteButton().exists()).toBe(true);
+ });
+
+ describe('when modal is opened', () => {
+ it('displays modal title', () => {
+ expect(findModal().props('title')).toBe(MODAL_TITLE);
+ });
+
+ it('displays modal body', () => {
+ expect(findModalText().exists()).toBe(true);
+ });
+
+ it('submits the form when primary action is clicked', () => {
+ const submitSpy = jest.spyOn(findForm().element, 'submit');
+
+ findModal().vm.$emit('primary');
+
+ expect(submitSpy).toHaveBeenCalled();
+ });
+
+ it('displays form with correct action and inputs', () => {
+ const form = findForm();
+
+ expect(form.attributes('action')).toBe('/delete');
+ expect(form.find('input[name="_method"]').attributes('value')).toBe('delete');
+ expect(form.find('input[name="authenticity_token"]').attributes('value')).toBe(csrfToken);
+ });
+ });
+});
diff --git a/spec/frontend/ml/experiment_tracking/routes/candidates/show/__snapshots__/ml_candidates_show_spec.js.snap b/spec/frontend/ml/experiment_tracking/routes/candidates/show/__snapshots__/ml_candidates_show_spec.js.snap
index dc21db39259..6fd3dce1941 100644
--- a/spec/frontend/ml/experiment_tracking/routes/candidates/show/__snapshots__/ml_candidates_show_spec.js.snap
+++ b/spec/frontend/ml/experiment_tracking/routes/candidates/show/__snapshots__/ml_candidates_show_spec.js.snap
@@ -2,99 +2,36 @@
exports[`MlCandidatesShow renders correctly 1`] = `
<div>
+ <incubation-alert-stub
+ featurename="Machine learning experiment tracking"
+ linktofeedbackissue="https://gitlab.com/gitlab-org/gitlab/-/issues/381660"
+ />
+
<div
- class="gl-alert gl-alert-warning"
+ class="detail-page-header gl-flex-wrap"
>
- <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"
+ class="detail-page-header-body"
>
- <h2
- class="gl-alert-title"
- >
- Machine learning experiment tracking is in incubating phase
- </h2>
-
- <div
- class="gl-alert-body"
+ <h1
+ class="page-title gl-font-size-h-display flex-fill"
>
- 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 about incubating features
- </a>
- </div>
+ Model candidate details
+
+ </h1>
- <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"
- >
-
- Give feedback on this feature
-
- </span>
- </a>
- </div>
+ <delete-button-stub
+ actionprimarytext="Delete candidate"
+ deleteconfirmationtext="Deleting this candidate will delete the associated parameters, metrics, and metadata."
+ deletepath="path_to_candidate"
+ modaltitle="Delete candidate?"
+ />
</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>
-
- Model candidate details
-
- </h3>
-
<table
- class="candidate-details"
+ class="candidate-details gl-w-full"
>
<tbody>
<tr
@@ -143,12 +80,9 @@ exports[`MlCandidatesShow renders correctly 1`] = `
</td>
<td>
- <a
- class="gl-link"
- href="#"
- >
+ <gl-link-stub>
The Experiment
- </a>
+ </gl-link-stub>
</td>
</tr>
@@ -162,12 +96,11 @@ exports[`MlCandidatesShow renders correctly 1`] = `
</td>
<td>
- <a
- class="gl-link"
+ <gl-link-stub
href="path_to_artifact"
>
Artifacts
- </a>
+ </gl-link-stub>
</td>
</tr>
diff --git a/spec/frontend/ml/experiment_tracking/routes/candidates/show/ml_candidates_show_spec.js b/spec/frontend/ml/experiment_tracking/routes/candidates/show/ml_candidates_show_spec.js
index 36455339041..7d03ab3b509 100644
--- a/spec/frontend/ml/experiment_tracking/routes/candidates/show/ml_candidates_show_spec.js
+++ b/spec/frontend/ml/experiment_tracking/routes/candidates/show/ml_candidates_show_spec.js
@@ -1,6 +1,7 @@
-import { GlAlert } from '@gitlab/ui';
-import { mountExtended } from 'helpers/vue_test_utils_helper';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import MlCandidatesShow from '~/ml/experiment_tracking/routes/candidates/show';
+import DeleteButton from '~/ml/experiment_tracking/components/delete_button.vue';
+import IncubationAlert from '~/vue_shared/components/incubation/incubation_alert.vue';
describe('MlCandidatesShow', () => {
let wrapper;
@@ -25,23 +26,31 @@ describe('MlCandidatesShow', () => {
experiment_name: 'The Experiment',
experiment_path: 'path/to/experiment',
status: 'SUCCESS',
+ path: 'path_to_candidate',
},
};
- return mountExtended(MlCandidatesShow, { propsData: { candidate } });
+ wrapper = shallowMountExtended(MlCandidatesShow, { propsData: { candidate } });
};
- const findAlert = () => wrapper.findComponent(GlAlert);
+ beforeEach(createWrapper);
- it('shows incubation warning', () => {
- wrapper = createWrapper();
+ const findAlert = () => wrapper.findComponent(IncubationAlert);
+ const findDeleteButton = () => wrapper.findComponent(DeleteButton);
+ it('shows incubation warning', () => {
expect(findAlert().exists()).toBe(true);
});
- it('renders correctly', () => {
- wrapper = createWrapper();
+ it('shows delete button', () => {
+ expect(findDeleteButton().exists()).toBe(true);
+ });
+ it('passes the delete path to delete button', () => {
+ expect(findDeleteButton().props('deletePath')).toBe('path_to_candidate');
+ });
+
+ it('renders correctly', () => {
expect(wrapper.element).toMatchSnapshot();
});
});
diff --git a/spec/frontend/ml/experiment_tracking/routes/experiments/show/components/experiment_header_spec.js b/spec/frontend/ml/experiment_tracking/routes/experiments/show/components/experiment_header_spec.js
new file mode 100644
index 00000000000..b56755043fb
--- /dev/null
+++ b/spec/frontend/ml/experiment_tracking/routes/experiments/show/components/experiment_header_spec.js
@@ -0,0 +1,55 @@
+import { GlButton } from '@gitlab/ui';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import ExperimentHeader from '~/ml/experiment_tracking/routes/experiments/show/components/experiment_header.vue';
+import DeleteButton from '~/ml/experiment_tracking/components/delete_button.vue';
+import setWindowLocation from 'helpers/set_window_location_helper';
+import * as urlHelpers from '~/lib/utils/url_utility';
+import { MOCK_EXPERIMENT } from '../mock_data';
+
+const DELETE_INFO = {
+ deletePath: '/delete',
+ deleteConfirmationText: 'MODAL_BODY',
+ actionPrimaryText: 'Delete!',
+ modalTitle: 'MODAL_TITLE',
+};
+
+describe('~/ml/experiment_tracking/routes/experiments/show/components/experiment_header.vue', () => {
+ let wrapper;
+
+ const createWrapper = () => {
+ wrapper = mountExtended(ExperimentHeader, {
+ propsData: { title: MOCK_EXPERIMENT.name, deleteInfo: DELETE_INFO },
+ });
+ };
+
+ const findDeleteButton = () => wrapper.findComponent(DeleteButton);
+ const findButton = () => wrapper.findComponent(GlButton);
+
+ beforeEach(createWrapper);
+
+ describe('Delete', () => {
+ it('shows delete button', () => {
+ expect(findDeleteButton().exists()).toBe(true);
+ });
+
+ it('passes the right props', () => {
+ expect(findDeleteButton().props()).toMatchObject(DELETE_INFO);
+ });
+ });
+
+ describe('CSV download', () => {
+ it('shows download CSV button', () => {
+ expect(findDeleteButton().exists()).toBe(true);
+ });
+
+ it('calls the action to download the CSV', () => {
+ setWindowLocation('https://blah.com/something/1?name=query&orderBy=name');
+ jest.spyOn(urlHelpers, 'visitUrl').mockImplementation(() => {});
+
+ findButton().vm.$emit('click');
+
+ expect(urlHelpers.visitUrl).toHaveBeenCalledTimes(1);
+ expect(urlHelpers.visitUrl).toHaveBeenCalledWith('/something/1.csv?name=query&orderBy=name');
+ });
+ });
+});
diff --git a/spec/frontend/ml/experiment_tracking/routes/experiments/show/ml_experiments_show_spec.js b/spec/frontend/ml/experiment_tracking/routes/experiments/show/ml_experiments_show_spec.js
index 97a5049ea88..38b3d96ed11 100644
--- a/spec/frontend/ml/experiment_tracking/routes/experiments/show/ml_experiments_show_spec.js
+++ b/spec/frontend/ml/experiment_tracking/routes/experiments/show/ml_experiments_show_spec.js
@@ -1,11 +1,12 @@
import { GlAlert, GlTableLite, GlLink, GlEmptyState } from '@gitlab/ui';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import MlExperimentsShow from '~/ml/experiment_tracking/routes/experiments/show/ml_experiments_show.vue';
+import ExperimentHeader from '~/ml/experiment_tracking/routes/experiments/show/components/experiment_header.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';
-import { MOCK_START_CURSOR, MOCK_PAGE_INFO, MOCK_CANDIDATES } from './mock_data';
+import { MOCK_START_CURSOR, MOCK_PAGE_INFO, MOCK_CANDIDATES, MOCK_EXPERIMENT } from './mock_data';
describe('MlExperimentsShow', () => {
let wrapper;
@@ -15,9 +16,10 @@ describe('MlExperimentsShow', () => {
metricNames = [],
paramNames = [],
pageInfo = MOCK_PAGE_INFO,
+ experiment = MOCK_EXPERIMENT,
) => {
wrapper = mountExtended(MlExperimentsShow, {
- propsData: { candidates, metricNames, paramNames, pageInfo },
+ propsData: { experiment, candidates, metricNames, paramNames, pageInfo },
});
};
@@ -34,6 +36,8 @@ describe('MlExperimentsShow', () => {
const findTableRows = () => findTable().findAll('tbody > tr');
const findNthTableRow = (idx) => findTableRows().at(idx);
const findColumnInRow = (row, col) => findNthTableRow(row).findAll('td').at(col);
+ const findExperimentHeader = () => wrapper.findComponent(ExperimentHeader);
+
const hrefInRowAndColumn = (row, col) =>
findColumnInRow(row, col).findComponent(GlLink).attributes().href;
@@ -44,7 +48,7 @@ describe('MlExperimentsShow', () => {
});
describe('default inputs', () => {
- beforeEach(async () => {
+ beforeEach(() => {
createWrapper();
});
@@ -56,6 +60,14 @@ describe('MlExperimentsShow', () => {
expect(findPagination().exists()).toBe(false);
});
+ it('shows experiment header', () => {
+ expect(findExperimentHeader().exists()).toBe(true);
+ });
+
+ it('passes the correct title to experiment header', () => {
+ expect(findExperimentHeader().props('title')).toBe(MOCK_EXPERIMENT.name);
+ });
+
it('does not show table', () => {
expect(findTable().exists()).toBe(false);
});
diff --git a/spec/frontend/ml/experiment_tracking/routes/experiments/show/mock_data.js b/spec/frontend/ml/experiment_tracking/routes/experiments/show/mock_data.js
index 66378cd3f0d..adfb3dbf773 100644
--- a/spec/frontend/ml/experiment_tracking/routes/experiments/show/mock_data.js
+++ b/spec/frontend/ml/experiment_tracking/routes/experiments/show/mock_data.js
@@ -7,6 +7,8 @@ export const MOCK_PAGE_INFO = {
hasPreviousPage: true,
};
+export const MOCK_EXPERIMENT = { name: 'experiment', path: '/path/to/experiment' };
+
export const MOCK_CANDIDATES = [
{
rmse: 1,
diff --git a/spec/frontend/monitoring/components/variables/dropdown_field_spec.js b/spec/frontend/monitoring/components/variables/dropdown_field_spec.js
index 96b228fd3b2..e6c5569fa19 100644
--- a/spec/frontend/monitoring/components/variables/dropdown_field_spec.js
+++ b/spec/frontend/monitoring/components/variables/dropdown_field_spec.js
@@ -53,7 +53,7 @@ describe('Custom variable component', () => {
expect(findDropdown().exists()).toBe(true);
});
- it('changing dropdown items triggers update', async () => {
+ it('changing dropdown items triggers update', () => {
createShallowWrapper();
findDropdownItems().at(1).vm.$emit('click');
diff --git a/spec/frontend/monitoring/pages/dashboard_page_spec.js b/spec/frontend/monitoring/pages/dashboard_page_spec.js
index c5a8b50ee60..3de99673e71 100644
--- a/spec/frontend/monitoring/pages/dashboard_page_spec.js
+++ b/spec/frontend/monitoring/pages/dashboard_page_spec.js
@@ -2,6 +2,7 @@ import { shallowMount } from '@vue/test-utils';
import Dashboard from '~/monitoring/components/dashboard.vue';
import DashboardPage from '~/monitoring/pages/dashboard_page.vue';
import { createStore } from '~/monitoring/stores';
+import { assertProps } from 'helpers/assert_props';
import { dashboardProps } from '../fixture_data';
describe('monitoring/pages/dashboard_page', () => {
@@ -45,7 +46,7 @@ describe('monitoring/pages/dashboard_page', () => {
});
it('throws errors if dashboard props are not passed', () => {
- expect(() => buildWrapper()).toThrow('Missing required prop: "dashboardProps"');
+ expect(() => assertProps(DashboardPage, {})).toThrow('Missing required prop: "dashboardProps"');
});
it('renders the dashboard page with dashboard component', () => {
diff --git a/spec/frontend/monitoring/store/actions_spec.js b/spec/frontend/monitoring/store/actions_spec.js
index 8097857f226..b3b198d6b51 100644
--- a/spec/frontend/monitoring/store/actions_spec.js
+++ b/spec/frontend/monitoring/store/actions_spec.js
@@ -260,7 +260,7 @@ describe('Monitoring store actions', () => {
});
});
- it('does not show an alert error when showErrorBanner is disabled', async () => {
+ it('does not show an alert when showErrorBanner is disabled', async () => {
state.showErrorBanner = false;
await result();
diff --git a/spec/frontend/nav/components/new_nav_toggle_spec.js b/spec/frontend/nav/components/new_nav_toggle_spec.js
index fe543a346b5..cf8e59d6522 100644
--- a/spec/frontend/nav/components/new_nav_toggle_spec.js
+++ b/spec/frontend/nav/components/new_nav_toggle_spec.js
@@ -9,6 +9,7 @@ import NewNavToggle from '~/nav/components/new_nav_toggle.vue';
import waitForPromises from 'helpers/wait_for_promises';
import { createAlert } from '~/alert';
import { s__ } from '~/locale';
+import { mockTracking } from 'helpers/tracking_helper';
jest.mock('~/alert');
@@ -18,6 +19,7 @@ describe('NewNavToggle', () => {
useMockLocationHelper();
let wrapper;
+ let trackingSpy;
const findToggle = () => wrapper.findComponent(GlToggle);
const findDisclosureItem = () => wrapper.findComponent(GlDisclosureDropdownItem);
@@ -29,6 +31,8 @@ describe('NewNavToggle', () => {
...propsData,
},
});
+
+ trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
};
const getByText = (text, options) =>
@@ -61,15 +65,17 @@ describe('NewNavToggle', () => {
});
describe.each`
- desc | actFn
- ${'when toggle button is clicked'} | ${() => findToggle().trigger('click')}
- ${'on menu item action'} | ${() => findDisclosureItem().vm.$emit('action')}
- `('$desc', ({ actFn }) => {
+ desc | actFn | toggleValue | trackingLabel | trackingProperty
+ ${'when toggle button is clicked'} | ${() => findToggle().trigger('click')} | ${false} | ${'enable_new_nav_beta'} | ${'navigation_top'}
+ ${'when menu item text is clicked'} | ${() => getByText('New navigation').trigger('click')} | ${false} | ${'enable_new_nav_beta'} | ${'navigation_top'}
+ ${'when toggle button is clicked'} | ${() => findToggle().trigger('click')} | ${true} | ${'disable_new_nav_beta'} | ${'nav_user_menu'}
+ ${'when menu item text is clicked'} | ${() => getByText('New navigation').trigger('click')} | ${true} | ${'disable_new_nav_beta'} | ${'nav_user_menu'}
+ `('$desc', ({ actFn, toggleValue, trackingLabel, trackingProperty }) => {
let mock;
beforeEach(() => {
mock = new MockAdapter(axios);
- createComponent({ enabled: false, newNavigation: true });
+ createComponent({ enabled: toggleValue });
});
it('reloads the page on success', async () => {
@@ -100,7 +106,17 @@ describe('NewNavToggle', () => {
it('changes the toggle', async () => {
await actFn();
- expect(findToggle().props('value')).toBe(true);
+ expect(findToggle().props('value')).toBe(!toggleValue);
+ });
+
+ it('tracks the Snowplow event', async () => {
+ mock.onPut(TEST_ENDPONT).reply(HTTP_STATUS_OK);
+ await actFn();
+ await waitForPromises();
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_toggle', {
+ label: trackingLabel,
+ property: trackingProperty,
+ });
});
afterEach(() => {
@@ -136,15 +152,17 @@ describe('NewNavToggle', () => {
});
describe.each`
- desc | actFn
- ${'when toggle button is clicked'} | ${() => findToggle().trigger('click')}
- ${'when menu item text is clicked'} | ${() => getByText('New navigation').trigger('click')}
- `('$desc', ({ actFn }) => {
+ desc | actFn | toggleValue | trackingLabel | trackingProperty
+ ${'when toggle button is clicked'} | ${() => findToggle().trigger('click')} | ${false} | ${'enable_new_nav_beta'} | ${'navigation_top'}
+ ${'when menu item text is clicked'} | ${() => getByText('New navigation').trigger('click')} | ${false} | ${'enable_new_nav_beta'} | ${'navigation_top'}
+ ${'when toggle button is clicked'} | ${() => findToggle().trigger('click')} | ${true} | ${'disable_new_nav_beta'} | ${'nav_user_menu'}
+ ${'when menu item text is clicked'} | ${() => getByText('New navigation').trigger('click')} | ${true} | ${'disable_new_nav_beta'} | ${'nav_user_menu'}
+ `('$desc', ({ actFn, toggleValue, trackingLabel, trackingProperty }) => {
let mock;
beforeEach(() => {
mock = new MockAdapter(axios);
- createComponent({ enabled: false });
+ createComponent({ enabled: toggleValue });
});
it('reloads the page on success', async () => {
@@ -175,7 +193,17 @@ describe('NewNavToggle', () => {
it('changes the toggle', async () => {
await actFn();
- expect(findToggle().props('value')).toBe(true);
+ expect(findToggle().props('value')).toBe(!toggleValue);
+ });
+
+ it('tracks the Snowplow event', async () => {
+ mock.onPut(TEST_ENDPONT).reply(HTTP_STATUS_OK);
+ await actFn();
+ await waitForPromises();
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_toggle', {
+ label: trackingLabel,
+ property: trackingProperty,
+ });
});
afterEach(() => {
diff --git a/spec/frontend/new_branch_spec.js b/spec/frontend/new_branch_spec.js
index 5a09598059d..766e840cac5 100644
--- a/spec/frontend/new_branch_spec.js
+++ b/spec/frontend/new_branch_spec.js
@@ -11,7 +11,7 @@ describe('Branch', () => {
describe('create a new branch', () => {
function fillNameWith(value) {
document.querySelector('.js-branch-name').value = value;
- const event = new CustomEvent('blur');
+ const event = new CustomEvent('change');
document.querySelector('.js-branch-name').dispatchEvent(event);
}
diff --git a/spec/frontend/notebook/cells/output/dataframe_spec.js b/spec/frontend/notebook/cells/output/dataframe_spec.js
new file mode 100644
index 00000000000..abf6631353c
--- /dev/null
+++ b/spec/frontend/notebook/cells/output/dataframe_spec.js
@@ -0,0 +1,59 @@
+import { shallowMount } from '@vue/test-utils';
+import DataframeOutput from '~/notebook/cells/output/dataframe.vue';
+import JSONTable from '~/behaviors/components/json_table.vue';
+import { outputWithDataframe } from '../../mock_data';
+
+describe('~/notebook/cells/output/DataframeOutput', () => {
+ let wrapper;
+
+ function createComponent(rawCode) {
+ wrapper = shallowMount(DataframeOutput, {
+ propsData: {
+ rawCode,
+ count: 0,
+ index: 0,
+ },
+ });
+ }
+
+ const findTable = () => wrapper.findComponent(JSONTable);
+
+ describe('with valid dataframe', () => {
+ beforeEach(() => createComponent(outputWithDataframe.data['text/html'].join('')));
+
+ it('mounts the table', () => {
+ expect(findTable().exists()).toBe(true);
+ });
+
+ it('table caption is empty', () => {
+ expect(findTable().props().caption).toEqual('');
+ });
+
+ it('allows filtering', () => {
+ expect(findTable().props().hasFilter).toBe(true);
+ });
+
+ it('sets the correct fields', () => {
+ expect(findTable().props().fields).toEqual([
+ { key: 'index', label: '', sortable: true },
+ { key: 'column_1', label: 'column_1', sortable: true },
+ { key: 'column_2', label: 'column_2', sortable: true },
+ ]);
+ });
+
+ it('sets the correct items', () => {
+ expect(findTable().props().items).toEqual([
+ { index: 0, column_1: 'abc de f', column_2: 'a' },
+ { index: 1, column_1: 'True', column_2: '0.1' },
+ ]);
+ });
+ });
+
+ describe('invalid dataframe', () => {
+ it('still displays the table', () => {
+ createComponent('dataframe');
+
+ expect(findTable().exists()).toBe(true);
+ });
+ });
+});
diff --git a/spec/frontend/notebook/cells/output/dataframe_util_spec.js b/spec/frontend/notebook/cells/output/dataframe_util_spec.js
new file mode 100644
index 00000000000..ddc1b3cfe26
--- /dev/null
+++ b/spec/frontend/notebook/cells/output/dataframe_util_spec.js
@@ -0,0 +1,113 @@
+import { isDataframe, convertHtmlTableToJson } from '~/notebook/cells/output/dataframe_util';
+import { outputWithDataframeContent } from '../../mock_data';
+import sanitizeTests from './html_sanitize_fixtures';
+
+describe('notebook/cells/output/dataframe_utils', () => {
+ describe('isDataframe', () => {
+ describe('when output data has no text/html', () => {
+ it('is is not a dataframe', () => {
+ const input = { data: { 'image/png': ['blah'] } };
+
+ expect(isDataframe(input)).toBe(false);
+ });
+ });
+
+ describe('when output data has no text/html, but no mention of dataframe', () => {
+ it('is is not a dataframe', () => {
+ const input = { data: { 'text/html': ['blah'] } };
+
+ expect(isDataframe(input)).toBe(false);
+ });
+ });
+
+ describe('when output data has text/html, but no mention of dataframe in the first 20 lines', () => {
+ it('is is not a dataframe', () => {
+ const input = { data: { 'text/html': [...new Array(20).fill('a'), 'dataframe'] } };
+
+ expect(isDataframe(input)).toBe(false);
+ });
+ });
+
+ describe('when output data has text/html, and includes "dataframe" within the first 20 lines', () => {
+ it('is is not a dataframe', () => {
+ const input = { data: { 'text/html': ['dataframe'] } };
+
+ expect(isDataframe(input)).toBe(true);
+ });
+ });
+ });
+
+ describe('convertHtmlTableToJson', () => {
+ it('converts table correctly', () => {
+ const input = outputWithDataframeContent;
+
+ const output = {
+ fields: [
+ { key: 'index', label: '', sortable: true },
+ { key: 'column_1', label: 'column_1', sortable: true },
+ { key: 'column_2', label: 'column_2', sortable: true },
+ ],
+ items: [
+ { index: 0, column_1: 'abc de f', column_2: 'a' },
+ { index: 1, column_1: 'True', column_2: '0.1' },
+ ],
+ };
+
+ expect(convertHtmlTableToJson(input)).toEqual(output);
+ });
+
+ describe('sanitizes input before parsing table', () => {
+ it('sanitizes input html', () => {
+ const parser = new DOMParser();
+ const spy = jest.spyOn(parser, 'parseFromString');
+ const input = 'hello<style>p {width:50%;}</style><script>alert(1)</script>';
+
+ convertHtmlTableToJson(input, parser);
+
+ expect(spy).toHaveBeenCalledWith('hello', 'text/html');
+ });
+ });
+
+ describe('does not include harmful html', () => {
+ const makeDataframeWithHtml = (html) => {
+ return [
+ '<table border="1" class="dataframe">\n',
+ ' <thead>\n',
+ ' <tr style="text-align: right;">\n',
+ ' <th></th>\n',
+ ' <th>column_1</th>\n',
+ ' </tr>\n',
+ ' </thead>\n',
+ ' <tbody>\n',
+ ' <tr>\n',
+ ' <th>0</th>\n',
+ ` <td>${html}</td>\n`,
+ ' </tr>\n',
+ ' </tbody>\n',
+ '</table>\n',
+ '</div>',
+ ];
+ };
+
+ it.each([
+ ['table', 0],
+ ['style', 1],
+ ['iframe', 2],
+ ['svg', 3],
+ ])('sanitizes output for: %p', (tag, index) => {
+ const inputHtml = makeDataframeWithHtml(sanitizeTests[index][1].input);
+ const convertedHtml = convertHtmlTableToJson(inputHtml).items[0].column_1;
+
+ expect(convertedHtml).not.toContain(tag);
+ });
+ });
+
+ describe('when dataframe is invalid', () => {
+ it('returns empty', () => {
+ const input = [' dataframe', ' blah'];
+
+ expect(convertHtmlTableToJson(input)).toEqual({ fields: [], items: [] });
+ });
+ });
+ });
+});
diff --git a/spec/frontend/notebook/cells/output/index_spec.js b/spec/frontend/notebook/cells/output/index_spec.js
index 1241c133b89..efbdfca8d8c 100644
--- a/spec/frontend/notebook/cells/output/index_spec.js
+++ b/spec/frontend/notebook/cells/output/index_spec.js
@@ -2,7 +2,13 @@ import { mount } from '@vue/test-utils';
import json from 'test_fixtures/blob/notebook/basic.json';
import Output from '~/notebook/cells/output/index.vue';
import MarkdownOutput from '~/notebook/cells/output/markdown.vue';
-import { relativeRawPath, markdownCellContent } from '../../mock_data';
+import DataframeOutput from '~/notebook/cells/output/dataframe.vue';
+import {
+ relativeRawPath,
+ markdownCellContent,
+ outputWithDataframe,
+ outputWithDataframeContent,
+} from '../../mock_data';
describe('Output component', () => {
let wrapper;
@@ -105,6 +111,16 @@ describe('Output component', () => {
});
});
+ describe('Dataframe output', () => {
+ it('renders DataframeOutput component', () => {
+ createComponent(outputWithDataframe);
+
+ expect(wrapper.findComponent(DataframeOutput).props('rawCode')).toBe(
+ outputWithDataframeContent.join(''),
+ );
+ });
+ });
+
describe('default to plain text', () => {
beforeEach(() => {
const unknownType = json.cells[6];
diff --git a/spec/frontend/notebook/mock_data.js b/spec/frontend/notebook/mock_data.js
index 5c47cb5aa9b..15db2931b3c 100644
--- a/spec/frontend/notebook/mock_data.js
+++ b/spec/frontend/notebook/mock_data.js
@@ -6,3 +6,47 @@ export const errorOutputContent = [
'\u001b[0;32m/var/folders/cq/l637k4x13gx6y9p_gfs4c_gc0000gn/T/ipykernel_79203/294318627.py\u001b[0m in \u001b[0;36m<module>\u001b[0;34m\u001b[0m\n\u001b[0;32m----> 1\u001b[0;31m \u001b[0mTo\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m',
"\u001b[0;31mNameError\u001b[0m: name 'To' is not defined",
];
+export const outputWithDataframeContent = [
+ '<div>\n',
+ '<style scoped>\n',
+ ' .dataframe tbody tr th:only-of-type {\n',
+ ' vertical-align: middle;\n',
+ ' }\n',
+ '\n',
+ ' .dataframe tbody tr th {\n',
+ ' vertical-align: top;\n',
+ ' }\n',
+ '\n',
+ ' .dataframe thead th {\n',
+ ' text-align: right;\n',
+ ' }\n',
+ '</style>\n',
+ '<table border="1" class="dataframe">\n',
+ ' <thead>\n',
+ ' <tr style="text-align: right;">\n',
+ ' <th></th>\n',
+ ' <th>column_1</th>\n',
+ ' <th>column_2</th>\n',
+ ' </tr>\n',
+ ' </thead>\n',
+ ' <tbody>\n',
+ ' <tr>\n',
+ ' <th>0</th>\n',
+ ' <td>abc de f</td>\n',
+ ' <td>a</td>\n',
+ ' </tr>\n',
+ ' <tr>\n',
+ ' <th>1</th>\n',
+ ' <td>True</td>\n',
+ ' <td>0.1</td>\n',
+ ' </tr>\n',
+ ' </tbody>\n',
+ '</table>\n',
+ '</div>',
+];
+
+export const outputWithDataframe = {
+ data: {
+ 'text/html': outputWithDataframeContent,
+ },
+};
diff --git a/spec/frontend/notes/components/comment_form_spec.js b/spec/frontend/notes/components/comment_form_spec.js
index 062cd098640..04143bb5b60 100644
--- a/spec/frontend/notes/components/comment_form_spec.js
+++ b/spec/frontend/notes/components/comment_form_spec.js
@@ -5,6 +5,7 @@ import MockAdapter from 'axios-mock-adapter';
import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import { useLocalStorageSpy } from 'helpers/local_storage_helper';
import batchComments from '~/batch_comments/stores/modules/batch_comments';
import { refreshUserMergeRequestCounts } from '~/commons/nav/user_merge_requests';
import { createAlert } from '~/alert';
@@ -27,6 +28,8 @@ jest.mock('~/alert');
Vue.use(Vuex);
describe('issue_comment_form component', () => {
+ useLocalStorageSpy();
+
let store;
let wrapper;
let axiosMock;
@@ -649,6 +652,37 @@ describe('issue_comment_form component', () => {
});
});
+ describe('check sensitive tokens', () => {
+ const sensitiveMessage = 'token: glpat-1234567890abcdefghij';
+ const nonSensitiveMessage = 'text';
+
+ it('should not save note when it contains sensitive token', () => {
+ mountComponent({
+ mountFunction: mount,
+ initialData: { note: sensitiveMessage },
+ });
+
+ jest.spyOn(wrapper.vm, 'saveNote').mockResolvedValue();
+
+ clickCommentButton();
+
+ expect(wrapper.vm.saveNote).not.toHaveBeenCalled();
+ });
+
+ it('should save note it does not contain sensitive token', () => {
+ mountComponent({
+ mountFunction: mount,
+ initialData: { note: nonSensitiveMessage },
+ });
+
+ jest.spyOn(wrapper.vm, 'saveNote').mockResolvedValue();
+
+ clickCommentButton();
+
+ expect(wrapper.vm.saveNote).toHaveBeenCalled();
+ });
+ });
+
describe('user is not logged in', () => {
beforeEach(() => {
mountComponent({ userData: null, noteableData: loggedOutnoteableData, mountFunction: mount });
diff --git a/spec/frontend/notes/components/discussion_filter_spec.js b/spec/frontend/notes/components/discussion_filter_spec.js
index ed1ced1b3d1..28e5e65c177 100644
--- a/spec/frontend/notes/components/discussion_filter_spec.js
+++ b/spec/frontend/notes/components/discussion_filter_spec.js
@@ -1,4 +1,4 @@
-import { GlDropdown } from '@gitlab/ui';
+import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import AxiosMockAdapter from 'axios-mock-adapter';
@@ -77,17 +77,16 @@ describe('DiscussionFilter component', () => {
// as it doesn't matter for our tests here
mock.onGet(DISCUSSION_PATH).reply(HTTP_STATUS_OK, '');
window.mrTabs = undefined;
- wrapper = mountComponent();
jest.spyOn(Tracking, 'event');
});
afterEach(() => {
- wrapper.vm.$destroy();
mock.restore();
});
describe('default', () => {
beforeEach(() => {
+ wrapper = mountComponent();
jest.spyOn(store, 'dispatch').mockImplementation();
});
@@ -104,6 +103,7 @@ describe('DiscussionFilter component', () => {
describe('when asc', () => {
beforeEach(() => {
+ wrapper = mountComponent();
jest.spyOn(store, 'dispatch').mockImplementation();
});
@@ -123,6 +123,7 @@ describe('DiscussionFilter component', () => {
describe('when desc', () => {
beforeEach(() => {
+ wrapper = mountComponent();
store.state.discussionSortOrder = DESC;
jest.spyOn(store, 'dispatch').mockImplementation();
});
@@ -145,56 +146,62 @@ describe('DiscussionFilter component', () => {
});
});
- it('renders the all filters', () => {
- expect(wrapper.findAll('.discussion-filter-container .dropdown-item').length).toBe(
- discussionFiltersMock.length,
- );
- });
+ describe('discussion filter functionality', () => {
+ beforeEach(() => {
+ wrapper = mountComponent();
+ });
- it('renders the default selected item', () => {
- expect(wrapper.find('.discussion-filter-container .dropdown-item').text().trim()).toBe(
- discussionFiltersMock[0].title,
- );
- });
+ it('renders the all filters', () => {
+ expect(wrapper.findAll('.discussion-filter-container .dropdown-item').length).toBe(
+ discussionFiltersMock.length,
+ );
+ });
- it('disables the dropdown when discussions are loading', () => {
- store.state.isLoading = true;
+ it('renders the default selected item', () => {
+ expect(wrapper.find('.discussion-filter-container .dropdown-item').text().trim()).toBe(
+ discussionFiltersMock[0].title,
+ );
+ });
- expect(wrapper.findComponent(GlDropdown).props('disabled')).toBe(true);
- });
+ it('disables the dropdown when discussions are loading', () => {
+ store.state.isLoading = true;
- it('updates to the selected item', () => {
- const filterItem = findFilter(DISCUSSION_FILTER_TYPES.ALL);
+ expect(wrapper.findComponent(GlDropdown).props('disabled')).toBe(true);
+ });
- filterItem.trigger('click');
+ it('updates to the selected item', () => {
+ const filterItem = findFilter(DISCUSSION_FILTER_TYPES.ALL);
- expect(wrapper.vm.currentFilter.title).toBe(filterItem.text().trim());
- });
+ filterItem.trigger('click');
- it('only updates when selected filter changes', () => {
- findFilter(DISCUSSION_FILTER_TYPES.ALL).trigger('click');
+ expect(filterItem.text().trim()).toBe('Show all activity');
+ });
- expect(filterDiscussion).not.toHaveBeenCalled();
- });
+ it('only updates when selected filter changes', () => {
+ findFilter(DISCUSSION_FILTER_TYPES.ALL).trigger('click');
+
+ expect(filterDiscussion).not.toHaveBeenCalled();
+ });
- it('disables timeline view if it was enabled', () => {
- store.state.isTimelineEnabled = true;
+ it('disables timeline view if it was enabled', () => {
+ store.state.isTimelineEnabled = true;
- findFilter(DISCUSSION_FILTER_TYPES.HISTORY).trigger('click');
+ findFilter(DISCUSSION_FILTER_TYPES.HISTORY).trigger('click');
- expect(wrapper.vm.$store.state.isTimelineEnabled).toBe(false);
- });
+ expect(store.state.isTimelineEnabled).toBe(false);
+ });
- it('disables commenting when "Show history only" filter is applied', () => {
- findFilter(DISCUSSION_FILTER_TYPES.HISTORY).trigger('click');
+ it('disables commenting when "Show history only" filter is applied', () => {
+ findFilter(DISCUSSION_FILTER_TYPES.HISTORY).trigger('click');
- expect(wrapper.vm.$store.state.commentsDisabled).toBe(true);
- });
+ expect(store.state.commentsDisabled).toBe(true);
+ });
- it('enables commenting when "Show history only" filter is not applied', () => {
- findFilter(DISCUSSION_FILTER_TYPES.ALL).trigger('click');
+ it('enables commenting when "Show history only" filter is not applied', () => {
+ findFilter(DISCUSSION_FILTER_TYPES.ALL).trigger('click');
- expect(wrapper.vm.$store.state.commentsDisabled).toBe(false);
+ expect(store.state.commentsDisabled).toBe(false);
+ });
});
describe('Merge request tabs', () => {
@@ -222,52 +229,41 @@ describe('DiscussionFilter component', () => {
});
describe('URL with Links to notes', () => {
+ const findDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
+
afterEach(() => {
window.location.hash = '';
});
- it('updates the filter when the URL links to a note', async () => {
- window.location.hash = `note_${discussionMock.notes[0].id}`;
- wrapper.vm.currentValue = discussionFiltersMock[2].value;
- wrapper.vm.handleLocationHash();
-
- await nextTick();
- expect(wrapper.vm.currentValue).toBe(DISCUSSION_FILTERS_DEFAULT_VALUE);
- });
-
it('does not update the filter when the current filter is "Show all activity"', async () => {
window.location.hash = `note_${discussionMock.notes[0].id}`;
- wrapper.vm.handleLocationHash();
+ wrapper = mountComponent();
await nextTick();
- expect(wrapper.vm.currentValue).toBe(DISCUSSION_FILTERS_DEFAULT_VALUE);
+ const filtered = findDropdownItems().filter((el) => el.classes('is-active'));
+
+ expect(filtered).toHaveLength(1);
+ expect(filtered.at(0).text()).toBe(discussionFiltersMock[0].title);
});
it('only updates filter when the URL links to a note', async () => {
window.location.hash = `testing123`;
- wrapper.vm.handleLocationHash();
+ wrapper = mountComponent();
await nextTick();
- expect(wrapper.vm.currentValue).toBe(DISCUSSION_FILTERS_DEFAULT_VALUE);
- });
+ const filtered = findDropdownItems().filter((el) => el.classes('is-active'));
- it('fetches discussions when there is a hash', async () => {
- window.location.hash = `note_${discussionMock.notes[0].id}`;
- wrapper.vm.currentValue = discussionFiltersMock[2].value;
- jest.spyOn(wrapper.vm, 'selectFilter').mockImplementation(() => {});
- wrapper.vm.handleLocationHash();
-
- await nextTick();
- expect(wrapper.vm.selectFilter).toHaveBeenCalled();
+ expect(filtered).toHaveLength(1);
+ expect(filtered.at(0).text()).toBe(discussionFiltersMock[0].title);
});
it('does not fetch discussions when there is no hash', async () => {
window.location.hash = '';
- jest.spyOn(wrapper.vm, 'selectFilter').mockImplementation(() => {});
- wrapper.vm.handleLocationHash();
+ const selectFilterSpy = jest.spyOn(wrapper.vm, 'selectFilter').mockImplementation(() => {});
+ wrapper = mountComponent();
await nextTick();
- expect(wrapper.vm.selectFilter).not.toHaveBeenCalled();
+ expect(selectFilterSpy).not.toHaveBeenCalled();
});
});
});
diff --git a/spec/frontend/notes/components/note_actions/timeline_event_button_spec.js b/spec/frontend/notes/components/note_actions/timeline_event_button_spec.js
index bee08ee0605..7860e9d45da 100644
--- a/spec/frontend/notes/components/note_actions/timeline_event_button_spec.js
+++ b/spec/frontend/notes/components/note_actions/timeline_event_button_spec.js
@@ -22,7 +22,7 @@ describe('NoteTimelineEventButton', () => {
const findTimelineButton = () => wrapper.findComponent(GlButton);
- it('emits click-promote-comment-to-event', async () => {
+ it('emits click-promote-comment-to-event', () => {
findTimelineButton().vm.$emit('click');
expect(wrapper.emitted('click-promote-comment-to-event')).toEqual([[emitData]]);
diff --git a/spec/frontend/notes/components/note_awards_list_spec.js b/spec/frontend/notes/components/note_awards_list_spec.js
index 89ac0216f41..0107b27f980 100644
--- a/spec/frontend/notes/components/note_awards_list_spec.js
+++ b/spec/frontend/notes/components/note_awards_list_spec.js
@@ -1,76 +1,110 @@
import AxiosMockAdapter from 'axios-mock-adapter';
import Vue from 'vue';
+import Vuex from 'vuex';
import { TEST_HOST } from 'helpers/test_constants';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import { userDataMock } from 'jest/notes/mock_data';
+import EmojiPicker from '~/emoji/components/picker.vue';
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';
-describe('note_awards_list component', () => {
- let store;
- let vm;
- let awardsMock;
- let mock;
-
- const toggleAwardPath = `${TEST_HOST}/gitlab-org/gitlab-foss/notes/545/toggle_award_emoji`;
-
- beforeEach(() => {
- mock = new AxiosMockAdapter(axios);
-
- mock.onPost(toggleAwardPath).reply(HTTP_STATUS_OK, '');
+Vue.use(Vuex);
- const Component = Vue.extend(awardsNote);
-
- store = createStore();
- store.dispatch('setNoteableData', noteableDataMock);
- store.dispatch('setNotesData', notesDataMock);
- awardsMock = [
- {
- name: 'flag_tz',
- user: { id: 1, name: 'Administrator', username: 'root' },
- },
- {
- name: 'cartwheel_tone3',
- user: { id: 12, name: 'Bobbie Stehr', username: 'erin' },
- },
- ];
+describe('Note Awards List', () => {
+ let wrapper;
+ let mock;
- vm = new Component({
+ const awardsMock = [
+ {
+ name: 'flag_tz',
+ user: { id: 1, name: 'Administrator', username: 'root' },
+ },
+ {
+ name: 'cartwheel_tone3',
+ user: { id: 12, name: 'Bobbie Stehr', username: 'erin' },
+ },
+ ];
+ const toggleAwardPathMock = `${TEST_HOST}/gitlab-org/gitlab-foss/notes/545/toggle_award_emoji`;
+
+ const defaultProps = {
+ awards: awardsMock,
+ noteAuthorId: 2,
+ noteId: '545',
+ canAwardEmoji: false,
+ toggleAwardPath: '/gitlab-org/gitlab-foss/notes/545/toggle_award_emoji',
+ };
+
+ const findAddAward = () => wrapper.find('.js-add-award');
+ const findAwardButton = () => wrapper.findByTestId('award-button');
+ const findAllEmojiAwards = () => wrapper.findAll('gl-emoji');
+ const findEmojiPicker = () => wrapper.findComponent(EmojiPicker);
+
+ const createComponent = (props = defaultProps, store = createStore()) => {
+ wrapper = mountExtended(awardsNote, {
store,
propsData: {
- awards: awardsMock,
- noteAuthorId: 2,
- noteId: '545',
- canAwardEmoji: true,
- toggleAwardPath,
+ ...props,
},
- }).$mount();
- });
+ });
+ };
+
+ describe('Note Awards functionality', () => {
+ const toggleAwardRequestSpy = jest.fn();
+ const fakeStore = () => {
+ return new Vuex.Store({
+ getters: {
+ getUserData: () => userDataMock,
+ },
+ actions: {
+ toggleAwardRequest: toggleAwardRequestSpy,
+ },
+ });
+ };
- afterEach(() => {
- mock.restore();
- vm.$destroy();
- });
+ beforeEach(() => {
+ mock = new AxiosMockAdapter(axios);
+ mock.onPost(toggleAwardPathMock).reply(HTTP_STATUS_OK, '');
- it('should render awarded emojis', () => {
- expect(vm.$el.querySelector('.js-awards-block button [data-name="flag_tz"]')).toBeDefined();
- expect(
- vm.$el.querySelector('.js-awards-block button [data-name="cartwheel_tone3"]'),
- ).toBeDefined();
- });
+ createComponent(
+ {
+ awards: awardsMock,
+ noteAuthorId: 2,
+ noteId: '545',
+ canAwardEmoji: true,
+ toggleAwardPath: '/gitlab-org/gitlab-foss/notes/545/toggle_award_emoji',
+ },
+ fakeStore(),
+ );
+ });
- it('should be possible to remove awarded emoji', () => {
- jest.spyOn(vm, 'handleAward');
- jest.spyOn(vm, 'toggleAwardRequest');
- vm.$el.querySelector('.js-awards-block button').click();
+ afterEach(() => {
+ mock.restore();
+ });
- expect(vm.handleAward).toHaveBeenCalledWith('flag_tz');
- expect(vm.toggleAwardRequest).toHaveBeenCalled();
- });
+ it('should render awarded emojis', () => {
+ const emojiAwards = findAllEmojiAwards();
+
+ expect(emojiAwards).toHaveLength(awardsMock.length);
+ expect(emojiAwards.at(0).attributes('data-name')).toBe('flag_tz');
+ expect(emojiAwards.at(1).attributes('data-name')).toBe('cartwheel_tone3');
+ });
+
+ it('should be possible to add new emoji', () => {
+ expect(findEmojiPicker().exists()).toBe(true);
+ });
+
+ it('should be possible to remove awarded emoji', async () => {
+ await findAwardButton().vm.$emit('click');
- it('should be possible to add new emoji', () => {
- expect(vm.$el.querySelector('.js-add-award')).toBeDefined();
+ const { toggleAwardPath, noteId } = defaultProps;
+ expect(toggleAwardRequestSpy).toHaveBeenCalledWith(expect.anything(), {
+ awardName: awardsMock[0].name,
+ endpoint: toggleAwardPath,
+ noteId,
+ });
+ });
});
describe('when the user name contains special HTML characters', () => {
@@ -79,85 +113,69 @@ describe('note_awards_list component', () => {
user: { id: index, name: `&<>"\`'-${index}`, username: `user-${index}` },
});
- const mountComponent = () => {
- const Component = Vue.extend(awardsNote);
- vm = new Component({
- store,
- propsData: {
- awards: awardsMock,
- noteAuthorId: 0,
- noteId: '545',
- canAwardEmoji: true,
- toggleAwardPath: '/gitlab-org/gitlab-foss/notes/545/toggle_award_emoji',
- },
- }).$mount();
+ const customProps = {
+ awards: awardsMock,
+ noteAuthorId: 0,
+ noteId: '545',
+ canAwardEmoji: true,
+ toggleAwardPath: '/gitlab-org/gitlab-foss/notes/545/toggle_award_emoji',
};
- const findTooltip = () => vm.$el.querySelector('[title]').getAttribute('title');
-
- it('should only escape & and " characters', () => {
- awardsMock = [...new Array(1)].map(createAwardEmoji);
- mountComponent();
- const escapedName = awardsMock[0].user.name.replace(/&/g, '&amp;').replace(/"/g, '&quot;');
-
- expect(vm.$el.querySelector('[title]').outerHTML).toContain(escapedName);
- });
-
it('should not escape special HTML characters twice when only 1 person awarded', () => {
- awardsMock = [...new Array(1)].map(createAwardEmoji);
- mountComponent();
+ const awardsCopy = [...new Array(1)].map(createAwardEmoji);
+ createComponent({
+ ...customProps,
+ awards: awardsCopy,
+ });
- awardsMock.forEach((award) => {
- expect(findTooltip()).toContain(award.user.name);
+ awardsCopy.forEach((award) => {
+ expect(findAwardButton().attributes('title')).toContain(award.user.name);
});
});
it('should not escape special HTML characters twice when 2 people awarded', () => {
- awardsMock = [...new Array(2)].map(createAwardEmoji);
- mountComponent();
+ const awardsCopy = [...new Array(2)].map(createAwardEmoji);
+ createComponent({
+ ...customProps,
+ awards: awardsCopy,
+ });
- awardsMock.forEach((award) => {
- expect(findTooltip()).toContain(award.user.name);
+ awardsCopy.forEach((award) => {
+ expect(findAwardButton().attributes('title')).toContain(award.user.name);
});
});
it('should not escape special HTML characters twice when more than 10 people awarded', () => {
- awardsMock = [...new Array(11)].map(createAwardEmoji);
- mountComponent();
+ const awardsCopy = [...new Array(11)].map(createAwardEmoji);
+ createComponent({
+ ...customProps,
+ awards: awardsCopy,
+ });
// Testing only the first 10 awards since 11 onward will not be displayed.
- awardsMock.slice(0, 10).forEach((award) => {
- expect(findTooltip()).toContain(award.user.name);
+ awardsCopy.slice(0, 10).forEach((award) => {
+ expect(findAwardButton().attributes('title')).toContain(award.user.name);
});
});
});
- describe('when the user cannot award emoji', () => {
+ describe('when the user cannot award an emoji', () => {
beforeEach(() => {
- const Component = Vue.extend(awardsNote);
-
- vm = new Component({
- store,
- propsData: {
- awards: awardsMock,
- noteAuthorId: 2,
- noteId: '545',
- canAwardEmoji: false,
- toggleAwardPath: '/gitlab-org/gitlab-foss/notes/545/toggle_award_emoji',
- },
- }).$mount();
+ createComponent({
+ awards: awardsMock,
+ noteAuthorId: 2,
+ noteId: '545',
+ canAwardEmoji: false,
+ toggleAwardPath: '/gitlab-org/gitlab-foss/notes/545/toggle_award_emoji',
+ });
});
- it('should not be possible to remove awarded emoji', () => {
- jest.spyOn(vm, 'toggleAwardRequest');
-
- vm.$el.querySelector('.js-awards-block button').click();
-
- expect(vm.toggleAwardRequest).not.toHaveBeenCalled();
+ it('should display an award emoji button with a disabled class', () => {
+ expect(findAwardButton().classes()).toContain('disabled');
});
it('should not be possible to add new emoji', () => {
- expect(vm.$el.querySelector('.js-add-award')).toBeNull();
+ expect(findAddAward().exists()).toBe(false);
});
});
});
diff --git a/spec/frontend/notes/components/note_body_spec.js b/spec/frontend/notes/components/note_body_spec.js
index b4f185004bb..c4f8e50b969 100644
--- a/spec/frontend/notes/components/note_body_spec.js
+++ b/spec/frontend/notes/components/note_body_spec.js
@@ -7,10 +7,7 @@ import NoteAwardsList from '~/notes/components/note_awards_list.vue';
import NoteForm from '~/notes/components/note_form.vue';
import createStore from '~/notes/stores';
import notes from '~/notes/stores/modules/index';
-import Autosave from '~/autosave';
-
import Suggestions from '~/vue_shared/components/markdown/suggestions.vue';
-
import { noteableDataMock, notesDataMock, note } from '../mock_data';
jest.mock('~/autosave');
@@ -82,11 +79,6 @@ describe('issue_note_body component', () => {
expect(wrapper.findComponent(NoteForm).props('saveButtonTitle')).toBe(buttonText);
});
- it('adds autosave', () => {
- // passing undefined instead of an element because of shallowMount
- expect(Autosave).toHaveBeenCalledWith(undefined, ['Note', note.noteable_type, note.id]);
- });
-
describe('isInternalNote', () => {
beforeEach(() => {
wrapper.setProps({ isInternalNote: true });
diff --git a/spec/frontend/notes/components/note_form_spec.js b/spec/frontend/notes/components/note_form_spec.js
index 59362e18098..d6413d33c99 100644
--- a/spec/frontend/notes/components/note_form_spec.js
+++ b/spec/frontend/notes/components/note_form_spec.js
@@ -1,42 +1,39 @@
-import { GlLink } from '@gitlab/ui';
-import { mount } from '@vue/test-utils';
+import { GlLink, GlFormCheckbox } from '@gitlab/ui';
import { nextTick } from 'vue';
import batchComments from '~/batch_comments/stores/modules/batch_comments';
-import { getDraft, updateDraft } from '~/lib/utils/autosave';
import NoteForm from '~/notes/components/note_form.vue';
import createStore from '~/notes/stores';
import MarkdownField from '~/vue_shared/components/markdown/field.vue';
import { AT_WHO_ACTIVE_CLASS } from '~/gfm_auto_complete';
+import eventHub from '~/environments/event_hub';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
import { noteableDataMock, notesDataMock, discussionMock, note } from '../mock_data';
jest.mock('~/lib/utils/autosave');
describe('issue_note_form component', () => {
- const dummyAutosaveKey = 'some-autosave-key';
- const dummyDraft = 'dummy draft content';
-
let store;
let wrapper;
let props;
- const createComponentWrapper = () => {
- return mount(NoteForm, {
+ const createComponentWrapper = (propsData = {}, provide = {}) => {
+ wrapper = mountExtended(NoteForm, {
store,
- propsData: props,
+ propsData: {
+ ...props,
+ ...propsData,
+ },
+ provide: {
+ glFeatures: provide,
+ },
});
};
- const findCancelButton = () => wrapper.find('[data-testid="cancel"]');
+ const findCancelButton = () => wrapper.findByTestId('cancel');
+ const findCancelCommentButton = () => wrapper.findByTestId('cancelBatchCommentsEnabled');
+ const findMarkdownField = () => wrapper.findComponent(MarkdownField);
beforeEach(() => {
- getDraft.mockImplementation((key) => {
- if (key === dummyAutosaveKey) {
- return dummyDraft;
- }
-
- return null;
- });
-
store = createStore();
store.dispatch('setNoteableData', noteableDataMock);
store.dispatch('setNotesData', notesDataMock);
@@ -50,27 +47,37 @@ describe('issue_note_form component', () => {
describe('noteHash', () => {
beforeEach(() => {
- wrapper = createComponentWrapper();
+ createComponentWrapper();
});
it('returns note hash string based on `noteId`', () => {
expect(wrapper.vm.noteHash).toBe(`#note_${props.noteId}`);
});
- it('return note hash as `#` when `noteId` is empty', async () => {
- wrapper.setProps({
- ...props,
+ it('return note hash as `#` when `noteId` is empty', () => {
+ createComponentWrapper({
noteId: '',
});
- await nextTick();
expect(wrapper.vm.noteHash).toBe('#');
});
});
+ it('hides content editor switcher if feature flag content_editor_on_issues is off', () => {
+ createComponentWrapper({}, { contentEditorOnIssues: false });
+
+ expect(wrapper.text()).not.toContain('Rich text');
+ });
+
+ it('shows content editor switcher if feature flag content_editor_on_issues is on', () => {
+ createComponentWrapper({}, { contentEditorOnIssues: true });
+
+ expect(wrapper.text()).toContain('Rich text');
+ });
+
describe('conflicts editing', () => {
beforeEach(() => {
- wrapper = createComponentWrapper();
+ createComponentWrapper();
});
it('should show conflict message if note changes outside the component', async () => {
@@ -94,15 +101,13 @@ describe('issue_note_form component', () => {
describe('form', () => {
beforeEach(() => {
- wrapper = createComponentWrapper();
+ createComponentWrapper();
});
it('should render text area with placeholder', () => {
const textarea = wrapper.find('textarea');
- expect(textarea.attributes('placeholder')).toEqual(
- 'Write a comment or drag your files here…',
- );
+ expect(textarea.attributes('placeholder')).toBe('Write a comment or drag your files here…');
});
it('should set data-supports-quick-actions to enable autocomplete', () => {
@@ -117,23 +122,21 @@ describe('issue_note_form component', () => {
${true} | ${'Write an internal note or drag your files here…'}
`(
'should set correct textarea placeholder text when discussion confidentiality is $internal',
- ({ internal, placeholder }) => {
+ async ({ internal, placeholder }) => {
props.note = {
...note,
internal,
};
- wrapper = createComponentWrapper();
+ createComponentWrapper();
+
+ await nextTick();
expect(wrapper.find('textarea').attributes('placeholder')).toBe(placeholder);
},
);
it('should link to markdown docs', () => {
- const { markdownDocsPath } = notesDataMock;
- const markdownField = wrapper.findComponent(MarkdownField);
- const markdownFieldProps = markdownField.props();
-
- expect(markdownFieldProps.markdownDocsPath).toBe(markdownDocsPath);
+ expect(findMarkdownField().props('markdownDocsPath')).toBe(notesDataMock.markdownDocsPath);
});
describe('keyboard events', () => {
@@ -146,12 +149,11 @@ describe('issue_note_form component', () => {
describe('up', () => {
it('should ender edit mode', () => {
- // TODO: do not spy on vm
- jest.spyOn(wrapper.vm, 'editMyLastNote');
+ const eventHubSpy = jest.spyOn(eventHub, '$emit');
textarea.trigger('keydown.up');
- expect(wrapper.vm.editMyLastNote).toHaveBeenCalled();
+ expect(eventHubSpy).not.toHaveBeenCalled();
});
});
@@ -159,17 +161,13 @@ describe('issue_note_form component', () => {
it('should save note when cmd+enter is pressed', () => {
textarea.trigger('keydown.enter', { metaKey: true });
- const { handleFormUpdate } = wrapper.emitted();
-
- expect(handleFormUpdate.length).toBe(1);
+ expect(wrapper.emitted('handleFormUpdate')).toHaveLength(1);
});
it('should save note when ctrl+enter is pressed', () => {
textarea.trigger('keydown.enter', { ctrlKey: true });
- const { handleFormUpdate } = wrapper.emitted();
-
- expect(handleFormUpdate.length).toBe(1);
+ expect(wrapper.emitted('handleFormUpdate')).toHaveLength(1);
});
it('should disable textarea when ctrl+enter is pressed', async () => {
@@ -185,151 +183,62 @@ describe('issue_note_form component', () => {
});
describe('actions', () => {
- it('should be possible to cancel', async () => {
- wrapper.setProps({
- ...props,
- });
- await nextTick();
+ it('should be possible to cancel', () => {
+ createComponentWrapper();
- const cancelButton = findCancelButton();
- cancelButton.vm.$emit('click');
- await nextTick();
+ findCancelButton().vm.$emit('click');
- expect(wrapper.emitted().cancelForm).toHaveLength(1);
+ expect(wrapper.emitted('cancelForm')).toHaveLength(1);
});
it('will not cancel form if there is an active at-who-active class', async () => {
- wrapper.setProps({
- ...props,
- });
- await nextTick();
+ createComponentWrapper();
- const textareaEl = wrapper.vm.$refs.textarea;
+ const textareaEl = wrapper.vm.$refs.markdownEditor.$el.querySelector('textarea');
const cancelButton = findCancelButton();
textareaEl.classList.add(AT_WHO_ACTIVE_CLASS);
cancelButton.vm.$emit('click');
await nextTick();
- expect(wrapper.emitted().cancelForm).toBeUndefined();
+ expect(wrapper.emitted('cancelForm')).toBeUndefined();
});
- it('should be possible to update the note', async () => {
- wrapper.setProps({
- ...props,
- });
- await nextTick();
+ it('should be possible to update the note', () => {
+ createComponentWrapper();
const textarea = wrapper.find('textarea');
textarea.setValue('Foo');
const saveButton = wrapper.find('.js-vue-issue-save');
saveButton.vm.$emit('click');
- expect(wrapper.vm.isSubmitting).toBe(true);
+ expect(wrapper.emitted('handleFormUpdate')).toHaveLength(1);
});
});
});
- describe('with autosaveKey', () => {
- describe('with draft', () => {
- beforeEach(() => {
- Object.assign(props, {
- noteBody: '',
- autosaveKey: dummyAutosaveKey,
- });
- wrapper = createComponentWrapper();
-
- return nextTick();
- });
-
- it('displays the draft in textarea', () => {
- const textarea = wrapper.find('textarea');
-
- expect(textarea.element.value).toBe(dummyDraft);
- });
- });
-
- describe('without draft', () => {
- beforeEach(() => {
- Object.assign(props, {
- noteBody: '',
- autosaveKey: 'some key without draft',
- });
- wrapper = createComponentWrapper();
-
- return nextTick();
- });
-
- it('leaves the textarea empty', () => {
- const textarea = wrapper.find('textarea');
-
- expect(textarea.element.value).toBe('');
- });
- });
-
- it('updates the draft if textarea content changes', () => {
- Object.assign(props, {
- noteBody: '',
- autosaveKey: dummyAutosaveKey,
- });
- wrapper = createComponentWrapper();
- const textarea = wrapper.find('textarea');
- const dummyContent = 'some new content';
-
- textarea.setValue(dummyContent);
-
- expect(updateDraft).toHaveBeenCalledWith(dummyAutosaveKey, dummyContent);
- });
-
- it('does not save draft when ctrl+enter is pressed', () => {
- const options = {
- noteBody: '',
- autosaveKey: dummyAutosaveKey,
- };
-
- props = { ...props, ...options };
- wrapper = createComponentWrapper();
-
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({ isSubmittingWithKeydown: true });
-
- const textarea = wrapper.find('textarea');
- textarea.setValue('some content');
- textarea.trigger('keydown.enter', { metaKey: true });
-
- expect(updateDraft).not.toHaveBeenCalled();
- });
- });
-
describe('with batch comments', () => {
beforeEach(() => {
store.registerModule('batchComments', batchComments());
- wrapper = createComponentWrapper();
- wrapper.setProps({
- ...props,
+ createComponentWrapper({
isDraft: true,
noteId: '',
discussion: { ...discussionMock, for_commit: false },
});
});
- it('should be possible to cancel', async () => {
- jest.spyOn(wrapper.vm, 'cancelHandler');
+ it('should be possible to cancel', () => {
+ findCancelCommentButton().vm.$emit('click');
- await nextTick();
- const cancelButton = wrapper.find('[data-testid="cancelBatchCommentsEnabled"]');
- cancelButton.vm.$emit('click');
-
- expect(wrapper.vm.cancelHandler).toHaveBeenCalledWith(true);
+ expect(wrapper.emitted('cancelForm')).toEqual([[true, false]]);
});
it('shows resolve checkbox', () => {
- expect(wrapper.find('.js-resolve-checkbox').exists()).toBe(true);
+ expect(wrapper.findComponent(GlFormCheckbox).exists()).toBe(true);
});
- it('hides resolve checkbox', async () => {
- wrapper.setProps({
+ it('hides resolve checkbox', () => {
+ createComponentWrapper({
isDraft: false,
discussion: {
...discussionMock,
@@ -344,15 +253,11 @@ describe('issue_note_form component', () => {
},
});
- await nextTick();
-
- expect(wrapper.find('.js-resolve-checkbox').exists()).toBe(false);
+ expect(wrapper.findComponent(GlFormCheckbox).exists()).toBe(false);
});
- it('hides actions for commits', async () => {
- wrapper.setProps({ discussion: { for_commit: true } });
-
- await nextTick();
+ it('hides actions for commits', () => {
+ createComponentWrapper({ discussion: { for_commit: true } });
expect(wrapper.find('.note-form-actions').text()).not.toContain('Start a review');
});
@@ -361,13 +266,12 @@ describe('issue_note_form component', () => {
it('should start review or add to review when cmd+enter is pressed', async () => {
const textarea = wrapper.find('textarea');
- jest.spyOn(wrapper.vm, 'handleAddToReview');
-
textarea.setValue('Foo');
textarea.trigger('keydown.enter', { metaKey: true });
await nextTick();
- expect(wrapper.vm.handleAddToReview).toHaveBeenCalled();
+
+ expect(wrapper.emitted('handleFormUpdateAddToReview')).toEqual([['Foo', false]]);
});
});
});
diff --git a/spec/frontend/notes/components/noteable_note_spec.js b/spec/frontend/notes/components/noteable_note_spec.js
index b158cfff10d..bce335aa035 100644
--- a/spec/frontend/notes/components/noteable_note_spec.js
+++ b/spec/frontend/notes/components/noteable_note_spec.js
@@ -375,6 +375,17 @@ describe('issue_note', () => {
expect(wrapper.emitted('handleUpdateNote')).toHaveLength(1);
});
+ it('should not update note with sensitive token', () => {
+ const sensitiveMessage = 'token: glpat-1234567890abcdefghij';
+
+ createWrapper();
+ updateActions();
+ wrapper
+ .findComponent(NoteBody)
+ .vm.$emit('handleFormUpdate', { ...params, noteText: sensitiveMessage });
+ expect(updateNote).not.toHaveBeenCalled();
+ });
+
it('does not stringify empty position', () => {
createWrapper();
updateActions();
diff --git a/spec/frontend/notes/components/notes_app_spec.js b/spec/frontend/notes/components/notes_app_spec.js
index 832264aa7d3..3fe31506223 100644
--- a/spec/frontend/notes/components/notes_app_spec.js
+++ b/spec/frontend/notes/components/notes_app_spec.js
@@ -174,7 +174,7 @@ describe('note_app', () => {
});
describe('while fetching data', () => {
- beforeEach(async () => {
+ beforeEach(() => {
wrapper = mountComponent();
});
diff --git a/spec/frontend/notes/deprecated_notes_spec.js b/spec/frontend/notes/deprecated_notes_spec.js
index 40f10ca901b..355ecb78187 100644
--- a/spec/frontend/notes/deprecated_notes_spec.js
+++ b/spec/frontend/notes/deprecated_notes_spec.js
@@ -1,9 +1,11 @@
/* eslint-disable import/no-commonjs, no-new */
-import MockAdapter from 'axios-mock-adapter';
import $ from 'jquery';
+import MockAdapter from 'axios-mock-adapter';
+import htmlPipelineSchedulesEditSnippets from 'test_fixtures/snippets/show.html';
+import htmlPipelineSchedulesEditCommit from 'test_fixtures/commit/show.html';
import '~/behaviors/markdown/render_gfm';
-import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
+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';
@@ -19,7 +21,6 @@ const Notes = require('~/deprecated_notes').default;
const FLASH_TYPE_ALERT = 'alert';
const NOTES_POST_PATH = /(.*)\/notes\?html=true$/;
-const fixture = 'snippets/show.html';
let mockAxios;
window.project_uploads_path = `${TEST_HOST}/uploads`;
@@ -36,7 +37,7 @@ function wrappedDiscussionNote(note) {
// eslint-disable-next-line jest/no-disabled-tests
describe.skip('Old Notes (~/deprecated_notes.js)', () => {
beforeEach(() => {
- loadHTMLFixture(fixture);
+ setHTMLFixture(htmlPipelineSchedulesEditSnippets);
// Re-declare this here so that test_setup.js#beforeEach() doesn't
// overwrite it.
@@ -671,7 +672,7 @@ describe.skip('Old Notes (~/deprecated_notes.js)', () => {
let $notesContainer;
beforeEach(() => {
- loadHTMLFixture('commit/show.html');
+ setHTMLFixture(htmlPipelineSchedulesEditCommit);
mockAxios.onPost(NOTES_POST_PATH).reply(HTTP_STATUS_OK, note);
new Notes('', []);
diff --git a/spec/frontend/notes/stores/actions_spec.js b/spec/frontend/notes/stores/actions_spec.js
index 0d3ebea7af2..97249d232dc 100644
--- a/spec/frontend/notes/stores/actions_spec.js
+++ b/spec/frontend/notes/stores/actions_spec.js
@@ -257,14 +257,14 @@ describe('Actions Notes Store', () => {
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) => {
+ const advanceAndRAF = (time) => {
if (time) {
jest.advanceTimersByTime(time);
}
return waitForPromises();
};
- const advanceXMoreIntervals = async (number) => {
+ const advanceXMoreIntervals = (number) => {
const timeoutLength = pollInterval * number;
return advanceAndRAF(timeoutLength);
@@ -273,7 +273,7 @@ describe('Actions Notes Store', () => {
await store.dispatch('poll');
await advanceAndRAF(2);
};
- const cleanUp = async () => {
+ const cleanUp = () => {
jest.clearAllTimers();
return store.dispatch('stopPolling');
diff --git a/spec/frontend/notifications/components/custom_notifications_modal_spec.js b/spec/frontend/notifications/components/custom_notifications_modal_spec.js
index 0fbd073191e..480d617fcb2 100644
--- a/spec/frontend/notifications/components/custom_notifications_modal_spec.js
+++ b/spec/frontend/notifications/components/custom_notifications_modal_spec.js
@@ -103,7 +103,7 @@ describe('CustomNotificationsModal', () => {
${1} | ${'new_note'} | ${'New note'} | ${false} | ${false}
`(
'renders a checkbox for "$eventName" with checked=$enabled',
- async ({ index, eventName, enabled, loading }) => {
+ ({ index, eventName, enabled, loading }) => {
const checkbox = findCheckboxAt(index);
expect(checkbox.text()).toContain(eventName);
expect(checkbox.vm.$attrs.checked).toBe(enabled);
diff --git a/spec/frontend/oauth_application/components/oauth_secret_spec.js b/spec/frontend/oauth_application/components/oauth_secret_spec.js
new file mode 100644
index 00000000000..c38bd066da8
--- /dev/null
+++ b/spec/frontend/oauth_application/components/oauth_secret_spec.js
@@ -0,0 +1,116 @@
+import { GlButton, GlModal } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import MockAdapter from 'axios-mock-adapter';
+import waitForPromises from 'helpers/wait_for_promises';
+import { createAlert, VARIANT_SUCCESS, VARIANT_WARNING } from '~/alert';
+import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
+import OAuthSecret from '~/oauth_application/components/oauth_secret.vue';
+import {
+ RENEW_SECRET_FAILURE,
+ RENEW_SECRET_SUCCESS,
+ WARNING_NO_SECRET,
+} from '~/oauth_application/constants';
+import InputCopyToggleVisibility from '~/vue_shared/components/form/input_copy_toggle_visibility.vue';
+
+jest.mock('~/alert');
+const mockEvent = { preventDefault: jest.fn() };
+
+describe('OAuthSecret', () => {
+ let wrapper;
+ const renewPath = '/applications/1/renew';
+
+ const createComponent = (provide = {}) => {
+ wrapper = shallowMount(OAuthSecret, {
+ provide: {
+ initialSecret: undefined,
+ renewPath,
+ ...provide,
+ },
+ });
+ };
+
+ const findInputCopyToggleVisibility = () => wrapper.findComponent(InputCopyToggleVisibility);
+ const findRenewSecretButton = () => wrapper.findComponent(GlButton);
+ const findModal = () => wrapper.findComponent(GlModal);
+
+ describe('when secret is provided', () => {
+ const initialSecret = 'my secret';
+ beforeEach(() => {
+ createComponent({ initialSecret });
+ });
+
+ it('shows the masked secret', () => {
+ expect(findInputCopyToggleVisibility().props('value')).toBe(initialSecret);
+ });
+
+ it('shows the renew secret button', () => {
+ expect(findRenewSecretButton().exists()).toBe(true);
+ });
+ });
+
+ describe('when secret is not provided', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('shows an alert', () => {
+ expect(createAlert).toHaveBeenCalledWith({
+ message: WARNING_NO_SECRET,
+ variant: VARIANT_WARNING,
+ });
+ });
+
+ it('shows the renew secret button', () => {
+ expect(findRenewSecretButton().exists()).toBe(true);
+ });
+
+ describe('when renew secret button is selected', () => {
+ beforeEach(() => {
+ createComponent();
+ findRenewSecretButton().vm.$emit('click');
+ });
+
+ it('shows a modal', () => {
+ expect(findModal().props('visible')).toBe(true);
+ });
+
+ describe('when secret renewal succeeds', () => {
+ const initialSecret = 'my secret';
+
+ beforeEach(async () => {
+ const mockAxios = new MockAdapter(axios);
+ mockAxios.onPut().reply(HTTP_STATUS_OK, { secret: initialSecret });
+ findModal().vm.$emit('primary', mockEvent);
+ await waitForPromises();
+ });
+
+ it('shows an alert', () => {
+ expect(createAlert).toHaveBeenCalledWith({
+ message: RENEW_SECRET_SUCCESS,
+ variant: VARIANT_SUCCESS,
+ });
+ });
+
+ it('shows the new secret', () => {
+ expect(findInputCopyToggleVisibility().props('value')).toBe(initialSecret);
+ });
+ });
+
+ describe('when secret renewal fails', () => {
+ beforeEach(async () => {
+ const mockAxios = new MockAdapter(axios);
+ mockAxios.onPut().reply(HTTP_STATUS_INTERNAL_SERVER_ERROR);
+ findModal().vm.$emit('primary', mockEvent);
+ await waitForPromises();
+ });
+
+ it('creates an alert', () => {
+ expect(createAlert).toHaveBeenCalledWith({
+ message: RENEW_SECRET_FAILURE,
+ });
+ });
+ });
+ });
+ });
+});
diff --git a/spec/frontend/oauth_remember_me_spec.js b/spec/frontend/oauth_remember_me_spec.js
index 1fa0e0aa8f6..7be3d441eb3 100644
--- a/spec/frontend/oauth_remember_me_spec.js
+++ b/spec/frontend/oauth_remember_me_spec.js
@@ -17,19 +17,16 @@ describe('OAuthRememberMe', () => {
resetHTMLFixture();
});
- it('adds the "remember_me" query parameter to all OAuth login buttons', () => {
- $('#oauth-container #remember_me').click();
+ it('adds and removes the "remember_me" query parameter from all OAuth login buttons', () => {
+ $('#oauth-container #remember_me_omniauth').click();
expect(findFormAction('.twitter')).toBe('http://example.com/?remember_me=1');
expect(findFormAction('.github')).toBe('http://example.com/?remember_me=1');
expect(findFormAction('.facebook')).toBe(
'http://example.com/?redirect_fragment=L1&remember_me=1',
);
- });
- it('removes the "remember_me" query parameter from all OAuth login buttons', () => {
- $('#oauth-container #remember_me').click();
- $('#oauth-container #remember_me').click();
+ $('#oauth-container #remember_me_omniauth').click();
expect(findFormAction('.twitter')).toBe('http://example.com/');
expect(findFormAction('.github')).toBe('http://example.com/');
diff --git a/spec/frontend/observability/index_spec.js b/spec/frontend/observability/index_spec.js
index 83f72ff72b5..25eb048c62b 100644
--- a/spec/frontend/observability/index_spec.js
+++ b/spec/frontend/observability/index_spec.js
@@ -52,7 +52,7 @@ describe('renderObservability', () => {
);
});
- it('handle route-update events', async () => {
+ it('handle route-update events', () => {
component.vm.$router.push('/something?foo=bar');
component.vm.$emit('route-update', { url: '/some_path' });
expect(component.vm.$router.currentRoute.path).toBe('/something');
diff --git a/spec/frontend/operation_settings/components/metrics_settings_spec.js b/spec/frontend/operation_settings/components/metrics_settings_spec.js
index ee450dfc851..6ea08d4a9a5 100644
--- a/spec/frontend/operation_settings/components/metrics_settings_spec.js
+++ b/spec/frontend/operation_settings/components/metrics_settings_spec.js
@@ -198,7 +198,7 @@ describe('operation settings external dashboard component', () => {
expect(refreshCurrentPage).toHaveBeenCalled();
});
- it('creates alert banner on error', async () => {
+ it('creates an alert on error', async () => {
mountComponent(false);
const message = 'mockErrorMessage';
axios.patch.mockRejectedValue({ response: { data: { message } } });
diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/details_header_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/details_header_spec.js
index 9e443234c34..01089422376 100644
--- a/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/details_header_spec.js
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/details_header_spec.js
@@ -1,10 +1,10 @@
import { GlDropdownItem, GlIcon, GlDropdown } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
import Vue, { nextTick } from 'vue';
import { numberToHumanSize } from '~/lib/utils/number_utils';
import { useFakeDate } from 'helpers/fake_date';
import createMockApollo from 'helpers/mock_apollo_helper';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import waitForPromises from 'helpers/wait_for_promises';
import component from '~/packages_and_registries/container_registry/explorer/components/details_page/details_header.vue';
@@ -22,37 +22,27 @@ import {
} from '~/packages_and_registries/container_registry/explorer/constants';
import getContainerRepositoryMetadata from '~/packages_and_registries/container_registry/explorer/graphql/queries/get_container_repository_metadata.query.graphql';
import TitleArea from '~/vue_shared/components/registry/title_area.vue';
-import { imageTagsCountMock } from '../../mock_data';
+import { containerRepositoryMock, imageTagsCountMock } from '../../mock_data';
describe('Details Header', () => {
let wrapper;
let apolloProvider;
const defaultImage = {
- name: 'foo',
- updatedAt: '2020-11-03T13:29:21Z',
- canDelete: true,
- project: {
- visibility: 'public',
- path: 'path',
- containerExpirationPolicy: {
- enabled: false,
- },
- },
+ ...containerRepositoryMock,
};
// set the date to Dec 4, 2020
useFakeDate(2020, 11, 4);
- const findByTestId = (testId) => wrapper.find(`[data-testid="${testId}"]`);
- const findLastUpdatedAndVisibility = () => findByTestId('updated-and-visibility');
- const findTitle = () => findByTestId('title');
- const findTagsCount = () => findByTestId('tags-count');
- const findCleanup = () => findByTestId('cleanup');
+ const findCreatedAndVisibility = () => wrapper.findByTestId('created-and-visibility');
+ const findTitle = () => wrapper.findByTestId('title');
+ const findTagsCount = () => wrapper.findByTestId('tags-count');
+ const findCleanup = () => wrapper.findByTestId('cleanup');
const findDeleteButton = () => wrapper.findComponent(GlDropdownItem);
const findInfoIcon = () => wrapper.findComponent(GlIcon);
const findMenu = () => wrapper.findComponent(GlDropdown);
- const findSize = () => findByTestId('image-size');
+ const findSize = () => wrapper.findByTestId('image-size');
const waitForMetadataItems = async () => {
// Metadata items are printed by a loop in the title-area and it takes two ticks for them to be available
@@ -69,7 +59,7 @@ describe('Details Header', () => {
const requestHandlers = [[getContainerRepositoryMetadata, resolver]];
apolloProvider = createMockApollo(requestHandlers);
- wrapper = shallowMount(component, {
+ wrapper = shallowMountExtended(component, {
apolloProvider,
propsData,
directives: {
@@ -97,7 +87,7 @@ describe('Details Header', () => {
});
it('root image shows project path name', () => {
- expect(findTitle().text()).toBe('path');
+ expect(findTitle().text()).toBe('gitlab-test');
});
it('has an icon', () => {
@@ -119,7 +109,7 @@ describe('Details Header', () => {
});
it('shows image.name', () => {
- expect(findTitle().text()).toContain('foo');
+ expect(findTitle().text()).toContain('rails-12009');
});
it('has no icon', () => {
@@ -247,7 +237,7 @@ describe('Details Header', () => {
expect(findCleanup().props('icon')).toBe('expire');
});
- it('when the expiration policy is disabled', async () => {
+ it('when cleanup is not scheduled', async () => {
mountComponent();
await waitForMetadataItems();
@@ -287,12 +277,12 @@ describe('Details Header', () => {
);
});
- describe('visibility and updated at', () => {
- it('has last updated text', async () => {
+ describe('visibility and created at', () => {
+ it('has created text', async () => {
mountComponent();
await waitForMetadataItems();
- expect(findLastUpdatedAndVisibility().props('text')).toBe('Last updated 1 month ago');
+ expect(findCreatedAndVisibility().props('text')).toBe('Created Nov 3, 2020 13:29');
});
describe('visibility icon', () => {
@@ -300,7 +290,7 @@ describe('Details Header', () => {
mountComponent();
await waitForMetadataItems();
- expect(findLastUpdatedAndVisibility().props('icon')).toBe('eye');
+ expect(findCreatedAndVisibility().props('icon')).toBe('eye');
});
it('shows an eye slashed when the project is not public', async () => {
mountComponent({
@@ -308,7 +298,7 @@ describe('Details Header', () => {
});
await waitForMetadataItems();
- expect(findLastUpdatedAndVisibility().props('icon')).toBe('eye-slash');
+ expect(findCreatedAndVisibility().props('icon')).toBe('eye-slash');
});
});
});
diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/tags_list_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/tags_list_spec.js
index 09d0370efbf..0cbb9eab018 100644
--- a/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/tags_list_spec.js
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/tags_list_spec.js
@@ -4,13 +4,15 @@ import { GlEmptyState } from '@gitlab/ui';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
-
+import Tracking from '~/tracking';
import component from '~/packages_and_registries/container_registry/explorer/components/details_page/tags_list.vue';
import TagsListRow from '~/packages_and_registries/container_registry/explorer/components/details_page/tags_list_row.vue';
import TagsLoader from '~/packages_and_registries/shared/components/tags_loader.vue';
import RegistryList from '~/packages_and_registries/shared/components/registry_list.vue';
import PersistedSearch from '~/packages_and_registries/shared/components/persisted_search.vue';
import getContainerRepositoryTagsQuery from '~/packages_and_registries/container_registry/explorer/graphql/queries/get_container_repository_tags.query.graphql';
+import deleteContainerRepositoryTagsMutation from '~/packages_and_registries/container_registry/explorer/graphql/mutations/delete_container_repository_tags.mutation.graphql';
+
import {
GRAPHQL_PAGE_SIZE,
NO_TAGS_TITLE,
@@ -19,7 +21,13 @@ import {
NO_TAGS_MATCHING_FILTERS_DESCRIPTION,
} from '~/packages_and_registries/container_registry/explorer/constants/index';
import { FILTERED_SEARCH_TERM } from '~/vue_shared/components/filtered_search_bar/constants';
-import { tagsMock, imageTagsMock, tagsPageInfo } from '../../mock_data';
+import {
+ graphQLDeleteImageRepositoryTagsMock,
+ tagsMock,
+ imageTagsMock,
+ tagsPageInfo,
+} from '../../mock_data';
+import { DeleteModal } from '../../stubs';
describe('Tags List', () => {
let wrapper;
@@ -31,6 +39,7 @@ describe('Tags List', () => {
noContainersImage: 'noContainersImage',
};
+ const findDeleteModal = () => wrapper.findComponent(DeleteModal);
const findPersistedSearch = () => wrapper.findComponent(PersistedSearch);
const findTagsListRow = () => wrapper.findAllComponents(TagsListRow);
const findRegistryList = () => wrapper.findComponent(RegistryList);
@@ -42,20 +51,23 @@ describe('Tags List', () => {
};
const waitForApolloRequestRender = async () => {
+ fireFirstSortUpdate();
await waitForPromises();
- await nextTick();
};
- const mountComponent = ({ propsData = { isMobile: false, id: 1 } } = {}) => {
+ const mountComponent = ({ propsData = { isMobile: false, id: 1 }, mutationResolver } = {}) => {
Vue.use(VueApollo);
- const requestHandlers = [[getContainerRepositoryTagsQuery, resolver]];
+ const requestHandlers = [
+ [getContainerRepositoryTagsQuery, resolver],
+ [deleteContainerRepositoryTagsMutation, mutationResolver],
+ ];
apolloProvider = createMockApollo(requestHandlers);
wrapper = shallowMount(component, {
apolloProvider,
propsData,
- stubs: { RegistryList },
+ stubs: { RegistryList, DeleteModal },
provide() {
return {
config: defaultConfig,
@@ -66,12 +78,12 @@ describe('Tags List', () => {
beforeEach(() => {
resolver = jest.fn().mockResolvedValue(imageTagsMock());
+ jest.spyOn(Tracking, 'event');
});
describe('registry list', () => {
beforeEach(async () => {
mountComponent();
- fireFirstSortUpdate();
await waitForApolloRequestRender();
});
@@ -126,11 +138,46 @@ describe('Tags List', () => {
});
});
- it('emits a delete event when list emits delete', () => {
- const eventPayload = 'foo';
- findRegistryList().vm.$emit('delete', eventPayload);
+ describe('delete event', () => {
+ describe('single item', () => {
+ beforeEach(() => {
+ findRegistryList().vm.$emit('delete', [tags[0]]);
+ });
+
+ it('opens the modal', () => {
+ expect(DeleteModal.methods.show).toHaveBeenCalled();
+ });
+
+ it('sets modal props', () => {
+ expect(findDeleteModal().props('itemsToBeDeleted')).toMatchObject([tags[0]]);
+ });
+
+ it('tracks a single delete event', () => {
+ expect(Tracking.event).toHaveBeenCalledWith(undefined, 'click_button', {
+ label: 'registry_tag_delete',
+ });
+ });
+ });
+
+ describe('multiple items', () => {
+ beforeEach(() => {
+ findRegistryList().vm.$emit('delete', tags);
+ });
+
+ it('opens the modal', () => {
+ expect(DeleteModal.methods.show).toHaveBeenCalled();
+ });
- expect(wrapper.emitted('delete')).toEqual([[eventPayload]]);
+ it('sets modal props', () => {
+ expect(findDeleteModal().props('itemsToBeDeleted')).toMatchObject(tags);
+ });
+
+ it('tracks multiple delete event', () => {
+ expect(Tracking.event).toHaveBeenCalledWith(undefined, 'click_button', {
+ label: 'bulk_registry_tag_delete',
+ });
+ });
+ });
});
});
});
@@ -138,7 +185,6 @@ describe('Tags List', () => {
describe('list rows', () => {
it('one row exist for each tag', async () => {
mountComponent();
- fireFirstSortUpdate();
await waitForApolloRequestRender();
@@ -147,7 +193,6 @@ describe('Tags List', () => {
it('the correct props are bound to it', async () => {
mountComponent({ propsData: { disabled: true, id: 1 } });
- fireFirstSortUpdate();
await waitForApolloRequestRender();
@@ -162,7 +207,6 @@ describe('Tags List', () => {
describe('events', () => {
it('select event update the selected items', async () => {
mountComponent();
- fireFirstSortUpdate();
await waitForApolloRequestRender();
findTagsListRow().at(0).vm.$emit('select');
@@ -172,13 +216,44 @@ describe('Tags List', () => {
expect(findTagsListRow().at(0).attributes('selected')).toBe('true');
});
- it('delete event emit a delete event', async () => {
- mountComponent();
- fireFirstSortUpdate();
- await waitForApolloRequestRender();
+ describe('delete event', () => {
+ let mutationResolver;
+
+ beforeEach(async () => {
+ mutationResolver = jest.fn().mockResolvedValue(graphQLDeleteImageRepositoryTagsMock);
+ resolver = jest.fn().mockResolvedValue(imageTagsMock());
+ mountComponent({ mutationResolver });
- findTagsListRow().at(0).vm.$emit('delete');
- expect(wrapper.emitted('delete')[0][0][0].name).toBe(tags[0].name);
+ await waitForApolloRequestRender();
+ findTagsListRow().at(0).vm.$emit('delete');
+ });
+
+ it('opens the modal', () => {
+ expect(DeleteModal.methods.show).toHaveBeenCalled();
+ });
+
+ it('tracks a single delete event', () => {
+ expect(Tracking.event).toHaveBeenCalledWith(undefined, 'click_button', {
+ label: 'registry_tag_delete',
+ });
+ });
+
+ it('confirmDelete event calls apollo mutation with the right parameters and refetches the tags list query', async () => {
+ findDeleteModal().vm.$emit('confirmDelete');
+
+ expect(mutationResolver).toHaveBeenCalledWith(
+ expect.objectContaining({ tagNames: [tags[0].name] }),
+ );
+
+ await waitForPromises();
+
+ expect(resolver).toHaveBeenLastCalledWith({
+ first: GRAPHQL_PAGE_SIZE,
+ name: '',
+ sort: 'NAME_ASC',
+ id: '1',
+ });
+ });
});
});
});
@@ -187,7 +262,6 @@ describe('Tags List', () => {
it('sets registry list hiddenDelete prop to true', async () => {
resolver = jest.fn().mockResolvedValue(imageTagsMock({ canDelete: false }));
mountComponent();
- fireFirstSortUpdate();
await waitForApolloRequestRender();
expect(findRegistryList().props('hiddenDelete')).toBe(true);
@@ -198,7 +272,6 @@ describe('Tags List', () => {
beforeEach(async () => {
resolver = jest.fn().mockResolvedValue(imageTagsMock({ nodes: [] }));
mountComponent();
- fireFirstSortUpdate();
await waitForApolloRequestRender();
});
@@ -225,7 +298,7 @@ describe('Tags List', () => {
filters: [{ type: FILTERED_SEARCH_TERM, value: { data: 'foo' } }],
});
- await waitForApolloRequestRender();
+ await waitForPromises();
expect(findEmptyState().props()).toMatchObject({
svgPath: defaultConfig.noContainersImage,
@@ -236,6 +309,175 @@ describe('Tags List', () => {
});
});
+ describe('modal', () => {
+ it('exists', async () => {
+ mountComponent();
+ await waitForApolloRequestRender();
+
+ expect(findDeleteModal().exists()).toBe(true);
+ });
+
+ describe('cancel event', () => {
+ it('tracks cancel_delete', async () => {
+ mountComponent();
+ await waitForApolloRequestRender();
+
+ findDeleteModal().vm.$emit('cancel');
+
+ expect(Tracking.event).toHaveBeenCalledWith(undefined, 'cancel_delete', {
+ label: 'registry_tag_delete',
+ });
+ });
+ });
+
+ describe('confirmDelete event', () => {
+ let mutationResolver;
+
+ describe('when mutation', () => {
+ beforeEach(() => {
+ mutationResolver = jest.fn().mockResolvedValue(graphQLDeleteImageRepositoryTagsMock);
+ mountComponent({ mutationResolver });
+
+ return waitForApolloRequestRender();
+ });
+
+ it('is started renders loader', async () => {
+ findRegistryList().vm.$emit('delete', [tags[0]]);
+
+ findDeleteModal().vm.$emit('confirmDelete');
+ await nextTick();
+
+ expect(findTagsLoader().exists()).toBe(true);
+ expect(findTagsListRow().exists()).toBe(false);
+ });
+
+ it('ends, loader is hidden', async () => {
+ findRegistryList().vm.$emit('delete', [tags[0]]);
+
+ findDeleteModal().vm.$emit('confirmDelete');
+ await waitForPromises();
+
+ expect(findTagsLoader().exists()).toBe(false);
+ expect(findTagsListRow().exists()).toBe(true);
+ });
+ });
+
+ describe.each([
+ {
+ description: 'rejection',
+ mutationMock: jest.fn().mockRejectedValue(),
+ },
+ {
+ description: 'error',
+ mutationMock: jest.fn().mockResolvedValue({
+ data: {
+ destroyContainerRepositoryTags: {
+ errors: [new Error()],
+ },
+ },
+ }),
+ },
+ ])('when mutation fails with $description', ({ mutationMock }) => {
+ beforeEach(() => {
+ mutationResolver = mutationMock;
+ mountComponent({ mutationResolver });
+
+ return waitForApolloRequestRender();
+ });
+
+ it('when one item is selected to be deleted calls apollo mutation with the right parameters and emits delete event with right arguments', async () => {
+ findRegistryList().vm.$emit('delete', [tags[0]]);
+
+ resolver.mockClear();
+
+ findDeleteModal().vm.$emit('confirmDelete');
+
+ expect(mutationResolver).toHaveBeenCalledWith(
+ expect.objectContaining({ tagNames: [tags[0].name] }),
+ );
+
+ expect(resolver).not.toHaveBeenCalled();
+
+ await waitForPromises();
+
+ expect(wrapper.emitted('delete')).toHaveLength(1);
+ expect(wrapper.emitted('delete')[0][0]).toEqual('danger_tag');
+ });
+
+ it('when more than one item is selected to be deleted calls apollo mutation with the right parameters and emits delete event with right arguments', async () => {
+ findRegistryList().vm.$emit('delete', tagsMock);
+ resolver.mockClear();
+
+ findDeleteModal().vm.$emit('confirmDelete');
+
+ expect(mutationResolver).toHaveBeenCalledWith(
+ expect.objectContaining({ tagNames: tagsMock.map((t) => t.name) }),
+ );
+
+ expect(resolver).not.toHaveBeenCalled();
+
+ await waitForPromises();
+
+ expect(wrapper.emitted('delete')).toHaveLength(1);
+ expect(wrapper.emitted('delete')[0][0]).toEqual('danger_tags');
+ });
+ });
+
+ describe('when mutation is successful', () => {
+ beforeEach(() => {
+ mutationResolver = jest.fn().mockResolvedValue(graphQLDeleteImageRepositoryTagsMock);
+ mountComponent({ mutationResolver });
+
+ return waitForApolloRequestRender();
+ });
+
+ it('and one item is selected to be deleted calls apollo mutation with the right parameters and refetches the tags list query', async () => {
+ findRegistryList().vm.$emit('delete', [tags[0]]);
+
+ findDeleteModal().vm.$emit('confirmDelete');
+
+ expect(mutationResolver).toHaveBeenCalledWith(
+ expect.objectContaining({ tagNames: [tags[0].name] }),
+ );
+
+ expect(resolver).toHaveBeenLastCalledWith({
+ first: GRAPHQL_PAGE_SIZE,
+ name: '',
+ sort: 'NAME_ASC',
+ id: '1',
+ });
+
+ await waitForPromises();
+
+ expect(wrapper.emitted('delete')).toHaveLength(1);
+ expect(wrapper.emitted('delete')[0][0]).toEqual('success_tag');
+ });
+
+ it('and more than one item is selected to be deleted calls apollo mutation with the right parameters and refetches the tags list query', async () => {
+ findRegistryList().vm.$emit('delete', tagsMock);
+
+ findDeleteModal().vm.$emit('confirmDelete');
+
+ expect(mutationResolver).toHaveBeenCalledWith(
+ expect.objectContaining({ tagNames: tagsMock.map((t) => t.name) }),
+ );
+
+ expect(resolver).toHaveBeenLastCalledWith({
+ first: GRAPHQL_PAGE_SIZE,
+ name: '',
+ sort: 'NAME_ASC',
+ id: '1',
+ });
+
+ await waitForPromises();
+
+ expect(wrapper.emitted('delete')).toHaveLength(1);
+ expect(wrapper.emitted('delete')[0][0]).toEqual('success_tags');
+ });
+ });
+ });
+ });
+
describe('loading state', () => {
it.each`
isImageLoading | queryExecuting | loadingVisible
@@ -247,7 +489,6 @@ describe('Tags List', () => {
'when the isImageLoading is $isImageLoading, and is $queryExecuting that the query is still executing is $loadingVisible that the loader is shown',
async ({ isImageLoading, queryExecuting, loadingVisible }) => {
mountComponent({ propsData: { isImageLoading, isMobile: false, id: 1 } });
- fireFirstSortUpdate();
if (!queryExecuting) {
await waitForApolloRequestRender();
}
diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/registry_header_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/registry_header_spec.js
index 45304cc2329..b7f3698e155 100644
--- a/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/registry_header_spec.js
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/registry_header_spec.js
@@ -81,7 +81,7 @@ describe('registry_header', () => {
});
});
- describe('expiration policy', () => {
+ describe('cleanup policy', () => {
it('when is disabled', async () => {
await mountComponent({
expirationPolicy: { enabled: false },
@@ -111,11 +111,11 @@ describe('registry_header', () => {
const cleanupLink = findSetupCleanUpLink();
expect(text.exists()).toBe(true);
- expect(text.props('text')).toBe('Expiration policy will run in ');
+ expect(text.props('text')).toBe('Cleanup will run in ');
expect(cleanupLink.exists()).toBe(true);
expect(cleanupLink.text()).toBe(SET_UP_CLEANUP);
});
- it('when the expiration policy is completely disabled', async () => {
+ it('when the cleanup policy is not scheduled', async () => {
await mountComponent({
expirationPolicy: { enabled: true },
expirationPolicyHelpPagePath: 'foo',
diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/mock_data.js b/spec/frontend/packages_and_registries/container_registry/explorer/mock_data.js
index cd54b856c97..8ca74f5077e 100644
--- a/spec/frontend/packages_and_registries/container_registry/explorer/mock_data.js
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/mock_data.js
@@ -127,7 +127,6 @@ export const containerRepositoryMock = {
location: 'host.docker.internal:5000/gitlab-org/gitlab-test/rails-12009',
canDelete: true,
createdAt: '2020-11-03T13:29:21Z',
- updatedAt: '2020-11-03T13:29:21Z',
expirationPolicyStartedAt: null,
expirationPolicyCleanupStatus: 'UNSCHEDULED',
project: {
diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/pages/details_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/pages/details_spec.js
index 888c3e5bffa..7fed81acead 100644
--- a/spec/frontend/packages_and_registries/container_registry/explorer/pages/details_spec.js
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/pages/details_spec.js
@@ -22,22 +22,15 @@ import {
MISSING_OR_DELETED_IMAGE_TITLE,
MISSING_OR_DELETED_IMAGE_MESSAGE,
} from '~/packages_and_registries/container_registry/explorer/constants';
-import deleteContainerRepositoryTagsMutation from '~/packages_and_registries/container_registry/explorer/graphql/mutations/delete_container_repository_tags.mutation.graphql';
import getContainerRepositoryDetailsQuery from '~/packages_and_registries/container_registry/explorer/graphql/queries/get_container_repository_details.query.graphql';
-import getContainerRepositoryTagsQuery from '~/packages_and_registries/container_registry/explorer/graphql/queries/get_container_repository_tags.query.graphql';
-import getContainerRepositoriesDetails from '~/packages_and_registries/container_registry/explorer/graphql/queries/get_container_repositories_details.query.graphql';
import component from '~/packages_and_registries/container_registry/explorer/pages/details.vue';
import Tracking from '~/tracking';
import {
graphQLImageDetailsMock,
- graphQLDeleteImageRepositoryTagsMock,
- graphQLProjectImageRepositoriesDetailsMock,
containerRepositoryMock,
graphQLEmptyImageDetailsMock,
- tagsMock,
- imageTagsMock,
} from '../mock_data';
import { DeleteModal } from '../stubs';
@@ -69,13 +62,6 @@ describe('Details Page', () => {
isGroupPage: false,
};
- const cleanTags = tagsMock.map((t) => {
- const result = { ...t };
- // eslint-disable-next-line no-underscore-dangle
- delete result.__typename;
- return result;
- });
-
const waitForApolloRequestRender = async () => {
await waitForPromises();
await nextTick();
@@ -83,20 +69,12 @@ describe('Details Page', () => {
const mountComponent = ({
resolver = jest.fn().mockResolvedValue(graphQLImageDetailsMock()),
- mutationResolver = jest.fn().mockResolvedValue(graphQLDeleteImageRepositoryTagsMock),
- tagsResolver = jest.fn().mockResolvedValue(graphQLImageDetailsMock(imageTagsMock())),
- detailsResolver = jest.fn().mockResolvedValue(graphQLProjectImageRepositoriesDetailsMock),
options,
config = defaultConfig,
} = {}) => {
Vue.use(VueApollo);
- const requestHandlers = [
- [getContainerRepositoryDetailsQuery, resolver],
- [deleteContainerRepositoryTagsMutation, mutationResolver],
- [getContainerRepositoryTagsQuery, tagsResolver],
- [getContainerRepositoriesDetails, detailsResolver],
- ];
+ const requestHandlers = [[getContainerRepositoryDetailsQuery, resolver]];
apolloProvider = createMockApollo(requestHandlers);
@@ -184,50 +162,6 @@ describe('Details Page', () => {
isMobile: false,
});
});
-
- describe('deleteEvent', () => {
- describe('single item', () => {
- let tagToBeDeleted;
- beforeEach(async () => {
- mountComponent();
-
- await waitForApolloRequestRender();
-
- [tagToBeDeleted] = cleanTags;
- findTagsList().vm.$emit('delete', [tagToBeDeleted]);
- });
-
- it('open the modal', async () => {
- expect(DeleteModal.methods.show).toHaveBeenCalled();
- });
-
- it('tracks a single delete event', () => {
- expect(Tracking.event).toHaveBeenCalledWith(undefined, 'click_button', {
- label: 'registry_tag_delete',
- });
- });
- });
-
- describe('multiple items', () => {
- beforeEach(async () => {
- mountComponent();
-
- await waitForApolloRequestRender();
-
- findTagsList().vm.$emit('delete', cleanTags);
- });
-
- it('open the modal', () => {
- expect(DeleteModal.methods.show).toHaveBeenCalled();
- });
-
- it('tracks a single delete event', () => {
- expect(Tracking.event).toHaveBeenCalledWith(undefined, 'click_button', {
- label: 'bulk_registry_tag_delete',
- });
- });
- });
- });
});
describe('modal', () => {
@@ -248,61 +182,24 @@ describe('Details Page', () => {
findDeleteModal().vm.$emit('cancel');
expect(Tracking.event).toHaveBeenCalledWith(undefined, 'cancel_delete', {
- label: 'registry_tag_delete',
+ label: 'registry_image_delete',
});
});
});
- describe('confirmDelete event', () => {
- let mutationResolver;
- let tagsResolver;
- let detailsResolver;
-
+ describe('tags list delete event', () => {
beforeEach(() => {
- mutationResolver = jest.fn().mockResolvedValue(graphQLDeleteImageRepositoryTagsMock);
- tagsResolver = jest.fn().mockResolvedValue(graphQLImageDetailsMock(imageTagsMock()));
- detailsResolver = jest.fn().mockResolvedValue(graphQLProjectImageRepositoriesDetailsMock);
- mountComponent({ mutationResolver, tagsResolver, detailsResolver });
+ mountComponent();
return waitForApolloRequestRender();
});
- describe('when one item is selected to be deleted', () => {
- it('calls apollo mutation with the right parameters and refetches the tags list query', async () => {
- findTagsList().vm.$emit('delete', [cleanTags[0]]);
-
- await nextTick();
-
- findDeleteModal().vm.$emit('confirmDelete');
-
- expect(mutationResolver).toHaveBeenCalledWith(
- expect.objectContaining({ tagNames: [cleanTags[0].name] }),
- );
-
- await waitForPromises();
-
- expect(tagsResolver).toHaveBeenCalled();
- expect(detailsResolver).toHaveBeenCalled();
- });
- });
-
- describe('when more than one item is selected to be deleted', () => {
- it('calls apollo mutation with the right parameters and refetches the tags list query', async () => {
- findTagsList().vm.$emit('delete', tagsMock);
-
- await nextTick();
+ it('sets delete alert modal deleteAlertType value', async () => {
+ findTagsList().vm.$emit('delete', 'success_tag');
- findDeleteModal().vm.$emit('confirmDelete');
-
- expect(mutationResolver).toHaveBeenCalledWith(
- expect.objectContaining({ tagNames: tagsMock.map((t) => t.name) }),
- );
-
- await waitForPromises();
+ await nextTick();
- expect(tagsResolver).toHaveBeenCalled();
- expect(detailsResolver).toHaveBeenCalled();
- });
+ expect(findDeleteAlert().props('deleteAlertType')).toBe('success_tag');
});
});
});
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 c2ae34ce697..2e7195aa59b 100644
--- a/spec/frontend/packages_and_registries/dependency_proxy/app_spec.js
+++ b/spec/frontend/packages_and_registries/dependency_proxy/app_spec.js
@@ -5,7 +5,6 @@ import {
GlFormInputGroup,
GlFormGroup,
GlModal,
- GlSkeletonLoader,
GlSprintf,
GlEmptyState,
} from '@gitlab/ui';
@@ -72,8 +71,6 @@ describe('DependencyProxyApp', () => {
const findClipBoardButton = () => wrapper.findComponent(ClipboardButton);
const findFormGroup = () => wrapper.findComponent(GlFormGroup);
const findFormInputGroup = () => wrapper.findComponent(GlFormInputGroup);
- const findSkeletonLoader = () => wrapper.findComponent(GlSkeletonLoader);
- const findMainArea = () => wrapper.findByTestId('main-area');
const findProxyCountText = () => wrapper.findByTestId('proxy-count');
const findManifestList = () => wrapper.findComponent(ManifestsList);
const findEmptyState = () => wrapper.findComponent(GlEmptyState);
@@ -99,23 +96,11 @@ describe('DependencyProxyApp', () => {
describe('when the dependency proxy is available', () => {
describe('when is loading', () => {
- it('renders the skeleton loader', () => {
- createComponent();
-
- expect(findSkeletonLoader().exists()).toBe(true);
- });
-
it('does not render a form group with label', () => {
createComponent();
expect(findFormGroup().exists()).toBe(false);
});
-
- it('does not show the main section', () => {
- createComponent();
-
- expect(findMainArea().exists()).toBe(false);
- });
});
describe('when the app is loaded', () => {
@@ -125,10 +110,6 @@ describe('DependencyProxyApp', () => {
return waitForPromises();
});
- it('renders the main area', () => {
- expect(findMainArea().exists()).toBe(true);
- });
-
it('renders a form group with a label', () => {
expect(findFormGroup().attributes('label')).toBe(
DependencyProxyApp.i18n.proxyImagePrefix,
@@ -213,13 +194,6 @@ describe('DependencyProxyApp', () => {
});
describe('triggering page event on list', () => {
- it('re-renders the skeleton loader', async () => {
- findManifestList().vm.$emit('next-page');
- await nextTick();
-
- expect(findSkeletonLoader().exists()).toBe(true);
- });
-
it('renders form group with label', async () => {
findManifestList().vm.$emit('next-page');
await nextTick();
@@ -228,13 +202,6 @@ describe('DependencyProxyApp', () => {
expect.stringMatching(DependencyProxyApp.i18n.proxyImagePrefix),
);
});
-
- it('does not show the main section', async () => {
- findManifestList().vm.$emit('next-page');
- await nextTick();
-
- expect(findMainArea().exists()).toBe(false);
- });
});
it('shows the clear cache dropdown list', () => {
diff --git a/spec/frontend/packages_and_registries/dependency_proxy/components/manifest_list_spec.js b/spec/frontend/packages_and_registries/dependency_proxy/components/manifest_list_spec.js
index 639a4fbb99d..0d8af42bae3 100644
--- a/spec/frontend/packages_and_registries/dependency_proxy/components/manifest_list_spec.js
+++ b/spec/frontend/packages_and_registries/dependency_proxy/components/manifest_list_spec.js
@@ -1,7 +1,6 @@
-import { GlKeysetPagination } from '@gitlab/ui';
+import { GlKeysetPagination, GlSkeletonLoader } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import ManifestRow from '~/packages_and_registries/dependency_proxy/components/manifest_row.vue';
-
import Component from '~/packages_and_registries/dependency_proxy/components/manifests_list.vue';
import {
proxyManifests,
@@ -14,6 +13,7 @@ describe('Manifests List', () => {
const defaultProps = {
manifests: proxyManifests(),
pagination: pagination(),
+ loading: false,
};
const createComponent = (propsData = defaultProps) => {
@@ -24,6 +24,8 @@ describe('Manifests List', () => {
const findRows = () => wrapper.findAllComponents(ManifestRow);
const findPagination = () => wrapper.findComponent(GlKeysetPagination);
+ const findMainArea = () => wrapper.findByTestId('main-area');
+ const findSkeletonLoader = () => wrapper.findComponent(GlSkeletonLoader);
it('has the correct title', () => {
createComponent();
@@ -45,6 +47,19 @@ describe('Manifests List', () => {
});
});
+ describe('loading', () => {
+ it.each`
+ loading | expectLoader | expectContent
+ ${false} | ${false} | ${true}
+ ${true} | ${true} | ${false}
+ `('when loading is $loading', ({ loading, expectLoader, expectContent }) => {
+ createComponent({ ...defaultProps, loading });
+
+ expect(findSkeletonLoader().exists()).toBe(expectLoader);
+ expect(findMainArea().exists()).toBe(expectContent);
+ });
+ });
+
describe('pagination', () => {
it('is hidden when there is no next or prev pages', () => {
createComponent({ ...defaultProps, pagination: {} });
diff --git a/spec/frontend/packages_and_registries/harbor_registry/pages/list_spec.js b/spec/frontend/packages_and_registries/harbor_registry/pages/list_spec.js
index 63ea8feb1e7..1bc2657822e 100644
--- a/spec/frontend/packages_and_registries/harbor_registry/pages/list_spec.js
+++ b/spec/frontend/packages_and_registries/harbor_registry/pages/list_spec.js
@@ -74,7 +74,7 @@ describe('Harbor List Page', () => {
});
describe('isLoading is true', () => {
- it('shows the skeleton loader', async () => {
+ it('shows the skeleton loader', () => {
mountComponent();
fireFirstSortUpdate();
@@ -93,7 +93,7 @@ describe('Harbor List Page', () => {
expect(findCliCommands().exists()).toBe(false);
});
- it('title has the metadataLoading props set to true', async () => {
+ it('title has the metadataLoading props set to true', () => {
mountComponent();
fireFirstSortUpdate();
diff --git a/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/infrastructure_title_spec.js b/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/infrastructure_title_spec.js
index 7c7faa8a3b0..12859b1d77c 100644
--- a/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/infrastructure_title_spec.js
+++ b/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/infrastructure_title_spec.js
@@ -32,7 +32,7 @@ describe('Infrastructure Title', () => {
});
it('has the correct title', () => {
- expect(findTitleArea().props('title')).toBe('Infrastructure Registry');
+ expect(findTitleArea().props('title')).toBe('Terraform Module Registry');
});
describe('with no modules', () => {
diff --git a/spec/frontend/packages_and_registries/package_registry/components/delete_modal_spec.js b/spec/frontend/packages_and_registries/package_registry/components/delete_modal_spec.js
index d0817a8678e..b0fc9ef0f0c 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/delete_modal_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/delete_modal_spec.js
@@ -1,7 +1,14 @@
-import { GlModal as RealGlModal } from '@gitlab/ui';
+import { GlModal as RealGlModal, GlSprintf } from '@gitlab/ui';
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 {
+ DELETE_PACKAGE_MODAL_PRIMARY_ACTION,
+ DELETE_PACKAGE_REQUEST_FORWARDING_MODAL_CONTENT,
+ DELETE_PACKAGE_WITH_REQUEST_FORWARDING_PRIMARY_ACTION,
+ DELETE_PACKAGES_REQUEST_FORWARDING_MODAL_CONTENT,
+ DELETE_PACKAGES_WITH_REQUEST_FORWARDING_PRIMARY_ACTION,
+} from '~/packages_and_registries/package_registry/constants';
const GlModal = stubComponent(RealGlModal, {
methods: {
@@ -15,21 +22,28 @@ describe('DeleteModal', () => {
const defaultItemsToBeDeleted = [
{
name: 'package 01',
+ version: '1.0.0',
},
{
name: 'package 02',
+ version: '1.0.0',
},
];
const findModal = () => wrapper.findComponent(GlModal);
- const mountComponent = ({ itemsToBeDeleted = defaultItemsToBeDeleted } = {}) => {
+ const mountComponent = ({
+ itemsToBeDeleted = defaultItemsToBeDeleted,
+ showRequestForwardingContent = false,
+ } = {}) => {
wrapper = shallowMountExtended(DeleteModal, {
propsData: {
itemsToBeDeleted,
+ showRequestForwardingContent,
},
stubs: {
GlModal,
+ GlSprintf,
},
});
};
@@ -50,11 +64,64 @@ describe('DeleteModal', () => {
});
it('renders description', () => {
- expect(findModal().text()).toContain(
+ expect(findModal().text()).toMatchInterpolatedText(
'You are about to delete 2 packages. This operation is irreversible.',
);
});
+ it('with only one item to be deleted renders correct description', () => {
+ mountComponent({ itemsToBeDeleted: [defaultItemsToBeDeleted[0]] });
+
+ expect(findModal().text()).toMatchInterpolatedText(
+ 'You are about to delete version 1.0.0 of package 01. Are you sure?',
+ );
+ });
+
+ it('sets the right action primary text', () => {
+ expect(findModal().props('actionPrimary')).toMatchObject({
+ text: DELETE_PACKAGE_MODAL_PRIMARY_ACTION,
+ });
+ });
+
+ describe('when showRequestForwardingContent is set', () => {
+ it('renders correct description', () => {
+ mountComponent({ showRequestForwardingContent: true });
+
+ expect(findModal().text()).toMatchInterpolatedText(
+ DELETE_PACKAGES_REQUEST_FORWARDING_MODAL_CONTENT,
+ );
+ });
+
+ it('sets the right action primary text', () => {
+ mountComponent({ showRequestForwardingContent: true });
+
+ expect(findModal().props('actionPrimary')).toMatchObject({
+ text: DELETE_PACKAGES_WITH_REQUEST_FORWARDING_PRIMARY_ACTION,
+ });
+ });
+
+ describe('and only one item to be deleted', () => {
+ beforeEach(() => {
+ mountComponent({
+ showRequestForwardingContent: true,
+ itemsToBeDeleted: [defaultItemsToBeDeleted[0]],
+ });
+ });
+
+ it('renders correct description', () => {
+ expect(findModal().text()).toMatchInterpolatedText(
+ DELETE_PACKAGE_REQUEST_FORWARDING_MODAL_CONTENT,
+ );
+ });
+
+ it('sets the right action primary text', () => {
+ expect(findModal().props('actionPrimary')).toMatchObject({
+ text: DELETE_PACKAGE_WITH_REQUEST_FORWARDING_PRIMARY_ACTION,
+ });
+ });
+ });
+ });
+
it('emits confirm when primary event is emitted', () => {
expect(wrapper.emitted('confirm')).toBeUndefined();
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 fc7f5c80d45..a700f42d367 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,5 +1,11 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import { GlAlert } from '@gitlab/ui';
+import * as Sentry from '@sentry/browser';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import createMockApollo from 'helpers/mock_apollo_helper';
import { stubComponent } from 'helpers/stub_component';
+import waitForPromises from 'helpers/wait_for_promises';
import DeleteModal from '~/packages_and_registries/package_registry/components/delete_modal.vue';
import DeletePackageModal from '~/packages_and_registries/shared/components/delete_package_modal.vue';
import PackageVersionsList from '~/packages_and_registries/package_registry/components/details/package_versions_list.vue';
@@ -14,24 +20,26 @@ import {
DELETE_PACKAGE_VERSIONS_TRACKING_ACTION,
REQUEST_DELETE_PACKAGE_VERSION_TRACKING_ACTION,
REQUEST_DELETE_PACKAGE_VERSIONS_TRACKING_ACTION,
+ GRAPHQL_PAGE_SIZE,
} from '~/packages_and_registries/package_registry/constants';
-import { packageData } from '../../mock_data';
+import getPackageVersionsQuery from '~/packages_and_registries/package_registry/graphql//queries/get_package_versions.query.graphql';
+import {
+ emptyPackageVersionsQuery,
+ packageVersionsQuery,
+ packageVersions,
+ pagination,
+} from '../../mock_data';
+
+Vue.use(VueApollo);
describe('PackageVersionsList', () => {
let wrapper;
+ let apolloProvider;
const EmptySlotStub = { name: 'empty-slot-stub', template: '<div>empty message</div>' };
- const packageList = [
- packageData({
- name: 'version 1',
- }),
- packageData({
- id: 'gid://gitlab/Packages::Package/112',
- name: 'version 2',
- }),
- ];
const uiElements = {
+ findAlert: () => wrapper.findComponent(GlAlert),
findLoader: () => wrapper.findComponent(PackagesListLoader),
findRegistryList: () => wrapper.findComponent(RegistryList),
findEmptySlot: () => wrapper.findComponent(EmptySlotStub),
@@ -40,12 +48,20 @@ describe('PackageVersionsList', () => {
findDeletePackagesModal: () => wrapper.findComponent(DeleteModal),
findPackageListDeleteModal: () => wrapper.findComponent(DeletePackageModal),
};
- const mountComponent = (props) => {
+
+ const mountComponent = ({
+ props = {},
+ resolver = jest.fn().mockResolvedValue(packageVersionsQuery()),
+ } = {}) => {
+ const requestHandlers = [[getPackageVersionsQuery, resolver]];
+ apolloProvider = createMockApollo(requestHandlers);
+
wrapper = shallowMountExtended(PackageVersionsList, {
+ apolloProvider,
propsData: {
- versions: packageList,
- pageInfo: {},
- isLoading: false,
+ packageId: packageVersionsQuery().data.package.id,
+ isMutationLoading: false,
+ count: packageVersions().length,
...props,
},
stubs: {
@@ -62,9 +78,13 @@ describe('PackageVersionsList', () => {
});
};
+ beforeEach(() => {
+ jest.spyOn(Sentry, 'captureException').mockImplementation();
+ });
+
describe('when list is loading', () => {
beforeEach(() => {
- mountComponent({ isLoading: true, versions: [] });
+ mountComponent({ props: { isMutationLoading: true } });
});
it('displays loader', () => {
expect(uiElements.findLoader().exists()).toBe(true);
@@ -81,11 +101,24 @@ describe('PackageVersionsList', () => {
it('does not display registry list', () => {
expect(uiElements.findRegistryList().exists()).toBe(false);
});
+
+ it('does not display alert', () => {
+ expect(uiElements.findAlert().exists()).toBe(false);
+ });
});
describe('when list is loaded and has no data', () => {
- beforeEach(() => {
- mountComponent({ isLoading: false, versions: [] });
+ const resolver = jest.fn().mockResolvedValue(emptyPackageVersionsQuery);
+ beforeEach(async () => {
+ mountComponent({
+ props: { isMutationLoading: false, count: 0 },
+ resolver,
+ });
+ await waitForPromises();
+ });
+
+ it('skips graphql query', () => {
+ expect(resolver).not.toHaveBeenCalled();
});
it('displays empty slot message', () => {
@@ -103,11 +136,44 @@ describe('PackageVersionsList', () => {
it('does not display registry list', () => {
expect(uiElements.findRegistryList().exists()).toBe(false);
});
+
+ it('does not display alert', () => {
+ expect(uiElements.findAlert().exists()).toBe(false);
+ });
+ });
+
+ describe('if load fails, alert', () => {
+ beforeEach(async () => {
+ mountComponent({ resolver: jest.fn().mockRejectedValue() });
+
+ await waitForPromises();
+ });
+
+ it('is displayed', () => {
+ expect(uiElements.findAlert().exists()).toBe(true);
+ });
+
+ it('shows error message', () => {
+ expect(uiElements.findAlert().text()).toMatchInterpolatedText('Failed to load version data');
+ });
+
+ it('is not dismissible', () => {
+ expect(uiElements.findAlert().props('dismissible')).toBe(false);
+ });
+
+ it('is of variant danger', () => {
+ expect(uiElements.findAlert().attributes('variant')).toBe('danger');
+ });
+
+ it('error is logged in sentry', () => {
+ expect(Sentry.captureException).toHaveBeenCalled();
+ });
});
describe('when list is loaded with data', () => {
- beforeEach(() => {
+ beforeEach(async () => {
mountComponent();
+ await waitForPromises();
});
it('displays package registry list', () => {
@@ -116,7 +182,7 @@ describe('PackageVersionsList', () => {
it('binds the right props', () => {
expect(uiElements.findRegistryList().props()).toMatchObject({
- items: packageList,
+ items: packageVersions(),
pagination: {},
isLoading: false,
hiddenDelete: true,
@@ -125,16 +191,16 @@ describe('PackageVersionsList', () => {
it('displays package version rows', () => {
expect(uiElements.findAllListRow().exists()).toEqual(true);
- expect(uiElements.findAllListRow()).toHaveLength(packageList.length);
+ expect(uiElements.findAllListRow()).toHaveLength(packageVersions().length);
});
it('binds the correct props', () => {
expect(uiElements.findAllListRow().at(0).props()).toMatchObject({
- packageEntity: expect.objectContaining(packageList[0]),
+ packageEntity: expect.objectContaining(packageVersions()[0]),
});
expect(uiElements.findAllListRow().at(1).props()).toMatchObject({
- packageEntity: expect.objectContaining(packageList[1]),
+ packageEntity: expect.objectContaining(packageVersions()[1]),
});
});
@@ -148,40 +214,52 @@ describe('PackageVersionsList', () => {
});
describe('when user interacts with pagination', () => {
- beforeEach(() => {
- mountComponent({ pageInfo: { hasNextPage: true } });
+ const resolver = jest.fn().mockResolvedValue(packageVersionsQuery());
+
+ beforeEach(async () => {
+ mountComponent({ resolver });
+ await waitForPromises();
});
- it('emits prev-page event when registry list emits prev event', () => {
- uiElements.findRegistryList().vm.$emit('prev-page');
+ it('when list emits next-page fetches the next set of records', async () => {
+ uiElements.findRegistryList().vm.$emit('next-page');
+ await waitForPromises();
- expect(wrapper.emitted('prev-page')).toHaveLength(1);
+ expect(resolver).toHaveBeenLastCalledWith(
+ expect.objectContaining({ after: pagination().endCursor, first: GRAPHQL_PAGE_SIZE }),
+ );
});
- it('emits next-page when registry list emits next event', () => {
- uiElements.findRegistryList().vm.$emit('next-page');
+ it('when list emits prev-page fetches the prev set of records', async () => {
+ uiElements.findRegistryList().vm.$emit('prev-page');
+ await waitForPromises();
- expect(wrapper.emitted('next-page')).toHaveLength(1);
+ expect(resolver).toHaveBeenLastCalledWith(
+ expect.objectContaining({ before: pagination().startCursor, last: GRAPHQL_PAGE_SIZE }),
+ );
});
});
describe.each`
description | finderFunction | deletePayload
- ${'when the user can destroy the package'} | ${uiElements.findListRow} | ${packageList[0]}
- ${'when the user can bulk destroy packages and deletes only one package'} | ${uiElements.findRegistryList} | ${[packageList[0]]}
+ ${'when the user can destroy the package'} | ${uiElements.findListRow} | ${packageVersions()[0]}
+ ${'when the user can bulk destroy packages and deletes only one package'} | ${uiElements.findRegistryList} | ${[packageVersions()[0]]}
`('$description', ({ finderFunction, deletePayload }) => {
let eventSpy;
const category = 'UI::NpmPackages';
const { findPackageListDeleteModal } = uiElements;
- beforeEach(() => {
+ beforeEach(async () => {
eventSpy = jest.spyOn(Tracking, 'event');
- mountComponent({ canDestroy: true });
+ mountComponent({ props: { canDestroy: true } });
+ await waitForPromises();
finderFunction().vm.$emit('delete', deletePayload);
});
it('passes itemToBeDeleted to the modal', () => {
- expect(findPackageListDeleteModal().props('itemToBeDeleted')).toStrictEqual(packageList[0]);
+ expect(findPackageListDeleteModal().props('itemToBeDeleted')).toStrictEqual(
+ packageVersions()[0],
+ );
});
it('requesting delete tracks the right action', () => {
@@ -198,7 +276,7 @@ describe('PackageVersionsList', () => {
});
it('emits delete when modal confirms', () => {
- expect(wrapper.emitted('delete')[0][0]).toEqual([packageList[0]]);
+ expect(wrapper.emitted('delete')[0][0]).toEqual([packageVersions()[0]]);
});
it('tracks the right action', () => {
@@ -231,14 +309,15 @@ describe('PackageVersionsList', () => {
let eventSpy;
const { findDeletePackagesModal, findRegistryList } = uiElements;
- beforeEach(() => {
+ beforeEach(async () => {
eventSpy = jest.spyOn(Tracking, 'event');
- mountComponent({ canDestroy: true });
+ mountComponent({ props: { canDestroy: true } });
+ await waitForPromises();
});
it('binds the right props', () => {
expect(uiElements.findRegistryList().props()).toMatchObject({
- items: packageList,
+ items: packageVersions(),
pagination: {},
isLoading: false,
hiddenDelete: false,
@@ -248,11 +327,13 @@ describe('PackageVersionsList', () => {
describe('upon deletion', () => {
beforeEach(() => {
- findRegistryList().vm.$emit('delete', packageList);
+ findRegistryList().vm.$emit('delete', packageVersions());
});
it('passes itemsToBeDeleted to the modal', () => {
- expect(findDeletePackagesModal().props('itemsToBeDeleted')).toStrictEqual(packageList);
+ expect(findDeletePackagesModal().props('itemsToBeDeleted')).toStrictEqual(
+ packageVersions(),
+ );
expect(wrapper.emitted('delete')).toBeUndefined();
});
@@ -270,7 +351,7 @@ describe('PackageVersionsList', () => {
});
it('emits delete event', () => {
- expect(wrapper.emitted('delete')[0]).toEqual([packageList]);
+ expect(wrapper.emitted('delete')[0]).toEqual([packageVersions()]);
});
it('tracks the right action', () => {
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 91417d2fc9f..52d222ed07b 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
@@ -132,7 +132,7 @@ describe('packages_list_row', () => {
});
});
- it('emits the delete event when the delete button is clicked', async () => {
+ it('emits the delete event when the delete button is clicked', () => {
mountComponent({ packageEntity: packageWithoutTags });
findDeleteDropdown().vm.$emit('click');
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 ae990f3ea00..483b7a9383d 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
@@ -4,7 +4,6 @@ import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { stubComponent } from 'helpers/stub_component';
import PackagesListRow from '~/packages_and_registries/package_registry/components/list/package_list_row.vue';
import PackagesListLoader from '~/packages_and_registries/shared/components/packages_list_loader.vue';
-import DeletePackageModal from '~/packages_and_registries/shared/components/delete_package_modal.vue';
import DeleteModal from '~/packages_and_registries/package_registry/components/delete_modal.vue';
import RegistryList from '~/packages_and_registries/shared/components/registry_list.vue';
import {
@@ -17,7 +16,7 @@ import {
} from '~/packages_and_registries/package_registry/constants';
import PackagesList from '~/packages_and_registries/package_registry/components/list/packages_list.vue';
import Tracking from '~/tracking';
-import { packageData } from '../../mock_data';
+import { defaultPackageGroupSettings, packageData } from '../../mock_data';
describe('packages_list', () => {
let wrapper;
@@ -39,18 +38,20 @@ describe('packages_list', () => {
list: [firstPackage, secondPackage],
isLoading: false,
pageInfo: {},
+ groupSettings: defaultPackageGroupSettings,
};
const EmptySlotStub = { name: 'empty-slot-stub', template: '<div>bar</div>' };
const findPackagesListLoader = () => wrapper.findComponent(PackagesListLoader);
- const findPackageListDeleteModal = () => wrapper.findComponent(DeletePackageModal);
const findEmptySlot = () => wrapper.findComponent(EmptySlotStub);
const findRegistryList = () => wrapper.findComponent(RegistryList);
const findPackagesListRow = () => wrapper.findComponent(PackagesListRow);
const findErrorPackageAlert = () => wrapper.findComponent(GlAlert);
const findDeletePackagesModal = () => wrapper.findComponent(DeleteModal);
+ const showMock = jest.fn();
+
const mountComponent = (props) => {
wrapper = shallowMountExtended(PackagesList, {
propsData: {
@@ -58,10 +59,9 @@ describe('packages_list', () => {
...props,
},
stubs: {
- DeletePackageModal,
DeleteModal: stubComponent(DeleteModal, {
methods: {
- show: jest.fn(),
+ show: showMock,
},
}),
GlSprintf,
@@ -119,15 +119,20 @@ describe('packages_list', () => {
});
describe('layout', () => {
- it("doesn't contain a visible modal component", () => {
+ beforeEach(() => {
mountComponent();
+ });
- expect(findPackageListDeleteModal().props('itemToBeDeleted')).toBeNull();
+ it('modal component is not shown', () => {
+ expect(showMock).not.toHaveBeenCalled();
});
- it('does not have an error alert displayed', () => {
- mountComponent();
+ it('modal component props is empty', () => {
+ expect(findDeletePackagesModal().props('itemsToBeDeleted')).toEqual([]);
+ expect(findDeletePackagesModal().props('showRequestForwardingContent')).toBe(false);
+ });
+ it('does not have an error alert displayed', () => {
expect(findErrorPackageAlert().exists()).toBe(false);
});
});
@@ -146,8 +151,8 @@ describe('packages_list', () => {
finderFunction().vm.$emit('delete', deletePayload);
});
- it('passes itemToBeDeleted to the modal', () => {
- expect(findPackageListDeleteModal().props('itemToBeDeleted')).toStrictEqual(firstPackage);
+ it('passes itemsToBeDeleted to the modal', () => {
+ expect(findDeletePackagesModal().props('itemsToBeDeleted')).toStrictEqual([firstPackage]);
});
it('requesting delete tracks the right action', () => {
@@ -158,9 +163,13 @@ describe('packages_list', () => {
);
});
+ it('modal component is shown', () => {
+ expect(showMock).toHaveBeenCalledTimes(1);
+ });
+
describe('when modal confirms', () => {
beforeEach(() => {
- findPackageListDeleteModal().vm.$emit('ok');
+ findDeletePackagesModal().vm.$emit('confirm');
});
it('emits delete when modal confirms', () => {
@@ -176,14 +185,14 @@ describe('packages_list', () => {
});
});
- it.each(['ok', 'cancel'])('resets itemToBeDeleted when modal emits %s', async (event) => {
- await findPackageListDeleteModal().vm.$emit(event);
+ it.each(['confirm', 'cancel'])('resets itemsToBeDeleted when modal emits %s', async (event) => {
+ await findDeletePackagesModal().vm.$emit(event);
- expect(findPackageListDeleteModal().props('itemToBeDeleted')).toBeNull();
+ expect(findDeletePackagesModal().props('itemsToBeDeleted')).toEqual([]);
});
it('canceling delete tracks the right action', () => {
- findPackageListDeleteModal().vm.$emit('cancel');
+ findDeletePackagesModal().vm.$emit('cancel');
expect(eventSpy).toHaveBeenCalledWith(
category,
@@ -237,7 +246,7 @@ describe('packages_list', () => {
it.each(['confirm', 'cancel'])('resets itemsToBeDeleted when modal emits %s', async (event) => {
await findDeletePackagesModal().vm.$emit(event);
- expect(findDeletePackagesModal().props('itemsToBeDeleted')).toHaveLength(0);
+ expect(findDeletePackagesModal().props('itemsToBeDeleted')).toEqual([]);
});
it('canceling delete tracks the right action', () => {
@@ -258,7 +267,7 @@ describe('packages_list', () => {
return nextTick();
});
- it('should display an alert message', () => {
+ it('should display an alert', () => {
expect(findErrorPackageAlert().exists()).toBe(true);
expect(findErrorPackageAlert().props('title')).toBe(
'There was an error publishing a error package package',
@@ -273,7 +282,9 @@ describe('packages_list', () => {
await nextTick();
- expect(findPackageListDeleteModal().text()).toContain(errorPackage.name);
+ expect(showMock).toHaveBeenCalledTimes(1);
+
+ expect(findDeletePackagesModal().props('itemsToBeDeleted')).toStrictEqual([errorPackage]);
});
});
diff --git a/spec/frontend/packages_and_registries/package_registry/components/list/packages_search_spec.js b/spec/frontend/packages_and_registries/package_registry/components/list/packages_search_spec.js
index 1250ecaf61f..82fa5b76367 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/list/packages_search_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/list/packages_search_spec.js
@@ -54,7 +54,7 @@ describe('Package Search', () => {
expect(findRegistrySearch().exists()).toBe(true);
});
- it('registry search is mounted after mount', async () => {
+ it('registry search is mounted after mount', () => {
mountComponent();
expect(findRegistrySearch().exists()).toBe(false);
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 19c098e1f82..9054e4998bb 100644
--- a/spec/frontend/packages_and_registries/package_registry/mock_data.js
+++ b/spec/frontend/packages_and_registries/package_registry/mock_data.js
@@ -103,12 +103,20 @@ export const linksData = {
},
};
+export const defaultPackageGroupSettings = {
+ mavenPackageRequestsForwarding: true,
+ npmPackageRequestsForwarding: true,
+ pypiPackageRequestsForwarding: true,
+ __typename: 'PackageSettings',
+};
+
export const packageVersions = () => [
{
createdAt: '2021-08-10T09:33:54Z',
id: 'gid://gitlab/Packages::Package/243',
name: '@gitlab-org/package-15',
status: 'DEFAULT',
+ packageType: 'NPM',
canDestroy: true,
tags: { nodes: packageTags() },
version: '1.0.1',
@@ -120,6 +128,7 @@ export const packageVersions = () => [
id: 'gid://gitlab/Packages::Package/244',
name: '@gitlab-org/package-15',
status: 'DEFAULT',
+ packageType: 'NPM',
canDestroy: true,
tags: { nodes: packageTags() },
version: '1.0.2',
@@ -130,7 +139,7 @@ export const packageVersions = () => [
export const packageData = (extend) => ({
__typename: 'Package',
- id: 'gid://gitlab/Packages::Package/111',
+ id: 'gid://gitlab/Packages::Package/1',
canDestroy: true,
name: '@gitlab-org/package-15',
packageType: 'NPM',
@@ -244,14 +253,6 @@ export const packageDetailsQuery = (extendPackage) => ({
},
versions: {
count: packageVersions().length,
- nodes: packageVersions(),
- pageInfo: {
- hasNextPage: true,
- hasPreviousPage: false,
- endCursor: 'endCursor',
- startCursor: 'startCursor',
- },
- __typename: 'PackageConnection',
},
dependencyLinks: {
nodes: dependencyLinks(),
@@ -298,6 +299,41 @@ export const packageMetadataQuery = (packageType) => {
};
};
+export const packageVersionsQuery = (versions = packageVersions()) => ({
+ data: {
+ package: {
+ id: 'gid://gitlab/Packages::Package/111',
+ versions: {
+ count: versions.length,
+ nodes: versions,
+ pageInfo: pagination(),
+ __typename: 'PackageConnection',
+ },
+ __typename: 'PackageDetailsType',
+ },
+ },
+});
+
+export const emptyPackageVersionsQuery = {
+ data: {
+ package: {
+ id: 'gid://gitlab/Packages::Package/111',
+ versions: {
+ count: 0,
+ nodes: [],
+ pageInfo: {
+ hasNextPage: false,
+ hasPreviousPage: false,
+ endCursor: 'endCursor',
+ startCursor: 'startCursor',
+ },
+ __typename: 'PackageConnection',
+ },
+ __typename: 'PackageDetailsType',
+ },
+ },
+};
+
export const packagesDestroyMutation = () => ({
data: {
destroyPackages: {
@@ -352,7 +388,12 @@ export const packageDestroyFilesMutationError = () => ({
],
});
-export const packagesListQuery = ({ type = 'group', extend = {}, extendPagination = {} } = {}) => ({
+export const packagesListQuery = ({
+ type = 'group',
+ extend = {},
+ extendPagination = {},
+ packageSettings = defaultPackageGroupSettings,
+} = {}) => ({
data: {
[type]: {
id: '1',
@@ -379,6 +420,14 @@ export const packagesListQuery = ({ type = 'group', extend = {}, extendPaginatio
pageInfo: pagination(extendPagination),
__typename: 'PackageConnection',
},
+ ...(type === 'group' && { packageSettings }),
+ ...(type === 'project' && {
+ group: {
+ id: '1',
+ packageSettings,
+ __typename: 'Group',
+ },
+ }),
...extend,
__typename: capitalize(type),
},
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 49f69a46395..e1765917035 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
@@ -1,4 +1,4 @@
-import { GlEmptyState, GlTabs, GlTab, GlSprintf } from '@gitlab/ui';
+import { GlEmptyState, GlModal, GlTabs, GlTab, GlSprintf } from '@gitlab/ui';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
@@ -7,7 +7,7 @@ import { useMockLocationHelper } from 'helpers/mock_window_location_helper';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { createAlert } from '~/alert';
-
+import { stubComponent } from 'helpers/stub_component';
import AdditionalMetadata from '~/packages_and_registries/package_registry/components/details/additional_metadata.vue';
import PackagesApp from '~/packages_and_registries/package_registry/pages/details.vue';
import DependencyRow from '~/packages_and_registries/package_registry/components/details/dependency_row.vue';
@@ -33,6 +33,7 @@ import {
import destroyPackageFilesMutation from '~/packages_and_registries/package_registry/graphql/mutations/destroy_package_files.mutation.graphql';
import getPackageDetails from '~/packages_and_registries/package_registry/graphql/queries/get_package_details.query.graphql';
+import getPackageVersionsQuery from '~/packages_and_registries/package_registry/graphql//queries/get_package_versions.query.graphql';
import {
packageDetailsQuery,
packageData,
@@ -42,12 +43,13 @@ import {
packageFiles,
packageDestroyFilesMutation,
packageDestroyFilesMutationError,
- pagination,
} from '../mock_data';
jest.mock('~/alert');
useMockLocationHelper();
+Vue.use(VueApollo);
+
describe('PackagesApp', () => {
let wrapper;
let apolloProvider;
@@ -57,7 +59,7 @@ describe('PackagesApp', () => {
};
const provide = {
- packageId: '111',
+ packageId: '1',
emptyListIllustration: 'svgPath',
projectListUrl: 'projectListUrl',
groupListUrl: 'groupListUrl',
@@ -66,14 +68,13 @@ describe('PackagesApp', () => {
};
const { __typename, ...packageWithoutTypename } = packageData();
+ const showMock = jest.fn();
function createComponent({
resolver = jest.fn().mockResolvedValue(packageDetailsQuery()),
filesDeleteMutationResolver = jest.fn().mockResolvedValue(packageDestroyFilesMutation()),
routeId = '1',
} = {}) {
- Vue.use(VueApollo);
-
const requestHandlers = [
[getPackageDetails, resolver],
[destroyPackageFilesMutation, filesDeleteMutationResolver],
@@ -86,17 +87,11 @@ describe('PackagesApp', () => {
stubs: {
PackageTitle,
DeletePackages,
- GlModal: {
- template: `
- <div>
- <slot name="modal-title"></slot>
- <p><slot></slot></p>
- </div>
- `,
+ GlModal: stubComponent(GlModal, {
methods: {
- show: jest.fn(),
+ show: showMock,
},
- },
+ }),
GlSprintf,
GlTabs,
GlTab,
@@ -251,7 +246,7 @@ describe('PackagesApp', () => {
await findDeleteButton().trigger('click');
- expect(findDeleteModal().find('p').text()).toBe(
+ expect(findDeleteModal().text()).toBe(
'You are about to delete version 1.0.0 of @gitlab-org/package-15. Are you sure?',
);
});
@@ -318,7 +313,7 @@ describe('PackagesApp', () => {
describe('deleting a file', () => {
const [fileToDelete] = packageFiles();
- const doDeleteFile = async () => {
+ const doDeleteFile = () => {
findPackageFiles().vm.$emit('delete-files', [fileToDelete]);
findDeleteFileModal().vm.$emit('primary');
@@ -331,13 +326,15 @@ describe('PackagesApp', () => {
await waitForPromises();
- const showDeleteFileSpy = jest.spyOn(wrapper.vm.$refs.deleteFileModal, 'show');
- const showDeletePackageSpy = jest.spyOn(wrapper.vm.$refs.deleteModal, 'show');
-
findPackageFiles().vm.$emit('delete-files', [fileToDelete]);
- expect(showDeletePackageSpy).not.toHaveBeenCalled();
- expect(showDeleteFileSpy).toHaveBeenCalled();
+ expect(showMock).toHaveBeenCalledTimes(1);
+
+ await waitForPromises();
+
+ expect(findDeleteFileModal().text()).toBe(
+ 'You are about to delete foo-1.0.1.tgz. This is a destructive action that may render your package unusable. Are you sure?',
+ );
});
it('when its the only file opens delete package confirmation modal', async () => {
@@ -360,17 +357,13 @@ describe('PackagesApp', () => {
await waitForPromises();
- const showDeleteFileSpy = jest.spyOn(wrapper.vm.$refs.deleteFileModal, 'show');
- const showDeletePackageSpy = jest.spyOn(wrapper.vm.$refs.deleteModal, 'show');
-
findPackageFiles().vm.$emit('delete-files', [fileToDelete]);
- expect(showDeletePackageSpy).toHaveBeenCalled();
- expect(showDeleteFileSpy).not.toHaveBeenCalled();
+ expect(showMock).toHaveBeenCalledTimes(1);
await waitForPromises();
- expect(findDeleteModal().find('p').text()).toBe(
+ expect(findDeleteModal().text()).toBe(
'Deleting the last package asset will remove version 1.0.0 of @gitlab-org/package-15. Are you sure?',
);
});
@@ -440,7 +433,7 @@ describe('PackagesApp', () => {
});
describe('deleting multiple files', () => {
- const doDeleteFiles = async () => {
+ const doDeleteFiles = () => {
findPackageFiles().vm.$emit('delete-files', packageFiles());
findDeleteFilesModal().vm.$emit('primary');
@@ -482,6 +475,8 @@ describe('PackagesApp', () => {
await doDeleteFiles();
+ expect(resolver).toHaveBeenCalledTimes(2);
+
expect(createAlert).toHaveBeenCalledWith(
expect.objectContaining({
message: DELETE_PACKAGE_FILES_SUCCESS_MESSAGE,
@@ -542,15 +537,13 @@ describe('PackagesApp', () => {
await waitForPromises();
- const showDeletePackageSpy = jest.spyOn(wrapper.vm.$refs.deleteModal, 'show');
-
findPackageFiles().vm.$emit('delete-files', packageFiles());
- expect(showDeletePackageSpy).toHaveBeenCalled();
+ expect(showMock).toHaveBeenCalledTimes(1);
await waitForPromises();
- expect(findDeleteModal().find('p').text()).toBe(
+ expect(findDeleteModal().text()).toBe(
'Deleting all package assets will remove version 1.0.0 of @gitlab-org/package-15. Are you sure?',
);
});
@@ -574,8 +567,6 @@ describe('PackagesApp', () => {
packageDetailsQuery({
versions: {
count: 0,
- nodes: [],
- pageInfo: pagination({ hasNextPage: false, hasPreviousPage: false }),
},
}),
),
@@ -591,61 +582,61 @@ describe('PackagesApp', () => {
});
it('binds the correct props', async () => {
- const versionNodes = packageVersions();
createComponent();
await waitForPromises();
expect(findVersionsList().props()).toMatchObject({
canDestroy: true,
- versions: expect.arrayContaining(versionNodes),
+ count: packageVersions().length,
+ isMutationLoading: false,
+ packageId: 'gid://gitlab/Packages::Package/1',
});
});
describe('delete packages', () => {
- it('exists and has the correct props', async () => {
+ beforeEach(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();
+ it('exists and has the correct props', () => {
+ expect(findDeletePackages().props('showSuccessAlert')).toBe(true);
+ expect(findDeletePackages().props('refetchQueries')).toEqual([
+ {
+ query: getPackageVersionsQuery,
+ variables: {
+ first: 20,
+ id: 'gid://gitlab/Packages::Package/1',
+ },
+ },
+ ]);
+ });
+ it('deletePackages is bound to package-versions-list delete event', () => {
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);
+ expect(findVersionsList().props('isMutationLoading')).toBe(true);
findDeletePackages().vm.$emit('end');
await nextTick();
- expect(findVersionsList().props('isLoading')).toBe(false);
+ expect(findVersionsList().props('isMutationLoading')).toBe(false);
});
});
});
describe('dependency links', () => {
- it('does not show the dependency links for a non nuget package', async () => {
+ it('does not show the dependency links for a non nuget package', () => {
createComponent();
expect(findDependenciesCountBadge().exists()).toBe(false);
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 60bb055b1db..2ee24200ed3 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,9 +1,11 @@
-import { GlEmptyState, GlSprintf, GlLink } from '@gitlab/ui';
+import { GlButton, GlEmptyState, GlSprintf, GlLink } from '@gitlab/ui';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
+import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
+import { s__ } from '~/locale';
import { WORKSPACE_GROUP, WORKSPACE_PROJECT } from '~/issues/constants';
import ListPage from '~/packages_and_registries/package_registry/pages/list.vue';
import PackageTitle from '~/packages_and_registries/package_registry/components/list/package_title.vue';
@@ -30,6 +32,7 @@ describe('PackagesListApp', () => {
emptyListIllustration: 'emptyListIllustration',
isGroupPage: true,
fullPath: 'gitlab-org',
+ settingsPath: 'settings-path',
};
const PackageList = {
@@ -49,6 +52,7 @@ describe('PackagesListApp', () => {
const findListComponent = () => wrapper.findComponent(PackageList);
const findEmptyState = () => wrapper.findComponent(GlEmptyState);
const findDeletePackages = () => wrapper.findComponent(DeletePackages);
+ const findSettingsLink = () => wrapper.findComponent(GlButton);
const mountComponent = ({
resolver = jest.fn().mockResolvedValue(packagesListQuery()),
@@ -71,13 +75,17 @@ describe('PackagesListApp', () => {
GlLoadingIcon,
GlSprintf,
GlLink,
+ PackageTitle,
PackageList,
DeletePackages,
},
+ directives: {
+ GlTooltip: createMockDirective('gl-tooltip'),
+ },
});
};
- const waitForFirstRequest = async () => {
+ const waitForFirstRequest = () => {
// emit a search update so the query is executed
findSearch().vm.$emit('update', { sort: 'NAME_DESC', filters: [] });
return waitForPromises();
@@ -103,6 +111,52 @@ describe('PackagesListApp', () => {
});
});
+ describe('link to settings', () => {
+ describe('when settings path is not provided', () => {
+ beforeEach(() => {
+ mountComponent({
+ provide: {
+ ...defaultProvide,
+ settingsPath: '',
+ },
+ });
+ });
+
+ it('is not rendered', () => {
+ expect(findSettingsLink().exists()).toBe(false);
+ });
+ });
+
+ describe('when settings path is provided', () => {
+ const label = s__('PackageRegistry|Configure in settings');
+
+ beforeEach(() => {
+ mountComponent();
+ });
+
+ it('is rendered', () => {
+ expect(findSettingsLink().exists()).toBe(true);
+ });
+
+ it('has the right icon', () => {
+ expect(findSettingsLink().props('icon')).toBe('settings');
+ });
+
+ it('has the right attributes', () => {
+ expect(findSettingsLink().attributes()).toMatchObject({
+ 'aria-label': label,
+ href: defaultProvide.settingsPath,
+ });
+ });
+
+ it('sets tooltip with right label', () => {
+ const tooltip = getBinding(findSettingsLink().element, 'gl-tooltip');
+
+ expect(tooltip.value).toBe(label);
+ });
+ });
+ });
+
describe('search component', () => {
it('exists', () => {
mountComponent();
@@ -141,6 +195,11 @@ describe('PackagesListApp', () => {
list: expect.arrayContaining([expect.objectContaining({ id: packageData().id })]),
isLoading: false,
pageInfo: expect.objectContaining({ endCursor: pagination().endCursor }),
+ groupSettings: expect.objectContaining({
+ mavenPackageRequestsForwarding: true,
+ npmPackageRequestsForwarding: true,
+ pypiPackageRequestsForwarding: true,
+ }),
});
});
@@ -191,6 +250,16 @@ describe('PackagesListApp', () => {
expect.objectContaining({ isGroupPage, [sortType]: 'NAME_DESC' }),
);
});
+
+ it('list component has group settings prop set', () => {
+ expect(findListComponent().props()).toMatchObject({
+ groupSettings: expect.objectContaining({
+ mavenPackageRequestsForwarding: true,
+ npmPackageRequestsForwarding: true,
+ pypiPackageRequestsForwarding: true,
+ }),
+ });
+ });
});
describe.each`
diff --git a/spec/frontend/packages_and_registries/settings/group/components/package_settings_spec.js b/spec/frontend/packages_and_registries/settings/group/components/package_settings_spec.js
index 22e42f8c0ab..49e76cfbae0 100644
--- a/spec/frontend/packages_and_registries/settings/group/components/package_settings_spec.js
+++ b/spec/frontend/packages_and_registries/settings/group/components/package_settings_spec.js
@@ -177,7 +177,7 @@ describe('Packages Settings', () => {
});
});
- it('renders ExceptionsInput and assigns duplication allowness and exception props', async () => {
+ it('renders ExceptionsInput and assigns duplication allowness and exception props', () => {
mountComponent({ mountFn: mountExtended });
const { genericDuplicatesAllowed, genericDuplicateExceptionRegex } = packageSettings;
@@ -192,7 +192,7 @@ describe('Packages Settings', () => {
});
});
- it('on update event calls the mutation', async () => {
+ it('on update event calls the mutation', () => {
const mutationResolver = jest.fn().mockResolvedValue(groupPackageSettingsMutationMock());
mountComponent({ mountFn: mountExtended, mutationResolver });
diff --git a/spec/frontend/packages_and_registries/settings/group/components/packages_forwarding_settings_spec.js b/spec/frontend/packages_and_registries/settings/group/components/packages_forwarding_settings_spec.js
index d57077b31c8..8a66a685733 100644
--- a/spec/frontend/packages_and_registries/settings/group/components/packages_forwarding_settings_spec.js
+++ b/spec/frontend/packages_and_registries/settings/group/components/packages_forwarding_settings_spec.js
@@ -1,12 +1,13 @@
import Vue from 'vue';
-import { GlButton } from '@gitlab/ui';
+import { GlButton, GlLink, GlSprintf } from '@gitlab/ui';
import VueApollo from 'vue-apollo';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
+import { s__ } from '~/locale';
import component from '~/packages_and_registries/settings/group/components/packages_forwarding_settings.vue';
import {
- PACKAGE_FORWARDING_SETTINGS_DESCRIPTION,
+ REQUEST_FORWARDING_HELP_PAGE_PATH,
PACKAGE_FORWARDING_SETTINGS_HEADER,
} from '~/packages_and_registries/settings/group/constants';
@@ -60,6 +61,7 @@ describe('Packages Forwarding Settings', () => {
forwardSettings,
},
stubs: {
+ GlSprintf,
SettingsBlock,
},
});
@@ -72,6 +74,7 @@ describe('Packages Forwarding Settings', () => {
const findMavenForwardingSettings = () => wrapper.findByTestId('maven');
const findNpmForwardingSettings = () => wrapper.findByTestId('npm');
const findPyPiForwardingSettings = () => wrapper.findByTestId('pypi');
+ const findRequestForwardingDocsLink = () => wrapper.findComponent(GlLink);
const fillApolloCache = () => {
apolloProvider.defaultClient.cache.writeQuery({
@@ -111,8 +114,18 @@ describe('Packages Forwarding Settings', () => {
it('has the correct description text', () => {
mountComponent();
- expect(findDescription().text()).toMatchInterpolatedText(
- PACKAGE_FORWARDING_SETTINGS_DESCRIPTION,
+ expect(findDescription().text()).toBe(
+ s__(
+ 'PackageRegistry|Forward package requests to a public registry if the packages are not found in the GitLab package registry.',
+ ),
+ );
+ });
+
+ it('has the right help link', () => {
+ mountComponent();
+
+ expect(findRequestForwardingDocsLink().attributes('href')).toBe(
+ REQUEST_FORWARDING_HELP_PAGE_PATH,
);
});
diff --git a/spec/frontend/packages_and_registries/settings/project/settings/components/cleanup_image_tags_spec.js b/spec/frontend/packages_and_registries/settings/project/settings/components/cleanup_image_tags_spec.js
index 49e8601da88..cbe68df5343 100644
--- a/spec/frontend/packages_and_registries/settings/project/settings/components/cleanup_image_tags_spec.js
+++ b/spec/frontend/packages_and_registries/settings/project/settings/components/cleanup_image_tags_spec.js
@@ -126,7 +126,7 @@ describe('Cleanup image tags project settings', () => {
});
describe('an admin is visiting the page', () => {
- it('shows the admin part of the alert message', async () => {
+ it('shows the admin part of the alert', async () => {
mountComponentWithApollo({
provide: { ...defaultProvidedValues, isAdmin: true },
resolver: jest.fn().mockResolvedValue(nullExpirationPolicyPayload()),
diff --git a/spec/frontend/packages_and_registries/settings/project/settings/components/container_expiration_policy_form_spec.js b/spec/frontend/packages_and_registries/settings/project/settings/components/container_expiration_policy_form_spec.js
index 57b48407174..a68087f7f57 100644
--- a/spec/frontend/packages_and_registries/settings/project/settings/components/container_expiration_policy_form_spec.js
+++ b/spec/frontend/packages_and_registries/settings/project/settings/components/container_expiration_policy_form_spec.js
@@ -46,7 +46,7 @@ describe('Container Expiration Policy Settings Form', () => {
const findOlderThanDropdown = () => wrapper.find('[data-testid="older-than-dropdown"]');
const findRemoveRegexInput = () => wrapper.find('[data-testid="remove-regex-input"]');
- const submitForm = async () => {
+ const submitForm = () => {
findForm().trigger('submit');
return waitForPromises();
};
diff --git a/spec/frontend/packages_and_registries/settings/project/settings/components/container_expiration_policy_spec.js b/spec/frontend/packages_and_registries/settings/project/settings/components/container_expiration_policy_spec.js
index 19f25d0aef7..c9dd9ce7a45 100644
--- a/spec/frontend/packages_and_registries/settings/project/settings/components/container_expiration_policy_spec.js
+++ b/spec/frontend/packages_and_registries/settings/project/settings/components/container_expiration_policy_spec.js
@@ -109,7 +109,7 @@ describe('Container expiration policy project settings', () => {
});
describe('an admin is visiting the page', () => {
- it('shows the admin part of the alert message', async () => {
+ it('shows the admin part of the alert', async () => {
mountComponentWithApollo({
provide: { ...defaultProvidedValues, isAdmin: true },
resolver: jest.fn().mockResolvedValue(nullExpirationPolicyPayload()),
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 b9c0c38bf9e..50b72d3ad72 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
@@ -48,7 +48,7 @@ describe('Packages Cleanup Policy Settings Form', () => {
wrapper.findByTestId('keep-n-duplicated-package-files-dropdown');
const findNextRunAt = () => wrapper.findByTestId('next-run-at');
- const submitForm = async () => {
+ const submitForm = () => {
findForm().trigger('submit');
return waitForPromises();
};
diff --git a/spec/frontend/packages_and_registries/settings/project/settings/components/registry_settings_app_spec.js b/spec/frontend/packages_and_registries/settings/project/settings/components/registry_settings_app_spec.js
index 54655acdf2a..12425909454 100644
--- a/spec/frontend/packages_and_registries/settings/project/settings/components/registry_settings_app_spec.js
+++ b/spec/frontend/packages_and_registries/settings/project/settings/components/registry_settings_app_spec.js
@@ -79,7 +79,7 @@ describe('Registry Settings app', () => {
${false} | ${true}
${false} | ${false}
`(
- 'container expiration policy $showContainerRegistrySettings and package cleanup policy is $showPackageRegistrySettings',
+ 'container cleanup policy $showContainerRegistrySettings and package cleanup policy is $showPackageRegistrySettings',
({ showContainerRegistrySettings, showPackageRegistrySettings }) => {
mountComponent({
showContainerRegistrySettings,
diff --git a/spec/frontend/packages_and_registries/shared/components/__snapshots__/registry_breadcrumb_spec.js.snap b/spec/frontend/packages_and_registries/shared/components/__snapshots__/registry_breadcrumb_spec.js.snap
index e6e89806ce0..e9ee6ebdb5c 100644
--- a/spec/frontend/packages_and_registries/shared/components/__snapshots__/registry_breadcrumb_spec.js.snap
+++ b/spec/frontend/packages_and_registries/shared/components/__snapshots__/registry_breadcrumb_spec.js.snap
@@ -5,7 +5,6 @@ exports[`Registry Breadcrumb when is not rootRoute renders 1`] = `
aria-label="Breadcrumb"
class="gl-breadcrumbs"
>
-
<ol
class="breadcrumb gl-breadcrumb-list"
>
@@ -16,29 +15,10 @@ exports[`Registry Breadcrumb when is not rootRoute renders 1`] = `
class=""
target="_self"
>
+ <!---->
<span>
</span>
-
- <span
- class="gl-breadcrumb-separator"
- data-testid="separator"
- >
- <span
- class="gl-mx-n5"
- >
- <svg
- aria-hidden="true"
- class="gl-icon s8"
- data-testid="chevron-lg-right-icon"
- role="img"
- >
- <use
- href="#chevron-lg-right"
- />
- </svg>
- </span>
- </span>
</a>
</li>
@@ -52,11 +32,10 @@ exports[`Registry Breadcrumb when is not rootRoute renders 1`] = `
href="#"
target="_self"
>
+ <!---->
<span>
</span>
-
- <!---->
</a>
</li>
@@ -70,7 +49,6 @@ exports[`Registry Breadcrumb when is rootRoute renders 1`] = `
aria-label="Breadcrumb"
class="gl-breadcrumbs"
>
-
<ol
class="breadcrumb gl-breadcrumb-list"
>
@@ -82,11 +60,10 @@ exports[`Registry Breadcrumb when is rootRoute renders 1`] = `
class=""
target="_self"
>
+ <!---->
<span>
</span>
-
- <!---->
</a>
</li>
diff --git a/spec/frontend/packages_and_registries/shared/components/persisted_search_spec.js b/spec/frontend/packages_and_registries/shared/components/persisted_search_spec.js
index 1484377a475..c1e86080d29 100644
--- a/spec/frontend/packages_and_registries/shared/components/persisted_search_spec.js
+++ b/spec/frontend/packages_and_registries/shared/components/persisted_search_spec.js
@@ -51,7 +51,7 @@ describe('Persisted Search', () => {
expect(findRegistrySearch().exists()).toBe(true);
});
- it('registry search is mounted after mount', async () => {
+ it('registry search is mounted after mount', () => {
mountComponent();
expect(findRegistrySearch().exists()).toBe(false);
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 a4e0d267023..85b4ca95d5d 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
@@ -116,7 +116,7 @@ describe('Registry List', () => {
expect(findDeleteSelected().exists()).toBe(false);
});
- it('populates the first slot prop correctly', async () => {
+ it('populates the first slot prop correctly', () => {
expect(findScopedSlots().at(0).exists()).toBe(true);
// it's the first slot
diff --git a/spec/frontend/pages/admin/abuse_reports/abuse_reports_spec.js b/spec/frontend/pages/admin/abuse_reports/abuse_reports_spec.js
index 6edfe9641b9..6cf30e84288 100644
--- a/spec/frontend/pages/admin/abuse_reports/abuse_reports_spec.js
+++ b/spec/frontend/pages/admin/abuse_reports/abuse_reports_spec.js
@@ -1,9 +1,9 @@
import $ from 'jquery';
-import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
+import htmlAbuseReportsList from 'test_fixtures/abuse_reports/abuse_reports_list.html';
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import AbuseReports from '~/pages/admin/abuse_reports/abuse_reports';
describe('Abuse Reports', () => {
- const FIXTURE = 'abuse_reports/abuse_reports_list.html';
const MAX_MESSAGE_LENGTH = 500;
let $messages;
@@ -15,7 +15,7 @@ describe('Abuse Reports', () => {
$messages.filter((index, element) => element.innerText.indexOf(searchText) > -1).first();
beforeEach(() => {
- loadHTMLFixture(FIXTURE);
+ setHTMLFixture(htmlAbuseReportsList);
new AbuseReports(); // eslint-disable-line no-new
$messages = $('.abuse-reports .message');
});
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 d422f5dade3..176ec36fffc 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,14 +1,13 @@
-import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
+import htmlApplicationSettingsAccountsAndLimit from 'test_fixtures/application_settings/accounts_and_limit.html';
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
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';
-
beforeEach(() => {
- loadHTMLFixture(FIXTURE);
+ setHTMLFixture(htmlApplicationSettingsAccountsAndLimit);
initAccountAndLimitsSection();
});
diff --git a/spec/frontend/pages/admin/application_settings/metrics_and_profiling/usage_statistics_spec.js b/spec/frontend/pages/admin/application_settings/metrics_and_profiling/usage_statistics_spec.js
index 3c512cfd6ae..72d2bb0f983 100644
--- a/spec/frontend/pages/admin/application_settings/metrics_and_profiling/usage_statistics_spec.js
+++ b/spec/frontend/pages/admin/application_settings/metrics_and_profiling/usage_statistics_spec.js
@@ -1,18 +1,18 @@
+import htmlApplicationSettingsUsage from 'test_fixtures/application_settings/usage.html';
import initSetHelperText, {
HELPER_TEXT_SERVICE_PING_DISABLED,
HELPER_TEXT_SERVICE_PING_ENABLED,
} from '~/pages/admin/application_settings/metrics_and_profiling/usage_statistics';
-import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
describe('UsageStatistics', () => {
- const FIXTURE = 'application_settings/usage.html';
let servicePingCheckBox;
let servicePingFeaturesCheckBox;
let servicePingFeaturesLabel;
let servicePingFeaturesHelperText;
beforeEach(() => {
- loadHTMLFixture(FIXTURE);
+ setHTMLFixture(htmlApplicationSettingsUsage);
initSetHelperText();
servicePingCheckBox = document.getElementById('application_setting_usage_ping_enabled');
servicePingFeaturesCheckBox = document.getElementById(
diff --git a/spec/frontend/pages/admin/jobs/index/components/cancel_jobs_modal_spec.js b/spec/frontend/pages/admin/jobs/components/cancel_jobs_modal_spec.js
index 366d148a608..afd7ee09ae8 100644
--- a/spec/frontend/pages/admin/jobs/index/components/cancel_jobs_modal_spec.js
+++ b/spec/frontend/pages/admin/jobs/components/cancel_jobs_modal_spec.js
@@ -4,7 +4,7 @@ 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 CancelJobsModal from '~/pages/admin/jobs/index/components/cancel_jobs_modal.vue';
+import CancelJobsModal from '~/pages/admin/jobs/components/cancel_jobs_modal.vue';
jest.mock('~/lib/utils/url_utility', () => ({
...jest.requireActual('~/lib/utils/url_utility'),
diff --git a/spec/frontend/pages/admin/jobs/index/components/cancel_jobs_spec.js b/spec/frontend/pages/admin/jobs/components/cancel_jobs_spec.js
index de6d44eabdc..d94de48f238 100644
--- a/spec/frontend/pages/admin/jobs/index/components/cancel_jobs_spec.js
+++ b/spec/frontend/pages/admin/jobs/components/cancel_jobs_spec.js
@@ -2,12 +2,12 @@ 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 CancelJobs from '~/pages/admin/jobs/components/cancel_jobs.vue';
+import CancelJobsModal from '~/pages/admin/jobs/components/cancel_jobs_modal.vue';
import {
CANCEL_JOBS_MODAL_ID,
CANCEL_BUTTON_TOOLTIP,
-} from '~/pages/admin/jobs/index/components/constants';
+} from '~/pages/admin/jobs/components/constants';
describe('CancelJobs component', () => {
let wrapper;
diff --git a/spec/frontend/pages/admin/jobs/components/table/admin_job_table_app_spec.js b/spec/frontend/pages/admin/jobs/components/table/admin_job_table_app_spec.js
new file mode 100644
index 00000000000..8dabc076980
--- /dev/null
+++ b/spec/frontend/pages/admin/jobs/components/table/admin_job_table_app_spec.js
@@ -0,0 +1,100 @@
+import { GlSkeletonLoader, GlLoadingIcon, GlEmptyState, GlAlert } 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 JobsTable from '~/jobs/components/table/jobs_table.vue';
+import getJobsQuery from '~/pages/admin/jobs/components/table/graphql/queries/get_all_jobs.query.graphql';
+import AdminJobsTableApp from '~/pages/admin/jobs/components/table/admin_jobs_table_app.vue';
+
+import {
+ mockAllJobsResponsePaginated,
+ mockJobsResponseEmpty,
+ statuses,
+} from '../../../../../jobs/mock_data';
+
+Vue.use(VueApollo);
+
+describe('Job table app', () => {
+ let wrapper;
+
+ const successHandler = jest.fn().mockResolvedValue(mockAllJobsResponsePaginated);
+ const emptyHandler = jest.fn().mockResolvedValue(mockJobsResponseEmpty);
+ const failedHandler = jest.fn().mockRejectedValue(new Error('GraphQL error'));
+
+ const findSkeletonLoader = () => wrapper.findComponent(GlSkeletonLoader);
+ const findLoadingSpinner = () => wrapper.findComponent(GlLoadingIcon);
+ const findTable = () => wrapper.findComponent(JobsTable);
+ const findEmptyState = () => wrapper.findComponent(GlEmptyState);
+ const findAlert = () => wrapper.findComponent(GlAlert);
+
+ const createMockApolloProvider = (handler) => {
+ const requestHandlers = [[getJobsQuery, handler]];
+
+ return createMockApollo(requestHandlers);
+ };
+
+ const createComponent = ({
+ handler = successHandler,
+ mountFn = shallowMount,
+ data = {},
+ } = {}) => {
+ wrapper = mountFn(AdminJobsTableApp, {
+ data() {
+ return {
+ ...data,
+ };
+ },
+ provide: {
+ jobStatuses: statuses,
+ },
+ apolloProvider: createMockApolloProvider(handler),
+ });
+ };
+
+ describe('loaded state', () => {
+ beforeEach(async () => {
+ createComponent();
+
+ await waitForPromises();
+ });
+
+ it('should display the jobs table with data', () => {
+ expect(findTable().exists()).toBe(true);
+ expect(findSkeletonLoader().exists()).toBe(false);
+ expect(findLoadingSpinner().exists()).toBe(false);
+ });
+ });
+
+ describe('empty state', () => {
+ it('should display empty state if there are no jobs and tab scope is null', async () => {
+ createComponent({ handler: emptyHandler, mountFn: mount });
+
+ await waitForPromises();
+
+ expect(findEmptyState().exists()).toBe(true);
+ expect(findTable().exists()).toBe(false);
+ });
+
+ it('should not display empty state if there are jobs and tab scope is not null', async () => {
+ createComponent({ handler: successHandler, mountFn: mount });
+
+ await waitForPromises();
+
+ expect(findEmptyState().exists()).toBe(false);
+ expect(findTable().exists()).toBe(true);
+ });
+ });
+
+ describe('error state', () => {
+ it('should show an alert if there is an error fetching the jobs data', async () => {
+ createComponent({ handler: failedHandler });
+
+ await waitForPromises();
+
+ expect(findAlert().text()).toBe('There was an error fetching the jobs.');
+ expect(findTable().exists()).toBe(false);
+ });
+ });
+});
diff --git a/spec/frontend/pages/admin/jobs/components/table/graphql/cache_config_spec.js b/spec/frontend/pages/admin/jobs/components/table/graphql/cache_config_spec.js
new file mode 100644
index 00000000000..59e9eda6343
--- /dev/null
+++ b/spec/frontend/pages/admin/jobs/components/table/graphql/cache_config_spec.js
@@ -0,0 +1,106 @@
+import cacheConfig from '~/pages/admin/jobs/components/table/graphql/cache_config';
+import {
+ CIJobConnectionExistingCache,
+ CIJobConnectionIncomingCache,
+ CIJobConnectionIncomingCacheRunningStatus,
+} from '../../../../../../jobs/mock_data';
+
+const firstLoadArgs = { first: 3, statuses: 'PENDING' };
+const runningArgs = { first: 3, statuses: 'RUNNING' };
+
+describe('jobs/components/table/graphql/cache_config', () => {
+ describe('when fetching data with the same statuses', () => {
+ it('should contain cache nodes and a status when merging caches on first load', () => {
+ const res = cacheConfig.typePolicies.CiJobConnection.merge({}, CIJobConnectionIncomingCache, {
+ args: firstLoadArgs,
+ });
+
+ expect(res.nodes).toHaveLength(CIJobConnectionIncomingCache.nodes.length);
+ expect(res.statuses).toBe('PENDING');
+ });
+
+ it('should add to existing caches when merging caches after first load', () => {
+ const res = cacheConfig.typePolicies.CiJobConnection.merge(
+ CIJobConnectionExistingCache,
+ CIJobConnectionIncomingCache,
+ {
+ args: firstLoadArgs,
+ },
+ );
+
+ expect(res.nodes).toHaveLength(
+ CIJobConnectionIncomingCache.nodes.length + CIJobConnectionExistingCache.nodes.length,
+ );
+ });
+
+ it('should not add to existing cache if the incoming elements are the same', () => {
+ // simulate that this is the last page
+ const finalExistingCache = {
+ ...CIJobConnectionExistingCache,
+ pageInfo: {
+ hasNextPage: false,
+ },
+ };
+
+ const res = cacheConfig.typePolicies.CiJobConnection.merge(
+ CIJobConnectionExistingCache,
+ finalExistingCache,
+ {
+ args: firstLoadArgs,
+ },
+ );
+
+ expect(res.nodes).toHaveLength(CIJobConnectionExistingCache.nodes.length);
+ });
+
+ it('should contain the pageInfo key as part of the result', () => {
+ const res = cacheConfig.typePolicies.CiJobConnection.merge({}, CIJobConnectionIncomingCache, {
+ args: firstLoadArgs,
+ });
+
+ expect(res.pageInfo).toEqual(
+ expect.objectContaining({
+ __typename: 'PageInfo',
+ endCursor: 'eyJpZCI6IjIwNTEifQ',
+ hasNextPage: true,
+ hasPreviousPage: false,
+ startCursor: 'eyJpZCI6IjIxNzMifQ',
+ }),
+ );
+ });
+ });
+
+ describe('when fetching data with different statuses', () => {
+ it('should reset cache when a cache already exists', () => {
+ const res = cacheConfig.typePolicies.CiJobConnection.merge(
+ CIJobConnectionExistingCache,
+ CIJobConnectionIncomingCacheRunningStatus,
+ {
+ args: runningArgs,
+ },
+ );
+
+ expect(res.nodes).not.toEqual(CIJobConnectionExistingCache.nodes);
+ expect(res.nodes).toHaveLength(CIJobConnectionIncomingCacheRunningStatus.nodes.length);
+ });
+ });
+
+ describe('when incoming data has no nodes', () => {
+ it('should return existing cache', () => {
+ const res = cacheConfig.typePolicies.CiJobConnection.merge(
+ CIJobConnectionExistingCache,
+ { __typename: 'CiJobConnection', count: 500 },
+ {
+ args: { statuses: 'SUCCESS' },
+ },
+ );
+
+ const expectedResponse = {
+ ...CIJobConnectionExistingCache,
+ statuses: 'SUCCESS',
+ };
+
+ expect(res).toEqual(expectedResponse);
+ });
+ });
+});
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 834d14e0fb3..c00dbc0ec02 100644
--- a/spec/frontend/pages/admin/projects/components/namespace_select_spec.js
+++ b/spec/frontend/pages/admin/projects/components/namespace_select_spec.js
@@ -45,7 +45,7 @@ describe('NamespaceSelect', () => {
expect(findNamespaceInput().exists()).toBe(false);
});
- it('sets appropriate props', async () => {
+ it('sets appropriate props', () => {
expect(findListbox().props()).toMatchObject({
items: [
{ text: 'user: Administrator', value: '10' },
@@ -84,7 +84,7 @@ describe('NamespaceSelect', () => {
expect(findNamespaceInput().attributes('value')).toBe(selectId);
});
- it('updates the listbox value', async () => {
+ it('updates the listbox value', () => {
expect(findListbox().props()).toMatchObject({
selected: selectId,
toggleText: expectToggleText,
diff --git a/spec/frontend/pages/dashboard/todos/index/todos_spec.js b/spec/frontend/pages/dashboard/todos/index/todos_spec.js
index 70d7cb9c839..52091d45ada 100644
--- a/spec/frontend/pages/dashboard/todos/index/todos_spec.js
+++ b/spec/frontend/pages/dashboard/todos/index/todos_spec.js
@@ -1,5 +1,6 @@
import MockAdapter from 'axios-mock-adapter';
-import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
+import htmlTodos from 'test_fixtures/todos/todos.html';
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import waitForPromises from 'helpers/wait_for_promises';
import '~/lib/utils/common_utils';
import axios from '~/lib/utils/axios_utils';
@@ -18,7 +19,7 @@ describe('Todos', () => {
let mock;
beforeEach(() => {
- loadHTMLFixture('todos/todos.html');
+ setHTMLFixture(htmlTodos);
mock = new MockAdapter(axios);
return new Todos();
diff --git a/spec/frontend/pages/groups/new/components/app_spec.js b/spec/frontend/pages/groups/new/components/app_spec.js
index 7ccded9b0f1..19240f1a044 100644
--- a/spec/frontend/pages/groups/new/components/app_spec.js
+++ b/spec/frontend/pages/groups/new/components/app_spec.js
@@ -6,7 +6,9 @@ describe('App component', () => {
let wrapper;
const createComponent = (propsData = {}) => {
- wrapper = shallowMount(App, { propsData: { groupsUrl: '/dashboard/groups', ...propsData } });
+ wrapper = shallowMount(App, {
+ propsData: { rootPath: '/', groupsUrl: '/dashboard/groups', ...propsData },
+ });
};
const findNewNamespacePage = () => wrapper.findComponent(NewNamespacePage);
@@ -20,6 +22,7 @@ describe('App component', () => {
createComponent();
expect(findNewNamespacePage().props('initialBreadcrumbs')).toEqual([
+ { href: '/', text: 'Your work' },
{ href: '/dashboard/groups', text: 'Groups' },
{ href: '#', text: 'New group' },
]);
diff --git a/spec/frontend/pages/import/bitbucket_server/components/bitbucket_server_status_table_spec.js b/spec/frontend/pages/import/bitbucket_server/components/bitbucket_server_status_table_spec.js
index b020caa3010..8eab5061e97 100644
--- a/spec/frontend/pages/import/bitbucket_server/components/bitbucket_server_status_table_spec.js
+++ b/spec/frontend/pages/import/bitbucket_server/components/bitbucket_server_status_table_spec.js
@@ -39,7 +39,7 @@ describe('BitbucketServerStatusTable', () => {
expect(wrapper.findComponent(BitbucketStatusTable).exists()).toBe(true);
});
- it('renders Reconfigure button', async () => {
+ it('renders Reconfigure button', () => {
createComponent(BitbucketStatusTableStub);
expect(findReconfigureButton().attributes().href).toBe('/reconfigure');
expect(findReconfigureButton().text()).toBe('Reconfigure');
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 477511cde64..8a7fc57c409 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
@@ -192,7 +192,7 @@ describe('BulkImportsHistoryApp', () => {
return axios.waitForAll();
});
- it('renders details button if relevant item has failures', async () => {
+ it('renders details button if relevant item has failures', () => {
expect(
extendedWrapper(wrapper.find('tbody').findAll('tr').at(1)).findByText('Details').exists(),
).toBe(true);
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 43cbac25fe8..8e14b5a24f8 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
@@ -61,7 +61,6 @@ describe('ImportHistoryApp', () => {
beforeEach(() => {
gon.api_version = 'v4';
- gon.features = { fullPathProjectSearch: true };
mock = new MockAdapter(axios);
});
@@ -167,7 +166,7 @@ describe('ImportHistoryApp', () => {
return axios.waitForAll();
});
- it('renders details button if relevant item has failed', async () => {
+ it('renders details button if relevant item has failed', () => {
expect(
extendedWrapper(wrapper.find('tbody').findAll('tr').at(1)).findByText('Details').exists(),
).toBe(true);
diff --git a/spec/frontend/pages/projects/forks/new/components/project_namespace_spec.js b/spec/frontend/pages/projects/forks/new/components/project_namespace_spec.js
index af578b69a81..b308d6305da 100644
--- a/spec/frontend/pages/projects/forks/new/components/project_namespace_spec.js
+++ b/spec/frontend/pages/projects/forks/new/components/project_namespace_spec.js
@@ -145,7 +145,7 @@ describe('ProjectNamespace component', () => {
await nextTick();
});
- it('creates an alert message and captures the error', () => {
+ it('creates an alert and captures the error', () => {
expect(createAlert).toHaveBeenCalledWith({
message: 'Something went wrong while loading data. Please refresh the page to try again.',
captureError: true,
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 0812be9745e..d8a848f0a2e 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
@@ -760,7 +760,7 @@ describe('Settings Panel', () => {
expect(findEnvironmentsSettings().exists()).toBe(true);
});
});
- describe('Feature Flags', () => {
+ describe('Feature flags', () => {
it('should show the feature flags toggle', () => {
wrapper = mountComponent({});
diff --git a/spec/frontend/pages/sessions/new/preserve_url_fragment_spec.js b/spec/frontend/pages/sessions/new/preserve_url_fragment_spec.js
index 4c4a0fbea11..e554521bfb5 100644
--- a/spec/frontend/pages/sessions/new/preserve_url_fragment_spec.js
+++ b/spec/frontend/pages/sessions/new/preserve_url_fragment_spec.js
@@ -1,5 +1,6 @@
import $ from 'jquery';
-import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
+import htmlSessionsNew from 'test_fixtures/sessions/new.html';
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import preserveUrlFragment from '~/pages/sessions/new/preserve_url_fragment';
describe('preserve_url_fragment', () => {
@@ -8,7 +9,7 @@ describe('preserve_url_fragment', () => {
};
beforeEach(() => {
- loadHTMLFixture('sessions/new.html');
+ setHTMLFixture(htmlSessionsNew);
});
afterEach(() => {
diff --git a/spec/frontend/pages/sessions/new/signin_tabs_memoizer_spec.js b/spec/frontend/pages/sessions/new/signin_tabs_memoizer_spec.js
index f736ce46f9b..cae2615e849 100644
--- a/spec/frontend/pages/sessions/new/signin_tabs_memoizer_spec.js
+++ b/spec/frontend/pages/sessions/new/signin_tabs_memoizer_spec.js
@@ -1,4 +1,5 @@
-import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
+import htmlStaticSigninTabs from 'test_fixtures_static/signin_tabs.html';
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import { useLocalStorageSpy } from 'helpers/local_storage_helper';
import AccessorUtilities from '~/lib/utils/accessor';
import SigninTabsMemoizer from '~/pages/sessions/new/signin_tabs_memoizer';
@@ -6,7 +7,6 @@ import SigninTabsMemoizer from '~/pages/sessions/new/signin_tabs_memoizer';
useLocalStorageSpy();
describe('SigninTabsMemoizer', () => {
- const fixtureTemplate = 'static/signin_tabs.html';
const tabSelector = 'ul.new-session-tabs';
const currentTabKey = 'current_signin_tab';
let memo;
@@ -20,7 +20,7 @@ describe('SigninTabsMemoizer', () => {
}
beforeEach(() => {
- loadHTMLFixture(fixtureTemplate);
+ setHTMLFixture(htmlStaticSigninTabs);
jest.spyOn(AccessorUtilities, 'canUseLocalStorage').mockReturnValue(true);
});
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 1be4a974f7a..ddaa3df71e8 100644
--- a/spec/frontend/pages/shared/wikis/components/wiki_form_spec.js
+++ b/spec/frontend/pages/shared/wikis/components/wiki_form_spec.js
@@ -116,6 +116,7 @@ describe('WikiForm', () => {
renderMarkdownPath: pageInfoPersisted.markdownPreviewPath,
uploadsPath: pageInfoPersisted.uploadsPath,
autofocus: pageInfoPersisted.persisted,
+ markdownDocsPath: pageInfoPersisted.markdownHelpPath,
}),
);
@@ -123,10 +124,6 @@ describe('WikiForm', () => {
id: 'wiki_content',
name: 'wiki[content]',
});
-
- expect(markdownEditor.vm.$attrs['markdown-docs-path']).toEqual(
- pageInfoPersisted.markdownHelpPath,
- );
});
it.each`
@@ -307,7 +304,7 @@ describe('WikiForm', () => {
expect(findFormat().element.getAttribute('disabled')).toBeDefined();
});
- it('sends tracking event when editor loads', async () => {
+ it('sends tracking event when editor loads', () => {
expect(trackingSpy).toHaveBeenCalledWith(undefined, CONTENT_EDITOR_LOADED_ACTION, {
label: WIKI_CONTENT_EDITOR_TRACKING_LABEL,
});
@@ -321,7 +318,7 @@ describe('WikiForm', () => {
await triggerFormSubmit();
});
- it('triggers tracking events on form submit', async () => {
+ it('triggers tracking events on form submit', () => {
expect(trackingSpy).toHaveBeenCalledWith(undefined, SAVED_USING_CONTENT_EDITOR_ACTION, {
label: WIKI_CONTENT_EDITOR_TRACKING_LABEL,
});
diff --git a/spec/frontend/performance_bar/components/performance_bar_app_spec.js b/spec/frontend/performance_bar/components/performance_bar_app_spec.js
index 2c9ab4bf78d..7a018236314 100644
--- a/spec/frontend/performance_bar/components/performance_bar_app_spec.js
+++ b/spec/frontend/performance_bar/components/performance_bar_app_spec.js
@@ -1,18 +1,53 @@
-import { shallowMount } from '@vue/test-utils';
+import { mount } from '@vue/test-utils';
+import { GlLink } from '@gitlab/ui';
import PerformanceBarApp from '~/performance_bar/components/performance_bar_app.vue';
import PerformanceBarStore from '~/performance_bar/stores/performance_bar_store';
describe('performance bar app', () => {
+ let wrapper;
const store = new PerformanceBarStore();
- const wrapper = shallowMount(PerformanceBarApp, {
- propsData: {
- store,
- env: 'development',
- requestId: '123',
- statsUrl: 'https://log.gprd.gitlab.net/app/dashboards#/view/',
- peekUrl: '/-/peek/results',
- profileUrl: '?lineprofiler=true',
- },
+ store.addRequest('123', 'https://gitlab.com', '', {}, 'GET');
+ const createComponent = () => {
+ wrapper = mount(PerformanceBarApp, {
+ propsData: {
+ store,
+ env: 'development',
+ requestId: '123',
+ requestMethod: 'GET',
+ statsUrl: 'https://log.gprd.gitlab.net/app/dashboards#/view/',
+ peekUrl: '/-/peek/results',
+ },
+ });
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ describe('flamegraph buttons', () => {
+ const flamegraphDiv = () => wrapper.find('#peek-flamegraph');
+ const flamegraphLinks = () => flamegraphDiv().findAllComponents(GlLink);
+
+ it('creates three flamegraph buttons based on the path', () => {
+ expect(flamegraphLinks()).toHaveLength(3);
+
+ ['wall', 'cpu', 'object'].forEach((path, index) => {
+ expect(flamegraphLinks().at(index).attributes('href')).toBe(
+ `https://gitlab.com?performance_bar=flamegraph&stackprof_mode=${path}`,
+ );
+ });
+ });
+ });
+
+ describe('memory report button', () => {
+ const memoryReportDiv = () => wrapper.find('#peek-memory-report');
+ const memoryReportLink = () => memoryReportDiv().findComponent(GlLink);
+
+ it('creates memory report button', () => {
+ expect(memoryReportLink().attributes('href')).toEqual(
+ 'https://gitlab.com?performance_bar=memory',
+ );
+ });
});
it('sets the class to match the environment', () => {
diff --git a/spec/frontend/performance_bar/index_spec.js b/spec/frontend/performance_bar/index_spec.js
index f09b0cc3df8..1849c373326 100644
--- a/spec/frontend/performance_bar/index_spec.js
+++ b/spec/frontend/performance_bar/index_spec.js
@@ -20,9 +20,9 @@ describe('performance bar wrapper', () => {
peekWrapper.setAttribute('id', 'js-peek');
peekWrapper.dataset.env = 'development';
peekWrapper.dataset.requestId = '123';
+ peekWrapper.dataset.requestMethod = 'GET';
peekWrapper.dataset.peekUrl = '/-/peek/results';
peekWrapper.dataset.statsUrl = 'https://log.gprd.gitlab.net/app/dashboards#/view/';
- peekWrapper.dataset.profileUrl = '?lineprofiler=true';
mock = new MockAdapter(axios);
@@ -70,7 +70,13 @@ describe('performance bar wrapper', () => {
it('adds the request immediately', () => {
vm.addRequest('123', 'https://gitlab.com/');
- expect(vm.store.addRequest).toHaveBeenCalledWith('123', 'https://gitlab.com/', undefined);
+ expect(vm.store.addRequest).toHaveBeenCalledWith(
+ '123',
+ 'https://gitlab.com/',
+ undefined,
+ undefined,
+ undefined,
+ );
});
});
diff --git a/spec/frontend/performance_bar/services/performance_bar_service_spec.js b/spec/frontend/performance_bar/services/performance_bar_service_spec.js
index 1bb70a43a1b..b1f5f4d6982 100644
--- a/spec/frontend/performance_bar/services/performance_bar_service_spec.js
+++ b/spec/frontend/performance_bar/services/performance_bar_service_spec.js
@@ -66,7 +66,7 @@ describe('PerformanceBarService', () => {
describe('operationName', () => {
function requestUrl(response, peekUrl) {
- return PerformanceBarService.callbackParams(response, peekUrl)[3];
+ return PerformanceBarService.callbackParams(response, peekUrl)[4];
}
it('gets the operation name from response.config', () => {
diff --git a/spec/frontend/performance_bar/stores/performance_bar_store_spec.js b/spec/frontend/performance_bar/stores/performance_bar_store_spec.js
index 7d5c5031792..170469db6ad 100644
--- a/spec/frontend/performance_bar/stores/performance_bar_store_spec.js
+++ b/spec/frontend/performance_bar/stores/performance_bar_store_spec.js
@@ -46,6 +46,14 @@ describe('PerformanceBarStore', () => {
store.addRequest('id', 'http://localhost:3001/api/graphql', 'someOperation');
expect(findUrl('id')).toBe('graphql (someOperation)');
});
+
+ it('appends the number of batches queries when it is a GraphQL call', () => {
+ store.addRequest('id', 'http://localhost:3001/api/graphql', 'someOperation');
+ store.addRequest('id', 'http://localhost:3001/api/graphql', 'anotherOperation');
+ store.addRequest('id', 'http://localhost:3001/api/graphql', 'anotherOne');
+ store.addRequest('anotherId', 'http://localhost:3001/api/graphql', 'operationName');
+ expect(findUrl('id')).toBe('graphql (someOperation) [3 queries batched]');
+ });
});
describe('setRequestDetailsData', () => {
diff --git a/spec/frontend/pipeline_wizard/components/commit_spec.js b/spec/frontend/pipeline_wizard/components/commit_spec.js
index 8f44a6c085b..7095525e948 100644
--- a/spec/frontend/pipeline_wizard/components/commit_spec.js
+++ b/spec/frontend/pipeline_wizard/components/commit_spec.js
@@ -128,7 +128,7 @@ describe('Pipeline Wizard - Commit Page', () => {
await waitForPromises();
});
- it('will not show an error', async () => {
+ it('will not show an error', () => {
expect(wrapper.findByTestId('commit-error').exists()).not.toBe(true);
});
@@ -155,7 +155,7 @@ describe('Pipeline Wizard - Commit Page', () => {
await waitForPromises();
});
- it('will show an error', async () => {
+ it('will show an error', () => {
expect(wrapper.findByTestId('commit-error').exists()).toBe(true);
expect(wrapper.findByTestId('commit-error').text()).toBe(i18n.errors.commitError);
});
@@ -236,11 +236,11 @@ describe('Pipeline Wizard - Commit Page', () => {
await waitForPromises();
});
- it('sets up without error', async () => {
+ it('sets up without error', () => {
expect(consoleSpy).not.toHaveBeenCalled();
});
- it('does not show a load error', async () => {
+ it('does not show a load error', () => {
expect(wrapper.findByTestId('load-error').exists()).not.toBe(true);
});
diff --git a/spec/frontend/pipeline_wizard/components/step_nav_spec.js b/spec/frontend/pipeline_wizard/components/step_nav_spec.js
index 8e2f0ab0281..e80eb01ea7a 100644
--- a/spec/frontend/pipeline_wizard/components/step_nav_spec.js
+++ b/spec/frontend/pipeline_wizard/components/step_nav_spec.js
@@ -25,7 +25,7 @@ describe('Pipeline Wizard - Step Navigation Component', () => {
${'has prev, but not next'} | ${true} | ${false}
${'has next, but not prev'} | ${false} | ${true}
${'has both next and prev'} | ${true} | ${true}
- `('$scenario', async ({ showBackButton, showNextButton }) => {
+ `('$scenario', ({ showBackButton, showNextButton }) => {
createComponent({ showBackButton, showNextButton });
expect(prevButton.exists()).toBe(showBackButton);
@@ -53,13 +53,13 @@ describe('Pipeline Wizard - Step Navigation Component', () => {
expect(wrapper.emitted().next.length).toBe(1);
});
- it('enables the next button if nextButtonEnabled ist set to true', async () => {
+ it('enables the next button if nextButtonEnabled ist set to true', () => {
createComponent({ nextButtonEnabled: true });
expect(nextButton.attributes('disabled')).not.toBe('disabled');
});
- it('disables the next button if nextButtonEnabled ist set to false', async () => {
+ it('disables the next button if nextButtonEnabled ist set to false', () => {
createComponent({ nextButtonEnabled: false });
expect(nextButton.attributes('disabled')).toBe('disabled');
diff --git a/spec/frontend/pipeline_wizard/components/step_spec.js b/spec/frontend/pipeline_wizard/components/step_spec.js
index 00b57f95ccc..4d5f563228c 100644
--- a/spec/frontend/pipeline_wizard/components/step_spec.js
+++ b/spec/frontend/pipeline_wizard/components/step_spec.js
@@ -207,7 +207,7 @@ describe('Pipeline Wizard - Step Page', () => {
findInputWrappers();
});
- it('injects the template when an input wrapper emits a beforeUpdate:compiled event', async () => {
+ it('injects the template when an input wrapper emits a beforeUpdate:compiled event', () => {
input1.vm.$emit('beforeUpdate:compiled');
expect(wrapper.vm.compiled.toString()).toBe(compiledYamlAfterInitialLoad);
diff --git a/spec/frontend/pipeline_wizard/components/widgets/list_spec.js b/spec/frontend/pipeline_wizard/components/widgets/list_spec.js
index b0eb7279a94..df8841e6ad3 100644
--- a/spec/frontend/pipeline_wizard/components/widgets/list_spec.js
+++ b/spec/frontend/pipeline_wizard/components/widgets/list_spec.js
@@ -51,7 +51,7 @@ describe('Pipeline Wizard - List Widget', () => {
expect(findGlFormGroup().attributes('labeldescription')).toBe(defaultProps.description);
});
- it('sets the input field type attribute to "text"', async () => {
+ it('sets the input field type attribute to "text"', () => {
createComponent();
expect(findFirstGlFormInputGroup().attributes('type')).toBe('text');
@@ -164,7 +164,7 @@ describe('Pipeline Wizard - List Widget', () => {
});
describe('form validation', () => {
- it('does not show validation state when untouched', async () => {
+ it('does not show validation state when untouched', () => {
createComponent({}, mountExtended);
expect(findGlFormGroup().classes()).not.toContain('is-valid');
expect(findGlFormGroup().classes()).not.toContain('is-invalid');
diff --git a/spec/frontend/pipeline_wizard/components/widgets/text_spec.js b/spec/frontend/pipeline_wizard/components/widgets/text_spec.js
index a11c0214d15..abfb4a33c0f 100644
--- a/spec/frontend/pipeline_wizard/components/widgets/text_spec.js
+++ b/spec/frontend/pipeline_wizard/components/widgets/text_spec.js
@@ -123,7 +123,7 @@ describe('Pipeline Wizard - Text Widget', () => {
expect(findGlFormGroup().classes()).toContain('is-invalid');
});
- it('does not update validation if not required', async () => {
+ it('does not update validation if not required', () => {
createComponent({
pattern: null,
validate: true,
diff --git a/spec/frontend/pipeline_wizard/components/wrapper_spec.js b/spec/frontend/pipeline_wizard/components/wrapper_spec.js
index 1056602c912..2808fd0c7a5 100644
--- a/spec/frontend/pipeline_wizard/components/wrapper_spec.js
+++ b/spec/frontend/pipeline_wizard/components/wrapper_spec.js
@@ -82,7 +82,7 @@ describe('Pipeline Wizard - wrapper.vue', () => {
expect(wrapper.findByTestId('editor-header').text()).toBe(expectedMessage);
});
- it('shows the editor header with a custom filename', async () => {
+ it('shows the editor header with a custom filename', () => {
const filename = 'my-file.yml';
createComponent({
filename,
@@ -142,7 +142,7 @@ describe('Pipeline Wizard - wrapper.vue', () => {
});
if (expectCommitStepShown) {
- it('does not show the step wrapper', async () => {
+ it('does not show the step wrapper', () => {
expect(wrapper.findComponent(WizardStep).isVisible()).toBe(false);
});
@@ -150,7 +150,7 @@ describe('Pipeline Wizard - wrapper.vue', () => {
expect(wrapper.findComponent(CommitStep).isVisible()).toBe(true);
});
} else {
- it('passes the correct step config to the step component', async () => {
+ it('passes the correct step config to the step component', () => {
expect(getStepWrapper().props('inputs')).toMatchObject(expectStepDef.inputs);
});
@@ -250,7 +250,7 @@ describe('Pipeline Wizard - wrapper.vue', () => {
});
describe('integration test', () => {
- beforeEach(async () => {
+ beforeEach(() => {
createComponent({}, mountExtended);
});
diff --git a/spec/frontend/pipelines/components/dag/dag_spec.js b/spec/frontend/pipelines/components/dag/dag_spec.js
index e2dc8120309..5483c1c7b99 100644
--- a/spec/frontend/pipelines/components/dag/dag_spec.js
+++ b/spec/frontend/pipelines/components/dag/dag_spec.js
@@ -59,7 +59,7 @@ describe('Pipeline DAG graph wrapper', () => {
});
});
- it('does not render the graph', async () => {
+ it('does not render the graph', () => {
expect(getGraph().exists()).toBe(false);
});
@@ -70,7 +70,7 @@ describe('Pipeline DAG graph wrapper', () => {
describe('when all query variables are defined', () => {
describe('but the parse fails', () => {
- beforeEach(async () => {
+ beforeEach(() => {
createComponent({
graphData: unparseableGraph,
});
@@ -88,7 +88,7 @@ describe('Pipeline DAG graph wrapper', () => {
});
describe('parse succeeds', () => {
- beforeEach(async () => {
+ beforeEach(() => {
createComponent({ method: mount });
});
@@ -102,7 +102,7 @@ describe('Pipeline DAG graph wrapper', () => {
});
describe('parse succeeds, but the resulting graph is too small', () => {
- beforeEach(async () => {
+ beforeEach(() => {
createComponent({
graphData: tooSmallGraph,
});
@@ -120,7 +120,7 @@ describe('Pipeline DAG graph wrapper', () => {
});
describe('the returned data is empty', () => {
- beforeEach(async () => {
+ beforeEach(() => {
createComponent({
method: mount,
graphData: graphWithoutDependencies,
@@ -139,7 +139,7 @@ describe('Pipeline DAG graph wrapper', () => {
});
describe('annotations', () => {
- beforeEach(async () => {
+ beforeEach(() => {
createComponent();
});
diff --git a/spec/frontend/pipelines/components/jobs/jobs_app_spec.js b/spec/frontend/pipelines/components/jobs/jobs_app_spec.js
index 52df7b4500b..39475788fe2 100644
--- a/spec/frontend/pipelines/components/jobs/jobs_app_spec.js
+++ b/spec/frontend/pipelines/components/jobs/jobs_app_spec.js
@@ -34,7 +34,7 @@ describe('Jobs app', () => {
const createComponent = (resolver) => {
wrapper = shallowMount(JobsApp, {
provide: {
- fullPath: 'root/ci-project',
+ projectPath: 'root/ci-project',
pipelineIid: 1,
},
apolloProvider: createMockApolloProvider(resolver),
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 864f2d66f60..21d92fec9bf 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
@@ -129,7 +129,7 @@ describe('Pipelines stage component', () => {
await axios.waitForAll();
});
- it('renders the received data and emits the correct events', async () => {
+ it('renders the received data and emits the correct events', () => {
expect(findDropdownMenu().text()).toContain(stageReply.latest_statuses[0].name);
expect(findDropdownMenuTitle().text()).toContain(stageReply.name);
expect(eventHub.$emit).toHaveBeenCalledWith('clickedDropdown');
diff --git a/spec/frontend/pipelines/components/pipelines_list/empty_state/pipelines_ci_templates_spec.js b/spec/frontend/pipelines/components/pipelines_list/empty_state/pipelines_ci_templates_spec.js
index 0f4a2b1d02f..4bf4257f462 100644
--- a/spec/frontend/pipelines/components/pipelines_list/empty_state/pipelines_ci_templates_spec.js
+++ b/spec/frontend/pipelines/components/pipelines_list/empty_state/pipelines_ci_templates_spec.js
@@ -1,24 +1,10 @@
import '~/commons';
-import { GlButton, GlSprintf } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
-import { stubExperiments } from 'helpers/experimentation_helper';
-import GitlabExperiment from '~/experimentation/components/gitlab_experiment.vue';
-import ExperimentTracking from '~/experimentation/experiment_tracking';
import PipelinesCiTemplates from '~/pipelines/components/pipelines_list/empty_state/pipelines_ci_templates.vue';
import CiTemplates from '~/pipelines/components/pipelines_list/empty_state/ci_templates.vue';
-import {
- RUNNERS_AVAILABILITY_SECTION_EXPERIMENT_NAME,
- RUNNERS_SETTINGS_LINK_CLICKED_EVENT,
- RUNNERS_DOCUMENTATION_LINK_CLICKED_EVENT,
- RUNNERS_SETTINGS_BUTTON_CLICKED_EVENT,
- I18N,
-} from '~/ci/pipeline_editor/constants';
const pipelineEditorPath = '/-/ci/editor';
-const ciRunnerSettingsPath = '/-/settings/ci_cd';
-
-jest.mock('~/experimentation/experiment_tracking');
describe('Pipelines CI Templates', () => {
let wrapper;
@@ -28,8 +14,6 @@ describe('Pipelines CI Templates', () => {
return shallowMountExtended(PipelinesCiTemplates, {
provide: {
pipelineEditorPath,
- ciRunnerSettingsPath,
- anyRunnersAvailable: true,
...propsData,
},
stubs,
@@ -38,19 +22,17 @@ describe('Pipelines CI Templates', () => {
const findTestTemplateLink = () => wrapper.findByTestId('test-template-link');
const findCiTemplates = () => wrapper.findComponent(CiTemplates);
- const findSettingsLink = () => wrapper.findByTestId('settings-link');
- const findDocumentationLink = () => wrapper.findByTestId('documentation-link');
- const findSettingsButton = () => wrapper.findByTestId('settings-button');
- describe('renders test template', () => {
+ describe('templates', () => {
beforeEach(() => {
wrapper = createWrapper();
});
- it('links to the getting started template', () => {
+ it('renders test template and Ci templates', () => {
expect(findTestTemplateLink().attributes('href')).toBe(
pipelineEditorPath.concat('?template=Getting-Started'),
);
+ expect(findCiTemplates().exists()).toBe(true);
});
});
@@ -73,84 +55,4 @@ describe('Pipelines CI Templates', () => {
});
});
});
-
- describe('when the runners_availability_section experiment is active', () => {
- beforeEach(() => {
- stubExperiments({ runners_availability_section: 'candidate' });
- });
-
- describe('when runners are available', () => {
- beforeEach(() => {
- wrapper = createWrapper({ anyRunnersAvailable: true }, { GitlabExperiment, GlSprintf });
- });
-
- it('show the runners available section', () => {
- expect(wrapper.text()).toContain(I18N.runners.title);
- });
-
- it('tracks an event when clicking the settings link', () => {
- findSettingsLink().vm.$emit('click');
-
- expect(ExperimentTracking).toHaveBeenCalledWith(
- RUNNERS_AVAILABILITY_SECTION_EXPERIMENT_NAME,
- );
- expect(ExperimentTracking.prototype.event).toHaveBeenCalledWith(
- RUNNERS_SETTINGS_LINK_CLICKED_EVENT,
- );
- });
-
- it('tracks an event when clicking the documentation link', () => {
- findDocumentationLink().vm.$emit('click');
-
- expect(ExperimentTracking).toHaveBeenCalledWith(
- RUNNERS_AVAILABILITY_SECTION_EXPERIMENT_NAME,
- );
- expect(ExperimentTracking.prototype.event).toHaveBeenCalledWith(
- RUNNERS_DOCUMENTATION_LINK_CLICKED_EVENT,
- );
- });
- });
-
- describe('when runners are not available', () => {
- beforeEach(() => {
- wrapper = createWrapper({ anyRunnersAvailable: false }, { GitlabExperiment, GlButton });
- });
-
- it('show the no runners available section', () => {
- expect(wrapper.text()).toContain(I18N.noRunners.title);
- });
-
- it('tracks an event when clicking the settings button', () => {
- findSettingsButton().trigger('click');
-
- expect(ExperimentTracking).toHaveBeenCalledWith(
- RUNNERS_AVAILABILITY_SECTION_EXPERIMENT_NAME,
- );
- expect(ExperimentTracking.prototype.event).toHaveBeenCalledWith(
- RUNNERS_SETTINGS_BUTTON_CLICKED_EVENT,
- );
- });
- });
- });
-
- describe.each`
- experimentVariant | anyRunnersAvailable | templatesRendered
- ${'control'} | ${true} | ${true}
- ${'control'} | ${false} | ${true}
- ${'candidate'} | ${true} | ${true}
- ${'candidate'} | ${false} | ${false}
- `(
- 'when the runners_availability_section experiment variant is $experimentVariant and runners are available: $anyRunnersAvailable',
- ({ experimentVariant, anyRunnersAvailable, templatesRendered }) => {
- beforeEach(() => {
- stubExperiments({ runners_availability_section: experimentVariant });
- wrapper = createWrapper({ anyRunnersAvailable });
- });
-
- it(`renders the templates: ${templatesRendered}`, () => {
- expect(findTestTemplateLink().exists()).toBe(templatesRendered);
- expect(findCiTemplates().exists()).toBe(templatesRendered);
- });
- },
- );
});
diff --git a/spec/frontend/pipelines/graph/graph_component_wrapper_spec.js b/spec/frontend/pipelines/graph/graph_component_wrapper_spec.js
index 42e47a23db8..cc952eac1d7 100644
--- a/spec/frontend/pipelines/graph/graph_component_wrapper_spec.js
+++ b/spec/frontend/pipelines/graph/graph_component_wrapper_spec.js
@@ -1,10 +1,10 @@
-import { GlAlert, GlButton, GlButtonGroup, GlLoadingIcon } from '@gitlab/ui';
-import { mount, shallowMount } from '@vue/test-utils';
+import { GlAlert, GlButton, GlButtonGroup, GlLoadingIcon, GlToggle } from '@gitlab/ui';
import MockAdapter from 'axios-mock-adapter';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import { useLocalStorageSpy } from 'helpers/local_storage_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
+import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { stubPerformanceWebAPI } from 'helpers/performance';
import waitForPromises from 'helpers/wait_for_promises';
import getPipelineDetails from 'shared_queries/pipelines/get_pipeline_details.query.graphql';
@@ -48,23 +48,25 @@ describe('Pipeline graph wrapper', () => {
useLocalStorageSpy();
let wrapper;
- const getAlert = () => wrapper.findComponent(GlAlert);
- const getDependenciesToggle = () => wrapper.find('[data-testid="show-links-toggle"]');
- const getLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
- const getLinksLayer = () => wrapper.findComponent(LinksLayer);
- const getGraph = () => wrapper.findComponent(PipelineGraph);
- const getStageColumnTitle = () => wrapper.find('[data-testid="stage-column-title"]');
- const getAllStageColumnGroupsInColumn = () =>
+ let requestHandlers;
+ const findAlert = () => wrapper.findComponent(GlAlert);
+ const findDependenciesToggle = () => wrapper.findByTestId('show-links-toggle');
+ const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
+ const findLinksLayer = () => wrapper.findComponent(LinksLayer);
+ const findGraph = () => wrapper.findComponent(PipelineGraph);
+ const findStageColumnTitle = () => wrapper.findByTestId('stage-column-title');
+ const findAllStageColumnGroupsInColumn = () =>
wrapper.findComponent(StageColumnComponent).findAll('[data-testid="stage-column-group"]');
- const getViewSelector = () => wrapper.findComponent(GraphViewSelector);
- const getViewSelectorTrip = () => getViewSelector().findComponent(GlAlert);
+ const findViewSelector = () => wrapper.findComponent(GraphViewSelector);
+ const findViewSelectorToggle = () => findViewSelector().findComponent(GlToggle);
+ const findViewSelectorTrip = () => findViewSelector().findComponent(GlAlert);
const getLocalStorageSync = () => wrapper.findComponent(LocalStorageSync);
const createComponent = ({
apolloProvider,
data = {},
provide = {},
- mountFn = shallowMount,
+ mountFn = shallowMountExtended,
} = {}) => {
wrapper = mountFn(PipelineGraphWrapper, {
provide: {
@@ -84,37 +86,41 @@ describe('Pipeline graph wrapper', () => {
calloutsList = [],
data = {},
getPipelineDetailsHandler = jest.fn().mockResolvedValue(mockPipelineResponse),
- mountFn = shallowMount,
+ mountFn = shallowMountExtended,
provide = {},
} = {}) => {
const callouts = mapCallouts(calloutsList);
- const getUserCalloutsHandler = jest.fn().mockResolvedValue(mockCalloutsResponse(callouts));
- const getPipelineHeaderDataHandler = jest.fn().mockResolvedValue(mockRunningPipelineHeaderData);
- const requestHandlers = [
- [getPipelineHeaderData, getPipelineHeaderDataHandler],
- [getPipelineDetails, getPipelineDetailsHandler],
- [getUserCallouts, getUserCalloutsHandler],
+ requestHandlers = {
+ getUserCalloutsHandler: jest.fn().mockResolvedValue(mockCalloutsResponse(callouts)),
+ getPipelineHeaderDataHandler: jest.fn().mockResolvedValue(mockRunningPipelineHeaderData),
+ getPipelineDetailsHandler,
+ };
+
+ const handlers = [
+ [getPipelineHeaderData, requestHandlers.getPipelineHeaderDataHandler],
+ [getPipelineDetails, requestHandlers.getPipelineDetailsHandler],
+ [getUserCallouts, requestHandlers.getUserCalloutsHandler],
];
- const apolloProvider = createMockApollo(requestHandlers);
+ const apolloProvider = createMockApollo(handlers);
createComponent({ apolloProvider, data, provide, mountFn });
};
describe('when data is loading', () => {
it('displays the loading icon', () => {
createComponentWithApollo();
- expect(getLoadingIcon().exists()).toBe(true);
+ expect(findLoadingIcon().exists()).toBe(true);
});
it('does not display the alert', () => {
createComponentWithApollo();
- expect(getAlert().exists()).toBe(false);
+ expect(findAlert().exists()).toBe(false);
});
it('does not display the graph', () => {
createComponentWithApollo();
- expect(getGraph().exists()).toBe(false);
+ expect(findGraph().exists()).toBe(false);
});
it('skips querying headerPipeline', () => {
@@ -130,19 +136,19 @@ describe('Pipeline graph wrapper', () => {
});
it('does not display the loading icon', () => {
- expect(getLoadingIcon().exists()).toBe(false);
+ expect(findLoadingIcon().exists()).toBe(false);
});
it('does not display the alert', () => {
- expect(getAlert().exists()).toBe(false);
+ expect(findAlert().exists()).toBe(false);
});
it('displays the graph', () => {
- expect(getGraph().exists()).toBe(true);
+ expect(findGraph().exists()).toBe(true);
});
it('passes the etag resource and metrics path to the graph', () => {
- expect(getGraph().props('configPaths')).toMatchObject({
+ expect(findGraph().props('configPaths')).toMatchObject({
graphqlResourceEtag: defaultProvide.graphqlResourceEtag,
metricsPath: defaultProvide.metricsPath,
});
@@ -158,15 +164,15 @@ describe('Pipeline graph wrapper', () => {
});
it('does not display the loading icon', () => {
- expect(getLoadingIcon().exists()).toBe(false);
+ expect(findLoadingIcon().exists()).toBe(false);
});
it('displays the alert', () => {
- expect(getAlert().exists()).toBe(true);
+ expect(findAlert().exists()).toBe(true);
});
it('does not display the graph', () => {
- expect(getGraph().exists()).toBe(false);
+ expect(findGraph().exists()).toBe(false);
});
});
@@ -181,18 +187,18 @@ describe('Pipeline graph wrapper', () => {
});
it('does not display the loading icon', () => {
- expect(getLoadingIcon().exists()).toBe(false);
+ expect(findLoadingIcon().exists()).toBe(false);
});
it('displays the no iid alert', () => {
- expect(getAlert().exists()).toBe(true);
- expect(getAlert().text()).toBe(
+ expect(findAlert().exists()).toBe(true);
+ expect(findAlert().text()).toBe(
'The data in this pipeline is too old to be rendered as a graph. Please check the Jobs tab to access historical data.',
);
});
it('does not display the graph', () => {
- expect(getGraph().exists()).toBe(false);
+ expect(findGraph().exists()).toBe(false);
});
});
@@ -203,11 +209,11 @@ describe('Pipeline graph wrapper', () => {
});
describe('when receiving `setSkipRetryModal` event', () => {
it('passes down `skipRetryModal` value as true', async () => {
- expect(getGraph().props('skipRetryModal')).toBe(false);
+ expect(findGraph().props('skipRetryModal')).toBe(false);
- await getGraph().vm.$emit('setSkipRetryModal');
+ await findGraph().vm.$emit('setSkipRetryModal');
- expect(getGraph().props('skipRetryModal')).toBe(true);
+ expect(findGraph().props('skipRetryModal')).toBe(true);
});
});
});
@@ -216,36 +222,37 @@ describe('Pipeline graph wrapper', () => {
beforeEach(async () => {
createComponentWithApollo();
await waitForPromises();
- await getGraph().vm.$emit('error', { type: ACTION_FAILURE });
+ await findGraph().vm.$emit('error', { type: ACTION_FAILURE });
});
it('does not display the loading icon', () => {
- expect(getLoadingIcon().exists()).toBe(false);
+ expect(findLoadingIcon().exists()).toBe(false);
});
it('displays the action error alert', () => {
- expect(getAlert().exists()).toBe(true);
- expect(getAlert().text()).toBe('An error occurred while performing this action.');
+ expect(findAlert().exists()).toBe(true);
+ expect(findAlert().text()).toBe('An error occurred while performing this action.');
});
it('displays the graph', () => {
- expect(getGraph().exists()).toBe(true);
+ expect(findGraph().exists()).toBe(true);
});
});
describe('when refresh action is emitted', () => {
beforeEach(async () => {
createComponentWithApollo();
- jest.spyOn(wrapper.vm.$apollo.queries.headerPipeline, 'refetch');
- jest.spyOn(wrapper.vm.$apollo.queries.pipeline, 'refetch');
await waitForPromises();
- getGraph().vm.$emit('refreshPipelineGraph');
+ findGraph().vm.$emit('refreshPipelineGraph');
});
it('calls refetch', () => {
- expect(wrapper.vm.$apollo.queries.headerPipeline.skip).toBe(false);
- expect(wrapper.vm.$apollo.queries.headerPipeline.refetch).toHaveBeenCalled();
- expect(wrapper.vm.$apollo.queries.pipeline.refetch).toHaveBeenCalled();
+ expect(requestHandlers.getPipelineHeaderDataHandler).toHaveBeenCalledWith({
+ fullPath: 'frog/amphibirama',
+ iid: '22',
+ });
+ expect(requestHandlers.getPipelineDetailsHandler).toHaveBeenCalledTimes(2);
+ expect(requestHandlers.getUserCalloutsHandler).toHaveBeenCalledWith({});
});
});
@@ -277,18 +284,18 @@ describe('Pipeline graph wrapper', () => {
it('shows correct errors and does not overwrite populated data when data is empty', async () => {
/* fails at first, shows error, no data yet */
- expect(getAlert().exists()).toBe(true);
- expect(getGraph().exists()).toBe(false);
+ expect(findAlert().exists()).toBe(true);
+ expect(findGraph().exists()).toBe(false);
/* succeeds, clears error, shows graph */
await advanceApolloTimers();
- expect(getAlert().exists()).toBe(false);
- expect(getGraph().exists()).toBe(true);
+ expect(findAlert().exists()).toBe(false);
+ expect(findGraph().exists()).toBe(true);
/* fails again, alert returns but data persists */
await advanceApolloTimers();
- expect(getAlert().exists()).toBe(true);
- expect(getGraph().exists()).toBe(true);
+ expect(findAlert().exists()).toBe(true);
+ expect(findGraph().exists()).toBe(true);
});
});
@@ -298,38 +305,38 @@ describe('Pipeline graph wrapper', () => {
beforeEach(async () => {
layersFn = jest.spyOn(parsingUtils, 'listByLayers');
createComponentWithApollo({
- mountFn: mount,
+ mountFn: mountExtended,
});
await waitForPromises();
});
it('appears when pipeline uses needs', () => {
- expect(getViewSelector().exists()).toBe(true);
+ expect(findViewSelector().exists()).toBe(true);
});
it('switches between views', async () => {
const groupsInFirstColumn =
mockPipelineResponse.data.project.pipeline.stages.nodes[0].groups.nodes.length;
- expect(getAllStageColumnGroupsInColumn()).toHaveLength(groupsInFirstColumn);
- expect(getStageColumnTitle().text()).toBe('build');
- await getViewSelector().vm.$emit('updateViewType', LAYER_VIEW);
- expect(getAllStageColumnGroupsInColumn()).toHaveLength(groupsInFirstColumn + 1);
- expect(getStageColumnTitle().text()).toBe('');
+ expect(findAllStageColumnGroupsInColumn()).toHaveLength(groupsInFirstColumn);
+ expect(findStageColumnTitle().text()).toBe('build');
+ await findViewSelector().vm.$emit('updateViewType', LAYER_VIEW);
+ expect(findAllStageColumnGroupsInColumn()).toHaveLength(groupsInFirstColumn + 1);
+ expect(findStageColumnTitle().text()).toBe('');
});
it('saves the view type to local storage', async () => {
- await getViewSelector().vm.$emit('updateViewType', LAYER_VIEW);
+ await findViewSelector().vm.$emit('updateViewType', LAYER_VIEW);
expect(localStorage.setItem.mock.calls).toEqual([[VIEW_TYPE_KEY, LAYER_VIEW]]);
});
it('calls listByLayers only once no matter how many times view is switched', async () => {
expect(layersFn).not.toHaveBeenCalled();
- await getViewSelector().vm.$emit('updateViewType', LAYER_VIEW);
+ await findViewSelector().vm.$emit('updateViewType', LAYER_VIEW);
expect(layersFn).toHaveBeenCalledTimes(1);
- await getViewSelector().vm.$emit('updateViewType', STAGE_VIEW);
- await getViewSelector().vm.$emit('updateViewType', LAYER_VIEW);
- await getViewSelector().vm.$emit('updateViewType', STAGE_VIEW);
+ await findViewSelector().vm.$emit('updateViewType', STAGE_VIEW);
+ await findViewSelector().vm.$emit('updateViewType', LAYER_VIEW);
+ await findViewSelector().vm.$emit('updateViewType', STAGE_VIEW);
expect(layersFn).toHaveBeenCalledTimes(1);
});
});
@@ -340,7 +347,7 @@ describe('Pipeline graph wrapper', () => {
data: {
currentViewType: LAYER_VIEW,
},
- mountFn: mount,
+ mountFn: mountExtended,
});
jest.runOnlyPendingTimers();
@@ -349,10 +356,10 @@ describe('Pipeline graph wrapper', () => {
it('sets showLinks to true', async () => {
/* This spec uses .props for performance reasons. */
- expect(getLinksLayer().exists()).toBe(true);
- expect(getLinksLayer().props('showLinks')).toBe(false);
- expect(getViewSelector().props('type')).toBe(LAYER_VIEW);
- await getDependenciesToggle().vm.$emit('change', true);
+ expect(findLinksLayer().exists()).toBe(true);
+ expect(findLinksLayer().props('showLinks')).toBe(false);
+ expect(findViewSelector().props('type')).toBe(LAYER_VIEW);
+ await findDependenciesToggle().vm.$emit('change', true);
jest.runOnlyPendingTimers();
await waitForPromises();
@@ -367,15 +374,15 @@ describe('Pipeline graph wrapper', () => {
currentViewType: LAYER_VIEW,
showLinks: true,
},
- mountFn: mount,
+ mountFn: mountExtended,
});
await waitForPromises();
});
it('shows the hover tip in the view selector', async () => {
- await getViewSelector().setData({ showLinksActive: true });
- expect(getViewSelectorTrip().exists()).toBe(true);
+ await findViewSelectorToggle().vm.$emit('change', true);
+ expect(findViewSelectorTrip().exists()).toBe(true);
});
});
@@ -386,7 +393,7 @@ describe('Pipeline graph wrapper', () => {
currentViewType: LAYER_VIEW,
showLinks: true,
},
- mountFn: mount,
+ mountFn: mountExtended,
calloutsList: ['pipeline_needs_hover_tip'.toUpperCase()],
});
@@ -395,8 +402,8 @@ describe('Pipeline graph wrapper', () => {
});
it('does not show the hover tip', async () => {
- await getViewSelector().setData({ showLinksActive: true });
- expect(getViewSelectorTrip().exists()).toBe(false);
+ await findViewSelectorToggle().vm.$emit('change', true);
+ expect(findViewSelectorTrip().exists()).toBe(false);
});
});
@@ -405,7 +412,7 @@ describe('Pipeline graph wrapper', () => {
localStorage.setItem(VIEW_TYPE_KEY, LAYER_VIEW);
createComponentWithApollo({
- mountFn: mount,
+ mountFn: mountExtended,
});
await waitForPromises();
@@ -436,7 +443,7 @@ describe('Pipeline graph wrapper', () => {
localStorage.setItem(VIEW_TYPE_KEY, LAYER_VIEW);
createComponentWithApollo({
- mountFn: mount,
+ mountFn: mountExtended,
getPipelineDetailsHandler: jest.fn().mockResolvedValue(nonNeedsResponse),
});
@@ -448,7 +455,7 @@ describe('Pipeline graph wrapper', () => {
});
it('still passes stage type to graph', () => {
- expect(getGraph().props('viewType')).toBe(STAGE_VIEW);
+ expect(findGraph().props('viewType')).toBe(STAGE_VIEW);
});
});
@@ -458,7 +465,7 @@ describe('Pipeline graph wrapper', () => {
nonNeedsResponse.data.project.pipeline.usesNeeds = false;
createComponentWithApollo({
- mountFn: mount,
+ mountFn: mountExtended,
getPipelineDetailsHandler: jest.fn().mockResolvedValue(nonNeedsResponse),
});
@@ -467,7 +474,7 @@ describe('Pipeline graph wrapper', () => {
});
it('does not appear when pipeline does not use needs', () => {
- expect(getViewSelector().exists()).toBe(false);
+ expect(findViewSelector().exists()).toBe(false);
});
});
});
diff --git a/spec/frontend/pipelines/graph/graph_view_selector_spec.js b/spec/frontend/pipelines/graph/graph_view_selector_spec.js
index 78265165d1f..65ae9d19978 100644
--- a/spec/frontend/pipelines/graph/graph_view_selector_spec.js
+++ b/spec/frontend/pipelines/graph/graph_view_selector_spec.js
@@ -144,6 +144,7 @@ describe('the graph view selector component', () => {
createComponent({
props: {
showLinks: true,
+ type: LAYER_VIEW,
},
data: {
showLinksActive: true,
@@ -162,6 +163,18 @@ describe('the graph view selector component', () => {
await findHoverTip().find('button').trigger('click');
expect(wrapper.emitted().dismissHoverTip).toHaveLength(1);
});
+
+ it('is displayed at first then hidden on swith to STAGE_VIEW then displayed on switch to LAYER_VIEW', async () => {
+ expect(findHoverTip().exists()).toBe(true);
+ expect(findHoverTip().text()).toBe(wrapper.vm.$options.i18n.hoverTipText);
+
+ await findStageViewButton().trigger('click');
+ expect(findHoverTip().exists()).toBe(false);
+
+ await findLayerViewButton().trigger('click');
+ expect(findHoverTip().exists()).toBe(true);
+ expect(findHoverTip().text()).toBe(wrapper.vm.$options.i18n.hoverTipText);
+ });
});
describe('when links are live and it has been previously dismissed', () => {
@@ -170,6 +183,7 @@ describe('the graph view selector component', () => {
props: {
showLinks: true,
tipPreviouslyDismissed: true,
+ type: LAYER_VIEW,
},
data: {
showLinksActive: true,
@@ -187,6 +201,7 @@ describe('the graph view selector component', () => {
createComponent({
props: {
showLinks: true,
+ type: LAYER_VIEW,
},
data: {
showLinksActive: false,
diff --git a/spec/frontend/pipelines/graph/linked_pipeline_spec.js b/spec/frontend/pipelines/graph/linked_pipeline_spec.js
index b5ef10dee12..bf92cd585d9 100644
--- a/spec/frontend/pipelines/graph/linked_pipeline_spec.js
+++ b/spec/frontend/pipelines/graph/linked_pipeline_spec.js
@@ -1,11 +1,10 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import { GlButton, GlLoadingIcon, GlTooltip } from '@gitlab/ui';
-import { mount } from '@vue/test-utils';
+import { createWrapper } 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 { mountExtended } from 'helpers/vue_test_utils_helper';
+import waitForPromises from 'helpers/wait_for_promises';
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';
@@ -14,10 +13,9 @@ import RetryPipelineMutation from '~/pipelines/graphql/mutations/retry_pipeline.
import CiStatus from '~/vue_shared/components/ci_icon.vue';
import mockPipeline from './linked_pipelines_mock_data';
-Vue.use(VueApollo);
-
describe('Linked pipeline', () => {
let wrapper;
+ let requestHandlers;
const downstreamProps = {
pipeline: {
@@ -47,15 +45,28 @@ describe('Linked pipeline', () => {
const findPipelineLink = () => wrapper.findByTestId('pipelineLink');
const findRetryButton = () => wrapper.findByLabelText('Retry downstream pipeline');
- const createWrapper = ({ propsData }) => {
- const mockApollo = createMockApollo();
+ const defaultHandlers = {
+ cancelPipeline: jest.fn().mockResolvedValue({ data: { pipelineCancel: { errors: [] } } }),
+ retryPipeline: jest.fn().mockResolvedValue({ data: { pipelineRetry: { errors: [] } } }),
+ };
- wrapper = extendedWrapper(
- mount(LinkedPipelineComponent, {
- propsData,
- apolloProvider: mockApollo,
- }),
- );
+ const createMockApolloProvider = (handlers) => {
+ Vue.use(VueApollo);
+
+ requestHandlers = handlers;
+ return createMockApollo([
+ [CancelPipelineMutation, requestHandlers.cancelPipeline],
+ [RetryPipelineMutation, requestHandlers.retryPipeline],
+ ]);
+ };
+
+ const createComponent = ({ propsData, handlers = defaultHandlers }) => {
+ const mockApollo = createMockApolloProvider(handlers);
+
+ wrapper = mountExtended(LinkedPipelineComponent, {
+ propsData,
+ apolloProvider: mockApollo,
+ });
};
describe('rendered output', () => {
@@ -68,7 +79,7 @@ describe('Linked pipeline', () => {
};
beforeEach(() => {
- createWrapper({ propsData: props });
+ createComponent({ propsData: props });
});
it('should render the project name', () => {
@@ -109,7 +120,7 @@ describe('Linked pipeline', () => {
describe('upstream pipelines', () => {
beforeEach(() => {
- createWrapper({ propsData: upstreamProps });
+ createComponent({ propsData: upstreamProps });
});
it('should display parent label when pipeline project id is the same as triggered_by pipeline project id', () => {
@@ -129,7 +140,7 @@ describe('Linked pipeline', () => {
describe('downstream pipelines', () => {
describe('styling', () => {
beforeEach(() => {
- createWrapper({ propsData: downstreamProps });
+ createComponent({ propsData: downstreamProps });
});
it('parent/child label container should exist', () => {
@@ -164,7 +175,7 @@ describe('Linked pipeline', () => {
pipeline: { ...mockPipeline, retryable: true },
};
- createWrapper({ propsData: retryablePipeline });
+ createComponent({ propsData: retryablePipeline });
});
it('does not show the retry or cancel button', () => {
@@ -175,14 +186,14 @@ describe('Linked pipeline', () => {
});
describe('on a downstream', () => {
+ const retryablePipeline = {
+ ...downstreamProps,
+ pipeline: { ...mockPipeline, retryable: true },
+ };
+
describe('when retryable', () => {
beforeEach(() => {
- const retryablePipeline = {
- ...downstreamProps,
- pipeline: { ...mockPipeline, retryable: true },
- };
-
- createWrapper({ propsData: retryablePipeline });
+ createComponent({ propsData: retryablePipeline });
});
it('shows only the retry button', () => {
@@ -205,50 +216,51 @@ describe('Linked pipeline', () => {
describe('and the retry button is clicked', () => {
describe('on success', () => {
beforeEach(async () => {
- jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue();
- jest.spyOn(wrapper.vm, '$emit');
await findRetryButton().trigger('click');
});
it('calls the retry mutation', () => {
- expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledTimes(1);
- expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({
- mutation: RetryPipelineMutation,
- variables: {
- id: convertToGraphQLId(TYPENAME_CI_PIPELINE, mockPipeline.id),
- },
+ expect(requestHandlers.retryPipeline).toHaveBeenCalledTimes(1);
+ expect(requestHandlers.retryPipeline).toHaveBeenCalledWith({
+ id: 'gid://gitlab/Ci::Pipeline/195',
});
});
- it('emits the refreshPipelineGraph event', () => {
- expect(wrapper.vm.$emit).toHaveBeenCalledWith('refreshPipelineGraph');
+ it('emits the refreshPipelineGraph event', async () => {
+ await waitForPromises();
+ expect(wrapper.emitted('refreshPipelineGraph')).toHaveLength(1);
});
});
describe('on failure', () => {
beforeEach(async () => {
- jest.spyOn(wrapper.vm.$apollo, 'mutate').mockRejectedValue({ errors: [] });
- jest.spyOn(wrapper.vm, '$emit');
+ createComponent({
+ propsData: retryablePipeline,
+ handlers: {
+ retryPipeline: jest.fn().mockRejectedValue({ errors: [] }),
+ cancelPipeline: jest.fn().mockRejectedValue({ errors: [] }),
+ },
+ });
+
await findRetryButton().trigger('click');
});
- it('emits an error event', () => {
- expect(wrapper.vm.$emit).toHaveBeenCalledWith('error', {
- type: ACTION_FAILURE,
- });
+ it('emits an error event', async () => {
+ await waitForPromises();
+ expect(wrapper.emitted('error')).toEqual([[{ type: ACTION_FAILURE }]]);
});
});
});
});
describe('when cancelable', () => {
- beforeEach(() => {
- const cancelablePipeline = {
- ...downstreamProps,
- pipeline: { ...mockPipeline, cancelable: true },
- };
+ const cancelablePipeline = {
+ ...downstreamProps,
+ pipeline: { ...mockPipeline, cancelable: true },
+ };
- createWrapper({ propsData: cancelablePipeline });
+ beforeEach(() => {
+ createComponent({ propsData: cancelablePipeline });
});
it('shows only the cancel button', () => {
@@ -271,34 +283,37 @@ describe('Linked pipeline', () => {
describe('and the cancel button is clicked', () => {
describe('on success', () => {
beforeEach(async () => {
- jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue();
- jest.spyOn(wrapper.vm, '$emit');
await findCancelButton().trigger('click');
});
it('calls the cancel mutation', () => {
- expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledTimes(1);
- expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({
- mutation: CancelPipelineMutation,
- variables: {
- id: convertToGraphQLId(TYPENAME_CI_PIPELINE, mockPipeline.id),
- },
+ expect(requestHandlers.cancelPipeline).toHaveBeenCalledTimes(1);
+ expect(requestHandlers.cancelPipeline).toHaveBeenCalledWith({
+ id: 'gid://gitlab/Ci::Pipeline/195',
});
});
- it('emits the refreshPipelineGraph event', () => {
- expect(wrapper.vm.$emit).toHaveBeenCalledWith('refreshPipelineGraph');
+ it('emits the refreshPipelineGraph event', async () => {
+ await waitForPromises();
+ expect(wrapper.emitted('refreshPipelineGraph')).toHaveLength(1);
});
});
+
describe('on failure', () => {
beforeEach(async () => {
- jest.spyOn(wrapper.vm.$apollo, 'mutate').mockRejectedValue({ errors: [] });
- jest.spyOn(wrapper.vm, '$emit');
+ createComponent({
+ propsData: cancelablePipeline,
+ handlers: {
+ retryPipeline: jest.fn().mockRejectedValue({ errors: [] }),
+ cancelPipeline: jest.fn().mockRejectedValue({ errors: [] }),
+ },
+ });
+
await findCancelButton().trigger('click');
});
- it('emits an error event', () => {
- expect(wrapper.vm.$emit).toHaveBeenCalledWith('error', {
- type: ACTION_FAILURE,
- });
+
+ it('emits an error event', async () => {
+ await waitForPromises();
+ expect(wrapper.emitted('error')).toEqual([[{ type: ACTION_FAILURE }]]);
});
});
});
@@ -311,7 +326,7 @@ describe('Linked pipeline', () => {
pipeline: { ...mockPipeline, cancelable: true, retryable: true },
};
- createWrapper({ propsData: pipelineWithTwoActions });
+ createComponent({ propsData: pipelineWithTwoActions });
});
it('only shows the cancel button', () => {
@@ -334,7 +349,7 @@ describe('Linked pipeline', () => {
},
};
- createWrapper({ propsData: pipelineWithTwoActions });
+ createComponent({ propsData: pipelineWithTwoActions });
});
it('does not show any action button', () => {
@@ -355,7 +370,7 @@ describe('Linked pipeline', () => {
`(
'$pipelineType.columnTitle pipeline button icon should be $chevronPosition with $buttonBorderClasses if expanded state is $expanded',
({ pipelineType, chevronPosition, buttonBorderClasses, expanded }) => {
- createWrapper({ propsData: { ...pipelineType, expanded } });
+ createComponent({ propsData: { ...pipelineType, expanded } });
expect(findExpandButton().props('icon')).toBe(chevronPosition);
expect(findExpandButton().classes()).toContain(buttonBorderClasses);
},
@@ -363,7 +378,7 @@ describe('Linked pipeline', () => {
describe('shadow border', () => {
beforeEach(() => {
- createWrapper({ propsData: downstreamProps });
+ createComponent({ propsData: downstreamProps });
});
it.each`
@@ -397,7 +412,7 @@ describe('Linked pipeline', () => {
};
beforeEach(() => {
- createWrapper({ propsData: props });
+ createComponent({ propsData: props });
});
it('loading icon is visible', () => {
@@ -415,36 +430,35 @@ describe('Linked pipeline', () => {
};
beforeEach(() => {
- createWrapper({ propsData: props });
+ createComponent({ propsData: props });
});
it('emits `pipelineClicked` event', () => {
- jest.spyOn(wrapper.vm, '$emit');
findButton().trigger('click');
- expect(wrapper.emitted().pipelineClicked).toHaveLength(1);
+ expect(wrapper.emitted('pipelineClicked')).toHaveLength(1);
});
- it(`should emit ${BV_HIDE_TOOLTIP} to close the tooltip`, () => {
- jest.spyOn(wrapper.vm.$root, '$emit');
- findButton().trigger('click');
+ it(`should emit ${BV_HIDE_TOOLTIP} to close the tooltip`, async () => {
+ const root = createWrapper(wrapper.vm.$root);
+ await findButton().vm.$emit('click');
- expect(wrapper.vm.$root.$emit.mock.calls[0]).toEqual([BV_HIDE_TOOLTIP]);
+ expect(root.emitted(BV_HIDE_TOOLTIP)).toHaveLength(1);
});
it('should emit downstreamHovered with job name on mouseover', () => {
findLinkedPipeline().trigger('mouseover');
- expect(wrapper.emitted().downstreamHovered).toStrictEqual([['test_c']]);
+ expect(wrapper.emitted('downstreamHovered')).toStrictEqual([['test_c']]);
});
it('should emit downstreamHovered with empty string on mouseleave', () => {
findLinkedPipeline().trigger('mouseleave');
- expect(wrapper.emitted().downstreamHovered).toStrictEqual([['']]);
+ expect(wrapper.emitted('downstreamHovered')).toStrictEqual([['']]);
});
it('should emit pipelineExpanded with job name and expanded state on click', () => {
findExpandButton().trigger('click');
- expect(wrapper.emitted().pipelineExpandToggle).toStrictEqual([['test_c', true]]);
+ expect(wrapper.emitted('pipelineExpandToggle')).toStrictEqual([['test_c', true]]);
});
});
});
diff --git a/spec/frontend/pipelines/pipeline_operations_spec.js b/spec/frontend/pipelines/pipeline_operations_spec.js
new file mode 100644
index 00000000000..b2191453824
--- /dev/null
+++ b/spec/frontend/pipelines/pipeline_operations_spec.js
@@ -0,0 +1,77 @@
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import PipelinesManualActions from '~/pipelines/components/pipelines_list/pipelines_manual_actions.vue';
+import PipelineMultiActions from '~/pipelines/components/pipelines_list/pipeline_multi_actions.vue';
+import PipelineOperations from '~/pipelines/components/pipelines_list/pipeline_operations.vue';
+import eventHub from '~/pipelines/event_hub';
+
+describe('Pipeline operations', () => {
+ let wrapper;
+
+ const defaultProps = {
+ pipeline: {
+ id: 329,
+ iid: 234,
+ details: {
+ has_manual_actions: true,
+ has_scheduled_actions: false,
+ },
+ flags: {
+ retryable: true,
+ cancelable: true,
+ },
+ cancel_path: '/root/ci-project/-/pipelines/329/cancel',
+ retry_path: '/root/ci-project/-/pipelines/329/retry',
+ },
+ };
+
+ const createComponent = (props = defaultProps) => {
+ wrapper = shallowMountExtended(PipelineOperations, {
+ propsData: {
+ ...props,
+ },
+ });
+ };
+
+ const findManualActions = () => wrapper.findComponent(PipelinesManualActions);
+ const findMultiActions = () => wrapper.findComponent(PipelineMultiActions);
+ const findRetryBtn = () => wrapper.findByTestId('pipelines-retry-button');
+ const findCancelBtn = () => wrapper.findByTestId('pipelines-cancel-button');
+
+ it('should display pipeline manual actions', () => {
+ createComponent();
+
+ expect(findManualActions().exists()).toBe(true);
+ });
+
+ it('should display pipeline multi actions', () => {
+ createComponent();
+
+ expect(findMultiActions().exists()).toBe(true);
+ });
+
+ describe('events', () => {
+ beforeEach(() => {
+ createComponent();
+
+ jest.spyOn(eventHub, '$emit').mockImplementation(() => {});
+ });
+
+ it('should emit retryPipeline event', () => {
+ findRetryBtn().vm.$emit('click');
+
+ expect(eventHub.$emit).toHaveBeenCalledWith(
+ 'retryPipeline',
+ defaultProps.pipeline.retry_path,
+ );
+ });
+
+ it('should emit openConfirmationModal event', () => {
+ findCancelBtn().vm.$emit('click');
+
+ expect(eventHub.$emit).toHaveBeenCalledWith('openConfirmationModal', {
+ pipeline: defaultProps.pipeline,
+ endpoint: defaultProps.pipeline.cancel_path,
+ });
+ });
+ });
+});
diff --git a/spec/frontend/pipelines/pipelines_actions_spec.js b/spec/frontend/pipelines/pipelines_actions_spec.js
deleted file mode 100644
index 2db9f5c2a83..00000000000
--- a/spec/frontend/pipelines/pipelines_actions_spec.js
+++ /dev/null
@@ -1,168 +0,0 @@
-import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
-import MockAdapter from 'axios-mock-adapter';
-import { nextTick } from 'vue';
-import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
-import waitForPromises from 'helpers/wait_for_promises';
-import { TEST_HOST } from 'spec/test_constants';
-import { createAlert } from '~/alert';
-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';
-import { TRACKING_CATEGORIES } from '~/pipelines/constants';
-
-jest.mock('~/alert');
-jest.mock('~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal');
-
-describe('Pipelines Actions dropdown', () => {
- let wrapper;
- let mock;
-
- const createComponent = (props, mountFn = shallowMount) => {
- wrapper = mountFn(PipelinesManualActions, {
- propsData: {
- ...props,
- },
- });
- };
-
- const findDropdown = () => wrapper.findComponent(GlDropdown);
- const findAllDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
- const findAllCountdowns = () => wrapper.findAllComponents(GlCountdown);
-
- beforeEach(() => {
- mock = new MockAdapter(axios);
- });
-
- afterEach(() => {
- mock.restore();
- confirmAction.mockReset();
- });
-
- describe('manual actions', () => {
- const mockActions = [
- {
- name: 'stop_review',
- path: `${TEST_HOST}/root/review-app/builds/1893/play`,
- },
- {
- name: 'foo',
- path: `${TEST_HOST}/disabled/pipeline/action`,
- playable: false,
- },
- ];
-
- beforeEach(() => {
- createComponent({ actions: mockActions });
- });
-
- it('renders a dropdown with the provided actions', () => {
- expect(findAllDropdownItems()).toHaveLength(mockActions.length);
- });
-
- it("renders a disabled action when it's not playable", () => {
- expect(findAllDropdownItems().at(1).attributes('disabled')).toBe('true');
- });
-
- describe('on click', () => {
- it('makes a request and toggles the loading state', async () => {
- mock.onPost(mockActions.path).reply(HTTP_STATUS_OK);
-
- findAllDropdownItems().at(0).vm.$emit('click');
-
- await nextTick();
- expect(findDropdown().props('loading')).toBe(true);
-
- await waitForPromises();
- expect(findDropdown().props('loading')).toBe(false);
- });
-
- it('makes a failed request and toggles the loading state', async () => {
- mock.onPost(mockActions.path).reply(HTTP_STATUS_INTERNAL_SERVER_ERROR);
-
- findAllDropdownItems().at(0).vm.$emit('click');
-
- await nextTick();
- expect(findDropdown().props('loading')).toBe(true);
-
- await waitForPromises();
- expect(findDropdown().props('loading')).toBe(false);
- expect(createAlert).toHaveBeenCalledTimes(1);
- });
- });
-
- describe('tracking', () => {
- afterEach(() => {
- unmockTracking();
- });
-
- it('tracks manual actions click', () => {
- const trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
-
- findDropdown().vm.$emit('shown');
-
- expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_manual_actions', {
- label: TRACKING_CATEGORIES.table,
- });
- });
- });
- });
-
- describe('scheduled jobs', () => {
- const scheduledJobAction = {
- name: 'scheduled action',
- path: `${TEST_HOST}/scheduled/job/action`,
- playable: true,
- scheduled_at: '2063-04-05T00:42:00Z',
- };
- const expiredJobAction = {
- name: 'expired action',
- path: `${TEST_HOST}/expired/job/action`,
- playable: true,
- scheduled_at: '2018-10-05T08:23:00Z',
- };
-
- beforeEach(() => {
- jest.spyOn(Date, 'now').mockImplementation(() => new Date('2063-04-04T00:42:00Z').getTime());
- createComponent({ actions: [scheduledJobAction, expiredJobAction] });
- });
-
- it('makes post request after confirming', async () => {
- mock.onPost(scheduledJobAction.path).reply(HTTP_STATUS_OK);
- confirmAction.mockResolvedValueOnce(true);
-
- findAllDropdownItems().at(0).vm.$emit('click');
-
- expect(confirmAction).toHaveBeenCalled();
-
- await waitForPromises();
-
- expect(mock.history.post).toHaveLength(1);
- });
-
- it('does not make post request if confirmation is cancelled', async () => {
- mock.onPost(scheduledJobAction.path).reply(HTTP_STATUS_OK);
- confirmAction.mockResolvedValueOnce(false);
-
- findAllDropdownItems().at(0).vm.$emit('click');
-
- expect(confirmAction).toHaveBeenCalled();
-
- await waitForPromises();
-
- expect(mock.history.post).toHaveLength(0);
- });
-
- it('displays the remaining time in the dropdown', () => {
- expect(findAllCountdowns().at(0).props('endDateString')).toBe(
- scheduledJobAction.scheduled_at,
- );
- });
-
- it('displays 00:00:00 for expired jobs in the dropdown', () => {
- expect(findAllCountdowns().at(1).props('endDateString')).toBe(expiredJobAction.scheduled_at);
- });
- });
-});
diff --git a/spec/frontend/pipelines/pipelines_manual_actions_spec.js b/spec/frontend/pipelines/pipelines_manual_actions_spec.js
new file mode 100644
index 00000000000..e47e57db887
--- /dev/null
+++ b/spec/frontend/pipelines/pipelines_manual_actions_spec.js
@@ -0,0 +1,216 @@
+import { GlDropdown, GlDropdownItem, GlLoadingIcon } from '@gitlab/ui';
+import MockAdapter from 'axios-mock-adapter';
+import Vue, { nextTick } from 'vue';
+import VueApollo from 'vue-apollo';
+import mockPipelineActionsQueryResponse from 'test_fixtures/graphql/pipelines/get_pipeline_actions.query.graphql.json';
+import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import { createAlert } from '~/alert';
+import 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 getPipelineActionsQuery from '~/pipelines/graphql/queries/get_pipeline_actions.query.graphql';
+import { TRACKING_CATEGORIES } from '~/pipelines/constants';
+import GlCountdown from '~/vue_shared/components/gl_countdown.vue';
+
+Vue.use(VueApollo);
+
+jest.mock('~/alert');
+jest.mock('~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal');
+
+describe('Pipeline manual actions', () => {
+ let wrapper;
+ let mock;
+
+ const queryHandler = jest.fn().mockResolvedValue(mockPipelineActionsQueryResponse);
+ const {
+ data: {
+ project: {
+ pipeline: {
+ jobs: { nodes },
+ },
+ },
+ },
+ } = mockPipelineActionsQueryResponse;
+
+ const mockPath = nodes[2].playPath;
+
+ const createComponent = (limit = 50) => {
+ wrapper = shallowMountExtended(PipelinesManualActions, {
+ provide: {
+ fullPath: 'root/ci-project',
+ manualActionsLimit: limit,
+ },
+ propsData: {
+ iid: 100,
+ },
+ stubs: {
+ GlDropdown,
+ },
+ apolloProvider: createMockApollo([[getPipelineActionsQuery, queryHandler]]),
+ });
+ };
+
+ const findDropdown = () => wrapper.findComponent(GlDropdown);
+ const findAllDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
+ const findAllCountdowns = () => wrapper.findAllComponents(GlCountdown);
+ const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
+ const findLimitMessage = () => wrapper.findByTestId('limit-reached-msg');
+
+ it('skips calling query on mount', () => {
+ createComponent();
+
+ expect(queryHandler).not.toHaveBeenCalled();
+ });
+
+ describe('loading', () => {
+ beforeEach(() => {
+ createComponent();
+
+ findDropdown().vm.$emit('shown');
+ });
+
+ it('display loading state while actions are being fetched', () => {
+ expect(findAllDropdownItems().at(0).text()).toBe('Loading...');
+ expect(findLoadingIcon().exists()).toBe(true);
+ expect(findAllDropdownItems()).toHaveLength(1);
+ });
+ });
+
+ describe('loaded', () => {
+ beforeEach(async () => {
+ mock = new MockAdapter(axios);
+
+ createComponent();
+
+ findDropdown().vm.$emit('shown');
+
+ await waitForPromises();
+ });
+
+ afterEach(() => {
+ mock.restore();
+ confirmAction.mockReset();
+ });
+
+ it('displays dropdown with the provided actions', () => {
+ expect(findAllDropdownItems()).toHaveLength(3);
+ });
+
+ it("displays a disabled action when it's not playable", () => {
+ expect(findAllDropdownItems().at(0).attributes('disabled')).toBe('true');
+ });
+
+ describe('on action click', () => {
+ it('makes a request and toggles the loading state', async () => {
+ mock.onPost(mockPath).reply(HTTP_STATUS_OK);
+
+ findAllDropdownItems().at(1).vm.$emit('click');
+
+ await nextTick();
+
+ expect(findDropdown().props('loading')).toBe(true);
+
+ await waitForPromises();
+
+ expect(findDropdown().props('loading')).toBe(false);
+ });
+
+ it('makes a failed request and toggles the loading state', async () => {
+ mock.onPost(mockPath).reply(HTTP_STATUS_INTERNAL_SERVER_ERROR);
+
+ findAllDropdownItems().at(1).vm.$emit('click');
+
+ await nextTick();
+
+ expect(findDropdown().props('loading')).toBe(true);
+
+ await waitForPromises();
+
+ expect(findDropdown().props('loading')).toBe(false);
+ expect(createAlert).toHaveBeenCalledTimes(1);
+ });
+ });
+
+ describe('tracking', () => {
+ afterEach(() => {
+ unmockTracking();
+ });
+
+ it('tracks manual actions click', () => {
+ const trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
+
+ findDropdown().vm.$emit('shown');
+
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_manual_actions', {
+ label: TRACKING_CATEGORIES.table,
+ });
+ });
+ });
+
+ describe('scheduled jobs', () => {
+ beforeEach(() => {
+ jest
+ .spyOn(Date, 'now')
+ .mockImplementation(() => new Date('2063-04-04T00:42:00Z').getTime());
+ });
+
+ it('makes post request after confirming', async () => {
+ mock.onPost(mockPath).reply(HTTP_STATUS_OK);
+
+ confirmAction.mockResolvedValueOnce(true);
+
+ findAllDropdownItems().at(2).vm.$emit('click');
+
+ expect(confirmAction).toHaveBeenCalled();
+
+ await waitForPromises();
+
+ expect(mock.history.post).toHaveLength(1);
+ });
+
+ it('does not make post request if confirmation is cancelled', async () => {
+ mock.onPost(mockPath).reply(HTTP_STATUS_OK);
+
+ confirmAction.mockResolvedValueOnce(false);
+
+ findAllDropdownItems().at(2).vm.$emit('click');
+
+ expect(confirmAction).toHaveBeenCalled();
+
+ await waitForPromises();
+
+ expect(mock.history.post).toHaveLength(0);
+ });
+
+ it('displays the remaining time in the dropdown', () => {
+ expect(findAllCountdowns().at(0).props('endDateString')).toBe(nodes[2].scheduledAt);
+ });
+ });
+ });
+
+ describe('limit message', () => {
+ it('limit message does not show', async () => {
+ createComponent();
+
+ findDropdown().vm.$emit('shown');
+
+ await waitForPromises();
+
+ expect(findLimitMessage().exists()).toBe(false);
+ });
+
+ it('limit message does show', async () => {
+ createComponent(3);
+
+ findDropdown().vm.$emit('shown');
+
+ await waitForPromises();
+
+ expect(findLimitMessage().exists()).toBe(true);
+ });
+ });
+});
diff --git a/spec/frontend/pipelines/pipelines_spec.js b/spec/frontend/pipelines/pipelines_spec.js
index 48539d84024..44f345fdf4e 100644
--- a/spec/frontend/pipelines/pipelines_spec.js
+++ b/spec/frontend/pipelines/pipelines_spec.js
@@ -245,7 +245,7 @@ describe('Pipelines', () => {
await waitForPromises();
});
- it('should filter pipelines', async () => {
+ it('should filter pipelines', () => {
expect(findPipelinesTable().exists()).toBe(true);
expect(findPipelineUrlLinks()).toHaveLength(1);
@@ -287,7 +287,7 @@ describe('Pipelines', () => {
await waitForPromises();
});
- it('should filter pipelines', async () => {
+ it('should filter pipelines', () => {
expect(findEmptyState().text()).toBe('There are currently no pipelines.');
});
@@ -330,11 +330,11 @@ describe('Pipelines', () => {
await waitForPromises();
});
- it('requests data with query params on filter submit', async () => {
+ it('requests data with query params on filter submit', () => {
expect(mock.history.get[1].params).toEqual(expectedParams);
});
- it('renders filtered pipelines', async () => {
+ it('renders filtered pipelines', () => {
expect(findPipelineUrlLinks()).toHaveLength(1);
expect(findPipelineUrlLinks().at(0).text()).toBe(`#${mockFilteredPipeline.id}`);
});
@@ -356,7 +356,7 @@ describe('Pipelines', () => {
await waitForPromises();
});
- it('requests data with query params on filter submit', async () => {
+ it('requests data with query params on filter submit', () => {
expect(mock.history.get[1].params).toEqual({ page: '1', scope: 'all' });
});
@@ -516,7 +516,7 @@ describe('Pipelines', () => {
expect(findNavigationTabs().exists()).toBe(true);
});
- it('is loading after a time', async () => {
+ it('is loading after a time', () => {
expect(findPipelineUrlLinks()).toHaveLength(mockPipelinesIds.length);
expect(findPipelineUrlLinks().at(0).text()).toBe(`#${mockPipelinesIds[0]}`);
expect(findPipelineUrlLinks().at(1).text()).toBe(`#${mockPipelinesIds[1]}`);
@@ -727,7 +727,7 @@ describe('Pipelines', () => {
});
describe('when pipelines cannot be loaded', () => {
- beforeEach(async () => {
+ beforeEach(() => {
mock.onGet(mockPipelinesEndpoint).reply(HTTP_STATUS_INTERNAL_SERVER_ERROR, {});
});
diff --git a/spec/frontend/pipelines/test_reports/stores/mutations_spec.js b/spec/frontend/pipelines/test_reports/stores/mutations_spec.js
index 9c374ea817a..685ac6ea3e5 100644
--- a/spec/frontend/pipelines/test_reports/stores/mutations_spec.js
+++ b/spec/frontend/pipelines/test_reports/stores/mutations_spec.js
@@ -58,7 +58,7 @@ describe('Mutations TestReports Store', () => {
expect(mockState.errorMessage).toBe(message);
});
- it('should show an alert message otherwise', () => {
+ it('should show an alert otherwise', () => {
mutations[types.SET_SUITE_ERROR](mockState, {});
expect(createAlert).toHaveBeenCalled();
diff --git a/spec/frontend/profile/account/components/update_username_spec.js b/spec/frontend/profile/account/components/update_username_spec.js
index d922820601e..3cb9cf3622a 100644
--- a/spec/frontend/profile/account/components/update_username_spec.js
+++ b/spec/frontend/profile/account/components/update_username_spec.js
@@ -93,7 +93,7 @@ describe('UpdateUsername component', () => {
await findNewUsernameInput().setValue(newUsername);
});
- it('confirmation modal contains proper header and body', async () => {
+ it('confirmation modal contains proper header and body', () => {
const { modal } = findElements();
expect(modal.props('title')).toBe('Change username?');
diff --git a/spec/frontend/profile/components/overview_tab_spec.js b/spec/frontend/profile/components/overview_tab_spec.js
index d4cb1dfd15d..aeab24cb730 100644
--- a/spec/frontend/profile/components/overview_tab_spec.js
+++ b/spec/frontend/profile/components/overview_tab_spec.js
@@ -1,15 +1,25 @@
-import { GlTab } from '@gitlab/ui';
+import { GlLoadingIcon, GlTab, GlLink } from '@gitlab/ui';
+import projects from 'test_fixtures/api/users/projects/get.json';
import { s__ } from '~/locale';
import OverviewTab from '~/profile/components/overview_tab.vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import ActivityCalendar from '~/profile/components/activity_calendar.vue';
+import ProjectsList from '~/vue_shared/components/projects_list/projects_list.vue';
+import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
describe('OverviewTab', () => {
let wrapper;
- const createComponent = () => {
- wrapper = shallowMountExtended(OverviewTab);
+ const defaultPropsData = {
+ personalProjects: convertObjectPropsToCamelCase(projects, { deep: true }),
+ personalProjectsLoading: false,
+ };
+
+ const createComponent = ({ propsData = {} } = {}) => {
+ wrapper = shallowMountExtended(OverviewTab, {
+ propsData: { ...defaultPropsData, ...propsData },
+ });
};
it('renders `GlTab` and sets `title` prop', () => {
@@ -23,4 +33,41 @@ describe('OverviewTab', () => {
expect(wrapper.findComponent(ActivityCalendar).exists()).toBe(true);
});
+
+ it('renders personal projects section heading and `View all` link', () => {
+ createComponent();
+
+ expect(
+ wrapper.findByRole('heading', { name: OverviewTab.i18n.personalProjects }).exists(),
+ ).toBe(true);
+ expect(wrapper.findComponent(GlLink).text()).toBe(OverviewTab.i18n.viewAll);
+ });
+
+ describe('when personal projects are loading', () => {
+ it('renders loading icon', () => {
+ createComponent({
+ propsData: {
+ personalProjects: [],
+ personalProjectsLoading: true,
+ },
+ });
+
+ expect(
+ wrapper.findByTestId('personal-projects-section').findComponent(GlLoadingIcon).exists(),
+ ).toBe(true);
+ });
+ });
+
+ describe('when projects are done loading', () => {
+ it('renders `ProjectsList` component and passes `projects` prop', () => {
+ createComponent();
+
+ expect(
+ wrapper
+ .findByTestId('personal-projects-section')
+ .findComponent(ProjectsList)
+ .props('projects'),
+ ).toMatchObject(defaultPropsData.personalProjects);
+ });
+ });
});
diff --git a/spec/frontend/profile/components/profile_tabs_spec.js b/spec/frontend/profile/components/profile_tabs_spec.js
index 11ab372f1dd..80a1ff422ab 100644
--- a/spec/frontend/profile/components/profile_tabs_spec.js
+++ b/spec/frontend/profile/components/profile_tabs_spec.js
@@ -1,6 +1,9 @@
+import projects from 'test_fixtures/api/users/projects/get.json';
import ProfileTabs from '~/profile/components/profile_tabs.vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-
+import { createAlert } from '~/alert';
+import { getUserProjects } from '~/rest_api';
+import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import OverviewTab from '~/profile/components/overview_tab.vue';
import ActivityTab from '~/profile/components/activity_tab.vue';
import GroupsTab from '~/profile/components/groups_tab.vue';
@@ -10,12 +13,20 @@ 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';
+import waitForPromises from 'helpers/wait_for_promises';
+
+jest.mock('~/alert');
+jest.mock('~/rest_api');
describe('ProfileTabs', () => {
let wrapper;
const createComponent = () => {
- wrapper = shallowMountExtended(ProfileTabs);
+ wrapper = shallowMountExtended(ProfileTabs, {
+ provide: {
+ userId: '1',
+ },
+ });
};
it.each([
@@ -33,4 +44,46 @@ describe('ProfileTabs', () => {
expect(wrapper.findComponent(tab).exists()).toBe(true);
});
+
+ describe('when personal projects API request is loading', () => {
+ beforeEach(() => {
+ getUserProjects.mockReturnValueOnce(new Promise(() => {}));
+ createComponent();
+ });
+
+ it('passes correct props to `OverviewTab` component', () => {
+ expect(wrapper.findComponent(OverviewTab).props()).toEqual({
+ personalProjects: [],
+ personalProjectsLoading: true,
+ });
+ });
+ });
+
+ describe('when personal projects API request is successful', () => {
+ beforeEach(async () => {
+ getUserProjects.mockResolvedValueOnce({ data: projects });
+ createComponent();
+ await waitForPromises();
+ });
+
+ it('passes correct props to `OverviewTab` component', () => {
+ expect(wrapper.findComponent(OverviewTab).props()).toMatchObject({
+ personalProjects: convertObjectPropsToCamelCase(projects, { deep: true }),
+ personalProjectsLoading: false,
+ });
+ });
+ });
+
+ describe('when personal projects API request is not successful', () => {
+ beforeEach(() => {
+ getUserProjects.mockRejectedValueOnce();
+ createComponent();
+ });
+
+ it('calls `createAlert`', () => {
+ expect(createAlert).toHaveBeenCalledWith({
+ message: ProfileTabs.i18n.personalProjectsErrorMessage,
+ });
+ });
+ });
});
diff --git a/spec/frontend/projects/commit/components/branches_dropdown_spec.js b/spec/frontend/projects/commit/components/branches_dropdown_spec.js
index 0e68bd21cd4..bff40c2bc39 100644
--- a/spec/frontend/projects/commit/components/branches_dropdown_spec.js
+++ b/spec/frontend/projects/commit/components/branches_dropdown_spec.js
@@ -59,7 +59,7 @@ describe('BranchesDropdown', () => {
});
describe('Selecting Dropdown Item', () => {
- it('emits event', async () => {
+ it('emits event', () => {
findDropdown().vm.$emit('select', '_anything_');
expect(wrapper.emitted()).toHaveProperty('input');
@@ -68,13 +68,11 @@ describe('BranchesDropdown', () => {
describe('When searching', () => {
it('invokes fetchBranches', async () => {
- const spy = jest.spyOn(wrapper.vm, 'fetchBranches');
-
findDropdown().vm.$emit('search', '_anything_');
await nextTick();
- expect(spy).toHaveBeenCalledWith('_anything_');
+ expect(spyFetchBranches).toHaveBeenCalledWith(expect.any(Object), '_anything_');
});
});
});
diff --git a/spec/frontend/projects/commit/components/commit_options_dropdown_spec.js b/spec/frontend/projects/commit/components/commit_options_dropdown_spec.js
index 70491405986..7df498f597b 100644
--- a/spec/frontend/projects/commit/components/commit_options_dropdown_spec.js
+++ b/spec/frontend/projects/commit/components/commit_options_dropdown_spec.js
@@ -85,7 +85,7 @@ describe('BranchesDropdown', () => {
expect(findTagItem().exists()).toBe(false);
});
- it('does not have a email patches options', () => {
+ it('does not have a patches options', () => {
createComponent({ canEmailPatches: false });
expect(findEmailPatchesItem().exists()).toBe(false);
diff --git a/spec/frontend/projects/commit/components/form_modal_spec.js b/spec/frontend/projects/commit/components/form_modal_spec.js
index 84cb30953c3..d40e2d7a48c 100644
--- a/spec/frontend/projects/commit/components/form_modal_spec.js
+++ b/spec/frontend/projects/commit/components/form_modal_spec.js
@@ -1,9 +1,9 @@
import { GlModal, GlForm, GlFormCheckbox, GlSprintf } from '@gitlab/ui';
import { within } from '@testing-library/dom';
-import { shallowMount, mount, createWrapper } from '@vue/test-utils';
+import { createWrapper } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import { nextTick } from 'vue';
-import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
import api from '~/api';
import axios from '~/lib/utils/axios_utils';
import { BV_SHOW_MODAL } from '~/lib/utils/constants';
@@ -21,21 +21,24 @@ describe('CommitFormModal', () => {
let store;
let axiosMock;
- const createComponent = (method, state = {}, provide = {}, propsData = {}) => {
+ const createComponent = ({
+ method = shallowMountExtended,
+ state = {},
+ provide = {},
+ propsData = {},
+ } = {}) => {
store = createStore({ ...mockData.mockModal, ...state });
- wrapper = extendedWrapper(
- method(CommitFormModal, {
- provide: {
- ...provide,
- },
- propsData: { ...mockData.modalPropsData, ...propsData },
- store,
- attrs: {
- static: true,
- visible: true,
- },
- }),
- );
+ wrapper = method(CommitFormModal, {
+ provide: {
+ ...provide,
+ },
+ propsData: { ...mockData.modalPropsData, ...propsData },
+ store,
+ attrs: {
+ static: true,
+ visible: true,
+ },
+ });
};
const findModal = () => wrapper.findComponent(GlModal);
@@ -62,13 +65,13 @@ describe('CommitFormModal', () => {
it('Listens for opening of modal on mount', () => {
jest.spyOn(eventHub, '$on');
- createComponent(shallowMount);
+ createComponent();
expect(eventHub.$on).toHaveBeenCalledWith(mockData.modalPropsData.openModal, wrapper.vm.show);
});
it('Shows modal', () => {
- createComponent(shallowMount);
+ createComponent();
const rootEmit = jest.spyOn(wrapper.vm.$root, '$emit');
wrapper.vm.show();
@@ -77,25 +80,25 @@ describe('CommitFormModal', () => {
});
it('Clears the modal state once modal is hidden', () => {
- createComponent(shallowMount);
+ createComponent();
jest.spyOn(store, 'dispatch').mockImplementation();
- wrapper.vm.checked = false;
+ findCheckBox().vm.$emit('input', false);
findModal().vm.$emit('hidden');
expect(store.dispatch).toHaveBeenCalledWith('clearModal');
expect(store.dispatch).toHaveBeenCalledWith('setSelectedBranch', '');
- expect(wrapper.vm.checked).toBe(true);
+ expect(findCheckBox().attributes('checked')).toBe('true');
});
it('Shows the checkbox for new merge request', () => {
- createComponent(shallowMount);
+ createComponent();
expect(findCheckBox().exists()).toBe(true);
});
it('Shows the prepended text', () => {
- createComponent(shallowMount, {}, { prependedText: '_prepended_text_' });
+ createComponent({ provide: { prependedText: '_prepended_text_' } });
expect(findPrependedText().exists()).toBe(true);
expect(findPrependedText().findComponent(GlSprintf).attributes('message')).toBe(
@@ -104,25 +107,25 @@ describe('CommitFormModal', () => {
});
it('Does not show prepended text', () => {
- createComponent(shallowMount);
+ createComponent();
expect(findPrependedText().exists()).toBe(false);
});
it('Does not show extra message text', () => {
- createComponent(shallowMount);
+ createComponent();
expect(findModal().find('[data-testid="appended-text"]').exists()).toBe(false);
});
it('Does not show the checkbox for new merge request', () => {
- createComponent(shallowMount, { pushCode: false });
+ createComponent({ state: { pushCode: false } });
expect(findCheckBox().exists()).toBe(false);
});
it('Shows the branch in fork message', () => {
- createComponent(shallowMount, { pushCode: false });
+ createComponent({ state: { pushCode: false } });
expect(findAppendedText().exists()).toBe(true);
expect(findAppendedText().findComponent(GlSprintf).attributes('message')).toContain(
@@ -131,7 +134,7 @@ describe('CommitFormModal', () => {
});
it('Shows the branch collaboration message', () => {
- createComponent(shallowMount, { pushCode: false, branchCollaboration: true });
+ createComponent({ state: { pushCode: false, branchCollaboration: true } });
expect(findAppendedText().exists()).toBe(true);
expect(findAppendedText().findComponent(GlSprintf).attributes('message')).toContain(
@@ -142,17 +145,13 @@ describe('CommitFormModal', () => {
describe('Taking action on the form', () => {
beforeEach(() => {
- createComponent(mount);
+ createComponent({ method: mountExtended });
});
it('Action primary button dispatches submit action', () => {
- const submitSpy = jest.spyOn(findForm().element, 'submit');
-
getByText(mockData.modalPropsData.i18n.actionPrimaryText).trigger('click');
- expect(submitSpy).toHaveBeenCalled();
-
- submitSpy.mockRestore();
+ expect(wrapper.vm.$refs.form.$el.submit).toHaveBeenCalled();
});
it('Changes the start_branch input value', async () => {
@@ -164,7 +163,7 @@ describe('CommitFormModal', () => {
});
it('Changes the target_project_id input value', async () => {
- createComponent(shallowMount, {}, {}, { isCherryPick: true });
+ createComponent({ propsData: { isCherryPick: true } });
findProjectsDropdown().vm.$emit('input', '_changed_project_value_');
await nextTick();
@@ -174,12 +173,9 @@ describe('CommitFormModal', () => {
});
it('action primary button triggers Redis HLL tracking api call', async () => {
- createComponent(mount, {}, {}, { primaryActionEventName: 'test_event' });
-
+ createComponent({ method: mountExtended, propsData: { primaryActionEventName: 'test_event' } });
await nextTick();
- jest.spyOn(findForm().element, 'submit');
-
getByText(mockData.modalPropsData.i18n.actionPrimaryText).trigger('click');
expect(api.trackRedisHllUserEvent).toHaveBeenCalledWith('test_event');
diff --git a/spec/frontend/projects/commit/store/actions_spec.js b/spec/frontend/projects/commit/store/actions_spec.js
index d48f9fd6fc0..adb87142fee 100644
--- a/spec/frontend/projects/commit/store/actions_spec.js
+++ b/spec/frontend/projects/commit/store/actions_spec.js
@@ -63,7 +63,7 @@ describe('Commit form modal store actions', () => {
);
});
- it('should show alert error and set error in state on fetchBranches failure', async () => {
+ it('should show an alert and set error in state on fetchBranches failure', async () => {
jest.spyOn(axios, 'get').mockRejectedValue();
await testAction(actions.fetchBranches, {}, state, [], [{ type: 'requestBranches' }]);
diff --git a/spec/frontend/projects/commits/components/author_select_spec.js b/spec/frontend/projects/commits/components/author_select_spec.js
index ff1d860fd53..b06463e73a7 100644
--- a/spec/frontend/projects/commits/components/author_select_spec.js
+++ b/spec/frontend/projects/commits/components/author_select_spec.js
@@ -3,6 +3,7 @@ import { shallowMount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
+import setWindowLocation from 'helpers/set_window_location_helper';
import * as urlUtility from '~/lib/utils/url_utility';
import AuthorSelect from '~/projects/commits/components/author_select.vue';
import { createStore } from '~/projects/commits/store';
@@ -64,39 +65,23 @@ describe('Author Select', () => {
const findDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
describe('user is searching via "filter by commit message"', () => {
- it('disables dropdown container', 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({ hasSearchParam: true });
+ beforeEach(() => {
+ setWindowLocation(`?search=foo`);
+ createComponent();
+ });
- await nextTick();
+ it('does not disable dropdown container', () => {
expect(findDropdownContainer().attributes('disabled')).toBeUndefined();
});
- it('has correct tooltip message', 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({ hasSearchParam: true });
-
- await nextTick();
+ it('has correct tooltip message', () => {
expect(findDropdownContainer().attributes('title')).toBe(
'Searching by both author and message is currently not supported.',
);
});
- it('disables dropdown', 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({ hasSearchParam: false });
-
- await nextTick();
- expect(findDropdown().attributes('disabled')).toBeUndefined();
- });
-
- it('hasSearchParam if user types a truthy string', () => {
- wrapper.vm.setSearchParam('false');
-
- expect(wrapper.vm.hasSearchParam).toBe(true);
+ it('disables dropdown', () => {
+ expect(findDropdown().attributes('disabled')).toBe('true');
});
});
@@ -106,9 +91,8 @@ describe('Author Select', () => {
});
it('displays the current selected author', 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({ currentAuthor });
+ setWindowLocation(`?author=${currentAuthor}`);
+ createComponent();
await nextTick();
expect(findDropdown().attributes('text')).toBe(currentAuthor);
@@ -146,12 +130,14 @@ describe('Author Select', () => {
expect(findDropdownItems().at(0).text()).toBe('Any Author');
});
- it('displays the project authors', async () => {
- await nextTick();
+ it('displays the project authors', () => {
expect(findDropdownItems()).toHaveLength(authors.length + 1);
});
it('has the correct props', async () => {
+ setWindowLocation(`?author=${currentAuthor}`);
+ createComponent();
+
const [{ avatar_url: avatarUrl, username }] = authors;
const result = {
avatarUrl,
@@ -159,16 +145,11 @@ describe('Author Select', () => {
isChecked: 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({ currentAuthor });
-
await nextTick();
expect(findDropdownItems().at(1).props()).toEqual(expect.objectContaining(result));
});
- it("display the author's name", async () => {
- await nextTick();
+ it("display the author's name", () => {
expect(findDropdownItems().at(1).text()).toBe(currentAuthor);
});
diff --git a/spec/frontend/projects/commits/store/actions_spec.js b/spec/frontend/projects/commits/store/actions_spec.js
index f5184e59420..8afa2a6fb8f 100644
--- a/spec/frontend/projects/commits/store/actions_spec.js
+++ b/spec/frontend/projects/commits/store/actions_spec.js
@@ -34,7 +34,7 @@ describe('Project commits actions', () => {
]));
});
- describe('shows an alert message when there is an error', () => {
+ describe('shows an alert when there is an error', () => {
it('creates an alert', () => {
const mockDispatchContext = { dispatch: () => {}, commit: () => {}, state };
actions.receiveAuthorsError(mockDispatchContext);
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 645d0483a5f..e289569f8ce 100644
--- a/spec/frontend/projects/compare/components/revision_dropdown_legacy_spec.js
+++ b/spec/frontend/projects/compare/components/revision_dropdown_legacy_spec.js
@@ -90,7 +90,7 @@ describe('RevisionDropdown component', () => {
expect(findTagsDropdownItem()).toHaveLength(0);
});
- it('shows alert message on error', async () => {
+ it('shows an alert on error', async () => {
axiosMock.onGet('some/invalid/path').replyOnce(HTTP_STATUS_NOT_FOUND);
await waitForPromises();
diff --git a/spec/frontend/projects/compare/components/revision_dropdown_spec.js b/spec/frontend/projects/compare/components/revision_dropdown_spec.js
index 3a256682549..1cf99f16601 100644
--- a/spec/frontend/projects/compare/components/revision_dropdown_spec.js
+++ b/spec/frontend/projects/compare/components/revision_dropdown_spec.js
@@ -78,7 +78,7 @@ describe('RevisionDropdown component', () => {
});
});
- it('shows alert message on error', async () => {
+ it('shows an alert on error', async () => {
axiosMock.onGet('some/invalid/path').replyOnce(HTTP_STATUS_NOT_FOUND);
createComponent();
@@ -104,7 +104,7 @@ describe('RevisionDropdown component', () => {
});
describe('search', () => {
- it('shows alert message on error', async () => {
+ it('shows alert on error', async () => {
axiosMock.onGet('some/invalid/path').replyOnce(HTTP_STATUS_NOT_FOUND);
createComponent();
diff --git a/spec/frontend/projects/new/components/app_spec.js b/spec/frontend/projects/new/components/app_spec.js
index 5b2dc25077e..16576523c66 100644
--- a/spec/frontend/projects/new/components/app_spec.js
+++ b/spec/frontend/projects/new/components/app_spec.js
@@ -7,7 +7,7 @@ describe('Experimental new project creation app', () => {
const createComponent = (propsData) => {
wrapper = shallowMount(App, {
- propsData: { projectsUrl: '/dashboard/projects', ...propsData },
+ propsData: { rootPath: '/', projectsUrl: '/dashboard/projects', ...propsData },
});
};
@@ -45,6 +45,7 @@ describe('Experimental new project creation app', () => {
createComponent();
expect(findNewNamespacePage().props('initialBreadcrumbs')).toEqual([
+ { href: '/', text: 'Your work' },
{ href: '/dashboard/projects', text: 'Projects' },
{ href: '#', text: 'New project' },
]);
diff --git a/spec/frontend/projects/new/components/new_project_url_select_spec.js b/spec/frontend/projects/new/components/new_project_url_select_spec.js
index fa720f4487c..ceac4435282 100644
--- a/spec/frontend/projects/new/components/new_project_url_select_spec.js
+++ b/spec/frontend/projects/new/components/new_project_url_select_spec.js
@@ -247,7 +247,7 @@ describe('NewProjectUrlSelect component', () => {
eventHub.$emit('select-template', getIdFromGraphQLId(id), fullPath);
});
- it('filters the dropdown items to the selected group and children', async () => {
+ it('filters the dropdown items to the selected group and children', () => {
const listItems = wrapper.findAll('li');
expect(listItems).toHaveLength(3);
diff --git a/spec/frontend/projects/pipelines/charts/components/__snapshots__/ci_cd_analytics_area_chart_spec.js.snap b/spec/frontend/projects/pipelines/charts/components/__snapshots__/ci_cd_analytics_area_chart_spec.js.snap
index 1545c52d7cb..61bcd44724c 100644
--- a/spec/frontend/projects/pipelines/charts/components/__snapshots__/ci_cd_analytics_area_chart_spec.js.snap
+++ b/spec/frontend/projects/pipelines/charts/components/__snapshots__/ci_cd_analytics_area_chart_spec.js.snap
@@ -17,6 +17,7 @@ exports[`CiCdAnalyticsAreaChart matches the snapshot 1`] = `
legendlayout="inline"
legendmaxtext="Max"
legendmintext="Min"
+ legendseriesinfo=""
option="[object Object]"
responsive=""
thresholds=""
diff --git a/spec/frontend/projects/settings/components/transfer_project_form_spec.js b/spec/frontend/projects/settings/components/transfer_project_form_spec.js
index d8c2cf83f38..a92ac1bed9d 100644
--- a/spec/frontend/projects/settings/components/transfer_project_form_spec.js
+++ b/spec/frontend/projects/settings/components/transfer_project_form_spec.js
@@ -64,17 +64,17 @@ describe('Transfer project form', () => {
expect(findTransferLocations().props('value')).toEqual(selectedItem);
});
- it('emits the `selectTransferLocation` event when a namespace is selected', async () => {
+ it('emits the `selectTransferLocation` event when a namespace is selected', () => {
const args = [selectedItem.id];
expect(wrapper.emitted('selectTransferLocation')).toEqual([args]);
});
- it('enables the confirm button', async () => {
+ it('enables the confirm button', () => {
expect(findConfirmDanger().attributes('disabled')).toBeUndefined();
});
- it('clicking the confirm button emits the `confirm` event', async () => {
+ it('clicking the confirm button emits the `confirm` event', () => {
findConfirmDanger().vm.$emit('confirm');
expect(wrapper.emitted('confirm')).toBeDefined();
diff --git a/spec/frontend/projects/settings/topics/components/topics_token_selector_spec.js b/spec/frontend/projects/settings/topics/components/topics_token_selector_spec.js
index 4b94c179f74..b2c03352cdc 100644
--- a/spec/frontend/projects/settings/topics/components/topics_token_selector_spec.js
+++ b/spec/frontend/projects/settings/topics/components/topics_token_selector_spec.js
@@ -83,7 +83,7 @@ describe('TopicsTokenSelector', () => {
});
});
- it('passes topic title to the avatar', async () => {
+ it('passes topic title to the avatar', () => {
createComponent();
const avatars = findAllAvatars();
diff --git a/spec/frontend/projects/settings_service_desk/components/service_desk_root_spec.js b/spec/frontend/projects/settings_service_desk/components/service_desk_root_spec.js
index 4d0d2191176..86e4e88e3cf 100644
--- a/spec/frontend/projects/settings_service_desk/components/service_desk_root_spec.js
+++ b/spec/frontend/projects/settings_service_desk/components/service_desk_root_spec.js
@@ -78,7 +78,7 @@ describe('ServiceDeskRoot', () => {
const alertBodyLink = alertEl.findComponent(GlLink);
expect(alertBodyLink.exists()).toBe(true);
expect(alertBodyLink.attributes('href')).toBe(
- '/help/user/project/service_desk.html#using-a-custom-email-address',
+ '/help/user/project/service_desk.html#use-a-custom-email-address',
);
expect(alertBodyLink.text()).toBe('How do I create a custom email address?');
});
@@ -147,7 +147,7 @@ describe('ServiceDeskRoot', () => {
await waitForPromises();
});
- it('sends a request to update template', async () => {
+ it('sends a request to update template', () => {
expect(spy).toHaveBeenCalledWith(provideData.endpoint, {
issue_template_key: 'Bug',
outgoing_name: 'GitLab Support Bot',
diff --git a/spec/frontend/prometheus_metrics/custom_metrics_spec.js b/spec/frontend/prometheus_metrics/custom_metrics_spec.js
index 3852f2678b7..823caec0211 100644
--- a/spec/frontend/prometheus_metrics/custom_metrics_spec.js
+++ b/spec/frontend/prometheus_metrics/custom_metrics_spec.js
@@ -1,5 +1,6 @@
+import prometheusIntegration from 'test_fixtures/integrations/prometheus/prometheus_integration.html';
import MockAdapter from 'axios-mock-adapter';
-import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
+import { setHTMLFixture, 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';
@@ -7,7 +8,6 @@ import CustomMetrics from '~/prometheus_metrics/custom_metrics';
import { metrics1 as metrics } from './mock_data';
describe('PrometheusMetrics', () => {
- const FIXTURE = 'integrations/prometheus/prometheus_integration.html';
const customMetricsEndpoint =
'http://test.host/frontend-fixtures/integrations-project/prometheus/metrics';
let mock;
@@ -17,7 +17,7 @@ describe('PrometheusMetrics', () => {
mock.onGet(customMetricsEndpoint).reply(HTTP_STATUS_OK, {
metrics,
});
- loadHTMLFixture(FIXTURE);
+ setHTMLFixture(prometheusIntegration);
});
afterEach(() => {
diff --git a/spec/frontend/prometheus_metrics/prometheus_metrics_spec.js b/spec/frontend/prometheus_metrics/prometheus_metrics_spec.js
index 45654d6a2eb..bc796ac53ca 100644
--- a/spec/frontend/prometheus_metrics/prometheus_metrics_spec.js
+++ b/spec/frontend/prometheus_metrics/prometheus_metrics_spec.js
@@ -1,5 +1,6 @@
+import prometheusIntegration from 'test_fixtures/integrations/prometheus/prometheus_integration.html';
import MockAdapter from 'axios-mock-adapter';
-import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
+import { setHTMLFixture, 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';
@@ -8,10 +9,8 @@ import PrometheusMetrics from '~/prometheus_metrics/prometheus_metrics';
import { metrics2 as metrics, missingVarMetrics } from './mock_data';
describe('PrometheusMetrics', () => {
- const FIXTURE = 'integrations/prometheus/prometheus_integration.html';
-
beforeEach(() => {
- loadHTMLFixture(FIXTURE);
+ setHTMLFixture(prometheusIntegration);
});
describe('constructor', () => {
diff --git a/spec/frontend/protected_branches/protected_branch_edit_spec.js b/spec/frontend/protected_branches/protected_branch_edit_spec.js
index 4141d000a1c..e1966908452 100644
--- a/spec/frontend/protected_branches/protected_branch_edit_spec.js
+++ b/spec/frontend/protected_branches/protected_branch_edit_spec.js
@@ -115,7 +115,7 @@ describe('ProtectedBranchEdit', () => {
});
describe('when clicked', () => {
- beforeEach(async () => {
+ beforeEach(() => {
mock
.onPatch(TEST_URL, { protected_branch: { [patchParam]: true } })
.replyOnce(HTTP_STATUS_OK, {});
diff --git a/spec/frontend/read_more_spec.js b/spec/frontend/read_more_spec.js
index 9eddc50d50a..e45405088b1 100644
--- a/spec/frontend/read_more_spec.js
+++ b/spec/frontend/read_more_spec.js
@@ -42,7 +42,7 @@ describe('Read more click-to-expand functionality', () => {
nestedElement.click();
});
- it('removes the trigger element', async () => {
+ it('removes the trigger element', () => {
expect(findTrigger()).toBe(null);
});
});
diff --git a/spec/frontend/ref/components/ref_selector_spec.js b/spec/frontend/ref/components/ref_selector_spec.js
index 6b90827f9c2..5e15ba26ece 100644
--- a/spec/frontend/ref/components/ref_selector_spec.js
+++ b/spec/frontend/ref/components/ref_selector_spec.js
@@ -35,6 +35,7 @@ describe('Ref selector component', () => {
const projectId = '8';
const totalBranchesCount = 123;
const totalTagsCount = 456;
+ const queryParams = { sort: 'updated_desc' };
let wrapper;
let branchesApiCallSpy;
@@ -738,4 +739,25 @@ describe('Ref selector component', () => {
expect(lastCallProps.matches).toMatchObject(expectedMatches);
});
});
+ describe('when queryParam prop is present', () => {
+ it('passes params to a branches API call', () => {
+ createComponent({ propsData: { queryParams } });
+
+ return waitForRequests().then(() => {
+ expect(branchesApiCallSpy).toHaveBeenCalledWith(
+ expect.objectContaining({ params: { per_page: 20, search: '', sort: queryParams.sort } }),
+ );
+ });
+ });
+
+ it('does not pass params to tags API call', () => {
+ createComponent({ propsData: { queryParams } });
+
+ return waitForRequests().then(() => {
+ expect(tagsApiCallSpy).toHaveBeenCalledWith(
+ expect.objectContaining({ params: { per_page: 20, search: '' } }),
+ );
+ });
+ });
+ });
});
diff --git a/spec/frontend/ref/stores/actions_spec.js b/spec/frontend/ref/stores/actions_spec.js
index 099ce062a3a..c6aac8c9c98 100644
--- a/spec/frontend/ref/stores/actions_spec.js
+++ b/spec/frontend/ref/stores/actions_spec.js
@@ -52,6 +52,13 @@ describe('Ref selector Vuex store actions', () => {
});
});
+ describe('setParams', () => {
+ it(`commits ${types.SET_PARAMS} with the provided params`, () => {
+ const params = { sort: 'updated_asc' };
+ testAction(actions.setParams, params, state, [{ type: types.SET_PARAMS, payload: params }]);
+ });
+ });
+
describe('search', () => {
it(`commits ${types.SET_QUERY} with the new search query`, () => {
const query = 'hello';
diff --git a/spec/frontend/ref/stores/mutations_spec.js b/spec/frontend/ref/stores/mutations_spec.js
index 37eee18dc10..8f16317b751 100644
--- a/spec/frontend/ref/stores/mutations_spec.js
+++ b/spec/frontend/ref/stores/mutations_spec.js
@@ -34,6 +34,7 @@ describe('Ref selector Vuex store mutations', () => {
error: null,
},
},
+ params: null,
selectedRef: null,
requestCount: 0,
});
@@ -56,6 +57,15 @@ describe('Ref selector Vuex store mutations', () => {
});
});
+ describe(`${types.SET_PARAMS}`, () => {
+ it('sets the additional query params', () => {
+ const params = { sort: 'updated_desc' };
+ mutations[types.SET_PARAMS](state, params);
+
+ expect(state.params).toBe(params);
+ });
+ });
+
describe(`${types.SET_PROJECT_ID}`, () => {
it('updates the project ID', () => {
const newProjectId = '4';
diff --git a/spec/frontend/releases/components/app_edit_new_spec.js b/spec/frontend/releases/components/app_edit_new_spec.js
index dcb6a3293a6..d253c42e03f 100644
--- a/spec/frontend/releases/components/app_edit_new_spec.js
+++ b/spec/frontend/releases/components/app_edit_new_spec.js
@@ -31,6 +31,8 @@ describe('Release edit/new component', () => {
let actions;
let getters;
let state;
+ let refActions;
+ let refState;
let mock;
const factory = async ({ featureFlags = {}, store: storeUpdates = {} } = {}) => {
@@ -62,6 +64,20 @@ describe('Release edit/new component', () => {
tagNameValidation: new ValidationResult(),
}),
formattedReleaseNotes: () => 'these notes are formatted',
+ isCreating: jest.fn(),
+ isSearching: jest.fn(),
+ isExistingTag: jest.fn(),
+ isNewTag: jest.fn(),
+ };
+
+ refState = {
+ matches: [],
+ };
+
+ refActions = {
+ setEnabledRefTypes: jest.fn(),
+ setProjectId: jest.fn(),
+ search: jest.fn(),
};
const store = new Vuex.Store(
@@ -74,6 +90,11 @@ describe('Release edit/new component', () => {
state,
getters,
},
+ ref: {
+ namespaced: true,
+ actions: refActions,
+ state: refState,
+ },
},
},
storeUpdates,
diff --git a/spec/frontend/releases/components/app_index_spec.js b/spec/frontend/releases/components/app_index_spec.js
index 7a0e9fb7326..2aa36056735 100644
--- a/spec/frontend/releases/components/app_index_spec.js
+++ b/spec/frontend/releases/components/app_index_spec.js
@@ -412,7 +412,7 @@ describe('app_index.vue', () => {
await createComponent();
});
- it('shows a toast', async () => {
+ it('shows a toast', () => {
expect(toast).toHaveBeenCalledWith(
sprintf(__('Release %{release} has been successfully deleted.'), {
release,
@@ -420,7 +420,7 @@ describe('app_index.vue', () => {
);
});
- it('clears session storage', async () => {
+ it('clears session storage', () => {
expect(window.sessionStorage.getItem(key)).toBe(null);
});
});
diff --git a/spec/frontend/releases/components/releases_sort_spec.js b/spec/frontend/releases/components/releases_sort_spec.js
index 92199896ab4..76907b4b8bb 100644
--- a/spec/frontend/releases/components/releases_sort_spec.js
+++ b/spec/frontend/releases/components/releases_sort_spec.js
@@ -1,5 +1,6 @@
import { GlSorting, GlSortingItem } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { assertProps } from 'helpers/assert_props';
import ReleasesSort from '~/releases/components/releases_sort.vue';
import { RELEASED_AT_ASC, RELEASED_AT_DESC, CREATED_ASC, CREATED_DESC } from '~/releases/constants';
@@ -92,7 +93,7 @@ describe('releases_sort.vue', () => {
describe('prop validation', () => {
it('validates that the `value` prop is one of the expected sort strings', () => {
expect(() => {
- createComponent('not a valid value');
+ assertProps(ReleasesSort, { value: 'not a valid value' });
}).toThrow('Invalid prop: custom validator check failed');
});
});
diff --git a/spec/frontend/releases/components/tag_create_spec.js b/spec/frontend/releases/components/tag_create_spec.js
new file mode 100644
index 00000000000..0df2483bcf2
--- /dev/null
+++ b/spec/frontend/releases/components/tag_create_spec.js
@@ -0,0 +1,107 @@
+import { GlButton, GlFormInput, GlFormTextarea } from '@gitlab/ui';
+import axios from 'axios';
+import MockAdapter from 'axios-mock-adapter';
+import { nextTick } from 'vue';
+import { shallowMount } from '@vue/test-utils';
+import { __, s__ } from '~/locale';
+import TagCreate from '~/releases/components/tag_create.vue';
+import RefSelector from '~/ref/components/ref_selector.vue';
+import createStore from '~/releases/stores';
+import createEditNewModule from '~/releases/stores/modules/edit_new';
+import { createRefModule } from '~/ref/stores';
+
+const TEST_PROJECT_ID = '1234';
+
+const VALUE = 'new-tag';
+
+describe('releases/components/tag_create', () => {
+ let store;
+ let wrapper;
+ let mock;
+
+ const createWrapper = () => {
+ wrapper = shallowMount(TagCreate, {
+ store,
+ propsData: { value: VALUE },
+ });
+ };
+
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+ store = createStore({
+ modules: {
+ editNew: createEditNewModule({
+ projectId: TEST_PROJECT_ID,
+ }),
+ ref: createRefModule(),
+ },
+ });
+ store.state.editNew.release = {
+ tagMessage: 'test',
+ };
+ store.state.editNew.createFrom = 'v1';
+ createWrapper();
+ });
+
+ afterEach(() => mock.restore());
+
+ const findTagInput = () => wrapper.findComponent(GlFormInput);
+ const findTagRef = () => wrapper.findComponent(RefSelector);
+ const findTagMessage = () => wrapper.findComponent(GlFormTextarea);
+ const findSave = () => wrapper.findAllComponents(GlButton).at(-2);
+ const findCancel = () => wrapper.findAllComponents(GlButton).at(-1);
+
+ it('initializes the input with value prop', () => {
+ expect(findTagInput().attributes('value')).toBe(VALUE);
+ });
+
+ it('emits a change event when the tag name chagnes', () => {
+ findTagInput().vm.$emit('input', 'new-value');
+
+ expect(wrapper.emitted('change')).toEqual([['new-value']]);
+ });
+
+ it('binds the store value to the ref selector', () => {
+ const ref = findTagRef();
+ expect(ref.props('value')).toBe('v1');
+
+ ref.vm.$emit('input', 'v2');
+
+ expect(ref.props('value')).toBe('v1');
+ });
+
+ it('passes the project id tot he ref selector', () => {
+ expect(findTagRef().props('projectId')).toBe(TEST_PROJECT_ID);
+ });
+
+ it('binds the store value to the message', async () => {
+ const message = findTagMessage();
+ expect(message.attributes('value')).toBe('test');
+
+ message.vm.$emit('input', 'hello');
+
+ await nextTick();
+
+ expect(message.attributes('value')).toBe('hello');
+ });
+
+ it('emits create event when Save is clicked', () => {
+ const button = findSave();
+
+ expect(button.text()).toBe(__('Save'));
+
+ button.vm.$emit('click');
+
+ expect(wrapper.emitted('create')).toEqual([[]]);
+ });
+
+ it('emits cancel event when Select another tag is clicked', () => {
+ const button = findCancel();
+
+ expect(button.text()).toBe(s__('Release|Select another tag'));
+
+ button.vm.$emit('click');
+
+ expect(wrapper.emitted('cancel')).toEqual([[]]);
+ });
+});
diff --git a/spec/frontend/releases/components/tag_field_new_spec.js b/spec/frontend/releases/components/tag_field_new_spec.js
index 2508495429c..3468338b8a7 100644
--- a/spec/frontend/releases/components/tag_field_new_spec.js
+++ b/spec/frontend/releases/components/tag_field_new_spec.js
@@ -1,18 +1,19 @@
-import { GlDropdownItem, GlFormGroup, GlSprintf } from '@gitlab/ui';
-import { mount, shallowMount } from '@vue/test-utils';
+import { GlFormGroup, GlDropdown, GlPopover } from '@gitlab/ui';
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
-import Vue, { nextTick } from 'vue';
-import { trimText } from 'helpers/text_helper';
-import { mountExtended } from 'helpers/vue_test_utils_helper';
+import { nextTick } from 'vue';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { __ } from '~/locale';
import TagFieldNew from '~/releases/components/tag_field_new.vue';
+import TagSearch from '~/releases/components/tag_search.vue';
+import TagCreate from '~/releases/components/tag_create.vue';
import createStore from '~/releases/stores';
import createEditNewModule from '~/releases/stores/modules/edit_new';
+import { CREATE } from '~/releases/stores/modules/edit_new/constants';
+import { createRefModule } from '~/ref/stores';
import { i18n } from '~/releases/constants';
const TEST_TAG_NAME = 'test-tag-name';
-const TEST_TAG_MESSAGE = 'Test tag message';
const TEST_PROJECT_ID = '1234';
const TEST_CREATE_FROM = 'test-create-from';
const NONEXISTENT_TAG_NAME = 'nonexistent-tag';
@@ -21,38 +22,12 @@ describe('releases/components/tag_field_new', () => {
let store;
let wrapper;
let mock;
- let RefSelectorStub;
-
- const createComponent = (
- mountFn = shallowMount,
- { searchQuery } = { searchQuery: NONEXISTENT_TAG_NAME },
- ) => {
- // A mock version of the RefSelector component that just renders the
- // #footer slot, so that the content inside this slot can be tested.
- RefSelectorStub = Vue.component('RefSelectorStub', {
- data() {
- return {
- footerSlotProps: {
- isLoading: false,
- matches: {
- tags: {
- totalCount: 1,
- list: [{ name: TEST_TAG_NAME }],
- },
- },
- query: searchQuery,
- },
- };
- },
- template: '<div><slot name="footer" v-bind="footerSlotProps"></slot></div>',
- });
- wrapper = mountFn(TagFieldNew, {
+ const createComponent = () => {
+ wrapper = shallowMountExtended(TagFieldNew, {
store,
stubs: {
- RefSelector: RefSelectorStub,
GlFormGroup,
- GlSprintf,
},
});
};
@@ -63,11 +38,12 @@ describe('releases/components/tag_field_new', () => {
editNew: createEditNewModule({
projectId: TEST_PROJECT_ID,
}),
+ ref: createRefModule(),
},
});
store.state.editNew.createFrom = TEST_CREATE_FROM;
- store.state.editNew.showCreateFrom = true;
+ store.state.editNew.step = CREATE;
store.state.editNew.release = {
tagName: TEST_TAG_NAME,
@@ -81,20 +57,13 @@ describe('releases/components/tag_field_new', () => {
gon.api_version = 'v4';
});
- afterEach(() => {
- mock.restore();
- });
-
- const findTagNameFormGroup = () => wrapper.find('[data-testid="tag-name-field"]');
- const findTagNameDropdown = () => findTagNameFormGroup().findComponent(RefSelectorStub);
+ afterEach(() => mock.restore());
- const findCreateFromFormGroup = () => wrapper.find('[data-testid="create-from-field"]');
- const findCreateFromDropdown = () => findCreateFromFormGroup().findComponent(RefSelectorStub);
-
- const findCreateNewTagOption = () => wrapper.findComponent(GlDropdownItem);
-
- const findAnnotatedTagMessageFormGroup = () =>
- wrapper.find('[data-testid="annotated-tag-message-field"]');
+ const findTagNameFormGroup = () => wrapper.findComponent(GlFormGroup);
+ const findTagNameInput = () => wrapper.findComponent(GlDropdown);
+ const findTagNamePopover = () => wrapper.findComponent(GlPopover);
+ const findTagNameSearch = () => wrapper.findComponent(TagSearch);
+ const findTagNameCreate = () => wrapper.findComponent(TagCreate);
describe('"Tag name" field', () => {
describe('rendering and behavior', () => {
@@ -102,20 +71,37 @@ describe('releases/components/tag_field_new', () => {
it('renders a label', () => {
expect(findTagNameFormGroup().attributes().label).toBe(__('Tag name'));
- expect(findTagNameFormGroup().props().labelDescription).toBe(__('*Required'));
+ expect(findTagNameFormGroup().props().optionalText).toBe(__('(required)'));
+ });
+
+ it('flips between search and create, passing the searched value', async () => {
+ let create = findTagNameCreate();
+ let search = findTagNameSearch();
+
+ expect(create.exists()).toBe(true);
+ expect(search.exists()).toBe(false);
+
+ await create.vm.$emit('cancel');
+
+ search = findTagNameSearch();
+ expect(create.exists()).toBe(false);
+ expect(search.exists()).toBe(true);
+
+ await search.vm.$emit('create', TEST_TAG_NAME);
+
+ create = findTagNameCreate();
+ expect(create.exists()).toBe(true);
+ expect(create.props('value')).toBe(TEST_TAG_NAME);
+ expect(search.exists()).toBe(false);
});
describe('when the user selects a new tag name', () => {
- beforeEach(async () => {
- findCreateNewTagOption().vm.$emit('click');
- });
+ it("updates the store's release.tagName property", async () => {
+ findTagNameCreate().vm.$emit('change', NONEXISTENT_TAG_NAME);
+ await findTagNameCreate().vm.$emit('create');
- it("updates the store's release.tagName property", () => {
expect(store.state.editNew.release.tagName).toBe(NONEXISTENT_TAG_NAME);
- });
-
- it('hides the "Create from" field', () => {
- expect(findCreateFromFormGroup().exists()).toBe(true);
+ expect(findTagNameInput().props('text')).toBe(NONEXISTENT_TAG_NAME);
});
});
@@ -123,19 +109,17 @@ describe('releases/components/tag_field_new', () => {
const updatedTagName = 'updated-tag-name';
beforeEach(async () => {
- findTagNameDropdown().vm.$emit('input', updatedTagName);
+ await findTagNameCreate().vm.$emit('cancel');
+ findTagNameSearch().vm.$emit('select', updatedTagName);
});
it("updates the store's release.tagName property", () => {
expect(store.state.editNew.release.tagName).toBe(updatedTagName);
+ expect(findTagNameInput().props('text')).toBe(updatedTagName);
});
it('hides the "Create from" field', () => {
- expect(findCreateFromFormGroup().exists()).toBe(false);
- });
-
- it('hides the "Tag message" field', () => {
- expect(findAnnotatedTagMessageFormGroup().exists()).toBe(false);
+ expect(findTagNameCreate().exists()).toBe(false);
});
it('fetches the release notes for the tag', () => {
@@ -145,131 +129,66 @@ describe('releases/components/tag_field_new', () => {
});
});
- describe('"Create tag" option', () => {
- describe('when the search query exactly matches one of the search results', () => {
- beforeEach(async () => {
- createComponent(mount, { searchQuery: TEST_TAG_NAME });
- });
-
- it('does not show the "Create tag" option', () => {
- expect(findCreateNewTagOption().exists()).toBe(false);
- });
- });
-
- describe('when the search query does not exactly match one of the search results', () => {
- beforeEach(async () => {
- createComponent(mount, { searchQuery: NONEXISTENT_TAG_NAME });
- });
-
- it('shows the "Create tag" option', () => {
- expect(findCreateNewTagOption().exists()).toBe(true);
- });
- });
- });
-
describe('validation', () => {
beforeEach(() => {
- createComponent(mount);
+ createComponent();
+ findTagNameCreate().vm.$emit('cancel');
});
/**
* Utility function to test the visibility of the validation message
- * @param {'shown' | 'hidden'} state The expected state of the validation message.
- * Should be passed either 'shown' or 'hidden'
+ * @param {boolean} isShown Whether or not the message is shown.
*/
- const expectValidationMessageToBe = async (state) => {
+ const expectValidationMessageToBeShown = async (isShown) => {
await nextTick();
- expect(findTagNameFormGroup().element).toHaveClass(
- state === 'shown' ? 'is-invalid' : 'is-valid',
- );
- expect(findTagNameFormGroup().element).not.toHaveClass(
- state === 'shown' ? 'is-valid' : 'is-invalid',
- );
+ const state = findTagNameFormGroup().attributes('state');
+
+ if (isShown) {
+ expect(state).toBeUndefined();
+ } else {
+ expect(state).toBe('true');
+ }
};
describe('when the user has not yet interacted with the component', () => {
it('does not display a validation error', async () => {
- findTagNameDropdown().vm.$emit('input', '');
-
- await expectValidationMessageToBe('hidden');
+ await expectValidationMessageToBeShown(false);
});
});
describe('when the user has interacted with the component and the value is not empty', () => {
it('does not display validation error', async () => {
- findTagNameDropdown().vm.$emit('hide');
+ findTagNameSearch().vm.$emit('select', 'vTest');
+ findTagNamePopover().vm.$emit('hide');
- await expectValidationMessageToBe('hidden');
+ await expectValidationMessageToBeShown(false);
});
it('displays a validation error if the tag has an associated release', async () => {
- findTagNameDropdown().vm.$emit('input', 'vTest');
- findTagNameDropdown().vm.$emit('hide');
+ findTagNameSearch().vm.$emit('select', 'vTest');
+ findTagNamePopover().vm.$emit('hide');
store.state.editNew.existingRelease = {};
- await expectValidationMessageToBe('shown');
- expect(findTagNameFormGroup().text()).toContain(i18n.tagIsAlredyInUseMessage);
+ await expectValidationMessageToBeShown(true);
+ expect(findTagNameFormGroup().attributes('invalidfeedback')).toBe(
+ i18n.tagIsAlredyInUseMessage,
+ );
});
});
describe('when the user has interacted with the component and the value is empty', () => {
it('displays a validation error', async () => {
- findTagNameDropdown().vm.$emit('input', '');
- findTagNameDropdown().vm.$emit('hide');
+ findTagNameSearch().vm.$emit('select', '');
+ findTagNamePopover().vm.$emit('hide');
- await expectValidationMessageToBe('shown');
- expect(findTagNameFormGroup().text()).toContain(i18n.tagNameIsRequiredMessage);
+ await expectValidationMessageToBeShown(true);
+ expect(findTagNameFormGroup().attributes('invalidfeedback')).toContain(
+ i18n.tagNameIsRequiredMessage,
+ );
});
});
});
});
-
- describe('"Create from" field', () => {
- beforeEach(() => createComponent());
-
- it('renders a label', () => {
- expect(findCreateFromFormGroup().attributes().label).toBe('Create from');
- });
-
- describe('when the user selects a git ref', () => {
- it("updates the store's createFrom property", async () => {
- const updatedCreateFrom = 'update-create-from';
- findCreateFromDropdown().vm.$emit('input', updatedCreateFrom);
-
- expect(store.state.editNew.createFrom).toBe(updatedCreateFrom);
- });
- });
- });
-
- describe('"Annotated Tag" field', () => {
- beforeEach(() => {
- createComponent(mountExtended);
- });
-
- it('renders a label', () => {
- expect(wrapper.findByRole('textbox', { name: 'Set tag message' }).exists()).toBe(true);
- });
-
- it('renders a description', () => {
- expect(trimText(findAnnotatedTagMessageFormGroup().text())).toContain(
- 'Add a message to the tag. Leaving this blank creates a lightweight tag.',
- );
- });
-
- it('updates the store', async () => {
- await findAnnotatedTagMessageFormGroup().find('textarea').setValue(TEST_TAG_MESSAGE);
-
- expect(store.state.editNew.release.tagMessage).toBe(TEST_TAG_MESSAGE);
- });
-
- it('shows a link', () => {
- const link = wrapper.findByRole('link', {
- name: 'lightweight tag',
- });
-
- expect(link.attributes('href')).toBe('https://git-scm.com/book/en/v2/Git-Basics-Tagging/');
- });
- });
});
diff --git a/spec/frontend/releases/components/tag_search_spec.js b/spec/frontend/releases/components/tag_search_spec.js
new file mode 100644
index 00000000000..4144a9cc297
--- /dev/null
+++ b/spec/frontend/releases/components/tag_search_spec.js
@@ -0,0 +1,144 @@
+import { GlButton, GlDropdownItem, GlSearchBoxByType } from '@gitlab/ui';
+import axios from 'axios';
+import MockAdapter from 'axios-mock-adapter';
+import { nextTick } from 'vue';
+import { mount } from '@vue/test-utils';
+import waitForPromises from 'helpers/wait_for_promises';
+import { DEFAULT_PER_PAGE } from '~/api';
+import { __, s__, sprintf } from '~/locale';
+import TagSearch from '~/releases/components/tag_search.vue';
+import createStore from '~/releases/stores';
+import createEditNewModule from '~/releases/stores/modules/edit_new';
+import { createRefModule } from '~/ref/stores';
+
+const TEST_TAG_NAME = 'test-tag-name';
+const TEST_PROJECT_ID = '1234';
+const TAGS = [{ name: 'v1' }, { name: 'v2' }, { name: 'v3' }];
+
+describe('releases/components/tag_search', () => {
+ let store;
+ let wrapper;
+ let mock;
+
+ const createWrapper = (propsData = {}) => {
+ wrapper = mount(TagSearch, {
+ store,
+ propsData,
+ });
+ };
+
+ beforeEach(() => {
+ store = createStore({
+ modules: {
+ editNew: createEditNewModule({
+ projectId: TEST_PROJECT_ID,
+ }),
+ ref: createRefModule(),
+ },
+ });
+
+ store.state.editNew.release = {};
+
+ mock = new MockAdapter(axios);
+ gon.api_version = 'v4';
+ });
+
+ afterEach(() => mock.restore());
+
+ const findSearch = () => wrapper.findComponent(GlSearchBoxByType);
+ const findCreate = () => wrapper.findAllComponents(GlButton).at(-1);
+ const findResults = () => wrapper.findAllComponents(GlDropdownItem);
+
+ describe('init', () => {
+ beforeEach(async () => {
+ mock
+ .onGet(`/api/v4/projects/${TEST_PROJECT_ID}/repository/tags`)
+ .reply(200, TAGS, { 'x-total': TAGS.length });
+
+ createWrapper();
+
+ await waitForPromises();
+ });
+
+ it('displays a set of results immediately', () => {
+ findResults().wrappers.forEach((w, i) => expect(w.text()).toBe(TAGS[i].name));
+ });
+
+ it('has a disabled button', () => {
+ const button = findCreate();
+ expect(button.text()).toBe(s__('Release|Or type a new tag name'));
+ expect(button.props('disabled')).toBe(true);
+ });
+
+ it('has an empty search input', () => {
+ expect(findSearch().props('value')).toBe('');
+ });
+
+ describe('searching', () => {
+ const query = TEST_TAG_NAME;
+
+ beforeEach(async () => {
+ mock.reset();
+ mock
+ .onGet(`/api/v4/projects/${TEST_PROJECT_ID}/repository/tags`, {
+ params: { search: query, per_page: DEFAULT_PER_PAGE },
+ })
+ .reply(200, [], { 'x-total': 0 });
+
+ findSearch().vm.$emit('input', query);
+
+ await nextTick();
+ await waitForPromises();
+ });
+
+ it('shows "No results found" when there are no results', () => {
+ expect(wrapper.text()).toContain(__('No results found'));
+ });
+
+ it('searches with the given input', () => {
+ expect(mock.history.get[0].params.search).toBe(query);
+ });
+
+ it('emits the query', () => {
+ expect(wrapper.emitted('change')).toEqual([[query]]);
+ });
+ });
+ });
+
+ describe('with query', () => {
+ const query = TEST_TAG_NAME;
+
+ beforeEach(async () => {
+ mock
+ .onGet(`/api/v4/projects/${TEST_PROJECT_ID}/repository/tags`, {
+ params: { search: query, per_page: DEFAULT_PER_PAGE },
+ })
+ .reply(200, TAGS, { 'x-total': TAGS.length });
+
+ createWrapper({ query });
+
+ await waitForPromises();
+ });
+
+ it('displays a set of results immediately', () => {
+ findResults().wrappers.forEach((w, i) => expect(w.text()).toBe(TAGS[i].name));
+ });
+
+ it('has an enabled button', () => {
+ const button = findCreate();
+ expect(button.text()).toMatchInterpolatedText(
+ sprintf(s__('Release|Create tag %{tag}'), { tag: query }),
+ );
+ expect(button.props('disabled')).toBe(false);
+ });
+
+ it('emits create event when button clicked', () => {
+ findCreate().vm.$emit('click');
+ expect(wrapper.emitted('create')).toEqual([[query]]);
+ });
+
+ it('has an empty search input', () => {
+ expect(findSearch().props('value')).toBe(query);
+ });
+ });
+});
diff --git a/spec/frontend/releases/stores/modules/detail/getters_spec.js b/spec/frontend/releases/stores/modules/detail/getters_spec.js
index 649e772f956..736eae13fb3 100644
--- a/spec/frontend/releases/stores/modules/detail/getters_spec.js
+++ b/spec/frontend/releases/stores/modules/detail/getters_spec.js
@@ -424,14 +424,27 @@ describe('Release edit/new getters', () => {
describe('formattedReleaseNotes', () => {
it.each`
- description | includeTagNotes | tagNotes | included
- ${'release notes'} | ${true} | ${'tag notes'} | ${true}
- ${'release notes'} | ${true} | ${''} | ${false}
- ${'release notes'} | ${false} | ${'tag notes'} | ${false}
+ description | includeTagNotes | tagNotes | included | showCreateFrom
+ ${'release notes'} | ${true} | ${'tag notes'} | ${true} | ${false}
+ ${'release notes'} | ${true} | ${''} | ${false} | ${false}
+ ${'release notes'} | ${false} | ${'tag notes'} | ${false} | ${false}
+ ${'release notes'} | ${true} | ${'tag notes'} | ${true} | ${true}
+ ${'release notes'} | ${true} | ${''} | ${false} | ${true}
+ ${'release notes'} | ${false} | ${'tag notes'} | ${false} | ${true}
`(
- 'should include tag notes=$included when includeTagNotes=$includeTagNotes and tagNotes=$tagNotes',
- ({ description, includeTagNotes, tagNotes, included }) => {
- const state = { release: { description }, includeTagNotes, tagNotes };
+ 'should include tag notes=$included when includeTagNotes=$includeTagNotes and tagNotes=$tagNotes and showCreateFrom=$showCreateFrom',
+ ({ description, includeTagNotes, tagNotes, included, showCreateFrom }) => {
+ let state;
+
+ if (showCreateFrom) {
+ state = {
+ release: { description, tagMessage: tagNotes },
+ includeTagNotes,
+ showCreateFrom,
+ };
+ } else {
+ state = { release: { description }, includeTagNotes, tagNotes, showCreateFrom };
+ }
const text = `### ${s__('Releases|Tag message')}\n\n${tagNotes}\n`;
if (included) {
diff --git a/spec/frontend/repository/components/blob_button_group_spec.js b/spec/frontend/repository/components/blob_button_group_spec.js
index 96dedd54126..2c63deb99c9 100644
--- a/spec/frontend/repository/components/blob_button_group_spec.js
+++ b/spec/frontend/repository/components/blob_button_group_spec.js
@@ -1,5 +1,6 @@
import { GlButton } from '@gitlab/ui';
import { mountExtended } from 'helpers/vue_test_utils_helper';
+import { stubComponent } from 'helpers/stub_component';
import BlobButtonGroup from '~/repository/components/blob_button_group.vue';
import DeleteBlobModal from '~/repository/components/delete_blob_modal.vue';
import UploadBlobModal from '~/repository/components/upload_blob_modal.vue';
@@ -26,7 +27,24 @@ const DEFAULT_INJECT = {
describe('BlobButtonGroup component', () => {
let wrapper;
+ let showUploadBlobModalMock;
+ let showDeleteBlobModalMock;
+
const createComponent = (props = {}) => {
+ showUploadBlobModalMock = jest.fn();
+ showDeleteBlobModalMock = jest.fn();
+
+ const UploadBlobModalStub = stubComponent(UploadBlobModal, {
+ methods: {
+ show: showUploadBlobModalMock,
+ },
+ });
+ const DeleteBlobModalStub = stubComponent(DeleteBlobModal, {
+ methods: {
+ show: showDeleteBlobModalMock,
+ },
+ });
+
wrapper = mountExtended(BlobButtonGroup, {
propsData: {
...DEFAULT_PROPS,
@@ -35,6 +53,10 @@ describe('BlobButtonGroup component', () => {
provide: {
...DEFAULT_INJECT,
},
+ stubs: {
+ UploadBlobModal: UploadBlobModalStub,
+ DeleteBlobModal: DeleteBlobModalStub,
+ },
});
};
@@ -57,8 +79,6 @@ describe('BlobButtonGroup component', () => {
describe('buttons', () => {
beforeEach(() => {
createComponent();
- jest.spyOn(findUploadBlobModal().vm, 'show');
- jest.spyOn(findDeleteBlobModal().vm, 'show');
});
it('renders both the replace and delete button', () => {
@@ -73,33 +93,31 @@ describe('BlobButtonGroup component', () => {
it('triggers the UploadBlobModal from the replace button', () => {
findReplaceButton().trigger('click');
- expect(findUploadBlobModal().vm.show).toHaveBeenCalled();
+ expect(showUploadBlobModalMock).toHaveBeenCalled();
});
it('triggers the DeleteBlobModal from the delete button', () => {
findDeleteButton().trigger('click');
- expect(findDeleteBlobModal().vm.show).toHaveBeenCalled();
+ expect(showDeleteBlobModalMock).toHaveBeenCalled();
});
describe('showForkSuggestion set to true', () => {
beforeEach(() => {
createComponent({ showForkSuggestion: true });
- jest.spyOn(findUploadBlobModal().vm, 'show');
- jest.spyOn(findDeleteBlobModal().vm, 'show');
});
it('does not trigger the UploadBlobModal from the replace button', () => {
findReplaceButton().trigger('click');
- expect(findUploadBlobModal().vm.show).not.toHaveBeenCalled();
+ expect(showUploadBlobModalMock).not.toHaveBeenCalled();
expect(wrapper.emitted().fork).toHaveLength(1);
});
it('does not trigger the DeleteBlobModal from the delete button', () => {
findDeleteButton().trigger('click');
- expect(findDeleteBlobModal().vm.show).not.toHaveBeenCalled();
+ expect(showDeleteBlobModalMock).not.toHaveBeenCalled();
expect(wrapper.emitted().fork).toHaveLength(1);
});
});
diff --git a/spec/frontend/repository/components/breadcrumbs_spec.js b/spec/frontend/repository/components/breadcrumbs_spec.js
index 8b7a7d91125..f4baa817d32 100644
--- a/spec/frontend/repository/components/breadcrumbs_spec.js
+++ b/spec/frontend/repository/components/breadcrumbs_spec.js
@@ -1,27 +1,60 @@
+import Vue, { nextTick } from 'vue';
+import VueApollo from 'vue-apollo';
import { GlDropdown } from '@gitlab/ui';
import { shallowMount, RouterLinkStub } from '@vue/test-utils';
-import { nextTick } from 'vue';
import Breadcrumbs from '~/repository/components/breadcrumbs.vue';
import UploadBlobModal from '~/repository/components/upload_blob_modal.vue';
import NewDirectoryModal from '~/repository/components/new_directory_modal.vue';
+import waitForPromises from 'helpers/wait_for_promises';
+
+import permissionsQuery from 'shared_queries/repository/permissions.query.graphql';
+import projectPathQuery from '~/repository/queries/project_path.query.graphql';
+
+import createApolloProvider from 'helpers/mock_apollo_helper';
const defaultMockRoute = {
name: 'blobPath',
};
+const TEST_PROJECT_PATH = 'test-project/path';
+
+Vue.use(VueApollo);
+
describe('Repository breadcrumbs component', () => {
let wrapper;
-
- const factory = (currentPath, extraProps = {}, mockRoute = {}) => {
- const $apollo = {
- queries: {
+ let permissionsQuerySpy;
+
+ const createPermissionsQueryResponse = ({
+ pushCode = false,
+ forkProject = false,
+ createMergeRequestIn = false,
+ } = {}) => ({
+ data: {
+ project: {
+ id: 1,
+ __typename: '__typename',
userPermissions: {
- loading: true,
+ __typename: '__typename',
+ pushCode,
+ forkProject,
+ createMergeRequestIn,
},
},
- };
+ },
+ });
+
+ const factory = (currentPath, extraProps = {}, mockRoute = {}) => {
+ const apolloProvider = createApolloProvider([[permissionsQuery, permissionsQuerySpy]]);
+
+ apolloProvider.clients.defaultClient.cache.writeQuery({
+ query: projectPathQuery,
+ data: {
+ projectPath: TEST_PROJECT_PATH,
+ },
+ });
wrapper = shallowMount(Breadcrumbs, {
+ apolloProvider,
propsData: {
currentPath,
...extraProps,
@@ -34,13 +67,29 @@ describe('Repository breadcrumbs component', () => {
defaultMockRoute,
...mockRoute,
},
- $apollo,
},
});
};
+ const findDropdown = () => wrapper.findComponent(GlDropdown);
const findUploadBlobModal = () => wrapper.findComponent(UploadBlobModal);
const findNewDirectoryModal = () => wrapper.findComponent(NewDirectoryModal);
+ const findRouterLink = () => wrapper.findAllComponents(RouterLinkStub);
+
+ beforeEach(() => {
+ permissionsQuerySpy = jest.fn().mockResolvedValue(createPermissionsQueryResponse());
+ });
+
+ it('queries for permissions', async () => {
+ factory('/');
+
+ // We need to wait for the projectPath query to resolve
+ await waitForPromises();
+
+ expect(permissionsQuerySpy).toHaveBeenCalledWith({
+ projectPath: TEST_PROJECT_PATH,
+ });
+ });
it.each`
path | linkCount
@@ -51,7 +100,7 @@ describe('Repository breadcrumbs component', () => {
`('renders $linkCount links for path $path', ({ path, linkCount }) => {
factory(path);
- expect(wrapper.findAllComponents(RouterLinkStub).length).toEqual(linkCount);
+ expect(findRouterLink().length).toEqual(linkCount);
});
it.each`
@@ -64,36 +113,27 @@ describe('Repository breadcrumbs component', () => {
'links to the correct router path when routeName is $routeName',
({ routeName, path, linkTo }) => {
factory(path, {}, { name: routeName });
- expect(wrapper.findAllComponents(RouterLinkStub).at(3).props('to')).toEqual(linkTo);
+ expect(findRouterLink().at(3).props('to')).toEqual(linkTo);
},
);
it('escapes hash in directory path', () => {
factory('app/assets/javascripts#');
- expect(wrapper.findAllComponents(RouterLinkStub).at(3).props('to')).toEqual(
- '/-/tree/app/assets/javascripts%23',
- );
+ expect(findRouterLink().at(3).props('to')).toEqual('/-/tree/app/assets/javascripts%23');
});
it('renders last link as active', () => {
factory('app/assets');
- expect(wrapper.findAllComponents(RouterLinkStub).at(2).attributes('aria-current')).toEqual(
- 'page',
- );
+ expect(findRouterLink().at(2).attributes('aria-current')).toEqual('page');
});
it('does not render add to tree dropdown when permissions are false', async () => {
- factory('/', { canCollaborate: false });
-
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({ userPermissions: { forkProject: false, createMergeRequestIn: false } });
-
+ factory('/', { canCollaborate: false }, {});
await nextTick();
- expect(wrapper.findComponent(GlDropdown).exists()).toBe(false);
+ expect(findDropdown().exists()).toBe(false);
});
it.each`
@@ -107,20 +147,19 @@ describe('Repository breadcrumbs component', () => {
'does render add to tree dropdown $isRendered when route is $routeName',
({ routeName, isRendered }) => {
factory('app/assets/javascripts.js', { canCollaborate: true }, { name: routeName });
- expect(wrapper.findComponent(GlDropdown).exists()).toBe(isRendered);
+ expect(findDropdown().exists()).toBe(isRendered);
},
);
it('renders add to tree dropdown when permissions are true', async () => {
- factory('/', { canCollaborate: 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({ userPermissions: { forkProject: true, createMergeRequestIn: true } });
+ permissionsQuerySpy.mockResolvedValue(
+ createPermissionsQueryResponse({ forkProject: true, createMergeRequestIn: true }),
+ );
+ factory('/', { canCollaborate: true });
await nextTick();
- expect(wrapper.findComponent(GlDropdown).exists()).toBe(true);
+ expect(findDropdown().exists()).toBe(true);
});
describe('renders the upload blob modal', () => {
@@ -133,10 +172,6 @@ describe('Repository breadcrumbs component', () => {
});
it('renders the modal once loaded', 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({ $apollo: { queries: { userPermissions: { loading: false } } } });
-
await nextTick();
expect(findUploadBlobModal().exists()).toBe(true);
@@ -152,10 +187,6 @@ describe('Repository breadcrumbs component', () => {
});
it('renders the modal once loaded', 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({ $apollo: { queries: { userPermissions: { loading: false } } } });
-
await nextTick();
expect(findNewDirectoryModal().exists()).toBe(true);
diff --git a/spec/frontend/repository/components/delete_blob_modal_spec.js b/spec/frontend/repository/components/delete_blob_modal_spec.js
index 9ca45bfb655..90f2150222c 100644
--- a/spec/frontend/repository/components/delete_blob_modal_spec.js
+++ b/spec/frontend/repository/components/delete_blob_modal_spec.js
@@ -182,13 +182,13 @@ describe('DeleteBlobModal', () => {
await fillForm({ targetText: '', commitText: '' });
});
- it('disables submit button', async () => {
+ it('disables submit button', () => {
expect(findModal().props('actionPrimary').attributes).toEqual(
expect.objectContaining({ disabled: true }),
);
});
- it('does not submit form', async () => {
+ it('does not submit form', () => {
findModal().vm.$emit('primary', { preventDefault: () => {} });
expect(submitSpy).not.toHaveBeenCalled();
});
@@ -202,13 +202,13 @@ describe('DeleteBlobModal', () => {
});
});
- it('enables submit button', async () => {
+ it('enables submit button', () => {
expect(findModal().props('actionPrimary').attributes).toEqual(
expect.objectContaining({ disabled: false }),
);
});
- it('submits form', async () => {
+ it('submits form', () => {
findModal().vm.$emit('primary', { preventDefault: () => {} });
expect(submitSpy).toHaveBeenCalled();
});
diff --git a/spec/frontend/repository/components/fork_info_spec.js b/spec/frontend/repository/components/fork_info_spec.js
index 7a2b03a8d8f..8521f91a6c7 100644
--- a/spec/frontend/repository/components/fork_info_spec.js
+++ b/spec/frontend/repository/components/fork_info_spec.js
@@ -1,23 +1,29 @@
-import Vue from 'vue';
+import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import { GlSkeletonLoader, GlIcon, GlLink, GlSprintf, GlButton, GlLoadingIcon } from '@gitlab/ui';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { stubComponent } from 'helpers/stub_component';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import { createAlert } from '~/alert';
+import { createAlert, VARIANT_INFO } from '~/alert';
import ForkInfo, { i18n } from '~/repository/components/fork_info.vue';
import ConflictsModal from '~/repository/components/fork_sync_conflicts_modal.vue';
import forkDetailsQuery from '~/repository/queries/fork_details.query.graphql';
import syncForkMutation from '~/repository/mutations/sync_fork.mutation.graphql';
+import eventHub from '~/repository/event_hub';
+import {
+ POLLING_INTERVAL_DEFAULT,
+ POLLING_INTERVAL_BACKOFF,
+ FORK_UPDATED_EVENT,
+} from '~/repository/constants';
import { propsForkInfo } from '../mock_data';
jest.mock('~/alert');
describe('ForkInfo component', () => {
let wrapper;
- let mockResolver;
+ let mockForkDetailsQuery;
const forkInfoError = new Error('Something went wrong');
const projectId = 'gid://gitlab/Project/1';
const showMock = jest.fn();
@@ -25,14 +31,19 @@ describe('ForkInfo component', () => {
Vue.use(VueApollo);
- const createForkDetailsData = (
+ const waitForPolling = async (interval = POLLING_INTERVAL_DEFAULT) => {
+ jest.advanceTimersByTime(interval);
+ await waitForPromises();
+ };
+
+ const mockResolvedForkDetailsQuery = (
forkDetails = { ahead: 3, behind: 7, isSyncing: false, hasConflicts: false },
) => {
- return {
+ mockForkDetailsQuery.mockResolvedValue({
data: {
project: { id: projectId, forkDetails },
},
- };
+ });
};
const createSyncForkDetailsData = (
@@ -45,14 +56,10 @@ describe('ForkInfo component', () => {
};
};
- const createComponent = (props = {}, data = {}, mutationData = {}, isRequestFailed = false) => {
- mockResolver = isRequestFailed
- ? jest.fn().mockRejectedValue(forkInfoError)
- : jest.fn().mockResolvedValue(createForkDetailsData(data));
-
+ const createComponent = (props = {}, mutationData = {}) => {
wrapper = shallowMountExtended(ForkInfo, {
apolloProvider: createMockApollo([
- [forkDetailsQuery, mockResolver],
+ [forkDetailsQuery, mockForkDetailsQuery],
[syncForkMutation, jest.fn().mockResolvedValue(createSyncForkDetailsData(mutationData))],
]),
propsData: { ...propsForkInfo, ...props },
@@ -77,13 +84,24 @@ describe('ForkInfo component', () => {
const findLink = () => wrapper.findComponent(GlLink);
const findSkeleton = () => wrapper.findComponent(GlSkeletonLoader);
const findIcon = () => wrapper.findComponent(GlIcon);
- const findUpdateForkButton = () => wrapper.findComponent(GlButton);
+ const findUpdateForkButton = () => wrapper.findByTestId('update-fork-button');
+ const findCreateMrButton = () => wrapper.findByTestId('create-mr-button');
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
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 () => {
+ const startForkUpdate = async () => {
+ findUpdateForkButton().vm.$emit('click');
+ await waitForPromises();
+ };
+
+ beforeEach(() => {
+ mockForkDetailsQuery = jest.fn();
+ mockResolvedForkDetailsQuery();
+ });
+
+ it('displays a skeleton while loading data', () => {
createComponent();
expect(findSkeleton().exists()).toBe(true);
});
@@ -100,12 +118,12 @@ describe('ForkInfo component', () => {
it('queries the data when sourceName is present', async () => {
await createComponent();
- expect(mockResolver).toHaveBeenCalled();
+ expect(mockForkDetailsQuery).toHaveBeenCalled();
});
it('does not query the data when sourceName is empty', async () => {
await createComponent({ sourceName: null });
- expect(mockResolver).not.toHaveBeenCalled();
+ expect(mockForkDetailsQuery).not.toHaveBeenCalled();
});
it('renders inaccessible message when fork source is not available', async () => {
@@ -122,48 +140,91 @@ describe('ForkInfo component', () => {
expect(link.attributes('href')).toBe(propsForkInfo.sourcePath);
});
- describe('Unknown divergence', () => {
- beforeEach(async () => {
- await createComponent(
- {},
- { ahead: null, behind: null, isSyncing: false, hasConflicts: false },
- );
+ it('renders Create MR Button with correct path', async () => {
+ await createComponent();
+ expect(findCreateMrButton().attributes('href')).toBe(propsForkInfo.createMrPath);
+ });
+
+ it('does not render create MR button if user had no permission to Create MR in fork', async () => {
+ await createComponent({ canUserCreateMrInFork: false });
+ expect(findCreateMrButton().exists()).toBe(false);
+ });
+
+ it('renders alert with error message when request fails', async () => {
+ mockForkDetailsQuery.mockRejectedValue(forkInfoError);
+ await createComponent({});
+ expect(createAlert).toHaveBeenCalledWith({
+ message: i18n.error,
+ captureError: true,
+ error: forkInfoError,
});
+ });
+ describe('Unknown divergence', () => {
it('renders unknown divergence message when divergence is unknown', async () => {
+ mockResolvedForkDetailsQuery({
+ ahead: null,
+ behind: null,
+ isSyncing: false,
+ hasConflicts: false,
+ });
+ await createComponent({});
expect(findDivergenceMessage().text()).toBe(i18n.unknown);
});
it('renders Update Fork button', async () => {
+ mockResolvedForkDetailsQuery({
+ ahead: null,
+ behind: null,
+ isSyncing: false,
+ hasConflicts: false,
+ });
+ await createComponent({});
expect(findUpdateForkButton().exists()).toBe(true);
- expect(findUpdateForkButton().text()).toBe(i18n.sync);
+ expect(findUpdateForkButton().text()).toBe(i18n.updateFork);
});
});
describe('Up to date divergence', () => {
beforeEach(async () => {
+ mockResolvedForkDetailsQuery({ ahead: 0, behind: 0, isSyncing: false, hasConflicts: false });
await createComponent({}, { ahead: 0, behind: 0, isSyncing: false, hasConflicts: false });
});
- it('renders up to date message when fork is up to date', async () => {
+ it('renders up to date message when fork is up to date', () => {
expect(findDivergenceMessage().text()).toBe(i18n.upToDate);
});
- it('does not render Update Fork button', async () => {
+ it('does not render Update Fork button', () => {
expect(findUpdateForkButton().exists()).toBe(false);
});
});
describe('Limited visibility project', () => {
beforeEach(async () => {
+ mockResolvedForkDetailsQuery(null);
await createComponent({}, null);
});
- it('renders limited visibility messsage when forkDetails are empty', async () => {
+ it('renders limited visibility message when forkDetails are empty', () => {
expect(findDivergenceMessage().text()).toBe(i18n.limitedVisibility);
});
- it('does not render Update Fork button', async () => {
+ it('does not render Update Fork button', () => {
+ expect(findUpdateForkButton().exists()).toBe(false);
+ });
+ });
+
+ describe('User cannot sync the branch', () => {
+ beforeEach(async () => {
+ mockResolvedForkDetailsQuery({ ahead: 0, behind: 7, isSyncing: false, hasConflicts: false });
+ await createComponent(
+ { canSyncBranch: false },
+ { ahead: 0, behind: 7, isSyncing: false, hasConflicts: false },
+ );
+ });
+
+ it('does not render Update Fork button', () => {
expect(findUpdateForkButton().exists()).toBe(false);
});
});
@@ -175,7 +236,8 @@ describe('ForkInfo component', () => {
message: '3 commits behind, 7 commits ahead of the upstream repository.',
firstLink: propsForkInfo.behindComparePath,
secondLink: propsForkInfo.aheadComparePath,
- hasButton: true,
+ hasUpdateButton: true,
+ hasCreateMrButton: true,
},
{
ahead: 7,
@@ -183,7 +245,8 @@ describe('ForkInfo component', () => {
message: '7 commits ahead of the upstream repository.',
firstLink: propsForkInfo.aheadComparePath,
secondLink: '',
- hasButton: false,
+ hasUpdateButton: false,
+ hasCreateMrButton: true,
},
{
ahead: 0,
@@ -191,13 +254,15 @@ describe('ForkInfo component', () => {
message: '3 commits behind the upstream repository.',
firstLink: propsForkInfo.behindComparePath,
secondLink: '',
- hasButton: true,
+ hasUpdateButton: true,
+ hasCreateMrButton: false,
},
])(
'renders correct divergence message for ahead: $ahead, behind: $behind divergence commits',
- ({ ahead, behind, message, firstLink, secondLink, hasButton }) => {
+ ({ ahead, behind, message, firstLink, secondLink, hasUpdateButton, hasCreateMrButton }) => {
beforeEach(async () => {
- await createComponent({}, { ahead, behind, isSyncing: false, hasConflicts: false });
+ mockResolvedForkDetailsQuery({ ahead, behind, isSyncing: false, hasConflicts: false });
+ await createComponent({});
});
it('displays correct text', () => {
@@ -214,17 +279,25 @@ describe('ForkInfo component', () => {
});
it('renders Update Fork button when fork is behind', () => {
- expect(findUpdateForkButton().exists()).toBe(hasButton);
- if (hasButton) {
- expect(findUpdateForkButton().text()).toBe(i18n.sync);
+ expect(findUpdateForkButton().exists()).toBe(hasUpdateButton);
+ if (hasUpdateButton) {
+ expect(findUpdateForkButton().text()).toBe(i18n.updateFork);
+ }
+ });
+
+ it('renders Create Merge Request button when fork is ahead', () => {
+ expect(findCreateMrButton().exists()).toBe(hasCreateMrButton);
+ if (hasCreateMrButton) {
+ expect(findCreateMrButton().text()).toBe(i18n.createMergeRequest);
}
});
},
);
describe('when sync is not possible due to conflicts', () => {
- it('opens Conflicts Modal', async () => {
- await createComponent({}, { ahead: 7, behind: 3, isSyncing: false, hasConflicts: true });
+ it('Opens Conflicts Modal', async () => {
+ mockResolvedForkDetailsQuery({ ahead: 7, behind: 3, isSyncing: false, hasConflicts: true });
+ await createComponent({});
findUpdateForkButton().vm.$emit('click');
expect(showMock).toHaveBeenCalled();
});
@@ -232,24 +305,71 @@ describe('ForkInfo component', () => {
describe('projectSyncFork mutation', () => {
it('changes button to have loading state', async () => {
- await createComponent(
- {},
- { ahead: 0, behind: 3, isSyncing: false, hasConflicts: false },
- { ahead: 0, behind: 3, isSyncing: true, hasConflicts: false },
- );
+ await createComponent({}, { ahead: 0, behind: 3, isSyncing: true, hasConflicts: false });
+ mockResolvedForkDetailsQuery({ ahead: 0, behind: 3, isSyncing: false, hasConflicts: false });
expect(findLoadingIcon().exists()).toBe(false);
- findUpdateForkButton().vm.$emit('click');
- await waitForPromises();
+ await startForkUpdate();
expect(findLoadingIcon().exists()).toBe(true);
});
});
- it('renders alert with error message when request fails', async () => {
- await createComponent({}, {}, true);
- expect(createAlert).toHaveBeenCalledWith({
- message: i18n.error,
- captureError: true,
- error: forkInfoError,
+ describe('polling', () => {
+ beforeEach(async () => {
+ await createComponent({}, { ahead: 0, behind: 3, isSyncing: true, hasConflicts: false });
+ mockResolvedForkDetailsQuery({ ahead: 0, behind: 3, isSyncing: true, hasConflicts: false });
+ });
+
+ it('fetches data on the initial load', () => {
+ expect(mockForkDetailsQuery).toHaveBeenCalledTimes(1);
+ });
+
+ it('starts polling after sync button is clicked', async () => {
+ await startForkUpdate();
+ await waitForPolling();
+ expect(mockForkDetailsQuery).toHaveBeenCalledTimes(2);
+
+ await waitForPolling(POLLING_INTERVAL_DEFAULT * POLLING_INTERVAL_BACKOFF);
+ expect(mockForkDetailsQuery).toHaveBeenCalledTimes(3);
+ });
+
+ it('stops polling once sync is finished', async () => {
+ mockResolvedForkDetailsQuery({ ahead: 0, behind: 0, isSyncing: false, hasConflicts: false });
+ await startForkUpdate();
+ await waitForPolling();
+ expect(mockForkDetailsQuery).toHaveBeenCalledTimes(2);
+ await waitForPolling(POLLING_INTERVAL_DEFAULT * POLLING_INTERVAL_BACKOFF);
+ expect(mockForkDetailsQuery).toHaveBeenCalledTimes(2);
+ await nextTick();
+ });
+ });
+
+ describe('once fork is updated', () => {
+ beforeEach(async () => {
+ await createComponent({}, { ahead: 0, behind: 3, isSyncing: true, hasConflicts: false });
+ mockResolvedForkDetailsQuery({ ahead: 0, behind: 0, isSyncing: false, hasConflicts: false });
+ });
+
+ it('shows info alert once the fork is updated', async () => {
+ await startForkUpdate();
+ await waitForPolling();
+ expect(createAlert).toHaveBeenCalledWith({
+ message: i18n.successMessage,
+ variant: VARIANT_INFO,
+ });
+ });
+
+ it('emits fork:updated event to eventHub', async () => {
+ jest.spyOn(eventHub, '$emit').mockImplementation();
+ await startForkUpdate();
+ await waitForPolling();
+ expect(eventHub.$emit).toHaveBeenCalledWith(FORK_UPDATED_EVENT);
+ });
+
+ it('hides update fork button', async () => {
+ jest.spyOn(eventHub, '$emit').mockImplementation();
+ await startForkUpdate();
+ await waitForPolling();
+ expect(findUpdateForkButton().exists()).toBe(false);
});
});
});
diff --git a/spec/frontend/repository/components/fork_sync_conflicts_modal_spec.js b/spec/frontend/repository/components/fork_sync_conflicts_modal_spec.js
index f97c970275b..3fd9284e29b 100644
--- a/spec/frontend/repository/components/fork_sync_conflicts_modal_spec.js
+++ b/spec/frontend/repository/components/fork_sync_conflicts_modal_spec.js
@@ -36,7 +36,11 @@ describe('ConflictsModal', () => {
expect(findInstructions().at(0).text()).toContain(propsConflictsModal.sourcePath);
});
- it('renders default branch name in a first intruction', () => {
+ it('renders default branch name in a first step intruction', () => {
expect(findInstructions().at(0).text()).toContain(propsConflictsModal.sourceDefaultBranch);
});
+
+ it('renders selected branch name in a second step intruction', () => {
+ expect(findInstructions().at(1).text()).toContain(propsConflictsModal.selectedBranch);
+ });
});
diff --git a/spec/frontend/repository/components/last_commit_spec.js b/spec/frontend/repository/components/last_commit_spec.js
index f16edcb0b7c..c207d32d61d 100644
--- a/spec/frontend/repository/components/last_commit_spec.js
+++ b/spec/frontend/repository/components/last_commit_spec.js
@@ -4,11 +4,12 @@ import { GlLoadingIcon } from '@gitlab/ui';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-
import LastCommit from '~/repository/components/last_commit.vue';
import SignatureBadge from '~/commit/components/signature_badge.vue';
import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
+import eventHub from '~/repository/event_hub';
import pathLastCommitQuery from 'shared_queries/repository/path_last_commit.query.graphql';
+import { FORK_UPDATED_EVENT } from '~/repository/constants';
import { refMock } from '../mock_data';
let wrapper;
@@ -100,7 +101,7 @@ const createCommitData = ({
};
};
-const createComponent = async (data = {}) => {
+const createComponent = (data = {}) => {
Vue.use(VueApollo);
const currentPath = 'path';
@@ -180,6 +181,25 @@ describe('Repository last commit component', () => {
expect(findCommitRowDescription().exists()).toBe(false);
});
+ describe('created', () => {
+ it('binds `epicsListScrolled` event listener via eventHub', () => {
+ jest.spyOn(eventHub, '$on').mockImplementation(() => {});
+ createComponent();
+
+ expect(eventHub.$on).toHaveBeenCalledWith(FORK_UPDATED_EVENT, expect.any(Function));
+ });
+ });
+
+ describe('beforeDestroy', () => {
+ it('unbinds `epicsListScrolled` event listener via eventHub', () => {
+ jest.spyOn(eventHub, '$off').mockImplementation(() => {});
+ createComponent();
+ wrapper.destroy();
+
+ expect(eventHub.$off).toHaveBeenCalledWith(FORK_UPDATED_EVENT, expect.any(Function));
+ });
+ });
+
describe('when the description is present', () => {
beforeEach(async () => {
createComponent({ descriptionHtml: '&#x000A;Update ADOPTERS.md' });
diff --git a/spec/frontend/repository/components/new_directory_modal_spec.js b/spec/frontend/repository/components/new_directory_modal_spec.js
index c920159375f..55a24089d48 100644
--- a/spec/frontend/repository/components/new_directory_modal_spec.js
+++ b/spec/frontend/repository/components/new_directory_modal_spec.js
@@ -124,7 +124,7 @@ describe('NewDirectoryModal', () => {
});
describe('form submission', () => {
- beforeEach(async () => {
+ beforeEach(() => {
mock = new MockAdapter(axios);
});
diff --git a/spec/frontend/repository/components/preview/index_spec.js b/spec/frontend/repository/components/preview/index_spec.js
index 8a88c5b9c61..316ddfb5731 100644
--- a/spec/frontend/repository/components/preview/index_spec.js
+++ b/spec/frontend/repository/components/preview/index_spec.js
@@ -54,7 +54,7 @@ describe('Repository file preview component', () => {
expect(handleLocationHash).toHaveBeenCalled();
});
- it('renders loading icon', async () => {
+ it('renders loading icon', () => {
expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
});
});
diff --git a/spec/frontend/repository/components/table/row_spec.js b/spec/frontend/repository/components/table/row_spec.js
index 055616d6e8e..02b505c828c 100644
--- a/spec/frontend/repository/components/table/row_spec.js
+++ b/spec/frontend/repository/components/table/row_spec.js
@@ -1,7 +1,10 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
import { GlBadge, GlLink, GlIcon, GlIntersectionObserver } from '@gitlab/ui';
import { shallowMount, RouterLinkStub } from '@vue/test-utils';
-import { nextTick } from 'vue';
+import refQuery from '~/repository/queries/ref.query.graphql';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
+import createMockApollo from 'helpers/mock_apollo_helper';
import TableRow from '~/repository/components/table/row.vue';
import FileIcon from '~/vue_shared/components/file_icon.vue';
import { FILE_SYMLINK_MODE } from '~/vue_shared/constants';
@@ -9,23 +12,37 @@ import { ROW_APPEAR_DELAY } from '~/repository/constants';
const COMMIT_MOCK = { lockLabel: 'Locked by Root', committedDate: '2019-01-01' };
-let vm;
+let wrapper;
let $router;
-function factory(propsData = {}) {
+const createMockApolloProvider = (mockData) => {
+ Vue.use(VueApollo);
+ const apolloProver = createMockApollo([]);
+ apolloProver.clients.defaultClient.cache.writeQuery({ query: refQuery, data: { ...mockData } });
+
+ return apolloProver;
+};
+
+function factory({ mockData = { ref: 'main', escapedRef: 'main' }, propsData = {} } = {}) {
$router = {
push: jest.fn(),
};
- vm = shallowMount(TableRow, {
+ wrapper = shallowMount(TableRow, {
+ apolloProvider: createMockApolloProvider(mockData),
propsData: {
+ id: '1',
+ sha: '0as4k',
commitInfo: COMMIT_MOCK,
- ...propsData,
- name: propsData.path,
+ name: 'name',
+ currentPath: 'gitlab-org/gitlab-ce',
projectPath: 'gitlab-org/gitlab-ce',
url: `https://test.com`,
totalEntries: 10,
rowNumber: 123,
+ path: 'gitlab-org/gitlab-ce',
+ type: 'tree',
+ ...propsData,
},
directives: {
GlHoverLoad: createMockDirective('gl-hover-load'),
@@ -37,63 +54,67 @@ function factory(propsData = {}) {
RouterLink: RouterLinkStub,
},
});
-
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- vm.setData({ escapedRef: 'main' });
}
describe('Repository table row component', () => {
- const findRouterLink = () => vm.findComponent(RouterLinkStub);
- const findIntersectionObserver = () => vm.findComponent(GlIntersectionObserver);
+ const findIcon = () => wrapper.findComponent(GlIcon);
+ const findFileIcon = () => wrapper.findComponent(FileIcon);
+ const findBadge = () => wrapper.findComponent(GlBadge);
+ const findRouterLink = () => wrapper.findComponent(RouterLinkStub);
+ const findIntersectionObserver = () => wrapper.findComponent(GlIntersectionObserver);
- it('renders table row', async () => {
+ it('renders table row', () => {
factory({
- id: '1',
- sha: '123',
- path: 'test',
- type: 'file',
- currentPath: '/',
+ propsData: {
+ id: '1',
+ sha: '123',
+ path: 'test',
+ type: 'file',
+ currentPath: '/',
+ },
});
- await nextTick();
- expect(vm.element).toMatchSnapshot();
+ expect(wrapper.element).toMatchSnapshot();
});
- it('renders a symlink table row', async () => {
+ it('renders a symlink table row', () => {
factory({
- id: '1',
- sha: '123',
- path: 'test',
- type: 'blob',
- currentPath: '/',
- mode: FILE_SYMLINK_MODE,
+ propsData: {
+ id: '1',
+ sha: '123',
+ path: 'test',
+ type: 'blob',
+ currentPath: '/',
+ mode: FILE_SYMLINK_MODE,
+ },
});
- await nextTick();
- expect(vm.element).toMatchSnapshot();
+ expect(wrapper.element).toMatchSnapshot();
});
- it('renders table row for path with special character', async () => {
+ it('renders table row for path with special character', () => {
factory({
- id: '1',
- sha: '123',
- path: 'test$/test',
- type: 'file',
- currentPath: 'test$',
+ propsData: {
+ id: '1',
+ sha: '123',
+ path: 'test$/test',
+ type: 'file',
+ currentPath: 'test$',
+ },
});
- await nextTick();
- expect(vm.element).toMatchSnapshot();
+ expect(wrapper.element).toMatchSnapshot();
});
it('renders a gl-hover-load directive', () => {
factory({
- id: '1',
- sha: '123',
- path: 'test',
- type: 'blob',
- currentPath: '/',
+ propsData: {
+ id: '1',
+ sha: '123',
+ path: 'test',
+ type: 'blob',
+ currentPath: '/',
+ },
});
const hoverLoadDirective = getBinding(findRouterLink().element, 'gl-hover-load');
@@ -107,150 +128,162 @@ describe('Repository table row component', () => {
${'tree'} | ${RouterLinkStub} | ${'RouterLink'}
${'blob'} | ${RouterLinkStub} | ${'RouterLink'}
${'commit'} | ${'a'} | ${'hyperlink'}
- `('renders a $componentName for type $type', async ({ type, component }) => {
+ `('renders a $componentName for type $type', ({ type, component }) => {
factory({
- id: '1',
- sha: '123',
- path: 'test',
- type,
- currentPath: '/',
+ propsData: {
+ id: '1',
+ sha: '123',
+ path: 'test',
+ type,
+ currentPath: '/',
+ },
});
- await nextTick();
- expect(vm.findComponent(component).exists()).toBe(true);
+ expect(wrapper.findComponent(component).exists()).toBe(true);
});
it.each`
path
${'test#'}
${'Änderungen'}
- `('renders link for $path', async ({ path }) => {
+ `('renders link for $path', ({ path }) => {
factory({
- id: '1',
- sha: '123',
- path,
- type: 'tree',
- currentPath: '/',
+ propsData: {
+ id: '1',
+ sha: '123',
+ path,
+ type: 'tree',
+ currentPath: '/',
+ },
});
- await nextTick();
- expect(vm.findComponent({ ref: 'link' }).props('to')).toEqual({
+ expect(wrapper.findComponent({ ref: 'link' }).props('to')).toEqual({
path: `/-/tree/main/${encodeURIComponent(path)}`,
});
});
- it('renders link for directory with hash', async () => {
+ it('renders link for directory with hash', () => {
factory({
- id: '1',
- sha: '123',
- path: 'test#',
- type: 'tree',
- currentPath: '/',
+ propsData: {
+ id: '1',
+ sha: '123',
+ path: 'test#',
+ type: 'tree',
+ currentPath: '/',
+ },
});
- await nextTick();
- expect(vm.find('.tree-item-link').props('to')).toEqual({ path: '/-/tree/main/test%23' });
+ expect(wrapper.find('.tree-item-link').props('to')).toEqual({ path: '/-/tree/main/test%23' });
});
- it('renders commit ID for submodule', async () => {
+ it('renders commit ID for submodule', () => {
factory({
- id: '1',
- sha: '123',
- path: 'test',
- type: 'commit',
- currentPath: '/',
+ propsData: {
+ id: '1',
+ sha: '123',
+ path: 'test',
+ type: 'commit',
+ currentPath: '/',
+ },
});
- await nextTick();
- expect(vm.find('.commit-sha').text()).toContain('1');
+ expect(wrapper.find('.commit-sha').text()).toContain('1');
});
- it('renders link with href', async () => {
+ it('renders link with href', () => {
factory({
- id: '1',
- sha: '123',
- path: 'test',
- type: 'blob',
- url: 'https://test.com',
- currentPath: '/',
+ propsData: {
+ id: '1',
+ sha: '123',
+ path: 'test',
+ type: 'blob',
+ url: 'https://test.com',
+ currentPath: '/',
+ },
});
- await nextTick();
- expect(vm.find('a').attributes('href')).toEqual('https://test.com');
+ expect(wrapper.find('a').attributes('href')).toEqual('https://test.com');
});
- it('renders LFS badge', async () => {
+ it('renders LFS badge', () => {
factory({
- id: '1',
- sha: '123',
- path: 'test',
- type: 'commit',
- currentPath: '/',
- lfsOid: '1',
+ propsData: {
+ id: '1',
+ sha: '123',
+ path: 'test',
+ type: 'commit',
+ currentPath: '/',
+ lfsOid: '1',
+ },
});
- await nextTick();
- expect(vm.findComponent(GlBadge).exists()).toBe(true);
+ expect(findBadge().exists()).toBe(true);
});
- it('renders commit and web links with href for submodule', async () => {
+ it('renders commit and web links with href for submodule', () => {
factory({
- id: '1',
- sha: '123',
- path: 'test',
- type: 'commit',
- url: 'https://test.com',
- submoduleTreeUrl: 'https://test.com/commit',
- currentPath: '/',
+ propsData: {
+ id: '1',
+ sha: '123',
+ path: 'test',
+ type: 'commit',
+ url: 'https://test.com',
+ submoduleTreeUrl: 'https://test.com/commit',
+ currentPath: '/',
+ },
});
- await nextTick();
- expect(vm.find('a').attributes('href')).toEqual('https://test.com');
- expect(vm.findComponent(GlLink).attributes('href')).toEqual('https://test.com/commit');
+ expect(wrapper.find('a').attributes('href')).toEqual('https://test.com');
+ expect(wrapper.findComponent(GlLink).attributes('href')).toEqual('https://test.com/commit');
});
- it('renders lock icon', async () => {
+ it('renders lock icon', () => {
factory({
- id: '1',
- sha: '123',
- path: 'test',
- type: 'tree',
- currentPath: '/',
+ propsData: {
+ id: '1',
+ sha: '123',
+ path: 'test',
+ type: 'tree',
+ currentPath: '/',
+ },
});
- await nextTick();
- expect(vm.findComponent(GlIcon).exists()).toBe(true);
- expect(vm.findComponent(GlIcon).props('name')).toBe('lock');
+ expect(findIcon().exists()).toBe(true);
+ expect(findIcon().props('name')).toBe('lock');
});
it('renders loading icon when path is loading', () => {
factory({
- id: '1',
- sha: '1',
- path: 'test',
- type: 'tree',
- currentPath: '/',
- loadingPath: 'test',
+ propsData: {
+ id: '1',
+ sha: '1',
+ path: 'test',
+ type: 'tree',
+ currentPath: '/',
+ loadingPath: 'test',
+ },
});
- expect(vm.findComponent(FileIcon).props('loading')).toBe(true);
+ expect(findFileIcon().props('loading')).toBe(true);
});
describe('row visibility', () => {
beforeEach(() => {
factory({
- id: '1',
- sha: '1',
- path: 'test',
- type: 'tree',
- currentPath: '/',
- commitInfo: null,
+ propsData: {
+ id: '1',
+ sha: '1',
+ path: 'test',
+ type: 'tree',
+ currentPath: '/',
+ commitInfo: null,
+ },
});
});
afterAll(() => jest.useRealTimers());
- it('emits a `row-appear` event', async () => {
+ it('emits a `row-appear` event', () => {
const setTimeoutSpy = jest.spyOn(global, 'setTimeout');
findIntersectionObserver().vm.$emit('appear');
@@ -258,7 +291,7 @@ describe('Repository table row component', () => {
expect(setTimeoutSpy).toHaveBeenCalledTimes(1);
expect(setTimeoutSpy).toHaveBeenLastCalledWith(expect.any(Function), ROW_APPEAR_DELAY);
- expect(vm.emitted('row-appear')).toEqual([[123]]);
+ expect(wrapper.emitted('row-appear')).toEqual([[123]]);
});
});
});
diff --git a/spec/frontend/repository/components/tree_content_spec.js b/spec/frontend/repository/components/tree_content_spec.js
index 9597d8a7b77..5b874656d39 100644
--- a/spec/frontend/repository/components/tree_content_spec.js
+++ b/spec/frontend/repository/components/tree_content_spec.js
@@ -99,7 +99,7 @@ describe('Repository table component', () => {
describe('FileTable showMore', () => {
describe('when is present', () => {
- beforeEach(async () => {
+ beforeEach(() => {
factory('/');
});
diff --git a/spec/frontend/repository/mock_data.js b/spec/frontend/repository/mock_data.js
index 418a93a10cc..afa183c0616 100644
--- a/spec/frontend/repository/mock_data.js
+++ b/spec/frontend/repository/mock_data.js
@@ -123,12 +123,16 @@ export const propsForkInfo = {
selectedBranch: 'main',
sourceName: 'gitLab',
sourcePath: 'gitlab-org/gitlab',
+ canSyncBranch: true,
aheadComparePath: '/nataliia/myGitLab/-/compare/main...ref?from_project_id=1',
behindComparePath: 'gitlab-org/gitlab/-/compare/ref...main?from_project_id=2',
+ createMrPath: 'path/to/new/mr',
+ canUserCreateMrInFork: true,
};
export const propsConflictsModal = {
sourceDefaultBranch: 'branch-name',
sourceName: 'source-name',
sourcePath: 'path/to/project',
+ selectedBranch: 'my-branch',
};
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
deleted file mode 100644
index 204afc744e7..00000000000
--- a/spec/frontend/saved_replies/components/__snapshots__/list_item_spec.js.snap
+++ /dev/null
@@ -1,67 +0,0 @@
-// 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
- data-testid="saved-reply-name"
- >
- test
- </strong>
-
- <div
- class="gl-ml-auto"
- >
- <gl-button-stub
- aria-label="Edit"
- buttontextclasses=""
- category="primary"
- class="gl-mr-3"
- data-testid="saved-reply-edit-btn"
- icon="pencil"
- size="medium"
- title="Edit"
- to="[object Object]"
- variant="default"
- />
-
- <gl-button-stub
- aria-label="Delete"
- buttontextclasses=""
- category="secondary"
- data-testid="saved-reply-delete-btn"
- icon="remove"
- size="medium"
- title="Delete"
- variant="danger"
- />
- </div>
- </div>
-
- <div
- class="gl-mt-3 gl-font-monospace"
- >
- /assign_reviewer
- </div>
-
- <gl-modal-stub
- actionprimary="[object Object]"
- actionsecondary="[object Object]"
- arialabel=""
- dismisslabel="Close"
- modalclass=""
- modalid="delete-saved-reply-2"
- size="sm"
- title="Delete saved reply"
- titletag="h4"
- >
- <gl-sprintf-stub
- message="Are you sure you want to delete %{name}? This action cannot be undone."
- />
- </gl-modal-stub>
-</li>
-`;
diff --git a/spec/frontend/saved_replies/components/list_item_spec.js b/spec/frontend/saved_replies/components/list_item_spec.js
deleted file mode 100644
index f1ecdfecb15..00000000000
--- a/spec/frontend/saved_replies/components/list_item_spec.js
+++ /dev/null
@@ -1,50 +0,0 @@
-import Vue from 'vue';
-import VueApollo from 'vue-apollo';
-import { shallowMount } from '@vue/test-utils';
-import { GlModal } from '@gitlab/ui';
-import createMockApollo from 'helpers/mock_apollo_helper';
-import { createMockDirective } from 'helpers/vue_mock_directive';
-import waitForPromises from 'helpers/wait_for_promises';
-import ListItem from '~/saved_replies/components/list_item.vue';
-import deleteSavedReplyMutation from '~/saved_replies/queries/delete_saved_reply.mutation.graphql';
-
-let wrapper;
-let deleteSavedReplyMutationResponse;
-
-function createComponent(propsData = {}) {
- Vue.use(VueApollo);
-
- deleteSavedReplyMutationResponse = jest
- .fn()
- .mockResolvedValue({ data: { savedReplyDestroy: { errors: [] } } });
-
- return shallowMount(ListItem, {
- propsData,
- directives: {
- GlModal: createMockDirective('gl-modal'),
- },
- apolloProvider: createMockApollo([
- [deleteSavedReplyMutation, deleteSavedReplyMutationResponse],
- ]),
- });
-}
-
-describe('Saved replies list item component', () => {
- it('renders list item', async () => {
- wrapper = createComponent({ reply: { name: 'test', content: '/assign_reviewer' } });
-
- expect(wrapper.element).toMatchSnapshot();
- });
-
- describe('delete button', () => {
- it('calls Apollo mutate', async () => {
- wrapper = createComponent({ reply: { name: 'test', content: '/assign_reviewer', id: 1 } });
-
- wrapper.findComponent(GlModal).vm.$emit('primary');
-
- await waitForPromises();
-
- expect(deleteSavedReplyMutationResponse).toHaveBeenCalledWith({ id: 1 });
- });
- });
-});
diff --git a/spec/frontend/scripts/frontend/__fixtures__/locale/de/converted.json b/spec/frontend/scripts/frontend/__fixtures__/locale/de/converted.json
new file mode 100644
index 00000000000..570980b1c27
--- /dev/null
+++ b/spec/frontend/scripts/frontend/__fixtures__/locale/de/converted.json
@@ -0,0 +1,21 @@
+{
+ "domain": "app",
+ "locale_data": {
+ "app": {
+ "": {
+ "domain": "app",
+ "lang": "de"
+ },
+ " %{start} to %{end}": [
+ " %{start} bis %{end}"
+ ],
+ "%d Alert:": [
+ "%d Warnung:",
+ "%d Warnungen:"
+ ],
+ "Example": [
+ ""
+ ]
+ }
+ }
+}
diff --git a/spec/frontend/scripts/frontend/__fixtures__/locale/de/gitlab.po b/spec/frontend/scripts/frontend/__fixtures__/locale/de/gitlab.po
new file mode 100644
index 00000000000..fe80cb72c29
--- /dev/null
+++ b/spec/frontend/scripts/frontend/__fixtures__/locale/de/gitlab.po
@@ -0,0 +1,13 @@
+# Simple translated string
+msgid " %{start} to %{end}"
+msgstr " %{start} bis %{end}"
+
+# Simple translated, pluralized string
+msgid "%d Alert:"
+msgid_plural "%d Alerts:"
+msgstr[0] "%d Warnung:"
+msgstr[1] "%d Warnungen:"
+
+# Simple string without translation
+msgid "Example"
+msgstr ""
diff --git a/spec/frontend/scripts/frontend/po_to_json_spec.js b/spec/frontend/scripts/frontend/po_to_json_spec.js
new file mode 100644
index 00000000000..858e3c9d3c7
--- /dev/null
+++ b/spec/frontend/scripts/frontend/po_to_json_spec.js
@@ -0,0 +1,244 @@
+import { join } from 'path';
+import { tmpdir } from 'os';
+import { readFile, rm, mkdtemp, stat } from 'fs/promises';
+import {
+ convertPoToJed,
+ convertPoFileForLocale,
+ main,
+} from '../../../../scripts/frontend/po_to_json';
+
+describe('PoToJson', () => {
+ const LOCALE = 'de';
+ const LOCALE_DIR = join(__dirname, '__fixtures__/locale');
+ const PO_FILE = join(LOCALE_DIR, LOCALE, 'gitlab.po');
+ const CONVERTED_FILE = join(LOCALE_DIR, LOCALE, 'converted.json');
+ let DE_CONVERTED = null;
+
+ beforeAll(async () => {
+ DE_CONVERTED = Object.freeze(JSON.parse(await readFile(CONVERTED_FILE, 'utf-8')));
+ });
+
+ describe('tests writing to the file system', () => {
+ let resultDir = null;
+
+ afterEach(async () => {
+ if (resultDir) {
+ await rm(resultDir, { recursive: true, force: true });
+ }
+ });
+
+ beforeEach(async () => {
+ resultDir = await mkdtemp(join(tmpdir(), 'locale-test'));
+ });
+
+ describe('#main', () => {
+ it('throws without arguments', () => {
+ return expect(main()).rejects.toThrow(/doesn't seem to be a folder/);
+ });
+
+ it('throws if outputDir does not exist', () => {
+ return expect(
+ main({
+ localeRoot: LOCALE_DIR,
+ outputDir: 'i-do-not-exist',
+ }),
+ ).rejects.toThrow(/doesn't seem to be a folder/);
+ });
+
+ it('throws if localeRoot does not exist', () => {
+ return expect(
+ main({
+ localeRoot: 'i-do-not-exist',
+ outputDir: resultDir,
+ }),
+ ).rejects.toThrow(/doesn't seem to be a folder/);
+ });
+
+ it('converts folder of po files to app.js files', async () => {
+ expect((await stat(resultDir)).isDirectory()).toBe(true);
+ await main({ localeRoot: LOCALE_DIR, outputDir: resultDir });
+
+ const resultFile = join(resultDir, LOCALE, 'app.js');
+ expect((await stat(resultFile)).isFile()).toBe(true);
+
+ window.translations = null;
+ await import(resultFile);
+ expect(window.translations).toEqual(DE_CONVERTED);
+ });
+ });
+
+ describe('#convertPoFileForLocale', () => {
+ it('converts simple PO to app.js, which exposes translations on the window', async () => {
+ await convertPoFileForLocale({ locale: 'de', localeFile: PO_FILE, resultDir });
+
+ const resultFile = join(resultDir, 'app.js');
+ expect((await stat(resultFile)).isFile()).toBe(true);
+
+ window.translations = null;
+ await import(resultFile);
+ expect(window.translations).toEqual(DE_CONVERTED);
+ });
+ });
+ });
+
+ describe('#convertPoToJed', () => {
+ it('converts simple PO to JED compatible JSON', async () => {
+ const poContent = await readFile(PO_FILE, 'utf-8');
+
+ expect(convertPoToJed(poContent, LOCALE).jed).toEqual(DE_CONVERTED);
+ });
+
+ it('returns null for empty string', () => {
+ const poContent = '';
+
+ expect(convertPoToJed(poContent, LOCALE).jed).toEqual(null);
+ });
+
+ describe('PO File headers', () => {
+ it('parses headers properly', () => {
+ const poContent = `
+msgid ""
+msgstr ""
+"Project-Id-Version: gitlab-ee\\n"
+"Report-Msgid-Bugs-To: \\n"
+"X-Crowdin-Project: gitlab-ee\\n"
+`;
+
+ expect(convertPoToJed(poContent, LOCALE).jed).toEqual({
+ domain: 'app',
+ locale_data: {
+ app: {
+ '': {
+ 'Project-Id-Version': 'gitlab-ee',
+ 'Report-Msgid-Bugs-To': '',
+ 'X-Crowdin-Project': 'gitlab-ee',
+ domain: 'app',
+ lang: LOCALE,
+ },
+ },
+ },
+ });
+ });
+
+ // JED needs that property, hopefully we could get
+ // rid of this in a future iteration
+ it("exposes 'Plural-Forms' as 'plural_forms' for `jed`", () => {
+ const poContent = `
+msgid ""
+msgstr ""
+"Plural-Forms: nplurals=2; plural=(n != 1);\\n"
+`;
+
+ expect(convertPoToJed(poContent, LOCALE).jed).toEqual({
+ domain: 'app',
+ locale_data: {
+ app: {
+ '': {
+ 'Plural-Forms': 'nplurals=2; plural=(n != 1);',
+ plural_forms: 'nplurals=2; plural=(n != 1);',
+ domain: 'app',
+ lang: LOCALE,
+ },
+ },
+ },
+ });
+ });
+
+ it('removes POT-Creation-Date', () => {
+ const poContent = `
+msgid ""
+msgstr ""
+"Plural-Forms: nplurals=2; plural=(n != 1);\\n"
+`;
+
+ expect(convertPoToJed(poContent, LOCALE).jed).toEqual({
+ domain: 'app',
+ locale_data: {
+ app: {
+ '': {
+ 'Plural-Forms': 'nplurals=2; plural=(n != 1);',
+ plural_forms: 'nplurals=2; plural=(n != 1);',
+ domain: 'app',
+ lang: LOCALE,
+ },
+ },
+ },
+ });
+ });
+ });
+
+ describe('escaping', () => {
+ it('escapes quotes in msgid and translation', () => {
+ const poContent = `
+# Escaped quotes in msgid and msgstr
+msgid "Changes the title to \\"%{title_param}\\"."
+msgstr "Ändert den Titel in \\"%{title_param}\\"."
+`;
+
+ expect(convertPoToJed(poContent, LOCALE).jed).toEqual({
+ domain: 'app',
+ locale_data: {
+ app: {
+ '': {
+ domain: 'app',
+ lang: LOCALE,
+ },
+ 'Changes the title to \\"%{title_param}\\".': [
+ 'Ändert den Titel in \\"%{title_param}\\".',
+ ],
+ },
+ },
+ });
+ });
+
+ it('escapes backslashes in msgid and translation', () => {
+ const poContent = `
+# Escaped backslashes in msgid and msgstr
+msgid "Example: ssh\\\\:\\\\/\\\\/"
+msgstr "Beispiel: ssh\\\\:\\\\/\\\\/"
+`;
+
+ expect(convertPoToJed(poContent, LOCALE).jed).toEqual({
+ domain: 'app',
+ locale_data: {
+ app: {
+ '': {
+ domain: 'app',
+ lang: LOCALE,
+ },
+ 'Example: ssh\\\\:\\\\/\\\\/': ['Beispiel: ssh\\\\:\\\\/\\\\/'],
+ },
+ },
+ });
+ });
+
+ // This is potentially faulty behavior but demands further investigation
+ // See also the escapeMsgstr method
+ it('escapes \\n and \\t in translation', () => {
+ const poContent = `
+# Escaped \\n
+msgid "Outdent line"
+msgstr "Désindenter la ligne\\n"
+
+# Escaped \\t
+msgid "Headers"
+msgstr "Cabeçalhos\\t"
+`;
+
+ expect(convertPoToJed(poContent, LOCALE).jed).toEqual({
+ domain: 'app',
+ locale_data: {
+ app: {
+ '': {
+ domain: 'app',
+ lang: LOCALE,
+ },
+ Headers: ['Cabeçalhos\\t'],
+ 'Outdent line': ['Désindenter la ligne\\n'],
+ },
+ },
+ });
+ });
+ });
+ });
+});
diff --git a/spec/frontend/search/highlight_blob_search_result_spec.js b/spec/frontend/search/highlight_blob_search_result_spec.js
index 15cff436076..91fc97c15ae 100644
--- a/spec/frontend/search/highlight_blob_search_result_spec.js
+++ b/spec/frontend/search/highlight_blob_search_result_spec.js
@@ -1,11 +1,11 @@
+import htmlPipelineSchedulesEdit from 'test_fixtures/search/blob_search_result.html';
import setHighlightClass from '~/search/highlight_blob_search_result';
-import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
-const fixture = 'search/blob_search_result.html';
const searchKeyword = 'Send'; // spec/frontend/fixtures/search.rb#79
describe('search/highlight_blob_search_result', () => {
- beforeEach(() => loadHTMLFixture(fixture));
+ beforeEach(() => setHTMLFixture(htmlPipelineSchedulesEdit));
afterEach(() => {
resetHTMLFixture();
diff --git a/spec/frontend/search/mock_data.js b/spec/frontend/search/mock_data.js
index 0aa4f0e1c84..f8dd6f6df27 100644
--- a/spec/frontend/search/mock_data.js
+++ b/spec/frontend/search/mock_data.js
@@ -6,6 +6,7 @@ export const MOCK_QUERY = {
state: 'all',
confidential: null,
group_id: 1,
+ language: ['C', 'JavaScript'],
};
export const MOCK_GROUP = {
@@ -193,214 +194,6 @@ export const MOCK_NAVIGATION_ACTION_MUTATION = {
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 },
@@ -604,13 +397,27 @@ export const MOCK_LANGUAGE_AGGREGATIONS_BUCKETS = [
{ key: 'random-label-bdownton5j', count: 200 },
];
+export const MOCK_AGGREGATIONS = [
+ {
+ name: 'language',
+ buckets: MOCK_LANGUAGE_AGGREGATIONS_BUCKETS,
+ },
+];
+
+export const SORTED_MOCK_AGGREGATIONS = [
+ {
+ name: 'language',
+ buckets: MOCK_LANGUAGE_AGGREGATIONS_BUCKETS.reverse(),
+ },
+];
+
export const MOCK_RECEIVE_AGGREGATIONS_SUCCESS_MUTATION = [
{
type: types.REQUEST_AGGREGATIONS,
},
{
type: types.RECEIVE_AGGREGATIONS_SUCCESS,
- payload: MOCK_AGGREGATIONS,
+ payload: SORTED_MOCK_AGGREGATIONS,
},
];
@@ -660,3 +467,78 @@ export const SMALL_MOCK_AGGREGATIONS = [
buckets: TEST_RAW_BUCKETS,
},
];
+
+export const MOCK_NAVIGATION_ITEMS = [
+ {
+ title: 'Projects',
+ icon: 'project',
+ link: '/search?scope=projects&search=et',
+ is_active: false,
+ pill_count: '10K+',
+ items: [],
+ },
+ {
+ title: 'Code',
+ icon: 'code',
+ link: '/search?scope=blobs&search=et',
+ is_active: false,
+ pill_count: '0',
+ items: [],
+ },
+ {
+ title: 'Issues',
+ icon: 'issues',
+ link: '/search?scope=issues&search=et',
+ is_active: true,
+ pill_count: '2.4K',
+ items: [],
+ },
+ {
+ title: 'Merge requests',
+ icon: 'merge-request',
+ link: '/search?scope=merge_requests&search=et',
+ is_active: false,
+ pill_count: '0',
+ items: [],
+ },
+ {
+ title: 'Wiki',
+ icon: 'overview',
+ link: '/search?scope=wiki_blobs&search=et',
+ is_active: false,
+ pill_count: '0',
+ items: [],
+ },
+ {
+ title: 'Commits',
+ icon: 'commit',
+ link: '/search?scope=commits&search=et',
+ is_active: false,
+ pill_count: '0',
+ items: [],
+ },
+ {
+ title: 'Comments',
+ icon: 'comments',
+ link: '/search?scope=notes&search=et',
+ is_active: false,
+ pill_count: '0',
+ items: [],
+ },
+ {
+ title: 'Milestones',
+ icon: 'tag',
+ link: '/search?scope=milestones&search=et',
+ is_active: false,
+ pill_count: '0',
+ items: [],
+ },
+ {
+ title: 'Users',
+ icon: 'users',
+ link: '/search?scope=users&search=et',
+ is_active: false,
+ pill_count: '0',
+ items: [],
+ },
+];
diff --git a/spec/frontend/search/sidebar/components/app_spec.js b/spec/frontend/search/sidebar/components/app_spec.js
index 8a35ae96d5e..963b73aeae5 100644
--- a/spec/frontend/search/sidebar/components/app_spec.js
+++ b/spec/frontend/search/sidebar/components/app_spec.js
@@ -5,7 +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';
+import LanguageFilter from '~/search/sidebar/components/language_filter/index.vue';
Vue.use(Vuex);
diff --git a/spec/frontend/search/sidebar/components/checkbox_filter_spec.js b/spec/frontend/search/sidebar/components/checkbox_filter_spec.js
index f7b35c7bb14..3907e199cae 100644
--- a/spec/frontend/search/sidebar/components/checkbox_filter_spec.js
+++ b/spec/frontend/search/sidebar/components/checkbox_filter_spec.js
@@ -1,17 +1,22 @@
import { GlFormCheckboxGroup, GlFormCheckbox } from '@gitlab/ui';
import Vue from 'vue';
import Vuex from 'vuex';
+import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
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 CheckboxFilter, {
+ TRACKING_LABEL_CHECKBOX,
+ TRACKING_LABEL_SET,
+} from '~/search/sidebar/components/checkbox_filter.vue';
-import { languageFilterData } from '~/search/sidebar/constants/language_filter_data';
+import { languageFilterData } from '~/search/sidebar/components/language_filter/data';
import { convertFiltersData } from '~/search/sidebar/utils';
Vue.use(Vuex);
describe('CheckboxFilter', () => {
let wrapper;
+ let trackingSpy;
const actionSpies = {
setQuery: jest.fn(),
@@ -23,9 +28,10 @@ describe('CheckboxFilter', () => {
const defaultProps = {
filtersData: convertFiltersData(MOCK_LANGUAGE_AGGREGATIONS_BUCKETS),
+ trackingNamespace: 'testNameSpace',
};
- const createComponent = () => {
+ const createComponent = (Props = defaultProps) => {
const store = new Vuex.Store({
state: {
query: MOCK_QUERY,
@@ -37,13 +43,13 @@ describe('CheckboxFilter', () => {
wrapper = shallowMountExtended(CheckboxFilter, {
store,
propsData: {
- ...defaultProps,
+ ...Props,
},
});
};
- beforeEach(() => {
- createComponent();
+ afterEach(() => {
+ unmockTracking();
});
const findFormCheckboxGroup = () => wrapper.findComponent(GlFormCheckboxGroup);
@@ -52,6 +58,11 @@ describe('CheckboxFilter', () => {
const fintAllCheckboxLabelCounts = () => wrapper.findAllByTestId('labelCount');
describe('Renders correctly', () => {
+ beforeEach(() => {
+ createComponent();
+ trackingSpy = mockTracking(undefined, undefined, jest.spyOn);
+ });
+
it('renders form', () => {
expect(findFormCheckboxGroup().exists()).toBe(true);
});
@@ -76,15 +87,34 @@ describe('CheckboxFilter', () => {
});
describe('actions', () => {
- it('triggers setQuery', () => {
- const filter =
- defaultProps.filtersData.filters[Object.keys(defaultProps.filtersData.filters)[0]].value;
- findFormCheckboxGroup().vm.$emit('input', filter);
+ const checkedLanguageName = MOCK_LANGUAGE_AGGREGATIONS_BUCKETS[0].key;
+
+ beforeEach(() => {
+ defaultProps.filtersData = convertFiltersData(MOCK_LANGUAGE_AGGREGATIONS_BUCKETS.slice(0, 3));
+ CheckboxFilter.computed.selectedFilter.get = jest.fn(() => checkedLanguageName);
+
+ createComponent();
+ trackingSpy = mockTracking(undefined, undefined, jest.spyOn);
+ findFormCheckboxGroup().vm.$emit('input', checkedLanguageName);
+ });
+ it('triggers setQuery', () => {
expect(actionSpies.setQuery).toHaveBeenCalledWith(expect.any(Object), {
key: languageFilterData.filterParam,
- value: filter,
+ value: checkedLanguageName,
});
});
+
+ it('sends tracking information when setQuery', () => {
+ findFormCheckboxGroup().vm.$emit('input', checkedLanguageName);
+ expect(trackingSpy).toHaveBeenCalledWith(
+ defaultProps.trackingNamespace,
+ TRACKING_LABEL_CHECKBOX,
+ {
+ label: TRACKING_LABEL_SET,
+ property: checkedLanguageName,
+ },
+ );
+ });
});
});
diff --git a/spec/frontend/search/sidebar/components/confidentiality_filter_spec.js b/spec/frontend/search/sidebar/components/confidentiality_filter_spec.js
index 4f146757454..1f65884e959 100644
--- a/spec/frontend/search/sidebar/components/confidentiality_filter_spec.js
+++ b/spec/frontend/search/sidebar/components/confidentiality_filter_spec.js
@@ -1,25 +1,52 @@
import { shallowMount } from '@vue/test-utils';
+import Vue from 'vue';
+import Vuex from 'vuex';
import ConfidentialityFilter from '~/search/sidebar/components/confidentiality_filter.vue';
import RadioFilter from '~/search/sidebar/components/radio_filter.vue';
+Vue.use(Vuex);
+
describe('ConfidentialityFilter', () => {
let wrapper;
- const createComponent = (initProps) => {
+ const createComponent = (state) => {
+ const store = new Vuex.Store({
+ state,
+ });
+
wrapper = shallowMount(ConfidentialityFilter, {
- ...initProps,
+ store,
});
};
const findRadioFilter = () => wrapper.findComponent(RadioFilter);
+ const findHR = () => wrapper.findComponent('hr');
- describe('template', () => {
+ describe('old sidebar', () => {
beforeEach(() => {
- createComponent();
+ createComponent({ useNewNavigation: false });
});
it('renders the component', () => {
expect(findRadioFilter().exists()).toBe(true);
});
+
+ it('renders the divider', () => {
+ expect(findHR().exists()).toBe(true);
+ });
+ });
+
+ describe('new sidebar', () => {
+ beforeEach(() => {
+ createComponent({ useNewNavigation: true });
+ });
+
+ it('renders the component', () => {
+ expect(findRadioFilter().exists()).toBe(true);
+ });
+
+ it("doesn't render the divider", () => {
+ expect(findHR().exists()).toBe(false);
+ });
});
});
diff --git a/spec/frontend/search/sidebar/components/language_filter_spec.js b/spec/frontend/search/sidebar/components/language_filter_spec.js
index 17656ba749b..5821def5b43 100644
--- a/spec/frontend/search/sidebar/components/language_filter_spec.js
+++ b/spec/frontend/search/sidebar/components/language_filter_spec.js
@@ -1,20 +1,35 @@
import { GlAlert, GlFormCheckbox, GlForm } from '@gitlab/ui';
import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
+import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
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 LanguageFilter from '~/search/sidebar/components/language_filter/index.vue';
import CheckboxFilter from '~/search/sidebar/components/checkbox_filter.vue';
-import { MAX_ITEM_LENGTH } from '~/search/sidebar/constants/language_filter_data';
+
+import {
+ TRACKING_LABEL_SHOW_MORE,
+ TRACKING_CATEGORY,
+ TRACKING_PROPERTY_MAX,
+ TRACKING_LABEL_MAX,
+ TRACKING_LABEL_FILTERS,
+ TRACKING_ACTION_SHOW,
+ TRACKING_ACTION_CLICK,
+ TRACKING_LABEL_APPLY,
+ TRACKING_LABEL_ALL,
+} from '~/search/sidebar/components/language_filter/tracking';
+
+import { MAX_ITEM_LENGTH } from '~/search/sidebar/components/language_filter/data';
Vue.use(Vuex);
describe('GlobalSearchSidebarLanguageFilter', () => {
let wrapper;
+ let trackingSpy;
const actionSpies = {
fetchLanguageAggregation: jest.fn(),
@@ -46,6 +61,10 @@ describe('GlobalSearchSidebarLanguageFilter', () => {
});
};
+ afterEach(() => {
+ unmockTracking();
+ });
+
const findForm = () => wrapper.findComponent(GlForm);
const findCheckboxFilter = () => wrapper.findComponent(CheckboxFilter);
const findApplyButton = () => wrapper.findByTestId('apply-button');
@@ -58,6 +77,7 @@ describe('GlobalSearchSidebarLanguageFilter', () => {
describe('Renders correctly', () => {
beforeEach(() => {
createComponent();
+ trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
});
it('renders form', () => {
@@ -130,20 +150,40 @@ describe('GlobalSearchSidebarLanguageFilter', () => {
describe('Show All button works', () => {
beforeEach(() => {
createComponent();
+ trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
});
it(`renders ${MAX_ITEM_LENGTH} amount of items`, async () => {
findShowMoreButton().vm.$emit('click');
+
await nextTick();
+
expect(findAllCheckboxes()).toHaveLength(MAX_ITEM_LENGTH);
});
+ it('sends tracking information when show more clicked', () => {
+ findShowMoreButton().vm.$emit('click');
+
+ expect(trackingSpy).toHaveBeenCalledWith(TRACKING_ACTION_CLICK, TRACKING_LABEL_SHOW_MORE, {
+ label: TRACKING_LABEL_ALL,
+ });
+ });
+
it(`renders more then ${MAX_ITEM_LENGTH} text`, async () => {
findShowMoreButton().vm.$emit('click');
await nextTick();
expect(findHasOverMax().exists()).toBe(true);
});
+ it('sends tracking information when show more clicked and max item reached', () => {
+ findShowMoreButton().vm.$emit('click');
+
+ expect(trackingSpy).toHaveBeenCalledWith(TRACKING_ACTION_SHOW, TRACKING_LABEL_FILTERS, {
+ label: TRACKING_LABEL_MAX,
+ property: TRACKING_PROPERTY_MAX,
+ });
+ });
+
it(`doesn't render show more button after click`, async () => {
findShowMoreButton().vm.$emit('click');
await nextTick();
@@ -154,6 +194,7 @@ describe('GlobalSearchSidebarLanguageFilter', () => {
describe('actions', () => {
beforeEach(() => {
createComponent({});
+ trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
});
it('uses getter languageAggregationBuckets', () => {
@@ -169,5 +210,13 @@ describe('GlobalSearchSidebarLanguageFilter', () => {
expect(actionSpies.applyQuery).toHaveBeenCalled();
});
+
+ it('sends tracking information clicking ApplyButton', () => {
+ findForm().vm.$emit('submit', { preventDefault: () => {} });
+
+ expect(trackingSpy).toHaveBeenCalledWith(TRACKING_ACTION_CLICK, TRACKING_LABEL_APPLY, {
+ label: TRACKING_CATEGORY,
+ });
+ });
});
});
diff --git a/spec/frontend/search/sidebar/components/scope_navigation_spec.js b/spec/frontend/search/sidebar/components/scope_navigation_spec.js
index 3b2d528e1d7..e8737384f27 100644
--- a/spec/frontend/search/sidebar/components/scope_navigation_spec.js
+++ b/spec/frontend/search/sidebar/components/scope_navigation_spec.js
@@ -121,4 +121,22 @@ describe('ScopeNavigation', () => {
expect(findGlNavItemActive().findComponent(GlIcon).exists()).toBe(true);
});
});
+
+ describe.each`
+ searchTherm | hasBeenCalled
+ ${null} | ${0}
+ ${'test'} | ${1}
+ `('fetchSidebarCount', ({ searchTherm, hasBeenCalled }) => {
+ beforeEach(() => {
+ createComponent({
+ urlQuery: {
+ search: searchTherm,
+ },
+ });
+ });
+
+ it('is only called when search term is set', () => {
+ expect(actionSpies.fetchSidebarCount).toHaveBeenCalledTimes(hasBeenCalled);
+ });
+ });
});
diff --git a/spec/frontend/search/sidebar/components/scope_new_navigation_spec.js b/spec/frontend/search/sidebar/components/scope_new_navigation_spec.js
new file mode 100644
index 00000000000..105beae8638
--- /dev/null
+++ b/spec/frontend/search/sidebar/components/scope_new_navigation_spec.js
@@ -0,0 +1,83 @@
+import { shallowMount } from '@vue/test-utils';
+import Vue, { nextTick } from 'vue';
+import Vuex from 'vuex';
+import ScopeNewNavigation from '~/search/sidebar/components/scope_new_navigation.vue';
+import NavItem from '~/super_sidebar/components/nav_item.vue';
+import { MOCK_QUERY, MOCK_NAVIGATION, MOCK_NAVIGATION_ITEMS } from '../../mock_data';
+
+Vue.use(Vuex);
+
+describe('ScopeNewNavigation', () => {
+ let wrapper;
+
+ const actionSpies = {
+ fetchSidebarCount: jest.fn(),
+ };
+
+ const getterSpies = {
+ currentScope: jest.fn(() => 'issues'),
+ navigationItems: jest.fn(() => MOCK_NAVIGATION_ITEMS),
+ };
+
+ const createComponent = (initialState) => {
+ const store = new Vuex.Store({
+ state: {
+ urlQuery: MOCK_QUERY,
+ navigation: MOCK_NAVIGATION,
+ ...initialState,
+ },
+ actions: actionSpies,
+ getters: getterSpies,
+ });
+
+ wrapper = shallowMount(ScopeNewNavigation, {
+ store,
+ stubs: {
+ NavItem,
+ },
+ });
+ };
+
+ const findNavElement = () => wrapper.findComponent('nav');
+ const findNavItems = () => wrapper.findAllComponents(NavItem);
+ const findNavItemActive = () => wrapper.find('[aria-current=page]');
+ const findNavItemActiveLabel = () =>
+ findNavItemActive().find('[class="gl-pr-3 gl-text-gray-900 gl-truncate-end"]');
+
+ describe('scope navigation', () => {
+ beforeEach(() => {
+ createComponent({ urlQuery: { ...MOCK_QUERY, search: 'test' } });
+ });
+
+ it('renders section', () => {
+ expect(findNavElement().exists()).toBe(true);
+ });
+
+ it('calls proper action when rendered', async () => {
+ await nextTick();
+ expect(actionSpies.fetchSidebarCount).toHaveBeenCalled();
+ });
+
+ it('renders all nav item components', () => {
+ expect(findNavItems()).toHaveLength(9);
+ });
+
+ it('has all proper links', () => {
+ const linkAtPosition = 3;
+ const { link } = MOCK_NAVIGATION[Object.keys(MOCK_NAVIGATION)[linkAtPosition]];
+
+ expect(findNavItems().at(linkAtPosition).findComponent('a').attributes('href')).toBe(link);
+ });
+ });
+
+ describe('scope navigation sets proper state with url scope set', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('has correct active item', () => {
+ expect(findNavItemActive().exists()).toBe(true);
+ expect(findNavItemActiveLabel().text()).toBe('Issues');
+ });
+ });
+});
diff --git a/spec/frontend/search/sidebar/components/status_filter_spec.js b/spec/frontend/search/sidebar/components/status_filter_spec.js
index 6704634ef36..a332a43e624 100644
--- a/spec/frontend/search/sidebar/components/status_filter_spec.js
+++ b/spec/frontend/search/sidebar/components/status_filter_spec.js
@@ -1,25 +1,52 @@
import { shallowMount } from '@vue/test-utils';
+import Vue from 'vue';
+import Vuex from 'vuex';
import RadioFilter from '~/search/sidebar/components/radio_filter.vue';
import StatusFilter from '~/search/sidebar/components/status_filter.vue';
+Vue.use(Vuex);
+
describe('StatusFilter', () => {
let wrapper;
- const createComponent = (initProps) => {
+ const createComponent = (state) => {
+ const store = new Vuex.Store({
+ state,
+ });
+
wrapper = shallowMount(StatusFilter, {
- ...initProps,
+ store,
});
};
const findRadioFilter = () => wrapper.findComponent(RadioFilter);
+ const findHR = () => wrapper.findComponent('hr');
- describe('template', () => {
+ describe('old sidebar', () => {
beforeEach(() => {
- createComponent();
+ createComponent({ useNewNavigation: false });
});
it('renders the component', () => {
expect(findRadioFilter().exists()).toBe(true);
});
+
+ it('renders the divider', () => {
+ expect(findHR().exists()).toBe(true);
+ });
+ });
+
+ describe('new sidebar', () => {
+ beforeEach(() => {
+ createComponent({ useNewNavigation: true });
+ });
+
+ it('renders the component', () => {
+ expect(findRadioFilter().exists()).toBe(true);
+ });
+
+ it("doesn't render the divider", () => {
+ expect(findHR().exists()).toBe(false);
+ });
});
});
diff --git a/spec/frontend/search/store/getters_spec.js b/spec/frontend/search/store/getters_spec.js
index 0ef0922c4b0..e3b8e7575a4 100644
--- a/spec/frontend/search/store/getters_spec.js
+++ b/spec/frontend/search/store/getters_spec.js
@@ -10,6 +10,7 @@ import {
MOCK_LANGUAGE_AGGREGATIONS_BUCKETS,
TEST_FILTER_DATA,
MOCK_NAVIGATION,
+ MOCK_NAVIGATION_ITEMS,
} from '../mock_data';
describe('Global Search Store Getters', () => {
@@ -68,4 +69,11 @@ describe('Global Search Store Getters', () => {
expect(getters.currentUrlQueryHasLanguageFilters(state)).toBe(result);
});
});
+
+ describe('navigationItems', () => {
+ it('returns the re-mapped navigation data', () => {
+ state.navigation = MOCK_NAVIGATION;
+ expect(getters.navigationItems(state)).toStrictEqual(MOCK_NAVIGATION_ITEMS);
+ });
+ });
});
diff --git a/spec/frontend/search/store/utils_spec.js b/spec/frontend/search/store/utils_spec.js
index dfe4e801f11..802c5219799 100644
--- a/spec/frontend/search/store/utils_spec.js
+++ b/spec/frontend/search/store/utils_spec.js
@@ -8,6 +8,7 @@ import {
formatSearchResultCount,
getAggregationsUrl,
prepareSearchAggregations,
+ addCountOverLimit,
} from '~/search/store/utils';
import { useMockLocationHelper } from 'helpers/mock_window_location_helper';
import {
@@ -288,4 +289,18 @@ describe('Global Search Store Utils', () => {
expect(prepareSearchAggregations({ query }, data)).toStrictEqual(result);
});
});
+
+ describe('addCountOverLimit', () => {
+ it("should return '+' if count includes '+'", () => {
+ expect(addCountOverLimit('10+')).toEqual('+');
+ });
+
+ it("should return empty string if count does not include '+'", () => {
+ expect(addCountOverLimit('10')).toEqual('');
+ });
+
+ it('should return empty string if count is not provided', () => {
+ expect(addCountOverLimit()).toEqual('');
+ });
+ });
});
diff --git a/spec/frontend/search/topbar/components/searchable_dropdown_spec.js b/spec/frontend/search/topbar/components/searchable_dropdown_spec.js
index 5e5f46ff34e..f7d847674eb 100644
--- a/spec/frontend/search/topbar/components/searchable_dropdown_spec.js
+++ b/spec/frontend/search/topbar/components/searchable_dropdown_spec.js
@@ -189,7 +189,7 @@ describe('Global Search Searchable Dropdown', () => {
findGlDropdown().vm.$emit('show');
});
- it('$emits @search and @first-open on the first open', async () => {
+ it('$emits @search and @first-open on the first open', () => {
expect(wrapper.emitted('search')[0]).toStrictEqual(['']);
expect(wrapper.emitted('first-open')[0]).toStrictEqual([]);
});
diff --git a/spec/frontend/search_autocomplete_spec.js b/spec/frontend/search_autocomplete_spec.js
deleted file mode 100644
index dfb31eeda78..00000000000
--- a/spec/frontend/search_autocomplete_spec.js
+++ /dev/null
@@ -1,292 +0,0 @@
-import AxiosMockAdapter from 'axios-mock-adapter';
-import $ from 'jquery';
-import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
-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;
-
- const userName = 'root';
- const userId = 1;
- const dashboardIssuesPath = '/dashboard/issues';
- const dashboardMRsPath = '/dashboard/merge_requests';
- const projectIssuesPath = '/gitlab-org/gitlab-foss/issues';
- const projectMRsPath = '/gitlab-org/gitlab-foss/-/merge_requests';
- const groupIssuesPath = '/groups/gitlab-org/-/issues';
- const groupMRsPath = '/groups/gitlab-org/-/merge_requests';
- const autocompletePath = '/search/autocomplete';
- const projectName = 'GitLab Community Edition';
- const groupName = 'Gitlab Org';
-
- const removeBodyAttributes = () => {
- const { body } = document;
-
- delete body.dataset.page;
- delete body.dataset.project;
- delete body.dataset.group;
- };
-
- // Add required attributes to body before starting the test.
- // section would be dashboard|group|project
- const addBodyAttributes = (section = 'dashboard') => {
- removeBodyAttributes();
-
- const { body } = document;
- switch (section) {
- case 'dashboard':
- body.dataset.page = 'root:index';
- break;
- case 'group':
- body.dataset.page = 'groups:show';
- body.dataset.group = 'gitlab-org';
- break;
- case 'project':
- body.dataset.page = 'projects:show';
- body.dataset.project = 'gitlab-ce';
- break;
- default:
- break;
- }
- };
-
- const disableProjectIssues = () => {
- document.querySelector('.js-search-project-options').dataset.issuesDisabled = true;
- };
-
- // Mock `gl` object in window for dashboard specific page. App code will need it.
- const mockDashboardOptions = () => {
- window.gl.dashboardOptions = {
- issuesPath: dashboardIssuesPath,
- mrPath: dashboardMRsPath,
- };
- };
-
- // Mock `gl` object in window for project specific page. App code will need it.
- const mockProjectOptions = () => {
- window.gl.projectOptions = {
- 'gitlab-ce': {
- issuesPath: projectIssuesPath,
- mrPath: projectMRsPath,
- projectName,
- },
- };
- };
-
- const mockGroupOptions = () => {
- window.gl.groupOptions = {
- 'gitlab-org': {
- issuesPath: groupIssuesPath,
- mrPath: groupMRsPath,
- projectName: groupName,
- },
- };
- };
-
- const assertLinks = (list, issuesPath, mrsPath) => {
- if (issuesPath) {
- const issuesAssignedToMeLink = `a[href="${issuesPath}/?assignee_username=${userName}"]`;
- const issuesIHaveCreatedLink = `a[href="${issuesPath}/?author_username=${userName}"]`;
-
- expect(list.find(issuesAssignedToMeLink).length).toBe(1);
- expect(list.find(issuesAssignedToMeLink).text()).toBe('Issues assigned to me');
- expect(list.find(issuesIHaveCreatedLink).length).toBe(1);
- expect(list.find(issuesIHaveCreatedLink).text()).toBe("Issues I've created");
- }
- const mrsAssignedToMeLink = `a[href="${mrsPath}/?assignee_username=${userName}"]`;
- const mrsIHaveCreatedLink = `a[href="${mrsPath}/?author_username=${userName}"]`;
-
- expect(list.find(mrsAssignedToMeLink).length).toBe(1);
- expect(list.find(mrsAssignedToMeLink).text()).toBe('Merge requests assigned to me');
- expect(list.find(mrsIHaveCreatedLink).length).toBe(1);
- expect(list.find(mrsIHaveCreatedLink).text()).toBe("Merge requests I've created");
- };
-
- beforeEach(() => {
- loadHTMLFixture('static/search_autocomplete.html');
-
- window.gon = {};
- window.gon.current_user_id = userId;
- window.gon.current_username = userName;
- window.gl = window.gl || (window.gl = {});
-
- widget = initSearchAutocomplete({ autocompletePath });
- });
-
- afterEach(() => {
- // Undo what we did to the shared <body>
- removeBodyAttributes();
-
- resetHTMLFixture();
- });
-
- it('should show Dashboard specific dropdown menu', () => {
- addBodyAttributes();
- mockDashboardOptions();
- widget.searchInput.triggerHandler('focus');
- const list = widget.wrap.find('.dropdown-menu').find('ul');
- return assertLinks(list, dashboardIssuesPath, dashboardMRsPath);
- });
-
- it('should show Group specific dropdown menu', () => {
- addBodyAttributes('group');
- mockGroupOptions();
- widget.searchInput.triggerHandler('focus');
- const list = widget.wrap.find('.dropdown-menu').find('ul');
- return assertLinks(list, groupIssuesPath, groupMRsPath);
- });
-
- it('should show Project specific dropdown menu', () => {
- addBodyAttributes('project');
- mockProjectOptions();
- widget.searchInput.triggerHandler('focus');
- const list = widget.wrap.find('.dropdown-menu').find('ul');
- return assertLinks(list, projectIssuesPath, projectMRsPath);
- });
-
- it('should show only Project mergeRequest dropdown menu items when project issues are disabled', () => {
- addBodyAttributes('project');
- disableProjectIssues();
- mockProjectOptions();
- widget.searchInput.triggerHandler('focus');
- const list = widget.wrap.find('.dropdown-menu').find('ul');
- assertLinks(list, null, projectMRsPath);
- });
-
- it('should not show category related menu if there is text in the input', () => {
- addBodyAttributes('project');
- mockProjectOptions();
- widget.searchInput.val('help');
- widget.searchInput.triggerHandler('focus');
- const list = widget.wrap.find('.dropdown-menu').find('ul');
- const link = `a[href='${projectIssuesPath}/?assignee_username=${userName}']`;
-
- expect(list.find(link).length).toBe(0);
- });
-
- it('should not submit the search form when selecting an autocomplete row with the keyboard', () => {
- const ENTER = 13;
- const DOWN = 40;
- addBodyAttributes();
- mockDashboardOptions(true);
- const submitSpy = jest.spyOn(document.querySelector('form'), 'submit');
- widget.searchInput.triggerHandler('focus');
- widget.wrap.trigger($.Event('keydown', { which: DOWN }));
- const enterKeyEvent = $.Event('keydown', { which: ENTER });
- widget.searchInput.trigger(enterKeyEvent);
-
- // This does not currently catch failing behavior. For security reasons,
- // browsers will not trigger default behavior (form submit, in this
- // example) on JavaScript-created keypresses.
- expect(submitSpy).not.toHaveBeenCalled();
- });
-
- describe('show autocomplete results', () => {
- beforeEach(() => {
- widget.enableAutocomplete();
-
- const axiosMock = new AxiosMockAdapter(axios);
- const autocompleteUrl = new RegExp(autocompletePath);
-
- axiosMock.onGet(autocompleteUrl).reply(HTTP_STATUS_OK, [
- {
- category: 'Projects',
- id: 1,
- value: 'Gitlab Test',
- label: 'Gitlab Org / Gitlab Test',
- url: '/gitlab-org/gitlab-test',
- avatar_url: '',
- },
- {
- category: 'Groups',
- id: 1,
- value: 'Gitlab Org',
- label: 'Gitlab Org',
- url: '/gitlab-org',
- avatar_url: '',
- },
- ]);
- });
-
- function triggerAutocomplete() {
- return new Promise((resolve) => {
- const dropdown = widget.searchInput.data('deprecatedJQueryDropdown');
- const filterCallback = dropdown.filter.options.callback;
- dropdown.filter.options.callback = jest.fn((data) => {
- filterCallback(data);
-
- resolve();
- });
-
- widget.searchInput.val('Gitlab');
- widget.searchInput.triggerHandler('input');
- });
- }
-
- it('suggest Projects', async () => {
- await triggerAutocomplete();
-
- const list = widget.wrap.find('.dropdown-menu').find('ul');
- const link = "a[href$='/gitlab-org/gitlab-test']";
-
- expect(list.find(link).length).toBe(1);
- });
-
- it('suggest Groups', async () => {
- await triggerAutocomplete();
-
- const list = widget.wrap.find('.dropdown-menu').find('ul');
- const link = "a[href$='/gitlab-org']";
-
- expect(list.find(link).length).toBe(1);
- });
- });
-
- describe('disableAutocomplete', () => {
- beforeEach(() => {
- widget.enableAutocomplete();
- });
-
- it('should close the Dropdown', () => {
- const toggleSpy = jest.spyOn(widget.dropdownToggle, 'dropdown');
-
- widget.dropdown.addClass('show');
- widget.disableAutocomplete();
-
- expect(toggleSpy).toHaveBeenCalledWith('toggle');
- });
- });
-
- describe('enableAutocomplete', () => {
- let toggleSpy;
- let trackingSpy;
-
- beforeEach(() => {
- toggleSpy = jest.spyOn(widget.dropdownToggle, 'dropdown');
- trackingSpy = mockTracking('_category_', undefined, jest.spyOn);
- document.body.dataset.page = 'some:page'; // default tracking for category
- });
-
- afterEach(() => {
- unmockTracking();
- });
-
- it('should open the Dropdown', () => {
- widget.enableAutocomplete();
-
- expect(toggleSpy).toHaveBeenCalledWith('toggle');
- });
-
- it('should track the opening', () => {
- widget.enableAutocomplete();
-
- expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_search_bar', {
- label: 'main_navigation',
- property: 'navigation',
- });
- });
- });
-});
diff --git a/spec/frontend/search_autocomplete_utils_spec.js b/spec/frontend/search_autocomplete_utils_spec.js
deleted file mode 100644
index 4fdec717e93..00000000000
--- a/spec/frontend/search_autocomplete_utils_spec.js
+++ /dev/null
@@ -1,114 +0,0 @@
-import {
- isInGroupsPage,
- isInProjectPage,
- getGroupSlug,
- getProjectSlug,
-} from '~/search_autocomplete_utils';
-
-describe('search_autocomplete_utils', () => {
- let originalBody;
-
- beforeEach(() => {
- originalBody = document.body;
- document.body = document.createElement('body');
- });
-
- afterEach(() => {
- document.body = originalBody;
- });
-
- describe('isInGroupsPage', () => {
- it.each`
- page | result
- ${'groups:index'} | ${true}
- ${'groups:show'} | ${true}
- ${'projects:show'} | ${false}
- `(`returns $result in for page $page`, ({ page, result }) => {
- document.body.dataset.page = page;
-
- expect(isInGroupsPage()).toBe(result);
- });
- });
-
- describe('isInProjectPage', () => {
- it.each`
- page | result
- ${'projects:index'} | ${true}
- ${'projects:show'} | ${true}
- ${'groups:show'} | ${false}
- `(`returns $result in for page $page`, ({ page, result }) => {
- document.body.dataset.page = page;
-
- expect(isInProjectPage()).toBe(result);
- });
- });
-
- describe('getProjectSlug', () => {
- it('returns null when no project is present or on project page', () => {
- expect(getProjectSlug()).toBe(null);
- });
-
- it('returns null when not on project page', () => {
- document.body.dataset.project = 'gitlab';
-
- expect(getProjectSlug()).toBe(null);
- });
-
- it('returns null when project is missing', () => {
- document.body.dataset.page = 'projects';
-
- expect(getProjectSlug()).toBe(undefined);
- });
-
- it('returns project', () => {
- document.body.dataset.page = 'projects';
- document.body.dataset.project = 'gitlab';
-
- expect(getProjectSlug()).toBe('gitlab');
- });
-
- it('returns project in edit page', () => {
- document.body.dataset.page = 'projects:edit';
- document.body.dataset.project = 'gitlab';
-
- expect(getProjectSlug()).toBe('gitlab');
- });
- });
-
- describe('getGroupSlug', () => {
- it('returns null when no group is present or on group page', () => {
- expect(getGroupSlug()).toBe(null);
- });
-
- it('returns null when not on group page', () => {
- document.body.dataset.group = 'gitlab-org';
-
- expect(getGroupSlug()).toBe(null);
- });
-
- it('returns null when group is missing on groups page', () => {
- document.body.dataset.page = 'groups';
-
- expect(getGroupSlug()).toBe(undefined);
- });
-
- it('returns null when group is missing on project page', () => {
- document.body.dataset.page = 'project';
-
- expect(getGroupSlug()).toBe(null);
- });
-
- it.each`
- page
- ${'groups'}
- ${'groups:edit'}
- ${'projects'}
- ${'projects:edit'}
- `(`returns group in page $page`, ({ page }) => {
- document.body.dataset.page = page;
- document.body.dataset.group = 'gitlab-org';
-
- expect(getGroupSlug()).toBe('gitlab-org');
- });
- });
-});
diff --git a/spec/frontend/security_configuration/components/app_spec.js b/spec/frontend/security_configuration/components/app_spec.js
index 0ca350f9ed7..ec47cda1cb2 100644
--- a/spec/frontend/security_configuration/components/app_spec.js
+++ b/spec/frontend/security_configuration/components/app_spec.js
@@ -26,8 +26,6 @@ import {
REPORT_TYPE_LICENSE_COMPLIANCE,
REPORT_TYPE_SAST,
} from '~/vue_shared/security_reports/constants';
-import { USER_FACING_ERROR_MESSAGE_PREFIX } from '~/lib/utils/error_message';
-import { manageViaMRErrorMessage } from '../constants';
const upgradePath = '/upgrade';
const autoDevopsHelpPagePath = '/autoDevopsHelpPagePath';
@@ -202,21 +200,20 @@ describe('App component', () => {
});
});
- describe('when user facing error occurs', () => {
+ describe('when error occurs', () => {
+ const errorMessage = 'There was a manage via MR error';
+
it('should show Alert with error Message', async () => {
expect(findManageViaMRErrorAlert().exists()).toBe(false);
- // Prefixed with USER_FACING_ERROR_MESSAGE_PREFIX as used in lib/gitlab/utils/error_message.rb to indicate a user facing error
- findFeatureCards()
- .at(1)
- .vm.$emit('error', `${USER_FACING_ERROR_MESSAGE_PREFIX} ${manageViaMRErrorMessage}`);
+ findFeatureCards().at(1).vm.$emit('error', errorMessage);
await nextTick();
expect(findManageViaMRErrorAlert().exists()).toBe(true);
- expect(findManageViaMRErrorAlert().text()).toEqual(manageViaMRErrorMessage);
+ expect(findManageViaMRErrorAlert().text()).toBe(errorMessage);
});
it('should hide Alert when it is dismissed', async () => {
- findFeatureCards().at(1).vm.$emit('error', manageViaMRErrorMessage);
+ findFeatureCards().at(1).vm.$emit('error', errorMessage);
await nextTick();
expect(findManageViaMRErrorAlert().exists()).toBe(true);
@@ -226,17 +223,6 @@ describe('App component', () => {
expect(findManageViaMRErrorAlert().exists()).toBe(false);
});
});
-
- describe('when non-user facing error occurs', () => {
- it('should show Alert with generic error Message', async () => {
- expect(findManageViaMRErrorAlert().exists()).toBe(false);
- findFeatureCards().at(1).vm.$emit('error', manageViaMRErrorMessage);
-
- await nextTick();
- expect(findManageViaMRErrorAlert().exists()).toBe(true);
- expect(findManageViaMRErrorAlert().text()).toEqual(i18n.genericErrorText);
- });
- });
});
describe('Auto DevOps hint alert', () => {
@@ -436,7 +422,7 @@ describe('App component', () => {
describe('Vulnerability management', () => {
const props = { securityTrainingEnabled: true };
- beforeEach(async () => {
+ beforeEach(() => {
createComponent({
...props,
});
diff --git a/spec/frontend/security_configuration/components/feature_card_spec.js b/spec/frontend/security_configuration/components/feature_card_spec.js
index 23edd8a69de..983a66a7fd3 100644
--- a/spec/frontend/security_configuration/components/feature_card_spec.js
+++ b/spec/frontend/security_configuration/components/feature_card_spec.js
@@ -1,10 +1,15 @@
import { GlIcon } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import { securityFeatures } from '~/security_configuration/components/constants';
import FeatureCard from '~/security_configuration/components/feature_card.vue';
import FeatureCardBadge from '~/security_configuration/components/feature_card_badge.vue';
import ManageViaMr from '~/vue_shared/security_configuration/components/manage_via_mr.vue';
-import { REPORT_TYPE_SAST } from '~/vue_shared/security_reports/constants';
+import {
+ REPORT_TYPE_BREACH_AND_ATTACK_SIMULATION,
+ REPORT_TYPE_SAST,
+ REPORT_TYPE_SAST_IAC,
+} from '~/vue_shared/security_reports/constants';
import { manageViaMRErrorMessage } from '../constants';
import { makeFeature } from './utils';
@@ -265,6 +270,56 @@ describe('FeatureCard component', () => {
expect(links.exists()).toBe(false);
});
});
+
+ describe('given an available secondary with a configuration guide', () => {
+ beforeEach(() => {
+ feature = makeFeature({
+ available: true,
+ configurationHelpPath: null,
+ secondary: {
+ name: 'secondary name',
+ description: 'secondary description',
+ configurationHelpPath: '/secondary',
+ configurationText: null,
+ },
+ });
+ createComponent({ feature });
+ });
+
+ it('shows the secondary action', () => {
+ const links = findLinks({
+ text: 'Configuration guide',
+ href: feature.secondary.configurationHelpPath,
+ });
+ expect(links.exists()).toBe(true);
+ expect(links).toHaveLength(1);
+ });
+ });
+
+ describe('given an unavailable secondary with a configuration guide', () => {
+ beforeEach(() => {
+ feature = makeFeature({
+ available: false,
+ configurationHelpPath: null,
+ secondary: {
+ name: 'secondary name',
+ description: 'secondary description',
+ configurationHelpPath: '/secondary',
+ configurationText: null,
+ },
+ });
+ createComponent({ feature });
+ });
+
+ it('does not show the secondary action', () => {
+ const links = findLinks({
+ text: 'Configuration guide',
+ href: feature.secondary.configurationHelpPath,
+ });
+ expect(links.exists()).toBe(false);
+ expect(links).toHaveLength(0);
+ });
+ });
});
describe('information badge', () => {
@@ -290,4 +345,48 @@ describe('FeatureCard component', () => {
});
});
});
+
+ describe('status and badge', () => {
+ describe.each`
+ context | available | configured | expectedStatus
+ ${'configured BAS feature'} | ${true} | ${true} | ${null}
+ ${'unavailable BAS feature'} | ${false} | ${false} | ${'Available with Ultimate'}
+ ${'unconfigured BAS feature'} | ${true} | ${false} | ${null}
+ `('given $context', ({ available, configured, expectedStatus }) => {
+ beforeEach(() => {
+ const securityFeature = securityFeatures.find(
+ ({ type }) => REPORT_TYPE_BREACH_AND_ATTACK_SIMULATION === type,
+ );
+ feature = { ...securityFeature, available, configured };
+ createComponent({ feature });
+ });
+
+ it('should show an incubating feature badge', () => {
+ expect(findBadge().exists()).toBe(true);
+ });
+
+ if (expectedStatus) {
+ it(`should show the status "${expectedStatus}"`, () => {
+ expect(wrapper.findByTestId('feature-status').text()).toBe(expectedStatus);
+ });
+ }
+ });
+
+ describe.each`
+ context | available | configured
+ ${'configured SAST IaC feature'} | ${true} | ${true}
+ ${'unavailable SAST IaC feature'} | ${false} | ${false}
+ ${'unconfigured SAST IaC feature'} | ${true} | ${false}
+ `('given $context', ({ available, configured }) => {
+ beforeEach(() => {
+ const securityFeature = securityFeatures.find(({ type }) => REPORT_TYPE_SAST_IAC === type);
+ feature = { ...securityFeature, available, configured };
+ createComponent({ feature });
+ });
+
+ it(`should not show a status`, () => {
+ expect(wrapper.findByTestId('feature-status').exists()).toBe(false);
+ });
+ });
+ });
});
diff --git a/spec/frontend/security_configuration/components/training_provider_list_spec.js b/spec/frontend/security_configuration/components/training_provider_list_spec.js
index 1f8f306c931..d35fc00057a 100644
--- a/spec/frontend/security_configuration/components/training_provider_list_spec.js
+++ b/spec/frontend/security_configuration/components/training_provider_list_spec.js
@@ -253,7 +253,7 @@ describe('TrainingProviderList component', () => {
expect(findLogos().at(provider).attributes('role')).toBe('presentation');
});
- it.each(providerIndexArray)('renders the svg content for provider %s', async (provider) => {
+ it.each(providerIndexArray)('renders the svg content for provider %s', (provider) => {
expect(findLogos().at(provider).html()).toContain(
TEMP_PROVIDER_LOGOS[testProviderName[provider]].svg,
);
diff --git a/spec/frontend/sentry/sentry_browser_wrapper_spec.js b/spec/frontend/sentry/sentry_browser_wrapper_spec.js
index f4d646bab78..55354eceb8d 100644
--- a/spec/frontend/sentry/sentry_browser_wrapper_spec.js
+++ b/spec/frontend/sentry/sentry_browser_wrapper_spec.js
@@ -25,7 +25,7 @@ describe('SentryBrowserWrapper', () => {
let mockCaptureMessage;
let mockWithScope;
- beforeEach(async () => {
+ beforeEach(() => {
mockCaptureException = jest.fn();
mockCaptureMessage = jest.fn();
mockWithScope = jest.fn();
diff --git a/spec/frontend/shortcuts_spec.js b/spec/frontend/shortcuts_spec.js
index e859d435f48..d1371ca0ef9 100644
--- a/spec/frontend/shortcuts_spec.js
+++ b/spec/frontend/shortcuts_spec.js
@@ -119,10 +119,21 @@ describe('Shortcuts', () => {
});
describe('focusSearch', () => {
- it('focuses the search bar', () => {
- Shortcuts.focusSearch(createEvent('KeyboardEvent'));
+ describe('when super sidebar is NOT enabled', () => {
+ let originalGon;
+ beforeEach(() => {
+ originalGon = window.gon;
+ window.gon = { use_new_navigation: false };
+ });
- expect(document.querySelector('#search').focus).toHaveBeenCalled();
+ afterEach(() => {
+ window.gon = originalGon;
+ });
+
+ it('focuses the search bar', () => {
+ Shortcuts.focusSearch(createEvent('KeyboardEvent'));
+ expect(document.querySelector('#search').focus).toHaveBeenCalled();
+ });
});
});
});
diff --git a/spec/frontend/sidebar/components/assignees/sidebar_participant_spec.js b/spec/frontend/sidebar/components/assignees/sidebar_participant_spec.js
index 7895274ab6d..25a19b5808b 100644
--- a/spec/frontend/sidebar/components/assignees/sidebar_participant_spec.js
+++ b/spec/frontend/sidebar/components/assignees/sidebar_participant_spec.js
@@ -36,12 +36,13 @@ describe('Sidebar participant component', () => {
createComponent();
expect(findAvatar().props('label')).toBe(user.name);
+ expect(wrapper.text()).not.toContain('Busy');
});
it('shows `Busy` status when user is busy', () => {
createComponent({ status: { availability: 'BUSY' } });
- expect(findAvatar().props('label')).toBe(`${user.name} (Busy)`);
+ expect(wrapper.text()).toContain('Busy');
});
it('does not render a warning icon', () => {
diff --git a/spec/frontend/sidebar/components/assignees/user_name_with_status_spec.js b/spec/frontend/sidebar/components/assignees/user_name_with_status_spec.js
index 877d7cd61ee..e54ba31a30c 100644
--- a/spec/frontend/sidebar/components/assignees/user_name_with_status_spec.js
+++ b/spec/frontend/sidebar/components/assignees/user_name_with_status_spec.js
@@ -37,7 +37,7 @@ describe('UserNameWithStatus', () => {
});
it('will render "Busy"', () => {
- expect(wrapper.text()).toContain('(Busy)');
+ expect(wrapper.text()).toContain('Busy');
});
});
@@ -49,7 +49,7 @@ describe('UserNameWithStatus', () => {
});
it("renders user's name with pronouns", () => {
- expect(wrapper.text()).toMatchInterpolatedText(`${name} (${pronouns})`);
+ expect(wrapper.text()).toMatchInterpolatedText(`${name}(${pronouns})`);
});
});
diff --git a/spec/frontend/sidebar/components/date/sidebar_date_widget_spec.js b/spec/frontend/sidebar/components/date/sidebar_date_widget_spec.js
index b9c8655d5d8..8f82a2d1258 100644
--- a/spec/frontend/sidebar/components/date/sidebar_date_widget_spec.js
+++ b/spec/frontend/sidebar/components/date/sidebar_date_widget_spec.js
@@ -11,7 +11,12 @@ import SidebarInheritDate from '~/sidebar/components/date/sidebar_inherit_date.v
import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue';
import epicStartDateQuery from '~/sidebar/queries/epic_start_date.query.graphql';
import issueDueDateQuery from '~/sidebar/queries/issue_due_date.query.graphql';
-import { issuableDueDateResponse, issuableStartDateResponse } from '../../mock_data';
+import issueDueDateSubscription from '~/graphql_shared/subscriptions/work_item_dates.subscription.graphql';
+import {
+ issuableDueDateResponse,
+ issuableStartDateResponse,
+ issueDueDateSubscriptionResponse,
+} from '../../mock_data';
jest.mock('~/alert');
@@ -29,6 +34,7 @@ describe('Sidebar date Widget', () => {
const createComponent = ({
dueDateQueryHandler = jest.fn().mockResolvedValue(issuableDueDateResponse()),
startDateQueryHandler = jest.fn().mockResolvedValue(issuableStartDateResponse()),
+ dueDateSubscriptionHandler = jest.fn().mockResolvedValue(issueDueDateSubscriptionResponse()),
canInherit = false,
dateType = undefined,
issuableType = 'issue',
@@ -36,6 +42,7 @@ describe('Sidebar date Widget', () => {
fakeApollo = createMockApollo([
[issueDueDateQuery, dueDateQueryHandler],
[epicStartDateQuery, startDateQueryHandler],
+ [issueDueDateSubscription, dueDateSubscriptionHandler],
]);
wrapper = shallowMount(SidebarDateWidget, {
@@ -124,18 +131,30 @@ describe('Sidebar date Widget', () => {
it('uses a correct prop to set the initial date and first day of the week for GlDatePicker', () => {
expect(findDatePicker().props()).toMatchObject({
- value: null,
+ value: new Date(date),
autocomplete: 'off',
defaultDate: expect.any(Object),
firstDay: window.gon.first_day_of_week,
});
});
- it('renders GlDatePicker', async () => {
+ it('renders GlDatePicker', () => {
expect(findDatePicker().exists()).toBe(true);
});
});
+ describe('real time issue due date feature', () => {
+ it('should call the subscription', async () => {
+ const dueDateSubscriptionHandler = jest
+ .fn()
+ .mockResolvedValue(issueDueDateSubscriptionResponse());
+ createComponent({ dueDateSubscriptionHandler });
+ await waitForPromises();
+
+ expect(dueDateSubscriptionHandler).toHaveBeenCalled();
+ });
+ });
+
it.each`
canInherit | component | componentName | expected
${true} | ${SidebarFormattedDate} | ${'SidebarFormattedDate'} | ${false}
@@ -152,7 +171,7 @@ describe('Sidebar date Widget', () => {
},
);
- it('does not render SidebarInheritDate when canInherit is true and date is loading', async () => {
+ it('does not render SidebarInheritDate when canInherit is true and date is loading', () => {
createComponent({ canInherit: true });
expect(wrapper.findComponent(SidebarInheritDate).exists()).toBe(false);
diff --git a/spec/frontend/sidebar/components/labels/labels_select_vue/dropdown_contents_create_view_spec.js b/spec/frontend/sidebar/components/labels/labels_select_vue/dropdown_contents_create_view_spec.js
index bb7554ff21d..7e53fcfe850 100644
--- a/spec/frontend/sidebar/components/labels/labels_select_vue/dropdown_contents_create_view_spec.js
+++ b/spec/frontend/sidebar/components/labels/labels_select_vue/dropdown_contents_create_view_spec.js
@@ -1,73 +1,70 @@
import { GlButton, GlFormInput, GlLink, GlLoadingIcon } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import DropdownContentsCreateView from '~/sidebar/components/labels/labels_select_vue/dropdown_contents_create_view.vue';
import labelSelectModule from '~/sidebar/components/labels/labels_select_vue/store';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { mockConfig, mockSuggestedColors } from './mock_data';
Vue.use(Vuex);
-const createComponent = (initialState = mockConfig) => {
- const store = new Vuex.Store(labelSelectModule());
-
- store.dispatch('setInitialState', initialState);
-
- return shallowMount(DropdownContentsCreateView, {
- store,
- });
-};
-
describe('DropdownContentsCreateView', () => {
let wrapper;
const colors = Object.keys(mockSuggestedColors).map((color) => ({
[color]: mockSuggestedColors[color],
}));
+ const createComponent = (initialState = mockConfig) => {
+ const store = new Vuex.Store(labelSelectModule());
+
+ store.dispatch('setInitialState', initialState);
+
+ wrapper = shallowMountExtended(DropdownContentsCreateView, {
+ store,
+ });
+ };
+
+ const findColorSelectorInput = () => wrapper.findByTestId('selected-color');
+ const findLabelTitleInput = () => wrapper.findByTestId('label-title');
+ const findCreateClickButton = () => wrapper.findByTestId('create-click');
+ const findAllLinks = () => wrapper.find('.dropdown-content').findAllComponents(GlLink);
+
beforeEach(() => {
gon.suggested_label_colors = mockSuggestedColors;
- wrapper = createComponent();
+ createComponent();
});
describe('computed', () => {
describe('disableCreate', () => {
it('returns `true` when label title and color is not defined', () => {
- expect(wrapper.vm.disableCreate).toBe(true);
+ expect(findCreateClickButton().props('disabled')).toBe(true);
});
it('returns `true` when `labelCreateInProgress` is true', 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({
- labelTitle: 'Foo',
- selectedColor: '#ff0000',
- });
+ await findColorSelectorInput().vm.$emit('input', '#ff0000');
+ await findLabelTitleInput().vm.$emit('input', 'Foo');
wrapper.vm.$store.dispatch('requestCreateLabel');
await nextTick();
- expect(wrapper.vm.disableCreate).toBe(true);
+
+ expect(findCreateClickButton().props('disabled')).toBe(true);
});
it('returns `false` when label title and color is defined and create request is not already in progress', 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({
- labelTitle: 'Foo',
- selectedColor: '#ff0000',
- });
+ await findColorSelectorInput().vm.$emit('input', '#ff0000');
+ await findLabelTitleInput().vm.$emit('input', 'Foo');
- await nextTick();
- expect(wrapper.vm.disableCreate).toBe(false);
+ expect(findCreateClickButton().props('disabled')).toBe(false);
});
});
describe('suggestedColors', () => {
it('returns array of color objects containing color code and name', () => {
colors.forEach((color, index) => {
- expect(wrapper.vm.suggestedColors[index]).toEqual(expect.objectContaining(color));
+ expect(findAllLinks().at(index).attributes('title')).toBe(Object.values(color)[0]);
});
});
});
@@ -82,29 +79,29 @@ describe('DropdownContentsCreateView', () => {
describe('getColorName', () => {
it('returns color name from color object', () => {
+ expect(findAllLinks().at(0).attributes('title')).toBe(Object.values(colors[0]).pop());
expect(wrapper.vm.getColorName(colors[0])).toBe(Object.values(colors[0]).pop());
});
});
describe('handleColorClick', () => {
- it('sets provided `color` param to `selectedColor` prop', () => {
- wrapper.vm.handleColorClick(colors[0]);
+ it('sets provided `color` param to `selectedColor` prop', async () => {
+ await findAllLinks()
+ .at(0)
+ .vm.$emit('click', { preventDefault: () => {} });
- expect(wrapper.vm.selectedColor).toBe(Object.keys(colors[0]).pop());
+ expect(findColorSelectorInput().attributes('value')).toBe(Object.keys(colors[0]).pop());
});
});
describe('handleCreateClick', () => {
it('calls action `createLabel` with object containing `labelTitle` & `selectedColor`', async () => {
jest.spyOn(wrapper.vm, 'createLabel').mockImplementation();
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({
- labelTitle: 'Foo',
- selectedColor: '#ff0000',
- });
- wrapper.vm.handleCreateClick();
+ await findColorSelectorInput().vm.$emit('input', '#ff0000');
+ await findLabelTitleInput().vm.$emit('input', 'Foo');
+
+ findCreateClickButton().vm.$emit('click');
await nextTick();
expect(wrapper.vm.createLabel).toHaveBeenCalledWith(
@@ -154,20 +151,14 @@ describe('DropdownContentsCreateView', () => {
});
it('renders color block element for all suggested colors', () => {
- const colorBlocksEl = wrapper.find('.dropdown-content').findAllComponents(GlLink);
-
- colorBlocksEl.wrappers.forEach((colorBlock, index) => {
+ findAllLinks().wrappers.forEach((colorBlock, index) => {
expect(colorBlock.attributes('style')).toContain('background-color');
expect(colorBlock.attributes('title')).toBe(Object.values(colors[index]).pop());
});
});
it('renders color input element', 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({
- selectedColor: '#ff0000',
- });
+ await findColorSelectorInput().vm.$emit('input', '#ff0000');
await nextTick();
const colorPreviewEl = wrapper
diff --git a/spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_contents_create_view_spec.js b/spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_contents_create_view_spec.js
index 4ca0a813da2..9c8d9656955 100644
--- a/spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_contents_create_view_spec.js
+++ b/spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_contents_create_view_spec.js
@@ -8,11 +8,13 @@ import { createAlert } from '~/alert';
import { workspaceLabelsQueries } from '~/sidebar/constants';
import DropdownContentsCreateView from '~/sidebar/components/labels/labels_select_widget/dropdown_contents_create_view.vue';
import createLabelMutation from '~/sidebar/components/labels/labels_select_widget/graphql/create_label.mutation.graphql';
+import { DEFAULT_LABEL_COLOR } from '~/sidebar/components/labels/labels_select_widget/constants';
import {
mockRegularLabel,
mockSuggestedColors,
createLabelSuccessfulResponse,
workspaceLabelsQueryResponse,
+ workspaceLabelsQueryEmptyResponse,
} from './mock_data';
jest.mock('~/alert');
@@ -61,14 +63,16 @@ describe('DropdownContentsCreateView', () => {
mutationHandler = createLabelSuccessHandler,
labelCreateType = 'project',
workspaceType = 'project',
+ labelsResponse = workspaceLabelsQueryResponse,
+ searchTerm = '',
} = {}) => {
const mockApollo = createMockApollo([[createLabelMutation, mutationHandler]]);
mockApollo.clients.defaultClient.cache.writeQuery({
query: workspaceLabelsQueries[workspaceType].query,
- data: workspaceLabelsQueryResponse.data,
+ data: labelsResponse.data,
variables: {
fullPath: '',
- searchTerm: '',
+ searchTerm,
},
});
@@ -94,7 +98,7 @@ describe('DropdownContentsCreateView', () => {
it('selects a color after clicking on colored block', async () => {
createComponent();
- expect(findSelectedColor().attributes('style')).toBeUndefined();
+ expect(findSelectedColorText().attributes('value')).toBe(DEFAULT_LABEL_COLOR);
findAllColors().at(0).vm.$emit('click', new Event('mouseclick'));
await nextTick();
@@ -104,7 +108,7 @@ describe('DropdownContentsCreateView', () => {
it('shows correct color hex code after selecting a color', async () => {
createComponent();
- expect(findSelectedColorText().attributes('value')).toBe('');
+ expect(findSelectedColorText().attributes('value')).toBe(DEFAULT_LABEL_COLOR);
findAllColors().at(0).vm.$emit('click', new Event('mouseclick'));
await nextTick();
@@ -123,6 +127,7 @@ describe('DropdownContentsCreateView', () => {
it('disables a Create button if color is not set', async () => {
createComponent();
findLabelTitleInput().vm.$emit('input', 'Test title');
+ findSelectedColorText().vm.$emit('input', '');
await nextTick();
expect(findCreateButton().props('disabled')).toBe(true);
@@ -232,4 +237,21 @@ describe('DropdownContentsCreateView', () => {
titleTakenError.data.labelCreate.errors[0],
);
});
+
+ describe('when empty labels response', () => {
+ it('is able to create label with searched text when empty response', async () => {
+ createComponent({ searchTerm: '', labelsResponse: workspaceLabelsQueryEmptyResponse });
+
+ findLabelTitleInput().vm.$emit('input', 'random');
+
+ findCreateButton().vm.$emit('click');
+ await waitForPromises();
+
+ expect(createLabelSuccessHandler).toHaveBeenCalledWith({
+ color: DEFAULT_LABEL_COLOR,
+ projectPath: '',
+ title: 'random',
+ });
+ });
+ });
});
diff --git a/spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_contents_labels_view_spec.js b/spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_contents_labels_view_spec.js
index c351a60735b..1abc708b72f 100644
--- a/spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_contents_labels_view_spec.js
+++ b/spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_contents_labels_view_spec.js
@@ -96,11 +96,11 @@ describe('DropdownContentsLabelsView', () => {
await waitForPromises();
});
- it('does not render loading icon', async () => {
+ it('does not render loading icon', () => {
expect(findLoadingIcon().exists()).toBe(false);
});
- it('renders labels list', async () => {
+ it('renders labels list', () => {
expect(findLabelsList().exists()).toBe(true);
expect(findLabels()).toHaveLength(2);
});
diff --git a/spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_header_spec.js b/spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_header_spec.js
index 824f91812fb..4861d2ca55e 100644
--- a/spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_header_spec.js
+++ b/spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_header_spec.js
@@ -45,7 +45,7 @@ describe('DropdownHeader', () => {
expect(findGoBackButton().exists()).toBe(true);
});
- it('does not render search input field', async () => {
+ it('does not render search input field', () => {
expect(findSearchInput().exists()).toBe(false);
});
});
@@ -81,7 +81,7 @@ describe('DropdownHeader', () => {
expect(findSearchInput().exists()).toBe(true);
});
- it('does not render title', async () => {
+ it('does not render title', () => {
expect(findDropdownTitle().exists()).toBe(false);
});
});
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 3101fd90f2e..b0a080ba1ef 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
@@ -199,7 +199,7 @@ describe('LabelsSelectRoot', () => {
});
});
- it('emits `updateSelectedLabels` event on dropdown contents `setLabels` event if iid is not set', async () => {
+ it('emits `updateSelectedLabels` event on dropdown contents `setLabels` event if iid is not set', () => {
const label = { id: 'gid://gitlab/ProjectLabel/1' };
createComponent({ config: { ...mockConfig, iid: undefined } });
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 5d5a7e9a200..b0b473625bb 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
@@ -117,6 +117,17 @@ export const workspaceLabelsQueryResponse = {
},
};
+export const workspaceLabelsQueryEmptyResponse = {
+ data: {
+ workspace: {
+ id: 'gid://gitlab/Project/126',
+ labels: {
+ nodes: [],
+ },
+ },
+ },
+};
+
export const issuableLabelsQueryResponse = {
data: {
workspace: {
diff --git a/spec/frontend/sidebar/components/move/issuable_move_dropdown_spec.js b/spec/frontend/sidebar/components/move/issuable_move_dropdown_spec.js
index e247f5d27fa..7c168a0ac41 100644
--- a/spec/frontend/sidebar/components/move/issuable_move_dropdown_spec.js
+++ b/spec/frontend/sidebar/components/move/issuable_move_dropdown_spec.js
@@ -328,7 +328,7 @@ describe('IssuableMoveDropdown', () => {
expect(axios.get).toHaveBeenCalled();
});
- it('gl-dropdown component prevents dropdown body from closing on `hide` event when `projectItemClick` prop is true', async () => {
+ it('gl-dropdown component prevents dropdown body from closing on `hide` event when `projectItemClick` prop is 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({
@@ -341,7 +341,7 @@ describe('IssuableMoveDropdown', () => {
expect(wrapper.vm.projectItemClick).toBe(false);
});
- it('gl-dropdown component emits `dropdown-close` event on component from `hide` event', async () => {
+ it('gl-dropdown component emits `dropdown-close` event on component from `hide` event', () => {
findDropdownEl().vm.$emit('hide');
expect(wrapper.emitted('dropdown-close')).toHaveLength(1);
diff --git a/spec/frontend/sidebar/components/move/move_issue_button_spec.js b/spec/frontend/sidebar/components/move/move_issue_button_spec.js
index eb5e23c6047..e2f5414056a 100644
--- a/spec/frontend/sidebar/components/move/move_issue_button_spec.js
+++ b/spec/frontend/sidebar/components/move/move_issue_button_spec.js
@@ -71,10 +71,6 @@ describe('MoveIssueButton', () => {
});
};
- afterEach(() => {
- fakeApollo = null;
- });
-
it('renders the project select dropdown', () => {
createComponent();
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 662a39c829d..2c7982a4b7f 100644
--- a/spec/frontend/sidebar/components/move/move_issues_button_spec.js
+++ b/spec/frontend/sidebar/components/move/move_issues_button_spec.js
@@ -346,7 +346,7 @@ describe('MoveIssuesButton', () => {
expect(issuableEventHub.$emit).not.toHaveBeenCalled();
});
- it('emits `issuables:bulkMoveStarted` when issues are moving', async () => {
+ it('emits `issuables:bulkMoveStarted` when issues are moving', () => {
createComponent({ selectedIssuables: selectedIssuesMocks.issuesTasksAndTestCases });
emitMoveIssuablesEvent();
diff --git a/spec/frontend/sidebar/components/reviewers/uncollapsed_reviewer_list_spec.js b/spec/frontend/sidebar/components/reviewers/uncollapsed_reviewer_list_spec.js
index 483449f924b..66bc1f393ae 100644
--- a/spec/frontend/sidebar/components/reviewers/uncollapsed_reviewer_list_spec.js
+++ b/spec/frontend/sidebar/components/reviewers/uncollapsed_reviewer_list_spec.js
@@ -1,9 +1,10 @@
import { shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
import { TEST_HOST } from 'helpers/test_constants';
import ReviewerAvatarLink from '~/sidebar/components/reviewers/reviewer_avatar_link.vue';
import UncollapsedReviewerList from '~/sidebar/components/reviewers/uncollapsed_reviewer_list.vue';
-const userDataMock = () => ({
+const userDataMock = ({ approved = false } = {}) => ({
id: 1,
name: 'Root',
state: 'active',
@@ -14,14 +15,21 @@ const userDataMock = () => ({
canMerge: true,
canUpdate: true,
reviewed: true,
- approved: false,
+ approved,
},
});
describe('UncollapsedReviewerList component', () => {
let wrapper;
- const reviewerApprovalIcons = () => wrapper.findAll('[data-testid="re-approved"]');
+ const findAllRerequestButtons = () => wrapper.findAll('[data-testid="re-request-button"]');
+ const findAllReviewerApprovalIcons = () => wrapper.findAll('[data-testid="approved"]');
+ const findAllReviewedNotApprovedIcons = () =>
+ wrapper.findAll('[data-testid="reviewed-not-approved"]');
+ const findAllReviewerAvatarLinks = () => wrapper.findAllComponents(ReviewerAvatarLink);
+
+ const hasApprovalIconAnimation = () =>
+ findAllReviewerApprovalIcons().at(0).classes('merge-request-approved-icon');
function createComponent(props = {}, glFeatures = {}) {
const propsData = {
@@ -48,27 +56,17 @@ describe('UncollapsedReviewerList component', () => {
});
it('only has one user', () => {
- expect(wrapper.findAllComponents(ReviewerAvatarLink).length).toBe(1);
+ expect(findAllReviewerAvatarLinks()).toHaveLength(1);
});
it('shows one user with avatar, and author name', () => {
- expect(wrapper.text()).toContain(user.name);
+ expect(wrapper.text()).toBe(user.name);
});
it('renders re-request loading icon', async () => {
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- await wrapper.setData({ loadingStates: { 1: 'loading' } });
-
- expect(wrapper.find('[data-testid="re-request-button"]').props('loading')).toBe(true);
- });
-
- it('renders re-request success icon', async () => {
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- await wrapper.setData({ loadingStates: { 1: 'success' } });
+ await findAllRerequestButtons().at(0).vm.$emit('click');
- expect(wrapper.find('[data-testid="re-request-success"]').exists()).toBe(true);
+ expect(findAllRerequestButtons().at(0).props('loading')).toBe(true);
});
});
@@ -84,51 +82,126 @@ describe('UncollapsedReviewerList component', () => {
approved: true,
},
};
+ const user3 = {
+ ...user,
+ id: 3,
+ name: 'lizabeth-wilderman',
+ username: 'lizabeth-wilderman',
+ mergeRequestInteraction: {
+ ...user.mergeRequestInteraction,
+ approved: false,
+ reviewed: true,
+ },
+ };
beforeEach(() => {
createComponent({
- users: [user, user2],
+ users: [user, user2, user3],
});
});
- it('has both users', () => {
- expect(wrapper.findAllComponents(ReviewerAvatarLink).length).toBe(2);
+ it('has three users', () => {
+ expect(findAllReviewerAvatarLinks()).toHaveLength(3);
});
- it('shows both users with avatar, and author name', () => {
+ it('shows all users with avatar, and author name', () => {
expect(wrapper.text()).toContain(user.name);
expect(wrapper.text()).toContain(user2.name);
+ expect(wrapper.text()).toContain(user3.name);
});
it('renders approval icon', () => {
- expect(reviewerApprovalIcons().length).toBe(1);
+ expect(findAllReviewerApprovalIcons()).toHaveLength(1);
});
it('shows that hello-world approved', () => {
- const icon = reviewerApprovalIcons().at(0);
+ const icon = findAllReviewerApprovalIcons().at(0);
+
+ expect(icon.attributes('title')).toBe('Approved by @hello-world');
+ });
+
+ it('shows that lizabeth-wilderman reviewed but did not approve', () => {
+ const icon = findAllReviewedNotApprovedIcons().at(1);
- expect(icon.attributes('title')).toEqual('Approved by @hello-world');
+ expect(icon.attributes('title')).toBe('Reviewed by @lizabeth-wilderman but not yet approved');
});
it('renders re-request loading icon', async () => {
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- await wrapper.setData({ loadingStates: { 2: 'loading' } });
+ await findAllRerequestButtons().at(1).vm.$emit('click');
- expect(wrapper.findAll('[data-testid="re-request-button"]').length).toBe(2);
- expect(wrapper.findAll('[data-testid="re-request-button"]').at(1).props('loading')).toBe(
- true,
- );
+ const allRerequestButtons = findAllRerequestButtons();
+
+ expect(allRerequestButtons).toHaveLength(3);
+ expect(allRerequestButtons.at(1).props('loading')).toBe(true);
+ });
+ });
+
+ describe('when updating reviewers list', () => {
+ it('does not animate icon on initial page load', () => {
+ const user = userDataMock({ approved: true });
+ createComponent({ users: [user] });
+
+ expect(hasApprovalIconAnimation()).toBe(false);
});
- it('renders re-request success icon', async () => {
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- await wrapper.setData({ loadingStates: { 2: 'success' } });
+ it('does not animate icon when adding a new reviewer', async () => {
+ const user = userDataMock({ approved: true });
+ const anotherUser = { ...user, id: 2 };
+ createComponent({ users: [user] });
+
+ await wrapper.setProps({ users: [user, anotherUser] });
+
+ expect(
+ findAllReviewerApprovalIcons().wrappers.every((w) =>
+ w.classes('merge-request-approved-icon'),
+ ),
+ ).toBe(false);
+ });
+
+ it('removes animation CSS class after 1500ms', async () => {
+ const previousUserState = userDataMock({ approved: false });
+ const currentUserState = userDataMock({ approved: true });
+
+ createComponent({
+ users: [previousUserState],
+ });
- expect(wrapper.findAll('[data-testid="re-request-button"]').length).toBe(1);
- expect(wrapper.findAll('[data-testid="re-request-success"]').length).toBe(1);
- expect(wrapper.find('[data-testid="re-request-success"]').exists()).toBe(true);
+ await wrapper.setProps({
+ users: [currentUserState],
+ });
+
+ expect(hasApprovalIconAnimation()).toBe(true);
+
+ jest.advanceTimersByTime(1500);
+ await nextTick();
+
+ expect(findAllReviewerApprovalIcons().at(0).classes('merge-request-approved-icon')).toBe(
+ false,
+ );
+ });
+
+ describe('when reviewer was present in the list', () => {
+ it.each`
+ previousApprovalState | currentApprovalState | shouldAnimate
+ ${false} | ${true} | ${true}
+ ${true} | ${true} | ${false}
+ `(
+ 'when approval state changes from $previousApprovalState to $currentApprovalState',
+ async ({ previousApprovalState, currentApprovalState, shouldAnimate }) => {
+ const previousUserState = userDataMock({ approved: previousApprovalState });
+ const currentUserState = userDataMock({ approved: currentApprovalState });
+
+ createComponent({
+ users: [previousUserState],
+ });
+
+ await wrapper.setProps({
+ users: [currentUserState],
+ });
+
+ expect(hasApprovalIconAnimation()).toBe(shouldAnimate);
+ },
+ );
});
});
});
diff --git a/spec/frontend/sidebar/components/severity/sidebar_severity_spec.js b/spec/frontend/sidebar/components/severity/sidebar_severity_widget_spec.js
index 8a27e82093d..bee90d2b2b6 100644
--- a/spec/frontend/sidebar/components/severity/sidebar_severity_spec.js
+++ b/spec/frontend/sidebar/components/severity/sidebar_severity_widget_spec.js
@@ -1,5 +1,7 @@
import { GlDropdown, GlDropdownItem, GlLoadingIcon, GlTooltip, GlSprintf } from '@gitlab/ui';
-import { nextTick } from 'vue';
+import Vue, { nextTick } from 'vue';
+import VueApollo from 'vue-apollo';
+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 '~/alert';
@@ -11,15 +13,18 @@ import SidebarSeverityWidget from '~/sidebar/components/severity/sidebar_severit
jest.mock('~/alert');
-describe('SidebarSeverity', () => {
+Vue.use(VueApollo);
+
+describe('SidebarSeverityWidget', () => {
let wrapper;
- let mutate;
+ let mockApollo;
const projectPath = 'gitlab-org/gitlab-test';
const iid = '1';
const severity = 'CRITICAL';
- let canUpdate = true;
- function createComponent(props = {}) {
+ function createComponent({ props, canUpdate = true, mutationMock } = {}) {
+ mockApollo = createMockApollo([[updateIssuableSeverity, mutationMock]]);
+
const propsData = {
projectPath,
iid,
@@ -27,102 +32,101 @@ describe('SidebarSeverity', () => {
initialSeverity: severity,
...props,
};
- mutate = jest.fn();
+
wrapper = mountExtended(SidebarSeverityWidget, {
propsData,
provide: {
canUpdate,
},
- mocks: {
- $apollo: {
- mutate,
- },
- },
+ apolloProvider: mockApollo,
stubs: {
GlSprintf,
},
});
}
- beforeEach(() => {
- createComponent();
+ afterEach(() => {
+ mockApollo = null;
});
const findSeverityToken = () => wrapper.findAllComponents(SeverityToken);
const findEditBtn = () => wrapper.findByTestId('edit-button');
const findDropdown = () => wrapper.findComponent(GlDropdown);
- const findCriticalSeverityDropdownItem = () => wrapper.findComponent(GlDropdownItem);
+ const findCriticalSeverityDropdownItem = () => wrapper.findComponent(GlDropdownItem); // First dropdown item is critical severity
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findTooltip = () => wrapper.findComponent(GlTooltip);
const findCollapsedSeverity = () => wrapper.findComponent({ ref: 'severity' });
describe('Severity widget', () => {
it('renders severity dropdown and token', () => {
+ createComponent();
+
expect(findSeverityToken().exists()).toBe(true);
expect(findDropdown().exists()).toBe(true);
});
describe('edit button', () => {
it('is rendered when `canUpdate` provided as `true`', () => {
+ createComponent();
+
expect(findEditBtn().exists()).toBe(true);
});
it('is NOT rendered when `canUpdate` provided as `false`', () => {
- canUpdate = false;
- createComponent();
+ createComponent({ canUpdate: false });
+
expect(findEditBtn().exists()).toBe(false);
});
});
});
describe('Update severity', () => {
- it('calls `$apollo.mutate` with `updateIssuableSeverity`', () => {
- jest
- .spyOn(wrapper.vm.$apollo, 'mutate')
- .mockResolvedValueOnce({ data: { issueSetSeverity: { issue: { severity } } } });
+ it('calls mutate with `updateIssuableSeverity`', () => {
+ const mutationMock = jest.fn().mockResolvedValue({
+ data: { issueSetSeverity: { issue: { severity } } },
+ });
+ createComponent({ mutationMock });
findCriticalSeverityDropdownItem().vm.$emit('click');
- expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({
- mutation: updateIssuableSeverity,
- variables: {
- iid,
- projectPath,
- severity,
- },
+
+ expect(mutationMock).toHaveBeenCalledWith({
+ iid,
+ projectPath,
+ severity,
});
});
it('shows error alert when severity update fails', async () => {
- const errorMsg = 'Something went wrong';
- jest.spyOn(wrapper.vm.$apollo, 'mutate').mockRejectedValueOnce(errorMsg);
- findCriticalSeverityDropdownItem().vm.$emit('click');
+ const mutationMock = jest.fn().mockRejectedValue('Something went wrong');
+ createComponent({ mutationMock });
+ findCriticalSeverityDropdownItem().vm.$emit('click');
await waitForPromises();
expect(createAlert).toHaveBeenCalled();
});
it('shows loading icon while updating', async () => {
- let resolvePromise;
- wrapper.vm.$apollo.mutate = jest.fn(
- () =>
- new Promise((resolve) => {
- resolvePromise = resolve;
- }),
- );
- findCriticalSeverityDropdownItem().vm.$emit('click');
+ const mutationMock = jest.fn().mockRejectedValue({});
+ createComponent({ mutationMock });
+ findCriticalSeverityDropdownItem().vm.$emit('click');
await nextTick();
+
expect(findLoadingIcon().exists()).toBe(true);
- resolvePromise();
await waitForPromises();
+
expect(findLoadingIcon().exists()).toBe(false);
});
});
describe('Switch between collapsed/expanded view of the sidebar', () => {
describe('collapsed', () => {
+ beforeEach(() => {
+ createComponent({ canUpdate: false });
+ });
+
it('should have collapsed icon class', () => {
expect(findCollapsedSeverity().classes('sidebar-collapsed-icon')).toBe(true);
});
@@ -136,17 +140,19 @@ describe('SidebarSeverity', () => {
describe('expanded', () => {
it('toggles dropdown with edit button', async () => {
- canUpdate = true;
createComponent();
await nextTick();
+
expect(findDropdown().isVisible()).toBe(false);
findEditBtn().vm.$emit('click');
await nextTick();
+
expect(findDropdown().isVisible()).toBe(true);
findEditBtn().vm.$emit('click');
await nextTick();
+
expect(findDropdown().isVisible()).toBe(false);
});
});
diff --git a/spec/frontend/sidebar/components/sidebar_dropdown_widget_spec.js b/spec/frontend/sidebar/components/sidebar_dropdown_widget_spec.js
index 53d81e3fcaf..27ab347775a 100644
--- a/spec/frontend/sidebar/components/sidebar_dropdown_widget_spec.js
+++ b/spec/frontend/sidebar/components/sidebar_dropdown_widget_spec.js
@@ -1,4 +1,4 @@
-import { GlDropdown, GlLink, GlLoadingIcon, GlSearchBoxByType } from '@gitlab/ui';
+import { GlLink, GlLoadingIcon, GlSearchBoxByType } from '@gitlab/ui';
import * as Sentry from '@sentry/browser';
import { shallowMount, mount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
@@ -133,7 +133,7 @@ describe('SidebarDropdownWidget', () => {
$apollo: {
mutate: mutationPromise(),
queries: {
- currentAttribute: { loading: false },
+ issuable: { loading: false },
attributesList: { loading: false },
...queries,
},
@@ -145,26 +145,22 @@ describe('SidebarDropdownWidget', () => {
stubs: {
SidebarEditableItem,
GlSearchBoxByType,
- GlDropdown,
},
}),
);
wrapper.vm.$refs.dropdown.show = jest.fn();
-
- // We need to mock out `showDropdown` which
- // invokes `show` method of BDropdown used inside GlDropdown.
- jest.spyOn(wrapper.vm, 'showDropdown').mockImplementation();
};
describe('when not editing', () => {
beforeEach(() => {
createComponent({
data: {
- currentAttribute: { id: 'id', title: 'title', webUrl: 'webUrl', dueDate: '2021-09-09' },
+ issuable: {
+ attribute: { id: 'id', title: 'title', webUrl: 'webUrl', dueDate: '2021-09-09' },
+ },
},
stubs: {
- GlDropdown,
SidebarEditableItem,
},
});
@@ -185,7 +181,7 @@ describe('SidebarDropdownWidget', () => {
it('shows a loading spinner while fetching the current attribute', () => {
createComponent({
queries: {
- currentAttribute: { loading: true },
+ issuable: { loading: true },
},
});
@@ -199,7 +195,7 @@ describe('SidebarDropdownWidget', () => {
selectedTitle: 'Some milestone title',
},
queries: {
- currentAttribute: { loading: false },
+ issuable: { loading: false },
},
});
@@ -224,10 +220,10 @@ describe('SidebarDropdownWidget', () => {
createComponent({
data: {
hasCurrentAttribute: true,
- currentAttribute: null,
+ issuable: {},
},
queries: {
- currentAttribute: { loading: false },
+ issuable: { loading: false },
},
});
@@ -251,7 +247,9 @@ describe('SidebarDropdownWidget', () => {
{ id: '123', title: '123' },
{ id: 'id', title: 'title' },
],
- currentAttribute: { id: '123' },
+ issuable: {
+ attribute: { id: '123' },
+ },
},
mutationPromise: mutationResp,
});
diff --git a/spec/frontend/sidebar/components/subscriptions/subscriptions_spec.js b/spec/frontend/sidebar/components/subscriptions/subscriptions_spec.js
index cae21189ee0..b644b7a9421 100644
--- a/spec/frontend/sidebar/components/subscriptions/subscriptions_spec.js
+++ b/spec/frontend/sidebar/components/subscriptions/subscriptions_spec.js
@@ -1,23 +1,24 @@
import { GlToggle } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
-import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { mockTracking } from 'helpers/tracking_helper';
import Subscriptions from '~/sidebar/components/subscriptions/subscriptions.vue';
import eventHub from '~/sidebar/event_hub';
describe('Subscriptions', () => {
let wrapper;
+ let trackingSpy;
const findToggleButton = () => wrapper.findComponent(GlToggle);
+ const findTooltip = () => wrapper.findComponent({ ref: 'tooltip' });
- const mountComponent = (propsData) =>
- extendedWrapper(
- shallowMount(Subscriptions, {
- propsData,
- }),
- );
+ const mountComponent = (propsData) => {
+ wrapper = shallowMountExtended(Subscriptions, {
+ propsData,
+ });
+ };
it('shows loading spinner when loading', () => {
- wrapper = mountComponent({
+ mountComponent({
loading: true,
subscribed: undefined,
});
@@ -26,7 +27,7 @@ describe('Subscriptions', () => {
});
it('is toggled "off" when currently not subscribed', () => {
- wrapper = mountComponent({
+ mountComponent({
subscribed: false,
});
@@ -34,7 +35,7 @@ describe('Subscriptions', () => {
});
it('is toggled "on" when currently subscribed', () => {
- wrapper = mountComponent({
+ mountComponent({
subscribed: true,
});
@@ -43,44 +44,38 @@ describe('Subscriptions', () => {
it('toggleSubscription method emits `toggleSubscription` event on eventHub and Component', () => {
const id = 42;
- wrapper = mountComponent({ subscribed: true, id });
+ mountComponent({ subscribed: true, id });
const eventHubSpy = jest.spyOn(eventHub, '$emit');
- const wrapperEmitSpy = jest.spyOn(wrapper.vm, '$emit');
- wrapper.vm.toggleSubscription();
+ findToggleButton().vm.$emit('change');
expect(eventHubSpy).toHaveBeenCalledWith('toggleSubscription', id);
- expect(wrapperEmitSpy).toHaveBeenCalledWith('toggleSubscription', id);
- eventHubSpy.mockRestore();
- wrapperEmitSpy.mockRestore();
+ expect(wrapper.emitted('toggleSubscription')).toEqual([[id]]);
});
it('tracks the event when toggled', () => {
- wrapper = mountComponent({ subscribed: true });
-
- const wrapperTrackSpy = jest.spyOn(wrapper.vm, 'track');
+ trackingSpy = mockTracking('_category_', undefined, jest.spyOn);
+ mountComponent({ subscribed: true });
- wrapper.vm.toggleSubscription();
+ findToggleButton().vm.$emit('change');
- expect(wrapperTrackSpy).toHaveBeenCalledWith('toggle_button', {
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, 'toggle_button', {
+ category: undefined,
+ label: 'right_sidebar',
property: 'notifications',
value: 0,
});
- wrapperTrackSpy.mockRestore();
});
it('onClickCollapsedIcon method emits `toggleSidebar` event on component', () => {
- wrapper = mountComponent({ subscribed: true });
- const spy = jest.spyOn(wrapper.vm, '$emit');
-
- wrapper.vm.onClickCollapsedIcon();
+ mountComponent({ subscribed: true });
+ findTooltip().trigger('click');
- expect(spy).toHaveBeenCalledWith('toggleSidebar');
- spy.mockRestore();
+ expect(wrapper.emitted('toggleSidebar')).toHaveLength(1);
});
it('has visually hidden label', () => {
- wrapper = mountComponent();
+ mountComponent();
expect(findToggleButton().props()).toMatchObject({
label: 'Notifications',
@@ -92,7 +87,7 @@ describe('Subscriptions', () => {
const subscribeDisabledDescription = 'Notifications have been disabled';
beforeEach(() => {
- wrapper = mountComponent({
+ mountComponent({
subscribed: false,
projectEmailsDisabled: true,
subscribeDisabledDescription,
@@ -103,9 +98,7 @@ describe('Subscriptions', () => {
expect(wrapper.findByTestId('subscription-title').text()).toContain(
subscribeDisabledDescription,
);
- expect(wrapper.findComponent({ ref: 'tooltip' }).attributes('title')).toBe(
- subscribeDisabledDescription,
- );
+ expect(findTooltip().attributes('title')).toBe(subscribeDisabledDescription);
});
it('does not render the toggle button', () => {
diff --git a/spec/frontend/sidebar/components/time_tracking/time_tracker_spec.js b/spec/frontend/sidebar/components/time_tracking/time_tracker_spec.js
index 91c013596d7..e23d24f9629 100644
--- a/spec/frontend/sidebar/components/time_tracking/time_tracker_spec.js
+++ b/spec/frontend/sidebar/components/time_tracking/time_tracker_spec.js
@@ -144,7 +144,7 @@ describe('Issuable Time Tracker', () => {
});
describe('Comparison pane when limitToHours is true', () => {
- beforeEach(async () => {
+ beforeEach(() => {
wrapper = mountComponent({
props: {
limitToHours: true,
diff --git a/spec/frontend/sidebar/components/toggle/toggle_sidebar_spec.js b/spec/frontend/sidebar/components/toggle/toggle_sidebar_spec.js
index 0370d5e337d..8e34a612705 100644
--- a/spec/frontend/sidebar/components/toggle/toggle_sidebar_spec.js
+++ b/spec/frontend/sidebar/components/toggle/toggle_sidebar_spec.js
@@ -25,7 +25,7 @@ describe('ToggleSidebar', () => {
expect(findGlButton().props('icon')).toBe('chevron-double-lg-left');
});
- it('should render the "chevron-double-lg-right" icon when expanded', async () => {
+ it('should render the "chevron-double-lg-right" icon when expanded', () => {
createComponent({ props: { collapsed: false } });
expect(findGlButton().props('icon')).toBe('chevron-double-lg-right');
diff --git a/spec/frontend/sidebar/mock_data.js b/spec/frontend/sidebar/mock_data.js
index 844320efc1c..05a7f504fd4 100644
--- a/spec/frontend/sidebar/mock_data.js
+++ b/spec/frontend/sidebar/mock_data.js
@@ -249,6 +249,17 @@ export const issuableDueDateResponse = (dueDate = null) => ({
},
});
+export const issueDueDateSubscriptionResponse = () => ({
+ data: {
+ issuableDatesUpdated: {
+ issue: {
+ id: 'gid://gitlab/Issue/4',
+ dueDate: '2022-12-31',
+ },
+ },
+ },
+});
+
export const issuableStartDateResponse = (startDate = null) => ({
data: {
workspace: {
diff --git a/spec/frontend/snippets/components/__snapshots__/snippet_visibility_edit_spec.js.snap b/spec/frontend/snippets/components/__snapshots__/snippet_visibility_edit_spec.js.snap
index f4ebc5c3e3f..ed54582ca29 100644
--- a/spec/frontend/snippets/components/__snapshots__/snippet_visibility_edit_spec.js.snap
+++ b/spec/frontend/snippets/components/__snapshots__/snippet_visibility_edit_spec.js.snap
@@ -13,7 +13,7 @@ exports[`Snippet Visibility Edit component rendering matches the snapshot 1`] =
target="_blank"
>
<gl-icon-stub
- name="question"
+ name="question-o"
size="12"
/>
</gl-link-stub>
diff --git a/spec/frontend/snippets/components/edit_spec.js b/spec/frontend/snippets/components/edit_spec.js
index 0d0e78e9179..957cfc0bc22 100644
--- a/spec/frontend/snippets/components/edit_spec.js
+++ b/spec/frontend/snippets/components/edit_spec.js
@@ -256,7 +256,7 @@ describe('Snippet Edit app', () => {
VISIBILITY_LEVEL_PRIVATE_STRING,
VISIBILITY_LEVEL_INTERNAL_STRING,
VISIBILITY_LEVEL_PUBLIC_STRING,
- ])('marks %s visibility by default', async (visibility) => {
+ ])('marks %s visibility by default', (visibility) => {
createComponent({
props: { snippetGid: '' },
selectedLevel: visibility,
diff --git a/spec/frontend/snippets/components/snippet_blob_view_spec.js b/spec/frontend/snippets/components/snippet_blob_view_spec.js
index 840bca8c9c8..05ff64c2296 100644
--- a/spec/frontend/snippets/components/snippet_blob_view_spec.js
+++ b/spec/frontend/snippets/components/snippet_blob_view_spec.js
@@ -1,5 +1,6 @@
-import { mount } from '@vue/test-utils';
-import { nextTick } from 'vue';
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import { shallowMount } from '@vue/test-utils';
import {
Blob as BlobMock,
SimpleViewerMock,
@@ -7,6 +8,7 @@ import {
RichBlobContentMock,
SimpleBlobContentMock,
} from 'jest/blob/components/mock_data';
+import GetBlobContent from 'shared_queries/snippet/snippet_blob_content.query.graphql';
import BlobContent from '~/blob/components/blob_content.vue';
import BlobHeader from '~/blob/components/blob_header.vue';
import {
@@ -17,9 +19,13 @@ import {
import SnippetBlobView from '~/snippets/components/snippet_blob_view.vue';
import { VISIBILITY_LEVEL_PUBLIC_STRING } from '~/visibility_level/constants';
import { RichViewer, SimpleViewer } from '~/vue_shared/components/blob_viewers';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
describe('Blob Embeddable', () => {
let wrapper;
+ let requestHandlers;
+
const snippet = {
id: 'gid://foo.bar/snippet',
webUrl: 'https://foo.bar',
@@ -29,23 +35,47 @@ describe('Blob Embeddable', () => {
activeViewerType: SimpleViewerMock.type,
};
+ const mockDefaultHandler = ({ path, nodes } = { path: BlobMock.path }) => {
+ const renderedNodes = nodes || [
+ { __typename: 'Blob', path, richData: 'richData', plainData: 'plainData' },
+ ];
+
+ return jest.fn().mockResolvedValue({
+ data: {
+ snippets: {
+ __typename: 'Snippet',
+ id: '1',
+ nodes: [
+ {
+ __typename: 'Snippet',
+ id: '2',
+ blobs: {
+ __typename: 'Blob',
+ hasUnretrievableBlobs: false,
+ nodes: renderedNodes,
+ },
+ },
+ ],
+ },
+ },
+ });
+ };
+
+ const createMockApolloProvider = (handler) => {
+ Vue.use(VueApollo);
+
+ requestHandlers = handler;
+ return createMockApollo([[GetBlobContent, requestHandlers]]);
+ };
+
function createComponent({
snippetProps = {},
data = dataMock,
blob = BlobMock,
- contentLoading = false,
+ handler = mockDefaultHandler(),
} = {}) {
- const $apollo = {
- queries: {
- blobContent: {
- loading: contentLoading,
- refetch: jest.fn(),
- skip: true,
- },
- },
- };
-
- wrapper = mount(SnippetBlobView, {
+ wrapper = shallowMount(SnippetBlobView, {
+ apolloProvider: createMockApolloProvider(handler),
propsData: {
snippet: {
...snippet,
@@ -58,41 +88,56 @@ describe('Blob Embeddable', () => {
...data,
};
},
- mocks: { $apollo },
+ stubs: {
+ BlobHeader,
+ BlobContent,
+ },
});
}
+ const findBlobHeader = () => wrapper.findComponent(BlobHeader);
+ const findBlobContent = () => wrapper.findComponent(BlobContent);
+ const findSimpleViewer = () => wrapper.findComponent(SimpleViewer);
+ const findRichViewer = () => wrapper.findComponent(RichViewer);
+
describe('rendering', () => {
it('renders correct components', () => {
createComponent();
- expect(wrapper.findComponent(BlobHeader).exists()).toBe(true);
- expect(wrapper.findComponent(BlobContent).exists()).toBe(true);
+ expect(findBlobHeader().exists()).toBe(true);
+ expect(findBlobContent().exists()).toBe(true);
});
- it('sets simple viewer correctly', () => {
+ it('sets simple viewer correctly', async () => {
createComponent();
- expect(wrapper.findComponent(SimpleViewer).exists()).toBe(true);
+ await waitForPromises();
+
+ expect(findSimpleViewer().exists()).toBe(true);
});
- it('sets rich viewer correctly', () => {
+ it('sets rich viewer correctly', async () => {
const data = { ...dataMock, activeViewerType: RichViewerMock.type };
createComponent({
data,
});
- expect(wrapper.findComponent(RichViewer).exists()).toBe(true);
+ await waitForPromises();
+ expect(findRichViewer().exists()).toBe(true);
});
it('correctly switches viewer type', async () => {
createComponent();
- expect(wrapper.findComponent(SimpleViewer).exists()).toBe(true);
+ await waitForPromises();
+
+ expect(findSimpleViewer().exists()).toBe(true);
- wrapper.vm.switchViewer(RichViewerMock.type);
+ findBlobContent().vm.$emit(BLOB_RENDER_EVENT_SHOW_SOURCE, RichViewerMock.type);
+ await waitForPromises();
- await nextTick();
- expect(wrapper.findComponent(RichViewer).exists()).toBe(true);
- await wrapper.vm.switchViewer(SimpleViewerMock.type);
+ expect(findRichViewer().exists()).toBe(true);
- expect(wrapper.findComponent(SimpleViewer).exists()).toBe(true);
+ findBlobContent().vm.$emit(BLOB_RENDER_EVENT_SHOW_SOURCE, SimpleViewerMock.type);
+ await waitForPromises();
+
+ expect(findSimpleViewer().exists()).toBe(true);
});
it('passes information about render error down to blob header', () => {
@@ -106,7 +151,7 @@ describe('Blob Embeddable', () => {
},
});
- expect(wrapper.findComponent(BlobHeader).props('hasRenderError')).toBe(true);
+ expect(findBlobHeader().props('hasRenderError')).toBe(true);
});
describe('bob content in multi-file scenario', () => {
@@ -119,47 +164,38 @@ describe('Blob Embeddable', () => {
richData: 'Another Rich Foo',
};
+ const MixedSimpleBlobContentMock = {
+ ...SimpleBlobContentMock,
+ richData: '<h1>Rich</h1>',
+ };
+
+ const MixedRichBlobContentMock = {
+ ...RichBlobContentMock,
+ plainData: 'Plain',
+ };
+
it.each`
- snippetBlobs | description | currentBlob | expectedContent
- ${[SimpleBlobContentMock]} | ${'one existing textual blob'} | ${SimpleBlobContentMock} | ${SimpleBlobContentMock.plainData}
- ${[RichBlobContentMock]} | ${'one existing rich blob'} | ${RichBlobContentMock} | ${RichBlobContentMock.richData}
- ${[SimpleBlobContentMock, RichBlobContentMock]} | ${'mixed blobs with current textual blob'} | ${SimpleBlobContentMock} | ${SimpleBlobContentMock.plainData}
- ${[SimpleBlobContentMock, RichBlobContentMock]} | ${'mixed blobs with current rich blob'} | ${RichBlobContentMock} | ${RichBlobContentMock.richData}
- ${[SimpleBlobContentMock, SimpleBlobContentMock2]} | ${'textual blobs with current textual blob'} | ${SimpleBlobContentMock} | ${SimpleBlobContentMock.plainData}
- ${[RichBlobContentMock, RichBlobContentMock2]} | ${'rich blobs with current rich blob'} | ${RichBlobContentMock} | ${RichBlobContentMock.richData}
+ snippetBlobs | description | currentBlob | expectedContent | activeViewerType
+ ${[SimpleBlobContentMock]} | ${'one existing textual blob'} | ${SimpleBlobContentMock} | ${SimpleBlobContentMock.plainData} | ${SimpleViewerMock.type}
+ ${[RichBlobContentMock]} | ${'one existing rich blob'} | ${RichBlobContentMock} | ${RichBlobContentMock.richData} | ${RichViewerMock.type}
+ ${[SimpleBlobContentMock, MixedRichBlobContentMock]} | ${'mixed blobs with current textual blob'} | ${SimpleBlobContentMock} | ${SimpleBlobContentMock.plainData} | ${SimpleViewerMock.type}
+ ${[MixedSimpleBlobContentMock, RichBlobContentMock]} | ${'mixed blobs with current rich blob'} | ${RichBlobContentMock} | ${RichBlobContentMock.richData} | ${RichViewerMock.type}
+ ${[SimpleBlobContentMock, SimpleBlobContentMock2]} | ${'textual blobs with current textual blob'} | ${SimpleBlobContentMock} | ${SimpleBlobContentMock.plainData} | ${SimpleViewerMock.type}
+ ${[RichBlobContentMock, RichBlobContentMock2]} | ${'rich blobs with current rich blob'} | ${RichBlobContentMock} | ${RichBlobContentMock.richData} | ${RichViewerMock.type}
`(
'renders correct content for $description',
- async ({ snippetBlobs, currentBlob, expectedContent }) => {
- const apolloData = {
- snippets: {
- nodes: [
- {
- blobs: {
- nodes: snippetBlobs,
- },
- },
- ],
- },
- };
+ async ({ snippetBlobs, currentBlob, expectedContent, activeViewerType }) => {
createComponent({
+ handler: mockDefaultHandler({ path: currentBlob.path, nodes: snippetBlobs }),
+ data: { activeViewerType },
blob: {
...BlobMock,
path: currentBlob.path,
},
});
+ await waitForPromises();
- // mimic apollo's update
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({
- blobContent: wrapper.vm.onContentUpdate(apolloData),
- });
-
- await nextTick();
-
- const findContent = () => wrapper.findComponent(BlobContent);
-
- expect(findContent().props('content')).toBe(expectedContent);
+ expect(findBlobContent().props('content')).toBe(expectedContent);
},
);
});
@@ -174,28 +210,32 @@ describe('Blob Embeddable', () => {
window.location.hash = '#LC2';
});
- it('renders simple viewer by default', () => {
+ it('renders simple viewer by default', async () => {
createComponent({
data: {},
});
+ await waitForPromises();
- expect(wrapper.vm.activeViewerType).toBe(SimpleViewerMock.type);
- expect(wrapper.findComponent(SimpleViewer).exists()).toBe(true);
+ expect(findBlobHeader().props('activeViewerType')).toBe(SimpleViewerMock.type);
+ expect(findSimpleViewer().exists()).toBe(true);
});
describe('switchViewer()', () => {
it('switches to the passed viewer', async () => {
createComponent();
+ await waitForPromises();
+
+ findBlobContent().vm.$emit(BLOB_RENDER_EVENT_SHOW_SOURCE, RichViewerMock.type);
+ await waitForPromises();
- wrapper.vm.switchViewer(RichViewerMock.type);
+ expect(findBlobHeader().props('activeViewerType')).toBe(RichViewerMock.type);
+ expect(findRichViewer().exists()).toBe(true);
- await nextTick();
- expect(wrapper.vm.activeViewerType).toBe(RichViewerMock.type);
- expect(wrapper.findComponent(RichViewer).exists()).toBe(true);
+ findBlobContent().vm.$emit(BLOB_RENDER_EVENT_SHOW_SOURCE, SimpleViewerMock.type);
+ await waitForPromises();
- await wrapper.vm.switchViewer(SimpleViewerMock.type);
- expect(wrapper.vm.activeViewerType).toBe(SimpleViewerMock.type);
- expect(wrapper.findComponent(SimpleViewer).exists()).toBe(true);
+ expect(findBlobHeader().props('activeViewerType')).toBe(SimpleViewerMock.type);
+ expect(findSimpleViewer().exists()).toBe(true);
});
});
});
@@ -205,28 +245,32 @@ describe('Blob Embeddable', () => {
window.location.hash = '#last-headline';
});
- it('renders rich viewer by default', () => {
+ it('renders rich viewer by default', async () => {
createComponent({
data: {},
});
+ await waitForPromises();
- expect(wrapper.vm.activeViewerType).toBe(RichViewerMock.type);
- expect(wrapper.findComponent(RichViewer).exists()).toBe(true);
+ expect(findBlobHeader().props('activeViewerType')).toBe(RichViewerMock.type);
+ expect(findRichViewer().exists()).toBe(true);
});
describe('switchViewer()', () => {
it('switches to the passed viewer', async () => {
createComponent();
+ await waitForPromises();
- wrapper.vm.switchViewer(SimpleViewerMock.type);
+ findBlobContent().vm.$emit(BLOB_RENDER_EVENT_SHOW_SOURCE, SimpleViewerMock.type);
+ await waitForPromises();
- await nextTick();
- expect(wrapper.vm.activeViewerType).toBe(SimpleViewerMock.type);
- expect(wrapper.findComponent(SimpleViewer).exists()).toBe(true);
+ expect(findBlobHeader().props('activeViewerType')).toBe(SimpleViewerMock.type);
+ expect(findSimpleViewer().exists()).toBe(true);
- await wrapper.vm.switchViewer(RichViewerMock.type);
- expect(wrapper.vm.activeViewerType).toBe(RichViewerMock.type);
- expect(wrapper.findComponent(RichViewer).exists()).toBe(true);
+ findBlobContent().vm.$emit(BLOB_RENDER_EVENT_SHOW_SOURCE, RichViewerMock.type);
+ await waitForPromises();
+
+ expect(findBlobHeader().props('activeViewerType')).toBe(RichViewerMock.type);
+ expect(findRichViewer().exists()).toBe(true);
});
});
});
@@ -235,19 +279,21 @@ describe('Blob Embeddable', () => {
describe('functionality', () => {
describe('render error', () => {
- const findContentEl = () => wrapper.findComponent(BlobContent);
-
it('correctly sets blob on the blob-content-error component', () => {
createComponent();
- expect(findContentEl().props('blob')).toEqual(BlobMock);
+ expect(findBlobContent().props('blob')).toEqual(BlobMock);
});
- it(`refetches blob content on ${BLOB_RENDER_EVENT_LOAD} event`, () => {
+ it(`refetches blob content on ${BLOB_RENDER_EVENT_LOAD} event`, async () => {
createComponent();
+ await waitForPromises();
+
+ expect(requestHandlers).toHaveBeenCalledTimes(1);
+
+ findBlobContent().vm.$emit(BLOB_RENDER_EVENT_LOAD);
+ await waitForPromises();
- expect(wrapper.vm.$apollo.queries.blobContent.refetch).not.toHaveBeenCalled();
- findContentEl().vm.$emit(BLOB_RENDER_EVENT_LOAD);
- expect(wrapper.vm.$apollo.queries.blobContent.refetch).toHaveBeenCalledTimes(1);
+ expect(requestHandlers).toHaveBeenCalledTimes(2);
});
it(`sets '${SimpleViewerMock.type}' as active on ${BLOB_RENDER_EVENT_SHOW_SOURCE} event`, () => {
@@ -257,7 +303,7 @@ describe('Blob Embeddable', () => {
},
});
- findContentEl().vm.$emit(BLOB_RENDER_EVENT_SHOW_SOURCE);
+ findBlobContent().vm.$emit(BLOB_RENDER_EVENT_SHOW_SOURCE);
expect(wrapper.vm.activeViewerType).toEqual(SimpleViewerMock.type);
});
});
diff --git a/spec/frontend/snippets/components/snippet_header_spec.js b/spec/frontend/snippets/components/snippet_header_spec.js
index 994cf65c1f5..4bf64bfd3cd 100644
--- a/spec/frontend/snippets/components/snippet_header_spec.js
+++ b/spec/frontend/snippets/components/snippet_header_spec.js
@@ -256,7 +256,7 @@ describe('Snippet header component', () => {
});
describe('submit snippet as spam', () => {
- beforeEach(async () => {
+ beforeEach(() => {
createComponent();
});
diff --git a/spec/frontend/super_sidebar/components/context_switcher_spec.js b/spec/frontend/super_sidebar/components/context_switcher_spec.js
index 538e87cf843..11da6ef1243 100644
--- a/spec/frontend/super_sidebar/components/context_switcher_spec.js
+++ b/spec/frontend/super_sidebar/components/context_switcher_spec.js
@@ -1,10 +1,11 @@
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
-import { GlSearchBoxByType } from '@gitlab/ui';
+import { GlSearchBoxByType, GlLoadingIcon, GlAlert } from '@gitlab/ui';
import * as Sentry from '@sentry/browser';
import { s__ } from '~/locale';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import ContextSwitcher from '~/super_sidebar/components/context_switcher.vue';
+import NavItem from '~/super_sidebar/components/nav_item.vue';
import ProjectsList from '~/super_sidebar/components/projects_list.vue';
import GroupsList from '~/super_sidebar/components/groups_list.vue';
import createMockApollo from 'helpers/mock_apollo_helper';
@@ -22,7 +23,11 @@ jest.mock('~/super_sidebar/utils', () => ({
.formatContextSwitcherItems,
trackContextAccess: jest.fn(),
}));
+const focusInputMock = jest.fn();
+const persistentLinks = [
+ { title: 'Explore', link: '/explore', icon: 'compass', link_classes: 'persistent-link-class' },
+];
const username = 'root';
const projectsPath = 'projectsPath';
const groupsPath = 'groupsPath';
@@ -33,9 +38,12 @@ describe('ContextSwitcher component', () => {
let wrapper;
let mockApollo;
+ const findNavItems = () => wrapper.findAllComponents(NavItem);
const findSearchBox = () => wrapper.findComponent(GlSearchBoxByType);
const findProjectsList = () => wrapper.findComponent(ProjectsList);
const findGroupsList = () => wrapper.findComponent(GroupsList);
+ const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
+ const findAlert = () => wrapper.findComponent(GlAlert);
const triggerSearchQuery = async () => {
findSearchBox().vm.$emit('input', 'foo');
@@ -60,6 +68,7 @@ describe('ContextSwitcher component', () => {
wrapper = shallowMountExtended(ContextSwitcher, {
apolloProvider: mockApollo,
propsData: {
+ persistentLinks,
username,
projectsPath,
groupsPath,
@@ -68,6 +77,7 @@ describe('ContextSwitcher component', () => {
stubs: {
GlSearchBoxByType: stubComponent(GlSearchBoxByType, {
props: ['placeholder'],
+ methods: { focusInput: focusInputMock },
}),
ProjectsList: stubComponent(ProjectsList, {
props: ['username', 'viewAllLink', 'isSearch', 'searchResults'],
@@ -84,9 +94,20 @@ describe('ContextSwitcher component', () => {
createWrapper();
});
+ it('renders the persistent links', () => {
+ const navItems = findNavItems();
+ const firstNavItem = navItems.at(0);
+
+ expect(navItems.length).toBe(persistentLinks.length);
+ expect(firstNavItem.props('item')).toBe(persistentLinks[0]);
+ expect(firstNavItem.props('linkClasses')).toEqual({
+ [persistentLinks[0].link_classes]: persistentLinks[0].link_classes,
+ });
+ });
+
it('passes the placeholder to the search box', () => {
expect(findSearchBox().props('placeholder')).toBe(
- s__('Navigation|Search for projects or groups'),
+ s__('Navigation|Search your projects or groups'),
);
});
@@ -108,9 +129,22 @@ describe('ContextSwitcher component', () => {
});
});
+ it('focuses the search input when focusInput is called', () => {
+ wrapper.vm.focusInput();
+
+ expect(focusInputMock).toHaveBeenCalledTimes(1);
+ });
+
it('does not trigger the search query on mount', () => {
expect(searchUserProjectsAndGroupsHandlerSuccess).not.toHaveBeenCalled();
});
+
+ it('shows a loading spinner when search query is typed in', async () => {
+ findSearchBox().vm.$emit('input', 'foo');
+ await nextTick();
+
+ expect(findLoadingIcon().exists()).toBe(true);
+ });
});
describe('item access tracking', () => {
@@ -138,10 +172,18 @@ describe('ContextSwitcher component', () => {
return triggerSearchQuery();
});
+ it('hides persistent links', () => {
+ expect(findNavItems().length).toBe(0);
+ });
+
it('triggers the search query on search', () => {
expect(searchUserProjectsAndGroupsHandlerSuccess).toHaveBeenCalled();
});
+ it('hides the loading spinner', () => {
+ expect(findLoadingIcon().exists()).toBe(false);
+ });
+
it('passes the projects to the frequent projects list', () => {
expect(findProjectsList().props('isSearch')).toBe(true);
expect(findProjectsList().props('searchResults')).toEqual(
@@ -192,7 +234,7 @@ describe('ContextSwitcher component', () => {
jest.spyOn(Sentry, 'captureException');
});
- it('captures exception if response is formatted incorrectly', async () => {
+ it('captures exception and shows an alert if response is formatted incorrectly', async () => {
createWrapper({
requestHandlers: {
searchUserProjectsAndGroupsQueryHandler: jest.fn().mockResolvedValue({
@@ -203,9 +245,10 @@ describe('ContextSwitcher component', () => {
await triggerSearchQuery();
expect(Sentry.captureException).toHaveBeenCalled();
+ expect(findAlert().exists()).toBe(true);
});
- it('captures exception if query fails', async () => {
+ it('captures exception and shows an alert if query fails', async () => {
createWrapper({
requestHandlers: {
searchUserProjectsAndGroupsQueryHandler: jest.fn().mockRejectedValue(),
@@ -214,6 +257,7 @@ describe('ContextSwitcher component', () => {
await triggerSearchQuery();
expect(Sentry.captureException).toHaveBeenCalled();
+ expect(findAlert().exists()).toBe(true);
});
});
});
diff --git a/spec/frontend/super_sidebar/components/create_menu_spec.js b/spec/frontend/super_sidebar/components/create_menu_spec.js
index b24c6b8de7f..e05b5d30e69 100644
--- a/spec/frontend/super_sidebar/components/create_menu_spec.js
+++ b/spec/frontend/super_sidebar/components/create_menu_spec.js
@@ -1,3 +1,4 @@
+import { nextTick } from 'vue';
import { GlDisclosureDropdown, GlTooltip } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { __ } from '~/locale';
@@ -23,6 +24,14 @@ describe('CreateMenu component', () => {
createWrapper();
});
+ it('passes popper options to the dropdown', () => {
+ createWrapper();
+
+ expect(findGlDisclosureDropdown().props('popperOptions')).toEqual({
+ modifiers: [{ name: 'offset', options: { offset: [-147, 4] } }],
+ });
+ });
+
it("sets the toggle's label", () => {
expect(findGlDisclosureDropdown().props('toggleText')).toBe(__('Create new...'));
});
@@ -35,5 +44,20 @@ describe('CreateMenu component', () => {
expect(findGlDisclosureDropdown().props('toggleId')).toBe(wrapper.vm.$options.toggleId);
expect(findGlTooltip().props('target')).toBe(`#${wrapper.vm.$options.toggleId}`);
});
+
+ it('hides the tooltip when the dropdown is opened', async () => {
+ findGlDisclosureDropdown().vm.$emit('shown');
+ await nextTick();
+
+ expect(findGlTooltip().exists()).toBe(false);
+ });
+
+ it('shows the tooltip when the dropdown is closed', async () => {
+ findGlDisclosureDropdown().vm.$emit('shown');
+ findGlDisclosureDropdown().vm.$emit('hidden');
+ await nextTick();
+
+ expect(findGlTooltip().exists()).toBe(true);
+ });
});
});
diff --git a/spec/frontend/super_sidebar/components/frequent_items_list_spec.js b/spec/frontend/super_sidebar/components/frequent_items_list_spec.js
index 1e98db091f2..86cec3f3d13 100644
--- a/spec/frontend/super_sidebar/components/frequent_items_list_spec.js
+++ b/spec/frontend/super_sidebar/components/frequent_items_list_spec.js
@@ -1,3 +1,4 @@
+import { GlIcon, GlButton } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { s__ } from '~/locale';
import FrequentItemsList from '~/super_sidebar/components//frequent_items_list.vue';
@@ -16,6 +17,7 @@ describe('FrequentItemsList component', () => {
let wrapper;
const findListTitle = () => wrapper.findByTestId('list-title');
+ const findListEditButton = () => findListTitle().findComponent(GlButton);
const findItemsList = () => wrapper.findComponent(ItemsList);
const findEmptyText = () => wrapper.findByTestId('empty-text');
@@ -64,5 +66,38 @@ describe('FrequentItemsList component', () => {
it('does not render the empty text slot', () => {
expect(findEmptyText().exists()).toBe(false);
});
+
+ describe('items editing', () => {
+ it('renders edit button within header', () => {
+ const itemsEditButton = findListEditButton();
+
+ expect(itemsEditButton.exists()).toBe(true);
+ expect(itemsEditButton.attributes('title')).toBe('Toggle edit mode');
+ expect(itemsEditButton.findComponent(GlIcon).props('name')).toBe('pencil');
+ });
+
+ it('clicking edit button makes items list editable', async () => {
+ // Off by default
+ expect(findItemsList().props('editable')).toBe(false);
+
+ // On when clicked
+ await findListEditButton().vm.$emit('click');
+ expect(findItemsList().props('editable')).toBe(true);
+
+ // Off when clicked again
+ await findListEditButton().vm.$emit('click');
+ expect(findItemsList().props('editable')).toBe(false);
+ });
+
+ it('remove-item event emission from items-list causes list item to be removed', async () => {
+ const localStorageProjects = findItemsList().props('items');
+ await findListEditButton().vm.$emit('click');
+
+ await findItemsList().vm.$emit('remove-item', localStorageProjects[0]);
+
+ expect(findItemsList().props('items')).toHaveLength(maxItems - 1);
+ expect(findItemsList().props('items')).not.toContain(localStorageProjects[0]);
+ });
+ });
});
});
diff --git a/spec/frontend/super_sidebar/components/global_search/components/global_search_autocomplete_items_spec.js b/spec/frontend/super_sidebar/components/global_search/components/global_search_autocomplete_items_spec.js
index e5ba1c63996..aac321bd8e0 100644
--- a/spec/frontend/super_sidebar/components/global_search/components/global_search_autocomplete_items_spec.js
+++ b/spec/frontend/super_sidebar/components/global_search/components/global_search_autocomplete_items_spec.js
@@ -1,31 +1,24 @@
-import { GlDropdownItem, GlLoadingIcon, GlAvatar, GlAlert, GlDropdownDivider } from '@gitlab/ui';
+import {
+ GlDisclosureDropdownGroup,
+ GlDisclosureDropdownItem,
+ GlLoadingIcon,
+ GlAvatar,
+ GlAlert,
+} from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import Vue, { nextTick } from 'vue';
+import Vue from 'vue';
import Vuex from 'vuex';
-import HeaderSearchAutocompleteItems from '~/super_sidebar/components/global_search/components/global_search_autocomplete_items.vue';
-import {
- LARGE_AVATAR_PX,
- SMALL_AVATAR_PX,
-} from '~/super_sidebar/components/global_search/constants';
-import {
- PROJECTS_CATEGORY,
- GROUPS_CATEGORY,
- ISSUES_CATEGORY,
- MERGE_REQUEST_CATEGORY,
- RECENT_EPICS_CATEGORY,
-} from '~/vue_shared/global_search/constants';
+import GlobalSearchAutocompleteItems from '~/super_sidebar/components/global_search/components/global_search_autocomplete_items.vue';
+
import {
MOCK_GROUPED_AUTOCOMPLETE_OPTIONS,
+ MOCK_SCOPED_SEARCH_OPTIONS,
MOCK_SORTED_AUTOCOMPLETE_OPTIONS,
- MOCK_GROUPED_AUTOCOMPLETE_OPTIONS_SETTINGS_HELP,
- MOCK_GROUPED_AUTOCOMPLETE_OPTIONS_HELP,
- MOCK_SEARCH,
- MOCK_GROUPED_AUTOCOMPLETE_OPTIONS_2,
} from '../mock_data';
Vue.use(Vuex);
-describe('HeaderSearchAutocompleteItems', () => {
+describe('GlobalSearchAutocompleteItems', () => {
let wrapper;
const createComponent = (initialState, mockGetters, props) => {
@@ -36,30 +29,34 @@ describe('HeaderSearchAutocompleteItems', () => {
},
getters: {
autocompleteGroupedSearchOptions: () => MOCK_GROUPED_AUTOCOMPLETE_OPTIONS,
+ scopedSearchOptions: () => MOCK_SCOPED_SEARCH_OPTIONS,
...mockGetters,
},
});
- wrapper = shallowMount(HeaderSearchAutocompleteItems, {
+ wrapper = shallowMount(GlobalSearchAutocompleteItems, {
store,
propsData: {
...props,
},
+ stubs: {
+ GlDisclosureDropdownGroup,
+ GlDisclosureDropdownItem,
+ },
});
};
- const findDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
- const findGlDropdownDividers = () => wrapper.findAllComponents(GlDropdownDivider);
- const findFirstDropdownItem = () => findDropdownItems().at(0);
- const findDropdownItemTitles = () =>
- findDropdownItems().wrappers.map((w) => w.findAll('span').at(1).text());
- const findDropdownItemSubTitles = () =>
- findDropdownItems()
- .wrappers.filter((w) => w.findAll('span').length > 2)
- .map((w) => w.findAll('span').at(2).text());
- const findDropdownItemLinks = () => findDropdownItems().wrappers.map((w) => w.attributes('href'));
+ const findItems = () => wrapper.findAllComponents(GlDisclosureDropdownItem);
+ const findItemTitles = () =>
+ findItems().wrappers.map((w) => w.find('[data-testid="autocomplete-item-name"]').text());
+ const findItemSubTitles = () =>
+ findItems()
+ .wrappers.map((w) => w.find('[data-testid="autocomplete-item-namespace"]'))
+ .filter((w) => w.exists())
+ .map((w) => w.text());
+ const findItemLinks = () => findItems().wrappers.map((w) => w.find('a').attributes('href'));
const findGlLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
- const findGlAvatar = () => wrapper.findComponent(GlAvatar);
+ const findAvatars = () => wrapper.findAllComponents(GlAvatar).wrappers.map((w) => w.props('src'));
const findGlAlert = () => wrapper.findComponent(GlAlert);
describe('template', () => {
@@ -73,7 +70,7 @@ describe('HeaderSearchAutocompleteItems', () => {
});
it('does not render autocomplete options', () => {
- expect(findDropdownItems()).toHaveLength(0);
+ expect(findItems()).toHaveLength(0);
});
});
@@ -86,6 +83,7 @@ describe('HeaderSearchAutocompleteItems', () => {
expect(findGlAlert().exists()).toBe(true);
});
});
+
describe('when loading is false', () => {
beforeEach(() => {
createComponent({ loading: false });
@@ -95,143 +93,35 @@ describe('HeaderSearchAutocompleteItems', () => {
expect(findGlLoadingIcon().exists()).toBe(false);
});
- describe('Dropdown items', () => {
+ describe('Search results items', () => {
it('renders item for each option in autocomplete option', () => {
- expect(findDropdownItems()).toHaveLength(MOCK_SORTED_AUTOCOMPLETE_OPTIONS.length);
+ expect(findItems()).toHaveLength(MOCK_SORTED_AUTOCOMPLETE_OPTIONS.length);
});
it('renders titles correctly', () => {
- const expectedTitles = MOCK_SORTED_AUTOCOMPLETE_OPTIONS.map((o) => o.value || o.label);
- expect(findDropdownItemTitles()).toStrictEqual(expectedTitles);
+ const expectedTitles = MOCK_SORTED_AUTOCOMPLETE_OPTIONS.map((o) => o.value || o.text);
+ expect(findItemTitles()).toStrictEqual(expectedTitles);
});
it('renders sub-titles correctly', () => {
const expectedSubTitles = MOCK_SORTED_AUTOCOMPLETE_OPTIONS.filter((o) => o.value).map(
- (o) => o.label,
+ (o) => o.namespace,
);
- expect(findDropdownItemSubTitles()).toStrictEqual(expectedSubTitles);
- });
-
- it('renders links correctly', () => {
- const expectedLinks = MOCK_SORTED_AUTOCOMPLETE_OPTIONS.map((o) => o.url);
- expect(findDropdownItemLinks()).toStrictEqual(expectedLinks);
- });
- });
-
- describe.each`
- item | showAvatar | avatarSize | searchContext | entityId | entityName
- ${{ data: [{ category: PROJECTS_CATEGORY, avatar_url: null }] }} | ${true} | ${String(LARGE_AVATAR_PX)} | ${{ project: { id: 29 } }} | ${'29'} | ${''}
- ${{ data: [{ category: GROUPS_CATEGORY, avatar_url: '/123' }] }} | ${true} | ${String(LARGE_AVATAR_PX)} | ${{ group: { id: 12 } }} | ${'12'} | ${''}
- ${{ data: [{ category: 'Help', avatar_url: '' }] }} | ${true} | ${String(SMALL_AVATAR_PX)} | ${null} | ${'0'} | ${''}
- ${{ data: [{ category: 'Settings' }] }} | ${false} | ${false} | ${null} | ${false} | ${false}
- ${{ data: [{ category: GROUPS_CATEGORY, avatar_url: null }] }} | ${true} | ${String(LARGE_AVATAR_PX)} | ${{ group: { id: 1, name: 'test1' } }} | ${'1'} | ${'test1'}
- ${{ data: [{ category: PROJECTS_CATEGORY, avatar_url: null }] }} | ${true} | ${String(LARGE_AVATAR_PX)} | ${{ project: { id: 2, name: 'test2' } }} | ${'2'} | ${'test2'}
- ${{ data: [{ category: ISSUES_CATEGORY, avatar_url: null }] }} | ${true} | ${String(SMALL_AVATAR_PX)} | ${{ project: { id: 3, name: 'test3' } }} | ${'3'} | ${'test3'}
- ${{ data: [{ category: MERGE_REQUEST_CATEGORY, avatar_url: null }] }} | ${true} | ${String(SMALL_AVATAR_PX)} | ${{ project: { id: 4, name: 'test4' } }} | ${'4'} | ${'test4'}
- ${{ data: [{ category: RECENT_EPICS_CATEGORY, avatar_url: null }] }} | ${true} | ${String(SMALL_AVATAR_PX)} | ${{ group: { id: 5, name: 'test5' } }} | ${'5'} | ${'test5'}
- ${{ data: [{ category: GROUPS_CATEGORY, avatar_url: null, group_id: 6, group_name: 'test6' }] }} | ${true} | ${String(LARGE_AVATAR_PX)} | ${null} | ${'6'} | ${'test6'}
- ${{ data: [{ category: PROJECTS_CATEGORY, avatar_url: null, project_id: 7, project_name: 'test7' }] }} | ${true} | ${String(LARGE_AVATAR_PX)} | ${null} | ${'7'} | ${'test7'}
- ${{ data: [{ category: ISSUES_CATEGORY, avatar_url: null, project_id: 8, project_name: 'test8' }] }} | ${true} | ${String(SMALL_AVATAR_PX)} | ${null} | ${'8'} | ${'test8'}
- ${{ data: [{ category: MERGE_REQUEST_CATEGORY, avatar_url: null, project_id: 9, project_name: 'test9' }] }} | ${true} | ${String(SMALL_AVATAR_PX)} | ${null} | ${'9'} | ${'test9'}
- ${{ data: [{ category: RECENT_EPICS_CATEGORY, avatar_url: null, group_id: 10, group_name: 'test10' }] }} | ${true} | ${String(SMALL_AVATAR_PX)} | ${null} | ${'10'} | ${'test10'}
- ${{ data: [{ category: GROUPS_CATEGORY, avatar_url: null, group_id: 11, group_name: 'test11' }] }} | ${true} | ${String(LARGE_AVATAR_PX)} | ${{ group: { id: 1, name: 'test1' } }} | ${'11'} | ${'test11'}
- ${{ data: [{ category: PROJECTS_CATEGORY, avatar_url: null, project_id: 12, project_name: 'test12' }] }} | ${true} | ${String(LARGE_AVATAR_PX)} | ${{ project: { id: 2, name: 'test2' } }} | ${'12'} | ${'test12'}
- ${{ data: [{ category: ISSUES_CATEGORY, avatar_url: null, project_id: 13, project_name: 'test13' }] }} | ${true} | ${String(SMALL_AVATAR_PX)} | ${{ project: { id: 3, name: 'test3' } }} | ${'13'} | ${'test13'}
- ${{ data: [{ category: MERGE_REQUEST_CATEGORY, avatar_url: null, project_id: 14, project_name: 'test14' }] }} | ${true} | ${String(SMALL_AVATAR_PX)} | ${{ project: { id: 4, name: 'test4' } }} | ${'14'} | ${'test14'}
- ${{ data: [{ category: RECENT_EPICS_CATEGORY, avatar_url: null, group_id: 15, group_name: 'test15' }] }} | ${true} | ${String(SMALL_AVATAR_PX)} | ${{ group: { id: 5, name: 'test5' } }} | ${'15'} | ${'test15'}
- `('GlAvatar', ({ item, showAvatar, avatarSize, searchContext, entityId, entityName }) => {
- describe(`when category is ${item.data[0].category} and avatar_url is ${item.data[0].avatar_url}`, () => {
- beforeEach(() => {
- createComponent({ searchContext }, { autocompleteGroupedSearchOptions: () => [item] });
- });
-
- it(`should${showAvatar ? '' : ' not'} render`, () => {
- expect(findGlAvatar().exists()).toBe(showAvatar);
- });
-
- it(`should set avatarSize to ${avatarSize}`, () => {
- expect(findGlAvatar().exists() && findGlAvatar().attributes('size')).toBe(avatarSize);
- });
-
- it(`should set avatar entityId to ${entityId}`, () => {
- expect(findGlAvatar().exists() && findGlAvatar().attributes('entityid')).toBe(entityId);
- });
-
- it(`should set avatar entityName to ${entityName}`, () => {
- expect(findGlAvatar().exists() && findGlAvatar().attributes('entityname')).toBe(
- entityName,
- );
- });
- });
- });
- });
-
- describe.each`
- currentFocusedOption | isFocused | ariaSelected
- ${null} | ${false} | ${undefined}
- ${{ html_id: 'not-a-match' }} | ${false} | ${undefined}
- ${MOCK_SORTED_AUTOCOMPLETE_OPTIONS[0]} | ${true} | ${'true'}
- `('isOptionFocused', ({ currentFocusedOption, isFocused, ariaSelected }) => {
- describe(`when currentFocusedOption.html_id is ${currentFocusedOption?.html_id}`, () => {
- beforeEach(() => {
- createComponent({}, {}, { currentFocusedOption });
- });
- it(`should${isFocused ? '' : ' not'} have gl-bg-gray-50 applied`, () => {
- expect(findFirstDropdownItem().classes('gl-bg-gray-50')).toBe(isFocused);
+ expect(findItemSubTitles()).toStrictEqual(expectedSubTitles);
});
- it(`sets "aria-selected to ${ariaSelected}`, () => {
- expect(findFirstDropdownItem().attributes('aria-selected')).toBe(ariaSelected);
+ it('renders links correctly', () => {
+ const expectedLinks = MOCK_SORTED_AUTOCOMPLETE_OPTIONS.map((o) => o.href);
+ expect(findItemLinks()).toStrictEqual(expectedLinks);
});
- });
- });
- describe.each`
- search | items | dividerCount
- ${null} | ${[]} | ${0}
- ${''} | ${[]} | ${0}
- ${'1'} | ${[]} | ${0}
- ${')'} | ${[]} | ${0}
- ${'t'} | ${MOCK_GROUPED_AUTOCOMPLETE_OPTIONS_SETTINGS_HELP} | ${1}
- ${'te'} | ${MOCK_GROUPED_AUTOCOMPLETE_OPTIONS_HELP} | ${0}
- ${'tes'} | ${MOCK_GROUPED_AUTOCOMPLETE_OPTIONS_2} | ${1}
- ${MOCK_SEARCH} | ${MOCK_GROUPED_AUTOCOMPLETE_OPTIONS_2} | ${1}
- `('Header Search Dropdown Dividers', ({ search, items, dividerCount }) => {
- describe(`when search is ${search}`, () => {
- beforeEach(() => {
- createComponent(
- { search },
- {
- autocompleteGroupedSearchOptions: () => items,
- },
- {},
+ it('renders avatars', () => {
+ const expectedAvatars = MOCK_SORTED_AUTOCOMPLETE_OPTIONS.map((o) => o.avatar_url).filter(
+ Boolean,
);
+ expect(findAvatars()).toStrictEqual(expectedAvatars);
});
-
- it(`component should have ${dividerCount} dividers`, () => {
- expect(findGlDropdownDividers()).toHaveLength(dividerCount);
- });
- });
- });
- });
-
- describe('watchers', () => {
- describe('currentFocusedOption', () => {
- beforeEach(() => {
- createComponent();
- });
-
- it('when focused changes to existing element calls scroll into view on the newly focused element', async () => {
- const focusedElement = findFirstDropdownItem().element;
- const scrollSpy = jest.spyOn(focusedElement, 'scrollIntoView');
-
- wrapper.setProps({ currentFocusedOption: MOCK_SORTED_AUTOCOMPLETE_OPTIONS[0] });
-
- await nextTick();
-
- expect(scrollSpy).toHaveBeenCalledWith(false);
- scrollSpy.mockRestore();
});
});
});
diff --git a/spec/frontend/super_sidebar/components/global_search/components/global_search_default_items_spec.js b/spec/frontend/super_sidebar/components/global_search/components/global_search_default_items_spec.js
index 132f8e60598..52e9aa52c14 100644
--- a/spec/frontend/super_sidebar/components/global_search/components/global_search_default_items_spec.js
+++ b/spec/frontend/super_sidebar/components/global_search/components/global_search_default_items_spec.js
@@ -1,13 +1,13 @@
-import { GlDropdownItem, GlDropdownSectionHeader } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
+import { GlDisclosureDropdownGroup, GlDisclosureDropdownItem } from '@gitlab/ui';
import Vue from 'vue';
import Vuex from 'vuex';
-import HeaderSearchDefaultItems from '~/super_sidebar/components/global_search/components/global_search_default_items.vue';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import GlobalSearchDefaultItems from '~/super_sidebar/components/global_search/components/global_search_default_items.vue';
import { MOCK_SEARCH_CONTEXT, MOCK_DEFAULT_SEARCH_OPTIONS } from '../mock_data';
Vue.use(Vuex);
-describe('HeaderSearchDefaultItems', () => {
+describe('GlobalSearchDefaultItems', () => {
let wrapper;
const createComponent = (initialState, props) => {
@@ -21,19 +21,19 @@ describe('HeaderSearchDefaultItems', () => {
},
});
- wrapper = shallowMount(HeaderSearchDefaultItems, {
+ wrapper = shallowMountExtended(GlobalSearchDefaultItems, {
store,
propsData: {
...props,
},
+ stubs: {
+ GlDisclosureDropdownGroup,
+ },
});
};
- const findDropdownHeader = () => wrapper.findComponent(GlDropdownSectionHeader);
- const findDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
- const findFirstDropdownItem = () => findDropdownItems().at(0);
- const findDropdownItemTitles = () => findDropdownItems().wrappers.map((w) => w.text());
- const findDropdownItemLinks = () => findDropdownItems().wrappers.map((w) => w.attributes('href'));
+ const findItems = () => wrapper.findAllComponents(GlDisclosureDropdownItem);
+ const findItemsData = () => findItems().wrappers.map((w) => w.props('item'));
describe('template', () => {
describe('Dropdown items', () => {
@@ -42,26 +42,20 @@ describe('HeaderSearchDefaultItems', () => {
});
it('renders item for each option in defaultSearchOptions', () => {
- expect(findDropdownItems()).toHaveLength(MOCK_DEFAULT_SEARCH_OPTIONS.length);
- });
-
- it('renders titles correctly', () => {
- const expectedTitles = MOCK_DEFAULT_SEARCH_OPTIONS.map((o) => o.title);
- expect(findDropdownItemTitles()).toStrictEqual(expectedTitles);
+ expect(findItems()).toHaveLength(MOCK_DEFAULT_SEARCH_OPTIONS.length);
});
- it('renders links correctly', () => {
- const expectedLinks = MOCK_DEFAULT_SEARCH_OPTIONS.map((o) => o.url);
- expect(findDropdownItemLinks()).toStrictEqual(expectedLinks);
+ it('provides the `item` prop to the `GlDisclosureDropdownItem` component', () => {
+ expect(findItemsData()).toStrictEqual(MOCK_DEFAULT_SEARCH_OPTIONS);
});
});
describe.each`
- group | project | dropdownTitle
+ group | project | groupHeader
${null} | ${null} | ${'All GitLab'}
${{ name: 'Test Group' }} | ${null} | ${'Test Group'}
${{ name: 'Test Group' }} | ${{ name: 'Test Project' }} | ${'Test Project'}
- `('Dropdown Header', ({ group, project, dropdownTitle }) => {
+ `('Group Header', ({ group, project, groupHeader }) => {
describe(`when group is ${group?.name} and project is ${project?.name}`, () => {
beforeEach(() => {
createComponent({
@@ -72,29 +66,8 @@ describe('HeaderSearchDefaultItems', () => {
});
});
- it(`should render as ${dropdownTitle}`, () => {
- expect(findDropdownHeader().text()).toBe(dropdownTitle);
- });
- });
- });
-
- describe.each`
- currentFocusedOption | isFocused | ariaSelected
- ${null} | ${false} | ${undefined}
- ${{ html_id: 'not-a-match' }} | ${false} | ${undefined}
- ${MOCK_DEFAULT_SEARCH_OPTIONS[0]} | ${true} | ${'true'}
- `('isOptionFocused', ({ currentFocusedOption, isFocused, ariaSelected }) => {
- describe(`when currentFocusedOption.html_id is ${currentFocusedOption?.html_id}`, () => {
- beforeEach(() => {
- createComponent({}, { currentFocusedOption });
- });
-
- it(`should${isFocused ? '' : ' not'} have gl-bg-gray-50 applied`, () => {
- expect(findFirstDropdownItem().classes('gl-bg-gray-50')).toBe(isFocused);
- });
-
- it(`sets "aria-selected to ${ariaSelected}`, () => {
- expect(findFirstDropdownItem().attributes('aria-selected')).toBe(ariaSelected);
+ it(`should render as ${groupHeader}`, () => {
+ expect(wrapper.text()).toContain(groupHeader);
});
});
});
diff --git a/spec/frontend/super_sidebar/components/global_search/components/global_search_scoped_items_spec.js b/spec/frontend/super_sidebar/components/global_search/components/global_search_scoped_items_spec.js
index fa91ef43ced..4976f3be4cd 100644
--- a/spec/frontend/super_sidebar/components/global_search/components/global_search_scoped_items_spec.js
+++ b/spec/frontend/super_sidebar/components/global_search/components/global_search_scoped_items_spec.js
@@ -1,21 +1,21 @@
-import { GlDropdownItem, GlToken, GlIcon } from '@gitlab/ui';
+import { GlDisclosureDropdownGroup, GlDisclosureDropdownItem, GlToken, GlIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import Vuex from 'vuex';
import { trimText } from 'helpers/text_helper';
-import HeaderSearchScopedItems from '~/super_sidebar/components/global_search/components/global_search_scoped_items.vue';
+import GlobalSearchScopedItems from '~/super_sidebar/components/global_search/components/global_search_scoped_items.vue';
import { truncate } from '~/lib/utils/text_utility';
import { SCOPE_TOKEN_MAX_LENGTH } from '~/super_sidebar/components/global_search/constants';
import { MSG_IN_ALL_GITLAB } from '~/vue_shared/global_search/constants';
import {
MOCK_SEARCH,
- MOCK_SCOPED_SEARCH_OPTIONS,
+ MOCK_SCOPED_SEARCH_GROUP,
MOCK_GROUPED_AUTOCOMPLETE_OPTIONS,
} from '../mock_data';
Vue.use(Vuex);
-describe('HeaderSearchScopedItems', () => {
+describe('GlobalSearchScopedItems', () => {
let wrapper;
const createComponent = (initialState, mockGetters, props) => {
@@ -25,96 +25,67 @@ describe('HeaderSearchScopedItems', () => {
...initialState,
},
getters: {
- scopedSearchOptions: () => MOCK_SCOPED_SEARCH_OPTIONS,
+ scopedSearchGroup: () => MOCK_SCOPED_SEARCH_GROUP,
autocompleteGroupedSearchOptions: () => MOCK_GROUPED_AUTOCOMPLETE_OPTIONS,
...mockGetters,
},
});
- wrapper = shallowMount(HeaderSearchScopedItems, {
+ wrapper = shallowMount(GlobalSearchScopedItems, {
store,
propsData: {
...props,
},
+ stubs: {
+ GlDisclosureDropdownGroup,
+ GlDisclosureDropdownItem,
+ },
});
};
- const findDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
- const findFirstDropdownItem = () => findDropdownItems().at(0);
- const findDropdownItemTitles = () => findDropdownItems().wrappers.map((w) => trimText(w.text()));
+ const findItems = () => wrapper.findAllComponents(GlDisclosureDropdownItem);
+ const findItemsText = () => findItems().wrappers.map((w) => trimText(w.text()));
const findScopeTokens = () => wrapper.findAllComponents(GlToken);
const findScopeTokensText = () => findScopeTokens().wrappers.map((w) => trimText(w.text()));
const findScopeTokensIcons = () =>
findScopeTokens().wrappers.map((w) => w.findAllComponents(GlIcon));
- const findDropdownItemAriaLabels = () =>
- findDropdownItems().wrappers.map((w) => trimText(w.attributes('aria-label')));
- const findDropdownItemLinks = () => findDropdownItems().wrappers.map((w) => w.attributes('href'));
-
- describe('template', () => {
- describe('Dropdown items', () => {
- beforeEach(() => {
- createComponent();
- });
-
- it('renders item for each option in scopedSearchOptions', () => {
- expect(findDropdownItems()).toHaveLength(MOCK_SCOPED_SEARCH_OPTIONS.length);
- });
+ const findItemLinks = () => findItems().wrappers.map((w) => w.find('a').attributes('href'));
- it('renders titles correctly', () => {
- findDropdownItemTitles().forEach((title) => expect(title).toContain(MOCK_SEARCH));
- });
-
- it('renders scope names correctly', () => {
- const expectedTitles = MOCK_SCOPED_SEARCH_OPTIONS.map((o) =>
- truncate(trimText(`in ${o.description || o.scope}`), SCOPE_TOKEN_MAX_LENGTH),
- );
+ describe('Search results scoped items', () => {
+ beforeEach(() => {
+ createComponent();
+ });
- expect(findScopeTokensText()).toStrictEqual(expectedTitles);
- });
+ it('renders item for each item in scopedSearchGroup', () => {
+ expect(findItems()).toHaveLength(MOCK_SCOPED_SEARCH_GROUP.items.length);
+ });
- it('renders scope icons correctly', () => {
- findScopeTokensIcons().forEach((icon, i) => {
- const w = icon.wrappers[0];
- expect(w?.attributes('name')).toBe(MOCK_SCOPED_SEARCH_OPTIONS[i].icon);
- });
- });
+ it('renders titles correctly', () => {
+ findItemsText().forEach((title) => expect(title).toContain(MOCK_SEARCH));
+ });
- it(`renders scope ${MSG_IN_ALL_GITLAB} correctly`, () => {
- expect(findScopeTokens().at(-1).findComponent(GlIcon).exists()).toBe(false);
- });
+ it('renders scope names correctly', () => {
+ const expectedTitles = MOCK_SCOPED_SEARCH_GROUP.items.map((o) =>
+ truncate(trimText(`in ${o.scope || o.description}`), SCOPE_TOKEN_MAX_LENGTH),
+ );
- it('renders aria-labels correctly', () => {
- const expectedLabels = MOCK_SCOPED_SEARCH_OPTIONS.map((o) =>
- trimText(`${MOCK_SEARCH} ${o.description || o.icon} ${o.scope || ''}`),
- );
- expect(findDropdownItemAriaLabels()).toStrictEqual(expectedLabels);
- });
+ expect(findScopeTokensText()).toStrictEqual(expectedTitles);
+ });
- it('renders links correctly', () => {
- const expectedLinks = MOCK_SCOPED_SEARCH_OPTIONS.map((o) => o.url);
- expect(findDropdownItemLinks()).toStrictEqual(expectedLinks);
+ it('renders scope icons correctly', () => {
+ findScopeTokensIcons().forEach((icon, i) => {
+ const w = icon.wrappers[0];
+ expect(w?.attributes('name')).toBe(MOCK_SCOPED_SEARCH_GROUP.items[i].icon);
});
});
- describe.each`
- currentFocusedOption | isFocused | ariaSelected
- ${null} | ${false} | ${undefined}
- ${{ html_id: 'not-a-match' }} | ${false} | ${undefined}
- ${MOCK_SCOPED_SEARCH_OPTIONS[0]} | ${true} | ${'true'}
- `('isOptionFocused', ({ currentFocusedOption, isFocused, ariaSelected }) => {
- describe(`when currentFocusedOption.html_id is ${currentFocusedOption?.html_id}`, () => {
- beforeEach(() => {
- createComponent({}, {}, { currentFocusedOption });
- });
-
- it(`should${isFocused ? '' : ' not'} have gl-bg-gray-50 applied`, () => {
- expect(findFirstDropdownItem().classes('gl-bg-gray-50')).toBe(isFocused);
- });
+ it(`renders scope ${MSG_IN_ALL_GITLAB} correctly`, () => {
+ expect(findScopeTokens().at(-1).findComponent(GlIcon).exists()).toBe(false);
+ });
- it(`sets "aria-selected to ${ariaSelected}`, () => {
- expect(findFirstDropdownItem().attributes('aria-selected')).toBe(ariaSelected);
- });
- });
+ it('renders links correctly', () => {
+ const expectedLinks = MOCK_SCOPED_SEARCH_GROUP.items.map((o) => o.href);
+ expect(findItemLinks()).toStrictEqual(expectedLinks);
});
});
});
diff --git a/spec/frontend/super_sidebar/components/global_search/components/global_search_spec.js b/spec/frontend/super_sidebar/components/global_search/components/global_search_spec.js
index 0dcfc448125..eb8801f68c6 100644
--- a/spec/frontend/super_sidebar/components/global_search/components/global_search_spec.js
+++ b/spec/frontend/super_sidebar/components/global_search/components/global_search_spec.js
@@ -1,30 +1,25 @@
import { GlSearchBoxByType, GlToken, GlIcon } from '@gitlab/ui';
-import Vue, { nextTick } from 'vue';
+import Vue from 'vue';
import Vuex from 'vuex';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import { mockTracking } from 'helpers/tracking_helper';
import { s__, sprintf } from '~/locale';
-import HeaderSearchApp from '~/super_sidebar/components/global_search/components/global_search.vue';
-import HeaderSearchAutocompleteItems from '~/super_sidebar/components/global_search/components/global_search_autocomplete_items.vue';
-import HeaderSearchDefaultItems from '~/super_sidebar/components/global_search/components/global_search_default_items.vue';
-import HeaderSearchScopedItems from '~/super_sidebar/components/global_search/components/global_search_scoped_items.vue';
+import GlobalSearchModal from '~/super_sidebar/components/global_search/components/global_search.vue';
+import GlobalSearchAutocompleteItems from '~/super_sidebar/components/global_search/components/global_search_autocomplete_items.vue';
+import GlobalSearchDefaultItems from '~/super_sidebar/components/global_search/components/global_search_default_items.vue';
+import GlobalSearchScopedItems from '~/super_sidebar/components/global_search/components/global_search_scoped_items.vue';
import {
SEARCH_INPUT_DESCRIPTION,
SEARCH_RESULTS_DESCRIPTION,
- SEARCH_BOX_INDEX,
ICON_PROJECT,
ICON_GROUP,
ICON_SUBGROUP,
SCOPE_TOKEN_MAX_LENGTH,
IS_SEARCHING,
- IS_NOT_FOCUSED,
- IS_FOCUSED,
SEARCH_SHORTCUTS_MIN_CHARACTERS,
} from '~/super_sidebar/components/global_search/constants';
-import DropdownKeyboardNavigation from '~/vue_shared/components/dropdown_keyboard_navigation.vue';
-import { ENTER_KEY } from '~/lib/utils/keys';
-import { visitUrl } from '~/lib/utils/url_utility';
import { truncate } from '~/lib/utils/text_utility';
+import { visitUrl } from '~/lib/utils/url_utility';
+import { ENTER_KEY } from '~/lib/utils/keys';
import {
MOCK_SEARCH,
MOCK_SEARCH_QUERY,
@@ -32,6 +27,8 @@ import {
MOCK_DEFAULT_SEARCH_OPTIONS,
MOCK_SCOPED_SEARCH_OPTIONS,
MOCK_SEARCH_CONTEXT_FULL,
+ MOCK_PROJECT,
+ MOCK_GROUP,
} from '../mock_data';
Vue.use(Vuex);
@@ -40,7 +37,7 @@ jest.mock('~/lib/utils/url_utility', () => ({
visitUrl: jest.fn(),
}));
-describe('HeaderSearchApp', () => {
+describe('GlobalSearchModal', () => {
let wrapper;
const actionSpies = {
@@ -49,21 +46,31 @@ describe('HeaderSearchApp', () => {
clearAutocomplete: jest.fn(),
};
- const createComponent = (initialState, mockGetters) => {
+ const deafaultMockState = {
+ searchContext: {
+ project: MOCK_PROJECT,
+ group: MOCK_GROUP,
+ },
+ };
+
+ const createComponent = (initialState, mockGetters, stubs) => {
const store = new Vuex.Store({
state: {
+ ...deafaultMockState,
...initialState,
},
actions: actionSpies,
getters: {
searchQuery: () => MOCK_SEARCH_QUERY,
searchOptions: () => MOCK_DEFAULT_SEARCH_OPTIONS,
+ scopedSearchOptions: () => MOCK_SCOPED_SEARCH_OPTIONS,
...mockGetters,
},
});
- wrapper = shallowMountExtended(HeaderSearchApp, {
+ wrapper = shallowMountExtended(GlobalSearchModal, {
store,
+ stubs,
});
};
@@ -80,16 +87,13 @@ describe('HeaderSearchApp', () => {
);
};
- const findHeaderSearchForm = () => wrapper.findByTestId('header-search-form');
- const findHeaderSearchInput = () => wrapper.findComponent(GlSearchBoxByType);
+ const findGlobalSearchForm = () => wrapper.findByTestId('global-search-form');
+ const findGlobalSearchInput = () => wrapper.findComponent(GlSearchBoxByType);
const findScopeToken = () => wrapper.findComponent(GlToken);
- const findHeaderSearchInputKBD = () => wrapper.find('.keyboard-shortcut-helper');
- const findHeaderSearchDropdown = () => wrapper.findByTestId('header-search-dropdown-menu');
- const findHeaderSearchDefaultItems = () => wrapper.findComponent(HeaderSearchDefaultItems);
- const findHeaderSearchScopedItems = () => wrapper.findComponent(HeaderSearchScopedItems);
- const findHeaderSearchAutocompleteItems = () =>
- wrapper.findComponent(HeaderSearchAutocompleteItems);
- const findDropdownKeyboardNavigation = () => wrapper.findComponent(DropdownKeyboardNavigation);
+ const findGlobalSearchDefaultItems = () => wrapper.findComponent(GlobalSearchDefaultItems);
+ const findGlobalSearchScopedItems = () => wrapper.findComponent(GlobalSearchScopedItems);
+ const findGlobalSearchAutocompleteItems = () =>
+ wrapper.findComponent(GlobalSearchAutocompleteItems);
const findSearchInputDescription = () => wrapper.find(`#${SEARCH_INPUT_DESCRIPTION}`);
const findSearchResultsDescription = () => wrapper.findByTestId(SEARCH_RESULTS_DESCRIPTION);
@@ -99,16 +103,8 @@ describe('HeaderSearchApp', () => {
createComponent();
});
- it('Header Search Input', () => {
- expect(findHeaderSearchInput().exists()).toBe(true);
- });
-
- it('Header Search Input KBD hint', () => {
- expect(findHeaderSearchInputKBD().exists()).toBe(true);
- expect(findHeaderSearchInputKBD().text()).toContain('/');
- expect(findHeaderSearchInputKBD().attributes('title')).toContain(
- 'Use the shortcut key <kbd>/</kbd> to start a search',
- );
+ it('Global Search Input', () => {
+ expect(findGlobalSearchInput().exists()).toBe(true);
});
it('Search Input Description', () => {
@@ -121,26 +117,6 @@ describe('HeaderSearchApp', () => {
});
describe.each`
- showDropdown | username | showSearchDropdown
- ${false} | ${null} | ${false}
- ${false} | ${MOCK_USERNAME} | ${false}
- ${true} | ${null} | ${false}
- ${true} | ${MOCK_USERNAME} | ${true}
- `('Header Search Dropdown', ({ showDropdown, username, showSearchDropdown }) => {
- describe(`when showDropdown is ${showDropdown} and current_username is ${username}`, () => {
- beforeEach(() => {
- window.gon.current_username = username;
- createComponent();
- findHeaderSearchInput().vm.$emit(showDropdown ? 'click' : '');
- });
-
- it(`should${showSearchDropdown ? '' : ' not'} render`, () => {
- expect(findHeaderSearchDropdown().exists()).toBe(showSearchDropdown);
- });
- });
- });
-
- describe.each`
search | showDefault | showScoped | showAutocomplete
${null} | ${true} | ${false} | ${false}
${''} | ${true} | ${false} | ${false}
@@ -148,71 +124,40 @@ describe('HeaderSearchApp', () => {
${'te'} | ${false} | ${false} | ${true}
${'tes'} | ${false} | ${true} | ${true}
${MOCK_SEARCH} | ${false} | ${true} | ${true}
- `('Header Search Dropdown Items', ({ search, showDefault, showScoped, showAutocomplete }) => {
+ `('Global Search Result Items', ({ search, showDefault, showScoped, showAutocomplete }) => {
describe(`when search is ${search}`, () => {
beforeEach(() => {
window.gon.current_username = MOCK_USERNAME;
createComponent({ search }, {});
- findHeaderSearchInput().vm.$emit('click');
- });
-
- it(`should${showDefault ? '' : ' not'} render the Default Dropdown Items`, () => {
- expect(findHeaderSearchDefaultItems().exists()).toBe(showDefault);
- });
-
- it(`should${showScoped ? '' : ' not'} render the Scoped Dropdown Items`, () => {
- expect(findHeaderSearchScopedItems().exists()).toBe(showScoped);
- });
-
- it(`should${showAutocomplete ? '' : ' not'} render the Autocomplete Dropdown Items`, () => {
- expect(findHeaderSearchAutocompleteItems().exists()).toBe(showAutocomplete);
- });
-
- it(`should render the Dropdown Navigation Component`, () => {
- expect(findDropdownKeyboardNavigation().exists()).toBe(true);
+ findGlobalSearchInput().vm.$emit('click');
});
- it(`should close the dropdown when press escape key`, async () => {
- findHeaderSearchInput().vm.$emit('keydown', new KeyboardEvent({ key: 27 }));
- await nextTick();
- expect(findHeaderSearchDropdown().exists()).toBe(false);
- expect(wrapper.emitted().expandSearchBar.length).toBe(1);
+ it(`should${showDefault ? '' : ' not'} render the Default Items`, () => {
+ expect(findGlobalSearchDefaultItems().exists()).toBe(showDefault);
});
- });
- });
- describe.each`
- username | showDropdown | expectedDesc
- ${null} | ${false} | ${HeaderSearchApp.i18n.SEARCH_INPUT_DESCRIBE_BY_NO_DROPDOWN}
- ${null} | ${true} | ${HeaderSearchApp.i18n.SEARCH_INPUT_DESCRIBE_BY_NO_DROPDOWN}
- ${MOCK_USERNAME} | ${false} | ${HeaderSearchApp.i18n.SEARCH_INPUT_DESCRIBE_BY_WITH_DROPDOWN}
- ${MOCK_USERNAME} | ${true} | ${HeaderSearchApp.i18n.SEARCH_INPUT_DESCRIBE_BY_WITH_DROPDOWN}
- `('Search Input Description', ({ username, showDropdown, expectedDesc }) => {
- describe(`current_username is ${username} and showDropdown is ${showDropdown}`, () => {
- beforeEach(() => {
- window.gon.current_username = username;
- createComponent();
- findHeaderSearchInput().vm.$emit(showDropdown ? 'click' : '');
+ it(`should${showScoped ? '' : ' not'} render the Scoped Items`, () => {
+ expect(findGlobalSearchScopedItems().exists()).toBe(showScoped);
});
- it(`sets description to ${expectedDesc}`, () => {
- expect(findSearchInputDescription().text()).toBe(expectedDesc);
+ it(`should${showAutocomplete ? '' : ' not'} render the Autocomplete Items`, () => {
+ expect(findGlobalSearchAutocompleteItems().exists()).toBe(showAutocomplete);
});
});
});
describe.each`
- username | showDropdown | search | loading | searchOptions | expectedDesc
- ${null} | ${true} | ${''} | ${false} | ${[]} | ${''}
- ${MOCK_USERNAME} | ${false} | ${''} | ${false} | ${[]} | ${''}
- ${MOCK_USERNAME} | ${true} | ${''} | ${false} | ${MOCK_DEFAULT_SEARCH_OPTIONS} | ${`${MOCK_DEFAULT_SEARCH_OPTIONS.length} default results provided. Use the up and down arrow keys to navigate search results list.`}
- ${MOCK_USERNAME} | ${true} | ${''} | ${true} | ${MOCK_DEFAULT_SEARCH_OPTIONS} | ${`${MOCK_DEFAULT_SEARCH_OPTIONS.length} default results provided. Use the up and down arrow keys to navigate search results list.`}
- ${MOCK_USERNAME} | ${true} | ${MOCK_SEARCH} | ${false} | ${MOCK_SCOPED_SEARCH_OPTIONS} | ${`Results updated. ${MOCK_SCOPED_SEARCH_OPTIONS.length} results available. Use the up and down arrow keys to navigate search results list, or ENTER to submit.`}
- ${MOCK_USERNAME} | ${true} | ${MOCK_SEARCH} | ${true} | ${MOCK_SCOPED_SEARCH_OPTIONS} | ${HeaderSearchApp.i18n.SEARCH_RESULTS_LOADING}
+ username | search | loading | searchOptions | expectedDesc
+ ${null} | ${'gi'} | ${false} | ${[]} | ${GlobalSearchModal.i18n.MIN_SEARCH_TERM}
+ ${MOCK_USERNAME} | ${'gi'} | ${false} | ${[]} | ${GlobalSearchModal.i18n.MIN_SEARCH_TERM}
+ ${MOCK_USERNAME} | ${''} | ${false} | ${MOCK_DEFAULT_SEARCH_OPTIONS} | ${`${MOCK_DEFAULT_SEARCH_OPTIONS.length} default results provided. Use the up and down arrow keys to navigate search results list.`}
+ ${MOCK_USERNAME} | ${MOCK_SEARCH} | ${true} | ${MOCK_DEFAULT_SEARCH_OPTIONS} | ${GlobalSearchModal.i18n.SEARCH_RESULTS_LOADING}
+ ${MOCK_USERNAME} | ${MOCK_SEARCH} | ${false} | ${MOCK_SCOPED_SEARCH_OPTIONS} | ${`Results updated. ${MOCK_SCOPED_SEARCH_OPTIONS.length} results available. Use the up and down arrow keys to navigate search results list, or ENTER to submit.`}
+ ${MOCK_USERNAME} | ${MOCK_SEARCH} | ${true} | ${MOCK_SCOPED_SEARCH_OPTIONS} | ${GlobalSearchModal.i18n.SEARCH_RESULTS_LOADING}
`(
'Search Results Description',
- ({ username, showDropdown, search, loading, searchOptions, expectedDesc }) => {
- describe(`search is "${search}", loading is ${loading}, and showSearchDropdown is ${showDropdown}`, () => {
+ ({ username, search, loading, searchOptions, expectedDesc }) => {
+ describe(`search is "${search}" and loading is ${loading}`, () => {
beforeEach(() => {
window.gon.current_username = username;
createComponent(
@@ -224,7 +169,6 @@ describe('HeaderSearchApp', () => {
searchOptions: () => searchOptions,
},
);
- findHeaderSearchInput().vm.$emit(showDropdown ? 'click' : '');
});
it(`sets description to ${expectedDesc}`, () => {
@@ -253,7 +197,7 @@ describe('HeaderSearchApp', () => {
searchOptions: () => searchOptions,
},
);
- findHeaderSearchInput().vm.$emit('click');
+ findGlobalSearchInput().vm.$emit('click');
});
it(`${hasToken ? 'is' : 'is NOT'} rendered when data set has type "${
@@ -274,42 +218,31 @@ describe('HeaderSearchApp', () => {
describe('form', () => {
describe.each`
- searchContext | search | searchOptions | isFocused
- ${MOCK_SEARCH_CONTEXT_FULL} | ${null} | ${[]} | ${true}
- ${MOCK_SEARCH_CONTEXT_FULL} | ${MOCK_SEARCH} | ${[]} | ${true}
- ${MOCK_SEARCH_CONTEXT_FULL} | ${MOCK_SEARCH} | ${MOCK_SCOPED_SEARCH_OPTIONS} | ${true}
- ${MOCK_SEARCH_CONTEXT_FULL} | ${MOCK_SEARCH} | ${MOCK_SCOPED_SEARCH_OPTIONS} | ${false}
- ${null} | ${MOCK_SEARCH} | ${MOCK_SCOPED_SEARCH_OPTIONS} | ${true}
- ${null} | ${null} | ${MOCK_SCOPED_SEARCH_OPTIONS} | ${true}
- ${null} | ${null} | ${[]} | ${true}
- `('wrapper', ({ searchContext, search, searchOptions, isFocused }) => {
+ searchContext | search | searchOptions
+ ${MOCK_SEARCH_CONTEXT_FULL} | ${null} | ${[]}
+ ${MOCK_SEARCH_CONTEXT_FULL} | ${MOCK_SEARCH} | ${[]}
+ ${MOCK_SEARCH_CONTEXT_FULL} | ${MOCK_SEARCH} | ${MOCK_SCOPED_SEARCH_OPTIONS}
+ ${MOCK_SEARCH_CONTEXT_FULL} | ${MOCK_SEARCH} | ${MOCK_SCOPED_SEARCH_OPTIONS}
+ ${null} | ${MOCK_SEARCH} | ${MOCK_SCOPED_SEARCH_OPTIONS}
+ ${null} | ${null} | ${MOCK_SCOPED_SEARCH_OPTIONS}
+ ${null} | ${null} | ${[]}
+ `('wrapper', ({ searchContext, search, searchOptions }) => {
beforeEach(() => {
window.gon.current_username = MOCK_USERNAME;
createComponent({ search, searchContext }, { searchOptions: () => searchOptions });
- if (isFocused) {
- findHeaderSearchInput().vm.$emit('click');
- }
});
const isSearching = search?.length > SEARCH_SHORTCUTS_MIN_CHARACTERS;
it(`classes ${isSearching ? 'contain' : 'do not contain'} "${IS_SEARCHING}"`, () => {
if (isSearching) {
- expect(findHeaderSearchForm().classes()).toContain(IS_SEARCHING);
+ expect(findGlobalSearchForm().classes()).toContain(IS_SEARCHING);
return;
}
if (!isSearching) {
- expect(findHeaderSearchForm().classes()).not.toContain(IS_SEARCHING);
+ expect(findGlobalSearchForm().classes()).not.toContain(IS_SEARCHING);
}
});
-
- it(`classes ${isSearching ? 'contain' : 'do not contain'} "${
- isFocused ? IS_FOCUSED : IS_NOT_FOCUSED
- }"`, () => {
- expect(findHeaderSearchForm().classes()).toContain(
- isFocused ? IS_FOCUSED : IS_NOT_FOCUSED,
- );
- });
});
});
@@ -328,7 +261,7 @@ describe('HeaderSearchApp', () => {
searchOptions: () => searchOptions,
},
);
- findHeaderSearchInput().vm.$emit('click');
+ findGlobalSearchInput().vm.$emit('click');
});
it(`icon for data set type "${searchOptions[0]?.html_id}" ${
@@ -350,56 +283,15 @@ describe('HeaderSearchApp', () => {
describe('events', () => {
beforeEach(() => {
- window.gon.current_username = MOCK_USERNAME;
createComponent();
+ window.gon.current_username = MOCK_USERNAME;
});
- describe('Header Search Input', () => {
- describe('when dropdown is closed', () => {
- let trackingSpy;
-
- beforeEach(() => {
- trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
- });
-
- it('onFocus opens dropdown and triggers snowplow event', async () => {
- expect(findHeaderSearchDropdown().exists()).toBe(false);
- findHeaderSearchInput().vm.$emit('focus');
-
- await nextTick();
-
- expect(findHeaderSearchDropdown().exists()).toBe(true);
- expect(trackingSpy).toHaveBeenCalledWith(undefined, 'focus_input', {
- label: 'global_search',
- property: 'navigation_top',
- });
- });
-
- it('onClick opens dropdown and triggers snowplow event', async () => {
- expect(findHeaderSearchDropdown().exists()).toBe(false);
- findHeaderSearchInput().vm.$emit('click');
-
- await nextTick();
-
- expect(findHeaderSearchDropdown().exists()).toBe(true);
- expect(trackingSpy).toHaveBeenCalledWith(undefined, 'focus_input', {
- label: 'global_search',
- property: 'navigation_top',
- });
- });
-
- it('onClick followed by onFocus only triggers a single snowplow event', async () => {
- findHeaderSearchInput().vm.$emit('click');
- findHeaderSearchInput().vm.$emit('focus');
-
- expect(trackingSpy).toHaveBeenCalledTimes(1);
- });
- });
-
+ describe('Global Search Input', () => {
describe('onInput', () => {
describe('when search has text', () => {
beforeEach(() => {
- findHeaderSearchInput().vm.$emit('input', MOCK_SEARCH);
+ findGlobalSearchInput().vm.$emit('input', MOCK_SEARCH);
});
it('calls setSearch with search term', () => {
@@ -417,7 +309,7 @@ describe('HeaderSearchApp', () => {
describe('when search is emptied', () => {
beforeEach(() => {
- findHeaderSearchInput().vm.$emit('input', '');
+ findGlobalSearchInput().vm.$emit('input', '');
});
it('calls setSearch with empty term', () => {
@@ -433,83 +325,29 @@ describe('HeaderSearchApp', () => {
});
});
});
- });
-
- describe('Dropdown Keyboard Navigation', () => {
- beforeEach(() => {
- findHeaderSearchInput().vm.$emit('click');
- });
-
- it('closes dropdown when @tab is emitted', async () => {
- expect(findHeaderSearchDropdown().exists()).toBe(true);
- findDropdownKeyboardNavigation().vm.$emit('tab');
-
- await nextTick();
-
- expect(findHeaderSearchDropdown().exists()).toBe(false);
- });
- });
- });
-
- describe('computed', () => {
- describe.each`
- MOCK_INDEX | search
- ${1} | ${null}
- ${SEARCH_BOX_INDEX} | ${'test'}
- ${2} | ${'test1'}
- `('currentFocusedOption', ({ MOCK_INDEX, search }) => {
- beforeEach(() => {
- window.gon.current_username = MOCK_USERNAME;
- createComponent({ search });
- findHeaderSearchInput().vm.$emit('click');
- });
-
- it(`when currentFocusIndex changes to ${MOCK_INDEX} updates the data to searchOptions[${MOCK_INDEX}]`, () => {
- findDropdownKeyboardNavigation().vm.$emit('change', MOCK_INDEX);
- expect(wrapper.vm.currentFocusedOption).toBe(MOCK_DEFAULT_SEARCH_OPTIONS[MOCK_INDEX]);
- });
- });
- });
-
- describe('Submitting a search', () => {
- describe('with no currentFocusedOption', () => {
- beforeEach(() => {
- createComponent();
- });
- it('onKey-enter submits a search', () => {
- findHeaderSearchInput().vm.$emit('keydown', new KeyboardEvent({ key: ENTER_KEY }));
-
- expect(visitUrl).toHaveBeenCalledWith(MOCK_SEARCH_QUERY);
- });
- });
-
- describe('with less than min characters and no dropdown results', () => {
- beforeEach(() => {
- createComponent({ search: 'x' });
- });
-
- it('onKey-enter will NOT submit a search', () => {
- findHeaderSearchInput().vm.$emit('keydown', new KeyboardEvent({ key: ENTER_KEY }));
+ describe('Submitting a search', () => {
+ beforeEach(() => {
+ createComponent();
+ });
- expect(visitUrl).not.toHaveBeenCalledWith(MOCK_SEARCH_QUERY);
- });
- });
+ it('onKey-enter submits a search', () => {
+ findGlobalSearchInput().vm.$emit('keydown', new KeyboardEvent({ key: ENTER_KEY }));
- describe('with currentFocusedOption', () => {
- const MOCK_INDEX = 1;
+ expect(visitUrl).toHaveBeenCalledWith(MOCK_SEARCH_QUERY);
+ });
- beforeEach(() => {
- window.gon.current_username = MOCK_USERNAME;
- createComponent();
- findHeaderSearchInput().vm.$emit('click');
- });
+ describe('with less than min characters', () => {
+ beforeEach(() => {
+ createComponent({ search: 'x' });
+ });
- it('onKey-enter clicks the selected dropdown item rather than submitting a search', () => {
- findDropdownKeyboardNavigation().vm.$emit('change', MOCK_INDEX);
+ it('onKey-enter will NOT submit a search', () => {
+ findGlobalSearchInput().vm.$emit('keydown', new KeyboardEvent({ key: ENTER_KEY }));
- findHeaderSearchInput().vm.$emit('keydown', new KeyboardEvent({ key: ENTER_KEY }));
- expect(visitUrl).toHaveBeenCalledWith(MOCK_DEFAULT_SEARCH_OPTIONS[MOCK_INDEX].url);
+ expect(visitUrl).not.toHaveBeenCalledWith(MOCK_SEARCH_QUERY);
+ });
+ });
});
});
});
diff --git a/spec/frontend/super_sidebar/components/global_search/mock_data.js b/spec/frontend/super_sidebar/components/global_search/mock_data.js
index 58e578e4c4c..0884fce567c 100644
--- a/spec/frontend/super_sidebar/components/global_search/mock_data.js
+++ b/spec/frontend/super_sidebar/components/global_search/mock_data.js
@@ -3,6 +3,7 @@ import {
ICON_GROUP,
ICON_SUBGROUP,
} from '~/super_sidebar/components/global_search/constants';
+
import {
PROJECTS_CATEGORY,
GROUPS_CATEGORY,
@@ -77,90 +78,107 @@ export const MOCK_SEARCH_CONTEXT_FULL = {
export const MOCK_DEFAULT_SEARCH_OPTIONS = [
{
- html_id: 'default-issues-assigned',
- title: MSG_ISSUES_ASSIGNED_TO_ME,
- url: `${MOCK_ISSUE_PATH}/?assignee_username=${MOCK_USERNAME}`,
+ text: MSG_ISSUES_ASSIGNED_TO_ME,
+ href: `${MOCK_ISSUE_PATH}/?assignee_username=${MOCK_USERNAME}`,
},
{
- html_id: 'default-issues-created',
- title: MSG_ISSUES_IVE_CREATED,
- url: `${MOCK_ISSUE_PATH}/?author_username=${MOCK_USERNAME}`,
+ text: MSG_ISSUES_IVE_CREATED,
+ href: `${MOCK_ISSUE_PATH}/?author_username=${MOCK_USERNAME}`,
},
{
- html_id: 'default-mrs-assigned',
- title: MSG_MR_ASSIGNED_TO_ME,
- url: `${MOCK_MR_PATH}/?assignee_username=${MOCK_USERNAME}`,
+ text: MSG_MR_ASSIGNED_TO_ME,
+ href: `${MOCK_MR_PATH}/?assignee_username=${MOCK_USERNAME}`,
},
{
- html_id: 'default-mrs-reviewer',
- title: MSG_MR_IM_REVIEWER,
- url: `${MOCK_MR_PATH}/?reviewer_username=${MOCK_USERNAME}`,
+ text: MSG_MR_IM_REVIEWER,
+ href: `${MOCK_MR_PATH}/?reviewer_username=${MOCK_USERNAME}`,
},
{
- html_id: 'default-mrs-created',
- title: MSG_MR_IVE_CREATED,
- url: `${MOCK_MR_PATH}/?author_username=${MOCK_USERNAME}`,
+ text: MSG_MR_IVE_CREATED,
+ href: `${MOCK_MR_PATH}/?author_username=${MOCK_USERNAME}`,
},
];
-
-export const MOCK_SCOPED_SEARCH_OPTIONS = [
+export const MOCK_SCOPED_SEARCH_OPTIONS_DEF = [
{
- html_id: 'scoped-in-project',
+ text: 'scoped-in-project',
scope: MOCK_PROJECT.name,
scopeCategory: PROJECTS_CATEGORY,
icon: ICON_PROJECT,
- url: MOCK_PROJECT.path,
- },
- {
- html_id: 'scoped-in-project-long',
- scope: MOCK_PROJECT_LONG.name,
- scopeCategory: PROJECTS_CATEGORY,
- icon: ICON_PROJECT,
- url: MOCK_PROJECT_LONG.path,
+ href: MOCK_PROJECT.path,
},
{
- html_id: 'scoped-in-group',
+ text: 'scoped-in-group',
scope: MOCK_GROUP.name,
scopeCategory: GROUPS_CATEGORY,
icon: ICON_GROUP,
- url: MOCK_GROUP.path,
- },
- {
- html_id: 'scoped-in-subgroup',
- scope: MOCK_SUBGROUP.name,
- scopeCategory: GROUPS_CATEGORY,
- icon: ICON_SUBGROUP,
- url: MOCK_SUBGROUP.path,
+ href: MOCK_GROUP.path,
},
{
- html_id: 'scoped-in-all',
+ text: 'scoped-in-all',
description: MSG_IN_ALL_GITLAB,
- url: MOCK_ALL_PATH,
+ href: MOCK_ALL_PATH,
},
];
-
-export const MOCK_SCOPED_SEARCH_OPTIONS_DEF = [
+export const MOCK_SCOPED_SEARCH_OPTIONS = [
{
- html_id: 'scoped-in-project',
+ text: 'scoped-in-project',
scope: MOCK_PROJECT.name,
scopeCategory: PROJECTS_CATEGORY,
icon: ICON_PROJECT,
url: MOCK_PROJECT.path,
},
{
- html_id: 'scoped-in-group',
+ text: 'scoped-in-project-long',
+ scope: MOCK_PROJECT_LONG.name,
+ scopeCategory: PROJECTS_CATEGORY,
+ icon: ICON_PROJECT,
+ url: MOCK_PROJECT_LONG.path,
+ },
+ {
+ text: 'scoped-in-group',
scope: MOCK_GROUP.name,
scopeCategory: GROUPS_CATEGORY,
icon: ICON_GROUP,
url: MOCK_GROUP.path,
},
{
- html_id: 'scoped-in-all',
+ text: 'scoped-in-subgroup',
+ scope: MOCK_SUBGROUP.name,
+ scopeCategory: GROUPS_CATEGORY,
+ icon: ICON_SUBGROUP,
+ url: MOCK_SUBGROUP.path,
+ },
+ {
+ text: 'scoped-in-all',
description: MSG_IN_ALL_GITLAB,
url: MOCK_ALL_PATH,
},
];
+export const MOCK_SCOPED_SEARCH_GROUP = {
+ items: [
+ {
+ text: 'scoped-in-project',
+ scope: MOCK_PROJECT.name,
+ scopeCategory: PROJECTS_CATEGORY,
+ icon: ICON_PROJECT,
+ href: MOCK_PROJECT.path,
+ },
+ {
+ text: 'scoped-in-group',
+ scope: MOCK_GROUP.name,
+ scopeCategory: GROUPS_CATEGORY,
+ icon: ICON_GROUP,
+ href: MOCK_GROUP.path,
+ },
+ {
+ text: 'scoped-in-all',
+ description: MSG_IN_ALL_GITLAB,
+ href: MOCK_ALL_PATH,
+ },
+ ],
+};
+
export const MOCK_AUTOCOMPLETE_OPTIONS_RES = [
{
category: 'Projects',
@@ -168,8 +186,10 @@ export const MOCK_AUTOCOMPLETE_OPTIONS_RES = [
label: 'Gitlab Org / MockProject1',
value: 'MockProject1',
url: 'project/1',
+ avatar_url: '/project/avatar/1/avatar.png',
},
{
+ avatar_url: '/groups/avatar/1/avatar.png',
category: 'Groups',
id: 1,
label: 'Gitlab Org / MockGroup1',
@@ -177,6 +197,7 @@ export const MOCK_AUTOCOMPLETE_OPTIONS_RES = [
url: 'group/1',
},
{
+ avatar_url: '/project/avatar/2/avatar.png',
category: 'Projects',
id: 2,
label: 'Gitlab Org / MockProject2',
@@ -193,31 +214,30 @@ export const MOCK_AUTOCOMPLETE_OPTIONS_RES = [
export const MOCK_AUTOCOMPLETE_OPTIONS = [
{
category: 'Projects',
- html_id: 'autocomplete-Projects-0',
id: 1,
label: 'Gitlab Org / MockProject1',
value: 'MockProject1',
url: 'project/1',
+ avatar_url: '/project/avatar/1/avatar.png',
},
{
category: 'Groups',
- html_id: 'autocomplete-Groups-1',
id: 1,
label: 'Gitlab Org / MockGroup1',
value: 'MockGroup1',
url: 'group/1',
+ avatar_url: '/groups/avatar/1/avatar.png',
},
{
category: 'Projects',
- html_id: 'autocomplete-Projects-2',
id: 2,
label: 'Gitlab Org / MockProject2',
value: 'MockProject2',
url: 'project/2',
+ avatar_url: '/project/avatar/2/avatar.png',
},
{
category: 'Help',
- html_id: 'autocomplete-Help-3',
label: 'GitLab Help',
url: 'help/gitlab',
},
@@ -225,51 +245,64 @@ export const MOCK_AUTOCOMPLETE_OPTIONS = [
export const MOCK_GROUPED_AUTOCOMPLETE_OPTIONS = [
{
- category: 'Groups',
- data: [
+ name: 'Groups',
+ items: [
{
category: 'Groups',
- html_id: 'autocomplete-Groups-1',
-
id: 1,
label: 'Gitlab Org / MockGroup1',
+ namespace: 'Gitlab Org / MockGroup1',
value: 'MockGroup1',
- url: 'group/1',
+ text: 'MockGroup1',
+ href: 'group/1',
+ avatar_url: '/groups/avatar/1/avatar.png',
+ avatar_size: 32,
+ entity_id: 1,
+ entity_name: 'MockGroup1',
},
],
},
{
- category: 'Projects',
- data: [
+ name: 'Projects',
+ items: [
{
category: 'Projects',
- html_id: 'autocomplete-Projects-0',
-
id: 1,
label: 'Gitlab Org / MockProject1',
+ namespace: 'Gitlab Org / MockProject1',
value: 'MockProject1',
- url: 'project/1',
+ text: 'MockProject1',
+ href: 'project/1',
+ avatar_url: '/project/avatar/1/avatar.png',
+ avatar_size: 32,
+ entity_id: 1,
+ entity_name: 'MockProject1',
},
{
category: 'Projects',
- html_id: 'autocomplete-Projects-2',
-
id: 2,
- label: 'Gitlab Org / MockProject2',
value: 'MockProject2',
- url: 'project/2',
+ label: 'Gitlab Org / MockProject2',
+ namespace: 'Gitlab Org / MockProject2',
+ text: 'MockProject2',
+ href: 'project/2',
+ avatar_url: '/project/avatar/2/avatar.png',
+ avatar_size: 32,
+ entity_id: 2,
+ entity_name: 'MockProject2',
},
],
},
{
- category: 'Help',
- data: [
+ name: 'Help',
+ items: [
{
category: 'Help',
- html_id: 'autocomplete-Help-3',
-
label: 'GitLab Help',
- url: 'help/gitlab',
+ text: 'GitLab Help',
+ href: 'help/gitlab',
+ avatar_size: 16,
+ entity_name: 'GitLab Help',
},
],
},
@@ -278,33 +311,50 @@ export const MOCK_GROUPED_AUTOCOMPLETE_OPTIONS = [
export const MOCK_SORTED_AUTOCOMPLETE_OPTIONS = [
{
category: 'Groups',
- html_id: 'autocomplete-Groups-1',
id: 1,
label: 'Gitlab Org / MockGroup1',
value: 'MockGroup1',
- url: 'group/1',
+ text: 'MockGroup1',
+ href: 'group/1',
+ namespace: 'Gitlab Org / MockGroup1',
+ avatar_url: '/groups/avatar/1/avatar.png',
+ avatar_size: 32,
+ entity_id: 1,
+ entity_name: 'MockGroup1',
},
{
+ avatar_size: 32,
+ avatar_url: '/project/avatar/1/avatar.png',
category: 'Projects',
- html_id: 'autocomplete-Projects-0',
+ entity_id: 1,
+ entity_name: 'MockProject1',
+ href: 'project/1',
id: 1,
label: 'Gitlab Org / MockProject1',
+ namespace: 'Gitlab Org / MockProject1',
+ text: 'MockProject1',
value: 'MockProject1',
- url: 'project/1',
},
{
+ avatar_size: 32,
+ avatar_url: '/project/avatar/2/avatar.png',
category: 'Projects',
- html_id: 'autocomplete-Projects-2',
+ entity_id: 2,
+ entity_name: 'MockProject2',
+ href: 'project/2',
id: 2,
label: 'Gitlab Org / MockProject2',
+ namespace: 'Gitlab Org / MockProject2',
+ text: 'MockProject2',
value: 'MockProject2',
- url: 'project/2',
},
{
+ avatar_size: 16,
+ entity_name: 'GitLab Help',
category: 'Help',
- html_id: 'autocomplete-Help-3',
label: 'GitLab Help',
- url: 'help/gitlab',
+ text: 'GitLab Help',
+ href: 'help/gitlab',
},
];
@@ -315,14 +365,16 @@ export const MOCK_GROUPED_AUTOCOMPLETE_OPTIONS_HELP = [
{
html_id: 'autocomplete-Help-1',
category: 'Help',
+ text: 'Rake Tasks Help',
label: 'Rake Tasks Help',
- url: '/help/raketasks/index',
+ href: '/help/raketasks/index',
},
{
html_id: 'autocomplete-Help-2',
category: 'Help',
+ text: 'System Hooks Help',
label: 'System Hooks Help',
- url: '/help/system_hooks/system_hooks',
+ href: '/help/system_hooks/system_hooks',
},
],
},
diff --git a/spec/frontend/super_sidebar/components/global_search/store/actions_spec.js b/spec/frontend/super_sidebar/components/global_search/store/actions_spec.js
index c87b4513309..f6d8e1f26eb 100644
--- a/spec/frontend/super_sidebar/components/global_search/store/actions_spec.js
+++ b/spec/frontend/super_sidebar/components/global_search/store/actions_spec.js
@@ -16,9 +16,7 @@ import {
MOCK_ISSUE_PATH,
} from '../mock_data';
-jest.mock('~/alert');
-
-describe('Header Search Store Actions', () => {
+describe('Global Search Store Actions', () => {
let state;
let mock;
diff --git a/spec/frontend/super_sidebar/components/global_search/store/getters_spec.js b/spec/frontend/super_sidebar/components/global_search/store/getters_spec.js
index dca96da01a7..68583d04b31 100644
--- a/spec/frontend/super_sidebar/components/global_search/store/getters_spec.js
+++ b/spec/frontend/super_sidebar/components/global_search/store/getters_spec.js
@@ -9,7 +9,7 @@ import {
MOCK_SEARCH_CONTEXT,
MOCK_DEFAULT_SEARCH_OPTIONS,
MOCK_SCOPED_SEARCH_OPTIONS,
- MOCK_SCOPED_SEARCH_OPTIONS_DEF,
+ MOCK_SCOPED_SEARCH_GROUP,
MOCK_PROJECT,
MOCK_GROUP,
MOCK_ALL_PATH,
@@ -17,9 +17,10 @@ import {
MOCK_AUTOCOMPLETE_OPTIONS,
MOCK_GROUPED_AUTOCOMPLETE_OPTIONS,
MOCK_SORTED_AUTOCOMPLETE_OPTIONS,
+ MOCK_SCOPED_SEARCH_OPTIONS_DEF,
} from '../mock_data';
-describe('Header Search Store Getters', () => {
+describe('Global Search Store Getters', () => {
let state;
const createState = (initialState) => {
@@ -288,7 +289,7 @@ describe('Header Search Store Getters', () => {
describe.each`
search | defaultSearchOptions | scopedSearchOptions | autocompleteGroupedSearchOptions | expectedArray
- ${null} | ${MOCK_DEFAULT_SEARCH_OPTIONS} | ${MOCK_SCOPED_SEARCH_OPTIONS} | ${MOCK_GROUPED_AUTOCOMPLETE_OPTIONS} | ${MOCK_DEFAULT_SEARCH_OPTIONS}
+ ${null} | ${MOCK_DEFAULT_SEARCH_OPTIONS} | ${MOCK_SCOPED_SEARCH_GROUP} | ${MOCK_GROUPED_AUTOCOMPLETE_OPTIONS} | ${MOCK_DEFAULT_SEARCH_OPTIONS}
${MOCK_SEARCH} | ${MOCK_DEFAULT_SEARCH_OPTIONS} | ${MOCK_SCOPED_SEARCH_OPTIONS} | ${[]} | ${MOCK_SCOPED_SEARCH_OPTIONS}
${MOCK_SEARCH} | ${MOCK_DEFAULT_SEARCH_OPTIONS} | ${[]} | ${MOCK_GROUPED_AUTOCOMPLETE_OPTIONS} | ${MOCK_SORTED_AUTOCOMPLETE_OPTIONS}
${MOCK_SEARCH} | ${MOCK_DEFAULT_SEARCH_OPTIONS} | ${MOCK_SCOPED_SEARCH_OPTIONS} | ${MOCK_GROUPED_AUTOCOMPLETE_OPTIONS} | ${MOCK_SCOPED_SEARCH_OPTIONS.concat(MOCK_SORTED_AUTOCOMPLETE_OPTIONS)}
diff --git a/spec/frontend/super_sidebar/components/global_search/store/mutations_spec.js b/spec/frontend/super_sidebar/components/global_search/store/mutations_spec.js
index d2dc484e825..4d275cf86c7 100644
--- a/spec/frontend/super_sidebar/components/global_search/store/mutations_spec.js
+++ b/spec/frontend/super_sidebar/components/global_search/store/mutations_spec.js
@@ -29,7 +29,7 @@ describe('Header Search Store Mutations', () => {
mutations[types.RECEIVE_AUTOCOMPLETE_SUCCESS](state, MOCK_AUTOCOMPLETE_OPTIONS_RES);
expect(state.loading).toBe(false);
- expect(state.autocompleteOptions).toStrictEqual(MOCK_AUTOCOMPLETE_OPTIONS);
+ expect(state.autocompleteOptions).toEqual(MOCK_AUTOCOMPLETE_OPTIONS);
expect(state.autocompleteError).toBe(false);
});
});
diff --git a/spec/frontend/super_sidebar/components/global_search/utils_spec.js b/spec/frontend/super_sidebar/components/global_search/utils_spec.js
new file mode 100644
index 00000000000..3b12063e733
--- /dev/null
+++ b/spec/frontend/super_sidebar/components/global_search/utils_spec.js
@@ -0,0 +1,60 @@
+import { getFormattedItem } from '~/super_sidebar/components/global_search/utils';
+import {
+ LARGE_AVATAR_PX,
+ SMALL_AVATAR_PX,
+} from '~/super_sidebar/components/global_search/constants';
+import {
+ GROUPS_CATEGORY,
+ PROJECTS_CATEGORY,
+ MERGE_REQUEST_CATEGORY,
+ ISSUES_CATEGORY,
+ RECENT_EPICS_CATEGORY,
+} from '~/vue_shared/global_search/constants';
+
+describe('getFormattedItem', () => {
+ describe.each`
+ item | avatarSize | searchContext | entityId | entityName
+ ${{ category: PROJECTS_CATEGORY, label: 'project1' }} | ${LARGE_AVATAR_PX} | ${{ project: { id: 29 } }} | ${29} | ${'project1'}
+ ${{ category: GROUPS_CATEGORY, label: 'project1' }} | ${LARGE_AVATAR_PX} | ${{ group: { id: 12 } }} | ${12} | ${'project1'}
+ ${{ category: 'Help', label: 'project1' }} | ${SMALL_AVATAR_PX} | ${null} | ${undefined} | ${'project1'}
+ ${{ category: 'Settings', label: 'project1' }} | ${SMALL_AVATAR_PX} | ${null} | ${undefined} | ${'project1'}
+ ${{ category: GROUPS_CATEGORY, value: 'group1', label: 'Group 1' }} | ${LARGE_AVATAR_PX} | ${{ group: { id: 1, name: 'test1' } }} | ${1} | ${'group1'}
+ ${{ category: PROJECTS_CATEGORY, value: 'group2', label: 'Group2' }} | ${LARGE_AVATAR_PX} | ${{ project: { id: 2, name: 'test2' } }} | ${2} | ${'group2'}
+ ${{ category: ISSUES_CATEGORY }} | ${SMALL_AVATAR_PX} | ${{ project: { id: 3, name: 'test3' } }} | ${3} | ${'test3'}
+ ${{ category: MERGE_REQUEST_CATEGORY }} | ${SMALL_AVATAR_PX} | ${{ project: { id: 4, name: 'test4' } }} | ${4} | ${'test4'}
+ ${{ category: RECENT_EPICS_CATEGORY }} | ${SMALL_AVATAR_PX} | ${{ group: { id: 5, name: 'test5' } }} | ${5} | ${'test5'}
+ ${{ category: GROUPS_CATEGORY, group_id: 6, group_name: 'test6' }} | ${LARGE_AVATAR_PX} | ${null} | ${6} | ${'test6'}
+ ${{ category: PROJECTS_CATEGORY, project_id: 7, project_name: 'test7' }} | ${LARGE_AVATAR_PX} | ${null} | ${7} | ${'test7'}
+ ${{ category: ISSUES_CATEGORY, project_id: 8, project_name: 'test8' }} | ${SMALL_AVATAR_PX} | ${null} | ${8} | ${'test8'}
+ ${{ category: MERGE_REQUEST_CATEGORY, project_id: 9, project_name: 'test9' }} | ${SMALL_AVATAR_PX} | ${null} | ${9} | ${'test9'}
+ ${{ category: RECENT_EPICS_CATEGORY, group_id: 10, group_name: 'test10' }} | ${SMALL_AVATAR_PX} | ${null} | ${10} | ${'test10'}
+ ${{ category: GROUPS_CATEGORY, group_id: 11, group_name: 'test11' }} | ${LARGE_AVATAR_PX} | ${{ group: { id: 1, name: 'test1' } }} | ${11} | ${'test11'}
+ ${{ category: PROJECTS_CATEGORY, project_id: 12, project_name: 'test12' }} | ${LARGE_AVATAR_PX} | ${{ project: { id: 2, name: 'test2' } }} | ${12} | ${'test12'}
+ ${{ category: ISSUES_CATEGORY, project_id: 13, project_name: 'test13' }} | ${SMALL_AVATAR_PX} | ${{ project: { id: 3, name: 'test3' } }} | ${13} | ${'test13'}
+ ${{ category: MERGE_REQUEST_CATEGORY, project_id: 14, project_name: 'test14' }} | ${SMALL_AVATAR_PX} | ${{ project: { id: 4, name: 'test4' } }} | ${14} | ${'test14'}
+ ${{ category: RECENT_EPICS_CATEGORY, group_id: 15, group_name: 'test15' }} | ${SMALL_AVATAR_PX} | ${{ group: { id: 5, name: 'test5' } }} | ${15} | ${'test15'}
+ `('formats the item', ({ item, avatarSize, searchContext, entityId, entityName }) => {
+ describe(`when item is ${JSON.stringify(item)}`, () => {
+ let formattedItem;
+ beforeEach(() => {
+ formattedItem = getFormattedItem(item, searchContext);
+ });
+
+ it(`should set text to ${item.value || item.label}`, () => {
+ expect(formattedItem.text).toBe(item.value || item.label);
+ });
+
+ it(`should set avatarSize to ${avatarSize}`, () => {
+ expect(formattedItem.avatar_size).toBe(avatarSize);
+ });
+
+ it(`should set avatar entityId to ${entityId}`, () => {
+ expect(formattedItem.entity_id).toBe(entityId);
+ });
+
+ it(`should set avatar entityName to ${entityName}`, () => {
+ expect(formattedItem.entity_name).toBe(entityName);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/super_sidebar/components/groups_list_spec.js b/spec/frontend/super_sidebar/components/groups_list_spec.js
index 6aee895f611..4fa3303c12f 100644
--- a/spec/frontend/super_sidebar/components/groups_list_spec.js
+++ b/spec/frontend/super_sidebar/components/groups_list_spec.js
@@ -19,11 +19,14 @@ describe('GroupsList component', () => {
const itRendersViewAllItem = () => {
it('renders the "View all..." item', () => {
- expect(findViewAllLink().props('item')).toEqual({
+ const link = findViewAllLink();
+
+ expect(link.props('item')).toEqual({
icon: 'group',
link: viewAllLink,
- title: s__('Navigation|View all groups'),
+ title: s__('Navigation|View all your groups'),
});
+ expect(link.props('linkClasses')).toEqual({ 'dashboard-shortcuts-groups': true });
});
};
@@ -75,7 +78,7 @@ describe('GroupsList component', () => {
it('passes the correct props to the frequent items list', () => {
expect(findFrequentItemsList().props()).toEqual({
- title: s__('Navigation|Frequent groups'),
+ title: s__('Navigation|Frequently visited groups'),
storageKey,
maxItems: MAX_FREQUENT_GROUPS_COUNT,
pristineText: s__('Navigation|Groups you visit often will appear here.'),
diff --git a/spec/frontend/super_sidebar/components/help_center_spec.js b/spec/frontend/super_sidebar/components/help_center_spec.js
index 1d072c0ba3c..4c0e7a89a43 100644
--- a/spec/frontend/super_sidebar/components/help_center_spec.js
+++ b/spec/frontend/super_sidebar/components/help_center_spec.js
@@ -1,4 +1,4 @@
-import { GlDisclosureDropdownGroup } from '@gitlab/ui';
+import { GlDisclosureDropdown, GlDisclosureDropdownGroup } from '@gitlab/ui';
import { within } from '@testing-library/dom';
import toggleWhatsNewDrawer from '~/whats_new';
import { mountExtended } from 'helpers/vue_test_utils_helper';
@@ -7,15 +7,18 @@ 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 { mockTracking } from 'helpers/tracking_helper';
import { sidebarData } from '../mock_data';
jest.mock('~/whats_new');
describe('HelpCenter component', () => {
let wrapper;
+ let trackingSpy;
const GlEmoji = { template: '<img/>' };
+ const findDropdown = () => wrapper.findComponent(GlDisclosureDropdown);
const findDropdownGroup = (i = 0) => {
return wrapper.findAllComponents(GlDisclosureDropdownGroup).at(i);
};
@@ -28,6 +31,15 @@ describe('HelpCenter component', () => {
propsData: { sidebarData },
stubs: { GlEmoji },
});
+ trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
+ };
+
+ const trackingAttrs = (label) => {
+ return {
+ 'data-track-action': 'click_link',
+ 'data-track-property': 'nav_help_menu',
+ 'data-track-label': label,
+ };
};
describe('default', () => {
@@ -37,16 +49,37 @@ describe('HelpCenter component', () => {
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.help, href: helpPagePath(), extraAttrs: trackingAttrs('help') },
+ {
+ text: HelpCenter.i18n.support,
+ href: sidebarData.support_path,
+ extraAttrs: trackingAttrs('support'),
+ },
+ {
+ text: HelpCenter.i18n.docs,
+ href: 'https://docs.gitlab.com',
+ extraAttrs: trackingAttrs('gitlab_documentation'),
+ },
+ {
+ text: HelpCenter.i18n.plans,
+ href: `${PROMO_URL}/pricing`,
+ extraAttrs: trackingAttrs('compare_gitlab_plans'),
+ },
+ {
+ text: HelpCenter.i18n.forum,
+ href: 'https://forum.gitlab.com/',
+ extraAttrs: trackingAttrs('community_forum'),
+ },
{
text: HelpCenter.i18n.contribute,
href: helpPagePath('', { anchor: 'contributing-to-gitlab' }),
+ extraAttrs: trackingAttrs('contribute_to_gitlab'),
+ },
+ {
+ text: HelpCenter.i18n.feedback,
+ href: 'https://about.gitlab.com/submit-feedback',
+ extraAttrs: trackingAttrs('submit_feedback'),
},
- { text: HelpCenter.i18n.feedback, href: 'https://about.gitlab.com/submit-feedback' },
]);
expect(findDropdownGroup(1).props('group').items).toEqual([
@@ -55,6 +88,12 @@ describe('HelpCenter component', () => {
]);
});
+ it('passes popper options to the dropdown', () => {
+ expect(findDropdown().props('popperOptions')).toEqual({
+ modifiers: [{ name: 'offset', options: { offset: [-4, 4] } }],
+ });
+ });
+
describe('with Gitlab version check feature enabled', () => {
beforeEach(() => {
createWrapper({ ...sidebarData, show_version_check: true });
@@ -62,7 +101,12 @@ describe('HelpCenter component', () => {
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' },
+ {
+ text: HelpCenter.i18n.version,
+ href: helpPagePath('update/index'),
+ version: '16.0',
+ extraAttrs: trackingAttrs('version_help_dropdown'),
+ },
]);
});
});
@@ -86,11 +130,24 @@ describe('HelpCenter component', () => {
// ~/behaviors/shortcuts/shortcuts.js.
expect(button.classList.contains('js-shortcuts-modal-trigger')).toBe(true);
});
+
+ it('should have Snowplow tracking attributes', () => {
+ expect(findButton('Keyboard shortcuts ?').dataset).toEqual(
+ expect.objectContaining({
+ trackAction: 'click_button',
+ trackLabel: 'keyboard_shortcuts_help',
+ trackProperty: 'nav_help_menu',
+ }),
+ );
+ });
});
describe('showWhatsNew', () => {
beforeEach(() => {
jest.spyOn(wrapper.vm.$refs.dropdown, 'close');
+ beforeEach(() => {
+ createWrapper({ ...sidebarData, show_version_check: true });
+ });
findButton("What's new 5").click();
});
@@ -107,6 +164,18 @@ describe('HelpCenter component', () => {
expect(toggleWhatsNewDrawer).toHaveBeenCalledTimes(2);
expect(toggleWhatsNewDrawer).toHaveBeenLastCalledWith();
});
+
+ it('should have Snowplow tracking attributes', () => {
+ createWrapper({ ...sidebarData, display_whats_new: true });
+
+ expect(findButton("What's new 5").dataset).toEqual(
+ expect.objectContaining({
+ trackAction: 'click_button',
+ trackLabel: 'whats_new',
+ trackProperty: 'nav_help_menu',
+ }),
+ );
+ });
});
describe('shouldShowWhatsNewNotification', () => {
@@ -153,5 +222,23 @@ describe('HelpCenter component', () => {
});
});
});
+
+ describe('toggle dropdown', () => {
+ it('should track Snowplow event when dropdown is shown', () => {
+ findDropdown().vm.$emit('shown');
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_toggle', {
+ label: 'show_help_dropdown',
+ property: 'nav_help_menu',
+ });
+ });
+
+ it('should track Snowplow event when dropdown is hidden', () => {
+ findDropdown().vm.$emit('hidden');
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_toggle', {
+ label: 'hide_help_dropdown',
+ property: 'nav_help_menu',
+ });
+ });
+ });
});
});
diff --git a/spec/frontend/super_sidebar/components/items_list_spec.js b/spec/frontend/super_sidebar/components/items_list_spec.js
index 8e00984f500..d49ef35e9d8 100644
--- a/spec/frontend/super_sidebar/components/items_list_spec.js
+++ b/spec/frontend/super_sidebar/components/items_list_spec.js
@@ -1,4 +1,5 @@
-import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { GlIcon } from '@gitlab/ui';
+import { shallowMountExtended, mountExtended } from 'helpers/vue_test_utils_helper';
import ItemsList from '~/super_sidebar/components/items_list.vue';
import NavItem from '~/super_sidebar/components/nav_item.vue';
import { cachedFrequentProjects } from '../mock_data';
@@ -11,8 +12,8 @@ describe('ItemsList component', () => {
const findNavItems = () => wrapper.findAllComponents(NavItem);
- const createWrapper = ({ props = {}, slots = {} } = {}) => {
- wrapper = shallowMountExtended(ItemsList, {
+ const createWrapper = ({ props = {}, slots = {}, mountFn = shallowMountExtended } = {}) => {
+ wrapper = mountFn(ItemsList, {
propsData: {
...props,
},
@@ -60,4 +61,42 @@ describe('ItemsList component', () => {
expect(wrapper.findByTestId(testId).exists()).toBe(true);
});
+
+ describe('item removal', () => {
+ const findRemoveButton = () => wrapper.findByTestId('item-remove');
+ const mockProject = {
+ ...firstMockedProject,
+ title: firstMockedProject.name,
+ };
+
+ beforeEach(() => {
+ createWrapper({
+ props: {
+ items: [mockProject],
+ editable: true,
+ },
+ mountFn: mountExtended,
+ });
+ });
+
+ it('renders the remove button', () => {
+ const itemRemoveButton = findRemoveButton();
+
+ expect(itemRemoveButton.exists()).toBe(true);
+ expect(itemRemoveButton.attributes('title')).toBe('Remove');
+ expect(itemRemoveButton.findComponent(GlIcon).props('name')).toBe('close');
+ });
+
+ it('emits `remove-item` event with item param when remove button is clicked', () => {
+ const itemRemoveButton = findRemoveButton();
+
+ itemRemoveButton.vm.$emit(
+ 'click',
+ { stopPropagation: jest.fn(), preventDefault: jest.fn() },
+ mockProject,
+ );
+
+ expect(wrapper.emitted('remove-item')).toEqual([[mockProject]]);
+ });
+ });
});
diff --git a/spec/frontend/super_sidebar/components/merge_request_menu_spec.js b/spec/frontend/super_sidebar/components/merge_request_menu_spec.js
index fe87c4be9c3..9c8fd0556f1 100644
--- a/spec/frontend/super_sidebar/components/merge_request_menu_spec.js
+++ b/spec/frontend/super_sidebar/components/merge_request_menu_spec.js
@@ -8,7 +8,7 @@ describe('MergeRequestMenu component', () => {
const findGlBadge = (at) => wrapper.findAllComponents(GlBadge).at(at);
const findGlDisclosureDropdown = () => wrapper.findComponent(GlDisclosureDropdown);
- const findLink = () => wrapper.findByRole('link');
+ const findLink = (name) => wrapper.findByRole('link', { name });
const createWrapper = () => {
wrapper = mountExtended(MergeRequestMenu, {
@@ -27,11 +27,18 @@ describe('MergeRequestMenu component', () => {
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.each(mergeRequestMenuGroup[0].items)('renders item text and count in link', (item) => {
+ const index = mergeRequestMenuGroup[0].items.indexOf(item);
+ const { text, href, count, extraAttrs } = mergeRequestMenuGroup[0].items[index];
+ const link = findLink(new RegExp(text));
+
+ expect(link.text()).toContain(text);
+ expect(link.text()).toContain(String(count));
+ expect(link.attributes('href')).toBe(href);
+ expect(link.attributes('data-track-action')).toBe(extraAttrs['data-track-action']);
+ expect(link.attributes('data-track-label')).toBe(extraAttrs['data-track-label']);
+ expect(link.attributes('data-track-property')).toBe(extraAttrs['data-track-property']);
+ expect(link.attributes('class')).toContain(extraAttrs.class);
});
it('renders item count string in badge', () => {
diff --git a/spec/frontend/super_sidebar/components/nav_item_spec.js b/spec/frontend/super_sidebar/components/nav_item_spec.js
index 22989c1a5f9..d96d2b77d21 100644
--- a/spec/frontend/super_sidebar/components/nav_item_spec.js
+++ b/spec/frontend/super_sidebar/components/nav_item_spec.js
@@ -1,18 +1,24 @@
import { GlBadge } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import NavItem from '~/super_sidebar/components/nav_item.vue';
+import {
+ CLICK_MENU_ITEM_ACTION,
+ TRACKING_UNKNOWN_ID,
+ TRACKING_UNKNOWN_PANEL,
+} from '~/super_sidebar/constants';
describe('NavItem component', () => {
let wrapper;
const findLink = () => wrapper.findByTestId('nav-item-link');
const findPill = () => wrapper.findComponent(GlBadge);
- const createWrapper = (item, props = {}) => {
+ const createWrapper = (item, props = {}, provide = {}) => {
wrapper = shallowMountExtended(NavItem, {
propsData: {
item,
...props,
},
+ provide,
});
};
@@ -46,4 +52,34 @@ describe('NavItem component', () => {
expect(findLink().attributes('class')).toContain(customClass);
});
+
+ describe('Data Tracking Attributes', () => {
+ it('adds no labels on sections', () => {
+ const id = 'my-id';
+ createWrapper({ title: 'Foo', id, items: [{ title: 'Baz' }] });
+
+ expect(findLink().attributes('data-track-action')).toBeUndefined();
+ expect(findLink().attributes('data-track-label')).toBeUndefined();
+ expect(findLink().attributes('data-track-property')).toBeUndefined();
+ expect(findLink().attributes('data-track-extra')).toBeUndefined();
+ });
+
+ it.each`
+ id | panelType | eventLabel | eventProperty | eventExtra
+ ${'abc'} | ${'xyz'} | ${'abc'} | ${'nav_panel_xyz'} | ${undefined}
+ ${undefined} | ${'xyz'} | ${TRACKING_UNKNOWN_ID} | ${'nav_panel_xyz'} | ${'{"title":"Foo"}'}
+ ${'abc'} | ${undefined} | ${'abc'} | ${TRACKING_UNKNOWN_PANEL} | ${'{"title":"Foo"}'}
+ ${undefined} | ${undefined} | ${TRACKING_UNKNOWN_ID} | ${TRACKING_UNKNOWN_PANEL} | ${'{"title":"Foo"}'}
+ `(
+ 'adds appropriate data tracking labels for id=$id and panelType=$panelType',
+ ({ id, eventLabel, panelType, eventProperty, eventExtra }) => {
+ createWrapper({ title: 'Foo', id }, {}, { panelType });
+
+ expect(findLink().attributes('data-track-action')).toBe(CLICK_MENU_ITEM_ACTION);
+ expect(findLink().attributes('data-track-label')).toBe(eventLabel);
+ expect(findLink().attributes('data-track-property')).toBe(eventProperty);
+ expect(findLink().attributes('data-track-extra')).toBe(eventExtra);
+ },
+ );
+ });
});
diff --git a/spec/frontend/super_sidebar/components/pinned_section_spec.js b/spec/frontend/super_sidebar/components/pinned_section_spec.js
new file mode 100644
index 00000000000..7ead6a40895
--- /dev/null
+++ b/spec/frontend/super_sidebar/components/pinned_section_spec.js
@@ -0,0 +1,75 @@
+import { nextTick } from 'vue';
+import Cookies from '~/lib/utils/cookies';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import PinnedSection from '~/super_sidebar/components/pinned_section.vue';
+import NavItem from '~/super_sidebar/components/nav_item.vue';
+import { SIDEBAR_PINS_EXPANDED_COOKIE, SIDEBAR_COOKIE_EXPIRATION } from '~/super_sidebar/constants';
+import { setCookie } from '~/lib/utils/common_utils';
+
+jest.mock('~/lib/utils/common_utils', () => ({
+ getCookie: jest.requireActual('~/lib/utils/common_utils').getCookie,
+ setCookie: jest.fn(),
+}));
+
+describe('PinnedSection component', () => {
+ let wrapper;
+
+ const findToggle = () => wrapper.find('a');
+
+ const createWrapper = () => {
+ wrapper = mountExtended(PinnedSection, {
+ propsData: {
+ items: [{ title: 'Pin 1', href: '/page1' }],
+ },
+ });
+ };
+
+ describe('expanded', () => {
+ describe('when cookie is not set', () => {
+ it('is expanded by default', () => {
+ createWrapper();
+ expect(wrapper.findComponent(NavItem).isVisible()).toBe(true);
+ });
+ });
+
+ describe('when cookie is set to false', () => {
+ beforeEach(() => {
+ Cookies.set(SIDEBAR_PINS_EXPANDED_COOKIE, 'false');
+ createWrapper();
+ });
+
+ it('is collapsed', () => {
+ expect(wrapper.findComponent(NavItem).isVisible()).toBe(false);
+ });
+
+ it('updates the cookie when expanding the section', async () => {
+ findToggle().trigger('click');
+ await nextTick();
+
+ expect(setCookie).toHaveBeenCalledWith(SIDEBAR_PINS_EXPANDED_COOKIE, true, {
+ expires: SIDEBAR_COOKIE_EXPIRATION,
+ });
+ });
+ });
+
+ describe('when cookie is set to true', () => {
+ beforeEach(() => {
+ Cookies.set(SIDEBAR_PINS_EXPANDED_COOKIE, 'true');
+ createWrapper();
+ });
+
+ it('is expanded', () => {
+ expect(wrapper.findComponent(NavItem).isVisible()).toBe(true);
+ });
+
+ it('updates the cookie when collapsing the section', async () => {
+ findToggle().trigger('click');
+ await nextTick();
+
+ expect(setCookie).toHaveBeenCalledWith(SIDEBAR_PINS_EXPANDED_COOKIE, false, {
+ expires: SIDEBAR_COOKIE_EXPIRATION,
+ });
+ });
+ });
+ });
+});
diff --git a/spec/frontend/super_sidebar/components/projects_list_spec.js b/spec/frontend/super_sidebar/components/projects_list_spec.js
index cdc003b14e0..93a414e1e8c 100644
--- a/spec/frontend/super_sidebar/components/projects_list_spec.js
+++ b/spec/frontend/super_sidebar/components/projects_list_spec.js
@@ -19,11 +19,14 @@ describe('ProjectsList component', () => {
const itRendersViewAllItem = () => {
it('renders the "View all..." item', () => {
- expect(findViewAllLink().props('item')).toEqual({
+ const link = findViewAllLink();
+
+ expect(link.props('item')).toEqual({
icon: 'project',
link: viewAllLink,
- title: s__('Navigation|View all projects'),
+ title: s__('Navigation|View all your projects'),
});
+ expect(link.props('linkClasses')).toEqual({ 'dashboard-shortcuts-projects': true });
});
};
@@ -70,7 +73,7 @@ describe('ProjectsList component', () => {
it('passes the correct props to the frequent items list', () => {
expect(findFrequentItemsList().props()).toEqual({
- title: s__('Navigation|Frequent projects'),
+ title: s__('Navigation|Frequently visited projects'),
storageKey,
maxItems: MAX_FREQUENT_PROJECTS_COUNT,
pristineText: s__('Navigation|Projects you visit often will appear here.'),
diff --git a/spec/frontend/super_sidebar/components/search_results_spec.js b/spec/frontend/super_sidebar/components/search_results_spec.js
index dd48935c138..daec5c2a9b4 100644
--- a/spec/frontend/super_sidebar/components/search_results_spec.js
+++ b/spec/frontend/super_sidebar/components/search_results_spec.js
@@ -1,7 +1,9 @@
+import { GlCollapse } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { s__ } from '~/locale';
import SearchResults from '~/super_sidebar/components/search_results.vue';
import ItemsList from '~/super_sidebar/components/items_list.vue';
+import { stubComponent } from 'helpers/stub_component';
const title = s__('Navigation|PROJECTS');
const noResultsText = s__('Navigation|No project matches found');
@@ -9,7 +11,8 @@ const noResultsText = s__('Navigation|No project matches found');
describe('SearchResults component', () => {
let wrapper;
- const findListTitle = () => wrapper.findByTestId('list-title');
+ const findSearchResultsToggle = () => wrapper.findByTestId('search-results-toggle');
+ const findCollapsibleSection = () => wrapper.findComponent(GlCollapse);
const findItemsList = () => wrapper.findComponent(ItemsList);
const findEmptyText = () => wrapper.findByTestId('empty-text');
@@ -20,6 +23,11 @@ describe('SearchResults component', () => {
noResultsText,
...props,
},
+ stubs: {
+ GlCollapse: stubComponent(GlCollapse, {
+ props: ['visible'],
+ }),
+ },
});
};
@@ -29,7 +37,11 @@ describe('SearchResults component', () => {
});
it("renders the list's title", () => {
- expect(findListTitle().text()).toBe(title);
+ expect(findSearchResultsToggle().text()).toBe(title);
+ });
+
+ it('is expanded', () => {
+ expect(findCollapsibleSection().props('visible')).toBe(true);
});
it('renders the empty text', () => {
diff --git a/spec/frontend/super_sidebar/components/sidebar_menu_spec.js b/spec/frontend/super_sidebar/components/sidebar_menu_spec.js
new file mode 100644
index 00000000000..26b146f0c8b
--- /dev/null
+++ b/spec/frontend/super_sidebar/components/sidebar_menu_spec.js
@@ -0,0 +1,151 @@
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import SidebarMenu from '~/super_sidebar/components/sidebar_menu.vue';
+import { PANELS_WITH_PINS } from '~/super_sidebar/constants';
+import { sidebarData } from '../mock_data';
+
+describe('SidebarMenu component', () => {
+ let wrapper;
+
+ const createWrapper = (mockData) => {
+ wrapper = mountExtended(SidebarMenu, {
+ propsData: {
+ items: mockData.current_menu_items,
+ pinnedItemIds: mockData.pinned_items,
+ panelType: mockData.panel_type,
+ updatePinsUrl: mockData.update_pins_url,
+ },
+ });
+ };
+
+ describe('computed', () => {
+ const menuItems = [
+ { id: 1, title: 'No subitems' },
+ { id: 2, title: 'With subitems', items: [{ id: 21, title: 'Pinned subitem' }] },
+ { id: 3, title: 'Empty subitems array', items: [] },
+ { id: 4, title: 'Also with subitems', items: [{ id: 41, title: 'Subitem' }] },
+ ];
+
+ describe('supportsPins', () => {
+ it('is true for the project sidebar', () => {
+ createWrapper({ ...sidebarData, panel_type: 'project' });
+ expect(wrapper.vm.supportsPins).toBe(true);
+ });
+
+ it('is true for the group sidebar', () => {
+ createWrapper({ ...sidebarData, panel_type: 'group' });
+ expect(wrapper.vm.supportsPins).toBe(true);
+ });
+
+ it('is false for any other sidebar', () => {
+ createWrapper({ ...sidebarData, panel_type: 'your_work' });
+ expect(wrapper.vm.supportsPins).toEqual(false);
+ });
+ });
+
+ describe('flatPinnableItems', () => {
+ it('returns all subitems in a flat array', () => {
+ createWrapper({ ...sidebarData, current_menu_items: menuItems });
+ expect(wrapper.vm.flatPinnableItems).toEqual([
+ { id: 21, title: 'Pinned subitem' },
+ { id: 41, title: 'Subitem' },
+ ]);
+ });
+ });
+
+ describe('staticItems', () => {
+ describe('when the sidebar supports pins', () => {
+ beforeEach(() => {
+ createWrapper({
+ ...sidebarData,
+ current_menu_items: menuItems,
+ panel_type: PANELS_WITH_PINS[0],
+ });
+ });
+
+ it('makes everything that has no subitems a static item', () => {
+ expect(wrapper.vm.staticItems.map((i) => i.title)).toEqual([
+ 'No subitems',
+ 'Empty subitems array',
+ ]);
+ });
+ });
+
+ describe('when the sidebar does not support pins', () => {
+ beforeEach(() => {
+ createWrapper({
+ ...sidebarData,
+ current_menu_items: menuItems,
+ panel_type: 'explore',
+ });
+ });
+
+ it('returns an empty array', () => {
+ expect(wrapper.vm.staticItems.map((i) => i.title)).toEqual([]);
+ });
+ });
+ });
+
+ describe('nonStaticItems', () => {
+ describe('when the sidebar supports pins', () => {
+ beforeEach(() => {
+ createWrapper({
+ ...sidebarData,
+ current_menu_items: menuItems,
+ panel_type: PANELS_WITH_PINS[0],
+ });
+ });
+
+ it('keeps items that have subitems (aka "sections") as non-static', () => {
+ expect(wrapper.vm.nonStaticItems.map((i) => i.title)).toEqual([
+ 'With subitems',
+ 'Also with subitems',
+ ]);
+ });
+ });
+
+ describe('when the sidebar does not support pins', () => {
+ beforeEach(() => {
+ createWrapper({
+ ...sidebarData,
+ current_menu_items: menuItems,
+ panel_type: 'explore',
+ });
+ });
+
+ it('keeps all items as non-static', () => {
+ expect(wrapper.vm.nonStaticItems).toEqual(menuItems);
+ });
+ });
+ });
+
+ describe('pinnedItems', () => {
+ describe('when user has no pinned item ids stored', () => {
+ beforeEach(() => {
+ createWrapper({
+ ...sidebarData,
+ current_menu_items: menuItems,
+ pinned_items: [],
+ });
+ });
+
+ it('returns an empty array', () => {
+ expect(wrapper.vm.pinnedItems).toEqual([]);
+ });
+ });
+
+ describe('when user has some pinned item ids stored', () => {
+ beforeEach(() => {
+ createWrapper({
+ ...sidebarData,
+ current_menu_items: menuItems,
+ pinned_items: [21],
+ });
+ });
+
+ it('returns the items matching the pinned ids', () => {
+ expect(wrapper.vm.pinnedItems).toEqual([{ id: 21, title: 'Pinned subitem' }]);
+ });
+ });
+ });
+ });
+});
diff --git a/spec/frontend/super_sidebar/components/super_sidebar_spec.js b/spec/frontend/super_sidebar/components/super_sidebar_spec.js
index 32921da23aa..85f2a63943d 100644
--- a/spec/frontend/super_sidebar/components/super_sidebar_spec.js
+++ b/spec/frontend/super_sidebar/components/super_sidebar_spec.js
@@ -1,44 +1,57 @@
+import { nextTick } from 'vue';
+import { GlCollapse } from '@gitlab/ui';
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 SidebarPortalTarget from '~/super_sidebar/components/sidebar_portal_target.vue';
-import { isCollapsed } from '~/super_sidebar/super_sidebar_collapsed_state_manager';
+import ContextSwitcher from '~/super_sidebar/components/context_switcher.vue';
+import {
+ SUPER_SIDEBAR_PEEK_OPEN_DELAY,
+ SUPER_SIDEBAR_PEEK_CLOSE_DELAY,
+} from '~/super_sidebar/constants';
+import { stubComponent } from 'helpers/stub_component';
import { sidebarData } from '../mock_data';
-jest.mock('~/super_sidebar/super_sidebar_collapsed_state_manager', () => ({
- isCollapsed: jest.fn(),
-}));
+const focusInputMock = jest.fn();
describe('SuperSidebar component', () => {
let wrapper;
- const findSidebar = () => wrapper.find('.super-sidebar');
+ const findSidebar = () => wrapper.findByTestId('super-sidebar');
+ const findHoverArea = () => wrapper.findByTestId('super-sidebar-hover-area');
const findUserBar = () => wrapper.findComponent(UserBar);
const findHelpCenter = () => wrapper.findComponent(HelpCenter);
const findSidebarPortalTarget = () => wrapper.findComponent(SidebarPortalTarget);
- const createWrapper = (props = {}) => {
+ const createWrapper = ({ props = {}, provide = {}, sidebarState = {} } = {}) => {
wrapper = shallowMountExtended(SuperSidebar, {
+ data() {
+ return {
+ ...sidebarState,
+ };
+ },
propsData: {
sidebarData,
...props,
},
+ stubs: {
+ ContextSwitcher: stubComponent(ContextSwitcher, {
+ methods: { focusInput: focusInputMock },
+ }),
+ },
+ provide,
});
};
describe('default', () => {
- it('add aria-hidden and inert attributes when collapsed', () => {
- isCollapsed.mockReturnValue(true);
- createWrapper();
- expect(findSidebar().attributes('aria-hidden')).toBe('true');
+ it('adds inert attribute when collapsed', () => {
+ createWrapper({ sidebarState: { isCollapsed: true } });
expect(findSidebar().attributes('inert')).toBe('inert');
});
- it('does not add aria-hidden and inert attributes when expanded', () => {
- isCollapsed.mockReturnValue(false);
+ it('does not add inert attribute when expanded', () => {
createWrapper();
- expect(findSidebar().attributes('aria-hidden')).toBe('false');
expect(findSidebar().attributes('inert')).toBe(undefined);
});
@@ -56,5 +69,120 @@ describe('SuperSidebar component', () => {
createWrapper();
expect(findSidebarPortalTarget().exists()).toBe(true);
});
+
+ it("does not call the context switcher's focusInput method initially", () => {
+ createWrapper();
+
+ expect(focusInputMock).not.toHaveBeenCalled();
+ });
+
+ it('renders hidden shortcut links', () => {
+ createWrapper();
+ const [linkAttrs] = sidebarData.shortcut_links;
+ const link = wrapper.find(`.${linkAttrs.css_class}`);
+
+ expect(link.exists()).toBe(true);
+ expect(link.attributes('href')).toBe(linkAttrs.href);
+ expect(link.attributes('class')).toContain('gl-display-none');
+ });
+ });
+
+ describe('when peeking on hover', () => {
+ const peekClass = 'super-sidebar-peek';
+
+ it('updates inert attribute and peek class', async () => {
+ createWrapper({
+ provide: { glFeatures: { superSidebarPeek: true } },
+ sidebarState: { isCollapsed: true },
+ });
+
+ findHoverArea().trigger('mouseenter');
+
+ jest.advanceTimersByTime(SUPER_SIDEBAR_PEEK_OPEN_DELAY - 1);
+ await nextTick();
+
+ // Not quite enough time has elapsed yet for sidebar to open
+ expect(findSidebar().classes()).not.toContain(peekClass);
+ expect(findSidebar().attributes('inert')).toBe('inert');
+
+ jest.advanceTimersByTime(1);
+ await nextTick();
+
+ // Exactly enough time has elapsed to open
+ expect(findSidebar().classes()).toContain(peekClass);
+ expect(findSidebar().attributes('inert')).toBe(undefined);
+
+ // Important: assume the cursor enters the sidebar
+ findSidebar().trigger('mouseenter');
+
+ jest.runAllTimers();
+ await nextTick();
+
+ // Sidebar remains peeked open indefinitely without a mouseleave
+ expect(findSidebar().classes()).toContain(peekClass);
+ expect(findSidebar().attributes('inert')).toBe(undefined);
+
+ findSidebar().trigger('mouseleave');
+
+ jest.advanceTimersByTime(SUPER_SIDEBAR_PEEK_CLOSE_DELAY - 1);
+ await nextTick();
+
+ // Not quite enough time has elapsed yet for sidebar to hide
+ expect(findSidebar().classes()).toContain(peekClass);
+ expect(findSidebar().attributes('inert')).toBe(undefined);
+
+ jest.advanceTimersByTime(1);
+ await nextTick();
+
+ // Exactly enough time has elapsed for sidebar to hide
+ expect(findSidebar().classes()).not.toContain('super-sidebar-peek');
+ expect(findSidebar().attributes('inert')).toBe('inert');
+ });
+
+ it('eventually closes the sidebar if cursor never enters sidebar', async () => {
+ createWrapper({
+ provide: { glFeatures: { superSidebarPeek: true } },
+ sidebarState: { isCollapsed: true },
+ });
+
+ findHoverArea().trigger('mouseenter');
+
+ jest.advanceTimersByTime(SUPER_SIDEBAR_PEEK_OPEN_DELAY);
+ await nextTick();
+
+ // Sidebar is now open
+ expect(findSidebar().classes()).toContain(peekClass);
+ expect(findSidebar().attributes('inert')).toBe(undefined);
+
+ // Important: do *not* fire a mouseenter event on the sidebar here. This
+ // imitates what happens if the cursor moves away from the sidebar before
+ // it actually appears.
+
+ jest.advanceTimersByTime(SUPER_SIDEBAR_PEEK_CLOSE_DELAY - 1);
+ await nextTick();
+
+ // Not quite enough time has elapsed yet for sidebar to hide
+ expect(findSidebar().classes()).toContain(peekClass);
+ expect(findSidebar().attributes('inert')).toBe(undefined);
+
+ jest.advanceTimersByTime(1);
+ await nextTick();
+
+ // Exactly enough time has elapsed for sidebar to hide
+ expect(findSidebar().classes()).not.toContain('super-sidebar-peek');
+ expect(findSidebar().attributes('inert')).toBe('inert');
+ });
+ });
+
+ describe('when opening the context switcher', () => {
+ beforeEach(() => {
+ createWrapper();
+ wrapper.findComponent(GlCollapse).vm.$emit('input', true);
+ wrapper.findComponent(GlCollapse).vm.$emit('shown');
+ });
+
+ it("calls the context switcher's focusInput method", () => {
+ expect(focusInputMock).toHaveBeenCalledTimes(1);
+ });
});
});
diff --git a/spec/frontend/super_sidebar/components/super_sidebar_toggle_spec.js b/spec/frontend/super_sidebar/components/super_sidebar_toggle_spec.js
new file mode 100644
index 00000000000..b9f94e662fe
--- /dev/null
+++ b/spec/frontend/super_sidebar/components/super_sidebar_toggle_spec.js
@@ -0,0 +1,106 @@
+import { nextTick } from 'vue';
+import { GlButton } from '@gitlab/ui';
+import { __ } from '~/locale';
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
+import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { JS_TOGGLE_COLLAPSE_CLASS, JS_TOGGLE_EXPAND_CLASS } from '~/super_sidebar/constants';
+import SuperSidebarToggle from '~/super_sidebar/components/super_sidebar_toggle.vue';
+import { toggleSuperSidebarCollapsed } from '~/super_sidebar/super_sidebar_collapsed_state_manager';
+
+jest.mock('~/super_sidebar/super_sidebar_collapsed_state_manager.js', () => ({
+ toggleSuperSidebarCollapsed: jest.fn(),
+}));
+
+describe('SuperSidebarToggle component', () => {
+ let wrapper;
+
+ const findButton = () => wrapper.findComponent(GlButton);
+ const getTooltip = () => getBinding(wrapper.element, 'gl-tooltip').value;
+
+ const createWrapper = ({ props = {}, sidebarState = {} } = {}) => {
+ wrapper = shallowMountExtended(SuperSidebarToggle, {
+ data() {
+ return {
+ ...sidebarState,
+ };
+ },
+ directives: {
+ GlTooltip: createMockDirective('gl-tooltip'),
+ },
+ propsData: {
+ ...props,
+ },
+ });
+ };
+
+ describe('attributes', () => {
+ it('has aria-controls attribute', () => {
+ createWrapper();
+ expect(findButton().attributes('aria-controls')).toBe('super-sidebar');
+ });
+
+ it('has aria-expanded as true when expanded', () => {
+ createWrapper();
+ expect(findButton().attributes('aria-expanded')).toBe('true');
+ });
+
+ it('has aria-expanded as false when collapsed', () => {
+ createWrapper({ sidebarState: { isCollapsed: true } });
+ expect(findButton().attributes('aria-expanded')).toBe('false');
+ });
+
+ it('has aria-label attribute', () => {
+ createWrapper();
+ expect(findButton().attributes('aria-label')).toBe(__('Navigation sidebar'));
+ });
+
+ it('is disabled when isPeek is true', () => {
+ createWrapper({ sidebarState: { isPeek: true } });
+ expect(findButton().attributes('disabled')).toBe('true');
+ });
+ });
+
+ describe('toolip', () => {
+ it('displays collapse when expanded', () => {
+ createWrapper();
+ expect(getTooltip().title).toBe(__('Collapse sidebar'));
+ });
+
+ it('displays expand when collapsed', () => {
+ createWrapper({ sidebarState: { isCollapsed: true } });
+ expect(getTooltip().title).toBe(__('Expand sidebar'));
+ });
+ });
+
+ describe('toggle', () => {
+ beforeEach(() => {
+ setHTMLFixture(`
+ <button class="${JS_TOGGLE_COLLAPSE_CLASS}">Collapse</button>
+ <button class="${JS_TOGGLE_EXPAND_CLASS}">Expand</button>
+ `);
+ });
+
+ afterEach(() => {
+ resetHTMLFixture();
+ });
+
+ it('collapses the sidebar and focuses the other toggle', async () => {
+ createWrapper();
+ findButton().vm.$emit('click');
+ await nextTick();
+ expect(toggleSuperSidebarCollapsed).toHaveBeenCalledWith(true, true);
+ expect(document.activeElement).toEqual(
+ document.querySelector(`.${JS_TOGGLE_COLLAPSE_CLASS}`),
+ );
+ });
+
+ it('expands the sidebar and focuses the other toggle', async () => {
+ createWrapper({ sidebarState: { isCollapsed: true } });
+ findButton().vm.$emit('click');
+ await nextTick();
+ expect(toggleSuperSidebarCollapsed).toHaveBeenCalledWith(false, true);
+ expect(document.activeElement).toEqual(document.querySelector(`.${JS_TOGGLE_EXPAND_CLASS}`));
+ });
+ });
+});
diff --git a/spec/frontend/super_sidebar/components/user_bar_spec.js b/spec/frontend/super_sidebar/components/user_bar_spec.js
index ae15dd55644..2b75fb27972 100644
--- a/spec/frontend/super_sidebar/components/user_bar_spec.js
+++ b/spec/frontend/super_sidebar/components/user_bar_spec.js
@@ -1,29 +1,65 @@
import { GlBadge } from '@gitlab/ui';
+import Vuex from 'vuex';
+import Vue, { nextTick } from 'vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { __ } from '~/locale';
import CreateMenu from '~/super_sidebar/components/create_menu.vue';
+import SearchModal from '~/super_sidebar/components/global_search/components/global_search.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 { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
+import waitForPromises from 'helpers/wait_for_promises';
+import { highCountTrim } from '~/lib/utils/text_utility';
import { sidebarData } from '../mock_data';
+import { MOCK_DEFAULT_SEARCH_OPTIONS } from './global_search/mock_data';
+
+jest.mock('~/lib/utils/text_utility', () => ({
+ highCountTrim: jest.fn().mockReturnValue('99+'),
+}));
describe('UserBar component', () => {
let wrapper;
const findCreateMenu = () => wrapper.findComponent(CreateMenu);
const findCounter = (at) => wrapper.findAllComponents(Counter).at(at);
+ const findIssuesCounter = () => findCounter(0);
+ const findMRsCounter = () => findCounter(1);
+ const findTodosCounter = () => findCounter(2);
const findMergeRequestMenu = () => wrapper.findComponent(MergeRequestMenu);
const findBrandLogo = () => wrapper.findByTestId('brand-header-custom-logo');
+ const findCollapseButton = () => wrapper.findByTestId('super-sidebar-collapse-button');
+ const findSearchButton = () => wrapper.findByTestId('super-sidebar-search-button');
+ const findSearchModal = () => wrapper.findComponent(SearchModal);
+ const findStopImpersonationButton = () => wrapper.findByTestId('stop-impersonation-btn');
+
+ Vue.use(Vuex);
- const createWrapper = (extraSidebarData = {}) => {
+ const store = new Vuex.Store({
+ getters: {
+ searchOptions: () => MOCK_DEFAULT_SEARCH_OPTIONS,
+ },
+ });
+ const createWrapper = ({
+ hasCollapseButton = true,
+ extraSidebarData = {},
+ provideOverrides = {},
+ } = {}) => {
wrapper = shallowMountExtended(UserBar, {
propsData: {
+ hasCollapseButton,
sidebarData: { ...sidebarData, ...extraSidebarData },
},
provide: {
rootPath: '/',
toggleNewNavEndpoint: '/-/profile/preferences',
+ isImpersonating: false,
+ ...provideOverrides,
},
+ directives: {
+ GlTooltip: createMockDirective('gl-tooltip'),
+ },
+ store,
});
};
@@ -41,32 +77,69 @@ describe('UserBar component', () => {
});
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'));
+ const isuesCounter = findIssuesCounter();
+ expect(isuesCounter.props('count')).toBe(sidebarData.assigned_open_issues_count);
+ expect(isuesCounter.props('href')).toBe(sidebarData.issues_dashboard_path);
+ expect(isuesCounter.props('label')).toBe(__('Issues'));
+ expect(isuesCounter.attributes('data-track-action')).toBe('click_link');
+ expect(isuesCounter.attributes('data-track-label')).toBe('issues_link');
+ expect(isuesCounter.attributes('data-track-property')).toBe('nav_core_menu');
+ expect(isuesCounter.attributes('class')).toContain('dashboard-shortcuts-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'));
+ const mrsCounter = findMRsCounter();
+ expect(mrsCounter.props('count')).toBe(sidebarData.total_merge_requests_count);
+ expect(mrsCounter.props('label')).toBe(__('Merge requests'));
+ expect(mrsCounter.attributes('data-track-action')).toBe('click_dropdown');
+ expect(mrsCounter.attributes('data-track-label')).toBe('merge_requests_menu');
+ expect(mrsCounter.attributes('data-track-property')).toBe('nav_core_menu');
});
- it('renders todos counter', () => {
- expect(findCounter(2).props('count')).toBe(sidebarData.todos_pending_count);
- expect(findCounter(2).props('href')).toBe('/dashboard/todos');
- expect(findCounter(2).props('label')).toBe(__('To-Do list'));
+ describe('Todos counter', () => {
+ it('renders it', () => {
+ const todosCounter = findTodosCounter();
+ expect(todosCounter.props('href')).toBe('/dashboard/todos');
+ expect(todosCounter.props('label')).toBe(__('To-Do list'));
+ expect(todosCounter.attributes('data-track-action')).toBe('click_link');
+ expect(todosCounter.attributes('data-track-label')).toBe('todos_link');
+ expect(todosCounter.attributes('data-track-property')).toBe('nav_core_menu');
+ expect(todosCounter.attributes('class')).toContain('shortcuts-todos');
+ });
+
+ it('should format and update todo counter when event is emitted', async () => {
+ createWrapper();
+ const count = 100;
+ document.dispatchEvent(new CustomEvent('todo:toggle', { detail: { count } }));
+ await nextTick();
+ expect(highCountTrim).toHaveBeenCalledWith(count);
+ expect(findTodosCounter().props('count')).toBe('99+');
+ });
});
it('renders branding logo', () => {
expect(findBrandLogo().exists()).toBe(true);
expect(findBrandLogo().attributes('src')).toBe(sidebarData.logo_url);
});
+
+ it('does not render the "Stop impersonating" button', () => {
+ expect(findStopImpersonationButton().exists()).toBe(false);
+ });
+
+ it('renders collapse button when hasCollapseButton is true', () => {
+ expect(findCollapseButton().exists()).toBe(true);
+ });
+
+ it('does not render collapse button when hasCollapseButton is false', () => {
+ createWrapper({ hasCollapseButton: false });
+ expect(findCollapseButton().exists()).toBe(false);
+ });
});
describe('GitLab Next badge', () => {
describe('when on canary', () => {
it('should render a badge to switch off GitLab Next', () => {
- createWrapper({ gitlab_com_and_canary: true });
+ createWrapper({ extraSidebarData: { gitlab_com_and_canary: true } });
const badge = wrapper.findComponent(GlBadge);
expect(badge.text()).toBe('Next');
expect(badge.attributes('href')).toBe(sidebarData.canary_toggle_com_url);
@@ -75,10 +148,55 @@ describe('UserBar component', () => {
describe('when not on canary', () => {
it('should not render the GitLab Next badge', () => {
- createWrapper({ gitlab_com_and_canary: false });
+ createWrapper({ extraSidebarData: { gitlab_com_and_canary: false } });
const badge = wrapper.findComponent(GlBadge);
expect(badge.exists()).toBe(false);
});
});
});
+
+ describe('Search', () => {
+ beforeEach(async () => {
+ createWrapper();
+ await waitForPromises();
+ });
+
+ it('should render search button', () => {
+ expect(findSearchButton().exists()).toBe(true);
+ });
+
+ it('search button should have tooltip', () => {
+ const tooltip = getBinding(findSearchButton().element, 'gl-tooltip');
+ expect(tooltip.value).toBe(`Search GitLab <kbd>/</kbd>`);
+ });
+
+ it('should render search modal', () => {
+ expect(findSearchModal().exists()).toBe(true);
+ });
+ });
+
+ describe('While impersonating a user', () => {
+ beforeEach(() => {
+ createWrapper({ provideOverrides: { isImpersonating: true } });
+ });
+
+ it('renders the "Stop impersonating" button', () => {
+ expect(findStopImpersonationButton().exists()).toBe(true);
+ });
+
+ it('sets the correct label on the button', () => {
+ const btn = findStopImpersonationButton();
+ const label = __('Stop impersonating');
+
+ expect(btn.attributes('title')).toBe(label);
+ expect(btn.attributes('aria-label')).toBe(label);
+ });
+
+ it('sets the href and data-method attributes', () => {
+ const btn = findStopImpersonationButton();
+
+ expect(btn.attributes('href')).toBe(sidebarData.stop_impersonation_path);
+ expect(btn.attributes('data-method')).toBe('delete');
+ });
+ });
});
diff --git a/spec/frontend/super_sidebar/components/user_menu_spec.js b/spec/frontend/super_sidebar/components/user_menu_spec.js
index b6231e03722..995095d0e35 100644
--- a/spec/frontend/super_sidebar/components/user_menu_spec.js
+++ b/spec/frontend/super_sidebar/components/user_menu_spec.js
@@ -14,7 +14,8 @@ describe('UserMenu component', () => {
const GlEmoji = { template: '<img/>' };
const toggleNewNavEndpoint = invalidUrl;
- const showDropdown = () => wrapper.findComponent(GlDisclosureDropdown).vm.$emit('shown');
+ const findDropdown = () => wrapper.findComponent(GlDisclosureDropdown);
+ const showDropdown = () => findDropdown().vm.$emit('shown');
const createWrapper = (userDataChanges = {}) => {
wrapper = mountExtended(UserMenu, {
@@ -36,6 +37,14 @@ describe('UserMenu component', () => {
trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
};
+ it('passes popper options to the dropdown', () => {
+ createWrapper();
+
+ expect(findDropdown().props('popperOptions')).toEqual({
+ modifiers: [{ name: 'offset', options: { offset: [-211, 4] } }],
+ });
+ });
+
describe('Toggle button', () => {
let toggle;
@@ -93,6 +102,14 @@ describe('UserMenu component', () => {
expect(item.find('.js-set-status-modal-trigger').exists()).toBe(true);
});
+ it('should close the dropdown when status modal opened', () => {
+ setItem({ can_update: true });
+ wrapper.vm.$refs.userDropdown.close = jest.fn();
+ expect(wrapper.vm.$refs.userDropdown.close).not.toHaveBeenCalled();
+ item.vm.$emit('action');
+ expect(wrapper.vm.$refs.userDropdown.close).toHaveBeenCalled();
+ });
+
describe('renders correct label', () => {
it.each`
busy | customized | label
@@ -117,22 +134,42 @@ describe('UserMenu component', () => {
expect(findModalWrapper().exists()).toBe(true);
});
- it('sets default data attributes when status is not customized', () => {
- setItem({ can_update: true });
- expect(findModalWrapper().attributes()).toMatchObject({
- 'data-current-emoji': '',
- 'data-current-message': '',
- 'data-default-emoji': 'speech_balloon',
+ describe('when user cannot update status', () => {
+ it('sets default data attributes', () => {
+ setItem({ can_update: true });
+ expect(findModalWrapper().attributes()).toMatchObject({
+ 'data-current-emoji': '',
+ 'data-current-message': '',
+ 'data-default-emoji': 'speech_balloon',
+ });
});
});
- it('sets user status as data attributes when status is customized', () => {
- setItem({ can_update: true, customized: true });
- expect(findModalWrapper().attributes()).toMatchObject({
- 'data-current-emoji': userMenuMockStatus.emoji,
- 'data-current-message': userMenuMockStatus.message,
- 'data-current-availability': userMenuMockStatus.availability,
- 'data-current-clear-status-after': userMenuMockStatus.clear_after,
+ describe.each`
+ busy | customized
+ ${true} | ${true}
+ ${true} | ${false}
+ ${false} | ${true}
+ ${false} | ${false}
+ `(`when user can update status`, ({ busy, customized }) => {
+ it(`and ${busy ? 'is busy' : 'is not busy'} and status ${
+ customized ? 'is' : 'is not'
+ } customized sets user status data attributes`, () => {
+ setItem({ can_update: true, busy, customized });
+ if (busy || customized) {
+ expect(findModalWrapper().attributes()).toMatchObject({
+ 'data-current-emoji': userMenuMockStatus.emoji,
+ 'data-current-message': userMenuMockStatus.message,
+ 'data-current-availability': userMenuMockStatus.availability,
+ 'data-current-clear-status-after': userMenuMockStatus.clear_after,
+ });
+ } else {
+ expect(findModalWrapper().attributes()).toMatchObject({
+ 'data-current-emoji': '',
+ 'data-current-message': '',
+ 'data-default-emoji': 'speech_balloon',
+ });
+ }
});
});
});
@@ -143,7 +180,7 @@ describe('UserMenu component', () => {
let item;
const setItem = ({ has_start_trial } = {}) => {
- createWrapper({ trial: { has_start_trial } });
+ createWrapper({ trial: { has_start_trial, url: '' } });
item = wrapper.findByTestId('start-trial-item');
};
@@ -160,6 +197,15 @@ describe('UserMenu component', () => {
expect(item.exists()).toBe(true);
});
});
+
+ it('has Snowplow tracking attributes', () => {
+ setItem({ has_start_trial: true });
+ expect(item.find('a').attributes()).toMatchObject({
+ 'data-track-property': 'nav_user_menu',
+ 'data-track-action': 'click_link',
+ 'data-track-label': 'start_trial',
+ });
+ });
});
describe('Buy Pipeline Minutes item', () => {
@@ -202,17 +248,30 @@ describe('UserMenu component', () => {
expect(item.exists()).toBe(true);
});
- it('tracks the Sentry event', () => {
- setItem({ show_buy_pipeline_minutes: true });
- showDropdown();
- expect(trackingSpy).toHaveBeenCalledWith(
- undefined,
- userMenuMockPipelineMinutes.tracking_attrs['track-action'],
- {
- label: userMenuMockPipelineMinutes.tracking_attrs['track-label'],
- property: userMenuMockPipelineMinutes.tracking_attrs['track-property'],
- },
- );
+ describe('Snowplow tracking attributes to track item click', () => {
+ beforeEach(() => {
+ setItem({ show_buy_pipeline_minutes: true });
+ });
+
+ it('has attributes to track item click in scope of new nav', () => {
+ expect(item.find('a').attributes()).toMatchObject({
+ 'data-track-property': 'nav_user_menu',
+ 'data-track-action': 'click_link',
+ 'data-track-label': 'buy_pipeline_minutes',
+ });
+ });
+
+ it('tracks the click on the item', () => {
+ item.vm.$emit('action');
+ expect(trackingSpy).toHaveBeenCalledWith(
+ undefined,
+ userMenuMockPipelineMinutes.tracking_attrs['track-action'],
+ {
+ label: userMenuMockPipelineMinutes.tracking_attrs['track-label'],
+ property: userMenuMockPipelineMinutes.tracking_attrs['track-property'],
+ },
+ );
+ });
});
describe('Callout & notification dot', () => {
@@ -292,33 +351,71 @@ describe('UserMenu component', () => {
});
describe('Edit profile item', () => {
- it('should render a link to the profile page', () => {
+ let item;
+
+ beforeEach(() => {
createWrapper();
- const item = wrapper.findByTestId('edit-profile-item');
+ item = wrapper.findByTestId('edit-profile-item');
+ });
+
+ it('should render a link to the profile page', () => {
expect(item.text()).toBe(UserMenu.i18n.editProfile);
expect(item.find('a').attributes('href')).toBe(userMenuMockData.settings.profile_path);
});
+
+ it('has Snowplow tracking attributes', () => {
+ expect(item.find('a').attributes()).toMatchObject({
+ 'data-track-property': 'nav_user_menu',
+ 'data-track-action': 'click_link',
+ 'data-track-label': 'user_edit_profile',
+ });
+ });
});
describe('Preferences item', () => {
- it('should render a link to the profile page', () => {
+ let item;
+
+ beforeEach(() => {
createWrapper();
- const item = wrapper.findByTestId('preferences-item');
+ item = wrapper.findByTestId('preferences-item');
+ });
+
+ it('should render a link to the profile page', () => {
expect(item.text()).toBe(UserMenu.i18n.preferences);
expect(item.find('a').attributes('href')).toBe(
userMenuMockData.settings.profile_preferences_path,
);
});
+
+ it('has Snowplow tracking attributes', () => {
+ expect(item.find('a').attributes()).toMatchObject({
+ 'data-track-property': 'nav_user_menu',
+ 'data-track-action': 'click_link',
+ 'data-track-label': 'user_preferences',
+ });
+ });
});
describe('GitLab Next item', () => {
describe('on gitlab.com', () => {
- it('should render a link to switch to GitLab Next', () => {
+ let item;
+
+ beforeEach(() => {
createWrapper({ gitlab_com_but_not_canary: true });
- const item = wrapper.findByTestId('gitlab-next-item');
+ item = wrapper.findByTestId('gitlab-next-item');
+ });
+ it('should render a link to switch to GitLab Next', () => {
expect(item.text()).toBe(UserMenu.i18n.gitlabNext);
expect(item.find('a').attributes('href')).toBe(userMenuMockData.canary_toggle_com_url);
});
+
+ it('has Snowplow tracking attributes', () => {
+ expect(item.find('a').attributes()).toMatchObject({
+ 'data-track-property': 'nav_user_menu',
+ 'data-track-action': 'click_link',
+ 'data-track-label': 'switch_to_canary',
+ });
+ });
});
describe('anywhere else', () => {
@@ -340,10 +437,23 @@ describe('UserMenu component', () => {
});
describe('Feedback item', () => {
- it('should render feedback item with a link to a new GitLab issue', () => {
+ let item;
+
+ beforeEach(() => {
createWrapper();
- const feedbackItem = wrapper.findByTestId('feedback-item');
- expect(feedbackItem.find('a').attributes('href')).toBe(UserMenu.feedbackUrl);
+ item = wrapper.findByTestId('feedback-item');
+ });
+
+ it('should render feedback item with a link to a new GitLab issue', () => {
+ expect(item.find('a').attributes('href')).toBe(UserMenu.feedbackUrl);
+ });
+
+ it('has Snowplow tracking attributes', () => {
+ expect(item.find('a').attributes()).toMatchObject({
+ 'data-track-property': 'nav_user_menu',
+ 'data-track-action': 'click_link',
+ 'data-track-label': 'provide_nav_beta_feedback',
+ });
});
});
@@ -370,6 +480,15 @@ describe('UserMenu component', () => {
);
expect(findSignOutGroup().find('a').attributes('data-method')).toBe('post');
});
+
+ it('should track Snowplow event on sign out', () => {
+ findSignOutGroup().vm.$emit('action');
+
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_link', {
+ label: 'user_sign_out',
+ property: 'nav_user_menu',
+ });
+ });
});
});
});
diff --git a/spec/frontend/super_sidebar/components/user_name_group_spec.js b/spec/frontend/super_sidebar/components/user_name_group_spec.js
index c06c8c218d4..6e3b18d3107 100644
--- a/spec/frontend/super_sidebar/components/user_name_group_spec.js
+++ b/spec/frontend/super_sidebar/components/user_name_group_spec.js
@@ -41,10 +41,12 @@ describe('UserNameGroup component', () => {
});
it('passes the item to the disclosure dropdown item', () => {
- expect(findGlDisclosureDropdownItem().props('item')).toEqual({
- text: userMenuMockData.name,
- href: userMenuMockData.link_to_profile,
- });
+ expect(findGlDisclosureDropdownItem().props('item')).toEqual(
+ expect.objectContaining({
+ text: userMenuMockData.name,
+ href: userMenuMockData.link_to_profile,
+ }),
+ );
});
it("renders user's name", () => {
@@ -97,4 +99,16 @@ describe('UserNameGroup component', () => {
});
});
});
+
+ describe('Tracking', () => {
+ it('sets the tracking attributes', () => {
+ expect(findGlDisclosureDropdownItem().find('a').attributes()).toEqual(
+ expect.objectContaining({
+ 'data-track-property': 'nav_user_menu',
+ 'data-track-action': 'click_link',
+ 'data-track-label': 'user_profile',
+ }),
+ );
+ });
+ });
});
diff --git a/spec/frontend/super_sidebar/mock_data.js b/spec/frontend/super_sidebar/mock_data.js
index b540f85d9fe..0c9449d98a9 100644
--- a/spec/frontend/super_sidebar/mock_data.js
+++ b/spec/frontend/super_sidebar/mock_data.js
@@ -49,11 +49,23 @@ export const mergeRequestMenuGroup = [
text: 'Assigned',
href: '/dashboard/merge_requests?assignee_username=root',
count: 4,
+ extraAttrs: {
+ 'data-track-action': 'click_link',
+ 'data-track-label': 'merge_requests_assigned',
+ 'data-track-property': 'nav_core_menu',
+ class: 'dashboard-shortcuts-merge_requests',
+ },
},
{
text: 'Review requests',
href: '/dashboard/merge_requests?reviewer_username=root',
count: 0,
+ extraAttrs: {
+ 'data-track-action': 'click_link',
+ 'data-track-label': 'merge_requests_to_review',
+ 'data-track-property': 'nav_core_menu',
+ class: 'dashboard-shortcuts-review_requests',
+ },
},
],
},
@@ -86,6 +98,21 @@ export const sidebarData = {
gitlab_version_check: { severity: 'success' },
gitlab_com_and_canary: false,
canary_toggle_com_url: 'https://next.gitlab.com',
+ context_switcher_links: [],
+ search: {
+ search_path: '/search',
+ },
+ pinned_items: [],
+ panel_type: 'your_work',
+ update_pins_url: 'path/to/pins',
+ stop_impersonation_path: '/admin/impersonation',
+ shortcut_links: [
+ {
+ title: 'Shortcut link',
+ href: '/shortcut-link',
+ css_class: 'shortcut-link-class',
+ },
+ ],
};
export const userMenuMockStatus = {
@@ -122,6 +149,7 @@ export const userMenuMockData = {
status: userMenuMockStatus,
trial: {
has_start_trial: false,
+ url: invalidUrl,
},
settings: {
profile_path: invalidUrl,
diff --git a/spec/frontend/super_sidebar/super_sidebar_collapsed_state_manager_spec.js b/spec/frontend/super_sidebar/super_sidebar_collapsed_state_manager_spec.js
index 3824965970b..cadcf8c08a3 100644
--- a/spec/frontend/super_sidebar/super_sidebar_collapsed_state_manager_spec.js
+++ b/spec/frontend/super_sidebar/super_sidebar_collapsed_state_manager_spec.js
@@ -1,16 +1,14 @@
import { GlBreakpointInstance as bp, breakpoints } from '@gitlab/ui/dist/utils';
import { getCookie, setCookie } from '~/lib/utils/common_utils';
import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
+import { sidebarState } from '~/super_sidebar/constants';
import {
SIDEBAR_COLLAPSED_CLASS,
SIDEBAR_COLLAPSED_COOKIE,
SIDEBAR_COLLAPSED_COOKIE_EXPIRATION,
toggleSuperSidebarCollapsed,
initSuperSidebarCollapsedState,
- bindSuperSidebarCollapsedEvents,
findPage,
- findSidebar,
- findToggles,
} from '~/super_sidebar/super_sidebar_collapsed_state_manager';
const { xl, sm } = breakpoints;
@@ -33,7 +31,6 @@ describe('Super Sidebar Collapsed State Manager', () => {
setHTMLFixture(`
<div class="page-with-super-sidebar">
<aside class="super-sidebar"></aside>
- <button class="js-super-sidebar-toggle"></button>
</div>
`);
});
@@ -61,8 +58,7 @@ describe('Super Sidebar Collapsed State Manager', () => {
toggleSuperSidebarCollapsed(collapsed, saveCookie);
pageHasCollapsedClass(hasClass);
- expect(findSidebar().ariaHidden).toBe(collapsed);
- expect(findSidebar().inert).toBe(collapsed);
+ expect(sidebarState.isCollapsed).toBe(collapsed);
if (saveCookie && windowWidth >= xl) {
expect(setCookie).toHaveBeenCalledWith(SIDEBAR_COLLAPSED_COOKIE, collapsed, {
@@ -73,29 +69,6 @@ describe('Super Sidebar Collapsed State Manager', () => {
}
},
);
-
- describe('focus', () => {
- it.each`
- collapsed | isUserAction
- ${false} | ${true}
- ${false} | ${false}
- ${true} | ${true}
- ${true} | ${false}
- `(
- 'when collapsed is $collapsed, isUserAction is $isUserAction',
- ({ collapsed, isUserAction }) => {
- const sidebar = findSidebar();
- jest.spyOn(sidebar, 'focus');
- toggleSuperSidebarCollapsed(collapsed, false, isUserAction);
-
- if (!collapsed && isUserAction) {
- expect(sidebar.focus).toHaveBeenCalled();
- } else {
- expect(sidebar.focus).not.toHaveBeenCalled();
- }
- },
- );
- });
});
describe('initSuperSidebarCollapsedState', () => {
@@ -118,40 +91,4 @@ describe('Super Sidebar Collapsed State Manager', () => {
},
);
});
-
- describe('bindSuperSidebarCollapsedEvents', () => {
- it.each`
- windowWidth | cookie | hasClass
- ${xl} | ${undefined} | ${true}
- ${sm} | ${undefined} | ${true}
- ${xl} | ${'true'} | ${false}
- ${sm} | ${'true'} | ${false}
- `(
- 'toggle click sets page class to `page-with-super-sidebar-collapsed` when windowWidth is $windowWidth and cookie value is $cookie',
- ({ windowWidth, cookie, hasClass }) => {
- setHTMLFixture(`
- <div class="page-with-super-sidebar ${cookie ? SIDEBAR_COLLAPSED_CLASS : ''}">
- <aside class="super-sidebar"></aside>
- <button class="js-super-sidebar-toggle"></button>
- </div>
- `);
- jest.spyOn(bp, 'windowWidth').mockReturnValue(windowWidth);
- getCookie.mockReturnValue(cookie);
-
- bindSuperSidebarCollapsedEvents();
-
- findToggles()[0].click();
-
- pageHasCollapsedClass(hasClass);
-
- if (windowWidth >= xl) {
- expect(setCookie).toHaveBeenCalledWith(SIDEBAR_COLLAPSED_COOKIE, !cookie, {
- expires: SIDEBAR_COLLAPSED_COOKIE_EXPIRATION,
- });
- } else {
- expect(setCookie).not.toHaveBeenCalled();
- }
- },
- );
- });
});
diff --git a/spec/frontend/surveys/merge_request_performance/app_spec.js b/spec/frontend/surveys/merge_request_performance/app_spec.js
index af91d8aeb6b..d03451c71a8 100644
--- a/spec/frontend/surveys/merge_request_performance/app_spec.js
+++ b/spec/frontend/surveys/merge_request_performance/app_spec.js
@@ -56,17 +56,17 @@ describe('MergeRequestExperienceSurveyApp', () => {
createWrapper();
});
- it('shows survey', async () => {
+ it('shows survey', () => {
expect(wrapper.html()).toContain('Overall, how satisfied are you with merge requests?');
expect(wrapper.findComponent(SatisfactionRate).exists()).toBe(true);
expect(wrapper.emitted().close).toBe(undefined);
});
- it('tracks render once', async () => {
+ it('tracks render once', () => {
expect(trackingSpy).toHaveBeenCalledWith(...createRenderTrackedArguments());
});
- it("doesn't track subsequent renders", async () => {
+ it("doesn't track subsequent renders", () => {
createWrapper();
expect(trackingSpy).toHaveBeenCalledWith(...createRenderTrackedArguments());
expect(trackingSpy).toHaveBeenCalledTimes(1);
@@ -77,15 +77,15 @@ describe('MergeRequestExperienceSurveyApp', () => {
findCloseButton().vm.$emit('click');
});
- it('triggers user callout on close', async () => {
+ it('triggers user callout on close', () => {
expect(dismiss).toHaveBeenCalledTimes(1);
});
- it('emits close event on close button click', async () => {
+ it('emits close event on close button click', () => {
expect(wrapper.emitted()).toMatchObject({ close: [[]] });
});
- it('tracks dismissal', async () => {
+ it('tracks dismissal', () => {
expect(trackingSpy).toHaveBeenCalledWith(undefined, 'survey:mr_experience', {
label: 'dismiss',
extra: {
@@ -94,7 +94,7 @@ describe('MergeRequestExperienceSurveyApp', () => {
});
});
- it('tracks subsequent renders', async () => {
+ it('tracks subsequent renders', () => {
createWrapper();
expect(trackingSpy.mock.calls).toEqual([
createRenderTrackedArguments(),
@@ -110,7 +110,7 @@ describe('MergeRequestExperienceSurveyApp', () => {
);
});
- it('dismisses user callout on survey rate', async () => {
+ it('dismisses user callout on survey rate', () => {
const rate = wrapper.findComponent(SatisfactionRate);
expect(dismiss).not.toHaveBeenCalled();
rate.vm.$emit('rate', 5);
@@ -126,7 +126,7 @@ describe('MergeRequestExperienceSurveyApp', () => {
);
});
- it('tracks survey rates', async () => {
+ it('tracks survey rates', () => {
const rate = wrapper.findComponent(SatisfactionRate);
rate.vm.$emit('rate', 5);
expect(trackingSpy).toHaveBeenCalledWith(undefined, 'survey:mr_experience', {
@@ -146,7 +146,7 @@ describe('MergeRequestExperienceSurveyApp', () => {
});
});
- it('shows legal note', async () => {
+ it('shows legal note', () => {
expect(wrapper.text()).toContain(
'By continuing, you acknowledge that responses will be used to improve GitLab and in accordance with the GitLab Privacy Policy.',
);
@@ -179,11 +179,11 @@ describe('MergeRequestExperienceSurveyApp', () => {
createWrapper({ shouldShowCallout: false });
});
- it('emits close event', async () => {
+ it('emits close event', () => {
expect(wrapper.emitted()).toMatchObject({ close: [[]] });
});
- it("doesn't track anything", async () => {
+ it("doesn't track anything", () => {
expect(trackingSpy).toHaveBeenCalledTimes(0);
});
});
@@ -195,12 +195,12 @@ describe('MergeRequestExperienceSurveyApp', () => {
document.dispatchEvent(event);
});
- it('emits close event', async () => {
+ it('emits close event', () => {
expect(wrapper.emitted()).toMatchObject({ close: [[]] });
expect(dismiss).toHaveBeenCalledTimes(1);
});
- it('tracks dismissal', async () => {
+ it('tracks dismissal', () => {
expect(trackingSpy).toHaveBeenCalledWith(undefined, 'survey:mr_experience', {
label: 'dismiss',
extra: {
diff --git a/spec/frontend/tags/components/delete_tag_modal_spec.js b/spec/frontend/tags/components/delete_tag_modal_spec.js
index 8438bdb7db0..8ec9925563a 100644
--- a/spec/frontend/tags/components/delete_tag_modal_spec.js
+++ b/spec/frontend/tags/components/delete_tag_modal_spec.js
@@ -69,7 +69,7 @@ describe('Delete tag modal', () => {
expect(submitFormSpy).toHaveBeenCalled();
});
- it('calls show on the modal when a `openModal` event is received through the event hub', async () => {
+ it('calls show on the modal when a `openModal` event is received through the event hub', () => {
const showSpy = jest.spyOn(wrapper.vm.$refs.modal, 'show');
eventHub.$emit('openModal', {
diff --git a/spec/frontend/terraform/components/states_table_actions_spec.js b/spec/frontend/terraform/components/states_table_actions_spec.js
index 31a644b39b4..ed85825c13a 100644
--- a/spec/frontend/terraform/components/states_table_actions_spec.js
+++ b/spec/frontend/terraform/components/states_table_actions_spec.js
@@ -143,7 +143,7 @@ describe('StatesTableActions', () => {
return waitForPromises();
});
- it('opens the modal', async () => {
+ it('opens the modal', () => {
expect(findCopyModal().exists()).toBe(true);
expect(findCopyModal().isVisible()).toBe(true);
});
diff --git a/spec/frontend/test_setup.js b/spec/frontend/test_setup.js
index 3fb226e5ed3..d3d3e5c8c72 100644
--- a/spec/frontend/test_setup.js
+++ b/spec/frontend/test_setup.js
@@ -1,8 +1,15 @@
/* Setup for unit test environment */
// eslint-disable-next-line no-restricted-syntax
import { setImmediate } from 'timers';
+import Dexie from 'dexie';
+import { IDBKeyRange, IDBFactory } from 'fake-indexeddb';
import 'helpers/shared_test_setup';
+const indexedDB = new IDBFactory();
+
+Dexie.dependencies.indexedDB = indexedDB;
+Dexie.dependencies.IDBKeyRange = IDBKeyRange;
+
afterEach(() =>
// give Promises a bit more time so they fail the right test
// eslint-disable-next-line no-restricted-syntax
@@ -11,3 +18,9 @@ afterEach(() =>
jest.runOnlyPendingTimers();
}),
);
+
+afterEach(async () => {
+ const dbs = await indexedDB.databases();
+
+ await Promise.all(dbs.map((db) => indexedDB.deleteDatabase(db.name)));
+});
diff --git a/spec/frontend/time_tracking/components/timelog_source_cell_spec.js b/spec/frontend/time_tracking/components/timelog_source_cell_spec.js
new file mode 100644
index 00000000000..b9be4689c38
--- /dev/null
+++ b/spec/frontend/time_tracking/components/timelog_source_cell_spec.js
@@ -0,0 +1,136 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import TimelogSourceCell from '~/time_tracking/components/timelog_source_cell.vue';
+import {
+ IssuableStatusText,
+ STATUS_CLOSED,
+ STATUS_MERGED,
+ STATUS_OPEN,
+ STATUS_LOCKED,
+ STATUS_REOPENED,
+} from '~/issues/constants';
+
+const createIssuableTimelogMock = (
+ type,
+ { title, state, webUrl, reference } = {
+ title: 'Issuable title',
+ state: STATUS_OPEN,
+ webUrl: 'https://example.com/issuable_url',
+ reference: '#111',
+ },
+) => {
+ return {
+ timelog: {
+ project: {
+ fullPath: 'group/project',
+ },
+ [type]: {
+ title,
+ state,
+ webUrl,
+ reference,
+ },
+ },
+ };
+};
+
+describe('TimelogSourceCell component', () => {
+ Vue.use(VueApollo);
+
+ let wrapper;
+
+ const findTitleContainer = () => wrapper.findByTestId('title-container');
+ const findReferenceContainer = () => wrapper.findByTestId('reference-container');
+ const findStateContainer = () => wrapper.findByTestId('state-container');
+
+ const mountComponent = ({ timelog } = {}) => {
+ wrapper = shallowMountExtended(TimelogSourceCell, {
+ propsData: {
+ timelog,
+ },
+ });
+ };
+
+ describe('when the timelog is associated to an issue', () => {
+ it('shows the issue title as link to the issue', () => {
+ mountComponent(
+ createIssuableTimelogMock('issue', {
+ title: 'Issue title',
+ webUrl: 'https://example.com/issue_url',
+ }),
+ );
+
+ const titleContainer = findTitleContainer();
+
+ expect(titleContainer.text()).toBe('Issue title');
+ expect(titleContainer.attributes('href')).toBe('https://example.com/issue_url');
+ });
+
+ it('shows the issue full reference as link to the issue', () => {
+ mountComponent(
+ createIssuableTimelogMock('issue', {
+ reference: '#111',
+ webUrl: 'https://example.com/issue_url',
+ }),
+ );
+
+ const referenceContainer = findReferenceContainer();
+
+ expect(referenceContainer.text()).toBe('group/project#111');
+ expect(referenceContainer.attributes('href')).toBe('https://example.com/issue_url');
+ });
+
+ it.each`
+ state | stateDescription
+ ${STATUS_OPEN} | ${IssuableStatusText[STATUS_OPEN]}
+ ${STATUS_REOPENED} | ${IssuableStatusText[STATUS_REOPENED]}
+ ${STATUS_LOCKED} | ${IssuableStatusText[STATUS_LOCKED]}
+ ${STATUS_CLOSED} | ${IssuableStatusText[STATUS_CLOSED]}
+ `('shows $stateDescription when the state is $state', ({ state, stateDescription }) => {
+ mountComponent(createIssuableTimelogMock('issue', { state }));
+
+ expect(findStateContainer().text()).toBe(stateDescription);
+ });
+ });
+
+ describe('when the timelog is associated to a merge request', () => {
+ it('shows the merge request title as link to the merge request', () => {
+ mountComponent(
+ createIssuableTimelogMock('mergeRequest', {
+ title: 'MR title',
+ webUrl: 'https://example.com/mr_url',
+ }),
+ );
+
+ const titleContainer = findTitleContainer();
+
+ expect(titleContainer.text()).toBe('MR title');
+ expect(titleContainer.attributes('href')).toBe('https://example.com/mr_url');
+ });
+
+ it('shows the merge request full reference as link to the merge request', () => {
+ mountComponent(
+ createIssuableTimelogMock('mergeRequest', {
+ reference: '!111',
+ webUrl: 'https://example.com/mr_url',
+ }),
+ );
+
+ const referenceContainer = findReferenceContainer();
+
+ expect(referenceContainer.text()).toBe('group/project!111');
+ expect(referenceContainer.attributes('href')).toBe('https://example.com/mr_url');
+ });
+ it.each`
+ state | stateDescription
+ ${STATUS_OPEN} | ${IssuableStatusText[STATUS_OPEN]}
+ ${STATUS_CLOSED} | ${IssuableStatusText[STATUS_CLOSED]}
+ ${STATUS_MERGED} | ${IssuableStatusText[STATUS_MERGED]}
+ `('shows $stateDescription when the state is $state', ({ state, stateDescription }) => {
+ mountComponent(createIssuableTimelogMock('mergeRequest', { state }));
+
+ expect(findStateContainer().text()).toBe(stateDescription);
+ });
+ });
+});
diff --git a/spec/frontend/time_tracking/components/timelogs_app_spec.js b/spec/frontend/time_tracking/components/timelogs_app_spec.js
new file mode 100644
index 00000000000..ca470ce63ac
--- /dev/null
+++ b/spec/frontend/time_tracking/components/timelogs_app_spec.js
@@ -0,0 +1,238 @@
+import Vue, { nextTick } from 'vue';
+import VueApollo from 'vue-apollo';
+import * as Sentry from '@sentry/browser';
+import { GlDatepicker, GlLoadingIcon, GlKeysetPagination } from '@gitlab/ui';
+import getTimelogsEmptyResponse from 'test_fixtures/graphql/get_timelogs_empty_response.json';
+import getPaginatedTimelogsResponse from 'test_fixtures/graphql/get_paginated_timelogs_response.json';
+import getNonPaginatedTimelogsResponse from 'test_fixtures/graphql/get_non_paginated_timelogs_response.json';
+import { createAlert } from '~/alert';
+import { mountExtended, extendedWrapper } from 'helpers/vue_test_utils_helper';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import getTimelogsQuery from '~/time_tracking/components/queries/get_timelogs.query.graphql';
+import TimelogsApp from '~/time_tracking/components/timelogs_app.vue';
+import TimelogsTable from '~/time_tracking/components/timelogs_table.vue';
+
+jest.mock('~/alert');
+jest.mock('@sentry/browser');
+
+describe('Timelogs app', () => {
+ Vue.use(VueApollo);
+
+ let wrapper;
+ let fakeApollo;
+
+ const findForm = () => wrapper.find('form');
+ const findUsernameInput = () => extendedWrapper(findForm()).findByTestId('form-username');
+ const findTableContainer = () => wrapper.findByTestId('table-container');
+ const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
+ const findTotalTimeSpentContainer = () => wrapper.findByTestId('total-time-spent-container');
+ const findTable = () => wrapper.findComponent(TimelogsTable);
+ const findPagination = () => wrapper.findComponent(GlKeysetPagination);
+
+ const findFormDatePicker = (testId) =>
+ findForm()
+ .findAllComponents(GlDatepicker)
+ .filter((c) => c.attributes('data-testid') === testId);
+ const findFromDatepicker = () => findFormDatePicker('form-from-date').at(0);
+ const findToDatepicker = () => findFormDatePicker('form-to-date').at(0);
+
+ const submitForm = () => findForm().trigger('submit');
+
+ const resolvedEmptyListMock = jest.fn().mockResolvedValue(getTimelogsEmptyResponse);
+ const resolvedPaginatedListMock = jest.fn().mockResolvedValue(getPaginatedTimelogsResponse);
+ const resolvedNonPaginatedListMock = jest.fn().mockResolvedValue(getNonPaginatedTimelogsResponse);
+ const rejectedMock = jest.fn().mockRejectedValue({});
+
+ const mountComponent = ({ props, data } = {}, queryResolverMock = resolvedEmptyListMock) => {
+ fakeApollo = createMockApollo([[getTimelogsQuery, queryResolverMock]]);
+
+ wrapper = mountExtended(TimelogsApp, {
+ data() {
+ return {
+ ...data,
+ };
+ },
+ propsData: {
+ limitToHours: false,
+ ...props,
+ },
+ apolloProvider: fakeApollo,
+ });
+ };
+
+ beforeEach(() => {
+ createAlert.mockClear();
+ Sentry.captureException.mockClear();
+ });
+
+ afterEach(() => {
+ fakeApollo = null;
+ });
+
+ describe('the content', () => {
+ it('shows the form and the loading icon when loading', () => {
+ mountComponent();
+
+ expect(findForm().exists()).toBe(true);
+ expect(findLoadingIcon().exists()).toBe(true);
+ expect(findTableContainer().exists()).toBe(false);
+ });
+
+ it('shows the form and the table container when finished loading', async () => {
+ mountComponent();
+
+ await waitForPromises();
+
+ expect(findForm().exists()).toBe(true);
+ expect(findLoadingIcon().exists()).toBe(false);
+ expect(findTableContainer().exists()).toBe(true);
+ });
+ });
+
+ describe('the filter form', () => {
+ it('runs the query with the correct data', async () => {
+ mountComponent();
+
+ const username = 'johnsmith';
+ const fromDate = new Date('2023-02-28');
+ const toDate = new Date('2023-03-28');
+
+ findUsernameInput().vm.$emit('input', username);
+ findFromDatepicker().vm.$emit('input', fromDate);
+ findToDatepicker().vm.$emit('input', toDate);
+
+ resolvedEmptyListMock.mockClear();
+
+ submitForm();
+
+ await waitForPromises();
+
+ expect(resolvedEmptyListMock).toHaveBeenCalledWith({
+ username,
+ startDate: fromDate,
+ endDate: toDate,
+ groupId: null,
+ projectId: null,
+ first: 20,
+ last: null,
+ after: null,
+ before: null,
+ });
+ expect(createAlert).not.toHaveBeenCalled();
+ expect(Sentry.captureException).not.toHaveBeenCalled();
+ });
+
+ it('runs the query with the correct data after the date filters are cleared', async () => {
+ mountComponent();
+
+ const username = 'johnsmith';
+
+ findUsernameInput().vm.$emit('input', username);
+ findFromDatepicker().vm.$emit('clear');
+ findToDatepicker().vm.$emit('clear');
+
+ resolvedEmptyListMock.mockClear();
+
+ submitForm();
+
+ await waitForPromises();
+
+ expect(resolvedEmptyListMock).toHaveBeenCalledWith({
+ username,
+ startDate: null,
+ endDate: null,
+ groupId: null,
+ projectId: null,
+ first: 20,
+ last: null,
+ after: null,
+ before: null,
+ });
+ expect(createAlert).not.toHaveBeenCalled();
+ expect(Sentry.captureException).not.toHaveBeenCalled();
+ });
+
+ it('shows an alert an logs to sentry when the mutation is rejected', async () => {
+ mountComponent({}, rejectedMock);
+
+ await waitForPromises();
+
+ expect(createAlert).toHaveBeenCalledWith({
+ message: 'Something went wrong. Please try again.',
+ });
+ expect(Sentry.captureException).toHaveBeenCalled();
+ });
+ });
+
+ describe('the total time spent container', () => {
+ it('is not visible when there are no timelogs', async () => {
+ mountComponent();
+
+ await waitForPromises();
+
+ expect(findTotalTimeSpentContainer().exists()).toBe(false);
+ });
+
+ it('shows the correct value when `limitToHours` is false', async () => {
+ mountComponent({}, resolvedNonPaginatedListMock);
+
+ await waitForPromises();
+
+ expect(findTotalTimeSpentContainer().exists()).toBe(true);
+ expect(findTotalTimeSpentContainer().text()).toBe('3d');
+ });
+
+ it('shows the correct value when `limitToHours` is true', async () => {
+ mountComponent({ props: { limitToHours: true } }, resolvedNonPaginatedListMock);
+
+ await waitForPromises();
+
+ expect(findTotalTimeSpentContainer().exists()).toBe(true);
+ expect(findTotalTimeSpentContainer().text()).toBe('24h');
+ });
+ });
+
+ describe('the table', () => {
+ it('gets created with the right props when `limitToHours` is false', async () => {
+ mountComponent({}, resolvedNonPaginatedListMock);
+
+ await waitForPromises();
+
+ expect(findTable().props()).toMatchObject({
+ limitToHours: false,
+ entries: getNonPaginatedTimelogsResponse.data.timelogs.nodes,
+ });
+ });
+
+ it('gets created with the right props when `limitToHours` is true', async () => {
+ mountComponent({ props: { limitToHours: true } }, resolvedNonPaginatedListMock);
+
+ await waitForPromises();
+
+ expect(findTable().props()).toMatchObject({
+ limitToHours: true,
+ entries: getNonPaginatedTimelogsResponse.data.timelogs.nodes,
+ });
+ });
+ });
+
+ describe('the pagination element', () => {
+ it('is not visible whene there is no pagination data', async () => {
+ mountComponent({}, resolvedNonPaginatedListMock);
+
+ await waitForPromises();
+
+ expect(findPagination().exists()).toBe(false);
+ });
+
+ it('is visible whene there is pagination data', async () => {
+ mountComponent({}, resolvedPaginatedListMock);
+
+ await waitForPromises();
+ await nextTick();
+
+ expect(findPagination().exists()).toBe(true);
+ });
+ });
+});
diff --git a/spec/frontend/time_tracking/components/timelogs_table_spec.js b/spec/frontend/time_tracking/components/timelogs_table_spec.js
new file mode 100644
index 00000000000..980fb79e8fb
--- /dev/null
+++ b/spec/frontend/time_tracking/components/timelogs_table_spec.js
@@ -0,0 +1,223 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import { GlTable } from '@gitlab/ui';
+import { mountExtended, extendedWrapper } from 'helpers/vue_test_utils_helper';
+import TimelogsTable from '~/time_tracking/components/timelogs_table.vue';
+import TimelogSourceCell from '~/time_tracking/components/timelog_source_cell.vue';
+import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
+import { STATUS_OPEN, STATUS_CLOSED, STATUS_MERGED } from '~/issues/constants';
+
+const baseTimelogMock = {
+ timeSpent: 600,
+ project: {
+ fullPath: 'group/project',
+ },
+ user: {
+ name: 'John Smith',
+ avatarUrl: 'https://example.gitlab.com/john.jpg',
+ webPath: 'https://example.gitlab.com/john',
+ },
+ spentAt: '2023-03-27T21:00:00Z',
+ note: null,
+ summary: 'Summary from timelog field',
+ issue: {
+ title: 'Issue title',
+ webUrl: 'https://example.gitlab.com/issue_url_a',
+ state: STATUS_OPEN,
+ reference: '#111',
+ },
+ mergeRequest: null,
+};
+
+const timelogsMock = [
+ baseTimelogMock,
+ {
+ timeSpent: 3600,
+ project: {
+ fullPath: 'group/project_b',
+ },
+ user: {
+ name: 'Paul Reed',
+ avatarUrl: 'https://example.gitlab.com/paul.jpg',
+ webPath: 'https://example.gitlab.com/paul',
+ },
+ spentAt: '2023-03-28T16:00:00Z',
+ note: {
+ body: 'Summary from the body',
+ },
+ summary: null,
+ issue: {
+ title: 'Other issue title',
+ webUrl: 'https://example.gitlab.com/issue_url_b',
+ state: STATUS_CLOSED,
+ reference: '#112',
+ },
+ mergeRequest: null,
+ },
+ {
+ timeSpent: 27 * 60 * 60, // 27h or 3d 3h (3 days of 8 hours)
+ project: {
+ fullPath: 'group/project_b',
+ },
+ user: {
+ name: 'Les Gibbons',
+ avatarUrl: 'https://example.gitlab.com/les.jpg',
+ webPath: 'https://example.gitlab.com/les',
+ },
+ spentAt: '2023-03-28T18:00:00Z',
+ note: null,
+ summary: 'Other timelog summary',
+ issue: null,
+ mergeRequest: {
+ title: 'MR title',
+ webUrl: 'https://example.gitlab.com/mr_url',
+ state: STATUS_MERGED,
+ reference: '!99',
+ },
+ },
+];
+
+describe('TimelogsTable component', () => {
+ Vue.use(VueApollo);
+
+ let wrapper;
+
+ const findTable = () => wrapper.findComponent(GlTable);
+ const findTableRows = () => findTable().find('tbody').findAll('tr');
+ const findRowSpentAt = (rowIndex) =>
+ extendedWrapper(findTableRows().at(rowIndex)).findByTestId('date-container');
+ const findRowSource = (rowIndex) => findTableRows().at(rowIndex).findComponent(TimelogSourceCell);
+ const findRowUser = (rowIndex) => findTableRows().at(rowIndex).findComponent(UserAvatarLink);
+ const findRowTimeSpent = (rowIndex) =>
+ extendedWrapper(findTableRows().at(rowIndex)).findByTestId('time-spent-container');
+ const findRowSummary = (rowIndex) =>
+ extendedWrapper(findTableRows().at(rowIndex)).findByTestId('summary-container');
+
+ const mountComponent = (props = {}) => {
+ wrapper = mountExtended(TimelogsTable, {
+ propsData: {
+ entries: timelogsMock,
+ limitToHours: false,
+ ...props,
+ },
+ stubs: { GlTable },
+ });
+ };
+
+ describe('when there are no entries', () => {
+ it('show the empty table message and no rows', () => {
+ mountComponent({ entries: [] });
+
+ expect(findTable().text()).toContain('There are no records to show');
+ expect(findTableRows()).toHaveLength(1);
+ });
+ });
+
+ describe('when there are some entries', () => {
+ it('does not show the empty table message and has the correct number of rows', () => {
+ mountComponent();
+
+ expect(findTable().text()).not.toContain('There are no records to show');
+ expect(findTableRows()).toHaveLength(3);
+ });
+
+ describe('Spent at column', () => {
+ it('shows the spent at value with in the correct format', () => {
+ mountComponent();
+
+ expect(findRowSpentAt(0).text()).toBe('March 27, 2023, 21:00 (UTC: +0000)');
+ });
+ });
+
+ describe('Source column', () => {
+ it('creates the source cell component passing the right props', () => {
+ mountComponent();
+
+ expect(findRowSource(0).props()).toMatchObject({
+ timelog: timelogsMock[0],
+ });
+ expect(findRowSource(1).props()).toMatchObject({
+ timelog: timelogsMock[1],
+ });
+ expect(findRowSource(2).props()).toMatchObject({
+ timelog: timelogsMock[2],
+ });
+ });
+ });
+
+ describe('User column', () => {
+ it('creates the user avatar component passing the right props', () => {
+ mountComponent();
+
+ expect(findRowUser(0).props()).toMatchObject({
+ linkHref: timelogsMock[0].user.webPath,
+ imgSrc: timelogsMock[0].user.avatarUrl,
+ imgSize: 16,
+ imgAlt: timelogsMock[0].user.name,
+ tooltipText: timelogsMock[0].user.name,
+ username: timelogsMock[0].user.name,
+ });
+ expect(findRowUser(1).props()).toMatchObject({
+ linkHref: timelogsMock[1].user.webPath,
+ imgSrc: timelogsMock[1].user.avatarUrl,
+ imgSize: 16,
+ imgAlt: timelogsMock[1].user.name,
+ tooltipText: timelogsMock[1].user.name,
+ username: timelogsMock[1].user.name,
+ });
+ expect(findRowUser(2).props()).toMatchObject({
+ linkHref: timelogsMock[2].user.webPath,
+ imgSrc: timelogsMock[2].user.avatarUrl,
+ imgSize: 16,
+ imgAlt: timelogsMock[2].user.name,
+ tooltipText: timelogsMock[2].user.name,
+ username: timelogsMock[2].user.name,
+ });
+ });
+ });
+
+ describe('Time spent column', () => {
+ it('shows the time spent value with the correct format when `limitToHours` is false', () => {
+ mountComponent();
+
+ expect(findRowTimeSpent(0).text()).toBe('10m');
+ expect(findRowTimeSpent(1).text()).toBe('1h');
+ expect(findRowTimeSpent(2).text()).toBe('3d 3h');
+ });
+
+ it('shows the time spent value with the correct format when `limitToHours` is true', () => {
+ mountComponent({ limitToHours: true });
+
+ expect(findRowTimeSpent(0).text()).toBe('10m');
+ expect(findRowTimeSpent(1).text()).toBe('1h');
+ expect(findRowTimeSpent(2).text()).toBe('27h');
+ });
+ });
+
+ describe('Summary column', () => {
+ it('shows the summary from the note when note body is present and not empty', () => {
+ mountComponent({
+ entries: [{ ...baseTimelogMock, note: { body: 'Summary from note body' } }],
+ });
+
+ expect(findRowSummary(0).text()).toBe('Summary from note body');
+ });
+
+ it('shows the summary from the timelog note body is present but empty', () => {
+ mountComponent({
+ entries: [{ ...baseTimelogMock, note: { body: '' } }],
+ });
+
+ expect(findRowSummary(0).text()).toBe('Summary from timelog field');
+ });
+
+ it('shows the summary from the timelog note body is not present', () => {
+ mountComponent({
+ entries: [baseTimelogMock],
+ });
+
+ expect(findRowSummary(0).text()).toBe('Summary from timelog field');
+ });
+ });
+ });
+});
diff --git a/spec/frontend/toggles/index_spec.js b/spec/frontend/toggles/index_spec.js
index 89e35991914..cccdf17a787 100644
--- a/spec/frontend/toggles/index_spec.js
+++ b/spec/frontend/toggles/index_spec.js
@@ -52,7 +52,7 @@ describe('toggles/index.js', () => {
initToggleWithOptions();
});
- it('attaches a GlToggle to the element', async () => {
+ it('attaches a GlToggle to the element', () => {
expect(toggleWrapper).not.toBe(null);
expect(toggleWrapper.querySelector(TOGGLE_LABEL_CLASS).textContent).toBe(toggleLabel);
});
diff --git a/spec/frontend/tracking/tracking_initialization_spec.js b/spec/frontend/tracking/tracking_initialization_spec.js
index f1628ad9793..0f888e6e1cc 100644
--- a/spec/frontend/tracking/tracking_initialization_spec.js
+++ b/spec/frontend/tracking/tracking_initialization_spec.js
@@ -52,14 +52,11 @@ describe('Tracking', () => {
hostname: 'app.test.com',
cookieDomain: '.test.com',
appId: '',
- userFingerprint: false,
respectDoNotTrack: true,
- forceSecureTracker: true,
eventMethod: 'post',
contexts: { webPage: true, performanceTiming: true },
formTracking: false,
linkClickTracking: false,
- pageUnloadTimer: 10,
formTrackingConfig: {
fields: { allow: [] },
forms: { allow: [] },
@@ -80,8 +77,14 @@ describe('Tracking', () => {
it('should activate features based on what has been enabled', () => {
initDefaultTrackers();
- expect(snowplowSpy).toHaveBeenCalledWith('enableActivityTracking', 30, 30);
- expect(snowplowSpy).toHaveBeenCalledWith('trackPageView', 'GitLab', [standardContext]);
+ expect(snowplowSpy).toHaveBeenCalledWith('enableActivityTracking', {
+ minimumVisitLength: 30,
+ heartbeatDelay: 30,
+ });
+ expect(snowplowSpy).toHaveBeenCalledWith('trackPageView', {
+ title: 'GitLab',
+ context: [standardContext],
+ });
expect(snowplowSpy).toHaveBeenCalledWith('setDocumentTitle', 'GitLab');
expect(snowplowSpy).not.toHaveBeenCalledWith('enableFormTracking');
expect(snowplowSpy).not.toHaveBeenCalledWith('enableLinkClickTracking');
@@ -131,10 +134,10 @@ describe('Tracking', () => {
it('includes those contexts alongside the standard context', () => {
initDefaultTrackers();
- expect(snowplowSpy).toHaveBeenCalledWith('trackPageView', 'GitLab', [
- standardContext,
- ...experimentContexts,
- ]);
+ expect(snowplowSpy).toHaveBeenCalledWith('trackPageView', {
+ title: 'GitLab',
+ context: [standardContext, ...experimentContexts],
+ });
});
});
});
diff --git a/spec/frontend/tracking/tracking_spec.js b/spec/frontend/tracking/tracking_spec.js
index 4871644d99f..c23790bb589 100644
--- a/spec/frontend/tracking/tracking_spec.js
+++ b/spec/frontend/tracking/tracking_spec.js
@@ -65,15 +65,14 @@ describe('Tracking', () => {
it('tracks to snowplow (our current tracking system)', () => {
Tracking.event(TEST_CATEGORY, TEST_ACTION, { label: TEST_LABEL });
- expect(snowplowSpy).toHaveBeenCalledWith(
- 'trackStructEvent',
- TEST_CATEGORY,
- TEST_ACTION,
- TEST_LABEL,
- undefined,
- undefined,
- [standardContext],
- );
+ expect(snowplowSpy).toHaveBeenCalledWith('trackStructEvent', {
+ category: TEST_CATEGORY,
+ action: TEST_ACTION,
+ label: TEST_LABEL,
+ property: undefined,
+ value: undefined,
+ context: [standardContext],
+ });
});
it('returns `true` if the Snowplow library was called without issues', () => {
@@ -93,14 +92,13 @@ describe('Tracking', () => {
Tracking.event(TEST_CATEGORY, TEST_ACTION, { extra });
- expect(snowplowSpy).toHaveBeenCalledWith(
- 'trackStructEvent',
- TEST_CATEGORY,
- TEST_ACTION,
- undefined,
- undefined,
- undefined,
- [
+ expect(snowplowSpy).toHaveBeenCalledWith('trackStructEvent', {
+ category: TEST_CATEGORY,
+ action: TEST_ACTION,
+ label: undefined,
+ property: undefined,
+ value: undefined,
+ context: [
{
...standardContext,
data: {
@@ -109,7 +107,7 @@ describe('Tracking', () => {
},
},
],
- );
+ });
});
it('skips tracking if snowplow is unavailable', () => {
@@ -209,14 +207,16 @@ describe('Tracking', () => {
describe('.enableFormTracking', () => {
it('tells snowplow to enable form tracking, with only explicit contexts', () => {
- const config = { forms: { allow: ['form-class1'] }, fields: { allow: ['input-class1'] } };
+ const config = {
+ forms: { allow: ['form-class1'] },
+ fields: { allow: ['input-class1'] },
+ };
Tracking.enableFormTracking(config, ['_passed_context_', standardContext]);
- expect(snowplowSpy).toHaveBeenCalledWith(
- 'enableFormTracking',
- { forms: { whitelist: ['form-class1'] }, fields: { whitelist: ['input-class1'] } },
- ['_passed_context_'],
- );
+ expect(snowplowSpy).toHaveBeenCalledWith('enableFormTracking', {
+ options: { forms: { allowlist: ['form-class1'] }, fields: { allowlist: ['input-class1'] } },
+ context: ['_passed_context_'],
+ });
});
it('throws an error if no allow rules are provided', () => {
@@ -232,11 +232,10 @@ describe('Tracking', () => {
it('does not add empty form allow rules', () => {
Tracking.enableFormTracking({ fields: { allow: ['input-class1'] } });
- expect(snowplowSpy).toHaveBeenCalledWith(
- 'enableFormTracking',
- { fields: { whitelist: ['input-class1'] } },
- [],
- );
+ expect(snowplowSpy).toHaveBeenCalledWith('enableFormTracking', {
+ options: { fields: { allowlist: ['input-class1'] } },
+ context: [],
+ });
});
describe('when `document.readyState` does not equal `complete`', () => {
@@ -285,15 +284,14 @@ describe('Tracking', () => {
Tracking.flushPendingEvents();
- expect(snowplowSpy).toHaveBeenCalledWith(
- 'trackStructEvent',
- TEST_CATEGORY,
- TEST_ACTION,
- TEST_LABEL,
- undefined,
- undefined,
- [standardContext],
- );
+ expect(snowplowSpy).toHaveBeenCalledWith('trackStructEvent', {
+ category: TEST_CATEGORY,
+ action: TEST_ACTION,
+ label: TEST_LABEL,
+ property: undefined,
+ value: undefined,
+ context: [standardContext],
+ });
});
});
@@ -457,15 +455,14 @@ describe('Tracking', () => {
value: '0',
});
- expect(snowplowSpy).toHaveBeenCalledWith(
- 'trackStructEvent',
- TEST_CATEGORY,
- 'click_input2',
- undefined,
- undefined,
- 0,
- [standardContext],
- );
+ expect(snowplowSpy).toHaveBeenCalledWith('trackStructEvent', {
+ category: TEST_CATEGORY,
+ action: 'click_input2',
+ label: undefined,
+ property: undefined,
+ value: 0,
+ context: [standardContext],
+ });
});
it('handles checkbox values correctly', () => {
diff --git a/spec/frontend/usage_quotas/storage/components/project_storage_detail_spec.js b/spec/frontend/usage_quotas/storage/components/project_storage_detail_spec.js
index 6065ec9e4bf..15758c94436 100644
--- a/spec/frontend/usage_quotas/storage/components/project_storage_detail_spec.js
+++ b/spec/frontend/usage_quotas/storage/components/project_storage_detail_spec.js
@@ -2,12 +2,7 @@ import { GlTableLite, GlPopover } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import ProjectStorageDetail from '~/usage_quotas/storage/components/project_storage_detail.vue';
-import {
- containerRegistryPopoverId,
- containerRegistryId,
- uploadsPopoverId,
- uploadsId,
-} from '~/usage_quotas/storage/constants';
+import { containerRegistryPopoverId, containerRegistryId } from '~/usage_quotas/storage/constants';
import { numberToHumanSize } from '~/lib/utils/number_utils';
import { projectData, projectHelpLinks } from '../mock_data';
@@ -47,9 +42,7 @@ describe('ProjectStorageDetail', () => {
const findPopoverById = (id) =>
wrapper.findAllComponents(GlPopover).filter((p) => p.attributes('data-testid') === id);
const findContainerRegistryPopover = () => findPopoverById(containerRegistryPopoverId);
- const findUploadsPopover = () => findPopoverById(uploadsPopoverId);
const findContainerRegistryWarningIcon = () => wrapper.find(`#${containerRegistryPopoverId}`);
- const findUploadsWarningIcon = () => wrapper.find(`#${uploadsPopoverId}`);
beforeEach(() => {
createComponent();
@@ -96,31 +89,19 @@ describe('ProjectStorageDetail', () => {
});
describe.each`
- description | mockStorageTypes | rendersContainerRegistryPopover | rendersUploadsPopover
- ${'without any storage type that has popover'} | ${[generateStorageType()]} | ${false} | ${false}
- ${'with container registry storage type'} | ${[generateStorageType(containerRegistryId)]} | ${true} | ${false}
- ${'with uploads storage type'} | ${[generateStorageType(uploadsId)]} | ${false} | ${true}
- ${'with container registry and uploads storage types'} | ${[generateStorageType(containerRegistryId), generateStorageType(uploadsId)]} | ${true} | ${true}
- `(
- '$description',
- ({ mockStorageTypes, rendersContainerRegistryPopover, rendersUploadsPopover }) => {
- beforeEach(() => {
- createComponent({ storageTypes: mockStorageTypes });
- });
-
- it(`does ${
- rendersContainerRegistryPopover ? '' : ' not'
- } render container registry warning icon and popover`, () => {
- expect(findContainerRegistryWarningIcon().exists()).toBe(rendersContainerRegistryPopover);
- expect(findContainerRegistryPopover().exists()).toBe(rendersContainerRegistryPopover);
- });
+ description | mockStorageTypes | rendersContainerRegistryPopover
+ ${'without any storage type that has popover'} | ${[generateStorageType()]} | ${false}
+ ${'with container registry storage type'} | ${[generateStorageType(containerRegistryId)]} | ${true}
+ `('$description', ({ mockStorageTypes, rendersContainerRegistryPopover }) => {
+ beforeEach(() => {
+ createComponent({ storageTypes: mockStorageTypes });
+ });
- it(`does ${
- rendersUploadsPopover ? '' : ' not'
- } render container registry warning icon and popover`, () => {
- expect(findUploadsWarningIcon().exists()).toBe(rendersUploadsPopover);
- expect(findUploadsPopover().exists()).toBe(rendersUploadsPopover);
- });
- },
- );
+ it(`does ${
+ rendersContainerRegistryPopover ? '' : ' not'
+ } render container registry warning icon and popover`, () => {
+ expect(findContainerRegistryWarningIcon().exists()).toBe(rendersContainerRegistryPopover);
+ expect(findContainerRegistryPopover().exists()).toBe(rendersContainerRegistryPopover);
+ });
+ });
});
diff --git a/spec/frontend/usage_quotas/storage/components/storage_type_icon_spec.js b/spec/frontend/usage_quotas/storage/components/storage_type_icon_spec.js
index 364cf1e587b..ebe4c4b7f4e 100644
--- a/spec/frontend/usage_quotas/storage/components/storage_type_icon_spec.js
+++ b/spec/frontend/usage_quotas/storage/components/storage_type_icon_spec.js
@@ -22,7 +22,6 @@ describe('StorageTypeIcon', () => {
${'snippet'} | ${'snippetsSize'}
${'infrastructure-registry'} | ${'repositorySize'}
${'package'} | ${'packagesSize'}
- ${'upload'} | ${'uploadsSize'}
${'disk'} | ${'wikiSize'}
${'disk'} | ${'anything-else'}
`(
diff --git a/spec/frontend/usage_quotas/storage/components/usage_graph_spec.js b/spec/frontend/usage_quotas/storage/components/usage_graph_spec.js
index 02268e1c9d8..2662711076b 100644
--- a/spec/frontend/usage_quotas/storage/components/usage_graph_spec.js
+++ b/spec/frontend/usage_quotas/storage/components/usage_graph_spec.js
@@ -28,11 +28,10 @@ describe('Storage Counter usage graph component', () => {
packagesSize: 3000,
containerRegistrySize: 2500,
lfsObjectsSize: 2000,
- buildArtifactsSize: 500,
- pipelineArtifactsSize: 500,
+ buildArtifactsSize: 700,
+ pipelineArtifactsSize: 300,
snippetsSize: 2000,
storageSize: 17000,
- uploadsSize: 1000,
},
limit: 2000,
};
@@ -51,7 +50,6 @@ describe('Storage Counter usage graph component', () => {
repositorySize,
wikiSize,
snippetsSize,
- uploadsSize,
} = data.rootStorageStatistics;
expect(types.at(0).text()).toMatchInterpolatedText(`Wiki ${numberToHumanSize(wikiSize)}`);
@@ -64,16 +62,16 @@ describe('Storage Counter usage graph component', () => {
expect(types.at(3).text()).toMatchInterpolatedText(
`Container Registry ${numberToHumanSize(containerRegistrySize)}`,
);
- expect(types.at(4).text()).toMatchInterpolatedText(
- `LFS storage ${numberToHumanSize(lfsObjectsSize)}`,
- );
+ expect(types.at(4).text()).toMatchInterpolatedText(`LFS ${numberToHumanSize(lfsObjectsSize)}`);
expect(types.at(5).text()).toMatchInterpolatedText(
`Snippets ${numberToHumanSize(snippetsSize)}`,
);
expect(types.at(6).text()).toMatchInterpolatedText(
- `Artifacts ${numberToHumanSize(buildArtifactsSize + pipelineArtifactsSize)}`,
+ `Job artifacts ${numberToHumanSize(buildArtifactsSize)}`,
+ );
+ expect(types.at(7).text()).toMatchInterpolatedText(
+ `Pipeline artifacts ${numberToHumanSize(pipelineArtifactsSize)}`,
);
- expect(types.at(7).text()).toMatchInterpolatedText(`Uploads ${numberToHumanSize(uploadsSize)}`);
});
describe('when storage type is not used', () => {
@@ -112,8 +110,8 @@ describe('Storage Counter usage graph component', () => {
'0.14705882352941177',
'0.11764705882352941',
'0.11764705882352941',
- '0.058823529411764705',
- '0.058823529411764705',
+ '0.041176470588235294',
+ '0.01764705882352941',
]);
});
});
@@ -132,8 +130,8 @@ describe('Storage Counter usage graph component', () => {
'0.14705882352941177',
'0.11764705882352941',
'0.11764705882352941',
- '0.058823529411764705',
- '0.058823529411764705',
+ '0.041176470588235294',
+ '0.01764705882352941',
]);
});
});
diff --git a/spec/frontend/usage_quotas/storage/mock_data.js b/spec/frontend/usage_quotas/storage/mock_data.js
index b1c6be10d80..b4b02f77b52 100644
--- a/spec/frontend/usage_quotas/storage/mock_data.js
+++ b/spec/frontend/usage_quotas/storage/mock_data.js
@@ -19,16 +19,25 @@ export const projectData = {
{
storageType: {
id: 'buildArtifactsSize',
- name: 'Artifacts',
- description: 'Pipeline artifacts and job artifacts, created with CI/CD.',
+ name: 'Job artifacts',
+ description: 'Job artifacts created by CI/CD.',
helpPath: '/build-artifacts',
},
value: 400000,
},
{
storageType: {
+ id: 'pipelineArtifactsSize',
+ name: 'Pipeline artifacts',
+ description: 'Pipeline artifacts created by CI/CD.',
+ helpPath: '/pipeline-artifacts',
+ },
+ value: 400000,
+ },
+ {
+ storageType: {
id: 'lfsObjectsSize',
- name: 'LFS storage',
+ name: 'LFS',
description: 'Audio samples, videos, datasets, and graphics.',
helpPath: '/lsf-objects',
},
@@ -63,15 +72,6 @@ export const projectData = {
},
{
storageType: {
- id: 'uploadsSize',
- name: 'Uploads',
- description: 'File attachments and smaller design graphics.',
- helpPath: '/uploads',
- },
- value: 900000,
- },
- {
- storageType: {
id: 'wikiSize',
name: 'Wiki',
description: 'Wiki content.',
@@ -87,11 +87,11 @@ export const projectHelpLinks = {
containerRegistry: '/container_registry',
usageQuotas: '/usage-quotas',
buildArtifacts: '/build-artifacts',
+ pipelineArtifacts: '/pipeline-artifacts',
lfsObjects: '/lsf-objects',
packages: '/packages',
repository: '/repository',
snippets: '/snippets',
- uploads: '/uploads',
wiki: '/wiki',
};
diff --git a/spec/frontend/user_lists/components/edit_user_list_spec.js b/spec/frontend/user_lists/components/edit_user_list_spec.js
index 5f067d9de3c..b5eb6313bed 100644
--- a/spec/frontend/user_lists/components/edit_user_list_spec.js
+++ b/spec/frontend/user_lists/components/edit_user_list_spec.js
@@ -67,7 +67,7 @@ describe('user_lists/components/edit_user_list', () => {
expect(alert.text()).toContain(message);
});
- it('should not be dismissible', async () => {
+ it('should not be dismissible', () => {
expect(alert.props('dismissible')).toBe(false);
});
diff --git a/spec/frontend/user_lists/components/user_lists_spec.js b/spec/frontend/user_lists/components/user_lists_spec.js
index 603289ac11e..2da2eb0dd5f 100644
--- a/spec/frontend/user_lists/components/user_lists_spec.js
+++ b/spec/frontend/user_lists/components/user_lists_spec.js
@@ -82,7 +82,7 @@ describe('~/user_lists/components/user_lists.vue', () => {
emptyState = wrapper.findComponent(GlEmptyState);
});
- it('should render the empty state', async () => {
+ it('should render the empty state', () => {
expect(emptyState.exists()).toBe(true);
});
diff --git a/spec/frontend/user_popovers_spec.js b/spec/frontend/user_popovers_spec.js
index 3808cc8b0fc..7fc1bcbc936 100644
--- a/spec/frontend/user_popovers_spec.js
+++ b/spec/frontend/user_popovers_spec.js
@@ -97,7 +97,7 @@ describe('User Popovers', () => {
expect(findPopovers().length).toBe(linksWithUsers.length);
});
- it('for elements added after initial load', async () => {
+ it('for elements added after initial load', () => {
const addedLinks = [createUserLink(), createUserLink()];
addedLinks.forEach((link) => {
document.body.appendChild(link);
@@ -113,7 +113,7 @@ describe('User Popovers', () => {
});
});
- it('does not initialize the popovers for group references', async () => {
+ it('does not initialize the popovers for group references', () => {
const [groupLink] = Array.from(document.querySelectorAll('.js-user-link[data-group]'));
triggerEvent('mouseover', groupLink);
@@ -122,7 +122,7 @@ describe('User Popovers', () => {
expect(findPopovers().length).toBe(0);
});
- it('does not initialize the popovers for @all references', async () => {
+ it('does not initialize the popovers for @all references', () => {
const [projectLink] = Array.from(document.querySelectorAll('.js-user-link[data-project]'));
triggerEvent('mouseover', projectLink);
@@ -131,7 +131,7 @@ describe('User Popovers', () => {
expect(findPopovers().length).toBe(0);
});
- it('does not initialize the user popovers twice for the same element', async () => {
+ it('does not initialize the user popovers twice for the same element', () => {
const [firstUserLink] = findFixtureLinks();
triggerEvent('mouseover', firstUserLink);
jest.runOnlyPendingTimers();
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 4775a0673b5..33647671853 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
@@ -1,10 +1,12 @@
import axios from 'axios';
+import { GlLoadingIcon, GlSprintf } from '@gitlab/ui';
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';
+import MemoryGraph from '~/vue_shared/components/memory_graph.vue';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
const url = '/root/acets-review-apps/environments/15/deployments/1/metrics';
const monitoringUrl = '/root/acets-review-apps/environments/15/metrics';
@@ -35,50 +37,49 @@ const metricsMockData = {
deployment_time: 1493718485,
};
-const createComponent = () => {
- const Component = Vue.extend(MemoryUsage);
-
- return new Component({
- el: document.createElement('div'),
- propsData: {
- metricsUrl: url,
- metricsMonitoringUrl: monitoringUrl,
- memoryMetrics: [],
- deploymentTime: 0,
- hasMetrics: false,
- loadFailed: false,
- loadingMetrics: true,
- backOffRequestCounter: 0,
- },
- });
-};
-
const messages = {
loadingMetrics: 'Loading deployment statistics',
- hasMetrics: 'Memory usage is unchanged at 0MB',
+ hasMetrics: 'Memory usage is unchanged at 0.00MB',
loadFailed: 'Failed to load deployment statistics',
metricsUnavailable: 'Deployment statistics are not available currently',
};
describe('MemoryUsage', () => {
- let vm;
- let el;
+ let wrapper;
let mock;
+ const createComponent = () => {
+ wrapper = shallowMountExtended(MemoryUsage, {
+ propsData: {
+ metricsUrl: url,
+ metricsMonitoringUrl: monitoringUrl,
+ memoryMetrics: [],
+ deploymentTime: 0,
+ hasMetrics: false,
+ loadFailed: false,
+ loadingMetrics: true,
+ backOffRequestCounter: 0,
+ },
+ stubs: {
+ GlSprintf,
+ },
+ });
+ };
+
+ const findGlLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
+ const findUsageInfo = () => wrapper.find('.js-usage-info');
+ const findUsageInfoFailed = () => wrapper.find('.usage-info-failed');
+ const findUsageInfoUnavailable = () => wrapper.find('.usage-info-unavailable');
+ const findMemoryGraph = () => wrapper.findComponent(MemoryGraph);
+
beforeEach(() => {
mock = new MockAdapter(axios);
mock.onGet(`${url}.json`).reply(HTTP_STATUS_OK);
-
- vm = createComponent();
- el = vm.$el;
- });
-
- afterEach(() => {
- mock.restore();
});
describe('data', () => {
it('should have default data', () => {
+ createComponent();
const data = MemoryUsage.data();
expect(Array.isArray(data.memoryMetrics)).toBe(true);
@@ -103,126 +104,182 @@ describe('MemoryUsage', () => {
describe('computed', () => {
describe('memoryChangeMessage', () => {
- it('should contain "increased" if memoryFrom value is less than memoryTo value', () => {
- vm.memoryFrom = 4.28;
- vm.memoryTo = 9.13;
+ it('should contain "increased" if memoryFrom value is less than memoryTo value', async () => {
+ jest.spyOn(MRWidgetService, 'fetchMetrics').mockResolvedValue({
+ data: {
+ ...metricsMockData,
+ metrics: {
+ ...metricsMockData.metrics,
+ memory_after: [
+ {
+ metric: {},
+ value: [1495787020.607, '54858853.130206379'],
+ },
+ ],
+ },
+ },
+ });
- expect(vm.memoryChangeMessage.indexOf('increased')).not.toEqual('-1');
+ createComponent();
+ await waitForPromises();
+
+ expect(findUsageInfo().text().indexOf('increased')).not.toEqual(-1);
});
- it('should contain "decreased" if memoryFrom value is less than memoryTo value', () => {
- vm.memoryFrom = 9.13;
- vm.memoryTo = 4.28;
+ it('should contain "decreased" if memoryFrom value is less than memoryTo value', async () => {
+ jest.spyOn(MRWidgetService, 'fetchMetrics').mockResolvedValue({
+ data: metricsMockData,
+ });
+
+ createComponent();
+ await waitForPromises();
- expect(vm.memoryChangeMessage.indexOf('decreased')).not.toEqual('-1');
+ expect(findUsageInfo().text().indexOf('decreased')).not.toEqual(-1);
});
- it('should contain "unchanged" if memoryFrom value equal to memoryTo value', () => {
- vm.memoryFrom = 1;
- vm.memoryTo = 1;
+ it('should contain "unchanged" if memoryFrom value equal to memoryTo value', async () => {
+ jest.spyOn(MRWidgetService, 'fetchMetrics').mockResolvedValue({
+ data: {
+ ...metricsMockData,
+ metrics: {
+ ...metricsMockData.metrics,
+ memory_after: [
+ {
+ metric: {},
+ value: [1495785220.607, '9572875.906976745'],
+ },
+ ],
+ },
+ },
+ });
+
+ createComponent();
+ await waitForPromises();
- expect(vm.memoryChangeMessage.indexOf('unchanged')).not.toEqual('-1');
+ expect(findUsageInfo().text().indexOf('unchanged')).not.toEqual(-1);
});
});
});
describe('methods', () => {
- const { metrics, deployment_time } = metricsMockData;
+ beforeEach(async () => {
+ jest.spyOn(MRWidgetService, 'fetchMetrics').mockResolvedValue({
+ data: metricsMockData,
+ });
+
+ createComponent();
+ await waitForPromises();
+ });
describe('getMegabytes', () => {
it('should return Megabytes from provided Bytes value', () => {
- const memoryInBytes = '9572875.906976745';
-
- expect(vm.getMegabytes(memoryInBytes)).toEqual('9.13');
+ expect(findUsageInfo().text()).toContain('9.13MB');
});
});
describe('computeGraphData', () => {
it('should populate sparkline graph', () => {
- // ignore BoostrapVue warnings
- jest.spyOn(console, 'warn').mockImplementation();
-
- vm.computeGraphData(metrics, deployment_time);
- const { hasMetrics, memoryMetrics, deploymentTime, memoryFrom, memoryTo } = vm;
-
- expect(hasMetrics).toBe(true);
- expect(memoryMetrics.length).toBeGreaterThan(0);
- expect(deploymentTime).toEqual(deployment_time);
- expect(memoryFrom).toEqual('9.13');
- expect(memoryTo).toEqual('4.28');
+ expect(findMemoryGraph().exists()).toBe(true);
+ expect(findMemoryGraph().props('metrics')).toHaveLength(1);
+ expect(findUsageInfo().text()).toContain('9.13MB');
+ expect(findUsageInfo().text()).toContain('4.28MB');
});
});
describe('loadMetrics', () => {
+ beforeEach(async () => {
+ createComponent();
+ await waitForPromises();
+ });
+
it('should load metrics data using MRWidgetService', async () => {
jest.spyOn(MRWidgetService, 'fetchMetrics').mockResolvedValue({
data: metricsMockData,
});
- jest.spyOn(vm, 'computeGraphData').mockImplementation(() => {});
-
- vm.loadMetrics();
await waitForPromises();
expect(MRWidgetService.fetchMetrics).toHaveBeenCalledWith(url);
- expect(vm.computeGraphData).toHaveBeenCalledWith(metrics, deployment_time);
});
});
});
describe('template', () => {
- it('should render template elements correctly', () => {
- expect(el.classList.contains('mr-memory-usage')).toBe(true);
- expect(el.querySelector('.js-usage-info')).toBeDefined();
- });
+ it('should render template elements correctly', async () => {
+ jest.spyOn(MRWidgetService, 'fetchMetrics').mockResolvedValue({
+ data: metricsMockData,
+ });
- it('should show loading metrics message while metrics are being loaded', async () => {
- vm.loadingMetrics = true;
- vm.hasMetrics = false;
- vm.loadFailed = false;
+ createComponent();
+ await waitForPromises();
- await nextTick();
+ expect(wrapper.classes()).toContain('mr-memory-usage');
+ expect(findUsageInfo().exists()).toBe(true);
+ });
+
+ it('should show loading metrics message while metrics are being loaded', () => {
+ createComponent();
- expect(el.querySelector('.js-usage-info.usage-info-loading')).toBeDefined();
- expect(el.querySelector('.js-usage-info .usage-info-load-spinner')).toBeDefined();
- expect(el.querySelector('.js-usage-info').innerText).toContain(messages.loadingMetrics);
+ expect(findGlLoadingIcon().exists()).toBe(true);
+ expect(findUsageInfo().exists()).toBe(true);
+ expect(findUsageInfo().text()).toBe(messages.loadingMetrics);
});
it('should show deployment memory usage when metrics are loaded', async () => {
- // ignore BoostrapVue warnings
- jest.spyOn(console, 'warn').mockImplementation();
-
- vm.loadingMetrics = false;
- vm.hasMetrics = true;
- vm.loadFailed = false;
- vm.memoryMetrics = metricsMockData.metrics.memory_values[0].values;
+ jest.spyOn(MRWidgetService, 'fetchMetrics').mockResolvedValue({
+ data: {
+ ...metricsMockData,
+ metrics: {
+ ...metricsMockData.metrics,
+ memory_after: [
+ {
+ metric: {},
+ value: [0, '0'],
+ },
+ ],
+ memory_before: [
+ {
+ metric: {},
+ value: [0, '0'],
+ },
+ ],
+ },
+ },
+ });
- await nextTick();
+ createComponent();
+ await waitForPromises();
- expect(el.querySelector('.memory-graph-container')).toBeDefined();
- expect(el.querySelector('.js-usage-info').innerText).toContain(messages.hasMetrics);
+ expect(findMemoryGraph().exists()).toBe(true);
+ expect(findUsageInfo().text()).toBe(messages.hasMetrics);
});
it('should show failure message when metrics loading failed', async () => {
- vm.loadingMetrics = false;
- vm.hasMetrics = false;
- vm.loadFailed = true;
+ jest.spyOn(MRWidgetService, 'fetchMetrics').mockRejectedValue({});
- await nextTick();
+ createComponent();
+ await waitForPromises();
- expect(el.querySelector('.js-usage-info.usage-info-failed')).toBeDefined();
- expect(el.querySelector('.js-usage-info').innerText).toContain(messages.loadFailed);
+ expect(findUsageInfoFailed().exists()).toBe(true);
+ expect(findUsageInfo().text()).toBe(messages.loadFailed);
});
it('should show metrics unavailable message when metrics loading failed', async () => {
- vm.loadingMetrics = false;
- vm.hasMetrics = false;
- vm.loadFailed = false;
+ jest.spyOn(MRWidgetService, 'fetchMetrics').mockResolvedValue({
+ data: {
+ ...metricsMockData,
+ metrics: {
+ ...metricsMockData.metrics,
+ memory_values: [],
+ },
+ },
+ });
- await nextTick();
+ createComponent();
+ await waitForPromises();
- expect(el.querySelector('.js-usage-info.usage-info-unavailable')).toBeDefined();
- expect(el.querySelector('.js-usage-info').innerText).toContain(messages.metricsUnavailable);
+ expect(findUsageInfoUnavailable().exists()).toBe(true);
+ expect(findUsageInfo().text()).toBe(messages.metricsUnavailable);
});
});
});
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 f284ec98a73..9bd46267daa 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
@@ -1,56 +1,101 @@
+import Vue, { nextTick } from 'vue';
+import VueApollo from 'vue-apollo';
import { GlModal } from '@gitlab/ui';
-import { mount } from '@vue/test-utils';
-import { nextTick } from 'vue';
+import BoldText from '~/vue_merge_request_widget/components/bold_text.vue';
import WidgetRebase from '~/vue_merge_request_widget/components/states/mr_widget_rebase.vue';
+import rebaseQuery from '~/vue_merge_request_widget/queries/states/rebase.query.graphql';
import eventHub from '~/vue_merge_request_widget/event_hub';
+import StateContainer from '~/vue_merge_request_widget/components/state_container.vue';
import toast from '~/vue_shared/plugins/global_toast';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import { stubComponent } from 'helpers/stub_component';
jest.mock('~/vue_shared/plugins/global_toast');
let wrapper;
-
-function createWrapper(propsData, provideData) {
- wrapper = mount(WidgetRebase, {
- provide: {
- ...provideData,
+const showMock = jest.fn();
+
+const mockPipelineNodes = [
+ {
+ id: '1',
+ project: {
+ id: '2',
+ fullPath: 'user/forked',
},
- propsData,
- data() {
- return {
- state: {
- rebaseInProgress: propsData.mr.rebaseInProgress,
- targetBranch: propsData.mr.targetBranch,
+ },
+];
+
+const mockQueryHandler = ({
+ rebaseInProgress = false,
+ targetBranch = '',
+ pushToSourceBranch = false,
+ nodes = mockPipelineNodes,
+} = {}) =>
+ jest.fn().mockResolvedValue({
+ data: {
+ project: {
+ id: '1',
+ mergeRequest: {
+ id: '2',
+ rebaseInProgress,
+ targetBranch,
userPermissions: {
- pushToSourceBranch: propsData.mr.canPushToSourceBranch,
+ pushToSourceBranch,
+ },
+ pipelines: {
+ nodes,
},
- pipelines: propsData.mr.pipelines,
},
- };
+ },
+ },
+ });
+
+const createMockApolloProvider = (handler) => {
+ Vue.use(VueApollo);
+
+ return createMockApollo([[rebaseQuery, handler]]);
+};
+
+function createWrapper({ propsData = {}, provideData = {}, handler = mockQueryHandler() } = {}) {
+ wrapper = shallowMountExtended(WidgetRebase, {
+ apolloProvider: createMockApolloProvider(handler),
+ provide: {
+ ...provideData,
},
- mocks: {
- $apollo: {
- queries: {
- state: { loading: false },
+ propsData: {
+ mr: {},
+ service: {},
+ ...propsData,
+ },
+ stubs: {
+ StateContainer,
+ GlModal: stubComponent(GlModal, {
+ methods: {
+ show: showMock,
},
- },
+ }),
},
});
}
describe('Merge request widget rebase component', () => {
- const findRebaseMessage = () => wrapper.find('[data-testid="rebase-message"]');
+ const findRebaseMessage = () => wrapper.findByTestId('rebase-message');
+ const findBoldText = () => wrapper.findComponent(BoldText);
const findRebaseMessageText = () => findRebaseMessage().text();
- const findStandardRebaseButton = () => wrapper.find('[data-testid="standard-rebase-button"]');
- const findRebaseWithoutCiButton = () => wrapper.find('[data-testid="rebase-without-ci-button"]');
+ const findStandardRebaseButton = () => wrapper.findByTestId('standard-rebase-button');
+ const findRebaseWithoutCiButton = () => wrapper.findByTestId('rebase-without-ci-button');
const findModal = () => wrapper.findComponent(GlModal);
describe('while rebasing', () => {
- it('should show progress message', () => {
+ it('should show progress message', async () => {
createWrapper({
- mr: { rebaseInProgress: true },
- service: {},
+ handler: mockQueryHandler({ rebaseInProgress: true }),
});
+ await waitForPromises();
+
expect(findRebaseMessageText()).toContain('Rebase in progress');
});
});
@@ -59,95 +104,110 @@ describe('Merge request widget rebase component', () => {
const rebaseMock = jest.fn().mockResolvedValue();
const pollMock = jest.fn().mockResolvedValue({});
- it('renders the warning message', () => {
+ it('renders the warning message', async () => {
createWrapper({
- mr: {
+ handler: mockQueryHandler({
rebaseInProgress: false,
- canPushToSourceBranch: true,
- },
- service: {
- rebase: rebaseMock,
- poll: pollMock,
- },
+ pushToSourceBranch: false,
+ }),
});
- const text = findRebaseMessageText();
+ await waitForPromises();
- expect(text).toContain('Merge blocked');
- expect(text.replace(/\s\s+/g, ' ')).toContain(
+ expect(findBoldText().props('message')).toContain('Merge blocked');
+ expect(findBoldText().props('message').replace(/\s\s+/g, ' ')).toContain(
'the source branch must be rebased onto the target branch',
);
});
it('renders an error message when rebasing has failed', async () => {
createWrapper({
- mr: {
- rebaseInProgress: false,
- canPushToSourceBranch: true,
- },
- service: {
- rebase: rebaseMock,
- poll: pollMock,
+ propsData: {
+ service: {
+ rebase: jest.fn().mockRejectedValue({
+ response: {
+ data: {
+ merge_error: 'Something went wrong!',
+ },
+ },
+ }),
+ },
},
+ handler: mockQueryHandler({ pushToSourceBranch: true }),
});
+ await waitForPromises();
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({ rebasingError: 'Something went wrong!' });
+ findStandardRebaseButton().vm.$emit('click');
- await nextTick();
+ await waitForPromises();
expect(findRebaseMessageText()).toContain('Something went wrong!');
});
describe('Rebase buttons', () => {
- beforeEach(() => {
+ it('renders both buttons', async () => {
createWrapper({
- mr: {
- rebaseInProgress: false,
- canPushToSourceBranch: true,
- },
- service: {
- rebase: rebaseMock,
- poll: pollMock,
- },
+ handler: mockQueryHandler({ pushToSourceBranch: true }),
});
- });
- it('renders both buttons', () => {
+ await waitForPromises();
+
expect(findRebaseWithoutCiButton().exists()).toBe(true);
expect(findStandardRebaseButton().exists()).toBe(true);
});
it('starts the rebase when clicking', async () => {
- findStandardRebaseButton().vm.$emit('click');
+ createWrapper({
+ propsData: {
+ service: {
+ rebase: rebaseMock,
+ poll: pollMock,
+ },
+ },
+ handler: mockQueryHandler({ pushToSourceBranch: true }),
+ });
- await nextTick();
+ await waitForPromises();
+
+ findStandardRebaseButton().vm.$emit('click');
expect(rebaseMock).toHaveBeenCalledWith({ skipCi: false });
});
it('starts the CI-skipping rebase when clicking on "Rebase without CI"', async () => {
- findRebaseWithoutCiButton().vm.$emit('click');
+ createWrapper({
+ propsData: {
+ service: {
+ rebase: rebaseMock,
+ poll: pollMock,
+ },
+ },
+ handler: mockQueryHandler({ pushToSourceBranch: true }),
+ });
- await nextTick();
+ await waitForPromises();
+
+ findRebaseWithoutCiButton().vm.$emit('click');
expect(rebaseMock).toHaveBeenCalledWith({ skipCi: true });
});
});
describe('Rebase when pipelines must succeed is enabled', () => {
- beforeEach(() => {
+ beforeEach(async () => {
createWrapper({
- mr: {
- rebaseInProgress: false,
- canPushToSourceBranch: true,
- onlyAllowMergeIfPipelineSucceeds: true,
- },
- service: {
- rebase: rebaseMock,
- poll: pollMock,
+ propsData: {
+ mr: {
+ onlyAllowMergeIfPipelineSucceeds: true,
+ },
+ service: {
+ rebase: rebaseMock,
+ poll: pollMock,
+ },
},
+ handler: mockQueryHandler({ pushToSourceBranch: true }),
});
+
+ await waitForPromises();
});
it('renders only the rebase button', () => {
@@ -165,19 +225,22 @@ describe('Merge request widget rebase component', () => {
});
describe('Rebase when pipelines must succeed and skipped pipelines are considered successful are enabled', () => {
- beforeEach(() => {
+ beforeEach(async () => {
createWrapper({
- mr: {
- rebaseInProgress: false,
- canPushToSourceBranch: true,
- onlyAllowMergeIfPipelineSucceeds: true,
- allowMergeOnSkippedPipeline: true,
- },
- service: {
- rebase: rebaseMock,
- poll: pollMock,
+ propsData: {
+ mr: {
+ onlyAllowMergeIfPipelineSucceeds: true,
+ allowMergeOnSkippedPipeline: true,
+ },
+ service: {
+ rebase: rebaseMock,
+ poll: pollMock,
+ },
},
+ handler: mockQueryHandler({ pushToSourceBranch: true }),
});
+
+ await waitForPromises();
});
it('renders both rebase buttons', () => {
@@ -203,51 +266,36 @@ describe('Merge request widget rebase component', () => {
});
describe('security modal', () => {
- it('displays modal and rebases after confirming', () => {
- createWrapper(
- {
+ it('displays modal and rebases after confirming', async () => {
+ createWrapper({
+ propsData: {
mr: {
- rebaseInProgress: false,
- canPushToSourceBranch: true,
sourceProjectFullPath: 'user/forked',
targetProjectFullPath: 'root/original',
- pipelines: {
- nodes: [
- {
- id: '1',
- project: {
- id: '2',
- fullPath: 'user/forked',
- },
- },
- ],
- },
},
service: {
rebase: rebaseMock,
poll: pollMock,
},
},
- { canCreatePipelineInTargetProject: true },
- );
+ provideData: { canCreatePipelineInTargetProject: true },
+ handler: mockQueryHandler({ pushToSourceBranch: true }),
+ });
- findModal().vm.show = jest.fn();
+ await waitForPromises();
findStandardRebaseButton().vm.$emit('click');
-
- expect(findModal().vm.show).toHaveBeenCalled();
+ expect(showMock).toHaveBeenCalled();
findModal().vm.$emit('primary');
expect(rebaseMock).toHaveBeenCalled();
});
- it('does not display modal', () => {
- createWrapper(
- {
+ it('does not display modal', async () => {
+ createWrapper({
+ propsData: {
mr: {
- rebaseInProgress: false,
- canPushToSourceBranch: true,
sourceProjectFullPath: 'user/forked',
targetProjectFullPath: 'root/original',
},
@@ -256,14 +304,15 @@ describe('Merge request widget rebase component', () => {
poll: pollMock,
},
},
- { canCreatePipelineInTargetProject: false },
- );
+ provideData: { canCreatePipelineInTargetProject: false },
+ handler: mockQueryHandler({ pushToSourceBranch: true }),
+ });
- findModal().vm.show = jest.fn();
+ await waitForPromises();
findStandardRebaseButton().vm.$emit('click');
- expect(findModal().vm.show).not.toHaveBeenCalled();
+ expect(showMock).not.toHaveBeenCalled();
expect(rebaseMock).toHaveBeenCalled();
});
});
@@ -273,42 +322,41 @@ describe('Merge request widget rebase component', () => {
const exampleTargetBranch = 'fake-branch-to-test-with';
describe('UI text', () => {
- beforeEach(() => {
+ beforeEach(async () => {
createWrapper({
- mr: {
- rebaseInProgress: false,
- canPushToSourceBranch: false,
+ handler: mockQueryHandler({
+ pushToSourceBranch: false,
targetBranch: exampleTargetBranch,
- },
- service: {},
+ }),
});
+
+ await waitForPromises();
});
it('renders a message explaining user does not have permissions', () => {
- const text = findRebaseMessageText();
-
- expect(text).toContain('Merge blocked:');
- expect(text).toContain('the source branch must be rebased');
+ expect(findBoldText().props('message')).toContain('Merge blocked');
+ expect(findBoldText().props('message')).toContain('the source branch must be rebased');
});
it('renders the correct target branch name', () => {
- const text = findRebaseMessageText();
-
- expect(text).toContain('Merge blocked:');
- expect(text).toContain('the source branch must be rebased onto the target branch.');
+ expect(findBoldText().props('message')).toContain('Merge blocked:');
+ expect(findBoldText().props('message')).toContain(
+ 'the source branch must be rebased onto the target branch.',
+ );
});
});
- it('does render the "Rebase without pipeline" button', () => {
+ it('does render the "Rebase without pipeline" button', async () => {
createWrapper({
- mr: {
+ handler: mockQueryHandler({
rebaseInProgress: false,
- canPushToSourceBranch: false,
+ pushToSourceBranch: false,
targetBranch: exampleTargetBranch,
- },
- service: {},
+ }),
});
+ await waitForPromises();
+
expect(findRebaseWithoutCiButton().exists()).toBe(true);
});
});
@@ -317,24 +365,27 @@ describe('Merge request widget rebase component', () => {
it('checkRebaseStatus', async () => {
jest.spyOn(eventHub, '$emit').mockImplementation(() => {});
createWrapper({
- mr: {},
- service: {
- rebase() {
- return Promise.resolve();
- },
- poll() {
- return Promise.resolve({
- data: {
- rebase_in_progress: false,
- should_be_rebased: false,
- merge_error: null,
- },
- });
+ propsData: {
+ service: {
+ rebase() {
+ return Promise.resolve();
+ },
+ poll() {
+ return Promise.resolve({
+ data: {
+ rebase_in_progress: false,
+ should_be_rebased: false,
+ merge_error: null,
+ },
+ });
+ },
},
},
});
- wrapper.vm.rebase();
+ await waitForPromises();
+
+ findRebaseWithoutCiButton().vm.$emit('click');
// Wait for the rebase request
await nextTick();
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 ca75ca11e5b..85acd5f9a9e 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
@@ -10,6 +10,8 @@ jest.mock('~/lib/utils/simple_poll', () =>
describe('MRWidgetMerging', () => {
let wrapper;
+ const pollMock = jest.fn().mockResolvedValue();
+
const GlEmoji = { template: '<img />' };
beforeEach(() => {
wrapper = shallowMount(MrWidgetMerging, {
@@ -20,7 +22,7 @@ describe('MRWidgetMerging', () => {
transitionStateMachine() {},
},
service: {
- poll: jest.fn().mockResolvedValue(),
+ poll: pollMock,
},
},
stubs: {
@@ -36,17 +38,11 @@ describe('MRWidgetMerging', () => {
describe('initiateMergePolling', () => {
it('should call simplePoll', () => {
- wrapper.vm.initiateMergePolling();
-
expect(simplePoll).toHaveBeenCalledWith(expect.any(Function), { timeout: 0 });
});
it('should call handleMergePolling', () => {
- jest.spyOn(wrapper.vm, 'handleMergePolling').mockImplementation(() => {});
-
- wrapper.vm.initiateMergePolling();
-
- expect(wrapper.vm.handleMergePolling).toHaveBeenCalled();
+ expect(pollMock).toHaveBeenCalled();
});
});
});
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 1e4e089e7c1..07fc0be9e51 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
@@ -113,6 +113,11 @@ const createComponent = (customConfig = {}, createState = true) => {
GlSprintf,
},
apolloProvider: createMockApollo([[readyToMergeQuery, readyToMergeResponseSpy]]),
+ provide: {
+ glFeatures: {
+ autoMergeLabelsMrWidget: false,
+ },
+ },
});
};
@@ -596,7 +601,7 @@ describe('ReadyToMerge', () => {
describe('commits edit components', () => {
describe('when fast-forward merge is enabled', () => {
- it('should not be rendered if squash is disabled', async () => {
+ it('should not be rendered if squash is disabled', () => {
createComponent({
mr: {
ffOnlyEnabled: true,
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 58b9f162815..19825318a4f 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
@@ -4,12 +4,19 @@ import { removeBreakLine } from 'helpers/text_helper';
import notesEventHub from '~/notes/event_hub';
import UnresolvedDiscussions from '~/vue_merge_request_widget/components/states/unresolved_discussions.vue';
-function createComponent({ path = '' } = {}) {
+function createComponent({ path = '', propsData = {}, provide = {} } = {}) {
return mount(UnresolvedDiscussions, {
propsData: {
mr: {
createIssueToResolveDiscussionsPath: path,
},
+ ...propsData,
+ },
+ provide: {
+ glFeatures: {
+ hideCreateIssueResolveAll: false,
+ },
+ ...provide,
},
});
}
@@ -21,7 +28,7 @@ describe('UnresolvedDiscussions', () => {
wrapper = createComponent();
});
- it('triggers the correct notes event when the jump to first unresolved discussion button is clicked', () => {
+ it('triggers the correct notes event when the go to first unresolved discussion button is clicked', () => {
jest.spyOn(notesEventHub, '$emit');
wrapper.find('[data-testid="jump-to-first"]').trigger('click');
@@ -39,8 +46,8 @@ describe('UnresolvedDiscussions', () => {
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');
+ expect(wrapper.element.innerText).toContain('Resolve all with new issue');
+ expect(wrapper.element.innerText).toContain('Go to first unresolved thread');
expect(wrapper.element.querySelector('.js-create-issue').getAttribute('href')).toEqual(
TEST_HOST,
);
@@ -53,9 +60,26 @@ describe('UnresolvedDiscussions', () => {
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');
+ expect(wrapper.element.innerText).not.toContain('Resolve all with new issue');
+ expect(wrapper.element.innerText).toContain('Go to first unresolved thread');
expect(wrapper.element.querySelector('.js-create-issue')).toEqual(null);
});
});
+
+ describe('when `hideCreateIssueResolveAll` is enabled', () => {
+ beforeEach(() => {
+ wrapper = createComponent({
+ path: TEST_HOST,
+ provide: {
+ glFeatures: {
+ hideCreateIssueResolveAll: true,
+ },
+ },
+ });
+ });
+
+ it('do not show jump to first button', () => {
+ expect(wrapper.text()).not.toContain('Create issue to resolve all threads');
+ });
+ });
});
diff --git a/spec/frontend/vue_merge_request_widget/components/widget/__snapshots__/dynamic_content_spec.js.snap b/spec/frontend/vue_merge_request_widget/components/widget/__snapshots__/dynamic_content_spec.js.snap
index e9a34453930..3947c50fe4e 100644
--- a/spec/frontend/vue_merge_request_widget/components/widget/__snapshots__/dynamic_content_spec.js.snap
+++ b/spec/frontend/vue_merge_request_widget/components/widget/__snapshots__/dynamic_content_spec.js.snap
@@ -1,35 +1,63 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`~/vue_merge_request_widget/components/widget/dynamic_content.vue renders given data 1`] = `
-"<content-row-stub level=\\"2\\" statusiconname=\\"success\\" widgetname=\\"MyWidget\\" header=\\"This is a header,This is a subheader\\" helppopover=\\"[object Object]\\" actionbuttons=\\"\\">
- <div class=\\"gl-display-flex gl-flex-direction-column\\">
- <div>
- <p class=\\"gl-mb-0\\">Main text for the row</p>
- <gl-link-stub href=\\"https://gitlab.com\\">Optional link to display after text</gl-link-stub>
- <!---->
- <gl-badge-stub size=\\"md\\" variant=\\"info\\" iconsize=\\"md\\">
- Badge is optional. Text to be displayed inside badge
- </gl-badge-stub>
- <actions-stub widget=\\"MyWidget\\" tertiarybuttons=\\"\\" class=\\"gl-ml-auto gl-pl-3\\"></actions-stub>
- <p class=\\"gl-m-0 gl-font-sm\\">Optional: Smaller sub-text to be displayed below the main text</p>
+"<div class=\\"gl-w-full gl-display-flex gl-border-t gl-py-3 gl-pl-7 gl-align-items-baseline\\">
+ <!---->
+ <div class=\\"gl-w-full\\">
+ <div class=\\"gl-display-flex\\">
+ <div class=\\"gl-mb-2\\"><strong class=\\"gl-display-block\\">This is a header</strong><span class=\\"gl-display-block\\">This is a subheader</span></div>
+ <div class=\\"gl-ml-auto gl-display-flex gl-align-items-baseline\\">
+ <help-popover-stub options=\\"[object Object]\\" icon=\\"information-o\\" class=\\"\\">
+ <p class=\\"gl-mb-0\\">Widget help popover content</p>
+ <!---->
+ </help-popover-stub>
+ <!---->
+ </div>
</div>
- <ul class=\\"gl-m-0 gl-p-0 gl-list-style-none\\">
- <li>
- <content-row-stub level=\\"3\\" statusiconname=\\"\\" widgetname=\\"MyWidget\\" header=\\"Child row header\\" actionbuttons=\\"\\" data-qa-selector=\\"child_content\\">
- <div class=\\"gl-display-flex gl-flex-direction-column\\">
- <div>
- <p class=\\"gl-mb-0\\">This is recursive. It will be listed in level 3.</p>
- <!---->
- <!---->
- <!---->
- <actions-stub widget=\\"MyWidget\\" tertiarybuttons=\\"\\" class=\\"gl-ml-auto gl-pl-3\\"></actions-stub>
+ <div class=\\"gl-display-flex gl-align-items-baseline gl-w-full\\">
+ <status-icon-stub level=\\"2\\" name=\\"MyWidget\\" iconname=\\"success\\"></status-icon-stub>
+ <div class=\\"gl-display-flex gl-flex-direction-column\\">
+ <div>
+ <p class=\\"gl-mb-0\\">Main text for the row</p>
+ <gl-link-stub href=\\"https://gitlab.com\\">Optional link to display after text</gl-link-stub>
+ <!---->
+ <gl-badge-stub size=\\"md\\" variant=\\"info\\" iconsize=\\"md\\">
+ Badge is optional. Text to be displayed inside badge
+ </gl-badge-stub>
+ <actions-stub widget=\\"MyWidget\\" tertiarybuttons=\\"\\" class=\\"gl-ml-auto gl-pl-3\\"></actions-stub>
+ <p class=\\"gl-m-0 gl-font-sm\\">Optional: Smaller sub-text to be displayed below the main text</p>
+ </div>
+ <ul class=\\"gl-m-0 gl-p-0 gl-list-style-none\\">
+ <li>
+ <div class=\\"gl-w-full gl-display-flex gl-align-items-center\\" data-qa-selector=\\"child_content\\">
<!---->
+ <div class=\\"gl-w-full\\">
+ <div class=\\"gl-display-flex\\">
+ <div class=\\"gl-mb-2\\"><strong class=\\"gl-display-block\\">Child row header</strong>
+ <!---->
+ </div>
+ <!---->
+ </div>
+ <div class=\\"gl-display-flex gl-align-items-baseline gl-w-full\\">
+ <!---->
+ <div class=\\"gl-display-flex gl-flex-direction-column\\">
+ <div>
+ <p class=\\"gl-mb-0\\">This is recursive. It will be listed in level 3.</p>
+ <!---->
+ <!---->
+ <!---->
+ <actions-stub widget=\\"MyWidget\\" tertiarybuttons=\\"\\" class=\\"gl-ml-auto gl-pl-3\\"></actions-stub>
+ <!---->
+ </div>
+ <!---->
+ </div>
+ </div>
+ </div>
</div>
- <!---->
- </div>
- </content-row-stub>
- </li>
- </ul>
+ </li>
+ </ul>
+ </div>
+ </div>
</div>
-</content-row-stub>"
+</div>"
`;
diff --git a/spec/frontend/vue_merge_request_widget/components/widget/dynamic_content_spec.js b/spec/frontend/vue_merge_request_widget/components/widget/dynamic_content_spec.js
index 527e800ddcf..16751bcc0f0 100644
--- a/spec/frontend/vue_merge_request_widget/components/widget/dynamic_content_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/widget/dynamic_content_spec.js
@@ -1,6 +1,7 @@
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { EXTENSION_ICONS } from '~/vue_merge_request_widget/constants';
import DynamicContent from '~/vue_merge_request_widget/components/widget/dynamic_content.vue';
+import ContentRow from '~/vue_merge_request_widget/components/widget/widget_content_row.vue';
describe('~/vue_merge_request_widget/components/widget/dynamic_content.vue', () => {
let wrapper;
@@ -13,6 +14,7 @@ describe('~/vue_merge_request_widget/components/widget/dynamic_content.vue', ()
},
stubs: {
DynamicContent,
+ ContentRow,
},
});
};
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 5887670a58d..4972c522733 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
@@ -3,6 +3,7 @@ import * as Sentry from '@sentry/browser';
import { shallowMountExtended, mountExtended } from 'helpers/vue_test_utils_helper';
import HelpPopover from '~/vue_shared/components/help_popover.vue';
import waitForPromises from 'helpers/wait_for_promises';
+import { assertProps } from 'helpers/assert_props';
import StatusIcon from '~/vue_merge_request_widget/components/extensions/status_icon.vue';
import ActionButtons from '~/vue_merge_request_widget/components/widget/action_buttons.vue';
import Widget from '~/vue_merge_request_widget/components/widget/widget.vue';
@@ -111,9 +112,7 @@ describe('~/vue_merge_request_widget/components/widget/widget.vue', () => {
it('validates widget name', () => {
expect(() => {
- createComponent({
- propsData: { widgetName: 'InvalidWidgetName' },
- });
+ assertProps(Widget, { widgetName: 'InvalidWidgetName' });
}).toThrow();
});
});
@@ -121,7 +120,7 @@ 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: HTTP_STATUS_OK, data: { vulnerabilities: [] } };
- createComponent({ propsData: { fetchCollapsedData: async () => mockData } });
+ createComponent({ propsData: { fetchCollapsedData: () => Promise.resolve(mockData) } });
await waitForPromises();
expect(wrapper.emitted('input')[0][0]).toEqual({ collapsed: mockData.data, expanded: null });
});
@@ -287,6 +286,21 @@ describe('~/vue_merge_request_widget/components/widget/widget.vue', () => {
expect(findExpandedSection().text()).toBe('More complex content');
});
+ it('emits a toggle even when button is toggled', () => {
+ createComponent({
+ propsData: {
+ isCollapsible: true,
+ },
+ slots: {
+ content: '<b>More complex content</b>',
+ },
+ });
+
+ expect(findExpandedSection().exists()).toBe(false);
+ findToggleButton().vm.$emit('click');
+ expect(wrapper.emitted('toggle')).toEqual([[{ expanded: true }]]);
+ });
+
it('does not display the toggle button if isCollapsible is false', () => {
createComponent({
propsData: {
diff --git a/spec/frontend/vue_merge_request_widget/extentions/accessibility/index_spec.js b/spec/frontend/vue_merge_request_widget/extentions/accessibility/index_spec.js
index 40158917f52..9b1e694d9c4 100644
--- a/spec/frontend/vue_merge_request_widget/extentions/accessibility/index_spec.js
+++ b/spec/frontend/vue_merge_request_widget/extentions/accessibility/index_spec.js
@@ -101,7 +101,7 @@ describe('Accessibility extension', () => {
await waitForPromises();
});
- it('displays all report list items in viewport', async () => {
+ it('displays all report list items in viewport', () => {
expect(findAllExtensionListItems()).toHaveLength(7);
});
diff --git a/spec/frontend/vue_merge_request_widget/extentions/code_quality/index_spec.js b/spec/frontend/vue_merge_request_widget/extentions/code_quality/index_spec.js
index 4b7870842bd..1478b3ac098 100644
--- a/spec/frontend/vue_merge_request_widget/extentions/code_quality/index_spec.js
+++ b/spec/frontend/vue_merge_request_widget/extentions/code_quality/index_spec.js
@@ -184,7 +184,7 @@ describe('Code Quality extension', () => {
await waitForPromises();
});
- it('displays all report list items in viewport', async () => {
+ it('displays all report list items in viewport', () => {
expect(findAllExtensionListItems()).toHaveLength(2);
});
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 52a244107bd..5baed8ff211 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
@@ -82,7 +82,7 @@ describe('Terraform extension', () => {
${'2 valid reports'} | ${{ 0: validPlanWithName, 1: validPlanWithName }} | ${'2 Terraform reports were generated in your pipelines'} | ${''}
${'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 () => {
+ beforeEach(() => {
mockPollingApi(HTTP_STATUS_OK, response, {});
return createComponent();
});
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 fad501ee7f5..132d64226e1 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
@@ -470,15 +470,15 @@ describe('MrWidgetOptions', () => {
});
it('should call setFavicon method', async () => {
- wrapper.vm.mr.ciStatusFaviconPath = overlayDataUrl;
+ wrapper.vm.mr.faviconOverlayPath = overlayDataUrl;
await wrapper.vm.setFaviconHelper();
expect(setFaviconOverlay).toHaveBeenCalledWith(overlayDataUrl);
});
- it('should not call setFavicon when there is no ciStatusFaviconPath', async () => {
- wrapper.vm.mr.ciStatusFaviconPath = null;
+ it('should not call setFavicon when there is no faviconOverlayPath', async () => {
+ wrapper.vm.mr.faviconOverlayPath = null;
await wrapper.vm.setFaviconHelper();
expect(faviconElement.getAttribute('href')).toEqual(null);
});
@@ -838,7 +838,7 @@ describe('MrWidgetOptions', () => {
});
describe('security widget', () => {
- const setup = async (hasPipeline) => {
+ const setup = (hasPipeline) => {
const mrData = {
...mockData,
...(hasPipeline ? {} : { pipeline: null }),
@@ -853,7 +853,9 @@ describe('MrWidgetOptions', () => {
apolloMock: [
[
securityReportMergeRequestDownloadPathsQuery,
- async () => ({ data: securityReportMergeRequestDownloadPathsQueryResponse }),
+ jest
+ .fn()
+ .mockResolvedValue({ data: securityReportMergeRequestDownloadPathsQueryResponse }),
],
],
});
@@ -1197,33 +1199,6 @@ describe('MrWidgetOptions', () => {
'i_code_review_merge_request_widget_test_extension_count_expand_warning',
);
});
-
- it.each`
- widgetName | nonStandardEvent
- ${'WidgetCodeQuality'} | ${'i_testing_code_quality_widget_total'}
- ${'WidgetTerraform'} | ${'i_testing_terraform_widget_total'}
- ${'WidgetIssues'} | ${'i_testing_issues_widget_total'}
- ${'WidgetTestSummary'} | ${'i_testing_summary_widget_total'}
- `(
- "sends non-standard events for the '$widgetName' widget",
- async ({ widgetName, nonStandardEvent }) => {
- const definition = {
- ...workingExtension(),
- name: widgetName,
- };
-
- registerExtension(definition);
- createComponent();
-
- await waitForPromises();
-
- api.trackRedisHllUserEvent.mockClear();
-
- findExtensionToggleButton().trigger('click');
-
- expect(api.trackRedisHllUserEvent).toHaveBeenCalledWith(nonStandardEvent);
- },
- );
});
it('triggers the "full report clicked" events when the appropriate button is clicked', () => {
diff --git a/spec/frontend/vue_shared/alert_details/alert_details_spec.js b/spec/frontend/vue_shared/alert_details/alert_details_spec.js
index 3bc191d988f..6c2b21053f0 100644
--- a/spec/frontend/vue_shared/alert_details/alert_details_spec.js
+++ b/spec/frontend/vue_shared/alert_details/alert_details_spec.js
@@ -86,12 +86,10 @@ describe('AlertDetails', () => {
});
afterEach(() => {
- if (wrapper) {
- wrapper.destroy();
- }
mock.restore();
});
+ const findTabs = () => wrapper.findByTestId('alertDetailsTabs');
const findCreateIncidentBtn = () => wrapper.findByTestId('createIncidentBtn');
const findViewIncidentBtn = () => wrapper.findByTestId('viewIncidentBtn');
const findIncidentCreationAlert = () => wrapper.findByTestId('incidentCreationError');
@@ -107,7 +105,7 @@ describe('AlertDetails', () => {
});
it('shows an empty state', () => {
- expect(wrapper.findByTestId('alertDetailsTabs').exists()).toBe(false);
+ expect(findTabs().exists()).toBe(false);
});
});
@@ -349,9 +347,7 @@ describe('AlertDetails', () => {
${1} | ${'metrics'}
${2} | ${'activity'}
`('will navigate to the correct tab via $tabId', ({ index, tabId }) => {
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({ currentTabIndex: index });
+ findTabs().vm.$emit('input', index);
expect($router.push).toHaveBeenCalledWith({ name: 'tab', params: { tabId } });
});
});
diff --git a/spec/frontend/vue_shared/alert_details/alert_metrics_spec.js b/spec/frontend/vue_shared/alert_details/alert_metrics_spec.js
deleted file mode 100644
index 9d84a535d67..00000000000
--- a/spec/frontend/vue_shared/alert_details/alert_metrics_spec.js
+++ /dev/null
@@ -1,63 +0,0 @@
-import { shallowMount } from '@vue/test-utils';
-import axios from 'axios';
-import MockAdapter from 'axios-mock-adapter';
-import { nextTick } from 'vue';
-import waitForPromises from 'helpers/wait_for_promises';
-import MetricEmbed from '~/monitoring/components/embeds/metric_embed.vue';
-import AlertMetrics from '~/vue_shared/alert_details/components/alert_metrics.vue';
-
-jest.mock('~/monitoring/stores', () => ({
- monitoringDashboard: {},
-}));
-
-jest.mock('~/monitoring/components/embeds/metric_embed.vue', () => ({
- render(h) {
- return h('div');
- },
-}));
-
-describe('Alert Metrics', () => {
- let wrapper;
- const mock = new MockAdapter(axios);
-
- function mountComponent({ props } = {}) {
- wrapper = shallowMount(AlertMetrics, {
- propsData: {
- ...props,
- },
- });
- }
-
- const findChart = () => wrapper.findComponent(MetricEmbed);
- const findEmptyState = () => wrapper.findComponent({ ref: 'emptyState' });
-
- afterEach(() => {
- if (wrapper) {
- wrapper.destroy();
- }
- });
-
- afterAll(() => {
- mock.restore();
- });
-
- describe('Empty state', () => {
- it('should display a message when metrics dashboard url is not provided', () => {
- mountComponent();
- expect(findChart().exists()).toBe(false);
- expect(findEmptyState().text()).toBe("Metrics weren't available in the alerts payload.");
- });
- });
-
- describe('Chart', () => {
- it('should be rendered when dashboard url is provided', async () => {
- mountComponent({ props: { dashboardUrl: 'metrics.url' } });
-
- await waitForPromises();
- await nextTick();
-
- expect(findEmptyState().exists()).toBe(false);
- expect(findChart().exists()).toBe(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 98a357bac2b..bf4435fae45 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
@@ -1,21 +1,28 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
import { GlDropdownItem } from '@gitlab/ui';
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';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
import mockAlerts from '../mocks/alerts.json';
const mockAlert = mockAlerts[0];
describe('Alert Details Sidebar Assignees', () => {
let wrapper;
+ let requestHandlers;
let mock;
const mockPath = '/-/autocomplete/users.json';
+ const mockUrlRoot = '/gitlab';
+ const expectedUrl = `${mockUrlRoot}${mockPath}`;
+
const mockUsers = [
{
avatar_url:
@@ -40,81 +47,64 @@ describe('Alert Details Sidebar Assignees', () => {
const findSidebarIcon = () => wrapper.findByTestId('assignees-icon');
const findUnassigned = () => wrapper.findByTestId('unassigned-users');
+ const mockDefaultHandler = (errors = []) =>
+ jest.fn().mockResolvedValue({
+ data: {
+ issuableSetAssignees: {
+ errors,
+ issuable: {
+ id: 'id',
+ iid: 'iid',
+ assignees: {
+ nodes: [],
+ },
+ notes: {
+ nodes: [],
+ },
+ },
+ },
+ },
+ });
+ const createMockApolloProvider = (handlers) => {
+ Vue.use(VueApollo);
+ requestHandlers = handlers;
+
+ return createMockApollo([[AlertSetAssignees, handlers]]);
+ };
+
function mountComponent({
- data,
- users = [],
- isDropdownSearching = false,
+ props,
sidebarCollapsed = true,
- loading = false,
- stubs = {},
+ handlers = mockDefaultHandler(),
} = {}) {
wrapper = shallowMountExtended(SidebarAssignees, {
- data() {
- return {
- users,
- isDropdownSearching,
- };
- },
+ apolloProvider: createMockApolloProvider(handlers),
propsData: {
alert: { ...mockAlert },
- ...data,
+ ...props,
sidebarCollapsed,
projectPath: 'projectPath',
projectId: '1',
},
- mocks: {
- $apollo: {
- mutate: jest.fn(),
- queries: {
- alert: {
- loading,
- },
- },
- },
- },
- stubs,
});
}
- afterEach(() => {
- if (wrapper) {
- wrapper.destroy();
- }
- mock.restore();
- });
-
describe('sidebar expanded', () => {
- const mockUpdatedMutationResult = {
- data: {
- alertSetAssignees: {
- errors: [],
- alert: {
- assigneeUsernames: ['root'],
- },
- },
- },
- };
-
beforeEach(() => {
mock = new MockAdapter(axios);
+ window.gon = {
+ relative_url_root: mockUrlRoot,
+ };
- mock.onGet(mockPath).replyOnce(HTTP_STATUS_OK, mockUsers);
+ mock.onGet(expectedUrl).reply(HTTP_STATUS_OK, mockUsers);
mountComponent({
- data: { alert: mockAlert },
+ props: { alert: mockAlert },
sidebarCollapsed: false,
- loading: false,
- users: mockUsers,
- stubs: {
- SidebarAssignee,
- },
});
});
it('renders a unassigned option', 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({ isDropdownSearching: false });
- await nextTick();
+ await waitForPromises();
expect(findDropdown().text()).toBe('Unassigned');
});
@@ -122,60 +112,38 @@ describe('Alert Details Sidebar Assignees', () => {
expect(findSidebarIcon().exists()).toBe(false);
});
- it('calls `$apollo.mutate` with `AlertSetAssignees` mutation and variables containing `iid`, `assigneeUsernames`, & `projectPath`', async () => {
- jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue(mockUpdatedMutationResult);
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({ isDropdownSearching: false });
-
- await nextTick();
+ it('calls `AlertSetAssignees` mutation and variables containing `iid`, `assigneeUsernames`, & `projectPath`', async () => {
+ await waitForPromises();
wrapper.findComponent(SidebarAssignee).vm.$emit('update-alert-assignees', 'root');
- expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({
- mutation: AlertSetAssignees,
- variables: {
- iid: '1527542',
- assigneeUsernames: ['root'],
- fullPath: 'projectPath',
- },
+ expect(requestHandlers).toHaveBeenCalledWith({
+ iid: '1527542',
+ assigneeUsernames: ['root'],
+ fullPath: 'projectPath',
});
});
it('emits an error when request contains error messages', 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({ isDropdownSearching: false });
- const errorMutationResult = {
- data: {
- issuableSetAssignees: {
- errors: ['There was a problem for sure.'],
- alert: {},
- },
- },
- };
-
- jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue(errorMutationResult);
+ mountComponent({
+ sidebarCollapsed: false,
+ handlers: mockDefaultHandler(['There was a problem for sure.']),
+ });
+ await waitForPromises();
- await nextTick();
const SideBarAssigneeItem = wrapper.findAllComponents(SidebarAssignee).at(0);
await SideBarAssigneeItem.vm.$emit('update-alert-assignees');
- expect(wrapper.emitted('alert-error')).toBeDefined();
+
+ await waitForPromises();
+ expect(wrapper.emitted('alert-error')).toHaveLength(1);
});
it('stops updating and cancels loading when the request fails', () => {
- jest.spyOn(wrapper.vm.$apollo, 'mutate').mockReturnValue(Promise.reject(new Error()));
- wrapper.vm.updateAlertAssignees('root');
expect(findUnassigned().text()).toBe('assign yourself');
});
it('shows a user avatar, username and full name when a user is set', () => {
mountComponent({
- data: { alert: mockAlerts[1] },
- sidebarCollapsed: false,
- loading: false,
- stubs: {
- SidebarAssignee,
- },
+ props: { alert: mockAlerts[1] },
});
expect(findAssigned().find('img').attributes('src')).toBe('/url');
@@ -188,15 +156,10 @@ describe('Alert Details Sidebar Assignees', () => {
beforeEach(() => {
mock = new MockAdapter(axios);
- mock.onGet(mockPath).replyOnce(HTTP_STATUS_OK, mockUsers);
+ mock.onGet(expectedUrl).replyOnce(HTTP_STATUS_OK, mockUsers);
mountComponent({
- data: { alert: mockAlert },
- loading: false,
- users: mockUsers,
- stubs: {
- SidebarAssignee,
- },
+ props: { alert: mockAlert },
});
});
it('does not display the status dropdown', () => {
diff --git a/spec/frontend/vue_shared/components/color_picker/color_picker_spec.js b/spec/frontend/vue_shared/components/color_picker/color_picker_spec.js
index a839af3b709..174e27af948 100644
--- a/spec/frontend/vue_shared/components/color_picker/color_picker_spec.js
+++ b/spec/frontend/vue_shared/components/color_picker/color_picker_spec.js
@@ -96,7 +96,7 @@ describe('ColorPicker', () => {
expect(colorTextInput().attributes('class')).not.toContain('is-invalid');
});
- it('shows invalid feedback when the state is marked as invalid', async () => {
+ it('shows invalid feedback when the state is marked as invalid', () => {
createComponent(mount, { invalidFeedback: invalidText, state: false });
expect(invalidFeedback().text()).toBe(invalidText);
diff --git a/spec/frontend/vue_shared/components/color_select_dropdown/color_select_root_spec.js b/spec/frontend/vue_shared/components/color_select_dropdown/color_select_root_spec.js
index 61a9ab3225e..f262b03414c 100644
--- a/spec/frontend/vue_shared/components/color_select_dropdown/color_select_root_spec.js
+++ b/spec/frontend/vue_shared/components/color_select_dropdown/color_select_root_spec.js
@@ -69,7 +69,7 @@ describe('LabelsSelectRoot', () => {
${'embedded'} | ${[...defaultClasses, 'is-embedded']}
`(
'renders component root element with CSS class `$cssClass` when variant is "$variant"',
- async ({ variant, cssClass }) => {
+ ({ variant, cssClass }) => {
createComponent({
propsData: { variant },
});
diff --git a/spec/frontend/vue_shared/components/color_select_dropdown/dropdown_contents_color_view_spec.js b/spec/frontend/vue_shared/components/color_select_dropdown/dropdown_contents_color_view_spec.js
index 914dfdbaab0..bdb9e8763e2 100644
--- a/spec/frontend/vue_shared/components/color_select_dropdown/dropdown_contents_color_view_spec.js
+++ b/spec/frontend/vue_shared/components/color_select_dropdown/dropdown_contents_color_view_spec.js
@@ -25,7 +25,7 @@ describe('DropdownContentsColorView', () => {
const findColors = () => wrapper.findAllComponents(ColorItem);
const findColorList = () => wrapper.findComponent(GlDropdownForm);
- it('renders color list', async () => {
+ it('renders color list', () => {
expect(findColorList().exists()).toBe(true);
expect(findColors()).toHaveLength(ISSUABLE_COLORS.length);
});
diff --git a/spec/frontend/vue_shared/components/color_select_dropdown/dropdown_contents_spec.js b/spec/frontend/vue_shared/components/color_select_dropdown/dropdown_contents_spec.js
index c07faab20d0..2e3a8550e97 100644
--- a/spec/frontend/vue_shared/components/color_select_dropdown/dropdown_contents_spec.js
+++ b/spec/frontend/vue_shared/components/color_select_dropdown/dropdown_contents_spec.js
@@ -2,6 +2,7 @@ import { nextTick } from 'vue';
import { GlDropdown } from '@gitlab/ui';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import { DROPDOWN_VARIANT } from '~/vue_shared/components/color_select_dropdown/constants';
+import { stubComponent } from 'helpers/stub_component';
import DropdownContents from '~/vue_shared/components/color_select_dropdown/dropdown_contents.vue';
import DropdownContentsColorView from '~/vue_shared/components/color_select_dropdown/dropdown_contents_color_view.vue';
import DropdownHeader from '~/vue_shared/components/color_select_dropdown/dropdown_header.vue';
@@ -19,12 +20,13 @@ const defaultProps = {
describe('DropdownContent', () => {
let wrapper;
- const createComponent = ({ propsData = {} } = {}) => {
+ const createComponent = ({ propsData = {}, stubs = {} } = {}) => {
wrapper = mountExtended(DropdownContents, {
propsData: {
...defaultProps,
...propsData,
},
+ stubs,
});
};
@@ -33,13 +35,22 @@ describe('DropdownContent', () => {
const findDropdown = () => wrapper.findComponent(GlDropdown);
it('calls dropdown `show` method on `isVisible` prop change', async () => {
- createComponent();
- const spy = jest.spyOn(wrapper.vm.$refs.dropdown, 'show');
+ const showDropdown = jest.fn();
+ const hideDropdown = jest.fn();
+ const dropdownStub = {
+ GlDropdown: stubComponent(GlDropdown, {
+ methods: {
+ show: showDropdown,
+ hide: hideDropdown,
+ },
+ }),
+ };
+ createComponent({ stubs: dropdownStub });
await wrapper.setProps({
isVisible: true,
});
- expect(spy).toHaveBeenCalledTimes(1);
+ expect(showDropdown).toHaveBeenCalledTimes(1);
});
it('does not emit `setColor` event on dropdown hide if color did not change', () => {
diff --git a/spec/frontend/vue_shared/components/color_select_dropdown/dropdown_value_spec.js b/spec/frontend/vue_shared/components/color_select_dropdown/dropdown_value_spec.js
index 825f37c97e0..01d3fde279b 100644
--- a/spec/frontend/vue_shared/components/color_select_dropdown/dropdown_value_spec.js
+++ b/spec/frontend/vue_shared/components/color_select_dropdown/dropdown_value_spec.js
@@ -31,12 +31,9 @@ describe('DropdownValue', () => {
index | cssClass
${0} | ${[]}
${1} | ${['hide-collapsed']}
- `(
- 'passes correct props to the ColorItem with CSS class `$cssClass`',
- async ({ index, cssClass }) => {
- expect(findColorItems().at(index).props()).toMatchObject(propsData.selectedColor);
- expect(findColorItems().at(index).classes()).toEqual(cssClass);
- },
- );
+ `('passes correct props to the ColorItem with CSS class `$cssClass`', ({ index, cssClass }) => {
+ expect(findColorItems().at(index).props()).toMatchObject(propsData.selectedColor);
+ expect(findColorItems().at(index).classes()).toEqual(cssClass);
+ });
});
});
diff --git a/spec/frontend/vue_shared/components/diff_viewer/utils_spec.js b/spec/frontend/vue_shared/components/diff_viewer/utils_spec.js
new file mode 100644
index 00000000000..b95e1ee283e
--- /dev/null
+++ b/spec/frontend/vue_shared/components/diff_viewer/utils_spec.js
@@ -0,0 +1,33 @@
+import { transition } from '~/vue_shared/components/diff_viewer/utils';
+import {
+ TRANSITION_LOAD_START,
+ TRANSITION_LOAD_ERROR,
+ TRANSITION_LOAD_SUCCEED,
+ TRANSITION_ACKNOWLEDGE_ERROR,
+ STATE_IDLING,
+ STATE_LOADING,
+ STATE_ERRORED,
+} from '~/diffs/constants';
+
+describe('transition', () => {
+ it.each`
+ state | transitionEvent | result
+ ${'idle'} | ${TRANSITION_LOAD_START} | ${STATE_LOADING}
+ ${'idle'} | ${TRANSITION_LOAD_ERROR} | ${STATE_IDLING}
+ ${'idle'} | ${TRANSITION_LOAD_SUCCEED} | ${STATE_IDLING}
+ ${'idle'} | ${TRANSITION_ACKNOWLEDGE_ERROR} | ${STATE_IDLING}
+ ${'loading'} | ${TRANSITION_LOAD_START} | ${STATE_LOADING}
+ ${'loading'} | ${TRANSITION_LOAD_ERROR} | ${STATE_ERRORED}
+ ${'loading'} | ${TRANSITION_LOAD_SUCCEED} | ${STATE_IDLING}
+ ${'loading'} | ${TRANSITION_ACKNOWLEDGE_ERROR} | ${STATE_LOADING}
+ ${'errored'} | ${TRANSITION_LOAD_START} | ${STATE_LOADING}
+ ${'errored'} | ${TRANSITION_LOAD_ERROR} | ${STATE_ERRORED}
+ ${'errored'} | ${TRANSITION_LOAD_SUCCEED} | ${STATE_ERRORED}
+ ${'errored'} | ${TRANSITION_ACKNOWLEDGE_ERROR} | ${STATE_IDLING}
+ `(
+ 'correctly updates the state to "$result" when it starts as "$state" and the transition is "$transitionEvent"',
+ ({ state, transitionEvent, result }) => {
+ expect(transition(state, transitionEvent)).toBe(result);
+ },
+ );
+});
diff --git a/spec/frontend/vue_shared/components/diff_viewer/viewers/renamed_spec.js b/spec/frontend/vue_shared/components/diff_viewer/viewers/renamed_spec.js
index 549388c1a5c..0d536b23c45 100644
--- a/spec/frontend/vue_shared/components/diff_viewer/viewers/renamed_spec.js
+++ b/spec/frontend/vue_shared/components/diff_viewer/viewers/renamed_spec.js
@@ -1,173 +1,119 @@
import { shallowMount, mount } from '@vue/test-utils';
-import Vue, { nextTick } from 'vue';
+import Vue from 'vue';
import Vuex from 'vuex';
+import { GlAlert, GlLink, GlLoadingIcon } from '@gitlab/ui';
+import waitForPromises from 'helpers/wait_for_promises';
+import * as transitionModule from '~/vue_shared/components/diff_viewer/utils';
import {
+ TRANSITION_ACKNOWLEDGE_ERROR,
TRANSITION_LOAD_START,
TRANSITION_LOAD_ERROR,
TRANSITION_LOAD_SUCCEED,
- TRANSITION_ACKNOWLEDGE_ERROR,
STATE_IDLING,
STATE_LOADING,
- STATE_ERRORED,
} from '~/diffs/constants';
import Renamed from '~/vue_shared/components/diff_viewer/viewers/renamed.vue';
Vue.use(Vuex);
-function createRenamedComponent({ props = {}, store = new Vuex.Store({}), deep = false }) {
+let wrapper;
+let store;
+let event;
+
+const DIFF_FILE_COMMIT_SHA = 'commitsha';
+const DIFF_FILE_SHORT_SHA = 'commitsh';
+const DIFF_FILE_VIEW_PATH = `blob/${DIFF_FILE_COMMIT_SHA}/filename.ext`;
+
+const defaultStore = {
+ modules: {
+ diffs: {
+ namespaced: true,
+ actions: { switchToFullDiffFromRenamedFile: jest.fn().mockResolvedValue() },
+ },
+ },
+};
+const diffFile = {
+ content_sha: DIFF_FILE_COMMIT_SHA,
+ view_path: DIFF_FILE_VIEW_PATH,
+ alternate_viewer: {
+ name: 'text',
+ },
+};
+const defaultProps = { diffFile };
+
+function createRenamedComponent({ props = {}, storeArg = defaultStore, deep = false } = {}) {
+ store = new Vuex.Store(storeArg);
const mnt = deep ? mount : shallowMount;
- return mnt(Renamed, {
- propsData: { ...props },
+ wrapper = mnt(Renamed, {
+ propsData: { ...defaultProps, ...props },
store,
});
}
-describe('Renamed Diff Viewer', () => {
- const DIFF_FILE_COMMIT_SHA = 'commitsha';
- const DIFF_FILE_SHORT_SHA = 'commitsh';
- const DIFF_FILE_VIEW_PATH = `blob/${DIFF_FILE_COMMIT_SHA}/filename.ext`;
- let diffFile;
- let wrapper;
+const findErrorAlert = () => wrapper.findComponent(GlAlert);
+const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
+const findShowFullDiffBtn = () => wrapper.findComponent(GlLink);
+const findPlainText = () => wrapper.find('[test-id="plaintext"]');
+describe('Renamed Diff Viewer', () => {
beforeEach(() => {
- diffFile = {
- content_sha: DIFF_FILE_COMMIT_SHA,
- view_path: DIFF_FILE_VIEW_PATH,
- alternate_viewer: {
- name: 'text',
- },
+ event = {
+ preventDefault: jest.fn(),
};
});
- afterEach(() => {
- if (wrapper) {
- wrapper.destroy();
- wrapper = null;
- }
- });
-
- describe('is', () => {
+ describe('when clicking to load full diff', () => {
beforeEach(() => {
- wrapper = createRenamedComponent({ props: { diffFile } });
+ createRenamedComponent();
});
- it.each`
- state | request | result
- ${'idle'} | ${'idle'} | ${true}
- ${'idle'} | ${'loading'} | ${false}
- ${'idle'} | ${'errored'} | ${false}
- ${'loading'} | ${'loading'} | ${true}
- ${'loading'} | ${'idle'} | ${false}
- ${'loading'} | ${'errored'} | ${false}
- ${'errored'} | ${'errored'} | ${true}
- ${'errored'} | ${'idle'} | ${false}
- ${'errored'} | ${'loading'} | ${false}
- `(
- 'returns the $result for "$request" when the state is "$state"',
- ({ request, result, state }) => {
- wrapper.vm.state = state;
+ it('shows a loading state', async () => {
+ expect(findLoadingIcon().exists()).toBe(false);
- expect(wrapper.vm.is(request)).toEqual(result);
- },
- );
- });
+ await findShowFullDiffBtn().vm.$emit('click', event);
- describe('transition', () => {
- beforeEach(() => {
- wrapper = createRenamedComponent({ props: { diffFile } });
+ expect(findLoadingIcon().exists()).toBe(true);
});
- it.each`
- state | transition | result
- ${'idle'} | ${TRANSITION_LOAD_START} | ${STATE_LOADING}
- ${'idle'} | ${TRANSITION_LOAD_ERROR} | ${STATE_IDLING}
- ${'idle'} | ${TRANSITION_LOAD_SUCCEED} | ${STATE_IDLING}
- ${'idle'} | ${TRANSITION_ACKNOWLEDGE_ERROR} | ${STATE_IDLING}
- ${'loading'} | ${TRANSITION_LOAD_START} | ${STATE_LOADING}
- ${'loading'} | ${TRANSITION_LOAD_ERROR} | ${STATE_ERRORED}
- ${'loading'} | ${TRANSITION_LOAD_SUCCEED} | ${STATE_IDLING}
- ${'loading'} | ${TRANSITION_ACKNOWLEDGE_ERROR} | ${STATE_LOADING}
- ${'errored'} | ${TRANSITION_LOAD_START} | ${STATE_LOADING}
- ${'errored'} | ${TRANSITION_LOAD_ERROR} | ${STATE_ERRORED}
- ${'errored'} | ${TRANSITION_LOAD_SUCCEED} | ${STATE_ERRORED}
- ${'errored'} | ${TRANSITION_ACKNOWLEDGE_ERROR} | ${STATE_IDLING}
- `(
- 'correctly updates the state to "$result" when it starts as "$state" and the transition is "$transition"',
- ({ state, transition, result }) => {
- wrapper.vm.state = state;
-
- wrapper.vm.transition(transition);
-
- expect(wrapper.vm.state).toEqual(result);
- },
- );
- });
-
- describe('switchToFull', () => {
- let store;
-
- beforeEach(() => {
- store = new Vuex.Store({
- modules: {
- diffs: {
- namespaced: true,
- actions: { switchToFullDiffFromRenamedFile: () => {} },
- },
- },
- });
-
+ it('calls the switchToFullDiffFromRenamedFile action when the method is triggered', () => {
jest.spyOn(store, 'dispatch');
- wrapper = createRenamedComponent({ props: { diffFile }, store });
- });
-
- afterEach(() => {
- store = null;
- });
-
- it('calls the switchToFullDiffFromRenamedFile action when the method is triggered', async () => {
- store.dispatch.mockResolvedValue();
-
- wrapper.vm.switchToFull();
+ findShowFullDiffBtn().vm.$emit('click', event);
- await nextTick();
expect(store.dispatch).toHaveBeenCalledWith('diffs/switchToFullDiffFromRenamedFile', {
diffFile,
});
});
it.each`
- after | resolvePromise | resolution
- ${STATE_IDLING} | ${'mockResolvedValue'} | ${'successful'}
- ${STATE_ERRORED} | ${'mockRejectedValue'} | ${'rejected'}
+ after | resolvePromise | resolution
+ ${TRANSITION_LOAD_SUCCEED} | ${'mockResolvedValue'} | ${'successful'}
+ ${TRANSITION_LOAD_ERROR} | ${'mockRejectedValue'} | ${'rejected'}
`(
'moves through the correct states during a $resolution request',
async ({ after, resolvePromise }) => {
- store.dispatch[resolvePromise]();
+ jest.spyOn(transitionModule, 'transition');
+ store.dispatch = jest.fn()[resolvePromise]();
- expect(wrapper.vm.state).toEqual(STATE_IDLING);
+ expect(transitionModule.transition).not.toHaveBeenCalled();
- wrapper.vm.switchToFull();
+ findShowFullDiffBtn().vm.$emit('click', event);
- expect(wrapper.vm.state).toEqual(STATE_LOADING);
+ expect(transitionModule.transition).toHaveBeenCalledWith(
+ STATE_IDLING,
+ TRANSITION_LOAD_START,
+ );
+
+ await waitForPromises();
- await nextTick(); // This tick is needed for when the action (promise) finishes
- await nextTick(); // This tick waits for the state change in the promise .then/.catch to bubble into the component
- expect(wrapper.vm.state).toEqual(after);
+ expect(transitionModule.transition).toHaveBeenCalledTimes(2);
+ expect(transitionModule.transition.mock.calls[1]).toEqual([STATE_LOADING, after]);
},
);
});
describe('clickLink', () => {
- let event;
-
- beforeEach(() => {
- event = {
- preventDefault: jest.fn(),
- };
- });
-
it.each`
alternateViewer | stops | handled
${'text'} | ${true} | ${'should'}
@@ -175,42 +121,51 @@ describe('Renamed Diff Viewer', () => {
`(
'given { alternate_viewer: { name: "$alternateViewer" } }, the click event $handled be handled in the component',
({ alternateViewer, stops }) => {
- wrapper = createRenamedComponent({
- props: {
- diffFile: {
- ...diffFile,
- alternate_viewer: { name: alternateViewer },
- },
+ const props = {
+ diffFile: {
+ ...diffFile,
+ alternate_viewer: { name: alternateViewer },
},
+ };
+
+ createRenamedComponent({
+ props,
});
- jest.spyOn(wrapper.vm, 'switchToFull').mockImplementation(() => {});
+ store.dispatch = jest.fn().mockResolvedValue();
- wrapper.vm.clickLink(event);
+ findShowFullDiffBtn().vm.$emit('click', event);
if (stops) {
expect(event.preventDefault).toHaveBeenCalled();
- expect(wrapper.vm.switchToFull).toHaveBeenCalled();
+ expect(store.dispatch).toHaveBeenCalledWith(
+ 'diffs/switchToFullDiffFromRenamedFile',
+ props,
+ );
} else {
expect(event.preventDefault).not.toHaveBeenCalled();
- expect(wrapper.vm.switchToFull).not.toHaveBeenCalled();
+ expect(store.dispatch).not.toHaveBeenCalled();
}
},
);
});
describe('dismissError', () => {
- let transitionSpy;
-
beforeEach(() => {
- wrapper = createRenamedComponent({ props: { diffFile } });
- transitionSpy = jest.spyOn(wrapper.vm, 'transition');
+ createRenamedComponent({ props: { diffFile } });
});
it(`transitions the component with "${TRANSITION_ACKNOWLEDGE_ERROR}"`, () => {
- wrapper.vm.dismissError();
+ jest.spyOn(transitionModule, 'transition');
+
+ expect(transitionModule.transition).not.toHaveBeenCalled();
+
+ findErrorAlert().vm.$emit('dismiss');
- expect(transitionSpy).toHaveBeenCalledWith(TRANSITION_ACKNOWLEDGE_ERROR);
+ expect(transitionModule.transition).toHaveBeenCalledWith(
+ expect.stringContaining(''),
+ TRANSITION_ACKNOWLEDGE_ERROR,
+ );
});
});
@@ -224,14 +179,19 @@ describe('Renamed Diff Viewer', () => {
`(
'with { alternate_viewer: { name: $nameDisplay } }, renders the component',
({ altViewer }) => {
- const file = { ...diffFile };
-
- file.alternate_viewer.name = altViewer;
- wrapper = createRenamedComponent({ props: { diffFile: file } });
+ createRenamedComponent({
+ props: {
+ diffFile: {
+ ...diffFile,
+ alternate_viewer: {
+ ...diffFile.alternate_viewer,
+ name: altViewer,
+ },
+ },
+ },
+ });
- expect(wrapper.find('[test-id="plaintext"]').text()).toEqual(
- 'File renamed with no changes.',
- );
+ expect(findPlainText().text()).toBe('File renamed with no changes.');
},
);
@@ -245,15 +205,15 @@ describe('Renamed Diff Viewer', () => {
const file = { ...diffFile };
file.alternate_viewer.name = altType;
- wrapper = createRenamedComponent({
+ createRenamedComponent({
deep: true,
props: { diffFile: file },
});
- const link = wrapper.find('a');
+ const link = findShowFullDiffBtn();
- expect(link.text()).toEqual(linkText);
- expect(link.attributes('href')).toEqual(DIFF_FILE_VIEW_PATH);
+ expect(link.text()).toBe(linkText);
+ expect(link.attributes('href')).toBe(DIFF_FILE_VIEW_PATH);
},
);
});
diff --git a/spec/frontend/vue_shared/components/dropdown/dropdown_widget_spec.js b/spec/frontend/vue_shared/components/dropdown/dropdown_widget_spec.js
index 4dfee20764c..dd5a05a40c6 100644
--- a/spec/frontend/vue_shared/components/dropdown/dropdown_widget_spec.js
+++ b/spec/frontend/vue_shared/components/dropdown/dropdown_widget_spec.js
@@ -55,7 +55,7 @@ describe('DropdownWidget component', () => {
expect(wrapper.emitted('set-search')).toEqual([[searchTerm]]);
});
- it('renders one selectable item per passed option', async () => {
+ it('renders one selectable item per passed option', () => {
expect(findDropdownItems()).toHaveLength(2);
});
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 ef42c17984a..4708a5555f8 100644
--- a/spec/frontend/vue_shared/components/dropdown_keyboard_navigation_spec.js
+++ b/spec/frontend/vue_shared/components/dropdown_keyboard_navigation_spec.js
@@ -44,7 +44,7 @@ describe('DropdownKeyboardNavigation', () => {
createComponent();
});
- it('should $emit @change with the default index', async () => {
+ it('should $emit @change with the default index', () => {
expect(wrapper.emitted('change')[0]).toStrictEqual([MOCK_DEFAULT_INDEX]);
});
@@ -100,6 +100,25 @@ describe('DropdownKeyboardNavigation', () => {
describe.each`
keyboardAction | direction | index | max | min
+ ${helpers.arrowDown} | ${1} | ${10} | ${10} | ${0}
+ ${helpers.arrowUp} | ${-1} | ${0} | ${10} | ${0}
+ `(
+ 'moving out of bounds with cycle enabled',
+ ({ keyboardAction, direction, index, max, min }) => {
+ beforeEach(() => {
+ createComponent({ index, max, min, enableCycle: true });
+ keyboardAction();
+ });
+
+ it(`in ${direction} direction does $emit correct @change event`, () => {
+ // The first @change`call happens on created() so we test that we only have 1 call
+ expect(wrapper.emitted('change')[1]).toStrictEqual([direction === 1 ? min : max]);
+ });
+ },
+ );
+
+ describe.each`
+ keyboardAction | direction | index | max | min
${helpers.arrowDown} | ${1} | ${0} | ${10} | ${0}
${helpers.arrowUp} | ${-1} | ${10} | ${10} | ${0}
`('moving in bounds', ({ keyboardAction, direction, index, max, min }) => {
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
index 6b98f6c5e89..fa2e09b6b9f 100644
--- a/spec/frontend/vue_shared/components/entity_select/entity_select_spec.js
+++ b/spec/frontend/vue_shared/components/entity_select/entity_select_spec.js
@@ -201,7 +201,7 @@ describe('EntitySelect', () => {
describe('pagination', () => {
const searchString = 'searchString';
- beforeEach(async () => {
+ beforeEach(() => {
let requestCount = 0;
fetchItemsMock.mockImplementation((searchQuery, page) => {
requestCount += 1;
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
index 32ce2155494..0a174c98efb 100644
--- a/spec/frontend/vue_shared/components/entity_select/project_select_spec.js
+++ b/spec/frontend/vue_shared/components/entity_select/project_select_spec.js
@@ -97,6 +97,7 @@ describe('ProjectSelect', () => {
${'defaultToggleText'} | ${PROJECT_TOGGLE_TEXT}
${'headerText'} | ${PROJECT_HEADER_TEXT}
${'clearable'} | ${true}
+ ${'block'} | ${false}
`('passes the $prop prop to entity-select', ({ prop, expectedValue }) => {
expect(findEntitySelect().props(prop)).toBe(expectedValue);
});
@@ -136,6 +137,18 @@ describe('ProjectSelect', () => {
expect(mock.history.get[0].params.include_subgroups).toBe(true);
});
+ it('does not include shared projects if withShared prop is false', async () => {
+ createComponent({
+ props: {
+ withShared: false,
+ },
+ });
+ openListbox();
+ await waitForPromises();
+
+ expect(mock.history.get[0].params.with_shared).toBe(false);
+ });
+
it('fetches projects globally if no group ID is provided', async () => {
createComponent({
props: {
diff --git a/spec/frontend/vue_shared/components/file_finder/index_spec.js b/spec/frontend/vue_shared/components/file_finder/index_spec.js
index 5cf891a2e52..d7569ed7b76 100644
--- a/spec/frontend/vue_shared/components/file_finder/index_spec.js
+++ b/spec/frontend/vue_shared/components/file_finder/index_spec.js
@@ -79,7 +79,7 @@ describe('File finder item spec', () => {
expect(vm.$el.querySelector('.dropdown-input-search').classList).toContain('hidden');
});
- it('clear button resets searchText', async () => {
+ it('clear button resets searchText', () => {
vm.searchText = 'index';
vm.clearSearchInput();
diff --git a/spec/frontend/vue_shared/components/file_finder/item_spec.js b/spec/frontend/vue_shared/components/file_finder/item_spec.js
index dce6c85b5b3..c73f14e9c6e 100644
--- a/spec/frontend/vue_shared/components/file_finder/item_spec.js
+++ b/spec/frontend/vue_shared/components/file_finder/item_spec.js
@@ -36,7 +36,7 @@ describe('File finder item spec', () => {
expect(wrapper.classes()).toContain('is-focused');
});
- it('does not have is-focused class when not focused', async () => {
+ it('does not have is-focused class when not focused', () => {
createComponent({ focused: false });
expect(wrapper.classes()).not.toContain('is-focused');
@@ -50,13 +50,13 @@ describe('File finder item spec', () => {
expect(wrapper.find('.diff-changed-stats').exists()).toBe(false);
});
- it('renders when a changed file', async () => {
+ it('renders when a changed file', () => {
createComponent({ file: { changed: true } });
expect(wrapper.find('.diff-changed-stats').exists()).toBe(true);
});
- it('renders when a temp file', async () => {
+ it('renders when a temp file', () => {
createComponent({ file: { tempFile: true } });
expect(wrapper.find('.diff-changed-stats').exists()).toBe(true);
@@ -80,7 +80,7 @@ describe('File finder item spec', () => {
expect(findChangedFilePath().findAll('.highlighted')).toHaveLength(4);
});
- it('adds ellipsis to long text', async () => {
+ it('adds ellipsis to long text', () => {
const path = new Array(70)
.fill()
.map((_, i) => `${i}-`)
@@ -101,7 +101,7 @@ describe('File finder item spec', () => {
expect(findChangedFileName().findAll('.highlighted')).toHaveLength(4);
});
- it('does not add ellipsis to long text', async () => {
+ it('does not add ellipsis to long text', () => {
const name = new Array(70)
.fill()
.map((_, i) => `${i}-`)
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_bar_root_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_bar_root_spec.js
index 123714353e2..f576121fc18 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_bar_root_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_bar_root_spec.js
@@ -398,7 +398,7 @@ describe('FilteredSearchBarRoot', () => {
expect(glFilteredSearchEl.props('historyItems')).toEqual(mockHistoryItems);
});
- it('renders checkbox when `showCheckbox` prop is true', async () => {
+ it('renders checkbox when `showCheckbox` prop is true', () => {
let wrapperWithCheckbox = createComponent({
showCheckbox: true,
});
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/base_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/base_token_spec.js
index 9941abbfaea..f115ec2d6ca 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/base_token_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/base_token_spec.js
@@ -18,6 +18,7 @@ import {
OPTIONS_NONE_ANY,
OPERATOR_IS,
OPERATOR_NOT,
+ OPERATOR_OR,
} from '~/vue_shared/components/filtered_search_bar/constants';
import {
getRecentlyUsedSuggestions,
@@ -300,6 +301,7 @@ describe('BaseToken', () => {
operator | shouldRenderFilteredSearchSuggestion
${OPERATOR_IS} | ${true}
${OPERATOR_NOT} | ${false}
+ ${OPERATOR_OR} | ${false}
`('when operator is $operator', ({ shouldRenderFilteredSearchSuggestion, operator }) => {
beforeEach(() => {
const props = {
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/crm_organization_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/crm_organization_token_spec.js
index 8526631c63d..f41c5b5d432 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/crm_organization_token_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/crm_organization_token_spec.js
@@ -157,7 +157,7 @@ describe('CrmOrganizationToken', () => {
});
});
- it('calls `createAlert` with alert error message when request fails', async () => {
+ it('calls `createAlert` when request fails', async () => {
mountComponent();
jest.spyOn(wrapper.vm.$apollo, 'query').mockRejectedValue({});
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/emoji_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/emoji_token_spec.js
index 4e00b6837a3..0dddae50c4e 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/emoji_token_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/emoji_token_spec.js
@@ -91,7 +91,7 @@ describe('EmojiToken', () => {
describe('when request is successful', () => {
const searchTerm = 'foo';
- beforeEach(async () => {
+ beforeEach(() => {
wrapper = createComponent({
config: {
fetchEmojis: jest.fn().mockResolvedValue({ data: mockEmojis }),
@@ -119,7 +119,7 @@ describe('EmojiToken', () => {
return triggerFetchEmojis();
});
- it('calls `createAlert` with alert error message', () => {
+ it('calls `createAlert`', () => {
expect(createAlert).toHaveBeenCalledWith({
message: 'There was a problem fetching emojis.',
});
@@ -135,7 +135,7 @@ describe('EmojiToken', () => {
describe('template', () => {
const defaultEmojis = OPTIONS_NONE_ANY;
- beforeEach(async () => {
+ beforeEach(() => {
wrapper = createComponent({
value: { data: `"${mockEmojis[0].name}"` },
config: {
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/label_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/label_token_spec.js
index b9275409125..696483df8ef 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/label_token_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/label_token_spec.js
@@ -152,7 +152,7 @@ describe('LabelToken', () => {
await triggerFetchLabels();
});
- it('calls `createAlert` with alert error message', () => {
+ it('calls `createAlert`', () => {
expect(createAlert).toHaveBeenCalledWith({
message: 'There was a problem fetching labels.',
});
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/milestone_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/milestone_token_spec.js
index fea1496a80b..c758e550ba2 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/milestone_token_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/milestone_token_spec.js
@@ -134,7 +134,7 @@ describe('MilestoneToken', () => {
return triggerFetchMilestones();
});
- it('calls `createAlert` with alert error message', () => {
+ it('calls `createAlert`', () => {
expect(createAlert).toHaveBeenCalledWith({
message: 'There was a problem fetching milestones.',
});
@@ -153,7 +153,7 @@ describe('MilestoneToken', () => {
{ text: 'bar', value: 'baz' },
];
- beforeEach(async () => {
+ beforeEach(() => {
wrapper = createComponent({
value: { data: `"${mockRegularMilestone.title}"` },
config: {
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/user_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/user_token_spec.js
index 89003296854..d0a6519f16d 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/user_token_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/user_token_spec.js
@@ -134,13 +134,13 @@ describe('UserToken', () => {
return triggerFetchUsers();
});
- it('calls `createAlert` with alert error message', () => {
+ it('calls `createAlert`', () => {
expect(createAlert).toHaveBeenCalledWith({
message: 'There was a problem fetching users.',
});
});
- it('sets `loading` to false when request completes', async () => {
+ it('sets `loading` to false when request completes', () => {
expect(findBaseToken().props('suggestionsLoading')).toBe(false);
});
});
@@ -187,7 +187,7 @@ describe('UserToken', () => {
expect(tokenValue.text()).toBe(mockUsers[0].name); // "Administrator"
});
- it('renders token value with correct avatarUrl from user object', async () => {
+ it('renders token value with correct avatarUrl from user object', () => {
const getAvatarEl = () =>
wrapper.findAllComponents(GlFilteredSearchTokenSegment).at(2).findComponent(GlAvatar);
diff --git a/spec/frontend/vue_shared/components/gl_countdown_spec.js b/spec/frontend/vue_shared/components/gl_countdown_spec.js
index 1de206123fe..38d54eff872 100644
--- a/spec/frontend/vue_shared/components/gl_countdown_spec.js
+++ b/spec/frontend/vue_shared/components/gl_countdown_spec.js
@@ -11,7 +11,7 @@ describe('GlCountdown', () => {
});
describe('when there is time remaining', () => {
- beforeEach(async () => {
+ beforeEach(() => {
wrapper = mount(GlCountdown, {
propsData: {
endDateString: '2000-01-01T01:02:03Z',
@@ -33,7 +33,7 @@ describe('GlCountdown', () => {
});
describe('when there is no time remaining', () => {
- beforeEach(async () => {
+ beforeEach(() => {
wrapper = mount(GlCountdown, {
propsData: {
endDateString: '1900-01-01T00:00:00Z',
diff --git a/spec/frontend/vue_shared/components/listbox_input/listbox_input_spec.js b/spec/frontend/vue_shared/components/listbox_input/listbox_input_spec.js
index 4e83f3e1c06..397fd270344 100644
--- a/spec/frontend/vue_shared/components/listbox_input/listbox_input_spec.js
+++ b/spec/frontend/vue_shared/components/listbox_input/listbox_input_spec.js
@@ -165,7 +165,7 @@ describe('ListboxInput', () => {
findGlListbox().vm.$emit('search', '1');
});
- it('passes only the items that match the search string', async () => {
+ it('passes only the items that match the search string', () => {
expect(findGlListbox().props('items')).toStrictEqual([
{
text: 'Group 1',
@@ -183,7 +183,7 @@ describe('ListboxInput', () => {
findGlListbox().vm.$emit('search', '1');
});
- it('passes only the items that match the search string', async () => {
+ it('passes only the items that match the search string', () => {
expect(findGlListbox().props('items')).toStrictEqual([{ text: 'Item 1', value: '1' }]);
});
});
diff --git a/spec/frontend/vue_shared/components/markdown/comment_templates_dropdown_spec.js b/spec/frontend/vue_shared/components/markdown/comment_templates_dropdown_spec.js
new file mode 100644
index 00000000000..bfa9b7dd706
--- /dev/null
+++ b/spec/frontend/vue_shared/components/markdown/comment_templates_dropdown_spec.js
@@ -0,0 +1,76 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import savedRepliesResponse from 'test_fixtures/graphql/comment_templates/saved_replies.query.graphql.json';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
+import { updateText } from '~/lib/utils/text_markdown';
+import CommentTemplatesDropdown from '~/vue_shared/components/markdown/comment_templates_dropdown.vue';
+import savedRepliesQuery from '~/vue_shared/components/markdown/saved_replies.query.graphql';
+
+jest.mock('~/lib/utils/text_markdown');
+
+let wrapper;
+let savedRepliesResp;
+
+function createMockApolloProvider(response) {
+ Vue.use(VueApollo);
+
+ savedRepliesResp = jest.fn().mockResolvedValue(response);
+
+ const requestHandlers = [[savedRepliesQuery, savedRepliesResp]];
+
+ return createMockApollo(requestHandlers);
+}
+
+function createComponent(options = {}) {
+ const { mockApollo } = options;
+
+ return mountExtended(CommentTemplatesDropdown, {
+ attachTo: '#root',
+ propsData: {
+ newCommentTemplatePath: '/new',
+ },
+ apolloProvider: mockApollo,
+ });
+}
+
+describe('Comment templates dropdown', () => {
+ beforeEach(() => {
+ setHTMLFixture('<div class="md-area"><textarea></textarea><div id="root"></div></div>');
+ });
+
+ afterEach(() => {
+ resetHTMLFixture();
+ });
+
+ it('fetches data when dropdown gets opened', async () => {
+ const mockApollo = createMockApolloProvider(savedRepliesResponse);
+ wrapper = createComponent({ mockApollo });
+
+ wrapper.findByTestId('comment-template-dropdown-toggle').trigger('click');
+
+ await waitForPromises();
+
+ expect(savedRepliesResp).toHaveBeenCalled();
+ });
+
+ it('adds content to textarea', async () => {
+ const mockApollo = createMockApolloProvider(savedRepliesResponse);
+ wrapper = createComponent({ mockApollo });
+
+ wrapper.findByTestId('comment-template-dropdown-toggle').trigger('click');
+
+ await waitForPromises();
+
+ wrapper.find('.gl-new-dropdown-item').trigger('click');
+
+ expect(updateText).toHaveBeenCalledWith({
+ textArea: document.querySelector('textarea'),
+ tag: savedRepliesResponse.data.currentUser.savedReplies.nodes[0].content,
+ cursorOffset: 0,
+ wrap: false,
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/markdown/editor_mode_dropdown_spec.js b/spec/frontend/vue_shared/components/markdown/editor_mode_dropdown_spec.js
index becd4257cbe..fd8493e0911 100644
--- a/spec/frontend/vue_shared/components/markdown/editor_mode_dropdown_spec.js
+++ b/spec/frontend/vue_shared/components/markdown/editor_mode_dropdown_spec.js
@@ -23,8 +23,8 @@ describe('vue_shared/component/markdown/editor_mode_dropdown', () => {
describe.each`
modeText | value | dropdownText | otherMode
- ${'Rich text'} | ${'richText'} | ${'Viewing rich text'} | ${'Markdown'}
- ${'Markdown'} | ${'markdown'} | ${'Viewing markdown'} | ${'Rich text'}
+ ${'Rich text'} | ${'richText'} | ${'Editing rich text'} | ${'Markdown'}
+ ${'Markdown'} | ${'markdown'} | ${'Editing markdown'} | ${'Rich text'}
`('$modeText', ({ modeText, value, dropdownText, otherMode }) => {
beforeEach(() => {
createComponent({ value });
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 681ff6c8dd3..69dedd6b68a 100644
--- a/spec/frontend/vue_shared/components/markdown/markdown_editor_spec.js
+++ b/spec/frontend/vue_shared/components/markdown/markdown_editor_spec.js
@@ -9,6 +9,7 @@ import ContentEditor from '~/content_editor/components/content_editor.vue';
import BubbleMenu from '~/content_editor/components/bubble_menus/bubble_menu.vue';
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
import MarkdownField from '~/vue_shared/components/markdown/field.vue';
+import { assertProps } from 'helpers/assert_props';
import { stubComponent } from 'helpers/stub_component';
import { useLocalStorageSpy } from 'helpers/local_storage_helper';
import waitForPromises from 'helpers/wait_for_promises';
@@ -33,23 +34,26 @@ describe('vue_shared/component/markdown/markdown_editor', () => {
const autocompleteDataSources = { commands: '/foobar/-/autcomplete_sources' };
let mock;
+ const defaultProps = {
+ value,
+ renderMarkdownPath,
+ markdownDocsPath,
+ quickActionsDocsPath,
+ enableAutocomplete,
+ autocompleteDataSources,
+ enablePreview,
+ formFieldProps: {
+ id: formFieldId,
+ name: formFieldName,
+ placeholder: formFieldPlaceholder,
+ 'aria-label': formFieldAriaLabel,
+ },
+ };
const buildWrapper = ({ propsData = {}, attachTo, stubs = {} } = {}) => {
wrapper = mountExtended(MarkdownEditor, {
attachTo,
propsData: {
- value,
- renderMarkdownPath,
- markdownDocsPath,
- quickActionsDocsPath,
- enableAutocomplete,
- autocompleteDataSources,
- enablePreview,
- formFieldProps: {
- id: formFieldId,
- name: formFieldName,
- placeholder: formFieldPlaceholder,
- 'aria-label': formFieldAriaLabel,
- },
+ ...defaultProps,
...propsData,
},
stubs: {
@@ -244,9 +248,9 @@ describe('vue_shared/component/markdown/markdown_editor', () => {
});
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"');
+ expect(() => assertProps(MarkdownEditor, { ...defaultProps, 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 () => {
@@ -275,7 +279,7 @@ describe('vue_shared/component/markdown/markdown_editor', () => {
await findTextarea().setValue(newValue);
- expect(wrapper.emitted('input')).toEqual([[newValue]]);
+ expect(wrapper.emitted('input')).toEqual([[value], [newValue]]);
});
it('autosaves the markdown value to local storage', async () => {
@@ -346,6 +350,16 @@ describe('vue_shared/component/markdown/markdown_editor', () => {
});
});
+ describe('when contentEditor is disabled', () => {
+ it('resets the editingMode to markdownField', () => {
+ localStorage.setItem('gl-markdown-editor-mode', 'contentEditor');
+
+ buildWrapper({ propsData: { autosaveKey: 'issue/1234', enableContentEditor: false } });
+
+ expect(wrapper.vm.editingMode).toBe(EDITING_MODE_MARKDOWN_FIELD);
+ });
+ });
+
describe(`when editingMode is ${EDITING_MODE_CONTENT_EDITOR}`, () => {
beforeEach(async () => {
buildWrapper({ propsData: { autosaveKey: 'issue/1234' } });
@@ -370,7 +384,7 @@ describe('vue_shared/component/markdown/markdown_editor', () => {
await findContentEditor().vm.$emit('change', { markdown: newValue });
- expect(wrapper.emitted('input')).toEqual([[newValue]]);
+ expect(wrapper.emitted('input')).toEqual([[value], [newValue]]);
});
it('autosaves the content editor value to local storage', async () => {
diff --git a/spec/frontend/vue_shared/components/markdown/saved_replies_dropdown_spec.js b/spec/frontend/vue_shared/components/markdown/saved_replies_dropdown_spec.js
deleted file mode 100644
index 8ad9ad30c1d..00000000000
--- a/spec/frontend/vue_shared/components/markdown/saved_replies_dropdown_spec.js
+++ /dev/null
@@ -1,62 +0,0 @@
-import Vue from 'vue';
-import VueApollo from 'vue-apollo';
-import savedRepliesResponse from 'test_fixtures/graphql/saved_replies/saved_replies.query.graphql.json';
-import { mountExtended } from 'helpers/vue_test_utils_helper';
-import createMockApollo from 'helpers/mock_apollo_helper';
-import waitForPromises from 'helpers/wait_for_promises';
-import SavedRepliesDropdown from '~/vue_shared/components/markdown/saved_replies_dropdown.vue';
-import savedRepliesQuery from '~/vue_shared/components/markdown/saved_replies.query.graphql';
-
-let wrapper;
-let savedRepliesResp;
-
-function createMockApolloProvider(response) {
- Vue.use(VueApollo);
-
- savedRepliesResp = jest.fn().mockResolvedValue(response);
-
- const requestHandlers = [[savedRepliesQuery, savedRepliesResp]];
-
- return createMockApollo(requestHandlers);
-}
-
-function createComponent(options = {}) {
- const { mockApollo } = options;
-
- return mountExtended(SavedRepliesDropdown, {
- propsData: {
- newSavedRepliesPath: '/new',
- },
- apolloProvider: mockApollo,
- });
-}
-
-describe('Saved replies dropdown', () => {
- it('fetches data when dropdown gets opened', async () => {
- const mockApollo = createMockApolloProvider(savedRepliesResponse);
- wrapper = createComponent({ mockApollo });
-
- wrapper.findByTestId('saved-replies-dropdown-toggle').trigger('click');
-
- await waitForPromises();
-
- expect(savedRepliesResp).toHaveBeenCalled();
- });
-
- it('adds markdown toolbar attributes to dropdown items', async () => {
- const mockApollo = createMockApolloProvider(savedRepliesResponse);
- wrapper = createComponent({ mockApollo });
-
- wrapper.findByTestId('saved-replies-dropdown-toggle').trigger('click');
-
- await waitForPromises();
-
- expect(wrapper.findByTestId('saved-reply-dropdown-item').attributes()).toEqual(
- expect.objectContaining({
- 'data-md-cursor-offset': '0',
- 'data-md-prepend': 'true',
- 'data-md-tag': 'Saved Reply Content',
- }),
- );
- });
-});
diff --git a/spec/frontend/vue_shared/components/markdown/suggestions_spec.js b/spec/frontend/vue_shared/components/markdown/suggestions_spec.js
index 8f4235cfe41..2fdab40b4bd 100644
--- a/spec/frontend/vue_shared/components/markdown/suggestions_spec.js
+++ b/spec/frontend/vue_shared/components/markdown/suggestions_spec.js
@@ -1,4 +1,5 @@
-import Vue, { nextTick } from 'vue';
+import { nextTick } from 'vue';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
import SuggestionsComponent from '~/vue_shared/components/markdown/suggestions.vue';
const MOCK_DATA = {
@@ -48,56 +49,37 @@ const MOCK_DATA = {
};
describe('Suggestion component', () => {
- let vm;
- let diffTable;
+ let wrapper;
- beforeEach(async () => {
- const Component = Vue.extend(SuggestionsComponent);
+ const createComponent = (props = {}) => {
+ wrapper = mountExtended(SuggestionsComponent, {
+ propsData: {
+ ...MOCK_DATA,
+ ...props,
+ },
+ });
+ };
- vm = new Component({
- propsData: MOCK_DATA,
- }).$mount();
+ const findSuggestionsContainer = () => wrapper.findByTestId('suggestions-container');
- diffTable = vm.generateDiff(0).$mount().$el;
+ beforeEach(async () => {
+ createComponent();
- jest.spyOn(vm, 'renderSuggestions').mockImplementation(() => {});
- vm.renderSuggestions();
await nextTick();
});
describe('mounted', () => {
it('renders a flash container', () => {
- expect(vm.$el.querySelector('.js-suggestions-flash')).not.toBeNull();
+ expect(wrapper.find('.js-suggestions-flash').exists()).toBe(true);
});
it('renders a container for suggestions', () => {
- expect(vm.$refs.container).not.toBeNull();
+ expect(findSuggestionsContainer().exists()).toBe(true);
});
it('renders suggestions', () => {
- expect(vm.renderSuggestions).toHaveBeenCalled();
- expect(vm.$el.innerHTML.includes('oldtest')).toBe(true);
- expect(vm.$el.innerHTML.includes('newtest')).toBe(true);
- });
- });
-
- describe('generateDiff', () => {
- it('generates a diff table', () => {
- expect(diffTable.querySelector('.md-suggestion-diff')).not.toBeNull();
- });
-
- it('generates a diff table that contains contents the suggested lines', () => {
- MOCK_DATA.suggestions[0].diff_lines.forEach((line) => {
- const text = line.text.substring(1);
-
- expect(diffTable.innerHTML.includes(text)).toBe(true);
- });
- });
-
- it('generates a diff table with the correct line number for each suggested line', () => {
- const lines = diffTable.querySelectorAll('.old_line');
-
- expect(parseInt([...lines][0].innerHTML, 10)).toBe(5);
+ expect(findSuggestionsContainer().text()).toContain('oldtest');
+ expect(findSuggestionsContainer().text()).toContain('newtest');
});
});
});
diff --git a/spec/frontend/vue_shared/components/markdown_drawer/markdown_drawer_spec.js b/spec/frontend/vue_shared/components/markdown_drawer/markdown_drawer_spec.js
index 37b0767616a..6f4902e3f96 100644
--- a/spec/frontend/vue_shared/components/markdown_drawer/markdown_drawer_spec.js
+++ b/spec/frontend/vue_shared/components/markdown_drawer/markdown_drawer_spec.js
@@ -156,7 +156,7 @@ describe('MarkdownDrawer', () => {
renderGLFMSpy.mockClear();
});
- it('fetches the Markdown and caches it', async () => {
+ it('fetches the Markdown and caches it', () => {
expect(getRenderedMarkdown).toHaveBeenCalledTimes(1);
expect(Object.keys(cache)).toHaveLength(1);
});
@@ -199,13 +199,13 @@ describe('MarkdownDrawer', () => {
afterEach(() => {
getRenderedMarkdown.mockClear();
});
- it('shows alert', () => {
+ it('shows an alert', () => {
expect(findAlert().exists()).toBe(true);
});
});
describe('While Markdown is fetching', () => {
- beforeEach(async () => {
+ beforeEach(() => {
getRenderedMarkdown.mockReturnValue(new Promise(() => {}));
createComponent();
@@ -215,7 +215,7 @@ describe('MarkdownDrawer', () => {
getRenderedMarkdown.mockClear();
});
- it('shows skeleton', async () => {
+ it('shows skeleton', () => {
expect(findSkeleton().exists()).toBe(true);
});
});
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 29e1a9ccf4d..7f3912dcadb 100644
--- a/spec/frontend/vue_shared/components/notes/system_note_spec.js
+++ b/spec/frontend/vue_shared/components/notes/system_note_spec.js
@@ -64,7 +64,7 @@ describe('system note component', () => {
it('should render svg icon', () => {
createComponent(props);
- expect(vm.find('.timeline-icon svg').exists()).toBe(true);
+ expect(vm.find('[data-testid="timeline-icon"]').exists()).toBe(true);
});
// Redcarpet Markdown renderer wraps text in `<p>` tags
diff --git a/spec/frontend/vue_shared/components/projects_list/projects_list_item_spec.js b/spec/frontend/vue_shared/components/projects_list/projects_list_item_spec.js
new file mode 100644
index 00000000000..a8e3536059e
--- /dev/null
+++ b/spec/frontend/vue_shared/components/projects_list/projects_list_item_spec.js
@@ -0,0 +1,169 @@
+import { GlAvatarLabeled, GlBadge, GlIcon } from '@gitlab/ui';
+import projects from 'test_fixtures/api/users/projects/get.json';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import ProjectsListItem from '~/vue_shared/components/projects_list/projects_list_item.vue';
+import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
+import {
+ VISIBILITY_TYPE_ICON,
+ VISIBILITY_LEVEL_PRIVATE_STRING,
+ PROJECT_VISIBILITY_TYPE,
+} from '~/visibility_level/constants';
+import UserAccessRoleBadge from '~/vue_shared/components/user_access_role_badge.vue';
+import { ACCESS_LEVEL_LABELS } from '~/access_level/constants';
+import { FEATURABLE_DISABLED, FEATURABLE_ENABLED } from '~/featurable/constants';
+
+describe('ProjectsListItem', () => {
+ let wrapper;
+
+ const [project] = convertObjectPropsToCamelCase(projects, { deep: true });
+
+ const defaultPropsData = { project };
+
+ const createComponent = ({ propsData = {} } = {}) => {
+ wrapper = mountExtended(ProjectsListItem, {
+ propsData: { ...defaultPropsData, ...propsData },
+ directives: {
+ GlTooltip: createMockDirective('gl-tooltip'),
+ },
+ });
+ };
+
+ const findAvatarLabeled = () => wrapper.findComponent(GlAvatarLabeled);
+ const findIssuesLink = () => wrapper.findByRole('link', { name: ProjectsListItem.i18n.issues });
+ const findForksLink = () => wrapper.findByRole('link', { name: ProjectsListItem.i18n.forks });
+
+ it('renders project avatar', () => {
+ createComponent();
+
+ const avatarLabeled = findAvatarLabeled();
+
+ expect(avatarLabeled.props()).toMatchObject({
+ label: project.name,
+ labelLink: project.webUrl,
+ });
+ expect(avatarLabeled.attributes()).toMatchObject({
+ 'entity-id': project.id.toString(),
+ 'entity-name': project.name,
+ shape: 'rect',
+ size: '48',
+ });
+ });
+
+ it('renders visibility icon with tooltip', () => {
+ createComponent();
+
+ const icon = findAvatarLabeled().findComponent(GlIcon);
+ const tooltip = getBinding(icon.element, 'gl-tooltip');
+
+ expect(icon.props('name')).toBe(VISIBILITY_TYPE_ICON[VISIBILITY_LEVEL_PRIVATE_STRING]);
+ expect(tooltip.value).toBe(PROJECT_VISIBILITY_TYPE[VISIBILITY_LEVEL_PRIVATE_STRING]);
+ });
+
+ it('renders access role badge', () => {
+ createComponent();
+
+ expect(findAvatarLabeled().findComponent(UserAccessRoleBadge).text()).toBe(
+ ACCESS_LEVEL_LABELS[project.permissions.projectAccess.accessLevel],
+ );
+ });
+
+ describe('if project is archived', () => {
+ beforeEach(() => {
+ createComponent({
+ propsData: {
+ project: {
+ ...project,
+ archived: true,
+ },
+ },
+ });
+ });
+
+ it('renders the archived badge', () => {
+ expect(
+ wrapper
+ .findAllComponents(GlBadge)
+ .wrappers.find((badge) => badge.text() === ProjectsListItem.i18n.archived),
+ ).not.toBeUndefined();
+ });
+ });
+
+ it('renders stars count', () => {
+ createComponent();
+
+ const starsLink = wrapper.findByRole('link', { name: ProjectsListItem.i18n.stars });
+ const tooltip = getBinding(starsLink.element, 'gl-tooltip');
+
+ expect(tooltip.value).toBe(ProjectsListItem.i18n.stars);
+ expect(starsLink.attributes('href')).toBe(`${project.webUrl}/-/starrers`);
+ expect(starsLink.text()).toBe(project.starCount.toString());
+ expect(starsLink.findComponent(GlIcon).props('name')).toBe('star-o');
+ });
+
+ describe('when issues are enabled', () => {
+ it('renders issues count', () => {
+ createComponent();
+
+ const issuesLink = findIssuesLink();
+ const tooltip = getBinding(issuesLink.element, 'gl-tooltip');
+
+ expect(tooltip.value).toBe(ProjectsListItem.i18n.issues);
+ expect(issuesLink.attributes('href')).toBe(`${project.webUrl}/-/issues`);
+ expect(issuesLink.text()).toBe(project.openIssuesCount.toString());
+ expect(issuesLink.findComponent(GlIcon).props('name')).toBe('issues');
+ });
+ });
+
+ describe('when issues are not enabled', () => {
+ it('does not render issues count', () => {
+ createComponent({
+ propsData: {
+ project: {
+ ...project,
+ issuesAccessLevel: FEATURABLE_DISABLED,
+ },
+ },
+ });
+
+ expect(findIssuesLink().exists()).toBe(false);
+ });
+ });
+
+ describe('when forking is enabled', () => {
+ it('renders forks count', () => {
+ createComponent();
+
+ const forksLink = findForksLink();
+ const tooltip = getBinding(forksLink.element, 'gl-tooltip');
+
+ expect(tooltip.value).toBe(ProjectsListItem.i18n.forks);
+ expect(forksLink.attributes('href')).toBe(`${project.webUrl}/-/forks`);
+ expect(forksLink.text()).toBe(project.openIssuesCount.toString());
+ expect(forksLink.findComponent(GlIcon).props('name')).toBe('fork');
+ });
+ });
+
+ describe('when forking is not enabled', () => {
+ it.each([
+ {
+ ...project,
+ forksCount: 2,
+ forkingAccessLevel: FEATURABLE_DISABLED,
+ },
+ {
+ ...project,
+ forksCount: undefined,
+ forkingAccessLevel: FEATURABLE_ENABLED,
+ },
+ ])('does not render forks count', (modifiedProject) => {
+ createComponent({
+ propsData: {
+ project: modifiedProject,
+ },
+ });
+
+ expect(findForksLink().exists()).toBe(false);
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/projects_list/projects_list_spec.js b/spec/frontend/vue_shared/components/projects_list/projects_list_spec.js
new file mode 100644
index 00000000000..9380e19c39e
--- /dev/null
+++ b/spec/frontend/vue_shared/components/projects_list/projects_list_spec.js
@@ -0,0 +1,34 @@
+import projects from 'test_fixtures/api/users/projects/get.json';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import ProjectsList from '~/vue_shared/components/projects_list/projects_list.vue';
+import ProjectsListItem from '~/vue_shared/components/projects_list/projects_list_item.vue';
+import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+
+describe('ProjectsList', () => {
+ let wrapper;
+
+ const defaultPropsData = {
+ projects: convertObjectPropsToCamelCase(projects, { deep: true }),
+ };
+
+ const createComponent = () => {
+ wrapper = shallowMountExtended(ProjectsList, {
+ propsData: defaultPropsData,
+ });
+ };
+
+ it('renders list with `ProjectListItem` component', () => {
+ createComponent();
+
+ const projectsListItemWrappers = wrapper.findAllComponents(ProjectsListItem).wrappers;
+ const expectedProps = projectsListItemWrappers.map((projectsListItemWrapper) =>
+ projectsListItemWrapper.props(),
+ );
+
+ expect(expectedProps).toEqual(
+ defaultPropsData.projects.map((project) => ({
+ project,
+ })),
+ );
+ });
+});
diff --git a/spec/frontend/vue_shared/components/registry/__snapshots__/code_instruction_spec.js.snap b/spec/frontend/vue_shared/components/registry/__snapshots__/code_instruction_spec.js.snap
index e8d76991b90..eadcb6ceeb7 100644
--- a/spec/frontend/vue_shared/components/registry/__snapshots__/code_instruction_spec.js.snap
+++ b/spec/frontend/vue_shared/components/registry/__snapshots__/code_instruction_spec.js.snap
@@ -46,31 +46,15 @@ exports[`Package code instruction single line to match the default snapshot 1`]
class="input-group-append"
data-testid="instruction-button"
>
- <button
- aria-label="Copy npm install command"
- aria-live="polite"
- class="btn input-group-text btn-default btn-md gl-button btn-default-secondary btn-icon"
- data-clipboard-handle-tooltip="false"
- data-clipboard-text="npm i @my-package"
- id="clipboard-button-1"
+ <clipboard-button-stub
+ category="secondary"
+ class="input-group-text"
+ size="medium"
+ text="npm i @my-package"
title="Copy npm install command"
- type="button"
- >
- <!---->
-
- <svg
- aria-hidden="true"
- class="gl-button-icon gl-icon s16"
- data-testid="copy-to-clipboard-icon"
- role="img"
- >
- <use
- href="#copy-to-clipboard"
- />
- </svg>
-
- <!---->
- </button>
+ tooltipplacement="top"
+ variant="default"
+ />
</span>
</div>
</div>
diff --git a/spec/frontend/vue_shared/components/registry/__snapshots__/history_item_spec.js.snap b/spec/frontend/vue_shared/components/registry/__snapshots__/history_item_spec.js.snap
index 66cf2354bc7..5c487754b87 100644
--- a/spec/frontend/vue_shared/components/registry/__snapshots__/history_item_spec.js.snap
+++ b/spec/frontend/vue_shared/components/registry/__snapshots__/history_item_spec.js.snap
@@ -8,7 +8,7 @@ exports[`History Item renders the correct markup 1`] = `
class="timeline-entry-inner"
>
<div
- class="timeline-icon"
+ class="gl--flex-center gl-rounded-full gl-mt-n1 gl-ml-2 gl-w-6 gl-h-6 gl-bg-gray-50 gl-text-gray-600 gl-float-left"
>
<gl-icon-stub
name="pencil"
diff --git a/spec/frontend/vue_shared/components/registry/code_instruction_spec.js b/spec/frontend/vue_shared/components/registry/code_instruction_spec.js
index 60c1293b7c1..299535775e0 100644
--- a/spec/frontend/vue_shared/components/registry/code_instruction_spec.js
+++ b/spec/frontend/vue_shared/components/registry/code_instruction_spec.js
@@ -1,4 +1,4 @@
-import { mount } from '@vue/test-utils';
+import { shallowMount } from '@vue/test-utils';
import Tracking from '~/tracking';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import CodeInstruction from '~/vue_shared/components/registry/code_instruction.vue';
@@ -14,7 +14,7 @@ describe('Package code instruction', () => {
};
function createComponent(props = {}) {
- wrapper = mount(CodeInstruction, {
+ wrapper = shallowMount(CodeInstruction, {
propsData: {
...defaultProps,
...props,
diff --git a/spec/frontend/vue_shared/components/resizable_chart/__snapshots__/skeleton_loader_spec.js.snap b/spec/frontend/vue_shared/components/resizable_chart/__snapshots__/skeleton_loader_spec.js.snap
index 623f7d083c5..65427374e1b 100644
--- a/spec/frontend/vue_shared/components/resizable_chart/__snapshots__/skeleton_loader_spec.js.snap
+++ b/spec/frontend/vue_shared/components/resizable_chart/__snapshots__/skeleton_loader_spec.js.snap
@@ -15,8 +15,8 @@ exports[`Resizable Skeleton Loader default setup renders the bars, labels, and g
</title>
<rect
clip-path="url(#null-idClip)"
+ fill="url(#null-idGradient)"
height="130"
- style="fill: url(#null-idGradient);"
width="400"
x="0"
y="0"
@@ -234,8 +234,8 @@ exports[`Resizable Skeleton Loader with custom settings renders the correct posi
</title>
<rect
clip-path="url(#-idClip)"
+ fill="url(#-idGradient)"
height="130"
- style="fill: url(#-idGradient);"
width="400"
x="0"
y="0"
diff --git a/spec/frontend/vue_shared/components/runner_instructions/instructions/runner_cli_instructions_spec.js b/spec/frontend/vue_shared/components/runner_instructions/instructions/runner_cli_instructions_spec.js
index c4d4f80c573..c6cd963fc33 100644
--- a/spec/frontend/vue_shared/components/runner_instructions/instructions/runner_cli_instructions_spec.js
+++ b/spec/frontend/vue_shared/components/runner_instructions/instructions/runner_cli_instructions_spec.js
@@ -65,7 +65,7 @@ describe('RunnerCliInstructions component', () => {
await waitForPromises();
});
- it('should not show alert', async () => {
+ it('should not show alert', () => {
expect(findAlert().exists()).toBe(false);
});
@@ -85,13 +85,13 @@ describe('RunnerCliInstructions component', () => {
});
});
- it('binary instructions are shown', async () => {
+ it('binary instructions are shown', () => {
const instructions = findBinaryInstructions().text();
expect(instructions).toBe(installInstructions.trim());
});
- it('register command is shown with a replaced token', async () => {
+ it('register command is shown with a replaced token', () => {
const command = findRegisterCommand().text();
expect(command).toBe(
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 cb35cbd35ad..cd4ebe334c0 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
@@ -80,7 +80,7 @@ describe('RunnerInstructionsModal component', () => {
await waitForPromises();
});
- it('should not show alert', async () => {
+ it('should not show alert', () => {
expect(findAlert().exists()).toBe(false);
});
@@ -202,7 +202,7 @@ describe('RunnerInstructionsModal component', () => {
expect(findAlert().exists()).toBe(true);
});
- it('should show alert when instructions cannot be loaded', async () => {
+ it('should show an alert when instructions cannot be loaded', async () => {
createComponent();
await waitForPromises();
diff --git a/spec/frontend/vue_shared/components/slot_switch_spec.js b/spec/frontend/vue_shared/components/slot_switch_spec.js
index f25b9877aba..daca4977817 100644
--- a/spec/frontend/vue_shared/components/slot_switch_spec.js
+++ b/spec/frontend/vue_shared/components/slot_switch_spec.js
@@ -1,4 +1,5 @@
import { shallowMount } from '@vue/test-utils';
+import { assertProps } from 'helpers/assert_props';
import SlotSwitch from '~/vue_shared/components/slot_switch.vue';
@@ -26,7 +27,9 @@ describe('SlotSwitch', () => {
});
it('throws an error if activeSlotNames is missing', () => {
- expect(createComponent).toThrow('[Vue warn]: Missing required prop: "activeSlotNames"');
+ expect(() => assertProps(SlotSwitch, {})).toThrow(
+ '[Vue warn]: Missing required prop: "activeSlotNames"',
+ );
});
it('renders no slots if activeSlotNames is empty', () => {
diff --git a/spec/frontend/vue_shared/components/source_viewer/components/chunk_line_spec.js b/spec/frontend/vue_shared/components/source_viewer/components/chunk_line_spec.js
index 6c8fc244fa0..9a38a96663d 100644
--- a/spec/frontend/vue_shared/components/source_viewer/components/chunk_line_spec.js
+++ b/spec/frontend/vue_shared/components/source_viewer/components/chunk_line_spec.js
@@ -10,16 +10,10 @@ const DEFAULT_PROPS = {
describe('Chunk Line component', () => {
let wrapper;
- const fileLineBlame = true;
const createComponent = (props = {}) => {
wrapper = shallowMountExtended(ChunkLine, {
propsData: { ...DEFAULT_PROPS, ...props },
- provide: {
- glFeatures: {
- fileLineBlame,
- },
- },
});
};
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 59880496d74..ff50326917f 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
@@ -11,7 +11,6 @@ describe('Chunk component', () => {
const createComponent = (props = {}) => {
wrapper = shallowMountExtended(Chunk, {
propsData: { ...CHUNK_1, ...props },
- provide: { glFeatures: { fileLineBlame: true } },
});
};
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
index c911e3d308b..4cec129b6e4 100644
--- 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
@@ -168,7 +168,7 @@ describe('Source Viewer component', () => {
});
describe('LineHighlighter', () => {
- it('instantiates the lineHighlighter class', async () => {
+ it('instantiates the lineHighlighter class', () => {
expect(LineHighlighter).toHaveBeenCalledWith({ scrollBehavior: 'auto' });
});
});
diff --git a/spec/frontend/vue_shared/components/split_button_spec.js b/spec/frontend/vue_shared/components/split_button_spec.js
index 6b869db4058..ffa25ae8448 100644
--- a/spec/frontend/vue_shared/components/split_button_spec.js
+++ b/spec/frontend/vue_shared/components/split_button_spec.js
@@ -2,6 +2,7 @@ import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
+import { assertProps } from 'helpers/assert_props';
import SplitButton from '~/vue_shared/components/split_button.vue';
const mockActionItems = [
@@ -42,12 +43,12 @@ describe('SplitButton', () => {
it('fails for empty actionItems', () => {
const actionItems = [];
- expect(() => createComponent({ actionItems })).toThrow();
+ expect(() => assertProps(SplitButton, { actionItems })).toThrow();
});
it('fails for single actionItems', () => {
const actionItems = [mockActionItems[0]];
- expect(() => createComponent({ actionItems })).toThrow();
+ expect(() => assertProps(SplitButton, { actionItems })).toThrow();
});
it('renders actionItems', () => {
diff --git a/spec/frontend/vue_shared/components/tooltip_on_truncate_spec.js b/spec/frontend/vue_shared/components/tooltip_on_truncate_spec.js
index 3807bb4cc63..f5da498a205 100644
--- a/spec/frontend/vue_shared/components/tooltip_on_truncate_spec.js
+++ b/spec/frontend/vue_shared/components/tooltip_on_truncate_spec.js
@@ -79,12 +79,12 @@ describe('TooltipOnTruncate component', () => {
};
describe('when truncated', () => {
- beforeEach(async () => {
+ beforeEach(() => {
hasHorizontalOverflow.mockReturnValueOnce(true);
createComponent();
});
- it('renders tooltip', async () => {
+ it('renders tooltip', () => {
expect(hasHorizontalOverflow).toHaveBeenLastCalledWith(wrapper.element);
expect(getTooltipValue()).toStrictEqual({
title: MOCK_TITLE,
@@ -96,7 +96,7 @@ describe('TooltipOnTruncate component', () => {
});
describe('with default target', () => {
- beforeEach(async () => {
+ beforeEach(() => {
hasHorizontalOverflow.mockReturnValueOnce(false);
createComponent();
});
diff --git a/spec/frontend/vue_shared/components/user_callout_dismisser_spec.js b/spec/frontend/vue_shared/components/user_callout_dismisser_spec.js
index b04e578c931..a4efbda06ce 100644
--- a/spec/frontend/vue_shared/components/user_callout_dismisser_spec.js
+++ b/spec/frontend/vue_shared/components/user_callout_dismisser_spec.js
@@ -31,18 +31,18 @@ describe('UserCalloutDismisser', () => {
const MOCK_FEATURE_NAME = 'mock_feature_name';
// Query handlers
- const successHandlerFactory = (dismissedCallouts = []) => async () =>
- userCalloutsResponse(dismissedCallouts);
- const anonUserHandler = async () => anonUserCalloutsResponse();
+ const successHandlerFactory = (dismissedCallouts = []) => () =>
+ Promise.resolve(userCalloutsResponse(dismissedCallouts));
+ const anonUserHandler = () => Promise.resolve(anonUserCalloutsResponse());
const errorHandler = () => Promise.reject(new Error('query error'));
const pendingHandler = () => new Promise(() => {});
// Mutation handlers
- const mutationSuccessHandlerSpy = jest.fn(async (variables) =>
- userCalloutMutationResponse(variables),
+ const mutationSuccessHandlerSpy = jest.fn((variables) =>
+ Promise.resolve(userCalloutMutationResponse(variables)),
);
- const mutationErrorHandlerSpy = jest.fn(async (variables) =>
- userCalloutMutationResponse(variables, ['mutation error']),
+ const mutationErrorHandlerSpy = jest.fn((variables) =>
+ Promise.resolve(userCalloutMutationResponse(variables, ['mutation error'])),
);
const defaultScopedSlotSpy = jest.fn();
diff --git a/spec/frontend/vue_shared/components/user_deletion_obstacles/user_deletion_obstacles_list_spec.js b/spec/frontend/vue_shared/components/user_deletion_obstacles/user_deletion_obstacles_list_spec.js
index 6491e5a66cd..d77e357a50c 100644
--- a/spec/frontend/vue_shared/components/user_deletion_obstacles/user_deletion_obstacles_list_spec.js
+++ b/spec/frontend/vue_shared/components/user_deletion_obstacles/user_deletion_obstacles_list_spec.js
@@ -61,7 +61,7 @@ describe('User deletion obstacles list', () => {
${true} | ${'You are currently a part of:'} | ${'Removing yourself may put your on-call team at risk of missing a notification.'}
${false} | ${`User ${userName} is currently part of:`} | ${'Removing this user may put their on-call team at risk of missing a notification.'}
`('when current user', ({ isCurrentUser, titleText, footerText }) => {
- it(`${isCurrentUser ? 'is' : 'is not'} a part of on-call management`, async () => {
+ it(`${isCurrentUser ? 'is' : 'is not'} a part of on-call management`, () => {
createComponent({
isCurrentUser,
});
diff --git a/spec/frontend/vue_shared/components/user_popover/user_popover_spec.js b/spec/frontend/vue_shared/components/user_popover/user_popover_spec.js
index 8ecab5cc043..41181ab9a68 100644
--- a/spec/frontend/vue_shared/components/user_popover/user_popover_spec.js
+++ b/spec/frontend/vue_shared/components/user_popover/user_popover_spec.js
@@ -1,5 +1,6 @@
import { GlSkeletonLoader, GlIcon } from '@gitlab/ui';
-import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
+import mrDiffCommentFixture from 'test_fixtures/merge_requests/diff_comment.html';
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import { sprintf } from '~/locale';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import { AVAILABILITY_STATUS } from '~/set_status_modal/constants';
@@ -41,12 +42,10 @@ const DEFAULT_PROPS = {
};
describe('User Popover Component', () => {
- const fixtureTemplate = 'merge_requests/diff_comment.html';
-
let wrapper;
beforeEach(() => {
- loadHTMLFixture(fixtureTemplate);
+ setHTMLFixture(mrDiffCommentFixture);
gon.features = {};
});
@@ -276,7 +275,7 @@ describe('User Popover Component', () => {
createWrapper({ user });
- expect(wrapper.findByText('(Busy)').exists()).toBe(true);
+ expect(wrapper.findByText('Busy').exists()).toBe(true);
});
it('should hide the busy status for any other status', () => {
@@ -287,7 +286,7 @@ describe('User Popover Component', () => {
createWrapper({ user });
- expect(wrapper.findByText('(Busy)').exists()).toBe(false);
+ expect(wrapper.findByText('Busy').exists()).toBe(false);
});
it('shows pronouns when user has them set', () => {
diff --git a/spec/frontend/vue_shared/components/vuex_module_provider_spec.js b/spec/frontend/vue_shared/components/vuex_module_provider_spec.js
index acbb931b7b6..e24c5a4609d 100644
--- a/spec/frontend/vue_shared/components/vuex_module_provider_spec.js
+++ b/spec/frontend/vue_shared/components/vuex_module_provider_spec.js
@@ -3,10 +3,10 @@ import Vue from 'vue';
import VueApollo from 'vue-apollo';
import VuexModuleProvider from '~/vue_shared/components/vuex_module_provider.vue';
-const TestComponent = Vue.extend({
+const TestComponent = {
inject: ['vuexModule'],
template: `<div data-testid="vuexModule">{{ vuexModule }}</div> `,
-});
+};
const TEST_VUEX_MODULE = 'testVuexModule';
@@ -32,6 +32,13 @@ describe('~/vue_shared/components/vuex_module_provider', () => {
expect(findProvidedVuexModule()).toBe(TEST_VUEX_MODULE);
});
+ it('provides "vuexModel" set from "vuex-module" prop when using @vue/compat', () => {
+ createComponent({
+ propsData: { 'vuex-module': TEST_VUEX_MODULE },
+ });
+ expect(findProvidedVuexModule()).toBe(TEST_VUEX_MODULE);
+ });
+
it('does not blow up when used with vue-apollo', () => {
// See https://github.com/vuejs/vue-apollo/pull/1153 for details
Vue.use(VueApollo);
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 f6eb11aaddf..4b2ce24a49f 100644
--- a/spec/frontend/vue_shared/components/web_ide_link_spec.js
+++ b/spec/frontend/vue_shared/components/web_ide_link_spec.js
@@ -345,7 +345,7 @@ describe('Web IDE link component', () => {
it.each(testActions)(
'emits the correct event when an action handler is called',
- async ({ props, expectedEventPayload }) => {
+ ({ props, expectedEventPayload }) => {
createComponent({ ...props, needsToFork: true, disableForkModal: true });
findActionsButton().props('actions')[0].handle();
@@ -354,7 +354,7 @@ describe('Web IDE link component', () => {
},
);
- it.each(testActions)('renders the fork confirmation modal', async ({ props }) => {
+ it.each(testActions)('renders the fork confirmation modal', ({ props }) => {
createComponent({ ...props, needsToFork: true });
expect(findForkConfirmModal().exists()).toBe(true);
diff --git a/spec/frontend/vue_shared/directives/track_event_spec.js b/spec/frontend/vue_shared/directives/track_event_spec.js
index 4bf84b06246..fc69e884258 100644
--- a/spec/frontend/vue_shared/directives/track_event_spec.js
+++ b/spec/frontend/vue_shared/directives/track_event_spec.js
@@ -1,50 +1,47 @@
import { shallowMount } from '@vue/test-utils';
-import Vue, { nextTick } from 'vue';
+import Vue from 'vue';
import Tracking from '~/tracking';
import TrackEvent from '~/vue_shared/directives/track_event';
jest.mock('~/tracking');
-const Component = Vue.component('DummyElement', {
- directives: {
- TrackEvent,
- },
- data() {
- return {
- trackingOptions: null,
- };
- },
- template: '<button id="trackable" v-track-event="trackingOptions"></button>',
-});
+describe('TrackEvent directive', () => {
+ let wrapper;
-let wrapper;
-let button;
+ const clickButton = () => wrapper.find('button').trigger('click');
-describe('Error Tracking directive', () => {
- beforeEach(() => {
- wrapper = shallowMount(Component);
- button = wrapper.find('#trackable');
- });
+ const createComponent = (trackingOptions) =>
+ Vue.component('DummyElement', {
+ directives: {
+ TrackEvent,
+ },
+ data() {
+ return {
+ trackingOptions,
+ };
+ },
+ template: '<button v-track-event="trackingOptions"></button>',
+ });
+
+ const mountComponent = (trackingOptions) => shallowMount(createComponent(trackingOptions));
+
+ it('does not track the event if required arguments are not provided', () => {
+ wrapper = mountComponent();
+ clickButton();
- it('should not track the event if required arguments are not provided', () => {
- button.trigger('click');
expect(Tracking.event).not.toHaveBeenCalled();
});
- it('should track event on click if tracking info provided', async () => {
- const trackingOptions = {
+ it('tracks event on click if tracking info provided', () => {
+ wrapper = mountComponent({
category: 'Tracking',
action: 'click_trackable_btn',
label: 'Trackable Info',
- };
-
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({ trackingOptions });
- const { category, action, label, property, value } = trackingOptions;
+ });
+ clickButton();
- await nextTick();
- button.trigger('click');
- expect(Tracking.event).toHaveBeenCalledWith(category, action, { label, property, value });
+ expect(Tracking.event).toHaveBeenCalledWith('Tracking', 'click_trackable_btn', {
+ label: 'Trackable Info',
+ });
});
});
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 61e6d2a420a..d5603d4ba4b 100644
--- a/spec/frontend/vue_shared/issuable/issuable_blocked_icon_spec.js
+++ b/spec/frontend/vue_shared/issuable/issuable_blocked_icon_spec.js
@@ -139,7 +139,7 @@ describe('IssuableBlockedIcon', () => {
expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
});
- it('should not query for blocking issuables by default', async () => {
+ it('should not query for blocking issuables by default', () => {
createWrapperWithApollo();
expect(findGlPopover().text()).not.toContain(mockBlockingIssue1.title);
@@ -195,18 +195,18 @@ describe('IssuableBlockedIcon', () => {
await mouseenter();
});
- it('should render a title of the issuable', async () => {
+ it('should render a title of the issuable', () => {
expect(findIssuableTitle().text()).toBe(mockBlockingIssue1.title);
});
- it('should render issuable reference and link to the issuable', async () => {
+ it('should render issuable reference and link to the issuable', () => {
const formattedRef = mockBlockingIssue1.reference.split('/')[1];
expect(findGlLink().text()).toBe(formattedRef);
expect(findGlLink().attributes('href')).toBe(mockBlockingIssue1.webUrl);
});
- it('should render popover title with correct blocking issuable count', async () => {
+ it('should render popover title with correct blocking issuable count', () => {
expect(findPopoverTitle().text()).toBe('Blocked by 1 issue');
});
});
@@ -241,7 +241,7 @@ describe('IssuableBlockedIcon', () => {
expect(wrapper.html()).toMatchSnapshot();
});
- it('should render popover title with correct blocking issuable count', async () => {
+ it('should render popover title with correct blocking issuable count', () => {
expect(findPopoverTitle().text()).toBe('Blocked by 4 issues');
});
@@ -249,7 +249,7 @@ describe('IssuableBlockedIcon', () => {
expect(findHiddenBlockingCount().text()).toBe('+ 1 more issue');
});
- it('should link to the blocked issue page at the related issue anchor', async () => {
+ it('should link to the blocked issue page at the related issue anchor', () => {
expect(findViewAllIssuableLink().text()).toBe('View all blocking issues');
expect(findViewAllIssuableLink().attributes('href')).toBe(
`${mockBlockedIssue2.webUrl}#related-issues`,
diff --git a/spec/frontend/vue_shared/issuable/list/components/issuable_item_spec.js b/spec/frontend/vue_shared/issuable/list/components/issuable_item_spec.js
index 45daf0dc34b..502fa609ebc 100644
--- a/spec/frontend/vue_shared/issuable/list/components/issuable_item_spec.js
+++ b/spec/frontend/vue_shared/issuable/list/components/issuable_item_spec.js
@@ -331,7 +331,7 @@ describe('IssuableItem', () => {
});
});
- it('renders spam icon when issuable is hidden', async () => {
+ it('renders spam icon when issuable is hidden', () => {
wrapper = createComponent({ issuable: { ...mockIssuable, hidden: true } });
const hiddenIcon = wrapper.findComponent(GlIcon);
diff --git a/spec/frontend/vue_shared/issuable/list/components/issuable_list_root_spec.js b/spec/frontend/vue_shared/issuable/list/components/issuable_list_root_spec.js
index 9a4636e0f4d..ec975dfdcb5 100644
--- a/spec/frontend/vue_shared/issuable/list/components/issuable_list_root_spec.js
+++ b/spec/frontend/vue_shared/issuable/list/components/issuable_list_root_spec.js
@@ -333,7 +333,7 @@ describe('IssuableListRoot', () => {
describe('alert', () => {
const error = 'oopsie!';
- it('shows alert when there is an error', () => {
+ it('shows an alert when there is an error', () => {
wrapper = createComponent({ props: { error } });
expect(findAlert().text()).toBe(error);
@@ -504,7 +504,7 @@ describe('IssuableListRoot', () => {
});
});
- it('has the page size change component', async () => {
+ it('has the page size change component', () => {
expect(findPageSizeSelector().exists()).toBe(true);
});
diff --git a/spec/frontend/vue_shared/issuable/list/mock_data.js b/spec/frontend/vue_shared/issuable/list/mock_data.js
index b67bd0f42fe..964b48f4275 100644
--- a/spec/frontend/vue_shared/issuable/list/mock_data.js
+++ b/spec/frontend/vue_shared/issuable/list/mock_data.js
@@ -60,6 +60,12 @@ export const mockIssuable = {
type: 'issue',
};
+export const mockIssuableItems = (n) =>
+ [...Array(n).keys()].map((i) => ({
+ id: i,
+ ...mockIssuable,
+ }));
+
export const mockIssuables = [
mockIssuable,
{
diff --git a/spec/frontend/vue_shared/issuable/show/components/issuable_body_spec.js b/spec/frontend/vue_shared/issuable/show/components/issuable_body_spec.js
index 7e665b7c76e..02e729a00bd 100644
--- a/spec/frontend/vue_shared/issuable/show/components/issuable_body_spec.js
+++ b/spec/frontend/vue_shared/issuable/show/components/issuable_body_spec.js
@@ -1,5 +1,5 @@
+import { GlLink } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import { nextTick } from 'vue';
import { useFakeDate } from 'helpers/fake_date';
import IssuableBody from '~/vue_shared/issuable/show/components/issuable_body.vue';
@@ -14,96 +14,76 @@ import { mockIssuableShowProps, mockIssuable } from '../mock_data';
jest.mock('~/autosave');
jest.mock('~/alert');
+jest.mock('~/task_list');
const issuableBodyProps = {
...mockIssuableShowProps,
issuable: mockIssuable,
};
-const createComponent = (propsData = issuableBodyProps) =>
- shallowMount(IssuableBody, {
- propsData,
- stubs: {
- IssuableTitle,
- IssuableDescription,
- IssuableEditForm,
- TimeAgoTooltip,
- },
- slots: {
- 'status-badge': 'Open',
- 'edit-form-actions': `
- <button class="js-save">Save changes</button>
- <button class="js-cancel">Cancel</button>
- `,
- },
- });
-
describe('IssuableBody', () => {
// Some assertions expect a date later than our default
useFakeDate(2020, 11, 11);
let wrapper;
+ const createComponent = (propsData = {}) => {
+ wrapper = shallowMount(IssuableBody, {
+ propsData: {
+ ...issuableBodyProps,
+ ...propsData,
+ },
+ stubs: {
+ IssuableTitle,
+ IssuableDescription,
+ IssuableEditForm,
+ TimeAgoTooltip,
+ },
+ slots: {
+ 'status-badge': 'Open',
+ 'edit-form-actions': `
+ <button class="js-save">Save changes</button>
+ <button class="js-cancel">Cancel</button>
+ `,
+ },
+ });
+ };
+
+ const findUpdatedLink = () => wrapper.findComponent(GlLink);
+ const findIssuableEditForm = () => wrapper.findComponent(IssuableEditForm);
+ const findIssuableEditFormButton = (type) => findIssuableEditForm().find(`button.js-${type}`);
+ const findIssuableTitle = () => wrapper.findComponent(IssuableTitle);
+
beforeEach(() => {
- wrapper = createComponent();
+ createComponent();
+ TaskList.mockClear();
});
describe('computed', () => {
- describe('isUpdated', () => {
- it.each`
- updatedAt | returnValue
- ${mockIssuable.updatedAt} | ${true}
- ${null} | ${false}
- ${''} | ${false}
- `(
- 'returns $returnValue when value of `updateAt` prop is `$updatedAt`',
- async ({ updatedAt, returnValue }) => {
- wrapper.setProps({
- issuable: {
- ...mockIssuable,
- updatedAt,
- },
- });
-
- await nextTick();
-
- expect(wrapper.vm.isUpdated).toBe(returnValue);
- },
- );
- });
-
describe('updatedBy', () => {
it('returns value of `issuable.updatedBy`', () => {
- expect(wrapper.vm.updatedBy).toBe(mockIssuable.updatedBy);
+ expect(findUpdatedLink().text()).toBe(mockIssuable.updatedBy.name);
+ expect(findUpdatedLink().attributes('href')).toBe(mockIssuable.updatedBy.webUrl);
});
});
});
describe('watchers', () => {
describe('editFormVisible', () => {
- it('calls initTaskList in nextTick', async () => {
- jest.spyOn(wrapper.vm, 'initTaskList');
- wrapper.setProps({
- editFormVisible: true,
- });
-
- await nextTick();
-
- wrapper.setProps({
+ it('calls initTaskList in nextTick', () => {
+ createComponent({
editFormVisible: false,
});
- await nextTick();
-
- expect(wrapper.vm.initTaskList).toHaveBeenCalled();
+ expect(TaskList).toHaveBeenCalled();
});
});
});
describe('mounted', () => {
it('initializes TaskList instance when enabledEdit and enableTaskList props are true', () => {
- expect(wrapper.vm.taskList instanceof TaskList).toBe(true);
- expect(wrapper.vm.taskList).toMatchObject({
+ createComponent();
+ expect(TaskList).toHaveBeenCalledWith({
dataType: 'issue',
fieldName: 'description',
lockVersion: issuableBodyProps.taskListLockVersion,
@@ -114,14 +94,12 @@ describe('IssuableBody', () => {
});
it('does not initialize TaskList instance when either enabledEdit or enableTaskList prop is false', () => {
- const wrapperNoTaskList = createComponent({
+ createComponent({
...issuableBodyProps,
enableTaskList: false,
});
- expect(wrapperNoTaskList.vm.taskList).not.toBeDefined();
-
- wrapperNoTaskList.destroy();
+ expect(TaskList).toHaveBeenCalledTimes(0);
});
});
@@ -150,10 +128,8 @@ describe('IssuableBody', () => {
describe('template', () => {
it('renders issuable-title component', () => {
- const titleEl = wrapper.findComponent(IssuableTitle);
-
- expect(titleEl.exists()).toBe(true);
- expect(titleEl.props()).toMatchObject({
+ expect(findIssuableTitle().exists()).toBe(true);
+ expect(findIssuableTitle().props()).toMatchObject({
issuable: issuableBodyProps.issuable,
statusIcon: issuableBodyProps.statusIcon,
enableEdit: issuableBodyProps.enableEdit,
@@ -168,42 +144,37 @@ describe('IssuableBody', () => {
});
it('renders issuable edit info', () => {
- const editedEl = wrapper.find('small');
-
- expect(editedEl.text()).toMatchInterpolatedText('Edited 3 months ago by Administrator');
+ expect(wrapper.find('small').text()).toMatchInterpolatedText(
+ 'Edited 3 months ago by Administrator',
+ );
});
- it('renders issuable-edit-form when `editFormVisible` prop is true', async () => {
- wrapper.setProps({
+ it('renders issuable-edit-form when `editFormVisible` prop is true', () => {
+ createComponent({
editFormVisible: true,
});
- await nextTick();
-
- const editFormEl = wrapper.findComponent(IssuableEditForm);
- expect(editFormEl.exists()).toBe(true);
- expect(editFormEl.props()).toMatchObject({
+ expect(findIssuableEditForm().exists()).toBe(true);
+ expect(findIssuableEditForm().props()).toMatchObject({
issuable: issuableBodyProps.issuable,
enableAutocomplete: issuableBodyProps.enableAutocomplete,
descriptionPreviewPath: issuableBodyProps.descriptionPreviewPath,
descriptionHelpPath: issuableBodyProps.descriptionHelpPath,
});
- expect(editFormEl.find('button.js-save').exists()).toBe(true);
- expect(editFormEl.find('button.js-cancel').exists()).toBe(true);
+ expect(findIssuableEditFormButton('save').exists()).toBe(true);
+ expect(findIssuableEditFormButton('cancel').exists()).toBe(true);
});
describe('events', () => {
it('component emits `edit-issuable` event bubbled via issuable-title', () => {
- const issuableTitle = wrapper.findComponent(IssuableTitle);
-
- issuableTitle.vm.$emit('edit-issuable');
+ findIssuableTitle().vm.$emit('edit-issuable');
expect(wrapper.emitted('edit-issuable')).toHaveLength(1);
});
it.each(['keydown-title', 'keydown-description'])(
'component emits `%s` event with event object and issuableMeta params via issuable-edit-form',
- async (eventName) => {
+ (eventName) => {
const eventObj = {
preventDefault: jest.fn(),
stopPropagation: jest.fn(),
@@ -213,15 +184,11 @@ describe('IssuableBody', () => {
issuableDescription: 'foobar',
};
- wrapper.setProps({
+ createComponent({
editFormVisible: true,
});
- await nextTick();
-
- const issuableEditForm = wrapper.findComponent(IssuableEditForm);
-
- issuableEditForm.vm.$emit(eventName, eventObj, issuableMeta);
+ findIssuableEditForm().vm.$emit(eventName, eventObj, issuableMeta);
expect(wrapper.emitted(eventName)).toHaveLength(1);
expect(wrapper.emitted(eventName)[0]).toMatchObject([eventObj, issuableMeta]);
diff --git a/spec/frontend/vue_shared/issuable/show/components/issuable_edit_form_spec.js b/spec/frontend/vue_shared/issuable/show/components/issuable_edit_form_spec.js
index 0d6cd1ad00b..4a52c2a8dad 100644
--- a/spec/frontend/vue_shared/issuable/show/components/issuable_edit_form_spec.js
+++ b/spec/frontend/vue_shared/issuable/show/components/issuable_edit_form_spec.js
@@ -165,7 +165,7 @@ describe('IssuableEditForm', () => {
stopPropagation: jest.fn(),
};
- it('component emits `keydown-title` event with event object and issuableMeta params via gl-form-input', async () => {
+ it('component emits `keydown-title` event with event object and issuableMeta params via gl-form-input', () => {
const titleInputEl = wrapper.findComponent(GlFormInput);
titleInputEl.vm.$emit('keydown', eventObj, 'title');
@@ -179,7 +179,7 @@ describe('IssuableEditForm', () => {
]);
});
- it('component emits `keydown-description` event with event object and issuableMeta params via textarea', async () => {
+ it('component emits `keydown-description` event with event object and issuableMeta params via textarea', () => {
const descriptionInputEl = wrapper.find('[data-testid="description"] textarea');
descriptionInputEl.trigger('keydown', eventObj, 'description');
diff --git a/spec/frontend/vue_shared/issuable/show/components/issuable_header_spec.js b/spec/frontend/vue_shared/issuable/show/components/issuable_header_spec.js
index d9f1b6c15a8..fa38ab8d44d 100644
--- a/spec/frontend/vue_shared/issuable/show/components/issuable_header_spec.js
+++ b/spec/frontend/vue_shared/issuable/show/components/issuable_header_spec.js
@@ -1,4 +1,4 @@
-import { GlBadge, GlIcon, GlAvatarLabeled } from '@gitlab/ui';
+import { GlButton, GlBadge, GlIcon, GlAvatarLabeled, GlAvatarLink } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import IssuableHeader from '~/vue_shared/issuable/show/components/issuable_header.vue';
@@ -13,7 +13,10 @@ const issuableHeaderProps = {
describe('IssuableHeader', () => {
let wrapper;
+ const findAvatar = () => wrapper.findByTestId('avatar');
const findTaskStatusEl = () => wrapper.findByTestId('task-status');
+ const findButton = () => wrapper.findComponent(GlButton);
+ const findGlAvatarLink = () => wrapper.findComponent(GlAvatarLink);
const createComponent = (props = {}, { stubs } = {}) => {
wrapper = shallowMountExtended(IssuableHeader, {
@@ -40,7 +43,7 @@ describe('IssuableHeader', () => {
describe('authorId', () => {
it('returns numeric ID from GraphQL ID of `author` prop', () => {
createComponent();
- expect(wrapper.vm.authorId).toBe(1);
+ expect(findGlAvatarLink().attributes('data-user-id')).toBe('1');
});
});
});
@@ -52,12 +55,14 @@ describe('IssuableHeader', () => {
it('dispatches `click` event on sidebar toggle button', () => {
createComponent();
- wrapper.vm.toggleSidebarButtonEl = document.querySelector('.js-toggle-right-sidebar-button');
- jest.spyOn(wrapper.vm.toggleSidebarButtonEl, 'dispatchEvent').mockImplementation(jest.fn);
+ const toggleSidebarButtonEl = document.querySelector('.js-toggle-right-sidebar-button');
+ const dispatchEvent = jest
+ .spyOn(toggleSidebarButtonEl, 'dispatchEvent')
+ .mockImplementation(jest.fn);
- wrapper.vm.handleRightSidebarToggleClick();
+ findButton().vm.$emit('click');
- expect(wrapper.vm.toggleSidebarButtonEl.dispatchEvent).toHaveBeenCalledWith(
+ expect(dispatchEvent).toHaveBeenCalledWith(
expect.objectContaining({
type: 'click',
}),
@@ -77,7 +82,7 @@ describe('IssuableHeader', () => {
expect(statusBoxEl.text()).toContain('Open');
});
- it('renders blocked icon when issuable is blocked', async () => {
+ it('renders blocked icon when issuable is blocked', () => {
createComponent({
blocked: true,
});
@@ -88,7 +93,7 @@ describe('IssuableHeader', () => {
expect(blockedEl.findComponent(GlIcon).props('name')).toBe('lock');
});
- it('renders confidential icon when issuable is confidential', async () => {
+ it('renders confidential icon when issuable is confidential', () => {
createComponent({
confidential: true,
});
@@ -109,7 +114,7 @@ describe('IssuableHeader', () => {
href: webUrl,
target: '_blank',
};
- const avatarEl = wrapper.findByTestId('avatar');
+ const avatarEl = findAvatar();
expect(avatarEl.exists()).toBe(true);
expect(avatarEl.attributes()).toMatchObject(avatarElAttrs);
expect(avatarEl.findComponent(GlAvatarLabeled).attributes()).toMatchObject({
diff --git a/spec/frontend/vue_shared/security_reports/components/manage_via_mr_spec.js b/spec/frontend/vue_shared/security_reports/components/manage_via_mr_spec.js
index 6345393951c..5cdb6612487 100644
--- a/spec/frontend/vue_shared/security_reports/components/manage_via_mr_spec.js
+++ b/spec/frontend/vue_shared/security_reports/components/manage_via_mr_spec.js
@@ -8,7 +8,9 @@ import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { humanize } from '~/lib/utils/text_utility';
import { redirectTo } from '~/lib/utils/url_utility';
-import ManageViaMr from '~/vue_shared/security_configuration/components/manage_via_mr.vue';
+import ManageViaMr, {
+ i18n,
+} from '~/vue_shared/security_configuration/components/manage_via_mr.vue';
import { REPORT_TYPE_SAST } from '~/vue_shared/security_reports/constants';
import { buildConfigureSecurityFeatureMockFactory } from './apollo_mocks';
@@ -17,6 +19,7 @@ jest.mock('~/lib/utils/url_utility');
Vue.use(VueApollo);
const projectFullPath = 'namespace/project';
+const ufErrorPrefix = 'Foo:';
describe('ManageViaMr component', () => {
let wrapper;
@@ -56,6 +59,10 @@ describe('ManageViaMr component', () => {
);
}
+ beforeEach(() => {
+ gon.uf_error_prefix = ufErrorPrefix;
+ });
+
// This component supports different report types/mutations depending on
// whether it's in a CE or EE context. This makes sure we are only testing
// the ones available in the current test context.
@@ -72,15 +79,19 @@ describe('ManageViaMr component', () => {
const buildConfigureSecurityFeatureMock = buildConfigureSecurityFeatureMockFactory(
mutationId,
);
- const successHandler = jest.fn(async () => buildConfigureSecurityFeatureMock());
- const noSuccessPathHandler = async () =>
+ const successHandler = jest.fn().mockResolvedValue(buildConfigureSecurityFeatureMock());
+ const noSuccessPathHandler = jest.fn().mockResolvedValue(
buildConfigureSecurityFeatureMock({
successPath: '',
- });
- const errorHandler = async () =>
- buildConfigureSecurityFeatureMock({
- errors: ['foo'],
- });
+ }),
+ );
+ const errorHandler = (message = 'foo') => {
+ return Promise.resolve(
+ buildConfigureSecurityFeatureMock({
+ errors: [message],
+ }),
+ );
+ };
const pendingHandler = () => new Promise(() => {});
describe('when feature is configured', () => {
@@ -147,9 +158,12 @@ describe('ManageViaMr component', () => {
});
describe.each`
- handler | message
- ${noSuccessPathHandler} | ${`${featureName} merge request creation mutation failed`}
- ${errorHandler} | ${'foo'}
+ handler | message
+ ${noSuccessPathHandler} | ${`${featureName} merge request creation mutation failed`}
+ ${errorHandler.bind(null, `${ufErrorPrefix} message`)} | ${'message'}
+ ${errorHandler.bind(null, 'Blah: message')} | ${i18n.genericErrorText}
+ ${errorHandler.bind(null, 'message')} | ${i18n.genericErrorText}
+ ${errorHandler} | ${i18n.genericErrorText}
`('given an error response', ({ handler, message }) => {
beforeEach(() => {
const apolloProvider = createMockApolloProvider(mutation, handler);
diff --git a/spec/frontend/webhooks/components/push_events_spec.js b/spec/frontend/webhooks/components/push_events_spec.js
index ccb61c4049a..6889d48e904 100644
--- a/spec/frontend/webhooks/components/push_events_spec.js
+++ b/spec/frontend/webhooks/components/push_events_spec.js
@@ -61,7 +61,7 @@ describe('Webhook push events form editor component', () => {
await nextTick();
});
- it('all_branches should be selected by default', async () => {
+ it('all_branches should be selected by default', () => {
expect(findPushEventRulesGroup().element).toMatchSnapshot();
});
diff --git a/spec/frontend/webhooks/components/test_dropdown_spec.js b/spec/frontend/webhooks/components/test_dropdown_spec.js
index 2f62ca13469..36777b0ba64 100644
--- a/spec/frontend/webhooks/components/test_dropdown_spec.js
+++ b/spec/frontend/webhooks/components/test_dropdown_spec.js
@@ -1,6 +1,6 @@
import { GlDisclosureDropdown } from '@gitlab/ui';
-import { mount } from '@vue/test-utils';
-import { getByRole } from '@testing-library/dom';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+
import HookTestDropdown from '~/webhooks/components/test_dropdown.vue';
const mockItems = [
@@ -14,17 +14,14 @@ 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, {
+ wrapper = mountExtended(HookTestDropdown, {
propsData: {
items: mockItems,
...props,
},
+ attachTo: document.body,
});
};
@@ -55,7 +52,7 @@ describe('HookTestDropdown', () => {
});
});
- clickItem(mockItems[0].text);
+ wrapper.findByTestId('disclosure-dropdown-item').find('a').trigger('click');
return railsEventPromise;
});
diff --git a/spec/frontend/whats_new/utils/notification_spec.js b/spec/frontend/whats_new/utils/notification_spec.js
index dac02ee07bd..8b5663ee764 100644
--- a/spec/frontend/whats_new/utils/notification_spec.js
+++ b/spec/frontend/whats_new/utils/notification_spec.js
@@ -1,4 +1,5 @@
-import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
+import htmlWhatsNewNotification from 'test_fixtures_static/whats_new_notification.html';
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import { useLocalStorageSpy } from 'helpers/local_storage_helper';
import { setNotification, getVersionDigest } from '~/whats_new/utils/notification';
@@ -12,7 +13,7 @@ describe('~/whats_new/utils/notification', () => {
const getAppEl = () => wrapper.querySelector('.app');
beforeEach(() => {
- loadHTMLFixture('static/whats_new_notification.html');
+ setHTMLFixture(htmlWhatsNewNotification);
wrapper = document.querySelector('.whats-new-notification-fixture-root');
});
diff --git a/spec/frontend/work_items/components/item_title_spec.js b/spec/frontend/work_items/components/item_title_spec.js
index aef310319ab..3a84ba4bd5e 100644
--- a/spec/frontend/work_items/components/item_title_spec.js
+++ b/spec/frontend/work_items/components/item_title_spec.js
@@ -47,7 +47,7 @@ describe('ItemTitle', () => {
expect(wrapper.emitted(eventName)).toBeDefined();
});
- it('renders only the text content from clipboard', async () => {
+ it('renders only the text content from clipboard', () => {
const htmlContent = '<strong>bold text</strong>';
const buildClipboardData = (data = {}) => ({
clipboardData: {
diff --git a/spec/frontend/work_items/components/notes/work_item_add_note_spec.js b/spec/frontend/work_items/components/notes/work_item_add_note_spec.js
index 2a65e91a906..a97164f9dce 100644
--- a/spec/frontend/work_items/components/notes/work_item_add_note_spec.js
+++ b/spec/frontend/work_items/components/notes/work_item_add_note_spec.js
@@ -1,7 +1,6 @@
-import { GlButton } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import { mockTracking } from 'helpers/tracking_helper';
import waitForPromises from 'helpers/wait_for_promises';
@@ -28,7 +27,7 @@ jest.mock('~/lib/utils/autosave');
const workItemId = workItemQueryResponse.data.workItem.id;
-describe('WorkItemCommentForm', () => {
+describe('Work item add note', () => {
let wrapper;
Vue.use(VueApollo);
@@ -38,6 +37,7 @@ describe('WorkItemCommentForm', () => {
let workItemResponseHandler;
const findCommentForm = () => wrapper.findComponent(WorkItemCommentForm);
+ const findTextarea = () => wrapper.findByTestId('note-reply-textarea');
const createComponent = async ({
mutationHandler = mutationSuccessHandler,
@@ -50,7 +50,6 @@ describe('WorkItemCommentForm', () => {
workItemType = 'Task',
} = {}) => {
workItemResponseHandler = jest.fn().mockResolvedValue(workItemResponse);
-
if (signedIn) {
window.gon.current_user_id = '1';
window.gon.current_user_avatar_url = 'avatar.png';
@@ -76,7 +75,7 @@ describe('WorkItemCommentForm', () => {
});
const { id } = workItemQueryResponse.data.workItem;
- wrapper = shallowMount(WorkItemAddNote, {
+ wrapper = shallowMountExtended(WorkItemAddNote, {
apolloProvider,
propsData: {
workItemId: id,
@@ -84,6 +83,8 @@ describe('WorkItemCommentForm', () => {
queryVariables,
fetchByIid,
workItemType,
+ markdownPreviewPath: '/group/project/preview_markdown?target_type=WorkItem',
+ autocompleteDataSources: {},
},
stubs: {
WorkItemCommentLocked,
@@ -93,7 +94,7 @@ describe('WorkItemCommentForm', () => {
await waitForPromises();
if (isEditing) {
- wrapper.findComponent(GlButton).vm.$emit('click');
+ findTextarea().trigger('click');
}
};
@@ -209,6 +210,38 @@ describe('WorkItemCommentForm', () => {
expect(wrapper.emitted('error')).toEqual([[error]]);
});
+
+ it('ignores errors when mutation returns additional information as errors for quick actions', async () => {
+ await createComponent({
+ isEditing: true,
+ mutationHandler: jest.fn().mockResolvedValue({
+ data: {
+ createNote: {
+ note: {
+ id: 'gid://gitlab/Discussion/c872ba2d7d3eb780d2255138d67ca8b04f65b122',
+ discussion: {
+ id: 'gid://gitlab/Discussion/c872ba2d7d3eb780d2255138d67ca8b04f65b122',
+ notes: {
+ nodes: [],
+ __typename: 'NoteConnection',
+ },
+ __typename: 'Discussion',
+ },
+ __typename: 'Note',
+ },
+ __typename: 'CreateNotePayload',
+ errors: ['Commands only Removed assignee @foobar.', 'Command names ["unassign"]'],
+ },
+ },
+ }),
+ });
+
+ findCommentForm().vm.$emit('submitForm', 'updated desc');
+
+ await waitForPromises();
+
+ expect(clearDraft).toHaveBeenCalledWith('gid://gitlab/WorkItem/1-comment');
+ });
});
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
index 23a9f285804..147f2904761 100644
--- a/spec/frontend/work_items/components/notes/work_item_comment_form_spec.js
+++ b/spec/frontend/work_items/components/notes/work_item_comment_form_spec.js
@@ -1,11 +1,23 @@
import { shallowMount } from '@vue/test-utils';
-import { nextTick } from 'vue';
+import Vue, { nextTick } from 'vue';
+import VueApollo from 'vue-apollo';
import waitForPromises from 'helpers/wait_for_promises';
import * as autosave from '~/lib/utils/autosave';
import { ESC_KEY, ENTER_KEY } from '~/lib/utils/keys';
+import {
+ STATE_OPEN,
+ STATE_CLOSED,
+ STATE_EVENT_REOPEN,
+ STATE_EVENT_CLOSE,
+} from '~/work_items/constants';
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';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
+import { updateWorkItemMutationResponse, workItemQueryResponse } from 'jest/work_items/mock_data';
+
+Vue.use(VueApollo);
const draftComment = 'draft comment';
@@ -18,6 +30,8 @@ jest.mock('~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal', () => ({
confirmAction: jest.fn().mockResolvedValue(true),
}));
+const workItemId = 'gid://gitlab/WorkItem/1';
+
describe('Work item comment form component', () => {
let wrapper;
@@ -27,14 +41,29 @@ describe('Work item comment form component', () => {
const findCancelButton = () => wrapper.find('[data-testid="cancel-button"]');
const findConfirmButton = () => wrapper.find('[data-testid="confirm-button"]');
- const createComponent = ({ isSubmitting = false, initialValue = '' } = {}) => {
+ const mutationSuccessHandler = jest.fn().mockResolvedValue(updateWorkItemMutationResponse);
+
+ const createComponent = ({
+ isSubmitting = false,
+ initialValue = '',
+ isNewDiscussion = false,
+ workItemState = STATE_OPEN,
+ workItemType = 'Task',
+ mutationHandler = mutationSuccessHandler,
+ } = {}) => {
wrapper = shallowMount(WorkItemCommentForm, {
+ apolloProvider: createMockApollo([[updateWorkItemMutation, mutationHandler]]),
propsData: {
- workItemType: 'Issue',
+ workItemState,
+ workItemId,
+ workItemType,
ariaLabel: 'test-aria-label',
autosaveKey: mockAutosaveKey,
isSubmitting,
initialValue,
+ markdownPreviewPath: '/group/project/preview_markdown?target_type=WorkItem',
+ autocompleteDataSources: {},
+ isNewDiscussion,
},
provide: {
fullPath: 'test-project-path',
@@ -42,11 +71,11 @@ describe('Work item comment form component', () => {
});
};
- it('passes correct markdown preview path to markdown editor', () => {
+ it('passes markdown preview path to markdown editor', () => {
createComponent();
expect(findMarkdownEditor().props('renderMarkdownPath')).toBe(
- '/test-project-path/preview_markdown?target_type=Issue',
+ '/group/project/preview_markdown?target_type=WorkItem',
);
});
@@ -99,7 +128,7 @@ describe('Work item comment form component', () => {
expect(findMarkdownEditor().props('value')).toBe('new comment');
});
- it('calls `updateDraft` with correct parameters', async () => {
+ it('calls `updateDraft` with correct parameters', () => {
findMarkdownEditor().vm.$emit('input', 'new comment');
expect(autosave.updateDraft).toHaveBeenCalledWith(mockAutosaveKey, 'new comment');
@@ -161,4 +190,63 @@ describe('Work item comment form component', () => {
expect(wrapper.emitted('submitForm')).toEqual([[draftComment]]);
});
+
+ describe('when used as a top level/is a new discussion', () => {
+ describe('cancel button text', () => {
+ it.each`
+ workItemState | workItemType | buttonText
+ ${STATE_OPEN} | ${'Task'} | ${'Close task'}
+ ${STATE_CLOSED} | ${'Task'} | ${'Reopen task'}
+ ${STATE_OPEN} | ${'Objective'} | ${'Close objective'}
+ ${STATE_CLOSED} | ${'Objective'} | ${'Reopen objective'}
+ ${STATE_OPEN} | ${'Key result'} | ${'Close key result'}
+ ${STATE_CLOSED} | ${'Key result'} | ${'Reopen key result'}
+ `(
+ 'is "$buttonText" when "$workItemType" state is "$workItemState"',
+ ({ workItemState, workItemType, buttonText }) => {
+ createComponent({ isNewDiscussion: true, workItemState, workItemType });
+
+ expect(findCancelButton().text()).toBe(buttonText);
+ },
+ );
+ });
+
+ describe('Close/reopen button click', () => {
+ it.each`
+ workItemState | stateEvent
+ ${STATE_OPEN} | ${STATE_EVENT_CLOSE}
+ ${STATE_CLOSED} | ${STATE_EVENT_REOPEN}
+ `(
+ 'calls mutation with "$stateEvent" when workItemState is "$workItemState"',
+ async ({ workItemState, stateEvent }) => {
+ createComponent({ isNewDiscussion: true, workItemState });
+
+ findCancelButton().vm.$emit('click');
+
+ await waitForPromises();
+
+ expect(mutationSuccessHandler).toHaveBeenCalledWith({
+ input: {
+ id: workItemQueryResponse.data.workItem.id,
+ stateEvent,
+ },
+ });
+ },
+ );
+
+ it('emits an error message when the mutation was unsuccessful', async () => {
+ createComponent({
+ isNewDiscussion: true,
+ mutationHandler: jest.fn().mockRejectedValue('Error!'),
+ });
+ findCancelButton().vm.$emit('click');
+
+ await waitForPromises();
+
+ expect(wrapper.emitted('error')).toEqual([
+ ['Something went wrong while updating the task. Please try again.'],
+ ]);
+ });
+ });
+ });
});
diff --git a/spec/frontend/work_items/components/notes/work_item_discussion_spec.js b/spec/frontend/work_items/components/notes/work_item_discussion_spec.js
index 6b95da0910b..568b76150c4 100644
--- a/spec/frontend/work_items/components/notes/work_item_discussion_spec.js
+++ b/spec/frontend/work_items/components/notes/work_item_discussion_spec.js
@@ -41,6 +41,8 @@ describe('Work Item Discussion', () => {
fetchByIid,
fullPath,
workItemType,
+ markdownPreviewPath: '/group/project/preview_markdown?target_type=WorkItem',
+ autocompleteDataSources: {},
},
});
};
@@ -70,7 +72,7 @@ describe('Work Item Discussion', () => {
expect(findToggleRepliesWidget().exists()).toBe(true);
});
- it('the number of threads should be equal to the response length', async () => {
+ it('the number of threads should be equal to the response length', () => {
expect(findAllThreads()).toHaveLength(
mockWorkItemNotesWidgetResponseWithComments.discussions.nodes[0].notes.nodes.length,
);
@@ -104,7 +106,7 @@ describe('Work Item Discussion', () => {
await findWorkItemAddNote().vm.$emit('replying', 'reply text');
});
- it('should show optimistic behavior when replying', async () => {
+ it('should show optimistic behavior when replying', () => {
expect(findAllThreads()).toHaveLength(2);
expect(findWorkItemNoteReplying().exists()).toBe(true);
});
diff --git a/spec/frontend/work_items/components/notes/work_item_note_actions_spec.js b/spec/frontend/work_items/components/notes/work_item_note_actions_spec.js
index b293127b6af..b406c9d843a 100644
--- a/spec/frontend/work_items/components/notes/work_item_note_actions_spec.js
+++ b/spec/frontend/work_items/components/notes/work_item_note_actions_spec.js
@@ -1,3 +1,4 @@
+import { GlDropdown } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
@@ -17,6 +18,10 @@ describe('Work Item Note Actions', () => {
const findReplyButton = () => wrapper.findComponent(ReplyButton);
const findEditButton = () => wrapper.find('[data-testid="edit-work-item-note"]');
const findEmojiButton = () => wrapper.find('[data-testid="note-emoji-button"]');
+ const findDropdown = () => wrapper.findComponent(GlDropdown);
+ const findDeleteNoteButton = () => wrapper.find('[data-testid="delete-note-action"]');
+ const findCopyLinkButton = () => wrapper.find('[data-testid="copy-link-action"]');
+ const findAssignUnassignButton = () => wrapper.find('[data-testid="assign-note-action"]');
const addEmojiMutationResolver = jest.fn().mockResolvedValue({
data: {
@@ -29,13 +34,19 @@ describe('Work Item Note Actions', () => {
template: '<div></div>',
};
- const createComponent = ({ showReply = true, showEdit = true, showAwardEmoji = true } = {}) => {
+ const createComponent = ({
+ showReply = true,
+ showEdit = true,
+ showAwardEmoji = true,
+ showAssignUnassign = false,
+ } = {}) => {
wrapper = shallowMount(WorkItemNoteActions, {
propsData: {
showReply,
showEdit,
noteId,
showAwardEmoji,
+ showAssignUnassign,
},
provide: {
glFeatures: {
@@ -113,4 +124,75 @@ describe('Work Item Note Actions', () => {
});
});
});
+
+ describe('delete note', () => {
+ it('should display the `Delete comment` dropdown item if user has a permission to delete a note', () => {
+ createComponent({
+ showEdit: true,
+ });
+
+ expect(findDropdown().exists()).toBe(true);
+ expect(findDeleteNoteButton().exists()).toBe(true);
+ });
+
+ it('should not display the `Delete comment` dropdown item if user has no permission to delete a note', () => {
+ createComponent({
+ showEdit: false,
+ });
+
+ expect(findDropdown().exists()).toBe(true);
+ expect(findDeleteNoteButton().exists()).toBe(false);
+ });
+
+ it('should emit `deleteNote` event when delete note action is clicked', () => {
+ createComponent({
+ showEdit: true,
+ });
+
+ findDeleteNoteButton().vm.$emit('click');
+
+ expect(wrapper.emitted('deleteNote')).toEqual([[]]);
+ });
+ });
+
+ describe('copy link', () => {
+ beforeEach(() => {
+ createComponent({});
+ });
+ it('should display Copy link always', () => {
+ expect(findCopyLinkButton().exists()).toBe(true);
+ });
+
+ it('should emit `notifyCopyDone` event when copy link note action is clicked', () => {
+ findCopyLinkButton().vm.$emit('click');
+
+ expect(wrapper.emitted('notifyCopyDone')).toEqual([[]]);
+ });
+ });
+
+ describe('assign/unassign to commenting user', () => {
+ it('should not display assign/unassign by default', () => {
+ createComponent();
+
+ expect(findAssignUnassignButton().exists()).toBe(false);
+ });
+
+ it('should display assign/unassign when the props is true', () => {
+ createComponent({
+ showAssignUnassign: true,
+ });
+
+ expect(findAssignUnassignButton().exists()).toBe(true);
+ });
+
+ it('should emit `assignUser` event when assign note action is clicked', () => {
+ createComponent({
+ showAssignUnassign: true,
+ });
+
+ findAssignUnassignButton().vm.$emit('click');
+
+ expect(wrapper.emitted('assignUser')).toEqual([[]]);
+ });
+ });
});
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 8e574dc1a81..69b7c7b0828 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,10 +1,9 @@
-import { GlAvatarLink, GlDropdown } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
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 { updateDraft, clearDraft } 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';
@@ -12,8 +11,17 @@ 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 workItemQuery from '~/work_items/graphql/work_item.query.graphql';
import updateWorkItemNoteMutation from '~/work_items/graphql/notes/update_work_item_note.mutation.graphql';
-import { mockWorkItemCommentNote } from 'jest/work_items/mock_data';
+import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
+import {
+ mockAssignees,
+ mockWorkItemCommentNote,
+ updateWorkItemMutationResponse,
+ workItemQueryResponse,
+} from 'jest/work_items/mock_data';
+import { i18n, TRACKING_CATEGORY_SHOW } from '~/work_items/constants';
+import { mockTracking } from 'helpers/tracking_helper';
Vue.use(VueApollo);
jest.mock('~/lib/utils/autosave');
@@ -22,6 +30,7 @@ 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 mockWorkItemId = workItemQueryResponse.data.workItem.id;
const successHandler = jest.fn().mockResolvedValue({
data: {
@@ -35,32 +44,51 @@ describe('Work Item Note', () => {
},
},
});
+
+ const workItemResponseHandler = jest.fn().mockResolvedValue(workItemQueryResponse);
+
+ const updateWorkItemMutationSuccessHandler = jest
+ .fn()
+ .mockResolvedValue(updateWorkItemMutationResponse);
+
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 findNoteActions = () => wrapper.findComponent(NoteActions);
- const findDropdown = () => wrapper.findComponent(GlDropdown);
const findCommentForm = () => wrapper.findComponent(WorkItemCommentForm);
const findEditedAt = () => wrapper.findComponent(EditedAt);
-
- 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,
+ workItemId = mockWorkItemId,
+ updateWorkItemMutationHandler = updateWorkItemMutationSuccessHandler,
+ assignees = mockAssignees,
+ queryVariables = { id: mockWorkItemId },
+ fetchByIid = false,
} = {}) => {
wrapper = shallowMount(WorkItemNote, {
propsData: {
+ workItemId,
note,
isFirstNote,
workItemType: 'Task',
+ markdownPreviewPath: '/group/project/preview_markdown?target_type=WorkItem',
+ autocompleteDataSources: {},
+ assignees,
+ queryVariables,
+ fetchByIid,
+ fullPath: 'test-project-path',
},
- apolloProvider: mockApollo([[updateWorkItemNoteMutation, updateNoteMutationHandler]]),
+ apolloProvider: mockApollo([
+ [workItemQuery, workItemResponseHandler],
+ [updateWorkItemNoteMutation, updateNoteMutationHandler],
+ [updateWorkItemMutation, updateWorkItemMutationHandler],
+ ]),
});
};
@@ -124,6 +152,7 @@ describe('Work Item Note', () => {
await waitForPromises();
expect(findCommentForm().exists()).toBe(false);
+ expect(clearDraft).toHaveBeenCalledWith(`${mockWorkItemCommentNote.id}-comment`);
});
describe('when mutation fails', () => {
@@ -178,8 +207,7 @@ describe('Work Item Note', () => {
},
});
- expect(findEditedAt().exists()).toBe(true);
- expect(findEditedAt().props()).toEqual({
+ expect(findEditedAt().props()).toMatchObject({
updatedAt: '2023-02-12T07:47:40Z',
updatedByName: 'Administrator',
updatedByPath: 'test-path',
@@ -215,45 +243,62 @@ describe('Work Item Note', () => {
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 the `Delete comment` dropdown item if user has a permission to delete a note', () => {
- createComponent({
- note: {
- ...mockWorkItemCommentNote,
- userPermissions: { ...mockWorkItemCommentNote.userPermissions, adminNote: true },
- },
+ describe('assign/unassign to commenting user', () => {
+ it('calls a mutation with correct variables', async () => {
+ createComponent({ assignees: mockAssignees });
+ await waitForPromises();
+ findNoteActions().vm.$emit('assignUser');
+
+ await waitForPromises();
+
+ expect(updateWorkItemMutationSuccessHandler).toHaveBeenCalledWith({
+ input: {
+ id: mockWorkItemId,
+ assigneesWidget: {
+ assigneeIds: [mockAssignees[1].id],
+ },
+ },
+ });
});
- expect(findDropdown().exists()).toBe(true);
- expect(findDeleteNoteButton().exists()).toBe(true);
- });
+ it('emits an error and resets assignees if mutation was rejected', async () => {
+ createComponent({
+ updateWorkItemMutationHandler: errorHandler,
+ assignees: [mockAssignees[0]],
+ });
- it('should not display the `Delete comment` dropdown item if user has no permission to delete a note', () => {
- createComponent();
+ await waitForPromises();
- expect(findDropdown().exists()).toBe(true);
- expect(findDeleteNoteButton().exists()).toBe(false);
- });
+ expect(findNoteActions().props('isAuthorAnAssignee')).toEqual(true);
- it('should emit `deleteNote` event when delete note action is clicked', () => {
- createComponent({
- note: {
- ...mockWorkItemCommentNote,
- userPermissions: { ...mockWorkItemCommentNote.userPermissions, adminNote: true },
- },
+ findNoteActions().vm.$emit('assignUser');
+
+ await waitForPromises();
+
+ expect(wrapper.emitted('error')).toEqual([[i18n.updateError]]);
+ expect(findNoteActions().props('isAuthorAnAssignee')).toEqual(true);
});
- findDeleteNoteButton().vm.$emit('click');
+ it('tracks the event', async () => {
+ createComponent();
+ await waitForPromises();
+ const trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
+
+ findNoteActions().vm.$emit('assignUser');
- expect(wrapper.emitted('deleteNote')).toEqual([[]]);
+ await waitForPromises();
+
+ expect(trackingSpy).toHaveBeenCalledWith(TRACKING_CATEGORY_SHOW, 'unassigned_user', {
+ category: TRACKING_CATEGORY_SHOW,
+ label: 'work_item_note_actions',
+ property: 'type_Task',
+ });
+ });
});
});
});
diff --git a/spec/frontend/work_items/components/widget_wrapper_spec.js b/spec/frontend/work_items/components/widget_wrapper_spec.js
index a87233300fc..87fbd1b3830 100644
--- a/spec/frontend/work_items/components/widget_wrapper_spec.js
+++ b/spec/frontend/work_items/components/widget_wrapper_spec.js
@@ -30,7 +30,7 @@ describe('WidgetWrapper component', () => {
expect(findWidgetBody().exists()).toBe(false);
});
- it('shows alert when list loading fails', () => {
+ it('shows an alert when list loading fails', () => {
const error = 'Some error';
createComponent({ error });
diff --git a/spec/frontend/work_items/components/work_item_actions_spec.js b/spec/frontend/work_items/components/work_item_actions_spec.js
index a0db8172bf6..a5006b46063 100644
--- a/spec/frontend/work_items/components/work_item_actions_spec.js
+++ b/spec/frontend/work_items/components/work_item_actions_spec.js
@@ -1,17 +1,35 @@
-import { GlDropdownDivider, GlModal } from '@gitlab/ui';
+import { GlDropdownDivider, GlModal, GlToggle } from '@gitlab/ui';
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { isLoggedIn } from '~/lib/utils/common_utils';
+import toast from '~/vue_shared/plugins/global_toast';
import WorkItemActions from '~/work_items/components/work_item_actions.vue';
+import {
+ TEST_ID_CONFIDENTIALITY_TOGGLE_ACTION,
+ TEST_ID_NOTIFICATIONS_TOGGLE_ACTION,
+ TEST_ID_NOTIFICATIONS_TOGGLE_FORM,
+ TEST_ID_DELETE_ACTION,
+} from '~/work_items/constants';
+import updateWorkItemNotificationsMutation from '~/work_items/graphql/update_work_item_notifications.mutation.graphql';
+import { workItemResponseFactory } from '../mock_data';
-const TEST_ID_CONFIDENTIALITY_TOGGLE_ACTION = 'confidentiality-toggle-action';
-const TEST_ID_DELETE_ACTION = 'delete-action';
+jest.mock('~/lib/utils/common_utils');
+jest.mock('~/vue_shared/plugins/global_toast');
describe('WorkItemActions component', () => {
+ Vue.use(VueApollo);
+
let wrapper;
let glModalDirective;
const findModal = () => wrapper.findComponent(GlModal);
const findConfidentialityToggleButton = () =>
wrapper.findByTestId(TEST_ID_CONFIDENTIALITY_TOGGLE_ACTION);
+ const findNotificationsToggleButton = () =>
+ wrapper.findByTestId(TEST_ID_NOTIFICATIONS_TOGGLE_ACTION);
const findDeleteButton = () => wrapper.findByTestId(TEST_ID_DELETE_ACTION);
const findDropdownItems = () => wrapper.findAll('[data-testid="work-item-actions-dropdown"] > *');
const findDropdownItemsActual = () =>
@@ -25,20 +43,27 @@ describe('WorkItemActions component', () => {
text: x.text(),
};
});
+ const findNotificationsToggle = () => wrapper.findComponent(GlToggle);
const createComponent = ({
canUpdate = true,
canDelete = true,
isConfidential = false,
+ subscribed = false,
isParentConfidential = false,
+ notificationsMock = [updateWorkItemNotificationsMutation, jest.fn()],
} = {}) => {
+ const handlers = [notificationsMock];
glModalDirective = jest.fn();
wrapper = shallowMountExtended(WorkItemActions, {
+ apolloProvider: createMockApollo(handlers),
+ isLoggedIn: isLoggedIn(),
propsData: {
- workItemId: '123',
+ workItemId: 'gid://gitlab/WorkItem/1',
canUpdate,
canDelete,
isConfidential,
+ subscribed,
isParentConfidential,
workItemType: 'Task',
},
@@ -52,6 +77,10 @@ describe('WorkItemActions component', () => {
});
};
+ beforeEach(() => {
+ isLoggedIn.mockReturnValue(true);
+ });
+
it('renders modal', () => {
createComponent();
@@ -64,6 +93,13 @@ describe('WorkItemActions component', () => {
expect(findDropdownItemsActual()).toEqual([
{
+ testId: TEST_ID_NOTIFICATIONS_TOGGLE_FORM,
+ text: '',
+ },
+ {
+ divider: true,
+ },
+ {
testId: TEST_ID_CONFIDENTIALITY_TOGGLE_ACTION,
text: 'Turn on confidentiality',
},
@@ -133,7 +169,110 @@ describe('WorkItemActions component', () => {
});
expect(findDeleteButton().exists()).toBe(false);
- expect(wrapper.findComponent(GlDropdownDivider).exists()).toBe(false);
+ });
+ });
+
+ describe('notifications action', () => {
+ const errorMessage = 'Failed to subscribe';
+ const notificationToggledOffMessage = 'Notifications turned off.';
+ const notificationToggledOnMessage = 'Notifications turned on.';
+
+ const workItemQueryResponse = workItemResponseFactory({ canUpdate: true, canDelete: true });
+ const inputVariablesOff = {
+ id: workItemQueryResponse.data.workItem.id,
+ notificationsWidget: {
+ subscribed: false,
+ },
+ };
+
+ const inputVariablesOn = {
+ id: workItemQueryResponse.data.workItem.id,
+ notificationsWidget: {
+ subscribed: true,
+ },
+ };
+
+ const notificationsOffExpectedResponse = workItemResponseFactory({
+ subscribed: false,
+ });
+
+ const toggleNotificationsOffHandler = jest.fn().mockResolvedValue({
+ data: {
+ workItemUpdate: {
+ workItem: notificationsOffExpectedResponse.data.workItem,
+ errors: [],
+ },
+ },
+ });
+
+ const notificationsOnExpectedResponse = workItemResponseFactory({
+ subscribed: true,
+ });
+
+ const toggleNotificationsOnHandler = jest.fn().mockResolvedValue({
+ data: {
+ workItemUpdate: {
+ workItem: notificationsOnExpectedResponse.data.workItem,
+ errors: [],
+ },
+ },
+ });
+
+ const toggleNotificationsFailureHandler = jest.fn().mockRejectedValue(new Error(errorMessage));
+
+ const notificationsOffMock = [
+ updateWorkItemNotificationsMutation,
+ toggleNotificationsOffHandler,
+ ];
+
+ const notificationsOnMock = [updateWorkItemNotificationsMutation, toggleNotificationsOnHandler];
+
+ const notificationsFailureMock = [
+ updateWorkItemNotificationsMutation,
+ toggleNotificationsFailureHandler,
+ ];
+
+ beforeEach(() => {
+ createComponent();
+ isLoggedIn.mockReturnValue(true);
+ });
+
+ it('renders toggle button', () => {
+ expect(findNotificationsToggleButton().exists()).toBe(true);
+ });
+
+ it.each`
+ scenario | subscribedToNotifications | notificationsMock | inputVariables | toastMessage
+ ${'turned off'} | ${false} | ${notificationsOffMock} | ${inputVariablesOff} | ${notificationToggledOffMessage}
+ ${'turned on'} | ${true} | ${notificationsOnMock} | ${inputVariablesOn} | ${notificationToggledOnMessage}
+ `(
+ 'calls mutation and displays toast when notification toggle is $scenario',
+ async ({ subscribedToNotifications, notificationsMock, inputVariables, toastMessage }) => {
+ createComponent({ notificationsMock });
+
+ await waitForPromises();
+
+ findNotificationsToggle().vm.$emit('change', subscribedToNotifications);
+
+ await waitForPromises();
+
+ expect(notificationsMock[1]).toHaveBeenCalledWith({
+ input: inputVariables,
+ });
+ expect(toast).toHaveBeenCalledWith(toastMessage);
+ },
+ );
+
+ it('emits error when the update notification mutation fails', async () => {
+ createComponent({ notificationsMock: notificationsFailureMock });
+
+ await waitForPromises();
+
+ findNotificationsToggle().vm.$emit('change', false);
+
+ await waitForPromises();
+
+ expect(wrapper.emitted('error')).toEqual([[errorMessage]]);
});
});
});
diff --git a/spec/frontend/work_items/components/work_item_assignees_spec.js b/spec/frontend/work_items/components/work_item_assignees_spec.js
index 2a8159f7294..af97b3680f9 100644
--- a/spec/frontend/work_items/components/work_item_assignees_spec.js
+++ b/spec/frontend/work_items/components/work_item_assignees_spec.js
@@ -318,7 +318,7 @@ describe('WorkItemAssignees component', () => {
return waitForPromises();
});
- it('renders `Assign myself` button', async () => {
+ it('renders `Assign myself` button', () => {
findTokenSelector().trigger('mouseover');
expect(findAssignSelfButton().exists()).toBe(true);
});
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 b4b7b8989ea..099c45ac683 100644
--- a/spec/frontend/work_items/components/work_item_description_spec.js
+++ b/spec/frontend/work_items/components/work_item_description_spec.js
@@ -116,10 +116,7 @@ describe('WorkItemDescription', () => {
supportsQuickActions: true,
renderMarkdownPath: markdownPreviewPath(fullPath, iid),
quickActionsDocsPath: wrapper.vm.$options.quickActionsDocsPath,
- });
-
- expect(findMarkdownEditor().vm.$attrs).toMatchObject({
- 'autocomplete-data-sources': autocompleteDataSources(fullPath, iid),
+ autocompleteDataSources: autocompleteDataSources(fullPath, iid),
});
});
});
@@ -179,7 +176,7 @@ describe('WorkItemDescription', () => {
}),
});
- expect(findEditedAt().props()).toEqual({
+ expect(findEditedAt().props()).toMatchObject({
updatedAt: lastEditedAt,
updatedByName: lastEditedBy.name,
updatedByPath: lastEditedBy.webPath,
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 fe7556f8ec6..8e5b607cee7 100644
--- a/spec/frontend/work_items/components/work_item_detail_spec.js
+++ b/spec/frontend/work_items/components/work_item_detail_spec.js
@@ -29,12 +29,13 @@ import WorkItemDetailModal from '~/work_items/components/work_item_detail_modal.
import { i18n } from '~/work_items/constants';
import workItemQuery from '~/work_items/graphql/work_item.query.graphql';
import workItemByIidQuery from '~/work_items/graphql/work_item_by_iid.query.graphql';
-import workItemDatesSubscription from '~/work_items/graphql/work_item_dates.subscription.graphql';
+import workItemDatesSubscription from '~/graphql_shared/subscriptions/work_item_dates.subscription.graphql';
import workItemTitleSubscription from '~/work_items/graphql/work_item_title.subscription.graphql';
import workItemAssigneesSubscription from '~/work_items/graphql/work_item_assignees.subscription.graphql';
import workItemMilestoneSubscription from '~/work_items/graphql/work_item_milestone.subscription.graphql';
import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
import updateWorkItemTaskMutation from '~/work_items/graphql/update_work_item_task.mutation.graphql';
+
import {
mockParent,
workItemDatesSubscriptionResponse,
@@ -337,7 +338,7 @@ describe('WorkItemDetail component', () => {
expect(findLoadingIcon().exists()).toBe(false);
});
- it('shows alert message when mutation fails', async () => {
+ it('shows an alert when mutation fails', async () => {
createComponent({
handler: handlerMock,
confidentialityMock: confidentialityFailureMock,
@@ -388,11 +389,12 @@ describe('WorkItemDetail component', () => {
expect(findParent().exists()).toBe(false);
});
- it('shows work item type if there is not a parent', async () => {
+ it('shows work item type with reference when there is no a parent', async () => {
createComponent({ handler: jest.fn().mockResolvedValue(workItemQueryResponseWithoutParent) });
await waitForPromises();
expect(findWorkItemType().exists()).toBe(true);
+ expect(findWorkItemType().text()).toBe('Task #1');
});
describe('with parent', () => {
@@ -407,7 +409,7 @@ describe('WorkItemDetail component', () => {
expect(findParent().exists()).toBe(true);
});
- it('does not show work item type', async () => {
+ it('does not show work item type', () => {
expect(findWorkItemType().exists()).toBe(false);
});
diff --git a/spec/frontend/work_items/components/work_item_links/work_item_link_child_metadata_spec.js b/spec/frontend/work_items/components/work_item_links/work_item_link_child_metadata_spec.js
index e693ccfb156..07efb1c5ac8 100644
--- a/spec/frontend/work_items/components/work_item_links/work_item_link_child_metadata_spec.js
+++ b/spec/frontend/work_items/components/work_item_links/work_item_link_child_metadata_spec.js
@@ -1,4 +1,4 @@
-import { GlLabel, GlAvatarsInline } from '@gitlab/ui';
+import { GlAvatarsInline } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
@@ -8,10 +8,9 @@ import WorkItemLinkChildMetadata from '~/work_items/components/work_item_links/w
import { workItemObjectiveMetadataWidgets } from '../../mock_data';
describe('WorkItemLinkChildMetadata', () => {
- const { MILESTONE, ASSIGNEES, LABELS } = workItemObjectiveMetadataWidgets;
+ const { MILESTONE, ASSIGNEES } = workItemObjectiveMetadataWidgets;
const mockMilestone = MILESTONE.milestone;
const mockAssignees = ASSIGNEES.assignees.nodes;
- const mockLabels = LABELS.labels.nodes;
let wrapper;
const createComponent = ({ metadataWidgets = workItemObjectiveMetadataWidgets } = {}) => {
@@ -53,18 +52,4 @@ describe('WorkItemLinkChildMetadata', () => {
badgeSrOnlyText: '',
});
});
-
- it('renders labels', () => {
- const labels = wrapper.findAllComponents(GlLabel);
- const mockLabel = mockLabels[0];
-
- expect(labels).toHaveLength(mockLabels.length);
- expect(labels.at(0).props()).toMatchObject({
- title: mockLabel.title,
- backgroundColor: mockLabel.color,
- description: mockLabel.description,
- scoped: false,
- });
- expect(labels.at(1).props('scoped')).toBe(true); // Second label is scoped
- });
});
diff --git a/spec/frontend/work_items/components/work_item_links/work_item_link_child_spec.js b/spec/frontend/work_items/components/work_item_links/work_item_link_child_spec.js
index 721436e217e..106f9d46513 100644
--- a/spec/frontend/work_items/components/work_item_links/work_item_link_child_spec.js
+++ b/spec/frontend/work_items/components/work_item_links/work_item_link_child_spec.js
@@ -1,4 +1,4 @@
-import { GlIcon } from '@gitlab/ui';
+import { GlLabel, GlIcon } from '@gitlab/ui';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
@@ -11,6 +11,7 @@ import { createAlert } from '~/alert';
import RichTimestampTooltip from '~/vue_shared/components/rich_timestamp_tooltip.vue';
import getWorkItemTreeQuery from '~/work_items/graphql/work_item_tree.query.graphql';
+import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
import WorkItemLinkChild from '~/work_items/components/work_item_links/work_item_link_child.vue';
import WorkItemLinksMenu from '~/work_items/components/work_item_links/work_item_links_menu.vue';
import WorkItemTreeChildren from '~/work_items/components/work_item_links/work_item_tree_children.vue';
@@ -29,6 +30,8 @@ import {
workItemHierarchyTreeResponse,
workItemHierarchyTreeFailureResponse,
workItemObjectiveMetadataWidgets,
+ changeIndirectWorkItemParentMutationResponse,
+ workItemUpdateFailureResponse,
} from '../../mock_data';
jest.mock('~/alert');
@@ -37,6 +40,14 @@ describe('WorkItemLinkChild', () => {
const WORK_ITEM_ID = 'gid://gitlab/WorkItem/2';
let wrapper;
let getWorkItemTreeQueryHandler;
+ let mutationChangeParentHandler;
+ const { LABELS } = workItemObjectiveMetadataWidgets;
+ const mockLabels = LABELS.labels.nodes;
+
+ const $toast = {
+ show: jest.fn(),
+ hide: jest.fn(),
+ };
Vue.use(VueApollo);
@@ -49,10 +60,17 @@ describe('WorkItemLinkChild', () => {
apolloProvider = null,
} = {}) => {
getWorkItemTreeQueryHandler = jest.fn().mockResolvedValue(workItemHierarchyTreeResponse);
+ mutationChangeParentHandler = jest
+ .fn()
+ .mockResolvedValue(changeIndirectWorkItemParentMutationResponse);
wrapper = shallowMountExtended(WorkItemLinkChild, {
apolloProvider:
- apolloProvider || createMockApollo([[getWorkItemTreeQuery, getWorkItemTreeQueryHandler]]),
+ apolloProvider ||
+ createMockApollo([
+ [getWorkItemTreeQuery, getWorkItemTreeQueryHandler],
+ [updateWorkItemMutation, mutationChangeParentHandler],
+ ]),
propsData: {
projectPath,
canUpdate,
@@ -60,6 +78,9 @@ describe('WorkItemLinkChild', () => {
childItem,
workItemType,
},
+ mocks: {
+ $toast,
+ },
});
};
@@ -165,8 +186,6 @@ describe('WorkItemLinkChild', () => {
expect(metadataEl.props()).toMatchObject({
metadataWidgets: workItemObjectiveMetadataWidgets,
});
-
- expect(wrapper.find('[data-testid="links-child"]').classes()).toContain('gl-py-3');
});
it('does not render item metadata component when item has no metadata present', () => {
@@ -176,8 +195,20 @@ describe('WorkItemLinkChild', () => {
});
expect(findMetadataComponent().exists()).toBe(false);
+ });
+
+ it('renders labels', () => {
+ const labels = wrapper.findAllComponents(GlLabel);
+ const mockLabel = mockLabels[0];
- expect(wrapper.find('[data-testid="links-child"]').classes()).toContain('gl-py-0');
+ expect(labels).toHaveLength(mockLabels.length);
+ expect(labels.at(0).props()).toMatchObject({
+ title: mockLabel.title,
+ backgroundColor: mockLabel.color,
+ description: mockLabel.description,
+ scoped: false,
+ });
+ expect(labels.at(1).props('scoped')).toBe(true); // Second label is scoped
});
});
@@ -216,6 +247,13 @@ describe('WorkItemLinkChild', () => {
const findExpandButton = () => wrapper.findByTestId('expand-child');
const findTreeChildren = () => wrapper.findComponent(WorkItemTreeChildren);
+ const getWidgetHierarchy = () =>
+ workItemHierarchyTreeResponse.data.workItem.widgets.find(
+ (widget) => widget.type === WIDGET_TYPE_HIERARCHY,
+ );
+ const getChildrenNodes = () => getWidgetHierarchy().children.nodes;
+ const findFirstItemId = () => getChildrenNodes()[0].id;
+
beforeEach(() => {
getWorkItemTreeQueryHandler.mockClear();
createComponent({
@@ -238,10 +276,8 @@ describe('WorkItemLinkChild', () => {
expect(getWorkItemTreeQueryHandler).toHaveBeenCalled();
expect(findTreeChildren().exists()).toBe(true);
- const widgetHierarchy = workItemHierarchyTreeResponse.data.workItem.widgets.find(
- (widget) => widget.type === WIDGET_TYPE_HIERARCHY,
- );
- expect(findTreeChildren().props('children')).toEqual(widgetHierarchy.children.nodes);
+ const childrenNodes = getChildrenNodes();
+ expect(findTreeChildren().props('children')).toEqual(childrenNodes);
});
it('does not fetch children if already fetched once while clicking expand button', async () => {
@@ -290,5 +326,74 @@ describe('WorkItemLinkChild', () => {
expect(wrapper.emitted('click')).toEqual([['event']]);
});
+
+ it('shows toast on removing child item', async () => {
+ findExpandButton().vm.$emit('click');
+ await waitForPromises();
+
+ findTreeChildren().vm.$emit('removeChild', findFirstItemId());
+ await waitForPromises();
+
+ expect($toast.show).toHaveBeenCalledWith('Child removed', {
+ action: { onClick: expect.any(Function), text: 'Undo' },
+ });
+ });
+
+ it('renders correct number of children after the removal', async () => {
+ findExpandButton().vm.$emit('click');
+ await waitForPromises();
+
+ const childrenNodes = getChildrenNodes();
+ expect(findTreeChildren().props('children')).toEqual(childrenNodes);
+
+ findTreeChildren().vm.$emit('removeChild', findFirstItemId());
+ await waitForPromises();
+
+ expect(findTreeChildren().props('children')).toEqual([]);
+ });
+
+ it('calls correct mutation with correct variables', async () => {
+ const firstItemId = findFirstItemId();
+
+ findExpandButton().vm.$emit('click');
+ await waitForPromises();
+
+ findTreeChildren().vm.$emit('removeChild', firstItemId);
+
+ expect(mutationChangeParentHandler).toHaveBeenCalledWith({
+ input: {
+ id: firstItemId,
+ hierarchyWidget: {
+ parentId: null,
+ },
+ },
+ });
+ });
+
+ it('shows the alert when workItem update fails', async () => {
+ mutationChangeParentHandler = jest.fn().mockRejectedValue(workItemUpdateFailureResponse);
+ const apolloProvider = createMockApollo([
+ [getWorkItemTreeQuery, getWorkItemTreeQueryHandler],
+ [updateWorkItemMutation, mutationChangeParentHandler],
+ ]);
+
+ createComponent({
+ childItem: workItemObjectiveWithChild,
+ workItemType: WORK_ITEM_TYPE_VALUE_OBJECTIVE,
+ apolloProvider,
+ });
+
+ findExpandButton().vm.$emit('click');
+ await waitForPromises();
+
+ findTreeChildren().vm.$emit('removeChild', findFirstItemId());
+ await waitForPromises();
+
+ expect(createAlert).toHaveBeenCalledWith({
+ captureError: true,
+ error: expect.any(Object),
+ message: 'Something went wrong while removing child.',
+ });
+ });
});
});
diff --git a/spec/frontend/work_items/components/work_item_links/work_item_links_menu_spec.js b/spec/frontend/work_items/components/work_item_links/work_item_links_menu_spec.js
index 4e53fc2987b..f02a9fbd021 100644
--- a/spec/frontend/work_items/components/work_item_links/work_item_links_menu_spec.js
+++ b/spec/frontend/work_items/components/work_item_links/work_item_links_menu_spec.js
@@ -13,7 +13,7 @@ describe('WorkItemLinksMenu', () => {
const findDropdown = () => wrapper.findComponent(GlDropdown);
const findRemoveDropdownItem = () => wrapper.findComponent(GlDropdownItem);
- beforeEach(async () => {
+ beforeEach(() => {
createComponent();
});
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 99e44b4d89c..e97c2328b83 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
@@ -179,7 +179,7 @@ describe('WorkItemLinks', () => {
expect(findWorkItemLinkChildItems()).toHaveLength(4);
});
- it('shows alert when list loading fails', async () => {
+ it('shows an alert when list loading fails', async () => {
const errorMessage = 'Some error';
await createComponent({
fetchHandler: jest.fn().mockRejectedValue(new Error(errorMessage)),
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 a067923b9fc..3cc6a9813fc 100644
--- a/spec/frontend/work_items/components/work_item_notes_spec.js
+++ b/spec/frontend/work_items/components/work_item_notes_spec.js
@@ -18,6 +18,7 @@ import workItemNoteUpdatedSubscription from '~/work_items/graphql/notes/work_ite
import workItemNoteDeletedSubscription from '~/work_items/graphql/notes/work_item_note_deleted.subscription.graphql';
import { DEFAULT_PAGE_SIZE_NOTES, WIDGET_TYPE_NOTES } from '~/work_items/constants';
import { ASC, DESC } from '~/notes/constants';
+import { autocompleteDataSources, markdownPreviewPath } from '~/work_items/utils';
import {
mockWorkItemNotesResponse,
workItemQueryResponse,
@@ -30,6 +31,7 @@ import {
} from '../mock_data';
const mockWorkItemId = workItemQueryResponse.data.workItem.id;
+const mockWorkItemIid = workItemQueryResponse.data.workItem.iid;
const mockNotesWidgetResponse = mockWorkItemNotesResponse.data.workItem.widgets.find(
(widget) => widget.type === WIDGET_TYPE_NOTES,
);
@@ -92,6 +94,7 @@ describe('WorkItemNotes component', () => {
const createComponent = ({
workItemId = mockWorkItemId,
fetchByIid = false,
+ workItemIid = mockWorkItemIid,
defaultWorkItemNotesQueryHandler = workItemNotesQueryHandler,
deleteWINoteMutationHandler = deleteWorkItemNoteMutationSuccessHandler,
} = {}) => {
@@ -106,6 +109,7 @@ describe('WorkItemNotes component', () => {
]),
propsData: {
workItemId,
+ workItemIid,
queryVariables: {
id: workItemId,
},
@@ -119,7 +123,7 @@ describe('WorkItemNotes component', () => {
});
};
- beforeEach(async () => {
+ beforeEach(() => {
createComponent();
});
@@ -258,9 +262,11 @@ describe('WorkItemNotes component', () => {
const commentIndex = 0;
const firstCommentNote = findWorkItemCommentNoteAtIndex(commentIndex);
- expect(firstCommentNote.props('discussion')).toEqual(
- mockDiscussions[commentIndex].notes.nodes,
- );
+ expect(firstCommentNote.props()).toMatchObject({
+ discussion: mockDiscussions[commentIndex].notes.nodes,
+ autocompleteDataSources: autocompleteDataSources('test-path', mockWorkItemIid),
+ markdownPreviewPath: markdownPreviewPath('test-path', mockWorkItemIid),
+ });
});
});
diff --git a/spec/frontend/work_items/mock_data.js b/spec/frontend/work_items/mock_data.js
index fecf98b2651..c3376556d6e 100644
--- a/spec/frontend/work_items/mock_data.js
+++ b/spec/frontend/work_items/mock_data.js
@@ -82,6 +82,7 @@ export const workItemQueryResponse = {
userPermissions: {
deleteWorkItem: false,
updateWorkItem: false,
+ setWorkItemMetadata: false,
__typename: 'WorkItemPermissions',
},
widgets: [
@@ -183,6 +184,7 @@ export const updateWorkItemMutationResponse = {
userPermissions: {
deleteWorkItem: false,
updateWorkItem: false,
+ setWorkItemMetadata: false,
__typename: 'WorkItemPermissions',
},
widgets: [
@@ -286,6 +288,8 @@ export const objectiveType = {
export const workItemResponseFactory = ({
canUpdate = false,
canDelete = false,
+ notificationsWidgetPresent = true,
+ subscribed = true,
allowsMultipleAssignees = true,
assigneesWidgetPresent = true,
datesWidgetPresent = true,
@@ -313,7 +317,7 @@ export const workItemResponseFactory = ({
workItem: {
__typename: 'WorkItem',
id: 'gid://gitlab/WorkItem/1',
- iid: 1,
+ iid: '1',
title: 'Updated title',
state: 'OPEN',
description: 'description',
@@ -332,6 +336,7 @@ export const workItemResponseFactory = ({
userPermissions: {
deleteWorkItem: canDelete,
updateWorkItem: canUpdate,
+ setWorkItemMetadata: canUpdate,
__typename: 'WorkItemPermissions',
},
widgets: [
@@ -469,6 +474,13 @@ export const workItemResponseFactory = ({
type: 'NOTES',
}
: { type: 'MOCK TYPE' },
+ notificationsWidgetPresent
+ ? {
+ __typename: 'WorkItemWidgetNotifications',
+ type: 'NOTIFICATIONS',
+ subscribed,
+ }
+ : { type: 'MOCK TYPE' },
],
},
},
@@ -542,6 +554,7 @@ export const createWorkItemMutationResponse = {
userPermissions: {
deleteWorkItem: false,
updateWorkItem: false,
+ setWorkItemMetadata: false,
__typename: 'WorkItemPermissions',
},
widgets: [],
@@ -591,6 +604,7 @@ export const createWorkItemFromTaskMutationResponse = {
userPermissions: {
deleteWorkItem: false,
updateWorkItem: false,
+ setWorkItemMetadata: false,
__typename: 'WorkItemPermissions',
},
widgets: [
@@ -632,6 +646,7 @@ export const createWorkItemFromTaskMutationResponse = {
userPermissions: {
deleteWorkItem: false,
updateWorkItem: false,
+ setWorkItemMetadata: false,
__typename: 'WorkItemPermissions',
},
widgets: [],
@@ -834,7 +849,7 @@ export const workItemHierarchyEmptyResponse = {
data: {
workItem: {
id: 'gid://gitlab/WorkItem/1',
- iid: 1,
+ iid: '1',
state: 'OPEN',
workItemType: {
id: 'gid://gitlab/WorkItems::Type/1',
@@ -857,6 +872,7 @@ export const workItemHierarchyEmptyResponse = {
userPermissions: {
deleteWorkItem: false,
updateWorkItem: false,
+ setWorkItemMetadata: false,
__typename: 'WorkItemPermissions',
},
confidential: false,
@@ -881,7 +897,7 @@ export const workItemHierarchyNoUpdatePermissionResponse = {
data: {
workItem: {
id: 'gid://gitlab/WorkItem/1',
- iid: 1,
+ iid: '1',
state: 'OPEN',
workItemType: {
id: 'gid://gitlab/WorkItems::Type/6',
@@ -898,6 +914,7 @@ export const workItemHierarchyNoUpdatePermissionResponse = {
userPermissions: {
deleteWorkItem: false,
updateWorkItem: false,
+ setWorkItemMetadata: false,
__typename: 'WorkItemPermissions',
},
project: {
@@ -1039,6 +1056,7 @@ export const workItemHierarchyResponse = {
userPermissions: {
deleteWorkItem: true,
updateWorkItem: true,
+ setWorkItemMetadata: true,
__typename: 'WorkItemPermissions',
},
author: {
@@ -1128,6 +1146,7 @@ export const workItemObjectiveWithChild = {
userPermissions: {
deleteWorkItem: true,
updateWorkItem: true,
+ setWorkItemMetadata: true,
__typename: 'WorkItemPermissions',
},
author: {
@@ -1195,6 +1214,7 @@ export const workItemHierarchyTreeResponse = {
userPermissions: {
deleteWorkItem: true,
updateWorkItem: true,
+ setWorkItemMetadata: true,
__typename: 'WorkItemPermissions',
},
confidential: false,
@@ -1258,6 +1278,68 @@ export const workItemHierarchyTreeFailureResponse = {
],
};
+export const changeIndirectWorkItemParentMutationResponse = {
+ data: {
+ workItemUpdate: {
+ workItem: {
+ __typename: 'WorkItem',
+ workItemType: {
+ id: 'gid://gitlab/WorkItems::Type/2411',
+ name: 'Objective',
+ iconName: 'issue-type-objective',
+ __typename: 'WorkItemType',
+ },
+ userPermissions: {
+ deleteWorkItem: true,
+ updateWorkItem: true,
+ setWorkItemMetadata: true,
+ __typename: 'WorkItemPermissions',
+ },
+ description: null,
+ id: 'gid://gitlab/WorkItem/13',
+ iid: '13',
+ state: 'OPEN',
+ title: 'Objective 2',
+ confidential: false,
+ createdAt: '2022-08-03T12:41:54Z',
+ updatedAt: null,
+ closedAt: null,
+ author: {
+ ...mockAssignees[0],
+ },
+ project: {
+ __typename: 'Project',
+ id: '1',
+ fullPath: 'test-project-path',
+ archived: false,
+ },
+ widgets: [
+ {
+ __typename: 'WorkItemWidgetHierarchy',
+ type: 'HIERARCHY',
+ parent: null,
+ hasChildren: false,
+ children: {
+ nodes: [],
+ },
+ },
+ ],
+ },
+ errors: [],
+ __typename: 'WorkItemUpdatePayload',
+ },
+ },
+};
+
+export const workItemUpdateFailureResponse = {
+ data: {},
+ errors: [
+ {
+ message: 'Something went wrong',
+ },
+ ],
+};
+
export const changeWorkItemParentMutationResponse = {
data: {
workItemUpdate: {
@@ -1272,6 +1354,7 @@ export const changeWorkItemParentMutationResponse = {
userPermissions: {
deleteWorkItem: true,
updateWorkItem: true,
+ setWorkItemMetadata: true,
__typename: 'WorkItemPermissions',
},
description: null,
diff --git a/spec/frontend/work_items/pages/work_item_root_spec.js b/spec/frontend/work_items/pages/work_item_root_spec.js
index 37326910e13..c480affe484 100644
--- a/spec/frontend/work_items/pages/work_item_root_spec.js
+++ b/spec/frontend/work_items/pages/work_item_root_spec.js
@@ -75,7 +75,7 @@ describe('Work items root component', () => {
expect(visitUrl).toHaveBeenCalledWith(issuesListPath);
});
- it('shows alert if delete fails', async () => {
+ it('shows an alert if delete fails', async () => {
const deleteWorkItemHandler = jest.fn().mockRejectedValue(deleteWorkItemFailureResponse);
createComponent({
diff --git a/spec/frontend/work_items/router_spec.js b/spec/frontend/work_items/router_spec.js
index 5dad7f7c43f..bd75c5be6f1 100644
--- a/spec/frontend/work_items/router_spec.js
+++ b/spec/frontend/work_items/router_spec.js
@@ -13,7 +13,7 @@ import {
} from 'jest/work_items/mock_data';
import App from '~/work_items/components/app.vue';
import workItemQuery from '~/work_items/graphql/work_item.query.graphql';
-import workItemDatesSubscription from '~/work_items/graphql/work_item_dates.subscription.graphql';
+import workItemDatesSubscription from '~/graphql_shared/subscriptions/work_item_dates.subscription.graphql';
import workItemTitleSubscription from '~/work_items/graphql/work_item_title.subscription.graphql';
import workItemAssigneesSubscription from '~/work_items/graphql/work_item_assignees.subscription.graphql';
import workItemLabelsSubscription from 'ee_else_ce/work_items/graphql/work_item_labels.subscription.graphql';