summaryrefslogtreecommitdiff
path: root/spec/frontend
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2021-07-20 09:55:51 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2021-07-20 09:55:51 +0000
commite8d2c2579383897a1dd7f9debd359abe8ae8373d (patch)
treec42be41678c2586d49a75cabce89322082698334 /spec/frontend
parentfc845b37ec3a90aaa719975f607740c22ba6a113 (diff)
downloadgitlab-ce-e8d2c2579383897a1dd7f9debd359abe8ae8373d.tar.gz
Add latest changes from gitlab-org/gitlab@14-1-stable-eev14.1.0-rc42
Diffstat (limited to 'spec/frontend')
-rw-r--r--spec/frontend/__helpers__/dom_shims/inner_text.js2
-rw-r--r--spec/frontend/add_context_commits_modal/components/__snapshots__/add_context_commits_modal_spec.js.snap1
-rw-r--r--spec/frontend/admin/users/components/actions/actions_spec.js29
-rw-r--r--spec/frontend/admin/users/components/modals/__snapshots__/delete_user_modal_spec.js.snap (renamed from spec/frontend/pages/admin/users/components/__snapshots__/delete_user_modal_spec.js.snap)1
-rw-r--r--spec/frontend/admin/users/components/modals/delete_user_modal_spec.js (renamed from spec/frontend/pages/admin/users/components/delete_user_modal_spec.js)2
-rw-r--r--spec/frontend/admin/users/components/modals/stubs/modal_stub.js (renamed from spec/frontend/pages/admin/users/components/stubs/modal_stub.js)0
-rw-r--r--spec/frontend/admin/users/components/modals/user_modal_manager_spec.js (renamed from spec/frontend/pages/admin/users/components/user_modal_manager_spec.js)2
-rw-r--r--spec/frontend/admin/users/components/user_actions_spec.js62
-rw-r--r--spec/frontend/admin/users/constants.js16
-rw-r--r--spec/frontend/admin/users/index_spec.js36
-rw-r--r--spec/frontend/admin/users/mock_data.js4
-rw-r--r--spec/frontend/analytics/devops_report/components/service_ping_disabled_spec.js61
-rw-r--r--spec/frontend/analytics/shared/components/daterange_spec.js120
-rw-r--r--spec/frontend/analytics/shared/components/metric_card_spec.js129
-rw-r--r--spec/frontend/analytics/shared/components/projects_dropdown_filter_spec.js264
-rw-r--r--spec/frontend/analytics/shared/utils_spec.js24
-rw-r--r--spec/frontend/analytics/usage_trends/components/usage_counts_spec.js (renamed from spec/frontend/analytics/usage_trends/components/instance_counts_spec.js)22
-rw-r--r--spec/frontend/api_spec.js8
-rw-r--r--spec/frontend/batch_comments/components/draft_note_spec.js12
-rw-r--r--spec/frontend/blob/components/__snapshots__/blob_header_filepath_spec.js.snap2
-rw-r--r--spec/frontend/blob/components/blob_edit_content_spec.js8
-rw-r--r--spec/frontend/blob/csv/csv_viewer_spec.js75
-rw-r--r--spec/frontend/blob/utils_spec.js12
-rw-r--r--spec/frontend/blob/viewer/index_spec.js5
-rw-r--r--spec/frontend/blob_edit/blob_bundle_spec.js10
-rw-r--r--spec/frontend/blob_edit/edit_blob_spec.js14
-rw-r--r--spec/frontend/boards/board_card_inner_spec.js126
-rw-r--r--spec/frontend/boards/board_list_helper.js95
-rw-r--r--spec/frontend/boards/board_list_spec.js96
-rw-r--r--spec/frontend/boards/boards_util_spec.js33
-rw-r--r--spec/frontend/boards/components/board_column_spec.js37
-rw-r--r--spec/frontend/boards/components/board_content_sidebar_spec.js20
-rw-r--r--spec/frontend/boards/components/board_content_spec.js7
-rw-r--r--spec/frontend/boards/components/board_form_spec.js2
-rw-r--r--spec/frontend/boards/components/board_settings_sidebar_spec.js11
-rw-r--r--spec/frontend/boards/components/issue_board_filtered_search_spec.js44
-rw-r--r--spec/frontend/boards/mock_data.js47
-rw-r--r--spec/frontend/boards/stores/actions_spec.js88
-rw-r--r--spec/frontend/boards/stores/getters_spec.js2
-rw-r--r--spec/frontend/boards/stores/mutations_spec.js3
-rw-r--r--spec/frontend/branches/components/delete_branch_button_spec.js6
-rw-r--r--spec/frontend/captcha/captcha_modal_axios_interceptor_spec.js55
-rw-r--r--spec/frontend/ci_lint/components/ci_lint_spec.js4
-rw-r--r--spec/frontend/clusters/clusters_bundle_spec.js166
-rw-r--r--spec/frontend/clusters/components/__snapshots__/applications_spec.js.snap105
-rw-r--r--spec/frontend/clusters/components/__snapshots__/remove_cluster_confirmation_spec.js.snap1
-rw-r--r--spec/frontend/clusters/components/application_row_spec.js505
-rw-r--r--spec/frontend/clusters/components/applications_spec.js510
-rw-r--r--spec/frontend/clusters/components/knative_domain_editor_spec.js179
-rw-r--r--spec/frontend/clusters/components/uninstall_application_button_spec.js39
-rw-r--r--spec/frontend/clusters/components/uninstall_application_confirmation_modal_spec.js57
-rw-r--r--spec/frontend/clusters/components/update_application_confirmation_modal_spec.js52
-rw-r--r--spec/frontend/clusters/services/application_state_machine_spec.js206
-rw-r--r--spec/frontend/clusters/services/crossplane_provider_stack_spec.js85
-rw-r--r--spec/frontend/clusters/services/mock_data.js155
-rw-r--r--spec/frontend/clusters/stores/clusters_store_spec.js192
-rw-r--r--spec/frontend/clusters_list/store/actions_spec.js6
-rw-r--r--spec/frontend/code_navigation/components/__snapshots__/popover_spec.js.snap2
-rw-r--r--spec/frontend/code_quality_walkthrough/components/step_spec.js6
-rw-r--r--spec/frontend/collapsed_sidebar_todo_spec.js171
-rw-r--r--spec/frontend/commit/commit_pipeline_status_component_spec.js4
-rw-r--r--spec/frontend/commit/pipelines/pipelines_table_spec.js4
-rw-r--r--spec/frontend/content_editor/components/content_editor_spec.js37
-rw-r--r--spec/frontend/content_editor/components/toolbar_button_spec.js34
-rw-r--r--spec/frontend/content_editor/components/toolbar_image_button_spec.js78
-rw-r--r--spec/frontend/content_editor/components/toolbar_link_button_spec.js47
-rw-r--r--spec/frontend/content_editor/components/toolbar_table_button_spec.js109
-rw-r--r--spec/frontend/content_editor/components/toolbar_text_style_dropdown_spec.js16
-rw-r--r--spec/frontend/content_editor/components/top_toolbar_spec.js24
-rw-r--r--spec/frontend/content_editor/components/wrappers/image_spec.js66
-rw-r--r--spec/frontend/content_editor/extensions/hard_break_spec.js46
-rw-r--r--spec/frontend/content_editor/extensions/horizontal_rule_spec.js20
-rw-r--r--spec/frontend/content_editor/extensions/image_spec.js193
-rw-r--r--spec/frontend/content_editor/markdown_processing_examples.js3
-rw-r--r--spec/frontend/content_editor/markdown_processing_spec.js16
-rw-r--r--spec/frontend/content_editor/services/create_content_editor_spec.js12
-rw-r--r--spec/frontend/content_editor/services/track_input_rules_and_shortcuts_spec.js17
-rw-r--r--spec/frontend/content_editor/services/upload_file_spec.js46
-rw-r--r--spec/frontend/content_editor/test_utils.js12
-rw-r--r--spec/frontend/contributors/store/actions_spec.js6
-rw-r--r--spec/frontend/cycle_analytics/filter_bar_spec.js224
-rw-r--r--spec/frontend/cycle_analytics/formatted_stage_count_spec.js34
-rw-r--r--spec/frontend/cycle_analytics/mock_data.js41
-rw-r--r--spec/frontend/cycle_analytics/store/actions_spec.js88
-rw-r--r--spec/frontend/cycle_analytics/store/mutations_spec.js69
-rw-r--r--spec/frontend/cycle_analytics/utils_spec.js10
-rw-r--r--spec/frontend/cycle_analytics/value_stream_filters_spec.js91
-rw-r--r--spec/frontend/design_management/components/design_notes/__snapshots__/design_note_spec.js.snap7
-rw-r--r--spec/frontend/design_management/components/design_notes/design_note_spec.js3
-rw-r--r--spec/frontend/design_management/components/design_todo_button_spec.js2
-rw-r--r--spec/frontend/design_management/pages/design/index_spec.js8
-rw-r--r--spec/frontend/diffs/components/app_spec.js46
-rw-r--r--spec/frontend/diffs/components/collapsed_files_warning_spec.js93
-rw-r--r--spec/frontend/diffs/components/diff_content_spec.js17
-rw-r--r--spec/frontend/diffs/components/diff_file_spec.js5
-rw-r--r--spec/frontend/diffs/components/diff_row_spec.js127
-rw-r--r--spec/frontend/diffs/components/diff_row_utils_spec.js43
-rw-r--r--spec/frontend/diffs/components/diff_view_spec.js18
-rw-r--r--spec/frontend/diffs/components/inline_diff_table_row_spec.js325
-rw-r--r--spec/frontend/diffs/components/inline_diff_view_spec.js57
-rw-r--r--spec/frontend/diffs/components/parallel_diff_table_row_spec.js445
-rw-r--r--spec/frontend/diffs/components/parallel_diff_view_spec.js37
-rw-r--r--spec/frontend/diffs/store/actions_spec.js31
-rw-r--r--spec/frontend/diffs/store/getters_versions_dropdowns_spec.js2
-rw-r--r--spec/frontend/editor/source_editor_ci_schema_ext_spec.js (renamed from spec/frontend/editor/editor_ci_schema_ext_spec.js)6
-rw-r--r--spec/frontend/editor/source_editor_extension_base_spec.js (renamed from spec/frontend/editor/editor_lite_extension_base_spec.js)62
-rw-r--r--spec/frontend/editor/source_editor_markdown_ext_spec.js (renamed from spec/frontend/editor/editor_markdown_ext_spec.js)8
-rw-r--r--spec/frontend/editor/source_editor_spec.js (renamed from spec/frontend/editor/editor_lite_spec.js)36
-rw-r--r--spec/frontend/emoji/awards_app/store/actions_spec.js1
-rw-r--r--spec/frontend/environment.js21
-rw-r--r--spec/frontend/environments/environment_item_spec.js11
-rw-r--r--spec/frontend/environments/environments_app_spec.js16
-rw-r--r--spec/frontend/feature_flags/components/edit_feature_flag_spec.js73
-rw-r--r--spec/frontend/feature_flags/components/feature_flags_table_spec.js184
-rw-r--r--spec/frontend/feature_flags/components/form_spec.js338
-rw-r--r--spec/frontend/feature_flags/components/new_feature_flag_spec.js15
-rw-r--r--spec/frontend/feature_flags/mock_data.js88
-rw-r--r--spec/frontend/feature_flags/store/edit/actions_spec.js45
-rw-r--r--spec/frontend/feature_flags/store/helpers_spec.js360
-rw-r--r--spec/frontend/feature_flags/store/index/actions_spec.js5
-rw-r--r--spec/frontend/feature_flags/store/index/mutations_spec.js17
-rw-r--r--spec/frontend/feature_flags/store/new/actions_spec.js81
-rw-r--r--spec/frontend/feature_highlight/feature_highlight_helper_spec.js9
-rw-r--r--spec/frontend/filtered_search/filtered_search_manager_spec.js12
-rw-r--r--spec/frontend/filtered_search/visual_token_value_spec.js8
-rw-r--r--spec/frontend/fixtures/api_markdown.rb34
-rw-r--r--spec/frontend/fixtures/api_markdown.yml33
-rw-r--r--spec/frontend/fixtures/application_settings.rb8
-rw-r--r--spec/frontend/fixtures/pipelines.rb1
-rw-r--r--spec/frontend/fixtures/projects.rb3
-rw-r--r--spec/frontend/fixtures/prometheus_service.rb4
-rw-r--r--spec/frontend/fixtures/releases.rb8
-rw-r--r--spec/frontend/fixtures/runner.rb9
-rw-r--r--spec/frontend/flash_spec.js115
-rw-r--r--spec/frontend/frequent_items/components/frequent_items_list_item_spec.js35
-rw-r--r--spec/frontend/gpg_badges_spec.js50
-rw-r--r--spec/frontend/grafana_integration/components/grafana_integration_spec.js1
-rw-r--r--spec/frontend/groups/components/app_spec.js17
-rw-r--r--spec/frontend/groups/components/group_item_spec.js18
-rw-r--r--spec/frontend/ide/components/ide_project_header_spec.js44
-rw-r--r--spec/frontend/ide/components/new_dropdown/modal_spec.js3
-rw-r--r--spec/frontend/ide/components/repo_editor_spec.js15
-rw-r--r--spec/frontend/ide/services/index_spec.js2
-rw-r--r--spec/frontend/ide/stores/modules/clientside/actions_spec.js2
-rw-r--r--spec/frontend/ide/stores/utils_spec.js6
-rw-r--r--spec/frontend/import_entities/components/group_dropdown_spec.js44
-rw-r--r--spec/frontend/import_entities/import_groups/components/import_table_row_spec.js6
-rw-r--r--spec/frontend/import_entities/import_projects/components/import_projects_table_spec.js4
-rw-r--r--spec/frontend/import_entities/import_projects/components/provider_repo_table_row_spec.js19
-rw-r--r--spec/frontend/incidents_settings/components/__snapshots__/incidents_settings_tabs_spec.js.snap2
-rw-r--r--spec/frontend/incidents_settings/components/incidents_settings_service_spec.js1
-rw-r--r--spec/frontend/integrations/edit/components/jira_issues_fields_spec.js109
-rw-r--r--spec/frontend/invite_members/components/invite_members_modal_spec.js295
-rw-r--r--spec/frontend/invite_members/components/members_token_select_spec.js15
-rw-r--r--spec/frontend/invite_members/mock_data/api_responses.js74
-rw-r--r--spec/frontend/invite_members/utils/response_message_parser_spec.js36
-rw-r--r--spec/frontend/issuable/components/issuable_by_email_spec.js5
-rw-r--r--spec/frontend/issuable_bulk_update_sidebar/components/status_select_spec.js77
-rw-r--r--spec/frontend/issuable_create/components/issuable_form_spec.js3
-rw-r--r--spec/frontend/issuable_show/components/issuable_show_root_spec.js8
-rw-r--r--spec/frontend/issuable_sidebar/components/issuable_sidebar_root_spec.js214
-rw-r--r--spec/frontend/issuable_spec.js2
-rw-r--r--spec/frontend/issues_list/components/issuables_list_app_spec.js4
-rw-r--r--spec/frontend/issues_list/components/issues_list_app_spec.js74
-rw-r--r--spec/frontend/issues_list/mock_data.js77
-rw-r--r--spec/frontend/issues_list/utils_spec.js29
-rw-r--r--spec/frontend/jira_connect/branches/components/project_dropdown_spec.js180
-rw-r--r--spec/frontend/jira_connect/branches/components/source_branch_dropdown_spec.js192
-rw-r--r--spec/frontend/jira_connect/components/groups_list_spec.js6
-rw-r--r--spec/frontend/jira_import/components/__snapshots__/jira_import_form_spec.js.snap2
-rw-r--r--spec/frontend/jobs/components/empty_state_spec.js1
-rw-r--r--spec/frontend/jobs/components/job_app_spec.js8
-rw-r--r--spec/frontend/jobs/components/log/collapsible_section_spec.js9
-rw-r--r--spec/frontend/jobs/components/log/line_spec.js30
-rw-r--r--spec/frontend/jobs/components/log/log_spec.js91
-rw-r--r--spec/frontend/jobs/components/log/mock_data.js65
-rw-r--r--spec/frontend/jobs/components/manual_variables_form_spec.js15
-rw-r--r--spec/frontend/jobs/components/sidebar_detail_row_spec.js2
-rw-r--r--spec/frontend/jobs/store/mutations_spec.js94
-rw-r--r--spec/frontend/jobs/store/utils_spec.js95
-rw-r--r--spec/frontend/lib/dompurify_spec.js16
-rw-r--r--spec/frontend/lib/graphql_spec.js54
-rw-r--r--spec/frontend/lib/utils/common_utils_spec.js146
-rw-r--r--spec/frontend/lib/utils/datetime/timeago_utility_spec.js103
-rw-r--r--spec/frontend/lib/utils/datetime_utility_spec.js40
-rw-r--r--spec/frontend/lib/utils/finite_state_machine_spec.js293
-rw-r--r--spec/frontend/lib/utils/text_markdown_spec.js2
-rw-r--r--spec/frontend/lib/utils/url_utility_spec.js115
-rw-r--r--spec/frontend/line_highlighter_spec.js9
-rw-r--r--spec/frontend/locale/index_spec.js86
-rw-r--r--spec/frontend/logs/stores/actions_spec.js6
-rw-r--r--spec/frontend/members/components/app_spec.js21
-rw-r--r--spec/frontend/members/components/filter_sort/members_filtered_search_bar_spec.js12
-rw-r--r--spec/frontend/members/components/members_tabs_spec.js75
-rw-r--r--spec/frontend/members/components/table/members_table_spec.js19
-rw-r--r--spec/frontend/milestones/milestone_utils_spec.js47
-rw-r--r--spec/frontend/monitoring/components/charts/time_series_spec.js4
-rw-r--r--spec/frontend/monitoring/components/dashboard_actions_menu_spec.js7
-rw-r--r--spec/frontend/nav/components/top_nav_menu_item_spec.js2
-rw-r--r--spec/frontend/notes/components/comment_form_spec.js14
-rw-r--r--spec/frontend/notes/components/discussion_notes_spec.js14
-rw-r--r--spec/frontend/notes/components/noteable_note_spec.js7
-rw-r--r--spec/frontend/notes/stores/actions_spec.js75
-rw-r--r--spec/frontend/notifications/components/custom_notifications_modal_spec.js10
-rw-r--r--spec/frontend/notifications/components/notifications_dropdown_spec.js5
-rw-r--r--spec/frontend/operation_settings/components/metrics_settings_spec.js1
-rw-r--r--spec/frontend/packages/list/components/__snapshots__/packages_list_app_spec.js.snap1
-rw-r--r--spec/frontend/packages/shared/utils_spec.js2
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/app_spec.js35
-rw-r--r--spec/frontend/packages_and_registries/settings/group/components/group_settings_app_spec.js6
-rw-r--r--spec/frontend/packages_and_registries/settings/project/settings/__snapshots__/utils_spec.js.snap8
-rw-r--r--spec/frontend/packages_and_registries/settings/project/settings/components/__snapshots__/settings_form_spec.js.snap4
-rw-r--r--spec/frontend/packages_and_registries/settings/project/settings/components/settings_form_spec.js20
-rw-r--r--spec/frontend/packages_and_registries/settings/project/settings/utils_spec.js1
-rw-r--r--spec/frontend/pager_spec.js1
-rw-r--r--spec/frontend/pages/admin/application_settings/metrics_and_profiling/usage_statistics_spec.js57
-rw-r--r--spec/frontend/pages/projects/forks/new/components/fork_form_spec.js23
-rw-r--r--spec/frontend/pages/projects/forks/new/components/fork_groups_list_item_spec.js4
-rw-r--r--spec/frontend/pages/projects/new/components/app_spec.js33
-rw-r--r--spec/frontend/pages/projects/shared/permissions/components/settings_panel_spec.js25
-rw-r--r--spec/frontend/persistent_user_callout_spec.js15
-rw-r--r--spec/frontend/pipeline_editor/components/editor/ci_config_merged_preview_spec.js6
-rw-r--r--spec/frontend/pipeline_editor/components/editor/ci_editor_header_spec.js53
-rw-r--r--spec/frontend/pipeline_editor/components/editor/text_editor_spec.js10
-rw-r--r--spec/frontend/pipeline_editor/components/file-nav/branch_switcher_spec.js21
-rw-r--r--spec/frontend/pipeline_editor/components/ui/editor_tab_spec.js6
-rw-r--r--spec/frontend/pipeline_editor/graphql/resolvers_spec.js39
-rw-r--r--spec/frontend/pipeline_editor/mock_data.js46
-rw-r--r--spec/frontend/pipeline_editor/pipeline_editor_app_spec.js176
-rw-r--r--spec/frontend/pipelines/empty_state_spec.js30
-rw-r--r--spec/frontend/pipelines/graph/mock_data.js8
-rw-r--r--spec/frontend/pipelines/graph/stage_column_component_spec.js64
-rw-r--r--spec/frontend/pipelines/graph_shared/__snapshots__/links_inner_spec.js.snap18
-rw-r--r--spec/frontend/pipelines/pipeline_graph/pipeline_graph_spec.js57
-rw-r--r--spec/frontend/pipelines/pipelines_ci_templates_spec.js34
-rw-r--r--spec/frontend/pipelines/pipelines_spec.js89
-rw-r--r--spec/frontend/profile/preferences/components/profile_preferences_spec.js18
-rw-r--r--spec/frontend/projects/commit/components/branches_dropdown_spec.js3
-rw-r--r--spec/frontend/projects/components/__snapshots__/project_delete_button_spec.js.snap4
-rw-r--r--spec/frontend/projects/pipelines/charts/components/__snapshots__/ci_cd_analytics_area_chart_spec.js.snap6
-rw-r--r--spec/frontend/projects/settings/components/shared_runners_toggle_spec.js1
-rw-r--r--spec/frontend/projects/terraform_notification/terraform_notification_spec.js62
-rw-r--r--spec/frontend/registry/explorer/components/list_page/cleanup_status_spec.js87
-rw-r--r--spec/frontend/registry/explorer/components/list_page/image_list_row_spec.js30
-rw-r--r--spec/frontend/registry/explorer/mock_data.js2
-rw-r--r--spec/frontend/releases/__snapshots__/util_spec.js.snap37
-rw-r--r--spec/frontend/releases/components/app_index_apollo_client_spec.js8
-rw-r--r--spec/frontend/releases/components/app_index_spec.js6
-rw-r--r--spec/frontend/reports/components/__snapshots__/grouped_issues_list_spec.js.snap1
-rw-r--r--spec/frontend/reports/grouped_test_report/grouped_test_reports_app_spec.js4
-rw-r--r--spec/frontend/repository/components/blob_button_group_spec.js117
-rw-r--r--spec/frontend/repository/components/blob_content_viewer_spec.js171
-rw-r--r--spec/frontend/repository/components/blob_edit_spec.js (renamed from spec/frontend/repository/components/blob_header_edit_spec.js)6
-rw-r--r--spec/frontend/repository/components/blob_replace_spec.js67
-rw-r--r--spec/frontend/repository/components/blob_viewers/__snapshots__/empty_viewer_spec.js.snap9
-rw-r--r--spec/frontend/repository/components/blob_viewers/download_viewer_spec.js70
-rw-r--r--spec/frontend/repository/components/blob_viewers/empty_viewer_spec.js14
-rw-r--r--spec/frontend/repository/components/blob_viewers/text_viewer_spec.js30
-rw-r--r--spec/frontend/repository/components/delete_blob_modal_spec.js130
-rw-r--r--spec/frontend/repository/components/table/__snapshots__/row_spec.js.snap3
-rw-r--r--spec/frontend/repository/components/tree_content_spec.js63
-rw-r--r--spec/frontend/repository/components/upload_blob_modal_spec.js4
-rw-r--r--spec/frontend/repository/log_tree_spec.js51
-rw-r--r--spec/frontend/right_sidebar_spec.js16
-rw-r--r--spec/frontend/runner/components/cells/runner_actions_cell_spec.js234
-rw-r--r--spec/frontend/runner/components/helpers/masked_value_spec.js51
-rw-r--r--spec/frontend/runner/components/runner_filtered_search_bar_spec.js27
-rw-r--r--spec/frontend/runner/components/runner_list_spec.js69
-rw-r--r--spec/frontend/runner/components/runner_manual_setup_help_spec.js9
-rw-r--r--spec/frontend/runner/components/runner_registration_token_reset_spec.js25
-rw-r--r--spec/frontend/runner/components/runner_tag_spec.js45
-rw-r--r--spec/frontend/runner/components/runner_tags_spec.js12
-rw-r--r--spec/frontend/runner/components/runner_update_form_spec.js33
-rw-r--r--spec/frontend/runner/components/search_tokens/tag_token_spec.js188
-rw-r--r--spec/frontend/runner/runner_detail/runner_details_app_spec.js27
-rw-r--r--spec/frontend/runner/runner_detail/runner_update_form_utils_spec.js96
-rw-r--r--spec/frontend/runner/runner_list/runner_list_app_spec.js31
-rw-r--r--spec/frontend/runner/runner_list/runner_search_utils_spec.js39
-rw-r--r--spec/frontend/runner/sentry_utils_spec.js39
-rw-r--r--spec/frontend/search/mock_data.js53
-rw-r--r--spec/frontend/search/store/actions_spec.js106
-rw-r--r--spec/frontend/search/store/getters_spec.js32
-rw-r--r--spec/frontend/search/store/mutations_spec.js9
-rw-r--r--spec/frontend/search/store/utils_spec.js197
-rw-r--r--spec/frontend/search/topbar/components/group_filter_spec.js56
-rw-r--r--spec/frontend/search/topbar/components/project_filter_spec.js38
-rw-r--r--spec/frontend/search/topbar/components/searchable_dropdown_spec.js87
-rw-r--r--spec/frontend/search_autocomplete_spec.js50
-rw-r--r--spec/frontend/search_autocomplete_utils_spec.js114
-rw-r--r--spec/frontend/security_configuration/components/auto_dev_ops_alert_spec.js55
-rw-r--r--spec/frontend/security_configuration/components/feature_card_spec.js25
-rw-r--r--spec/frontend/security_configuration/components/redesigned_app_spec.js82
-rw-r--r--spec/frontend/security_configuration/utils_spec.js22
-rw-r--r--spec/frontend/sentry/index_spec.js4
-rw-r--r--spec/frontend/sentry/sentry_config_spec.js8
-rw-r--r--spec/frontend/serverless/components/__snapshots__/empty_state_spec.js.snap2
-rw-r--r--spec/frontend/sidebar/components/assignees/collapsed_assignee_spec.js8
-rw-r--r--spec/frontend/sidebar/components/assignees/sidebar_assignees_widget_spec.js44
-rw-r--r--spec/frontend/sidebar/components/assignees/user_name_with_status_spec.js44
-rw-r--r--spec/frontend/sidebar/components/sidebar_dropdown_widget_spec.js6
-rw-r--r--spec/frontend/sidebar/components/todo_toggle/sidebar_todo_widget_spec.js126
-rw-r--r--spec/frontend/sidebar/lock/edit_form_buttons_spec.js10
-rw-r--r--spec/frontend/sidebar/mock_data.js37
-rw-r--r--spec/frontend/sidebar/sidebar_move_issue_spec.js6
-rw-r--r--spec/frontend/snippets/components/__snapshots__/snippet_blob_edit_spec.js.snap2
-rw-r--r--spec/frontend/snippets/components/edit_spec.js20
-rw-r--r--spec/frontend/snippets/components/snippet_blob_edit_spec.js4
-rw-r--r--spec/frontend/static_site_editor/services/submit_content_changes_spec.js10
-rw-r--r--spec/frontend/terraform/components/terraform_list_spec.js3
-rw-r--r--spec/frontend/token_access/mock_data.js84
-rw-r--r--spec/frontend/token_access/token_access_spec.js218
-rw-r--r--spec/frontend/token_access/token_projects_table_spec.js51
-rw-r--r--spec/frontend/tracking_spec.js46
-rw-r--r--spec/frontend/vue_alerts_spec.js4
-rw-r--r--spec/frontend/vue_mr_widget/components/mr_widget_header_spec.js70
-rw-r--r--spec/frontend/vue_mr_widget/components/mr_widget_rebase_spec.js14
-rw-r--r--spec/frontend/vue_mr_widget/components/states/commit_edit_spec.js2
-rw-r--r--spec/frontend/vue_mr_widget/components/states/mr_widget_conflicts_spec.js2
-rw-r--r--spec/frontend/vue_mr_widget/components/states/mr_widget_merged_spec.js2
-rw-r--r--spec/frontend/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js3
-rw-r--r--spec/frontend/vue_mr_widget/mock_data.js4
-rw-r--r--spec/frontend/vue_shared/components/__snapshots__/awards_list_spec.js.snap14
-rw-r--r--spec/frontend/vue_shared/components/__snapshots__/source_editor_spec.js.snap (renamed from spec/frontend/vue_shared/components/__snapshots__/editor_lite_spec.js.snap)4
-rw-r--r--spec/frontend/vue_shared/components/awards_list_spec.js18
-rw-r--r--spec/frontend/vue_shared/components/blob_viewers/rich_viewer_spec.js9
-rw-r--r--spec/frontend/vue_shared/components/blob_viewers/simple_viewer_spec.js16
-rw-r--r--spec/frontend/vue_shared/components/diff_viewer/viewers/image_diff_viewer_spec.js12
-rw-r--r--spec/frontend/vue_shared/components/dismissible_alert_spec.js41
-rw-r--r--spec/frontend/vue_shared/components/file_finder/index_spec.js12
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_utils_spec.js16
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/author_token_spec.js20
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/base_token_spec.js34
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/iteration_token_spec.js10
-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.js3
-rw-r--r--spec/frontend/vue_shared/components/notes/__snapshots__/placeholder_note_spec.js.snap2
-rw-r--r--spec/frontend/vue_shared/components/paginated_list_spec.js8
-rw-r--r--spec/frontend/vue_shared/components/project_avatar/default_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/project_avatar_spec.js67
-rw-r--r--spec/frontend/vue_shared/components/project_selector/project_list_item_spec.js3
-rw-r--r--spec/frontend/vue_shared/components/resizable_chart/__snapshots__/resizable_chart_container_spec.js.snap30
-rw-r--r--spec/frontend/vue_shared/components/resizable_chart/resizable_chart_container_spec.js10
-rw-r--r--spec/frontend/vue_shared/components/security_reports/artifact_downloads/merge_request_artifact_download_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/sidebar/copyable_field_spec.js5
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view_spec.js10
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/actions_spec.js19
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/mutations_spec.js29
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_value_spec.js113
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_widget/labels_select_root_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_widget/store/actions_spec.js11
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_widget/store/mutations_spec.js29
-rw-r--r--spec/frontend/vue_shared/components/sidebar/todo_button_spec.js (renamed from spec/frontend/vue_shared/components/todo_button_spec.js)22
-rw-r--r--spec/frontend/vue_shared/components/source_editor_spec.js (renamed from spec/frontend/vue_shared/components/editor_lite_spec.js)16
-rw-r--r--spec/frontend/vue_shared/components/user_popover/user_popover_spec.js20
-rw-r--r--spec/frontend/vue_shared/components/user_select_spec.js44
-rw-r--r--spec/frontend/vue_shared/components/web_ide_link_spec.js8
-rw-r--r--spec/frontend/vue_shared/new_namespace/components/welcome_spec.js26
-rw-r--r--spec/frontend/vue_shared/new_namespace/new_namespace_page_spec.js7
-rw-r--r--spec/frontend/vue_shared/oncall_schedules_list_spec.js2
-rw-r--r--spec/frontend/vue_shared/plugins/global_toast_spec.js9
-rw-r--r--spec/frontend/vue_shared/security_reports/components/manage_via_mr_spec.js24
-rw-r--r--spec/frontend/vue_shared/security_reports/components/security_report_download_dropdown_spec.js37
-rw-r--r--spec/frontend/vue_shared/security_reports/mock_data.js11
-rw-r--r--spec/frontend/vue_shared/security_reports/security_reports_app_spec.js10
-rw-r--r--spec/frontend/vuex_shared/bindings_spec.js10
365 files changed, 9804 insertions, 7118 deletions
diff --git a/spec/frontend/__helpers__/dom_shims/inner_text.js b/spec/frontend/__helpers__/dom_shims/inner_text.js
index 2b8201eed31..a48f0fee689 100644
--- a/spec/frontend/__helpers__/dom_shims/inner_text.js
+++ b/spec/frontend/__helpers__/dom_shims/inner_text.js
@@ -5,7 +5,7 @@ Object.defineProperty(global.Element.prototype, 'innerText', {
return this.textContent;
},
set(value) {
- this.textContext = value;
+ this.textContent = value;
},
configurable: true, // make it so that it doesn't blow chunks on re-running tests with things like --watch
});
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 1eb9ccc9c6c..10437c48f88 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
@@ -16,6 +16,7 @@ exports[`AddContextCommitsModal renders modal with 2 tabs 1`] = `
>
<gl-tabs-stub
contentclass="pt-0"
+ queryparamname="tab"
theme="indigo"
value="0"
>
diff --git a/spec/frontend/admin/users/components/actions/actions_spec.js b/spec/frontend/admin/users/components/actions/actions_spec.js
index 5db5b8a90a9..67d9bac8580 100644
--- a/spec/frontend/admin/users/components/actions/actions_spec.js
+++ b/spec/frontend/admin/users/components/actions/actions_spec.js
@@ -39,37 +39,12 @@ describe('Action components', () => {
await nextTick();
- const div = wrapper.find('div');
- expect(div.attributes('data-path')).toBe('/test');
- expect(div.attributes('data-modal-attributes')).toContain('John Doe');
+ expect(wrapper.attributes('data-path')).toBe('/test');
+ expect(wrapper.attributes('data-modal-attributes')).toContain('John Doe');
expect(findDropdownItem().exists()).toBe(true);
});
});
- describe('LINK_ACTIONS', () => {
- it.each`
- action | method
- ${'Approve'} | ${'put'}
- ${'Reject'} | ${'delete'}
- `(
- 'renders a dropdown item link with method "$method" for "$action"',
- async ({ action, method }) => {
- initComponent({
- component: Actions[action],
- props: {
- path: '/test',
- },
- });
-
- await nextTick();
-
- const item = wrapper.find(GlDropdownItem);
- expect(item.attributes('href')).toBe('/test');
- expect(item.attributes('data-method')).toContain(method);
- },
- );
- });
-
describe('DELETE_ACTION_COMPONENTS', () => {
const oncallSchedules = [{ name: 'schedule1' }, { name: 'schedule2' }];
it.each(DELETE_ACTIONS)('renders a dropdown item for "%s"', async (action) => {
diff --git a/spec/frontend/pages/admin/users/components/__snapshots__/delete_user_modal_spec.js.snap b/spec/frontend/admin/users/components/modals/__snapshots__/delete_user_modal_spec.js.snap
index 4c644a0d05f..5e367891337 100644
--- a/spec/frontend/pages/admin/users/components/__snapshots__/delete_user_modal_spec.js.snap
+++ b/spec/frontend/admin/users/components/modals/__snapshots__/delete_user_modal_spec.js.snap
@@ -10,6 +10,7 @@ exports[`User Operation confirmation modal renders modal with form included 1`]
<oncall-schedules-list-stub
schedules="schedule1,schedule2"
+ username="username"
/>
<p>
diff --git a/spec/frontend/pages/admin/users/components/delete_user_modal_spec.js b/spec/frontend/admin/users/components/modals/delete_user_modal_spec.js
index 93d9ee43179..fee74764645 100644
--- a/spec/frontend/pages/admin/users/components/delete_user_modal_spec.js
+++ b/spec/frontend/admin/users/components/modals/delete_user_modal_spec.js
@@ -1,6 +1,6 @@
import { GlButton, GlFormInput } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import DeleteUserModal from '~/pages/admin/users/components/delete_user_modal.vue';
+import DeleteUserModal from '~/admin/users/components/modals/delete_user_modal.vue';
import OncallSchedulesList from '~/vue_shared/components/oncall_schedules_list.vue';
import ModalStub from './stubs/modal_stub';
diff --git a/spec/frontend/pages/admin/users/components/stubs/modal_stub.js b/spec/frontend/admin/users/components/modals/stubs/modal_stub.js
index 4dc55e909a0..4dc55e909a0 100644
--- a/spec/frontend/pages/admin/users/components/stubs/modal_stub.js
+++ b/spec/frontend/admin/users/components/modals/stubs/modal_stub.js
diff --git a/spec/frontend/pages/admin/users/components/user_modal_manager_spec.js b/spec/frontend/admin/users/components/modals/user_modal_manager_spec.js
index 3669bc40d7e..65ce242662b 100644
--- a/spec/frontend/pages/admin/users/components/user_modal_manager_spec.js
+++ b/spec/frontend/admin/users/components/modals/user_modal_manager_spec.js
@@ -1,5 +1,5 @@
import { mount } from '@vue/test-utils';
-import UserModalManager from '~/pages/admin/users/components/user_modal_manager.vue';
+import UserModalManager from '~/admin/users/components/modals/user_modal_manager.vue';
import ModalStub from './stubs/modal_stub';
describe('Users admin page Modal Manager', () => {
diff --git a/spec/frontend/admin/users/components/user_actions_spec.js b/spec/frontend/admin/users/components/user_actions_spec.js
index debe964e7aa..43313424553 100644
--- a/spec/frontend/admin/users/components/user_actions_spec.js
+++ b/spec/frontend/admin/users/components/user_actions_spec.js
@@ -1,4 +1,5 @@
import { GlDropdownDivider } 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';
import AdminUserActions from '~/admin/users/components/user_actions.vue';
@@ -6,7 +7,7 @@ import { I18N_USER_ACTIONS } from '~/admin/users/constants';
import { generateUserPaths } from '~/admin/users/utils';
import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
-import { CONFIRMATION_ACTIONS, DELETE_ACTIONS, LINK_ACTIONS, LDAP, EDIT } from '../constants';
+import { CONFIRMATION_ACTIONS, DELETE_ACTIONS, LDAP, EDIT } from '../constants';
import { users, paths } from '../mock_data';
describe('AdminUserActions component', () => {
@@ -20,7 +21,7 @@ describe('AdminUserActions component', () => {
findUserActions(id).find('[data-testid="dropdown-toggle"]');
const findDropdownDivider = () => wrapper.findComponent(GlDropdownDivider);
- const initComponent = ({ actions = [] } = {}) => {
+ const initComponent = ({ actions = [], showButtonLabels } = {}) => {
wrapper = shallowMountExtended(AdminUserActions, {
propsData: {
user: {
@@ -28,6 +29,10 @@ describe('AdminUserActions component', () => {
actions,
},
paths,
+ showButtonLabels,
+ },
+ directives: {
+ GlTooltip: createMockDirective(),
},
});
};
@@ -62,7 +67,7 @@ describe('AdminUserActions component', () => {
describe('actions dropdown', () => {
describe('when there are actions', () => {
- const actions = [EDIT, ...LINK_ACTIONS];
+ const actions = [EDIT, ...CONFIRMATION_ACTIONS];
beforeEach(() => {
initComponent({ actions });
@@ -72,19 +77,6 @@ describe('AdminUserActions component', () => {
expect(findActionsDropdown().exists()).toBe(true);
});
- describe('when there are actions that should render as links', () => {
- beforeEach(() => {
- initComponent({ actions: LINK_ACTIONS });
- });
-
- it.each(LINK_ACTIONS)('renders an action component item for "%s"', (action) => {
- const component = wrapper.find(Actions[capitalizeFirstCharacter(action)]);
-
- expect(component.props('path')).toBe(userPaths[action]);
- expect(component.text()).toBe(I18N_USER_ACTIONS[action]);
- });
- });
-
describe('when there are actions that require confirmation', () => {
beforeEach(() => {
initComponent({ actions: CONFIRMATION_ACTIONS });
@@ -157,4 +149,42 @@ describe('AdminUserActions component', () => {
});
});
});
+
+ describe('when `showButtonLabels` prop is `false`', () => {
+ beforeEach(() => {
+ initComponent({ actions: [EDIT, ...CONFIRMATION_ACTIONS] });
+ });
+
+ it('does not render "Edit" button label', () => {
+ const tooltip = getBinding(findEditButton().element, 'gl-tooltip');
+
+ expect(findEditButton().text()).toBe('');
+ expect(findEditButton().attributes('aria-label')).toBe(I18N_USER_ACTIONS.edit);
+ expect(tooltip).toBeDefined();
+ expect(tooltip.value).toBe(I18N_USER_ACTIONS.edit);
+ });
+
+ it('does not render "User administration" dropdown button label', () => {
+ expect(findActionsDropdown().props('text')).toBe(I18N_USER_ACTIONS.userAdministration);
+ expect(findActionsDropdown().props('textSrOnly')).toBe(true);
+ });
+ });
+
+ describe('when `showButtonLabels` prop is `true`', () => {
+ beforeEach(() => {
+ initComponent({ actions: [EDIT, ...CONFIRMATION_ACTIONS], showButtonLabels: true });
+ });
+
+ it('renders "Edit" button label', () => {
+ const tooltip = getBinding(findEditButton().element, 'gl-tooltip');
+
+ expect(findEditButton().text()).toBe(I18N_USER_ACTIONS.edit);
+ expect(tooltip).not.toBeDefined();
+ });
+
+ it('renders "User administration" dropdown button label', () => {
+ expect(findActionsDropdown().props('text')).toBe(I18N_USER_ACTIONS.userAdministration);
+ expect(findActionsDropdown().props('textSrOnly')).toBe(false);
+ });
+ });
});
diff --git a/spec/frontend/admin/users/constants.js b/spec/frontend/admin/users/constants.js
index 60abdc6c248..d341eb03b1b 100644
--- a/spec/frontend/admin/users/constants.js
+++ b/spec/frontend/admin/users/constants.js
@@ -7,13 +7,23 @@ const ACTIVATE = 'activate';
const DEACTIVATE = 'deactivate';
const REJECT = 'reject';
const APPROVE = 'approve';
+const BAN = 'ban';
+const UNBAN = 'unban';
export const EDIT = 'edit';
export const LDAP = 'ldapBlocked';
-export const LINK_ACTIONS = [APPROVE, REJECT];
-
-export const CONFIRMATION_ACTIONS = [ACTIVATE, BLOCK, DEACTIVATE, UNLOCK, UNBLOCK];
+export const CONFIRMATION_ACTIONS = [
+ ACTIVATE,
+ BLOCK,
+ DEACTIVATE,
+ UNLOCK,
+ UNBLOCK,
+ BAN,
+ UNBAN,
+ APPROVE,
+ REJECT,
+];
export const DELETE_ACTIONS = [DELETE, DELETE_WITH_CONTRIBUTIONS];
diff --git a/spec/frontend/admin/users/index_spec.js b/spec/frontend/admin/users/index_spec.js
index 20b60bd8640..06dbadd6d3d 100644
--- a/spec/frontend/admin/users/index_spec.js
+++ b/spec/frontend/admin/users/index_spec.js
@@ -1,7 +1,8 @@
import { createWrapper } from '@vue/test-utils';
-import { initAdminUsersApp } from '~/admin/users';
+import { initAdminUsersApp, initAdminUserActions } from '~/admin/users';
import AdminUsersApp from '~/admin/users/components/app.vue';
-import { users, paths } from './mock_data';
+import UserActions from '~/admin/users/components/user_actions.vue';
+import { users, user, paths } from './mock_data';
describe('initAdminUsersApp', () => {
let wrapper;
@@ -14,15 +15,12 @@ describe('initAdminUsersApp', () => {
el.setAttribute('data-users', JSON.stringify(users));
el.setAttribute('data-paths', JSON.stringify(paths));
- document.body.appendChild(el);
-
wrapper = createWrapper(initAdminUsersApp(el));
});
afterEach(() => {
wrapper.destroy();
wrapper = null;
- el.remove();
el = null;
});
@@ -33,3 +31,31 @@ describe('initAdminUsersApp', () => {
});
});
});
+
+describe('initAdminUserActions', () => {
+ let wrapper;
+ let el;
+
+ const findUserActions = () => wrapper.find(UserActions);
+
+ beforeEach(() => {
+ el = document.createElement('div');
+ el.setAttribute('data-user', JSON.stringify(user));
+ el.setAttribute('data-paths', JSON.stringify(paths));
+
+ wrapper = createWrapper(initAdminUserActions(el));
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ el = null;
+ });
+
+ it('parses and passes props', () => {
+ expect(findUserActions().props()).toMatchObject({
+ user,
+ paths,
+ });
+ });
+});
diff --git a/spec/frontend/admin/users/mock_data.js b/spec/frontend/admin/users/mock_data.js
index 4689ab36773..ded3e6f7edf 100644
--- a/spec/frontend/admin/users/mock_data.js
+++ b/spec/frontend/admin/users/mock_data.js
@@ -18,6 +18,8 @@ export const users = [
},
];
+export const user = users[0];
+
export const paths = {
edit: '/admin/users/id/edit',
approve: '/admin/users/id/approve',
@@ -30,6 +32,8 @@ export const paths = {
delete: '/admin/users/id',
deleteWithContributions: '/admin/users/id',
adminUser: '/admin/users/id',
+ ban: '/admin/users/id/ban',
+ unban: '/admin/users/id/unban',
};
export const createGroupCountResponse = (groupCounts) => ({
diff --git a/spec/frontend/analytics/devops_report/components/service_ping_disabled_spec.js b/spec/frontend/analytics/devops_report/components/service_ping_disabled_spec.js
new file mode 100644
index 00000000000..75ef9d9db94
--- /dev/null
+++ b/spec/frontend/analytics/devops_report/components/service_ping_disabled_spec.js
@@ -0,0 +1,61 @@
+import { GlEmptyState, GlSprintf } from '@gitlab/ui';
+import { TEST_HOST } from 'helpers/test_constants';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import ServicePingDisabled from '~/analytics/devops_report/components/service_ping_disabled.vue';
+
+describe('~/analytics/devops_report/components/service_ping_disabled.vue', () => {
+ let wrapper;
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ const createWrapper = ({ isAdmin = false } = {}) => {
+ wrapper = shallowMountExtended(ServicePingDisabled, {
+ provide: {
+ isAdmin,
+ svgPath: TEST_HOST,
+ docsLink: TEST_HOST,
+ primaryButtonPath: TEST_HOST,
+ },
+ stubs: { GlEmptyState, GlSprintf },
+ });
+ };
+
+ const findEmptyState = () => wrapper.findComponent(GlEmptyState);
+ const findMessageForRegularUsers = () => wrapper.findComponent(GlSprintf);
+ const findDocsLink = () => wrapper.findByTestId('docs-link');
+ const findPowerOnButton = () => wrapper.findByTestId('power-on-button');
+
+ it('renders empty state with provided SVG path', () => {
+ createWrapper();
+
+ expect(findEmptyState().props('svgPath')).toBe(TEST_HOST);
+ });
+
+ describe('for regular users', () => {
+ beforeEach(() => {
+ createWrapper({ isAdmin: false });
+ });
+
+ it('renders message without power-on button', () => {
+ expect(findMessageForRegularUsers().exists()).toBe(true);
+ expect(findPowerOnButton().exists()).toBe(false);
+ });
+
+ it('renders docs link', () => {
+ expect(findDocsLink().exists()).toBe(true);
+ expect(findDocsLink().attributes('href')).toBe(TEST_HOST);
+ });
+ });
+
+ describe('for admins', () => {
+ beforeEach(() => {
+ createWrapper({ isAdmin: true });
+ });
+
+ it('renders power-on button', () => {
+ expect(findPowerOnButton().exists()).toBe(true);
+ });
+ });
+});
diff --git a/spec/frontend/analytics/shared/components/daterange_spec.js b/spec/frontend/analytics/shared/components/daterange_spec.js
new file mode 100644
index 00000000000..854582abb82
--- /dev/null
+++ b/spec/frontend/analytics/shared/components/daterange_spec.js
@@ -0,0 +1,120 @@
+import { GlDaterangePicker } from '@gitlab/ui';
+import { mount } from '@vue/test-utils';
+import { useFakeDate } from 'helpers/fake_date';
+import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
+import Daterange from '~/analytics/shared/components/daterange.vue';
+
+const defaultProps = {
+ startDate: new Date(2019, 8, 1),
+ endDate: new Date(2019, 8, 11),
+};
+
+describe('Daterange component', () => {
+ useFakeDate(2019, 8, 25);
+
+ let wrapper;
+
+ const factory = (props = defaultProps) => {
+ wrapper = mount(Daterange, {
+ propsData: {
+ ...defaultProps,
+ ...props,
+ },
+ directives: { GlTooltip: createMockDirective() },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ const findDaterangePicker = () => wrapper.find(GlDaterangePicker);
+
+ const findDateRangeIndicator = () => wrapper.find('.daterange-indicator');
+
+ describe('template', () => {
+ describe('when show is false', () => {
+ it('does not render the daterange picker', () => {
+ factory({ show: false });
+ expect(findDaterangePicker().exists()).toBe(false);
+ });
+ });
+
+ describe('when show is true', () => {
+ it('renders the daterange picker', () => {
+ factory({ show: true });
+ expect(findDaterangePicker().exists()).toBe(true);
+ });
+ });
+
+ describe('with a minDate being set', () => {
+ it('emits the change event with the minDate when the user enters a start date before the minDate', () => {
+ const startDate = new Date('2019-09-01');
+ const endDate = new Date('2019-09-30');
+ const minDate = new Date('2019-06-01');
+
+ factory({ show: true, startDate, endDate, minDate });
+
+ const input = findDaterangePicker().find('input');
+
+ input.setValue('2019-01-01');
+ input.trigger('change');
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(wrapper.emitted().change).toEqual([[{ startDate: minDate, endDate }]]);
+ });
+ });
+ });
+
+ describe('with a maxDateRange being set', () => {
+ beforeEach(() => {
+ factory({ maxDateRange: 30 });
+ });
+
+ it('displays the max date range indicator', () => {
+ expect(findDateRangeIndicator().exists()).toBe(true);
+ });
+
+ it('displays the correct number of selected days in the indicator', () => {
+ expect(findDateRangeIndicator().find('span').text()).toBe('10 days selected');
+ });
+
+ it('displays a tooltip', () => {
+ const icon = wrapper.find('[data-testid="helper-icon"]');
+ const tooltip = getBinding(icon.element, 'gl-tooltip');
+
+ expect(tooltip).toBeDefined();
+ expect(icon.attributes('title')).toBe(
+ 'Showing data for workflow items created in this date range. Date range cannot exceed 30 days.',
+ );
+ });
+ });
+ });
+
+ describe('computed', () => {
+ describe('dateRange', () => {
+ beforeEach(() => {
+ factory({ show: true });
+ });
+
+ describe('set', () => {
+ it('emits the change event with an object containing startDate and endDate', () => {
+ const startDate = new Date('2019-10-01');
+ const endDate = new Date('2019-10-05');
+ wrapper.vm.dateRange = { startDate, endDate };
+
+ expect(wrapper.emitted().change).toEqual([[{ startDate, endDate }]]);
+ });
+ });
+
+ describe('get', () => {
+ it("returns value of dateRange from state's startDate and endDate", () => {
+ expect(wrapper.vm.dateRange).toEqual({
+ startDate: defaultProps.startDate,
+ endDate: defaultProps.endDate,
+ });
+ });
+ });
+ });
+ });
+});
diff --git a/spec/frontend/analytics/shared/components/metric_card_spec.js b/spec/frontend/analytics/shared/components/metric_card_spec.js
deleted file mode 100644
index 7f587d227ab..00000000000
--- a/spec/frontend/analytics/shared/components/metric_card_spec.js
+++ /dev/null
@@ -1,129 +0,0 @@
-import { GlDeprecatedSkeletonLoading as GlSkeletonLoading } from '@gitlab/ui';
-import { mount } from '@vue/test-utils';
-import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
-import MetricCard from '~/analytics/shared/components/metric_card.vue';
-
-const metrics = [
- { key: 'first_metric', value: 10, label: 'First metric', unit: 'days', link: 'some_link' },
- { key: 'second_metric', value: 20, label: 'Yet another metric' },
- { key: 'third_metric', value: null, label: 'Null metric without value', unit: 'parsecs' },
- { key: 'fourth_metric', value: '-', label: 'Metric without value', unit: 'parsecs' },
-];
-
-const defaultProps = {
- title: 'My fancy title',
- isLoading: false,
- metrics,
-};
-
-describe('MetricCard', () => {
- let wrapper;
-
- const factory = (props = defaultProps) => {
- wrapper = mount(MetricCard, {
- propsData: {
- ...defaultProps,
- ...props,
- },
- directives: {
- GlTooltip: createMockDirective(),
- },
- });
- };
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- const findTitle = () => wrapper.find({ ref: 'title' });
- const findLoadingIndicator = () => wrapper.find(GlSkeletonLoading);
- const findMetricsWrapper = () => wrapper.find({ ref: 'metricsWrapper' });
- const findMetricItem = () => wrapper.findAll({ ref: 'metricItem' });
- const findTooltip = () => wrapper.find('[data-testid="tooltip"]');
-
- describe('template', () => {
- it('renders the title', () => {
- factory();
-
- expect(findTitle().text()).toContain('My fancy title');
- });
-
- describe('when isLoading is true', () => {
- beforeEach(() => {
- factory({ isLoading: true });
- });
-
- it('displays a loading indicator', () => {
- expect(findLoadingIndicator().exists()).toBe(true);
- });
-
- it('does not display the metrics container', () => {
- expect(findMetricsWrapper().exists()).toBe(false);
- });
- });
-
- describe('when isLoading is false', () => {
- beforeEach(() => {
- factory({ isLoading: false });
- });
-
- it('does not display a loading indicator', () => {
- expect(findLoadingIndicator().exists()).toBe(false);
- });
-
- it('displays the metrics container', () => {
- expect(findMetricsWrapper().exists()).toBe(true);
- });
-
- it('renders two metrics', () => {
- expect(findMetricItem()).toHaveLength(metrics.length);
- });
-
- describe('with tooltip text', () => {
- const tooltipText = 'This is a tooltip';
- const tooltipMetric = {
- key: 'fifth_metric',
- value: '-',
- label: 'Metric with tooltip',
- unit: 'parsecs',
- tooltipText,
- };
-
- beforeEach(() => {
- factory({
- isLoading: false,
- metrics: [tooltipMetric],
- });
- });
-
- it('will render a tooltip', () => {
- const tt = getBinding(findTooltip().element, 'gl-tooltip');
- expect(tt.value.title).toEqual(tooltipText);
- });
- });
-
- describe.each`
- columnIndex | label | value | unit | link
- ${0} | ${'First metric'} | ${10} | ${' days'} | ${'some_link'}
- ${1} | ${'Yet another metric'} | ${20} | ${''} | ${null}
- ${2} | ${'Null metric without value'} | ${'-'} | ${''} | ${null}
- ${3} | ${'Metric without value'} | ${'-'} | ${''} | ${null}
- `('metric columns', ({ columnIndex, label, value, unit, link }) => {
- it(`renders ${value}${unit} ${label} with URL ${link}`, () => {
- const allMetricItems = findMetricItem();
- const metricItem = allMetricItems.at(columnIndex);
- const text = metricItem.text();
-
- expect(text).toContain(`${value}${unit}`);
- expect(text).toContain(label);
-
- if (link) {
- expect(metricItem.find('a').attributes('href')).toBe(link);
- } else {
- expect(metricItem.find('a').exists()).toBe(false);
- }
- });
- });
- });
- });
-});
diff --git a/spec/frontend/analytics/shared/components/projects_dropdown_filter_spec.js b/spec/frontend/analytics/shared/components/projects_dropdown_filter_spec.js
new file mode 100644
index 00000000000..2537b8fb816
--- /dev/null
+++ b/spec/frontend/analytics/shared/components/projects_dropdown_filter_spec.js
@@ -0,0 +1,264 @@
+import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
+import { mount } from '@vue/test-utils';
+import { TEST_HOST } from 'helpers/test_constants';
+import ProjectsDropdownFilter from '~/analytics/shared/components/projects_dropdown_filter.vue';
+import getProjects from '~/analytics/shared/graphql/projects.query.graphql';
+
+const projects = [
+ {
+ id: 'gid://gitlab/Project/1',
+ name: 'Gitlab Test',
+ fullPath: 'gitlab-org/gitlab-test',
+ avatarUrl: `${TEST_HOST}/images/home/nasa.svg`,
+ },
+ {
+ id: 'gid://gitlab/Project/2',
+ name: 'Gitlab Shell',
+ fullPath: 'gitlab-org/gitlab-shell',
+ avatarUrl: null,
+ },
+ {
+ id: 'gid://gitlab/Project/3',
+ name: 'Foo',
+ fullPath: 'gitlab-org/foo',
+ avatarUrl: null,
+ },
+];
+
+const defaultMocks = {
+ $apollo: {
+ query: jest.fn().mockResolvedValue({
+ data: { group: { projects: { nodes: projects } } },
+ }),
+ },
+};
+
+let spyQuery;
+
+describe('ProjectsDropdownFilter component', () => {
+ let wrapper;
+
+ const createComponent = (props = {}) => {
+ spyQuery = defaultMocks.$apollo.query;
+ wrapper = mount(ProjectsDropdownFilter, {
+ mocks: { ...defaultMocks },
+ propsData: {
+ groupId: 1,
+ groupNamespace: 'gitlab-org',
+ ...props,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ const findDropdown = () => wrapper.find(GlDropdown);
+
+ const findDropdownItems = () =>
+ findDropdown()
+ .findAll(GlDropdownItem)
+ .filter((w) => w.text() !== 'No matching results');
+
+ const findDropdownAtIndex = (index) => findDropdownItems().at(index);
+
+ const findDropdownButton = () => findDropdown().find('.dropdown-toggle');
+ const findDropdownButtonAvatar = () => findDropdown().find('.gl-avatar');
+ const findDropdownButtonAvatarAtIndex = (index) =>
+ findDropdownAtIndex(index).find('img.gl-avatar');
+ const findDropdownButtonIdentIconAtIndex = (index) =>
+ findDropdownAtIndex(index).find('div.gl-avatar-identicon');
+
+ const findDropdownNameAtIndex = (index) =>
+ findDropdownAtIndex(index).find('[data-testid="project-name"');
+ const findDropdownFullPathAtIndex = (index) =>
+ findDropdownAtIndex(index).find('[data-testid="project-full-path"]');
+
+ const selectDropdownItemAtIndex = (index) =>
+ findDropdownAtIndex(index).find('button').trigger('click');
+
+ const selectedIds = () => wrapper.vm.selectedProjects.map(({ id }) => id);
+
+ describe('queryParams are applied when fetching data', () => {
+ beforeEach(() => {
+ createComponent({
+ queryParams: {
+ first: 50,
+ includeSubgroups: true,
+ },
+ });
+ });
+
+ it('applies the correct queryParams when making an api call', async () => {
+ wrapper.setData({ searchTerm: 'gitlab' });
+
+ expect(spyQuery).toHaveBeenCalledTimes(1);
+
+ await wrapper.vm.$nextTick(() => {
+ expect(spyQuery).toHaveBeenCalledWith({
+ query: getProjects,
+ variables: {
+ search: 'gitlab',
+ groupFullPath: wrapper.vm.groupNamespace,
+ first: 50,
+ includeSubgroups: true,
+ },
+ });
+ });
+ });
+ });
+
+ describe('when passed a an array of defaultProject as prop', () => {
+ beforeEach(() => {
+ createComponent({
+ defaultProjects: [projects[0]],
+ });
+ });
+
+ it("displays the defaultProject's name", () => {
+ expect(findDropdownButton().text()).toContain(projects[0].name);
+ });
+
+ it("renders the defaultProject's avatar", () => {
+ expect(findDropdownButtonAvatar().exists()).toBe(true);
+ });
+
+ it('marks the defaultProject as selected', () => {
+ expect(findDropdownAtIndex(0).props('isChecked')).toBe(true);
+ });
+ });
+
+ describe('when multiSelect is false', () => {
+ beforeEach(() => {
+ createComponent({ multiSelect: false });
+ });
+
+ describe('displays the correct information', () => {
+ it('contains 3 items', () => {
+ expect(findDropdownItems()).toHaveLength(3);
+ });
+
+ it('renders an avatar when the project has an avatarUrl', () => {
+ expect(findDropdownButtonAvatarAtIndex(0).exists()).toBe(true);
+ expect(findDropdownButtonIdentIconAtIndex(0).exists()).toBe(false);
+ });
+
+ it("renders an identicon when the project doesn't have an avatarUrl", () => {
+ expect(findDropdownButtonAvatarAtIndex(1).exists()).toBe(false);
+ expect(findDropdownButtonIdentIconAtIndex(1).exists()).toBe(true);
+ });
+
+ it('renders the project name', () => {
+ projects.forEach((project, index) => {
+ expect(findDropdownNameAtIndex(index).text()).toBe(project.name);
+ });
+ });
+
+ it('renders the project fullPath', () => {
+ projects.forEach((project, index) => {
+ expect(findDropdownFullPathAtIndex(index).text()).toBe(project.fullPath);
+ });
+ });
+ });
+
+ describe('on project click', () => {
+ it('should emit the "selected" event with the selected project', () => {
+ selectDropdownItemAtIndex(0);
+
+ expect(wrapper.emitted().selected).toEqual([[[projects[0]]]]);
+ });
+
+ it('should change selection when new project is clicked', () => {
+ selectDropdownItemAtIndex(1);
+
+ expect(wrapper.emitted().selected).toEqual([[[projects[1]]]]);
+ });
+
+ it('selection should be emptied when a project is deselected', () => {
+ selectDropdownItemAtIndex(0); // Select the item
+ selectDropdownItemAtIndex(0); // deselect it
+
+ expect(wrapper.emitted().selected).toEqual([[[projects[0]]], [[]]]);
+ });
+
+ it('renders an avatar in the dropdown button when the project has an avatarUrl', async () => {
+ selectDropdownItemAtIndex(0);
+
+ await wrapper.vm.$nextTick().then(() => {
+ expect(findDropdownButtonAvatarAtIndex(0).exists()).toBe(true);
+ expect(findDropdownButtonIdentIconAtIndex(0).exists()).toBe(false);
+ });
+ });
+
+ it("renders an identicon in the dropdown button when the project doesn't have an avatarUrl", async () => {
+ selectDropdownItemAtIndex(1);
+
+ await wrapper.vm.$nextTick().then(() => {
+ expect(findDropdownButtonAvatarAtIndex(1).exists()).toBe(false);
+ expect(findDropdownButtonIdentIconAtIndex(1).exists()).toBe(true);
+ });
+ });
+ });
+ });
+
+ describe('when multiSelect is true', () => {
+ beforeEach(() => {
+ createComponent({ multiSelect: true });
+ });
+
+ describe('displays the correct information', () => {
+ it('contains 3 items', () => {
+ expect(findDropdownItems()).toHaveLength(3);
+ });
+
+ it('renders an avatar when the project has an avatarUrl', () => {
+ expect(findDropdownButtonAvatarAtIndex(0).exists()).toBe(true);
+ expect(findDropdownButtonIdentIconAtIndex(0).exists()).toBe(false);
+ });
+
+ it("renders an identicon when the project doesn't have an avatarUrl", () => {
+ expect(findDropdownButtonAvatarAtIndex(1).exists()).toBe(false);
+ expect(findDropdownButtonIdentIconAtIndex(1).exists()).toBe(true);
+ });
+
+ it('renders the project name', () => {
+ projects.forEach((project, index) => {
+ expect(findDropdownNameAtIndex(index).text()).toBe(project.name);
+ });
+ });
+
+ it('renders the project fullPath', () => {
+ projects.forEach((project, index) => {
+ expect(findDropdownFullPathAtIndex(index).text()).toBe(project.fullPath);
+ });
+ });
+ });
+
+ describe('on project click', () => {
+ it('should add to selection when new project is clicked', () => {
+ selectDropdownItemAtIndex(0);
+ selectDropdownItemAtIndex(1);
+
+ expect(selectedIds()).toEqual([projects[0].id, projects[1].id]);
+ });
+
+ 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 wrapper.vm.$nextTick().then(() => {
+ expect(findDropdownButton().text()).toBe('2 projects selected');
+ });
+ });
+ });
+ });
+});
diff --git a/spec/frontend/analytics/shared/utils_spec.js b/spec/frontend/analytics/shared/utils_spec.js
new file mode 100644
index 00000000000..e3293f2d8bd
--- /dev/null
+++ b/spec/frontend/analytics/shared/utils_spec.js
@@ -0,0 +1,24 @@
+import { filterBySearchTerm } from '~/analytics/shared/utils';
+
+describe('filterBySearchTerm', () => {
+ const data = [
+ { name: 'eins', title: 'one' },
+ { name: 'zwei', title: 'two' },
+ { name: 'drei', title: 'three' },
+ ];
+ const searchTerm = 'rei';
+
+ it('filters data by `name` for the provided search term', () => {
+ expect(filterBySearchTerm(data, searchTerm)).toEqual([data[2]]);
+ });
+
+ it('with no search term returns the data', () => {
+ ['', null].forEach((search) => {
+ expect(filterBySearchTerm(data, search)).toEqual(data);
+ });
+ });
+
+ it('with a key, filters by the provided key', () => {
+ expect(filterBySearchTerm(data, 'ne', 'title')).toEqual([data[0]]);
+ });
+});
diff --git a/spec/frontend/analytics/usage_trends/components/instance_counts_spec.js b/spec/frontend/analytics/usage_trends/components/usage_counts_spec.js
index 707d2cc310f..703767dab47 100644
--- a/spec/frontend/analytics/usage_trends/components/instance_counts_spec.js
+++ b/spec/frontend/analytics/usage_trends/components/usage_counts_spec.js
@@ -1,5 +1,6 @@
+import { GlDeprecatedSkeletonLoading as GlSkeletonLoading } from '@gitlab/ui';
+import { GlSingleStat } from '@gitlab/ui/dist/charts';
import { shallowMount } from '@vue/test-utils';
-import MetricCard from '~/analytics/shared/components/metric_card.vue';
import UsageCounts from '~/analytics/usage_trends/components/usage_counts.vue';
import { mockUsageCounts } from '../mock_data';
@@ -27,18 +28,18 @@ describe('UsageCounts', () => {
afterEach(() => {
wrapper.destroy();
- wrapper = null;
});
- const findMetricCard = () => wrapper.find(MetricCard);
+ const findSkeletonLoader = () => wrapper.findComponent(GlSkeletonLoading);
+ const findAllSingleStats = () => wrapper.findAllComponents(GlSingleStat);
describe('while loading', () => {
beforeEach(() => {
createComponent({ loading: true });
});
- it('displays the metric card with isLoading=true', () => {
- expect(findMetricCard().props('isLoading')).toBe(true);
+ it('displays a loading indicator', () => {
+ expect(findSkeletonLoader().exists()).toBe(true);
});
});
@@ -47,8 +48,15 @@ describe('UsageCounts', () => {
createComponent({ data: { counts: mockUsageCounts } });
});
- it('passes the counts data to the metric card', () => {
- expect(findMetricCard().props('metrics')).toEqual(mockUsageCounts);
+ it.each`
+ index | value | title
+ ${0} | ${mockUsageCounts[0].value} | ${mockUsageCounts[0].label}
+ ${1} | ${mockUsageCounts[1].value} | ${mockUsageCounts[1].label}
+ `('renders a GlSingleStat for "$title"', ({ index, value, title }) => {
+ const singleStat = findAllSingleStats().at(index);
+
+ expect(singleStat.props('value')).toBe(`${value}`);
+ expect(singleStat.props('title')).toBe(title);
});
});
});
diff --git a/spec/frontend/api_spec.js b/spec/frontend/api_spec.js
index f708d8c7728..c3e5a2973d7 100644
--- a/spec/frontend/api_spec.js
+++ b/spec/frontend/api_spec.js
@@ -1481,7 +1481,7 @@ describe('Api', () => {
'Content-Type': 'application/json',
};
- describe('when usage data increment counter is called with feature flag disabled', () => {
+ describe('when service data increment counter is called with feature flag disabled', () => {
beforeEach(() => {
gon.features = { ...gon.features, usageDataApi: false };
});
@@ -1495,7 +1495,7 @@ describe('Api', () => {
});
});
- describe('when usage data increment counter is called', () => {
+ describe('when service data increment counter is called', () => {
beforeEach(() => {
gon.features = { ...gon.features, usageDataApi: true };
});
@@ -1526,7 +1526,7 @@ describe('Api', () => {
window.gon.current_user_id = 1;
});
- describe('when usage data increment unique users is called with feature flag disabled', () => {
+ describe('when service data increment unique users is called with feature flag disabled', () => {
beforeEach(() => {
gon.features = { ...gon.features, usageDataApi: false };
});
@@ -1541,7 +1541,7 @@ describe('Api', () => {
});
});
- describe('when usage data increment unique users is called', () => {
+ describe('when service data increment unique users is called', () => {
beforeEach(() => {
gon.features = { ...gon.features, usageDataApi: true };
});
diff --git a/spec/frontend/batch_comments/components/draft_note_spec.js b/spec/frontend/batch_comments/components/draft_note_spec.js
index c2d488a465e..5d22823e974 100644
--- a/spec/frontend/batch_comments/components/draft_note_spec.js
+++ b/spec/frontend/batch_comments/components/draft_note_spec.js
@@ -1,5 +1,6 @@
import { getByRole } from '@testing-library/dom';
import { shallowMount, createLocalVue } from '@vue/test-utils';
+import { stubComponent } from 'helpers/stub_component';
import DraftNote from '~/batch_comments/components/draft_note.vue';
import { createStore } from '~/batch_comments/stores';
import NoteableNote from '~/notes/components/noteable_note.vue';
@@ -8,6 +9,14 @@ import { createDraft } from '../mock_data';
const localVue = createLocalVue();
+const NoteableNoteStub = stubComponent(NoteableNote, {
+ template: `
+ <div>
+ <slot name="note-header-info">Test</slot>
+ </div>
+ `,
+});
+
describe('Batch comments draft note component', () => {
let store;
let wrapper;
@@ -26,6 +35,9 @@ describe('Batch comments draft note component', () => {
store,
propsData,
localVue,
+ stubs: {
+ NoteableNote: NoteableNoteStub,
+ },
});
jest.spyOn(wrapper.vm.$store, 'dispatch').mockImplementation();
diff --git a/spec/frontend/blob/components/__snapshots__/blob_header_filepath_spec.js.snap b/spec/frontend/blob/components/__snapshots__/blob_header_filepath_spec.js.snap
index 53815820bbe..dfa6b99080b 100644
--- a/spec/frontend/blob/components/__snapshots__/blob_header_filepath_spec.js.snap
+++ b/spec/frontend/blob/components/__snapshots__/blob_header_filepath_spec.js.snap
@@ -10,7 +10,7 @@ exports[`Blob Header Filepath rendering matches the snapshot 1`] = `
cssclasses="mr-2"
filemode=""
filename="foo/bar/dummy.md"
- size="18"
+ size="16"
/>
<strong
diff --git a/spec/frontend/blob/components/blob_edit_content_spec.js b/spec/frontend/blob/components/blob_edit_content_spec.js
index 7de8d9236ed..9fc2356c018 100644
--- a/spec/frontend/blob/components/blob_edit_content_spec.js
+++ b/spec/frontend/blob/components/blob_edit_content_spec.js
@@ -3,7 +3,7 @@ import { nextTick } from 'vue';
import BlobEditContent from '~/blob/components/blob_edit_content.vue';
import * as utils from '~/blob/utils';
-jest.mock('~/editor/editor_lite');
+jest.mock('~/editor/source_editor');
describe('Blob Header Editing', () => {
let wrapper;
@@ -26,7 +26,7 @@ describe('Blob Header Editing', () => {
}
beforeEach(() => {
- jest.spyOn(utils, 'initEditorLite').mockImplementation(() => ({
+ jest.spyOn(utils, 'initSourceEditor').mockImplementation(() => ({
onDidChangeModelContent,
updateModelLanguage,
getValue,
@@ -68,9 +68,9 @@ describe('Blob Header Editing', () => {
expect(wrapper.find('#editor').exists()).toBe(true);
});
- it('initialises Editor Lite', () => {
+ it('initialises Source Editor', () => {
const el = wrapper.find({ ref: 'editor' }).element;
- expect(utils.initEditorLite).toHaveBeenCalledWith({
+ expect(utils.initSourceEditor).toHaveBeenCalledWith({
el,
blobPath: fileName,
blobGlobalId: fileGlobalId,
diff --git a/spec/frontend/blob/csv/csv_viewer_spec.js b/spec/frontend/blob/csv/csv_viewer_spec.js
new file mode 100644
index 00000000000..abb914b8f57
--- /dev/null
+++ b/spec/frontend/blob/csv/csv_viewer_spec.js
@@ -0,0 +1,75 @@
+import { GlAlert, GlLoadingIcon, GlTable } from '@gitlab/ui';
+import { getAllByRole } from '@testing-library/dom';
+import { shallowMount, mount } from '@vue/test-utils';
+import { nextTick } from 'vue';
+import CSVViewer from '~/blob/csv/csv_viewer.vue';
+
+const validCsv = 'one,two,three';
+const brokenCsv = '{\n "json": 1,\n "key": [1, 2, 3]\n}';
+
+describe('app/assets/javascripts/blob/csv/csv_viewer.vue', () => {
+ let wrapper;
+
+ const createComponent = ({ csv = validCsv, mountFunction = shallowMount } = {}) => {
+ wrapper = mountFunction(CSVViewer, {
+ propsData: {
+ csv,
+ },
+ });
+ };
+
+ const findCsvTable = () => wrapper.findComponent(GlTable);
+ const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
+ const findAlert = () => wrapper.findComponent(GlAlert);
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('should render loading spinner', () => {
+ createComponent();
+
+ expect(findLoadingIcon().props()).toMatchObject({
+ size: 'lg',
+ });
+ });
+
+ describe('when the CSV contains errors', () => {
+ it('should render alert', async () => {
+ createComponent({ csv: brokenCsv });
+ await nextTick;
+
+ expect(findAlert().props()).toMatchObject({
+ variant: 'danger',
+ });
+ });
+ });
+
+ describe('when the CSV contains no errors', () => {
+ it('should not render alert', async () => {
+ createComponent();
+ await nextTick;
+
+ expect(findAlert().exists()).toBe(false);
+ });
+
+ it('renders the CSV table with the correct attributes', async () => {
+ createComponent();
+ await nextTick;
+
+ expect(findCsvTable().attributes()).toMatchObject({
+ 'empty-text': 'No CSV data to display.',
+ items: validCsv,
+ });
+ });
+
+ it('renders the CSV table with the correct content', async () => {
+ createComponent({ mountFunction: mount });
+ await nextTick;
+
+ expect(getAllByRole(wrapper.element, 'row', { name: /One/i })).toHaveLength(1);
+ expect(getAllByRole(wrapper.element, 'row', { name: /Two/i })).toHaveLength(1);
+ expect(getAllByRole(wrapper.element, 'row', { name: /Three/i })).toHaveLength(1);
+ });
+ });
+});
diff --git a/spec/frontend/blob/utils_spec.js b/spec/frontend/blob/utils_spec.js
index 3ff2e47e0b6..a543c0060cb 100644
--- a/spec/frontend/blob/utils_spec.js
+++ b/spec/frontend/blob/utils_spec.js
@@ -1,10 +1,10 @@
import * as utils from '~/blob/utils';
-import Editor from '~/editor/editor_lite';
+import Editor from '~/editor/source_editor';
-jest.mock('~/editor/editor_lite');
+jest.mock('~/editor/source_editor');
describe('Blob utilities', () => {
- describe('initEditorLite', () => {
+ describe('initSourceEditor', () => {
let editorEl;
const blobPath = 'foo.txt';
const blobContent = 'Foo bar';
@@ -15,8 +15,8 @@ describe('Blob utilities', () => {
});
describe('Monaco editor', () => {
- it('initializes the Editor Lite', () => {
- utils.initEditorLite({ el: editorEl });
+ it('initializes the Source Editor', () => {
+ utils.initSourceEditor({ el: editorEl });
expect(Editor).toHaveBeenCalledWith({
scrollbar: {
alwaysConsumeMouseWheel: false,
@@ -34,7 +34,7 @@ describe('Blob utilities', () => {
expect(Editor.prototype.createInstance).not.toHaveBeenCalled();
- utils.initEditorLite(params);
+ utils.initSourceEditor(params);
expect(Editor.prototype.createInstance).toHaveBeenCalledWith(params);
},
diff --git a/spec/frontend/blob/viewer/index_spec.js b/spec/frontend/blob/viewer/index_spec.js
index e4f145ae81b..6a24b76abc8 100644
--- a/spec/frontend/blob/viewer/index_spec.js
+++ b/spec/frontend/blob/viewer/index_spec.js
@@ -6,6 +6,10 @@ import { setTestTimeout } from 'helpers/timeout';
import BlobViewer from '~/blob/viewer/index';
import axios from '~/lib/utils/axios_utils';
+const execImmediately = (callback) => {
+ callback();
+};
+
describe('Blob viewer', () => {
let blob;
let mock;
@@ -17,6 +21,7 @@ describe('Blob viewer', () => {
setTestTimeout(2000);
beforeEach(() => {
+ jest.spyOn(window, 'requestIdleCallback').mockImplementation(execImmediately);
$.fn.extend(jQueryMock);
mock = new MockAdapter(axios);
diff --git a/spec/frontend/blob_edit/blob_bundle_spec.js b/spec/frontend/blob_edit/blob_bundle_spec.js
index eecc54be35b..8986dfbfa9c 100644
--- a/spec/frontend/blob_edit/blob_bundle_spec.js
+++ b/spec/frontend/blob_edit/blob_bundle_spec.js
@@ -3,21 +3,21 @@ import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
import waitForPromises from 'helpers/wait_for_promises';
import blobBundle from '~/blob_edit/blob_bundle';
-import EditorLite from '~/blob_edit/edit_blob';
+import SourceEditor from '~/blob_edit/edit_blob';
jest.mock('~/blob_edit/edit_blob');
describe('BlobBundle', () => {
- it('does not load EditorLite by default', () => {
+ it('does not load SourceEditor by default', () => {
blobBundle();
- expect(EditorLite).not.toHaveBeenCalled();
+ expect(SourceEditor).not.toHaveBeenCalled();
});
- it('loads EditorLite for the edit screen', async () => {
+ it('loads SourceEditor for the edit screen', async () => {
setFixtures(`<div class="js-edit-blob-form"></div>`);
blobBundle();
await waitForPromises();
- expect(EditorLite).toHaveBeenCalled();
+ expect(SourceEditor).toHaveBeenCalled();
});
describe('No Suggest Popover', () => {
diff --git a/spec/frontend/blob_edit/edit_blob_spec.js b/spec/frontend/blob_edit/edit_blob_spec.js
index 3134feedcf3..2be72ded0a2 100644
--- a/spec/frontend/blob_edit/edit_blob_spec.js
+++ b/spec/frontend/blob_edit/edit_blob_spec.js
@@ -1,12 +1,12 @@
import waitForPromises from 'helpers/wait_for_promises';
import EditBlob from '~/blob_edit/edit_blob';
-import EditorLite from '~/editor/editor_lite';
-import { FileTemplateExtension } from '~/editor/extensions/editor_file_template_ext';
-import { EditorMarkdownExtension } from '~/editor/extensions/editor_markdown_ext';
+import { FileTemplateExtension } from '~/editor/extensions/source_editor_file_template_ext';
+import { EditorMarkdownExtension } from '~/editor/extensions/source_editor_markdown_ext';
+import SourceEditor from '~/editor/source_editor';
-jest.mock('~/editor/editor_lite');
-jest.mock('~/editor/extensions/editor_markdown_ext');
-jest.mock('~/editor/extensions/editor_file_template_ext');
+jest.mock('~/editor/source_editor');
+jest.mock('~/editor/extensions/source_editor_markdown_ext');
+jest.mock('~/editor/extensions/source_editor_file_template_ext');
describe('Blob Editing', () => {
const useMock = jest.fn();
@@ -24,7 +24,7 @@ describe('Blob Editing', () => {
<textarea id="file-content"></textarea>
</form>
`);
- jest.spyOn(EditorLite.prototype, 'createInstance').mockReturnValue(mockInstance);
+ jest.spyOn(SourceEditor.prototype, 'createInstance').mockReturnValue(mockInstance);
});
afterEach(() => {
EditorMarkdownExtension.mockClear();
diff --git a/spec/frontend/boards/board_card_inner_spec.js b/spec/frontend/boards/board_card_inner_spec.js
index 15ea5d4eec4..87f9a68f5dd 100644
--- a/spec/frontend/boards/board_card_inner_spec.js
+++ b/spec/frontend/boards/board_card_inner_spec.js
@@ -1,7 +1,7 @@
-import { GlLabel, GlLoadingIcon } from '@gitlab/ui';
-import { mount } from '@vue/test-utils';
+import { GlLabel, GlLoadingIcon, GlTooltip } from '@gitlab/ui';
import { range } from 'lodash';
import Vuex from 'vuex';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
import BoardBlockedIcon from '~/boards/components/board_blocked_icon.vue';
import BoardCardInner from '~/boards/components/board_card_inner.vue';
import { issuableTypes } from '~/boards/constants';
@@ -35,8 +35,16 @@ describe('Board card component', () => {
let store;
const findBoardBlockedIcon = () => wrapper.find(BoardBlockedIcon);
-
- const createStore = () => {
+ const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
+ const findEpicCountablesTotalTooltip = () => wrapper.findComponent(GlTooltip);
+ const findEpicCountables = () => wrapper.findByTestId('epic-countables');
+ const findEpicCountablesBadgeIssues = () => wrapper.findByTestId('epic-countables-counts-issues');
+ const findEpicCountablesBadgeWeight = () => wrapper.findByTestId('epic-countables-weight-issues');
+ const findEpicBadgeProgress = () => wrapper.findByTestId('epic-progress');
+ const findEpicCountablesTotalWeight = () => wrapper.findByTestId('epic-countables-total-weight');
+ const findEpicProgressTooltip = () => wrapper.findByTestId('epic-progress-tooltip-content');
+
+ const createStore = ({ isEpicBoard = false } = {}) => {
store = new Vuex.Store({
...defaultStore,
state: {
@@ -45,16 +53,14 @@ describe('Board card component', () => {
},
getters: {
isGroupBoard: () => true,
- isEpicBoard: () => false,
+ isEpicBoard: () => isEpicBoard,
isProjectBoard: () => false,
},
});
};
const createWrapper = (props = {}) => {
- createStore();
-
- wrapper = mount(BoardCardInner, {
+ wrapper = mountExtended(BoardCardInner, {
store,
propsData: {
list,
@@ -88,6 +94,7 @@ describe('Board card component', () => {
weight: 1,
};
+ createStore();
createWrapper({ item: issue, list });
});
@@ -414,7 +421,108 @@ describe('Board card component', () => {
},
});
- expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
+ expect(findLoadingIcon().exists()).toBe(true);
+ });
+ });
+
+ describe('is an epic board', () => {
+ const descendantCounts = {
+ closedEpics: 0,
+ closedIssues: 0,
+ openedEpics: 0,
+ openedIssues: 0,
+ };
+
+ const descendantWeightSum = {
+ closedIssues: 0,
+ openedIssues: 0,
+ };
+
+ beforeEach(() => {
+ createStore({ isEpicBoard: true });
+ });
+
+ it('should render if the item has issues', () => {
+ createWrapper({
+ item: {
+ ...issue,
+ descendantCounts,
+ descendantWeightSum,
+ hasIssues: true,
+ },
+ });
+
+ expect(findEpicCountables().exists()).toBe(true);
+ });
+
+ it('should not render if the item does not have issues', () => {
+ createWrapper({
+ item: {
+ ...issue,
+ descendantCounts,
+ descendantWeightSum,
+ hasIssues: false,
+ },
+ });
+
+ expect(findEpicCountablesBadgeIssues().exists()).toBe(false);
+ });
+
+ it('shows render item countBadge, weights, and progress correctly', () => {
+ createWrapper({
+ item: {
+ ...issue,
+ descendantCounts: {
+ ...descendantCounts,
+ openedIssues: 1,
+ },
+ descendantWeightSum: {
+ closedIssues: 10,
+ openedIssues: 5,
+ },
+ hasIssues: true,
+ },
+ });
+
+ expect(findEpicCountablesBadgeIssues().text()).toBe('1');
+ expect(findEpicCountablesBadgeWeight().text()).toBe('15');
+ expect(findEpicBadgeProgress().text()).toBe('67%');
+ });
+
+ it('does not render progress when weight is zero', () => {
+ createWrapper({
+ item: {
+ ...issue,
+ descendantCounts: {
+ ...descendantCounts,
+ openedIssues: 1,
+ },
+ descendantWeightSum,
+ hasIssues: true,
+ },
+ });
+
+ expect(findEpicBadgeProgress().exists()).toBe(false);
+ });
+
+ it('renders the tooltip with the correct data', () => {
+ createWrapper({
+ item: {
+ ...issue,
+ descendantCounts,
+ descendantWeightSum: {
+ closedIssues: 10,
+ openedIssues: 5,
+ },
+ hasIssues: true,
+ },
+ });
+
+ const tooltip = findEpicCountablesTotalTooltip();
+ expect(tooltip).toBeDefined();
+
+ expect(findEpicCountablesTotalWeight().text()).toBe('15');
+ expect(findEpicProgressTooltip().text()).toBe('10 of 15 weight completed');
});
});
});
diff --git a/spec/frontend/boards/board_list_helper.js b/spec/frontend/boards/board_list_helper.js
index 915b470df8d..c440c110094 100644
--- a/spec/frontend/boards/board_list_helper.js
+++ b/spec/frontend/boards/board_list_helper.js
@@ -1,34 +1,57 @@
-/* global List */
-/* global ListIssue */
-import MockAdapter from 'axios-mock-adapter';
-import Sortable from 'sortablejs';
-import Vue from 'vue';
-import BoardList from '~/boards/components/board_list_deprecated.vue';
-import '~/boards/models/issue';
-import '~/boards/models/list';
-import store from '~/boards/stores';
-import boardsStore from '~/boards/stores/boards_store';
-import axios from '~/lib/utils/axios_utils';
-import { listObj, boardsMockInterceptor } from './mock_data';
+import { createLocalVue, shallowMount } from '@vue/test-utils';
+import Vuex from 'vuex';
-window.Sortable = Sortable;
+import BoardCard from '~/boards/components/board_card.vue';
+import BoardList from '~/boards/components/board_list.vue';
+import BoardNewIssue from '~/boards/components/board_new_issue.vue';
+import defaultState from '~/boards/stores/state';
+import { mockList, mockIssuesByListId, issues } from './mock_data';
export default function createComponent({
- done,
listIssueProps = {},
componentProps = {},
listProps = {},
-}) {
- const el = document.createElement('div');
+ actions = {},
+ getters = {},
+ provide = {},
+ state = defaultState,
+ stubs = {
+ BoardNewIssue,
+ BoardCard,
+ },
+} = {}) {
+ const localVue = createLocalVue();
+ localVue.use(Vuex);
- document.body.appendChild(el);
- const mock = new MockAdapter(axios);
- mock.onAny().reply(boardsMockInterceptor);
- boardsStore.create();
+ const store = new Vuex.Store({
+ state: {
+ boardItemsByListId: mockIssuesByListId,
+ boardItems: issues,
+ pageInfoByListId: {
+ 'gid://gitlab/List/1': { hasNextPage: true },
+ 'gid://gitlab/List/2': {},
+ },
+ listsFlags: {
+ 'gid://gitlab/List/1': {},
+ 'gid://gitlab/List/2': {},
+ },
+ selectedBoardItems: [],
+ ...state,
+ },
+ getters: {
+ isGroupBoard: () => false,
+ isProjectBoard: () => true,
+ isEpicBoard: () => false,
+ ...getters,
+ },
+ actions,
+ });
- const BoardListComp = Vue.extend(BoardList);
- const list = new List({ ...listObj, ...listProps });
- const issue = new ListIssue({
+ const list = {
+ ...mockList,
+ ...listProps,
+ };
+ const issue = {
title: 'Testing',
id: 1,
iid: 1,
@@ -36,31 +59,31 @@ export default function createComponent({
labels: [],
assignees: [],
...listIssueProps,
- });
- if (!Object.prototype.hasOwnProperty.call(listProps, 'issuesSize')) {
- list.issuesSize = 1;
+ };
+ if (!Object.prototype.hasOwnProperty.call(listProps, 'issuesCount')) {
+ list.issuesCount = 1;
}
- list.issues.push(issue);
- const component = new BoardListComp({
- el,
+ const component = shallowMount(BoardList, {
+ localVue,
store,
propsData: {
disabled: false,
list,
- issues: list.issues,
- loading: false,
+ boardItems: [issue],
+ canAdminList: true,
...componentProps,
},
provide: {
groupId: null,
rootPath: '/',
+ weightFeatureAvailable: false,
+ boardWeight: null,
+ canAdminList: true,
+ ...provide,
},
- }).$mount();
-
- Vue.nextTick(() => {
- done();
+ stubs,
});
- return { component, mock };
+ return component;
}
diff --git a/spec/frontend/boards/board_list_spec.js b/spec/frontend/boards/board_list_spec.js
index 76629c96f22..a3b1810ab80 100644
--- a/spec/frontend/boards/board_list_spec.js
+++ b/spec/frontend/boards/board_list_spec.js
@@ -1,95 +1,9 @@
-import { createLocalVue, shallowMount } from '@vue/test-utils';
-import Vuex from 'vuex';
import { useFakeRequestAnimationFrame } from 'helpers/fake_request_animation_frame';
+import createComponent from 'jest/boards/board_list_helper';
import BoardCard from '~/boards/components/board_card.vue';
-import BoardList from '~/boards/components/board_list.vue';
-import BoardNewIssue from '~/boards/components/board_new_issue.vue';
import eventHub from '~/boards/eventhub';
-import defaultState from '~/boards/stores/state';
-import { mockList, mockIssuesByListId, issues, mockIssues } from './mock_data';
-
-const localVue = createLocalVue();
-localVue.use(Vuex);
-
-const actions = {
- fetchItemsForList: jest.fn(),
-};
-
-const createStore = (state = defaultState) => {
- return new Vuex.Store({
- state,
- actions,
- getters: {
- isGroupBoard: () => false,
- isProjectBoard: () => true,
- isEpicBoard: () => false,
- },
- });
-};
-
-const createComponent = ({
- listIssueProps = {},
- componentProps = {},
- listProps = {},
- state = {},
-} = {}) => {
- const store = createStore({
- boardItemsByListId: mockIssuesByListId,
- boardItems: issues,
- pageInfoByListId: {
- 'gid://gitlab/List/1': { hasNextPage: true },
- 'gid://gitlab/List/2': {},
- },
- listsFlags: {
- 'gid://gitlab/List/1': {},
- 'gid://gitlab/List/2': {},
- },
- selectedBoardItems: [],
- ...state,
- });
- const list = {
- ...mockList,
- ...listProps,
- };
- const issue = {
- title: 'Testing',
- id: 1,
- iid: 1,
- confidential: false,
- labels: [],
- assignees: [],
- ...listIssueProps,
- };
- if (!Object.prototype.hasOwnProperty.call(listProps, 'issuesCount')) {
- list.issuesCount = 1;
- }
-
- const component = shallowMount(BoardList, {
- localVue,
- propsData: {
- disabled: false,
- list,
- boardItems: [issue],
- canAdminList: true,
- ...componentProps,
- },
- store,
- provide: {
- groupId: null,
- rootPath: '/',
- weightFeatureAvailable: false,
- boardWeight: null,
- canAdminList: true,
- },
- stubs: {
- BoardCard,
- BoardNewIssue,
- },
- });
-
- return component;
-};
+import { mockIssues } from './mock_data';
describe('Board list component', () => {
let wrapper;
@@ -101,7 +15,6 @@ describe('Board list component', () => {
afterEach(() => {
wrapper.destroy();
- wrapper = null;
});
describe('When Expanded', () => {
@@ -176,6 +89,10 @@ describe('Board list component', () => {
});
describe('load more issues', () => {
+ const actions = {
+ fetchItemsForList: jest.fn(),
+ };
+
beforeEach(() => {
wrapper = createComponent({
listProps: { issuesCount: 25 },
@@ -184,6 +101,7 @@ describe('Board list component', () => {
it('does not load issues if already loading', () => {
wrapper = createComponent({
+ actions,
state: { listsFlags: { 'gid://gitlab/List/1': { isLoadingMore: true } } },
});
wrapper.vm.listRef.dispatchEvent(new Event('scroll'));
diff --git a/spec/frontend/boards/boards_util_spec.js b/spec/frontend/boards/boards_util_spec.js
index 289905a1948..d45b6e35a45 100644
--- a/spec/frontend/boards/boards_util_spec.js
+++ b/spec/frontend/boards/boards_util_spec.js
@@ -1,4 +1,35 @@
-import { filterVariables } from '~/boards/boards_util';
+import { formatIssueInput, filterVariables } from '~/boards/boards_util';
+
+describe('formatIssueInput', () => {
+ it('correctly merges boardConfig into the issue', () => {
+ const boardConfig = {
+ labels: [
+ {
+ type: 'GroupLabel',
+ id: 44,
+ },
+ ],
+ assigneeId: '55',
+ milestoneId: 66,
+ weight: 1,
+ };
+
+ const issueInput = {
+ labelIds: ['gid://gitlab/GroupLabel/5'],
+ projectPath: 'gitlab-org/gitlab-test',
+ id: 'gid://gitlab/Issue/11',
+ };
+
+ const result = formatIssueInput(issueInput, boardConfig);
+ expect(result).toEqual({
+ projectPath: 'gitlab-org/gitlab-test',
+ id: 'gid://gitlab/Issue/11',
+ labelIds: ['gid://gitlab/GroupLabel/5', 'gid://gitlab/GroupLabel/44'],
+ assigneeIds: ['gid://gitlab/User/55'],
+ milestoneId: 'gid://gitlab/Milestone/66',
+ });
+ });
+});
describe('filterVariables', () => {
it.each([
diff --git a/spec/frontend/boards/components/board_column_spec.js b/spec/frontend/boards/components/board_column_spec.js
index 4e523d636cd..f1964daa8b2 100644
--- a/spec/frontend/boards/components/board_column_spec.js
+++ b/spec/frontend/boards/components/board_column_spec.js
@@ -15,6 +15,10 @@ describe('Board Column Component', () => {
wrapper = null;
});
+ const initStore = () => {
+ store = createStore();
+ };
+
const createComponent = ({ listType = ListType.backlog, collapsed = false } = {}) => {
const boardId = '1';
@@ -29,8 +33,6 @@ describe('Board Column Component', () => {
listMock.assignee = {};
}
- store = createStore();
-
wrapper = shallowMount(BoardColumn, {
store,
propsData: {
@@ -47,6 +49,10 @@ describe('Board Column Component', () => {
const isCollapsed = () => wrapper.classes('is-collapsed');
describe('Given different list types', () => {
+ beforeEach(() => {
+ initStore();
+ });
+
it('is expandable when List Type is `backlog`', () => {
createComponent({ listType: ListType.backlog });
@@ -79,4 +85,31 @@ describe('Board Column Component', () => {
expect(wrapper.element.scrollIntoView).toHaveBeenCalled();
});
});
+
+ describe('on mount', () => {
+ beforeEach(async () => {
+ initStore();
+ jest.spyOn(store, 'dispatch').mockImplementation();
+ });
+
+ describe('when list is collapsed', () => {
+ it('does not call fetchItemsForList when', async () => {
+ createComponent({ collapsed: true });
+
+ await nextTick();
+
+ expect(store.dispatch).toHaveBeenCalledTimes(0);
+ });
+ });
+
+ describe('when the list is not collapsed', () => {
+ it('calls fetchItemsForList when', async () => {
+ createComponent({ collapsed: false });
+
+ await nextTick();
+
+ expect(store.dispatch).toHaveBeenCalledWith('fetchItemsForList', { listId: 300 });
+ });
+ });
+ });
});
diff --git a/spec/frontend/boards/components/board_content_sidebar_spec.js b/spec/frontend/boards/components/board_content_sidebar_spec.js
index 10d739c65f5..8a8250205d0 100644
--- a/spec/frontend/boards/components/board_content_sidebar_spec.js
+++ b/spec/frontend/boards/components/board_content_sidebar_spec.js
@@ -1,5 +1,6 @@
import { GlDrawer } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
+import { MountingPortal } from 'portal-vue';
import Vuex from 'vuex';
import SidebarDropdownWidget from 'ee_else_ce/sidebar/components/sidebar_dropdown_widget.vue';
import { stubComponent } from 'helpers/stub_component';
@@ -9,7 +10,8 @@ import BoardSidebarTitle from '~/boards/components/sidebar/board_sidebar_title.v
import { ISSUABLE } from '~/boards/constants';
import SidebarDateWidget from '~/sidebar/components/date/sidebar_date_widget.vue';
import SidebarSubscriptionsWidget from '~/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue';
-import { mockIssue, mockIssueGroupPath, mockIssueProjectPath } from '../mock_data';
+import SidebarTodoWidget from '~/sidebar/components/todo_toggle/sidebar_todo_widget.vue';
+import { mockActiveIssue, mockIssue, mockIssueGroupPath, mockIssueProjectPath } from '../mock_data';
describe('BoardContentSidebar', () => {
let wrapper;
@@ -25,7 +27,7 @@ describe('BoardContentSidebar', () => {
},
getters: {
activeBoardItem: () => {
- return { ...mockIssue, epic: null };
+ return { ...mockActiveIssue, epic: null };
},
groupPathForActiveIssue: () => mockIssueGroupPath,
projectPathForActiveIssue: () => mockIssueProjectPath,
@@ -90,6 +92,14 @@ describe('BoardContentSidebar', () => {
expect(wrapper.findComponent(GlDrawer).exists()).toBe(true);
});
+ it('confirms we render MountingPortal', () => {
+ expect(wrapper.find(MountingPortal).props()).toMatchObject({
+ mountTo: '#js-right-sidebar-portal',
+ append: true,
+ name: 'board-content-sidebar',
+ });
+ });
+
it('does not render GlDrawer when isSidebarOpen is false', () => {
createStore({ mockGetters: { isSidebarOpen: () => false } });
createComponent();
@@ -101,6 +111,10 @@ describe('BoardContentSidebar', () => {
expect(wrapper.findComponent(GlDrawer).props('open')).toBe(true);
});
+ it('renders SidebarTodoWidget', () => {
+ expect(wrapper.findComponent(SidebarTodoWidget).exists()).toBe(true);
+ });
+
it('renders BoardSidebarLabelsSelect', () => {
expect(wrapper.findComponent(BoardSidebarLabelsSelect).exists()).toBe(true);
});
@@ -138,7 +152,7 @@ describe('BoardContentSidebar', () => {
expect(toggleBoardItem).toHaveBeenCalledTimes(1);
expect(toggleBoardItem).toHaveBeenCalledWith(expect.any(Object), {
- boardItem: { ...mockIssue, epic: null },
+ boardItem: { ...mockActiveIssue, epic: null },
sidebarType: ISSUABLE,
});
});
diff --git a/spec/frontend/boards/components/board_content_spec.js b/spec/frontend/boards/components/board_content_spec.js
index 8c1a7bd3947..5a799b6388e 100644
--- a/spec/frontend/boards/components/board_content_spec.js
+++ b/spec/frontend/boards/components/board_content_spec.js
@@ -1,5 +1,6 @@
import { GlAlert } from '@gitlab/ui';
-import { createLocalVue, shallowMount } from '@vue/test-utils';
+import { shallowMount } from '@vue/test-utils';
+import Vue from 'vue';
import Draggable from 'vuedraggable';
import Vuex from 'vuex';
import EpicsSwimlanes from 'ee_component/boards/components/epics_swimlanes.vue';
@@ -8,8 +9,7 @@ import BoardColumnDeprecated from '~/boards/components/board_column_deprecated.v
import BoardContent from '~/boards/components/board_content.vue';
import { mockLists, mockListsWithModel } from '../mock_data';
-const localVue = createLocalVue();
-localVue.use(Vuex);
+Vue.use(Vuex);
const actions = {
moveList: jest.fn(),
@@ -44,7 +44,6 @@ describe('BoardContent', () => {
...state,
});
wrapper = shallowMount(BoardContent, {
- localVue,
propsData: {
lists: mockListsWithModel,
disabled: false,
diff --git a/spec/frontend/boards/components/board_form_spec.js b/spec/frontend/boards/components/board_form_spec.js
index 80d740458dc..3966c3e6b87 100644
--- a/spec/frontend/boards/components/board_form_spec.js
+++ b/spec/frontend/boards/components/board_form_spec.js
@@ -12,8 +12,8 @@ import { createStore } from '~/boards/stores';
import { visitUrl } from '~/lib/utils/url_utility';
jest.mock('~/lib/utils/url_utility', () => ({
+ ...jest.requireActual('~/lib/utils/url_utility'),
visitUrl: jest.fn().mockName('visitUrlMock'),
- stripFinalUrlSegment: jest.requireActual('~/lib/utils/url_utility').stripFinalUrlSegment,
}));
const currentBoard = {
diff --git a/spec/frontend/boards/components/board_settings_sidebar_spec.js b/spec/frontend/boards/components/board_settings_sidebar_spec.js
index 464331b6e30..20a08be6c19 100644
--- a/spec/frontend/boards/components/board_settings_sidebar_spec.js
+++ b/spec/frontend/boards/components/board_settings_sidebar_spec.js
@@ -3,6 +3,7 @@ import { GlDrawer, GlLabel } from '@gitlab/ui';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
+import { MountingPortal } from 'portal-vue';
import Vuex from 'vuex';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import BoardSettingsSidebar from '~/boards/components/board_settings_sidebar.vue';
@@ -51,6 +52,16 @@ describe('BoardSettingsSidebar', () => {
wrapper.destroy();
});
+ it('finds a MountingPortal component', () => {
+ createComponent();
+
+ expect(wrapper.find(MountingPortal).props()).toMatchObject({
+ mountTo: '#js-right-sidebar-portal',
+ append: true,
+ name: 'board-settings-sidebar',
+ });
+ });
+
describe('when sidebarType is "list"', () => {
it('finds a GlDrawer component', () => {
createComponent();
diff --git a/spec/frontend/boards/components/issue_board_filtered_search_spec.js b/spec/frontend/boards/components/issue_board_filtered_search_spec.js
new file mode 100644
index 00000000000..0e3cf59901e
--- /dev/null
+++ b/spec/frontend/boards/components/issue_board_filtered_search_spec.js
@@ -0,0 +1,44 @@
+import { shallowMount } from '@vue/test-utils';
+import BoardFilteredSearch from '~/boards/components/board_filtered_search.vue';
+import IssueBoardFilteredSpec from '~/boards/components/issue_board_filtered_search.vue';
+import { BoardType } from '~/boards/constants';
+import issueBoardFilters from '~/boards/issue_board_filters';
+import { mockTokens } from '../mock_data';
+
+describe('IssueBoardFilter', () => {
+ let wrapper;
+
+ const createComponent = ({ initialFilterParams = {} } = {}) => {
+ wrapper = shallowMount(IssueBoardFilteredSpec, {
+ provide: { initialFilterParams },
+ props: { fullPath: '', boardType: '' },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('default', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('finds BoardFilteredSearch', () => {
+ expect(wrapper.find(BoardFilteredSearch).exists()).toBe(true);
+ });
+
+ it.each([[BoardType.group], [BoardType.project]])(
+ 'when boardType is %s we pass the correct tokens to BoardFilteredSearch',
+ (boardType) => {
+ const { fetchAuthors, fetchLabels } = issueBoardFilters({}, '', boardType);
+
+ const tokens = mockTokens(fetchLabels, fetchAuthors);
+
+ expect(wrapper.find(BoardFilteredSearch).props('tokens').toString()).toBe(
+ tokens.toString(),
+ );
+ },
+ );
+ });
+});
diff --git a/spec/frontend/boards/mock_data.js b/spec/frontend/boards/mock_data.js
index bcaca9522e4..6ac4db8cdaa 100644
--- a/spec/frontend/boards/mock_data.js
+++ b/spec/frontend/boards/mock_data.js
@@ -5,6 +5,9 @@ import Vue from 'vue';
import '~/boards/models/list';
import { ListType } from '~/boards/constants';
import boardsStore from '~/boards/stores/boards_store';
+import { __ } from '~/locale';
+import AuthorToken from '~/vue_shared/components/filtered_search_bar/tokens/author_token.vue';
+import LabelToken from '~/vue_shared/components/filtered_search_bar/tokens/label_token.vue';
export const boardObj = {
id: 1,
@@ -179,6 +182,7 @@ export const mockIssue = {
export const mockActiveIssue = {
...mockIssue,
+ fullId: 'gid://gitlab/Issue/436',
id: 436,
iid: '27',
subscribed: false,
@@ -287,7 +291,7 @@ export const setMockEndpoints = (opts = {}) => {
export const mockList = {
id: 'gid://gitlab/List/1',
- title: 'Backlog',
+ title: 'Open',
position: -Infinity,
listType: 'backlog',
collapsed: false,
@@ -526,3 +530,44 @@ export const mockMoveData = {
originalIssue: { foo: 'bar' },
...mockMoveIssueParams,
};
+
+export const mockTokens = (fetchLabels, fetchAuthors) => [
+ {
+ icon: 'labels',
+ title: __('Label'),
+ type: 'label_name',
+ operators: [
+ { value: '=', description: 'is' },
+ { value: '!=', description: 'is not' },
+ ],
+ token: LabelToken,
+ unique: false,
+ symbol: '~',
+ fetchLabels,
+ },
+ {
+ icon: 'pencil',
+ title: __('Author'),
+ type: 'author_username',
+ operators: [
+ { value: '=', description: 'is' },
+ { value: '!=', description: 'is not' },
+ ],
+ symbol: '@',
+ token: AuthorToken,
+ unique: true,
+ fetchAuthors,
+ },
+ {
+ icon: 'user',
+ title: __('Assignee'),
+ type: 'assignee_username',
+ operators: [
+ { value: '=', description: 'is' },
+ { value: '!=', description: 'is not' },
+ ],
+ token: AuthorToken,
+ unique: true,
+ fetchAuthors,
+ },
+];
diff --git a/spec/frontend/boards/stores/actions_spec.js b/spec/frontend/boards/stores/actions_spec.js
index b28412f2127..5e16e389ddc 100644
--- a/spec/frontend/boards/stores/actions_spec.js
+++ b/spec/frontend/boards/stores/actions_spec.js
@@ -492,6 +492,63 @@ describe('moveList', () => {
});
describe('updateList', () => {
+ const listId = 'gid://gitlab/List/1';
+ const createState = (boardItemsByListId = {}) => ({
+ fullPath: 'gitlab-org',
+ fullBoardId: 'gid://gitlab/Board/1',
+ boardType: 'group',
+ disabled: false,
+ boardLists: [{ type: 'closed' }],
+ issuableType: issuableTypes.issue,
+ boardItemsByListId,
+ });
+
+ describe('when state doesnt have list items', () => {
+ it('calls fetchItemsByList', async () => {
+ const dispatch = jest.fn();
+
+ jest.spyOn(gqlClient, 'mutate').mockResolvedValue({
+ data: {
+ updateBoardList: {
+ errors: [],
+ list: {
+ id: listId,
+ },
+ },
+ },
+ });
+
+ await actions.updateList({ commit: () => {}, state: createState(), dispatch }, { listId });
+
+ expect(dispatch.mock.calls).toEqual([['fetchItemsForList', { listId }]]);
+ });
+ });
+
+ describe('when state has list items', () => {
+ it('doesnt call fetchItemsByList', async () => {
+ const commit = jest.fn();
+ const dispatch = jest.fn();
+
+ jest.spyOn(gqlClient, 'mutate').mockResolvedValue({
+ data: {
+ updateBoardList: {
+ errors: [],
+ list: {
+ id: listId,
+ },
+ },
+ },
+ });
+
+ await actions.updateList(
+ { commit, state: createState({ [listId]: [] }), dispatch },
+ { listId },
+ );
+
+ expect(dispatch.mock.calls).toEqual([]);
+ });
+ });
+
it('should commit UPDATE_LIST_FAILURE mutation when API returns an error', (done) => {
jest.spyOn(gqlClient, 'mutate').mockResolvedValue({
data: {
@@ -502,19 +559,10 @@ describe('updateList', () => {
},
});
- const state = {
- fullPath: 'gitlab-org',
- fullBoardId: 'gid://gitlab/Board/1',
- boardType: 'group',
- disabled: false,
- boardLists: [{ type: 'closed' }],
- issuableType: issuableTypes.issue,
- };
-
testAction(
actions.updateList,
{ listId: 'gid://gitlab/List/1', position: 1 },
- state,
+ createState(),
[{ type: types.UPDATE_LIST_FAILURE }],
[],
done,
@@ -667,6 +715,19 @@ describe('fetchItemsForList', () => {
[listId]: pageInfo,
};
+ describe('when list id is undefined', () => {
+ it('does not call the query', async () => {
+ jest.spyOn(gqlClient, 'query').mockResolvedValue(queryResponse);
+
+ await actions.fetchItemsForList(
+ { state, getters: () => {}, commit: () => {} },
+ { listId: undefined },
+ );
+
+ expect(gqlClient.query).toHaveBeenCalledTimes(0);
+ });
+ });
+
it('should commit mutations REQUEST_ITEMS_FOR_LIST and RECEIVE_ITEMS_FOR_LIST_SUCCESS on success', (done) => {
jest.spyOn(gqlClient, 'query').mockResolvedValue(queryResponse);
@@ -1111,16 +1172,13 @@ describe('updateIssueOrder', () => {
describe('setAssignees', () => {
const node = { username: 'name' };
- const projectPath = 'h/h';
- const refPath = `${projectPath}#3`;
- const iid = '1';
describe('when succeeds', () => {
it('calls the correct mutation with the correct values', (done) => {
testAction(
actions.setAssignees,
- [node],
- { activeBoardItem: { iid, referencePath: refPath }, commit: () => {} },
+ { assignees: [node], iid: '1' },
+ { commit: () => {} },
[
{
type: 'UPDATE_BOARD_ITEM_BY_ID',
diff --git a/spec/frontend/boards/stores/getters_spec.js b/spec/frontend/boards/stores/getters_spec.js
index e7efb21bee5..c0774dd3ae1 100644
--- a/spec/frontend/boards/stores/getters_spec.js
+++ b/spec/frontend/boards/stores/getters_spec.js
@@ -92,7 +92,7 @@ describe('Boards - Getters', () => {
it.each`
id | expected
${'1'} | ${'issue'}
- ${''} | ${{}}
+ ${''} | ${{ id: '', iid: '', fullId: '' }}
`('returns $expected when $id is passed to state', ({ id, expected }) => {
const state = { boardItems: { 1: 'issue' }, activeId: id };
diff --git a/spec/frontend/boards/stores/mutations_spec.js b/spec/frontend/boards/stores/mutations_spec.js
index 5b38f04e77b..37f0969a39a 100644
--- a/spec/frontend/boards/stores/mutations_spec.js
+++ b/spec/frontend/boards/stores/mutations_spec.js
@@ -35,6 +35,7 @@ describe('Board Store Mutations', () => {
describe('SET_INITIAL_BOARD_DATA', () => {
it('Should set initial Boards data to state', () => {
+ const allowSubEpics = true;
const boardId = 1;
const fullPath = 'gitlab-org';
const boardType = 'group';
@@ -45,6 +46,7 @@ describe('Board Store Mutations', () => {
const issuableType = issuableTypes.issue;
mutations[types.SET_INITIAL_BOARD_DATA](state, {
+ allowSubEpics,
boardId,
fullPath,
boardType,
@@ -53,6 +55,7 @@ describe('Board Store Mutations', () => {
issuableType,
});
+ expect(state.allowSubEpics).toBe(allowSubEpics);
expect(state.boardId).toEqual(boardId);
expect(state.fullPath).toEqual(fullPath);
expect(state.boardType).toEqual(boardType);
diff --git a/spec/frontend/branches/components/delete_branch_button_spec.js b/spec/frontend/branches/components/delete_branch_button_spec.js
index acbc83a9bdc..b029f34c3d7 100644
--- a/spec/frontend/branches/components/delete_branch_button_spec.js
+++ b/spec/frontend/branches/components/delete_branch_button_spec.js
@@ -34,7 +34,7 @@ describe('Delete branch button', () => {
expect(findDeleteButton().attributes()).toMatchObject({
title: 'Delete branch',
- variant: 'danger',
+ variant: 'default',
icon: 'remove',
});
});
@@ -44,7 +44,7 @@ describe('Delete branch button', () => {
expect(findDeleteButton().attributes()).toMatchObject({
title: 'Delete protected branch',
- variant: 'danger',
+ variant: 'default',
icon: 'remove',
});
});
@@ -78,7 +78,7 @@ describe('Delete branch button', () => {
expect(findDeleteButton().attributes()).toMatchObject({
title: 'Delete branch',
- variant: 'danger',
+ variant: 'default',
});
});
diff --git a/spec/frontend/captcha/captcha_modal_axios_interceptor_spec.js b/spec/frontend/captcha/captcha_modal_axios_interceptor_spec.js
index df81b78d010..553ca52f9ce 100644
--- a/spec/frontend/captcha/captcha_modal_axios_interceptor_spec.js
+++ b/spec/frontend/captcha/captcha_modal_axios_interceptor_spec.js
@@ -1,6 +1,7 @@
import MockAdapter from 'axios-mock-adapter';
import { registerCaptchaModalInterceptor } from '~/captcha/captcha_modal_axios_interceptor';
+import UnsolvedCaptchaError from '~/captcha/unsolved_captcha_error';
import { waitForCaptchaToBeSolved } from '~/captcha/wait_for_captcha_to_be_solved';
import axios from '~/lib/utils/axios_utils';
import httpStatusCodes from '~/lib/utils/http_status';
@@ -25,22 +26,24 @@ describe('registerCaptchaModalInterceptor', () => {
let mock;
beforeEach(() => {
+ waitForCaptchaToBeSolved.mockRejectedValue(new UnsolvedCaptchaError());
+
mock = new MockAdapter(axios);
- mock.onAny('/no-captcha').reply(200, AXIOS_RESPONSE);
- mock.onAny('/error').reply(404, AXIOS_RESPONSE);
- mock.onAny('/captcha').reply((config) => {
+ mock.onAny('/endpoint-without-captcha').reply(200, AXIOS_RESPONSE);
+ mock.onAny('/endpoint-with-unrelated-error').reply(404, AXIOS_RESPONSE);
+ mock.onAny('/endpoint-with-captcha').reply((config) => {
if (!supportedMethods.includes(config.method)) {
return [httpStatusCodes.METHOD_NOT_ALLOWED, { method: config.method }];
}
- try {
- const { captcha_response, spam_log_id, ...rest } = JSON.parse(config.data);
- // eslint-disable-next-line babel/camelcase
- if (captcha_response === CAPTCHA_RESPONSE && spam_log_id === SPAM_LOG_ID) {
- return [httpStatusCodes.OK, { ...rest, method: config.method, CAPTCHA_SUCCESS }];
- }
- } catch (e) {
- return [httpStatusCodes.BAD_REQUEST, { method: config.method }];
+ const data = JSON.parse(config.data);
+ const {
+ 'X-GitLab-Captcha-Response': captchaResponse,
+ 'X-GitLab-Spam-Log-Id': spamLogId,
+ } = config.headers;
+
+ if (captchaResponse === CAPTCHA_RESPONSE && spamLogId === SPAM_LOG_ID) {
+ return [httpStatusCodes.OK, { ...data, method: config.method, CAPTCHA_SUCCESS }];
}
return [httpStatusCodes.CONFLICT, NEEDS_CAPTCHA_RESPONSE];
@@ -56,7 +59,7 @@ describe('registerCaptchaModalInterceptor', () => {
describe.each([...supportedMethods, ...unsupportedMethods])('For HTTP method %s', (method) => {
it('successful requests are passed through', async () => {
- const { data, status } = await axios[method]('/no-captcha');
+ const { data, status } = await axios[method]('/endpoint-without-captcha');
expect(status).toEqual(httpStatusCodes.OK);
expect(data).toEqual(AXIOS_RESPONSE);
@@ -64,7 +67,7 @@ describe('registerCaptchaModalInterceptor', () => {
});
it('error requests without needs_captcha_response_errors are passed through', async () => {
- await expect(() => axios[method]('/error')).rejects.toThrow(
+ await expect(() => axios[method]('/endpoint-with-unrelated-error')).rejects.toThrow(
expect.objectContaining({
response: expect.objectContaining({
status: httpStatusCodes.NOT_FOUND,
@@ -79,21 +82,35 @@ describe('registerCaptchaModalInterceptor', () => {
describe.each(supportedMethods)('For HTTP method %s', (method) => {
describe('error requests with needs_captcha_response_errors', () => {
const submittedData = { ID: 12345 };
+ const submittedHeaders = { 'Submitted-Header': 67890 };
it('re-submits request if captcha was solved correctly', async () => {
- waitForCaptchaToBeSolved.mockResolvedValue(CAPTCHA_RESPONSE);
- const { data: returnedData } = await axios[method]('/captcha', submittedData);
+ waitForCaptchaToBeSolved.mockResolvedValueOnce(CAPTCHA_RESPONSE);
+ const axiosResponse = await axios[method]('/endpoint-with-captcha', submittedData, {
+ headers: submittedHeaders,
+ });
+ const {
+ data: returnedData,
+ config: { headers: returnedHeaders },
+ } = axiosResponse;
expect(waitForCaptchaToBeSolved).toHaveBeenCalledWith(CAPTCHA_SITE_KEY);
expect(returnedData).toEqual({ ...submittedData, CAPTCHA_SUCCESS, method });
+ expect(returnedHeaders).toEqual(
+ expect.objectContaining({
+ ...submittedHeaders,
+ 'X-GitLab-Captcha-Response': CAPTCHA_RESPONSE,
+ 'X-GitLab-Spam-Log-Id': SPAM_LOG_ID,
+ }),
+ );
expect(mock.history[method]).toHaveLength(2);
});
it('does not re-submit request if captcha was not solved', async () => {
- const error = new Error('Captcha not solved');
- waitForCaptchaToBeSolved.mockRejectedValue(error);
- await expect(() => axios[method]('/captcha', submittedData)).rejects.toThrow(error);
+ await expect(() => axios[method]('/endpoint-with-captcha', submittedData)).rejects.toThrow(
+ new UnsolvedCaptchaError(),
+ );
expect(waitForCaptchaToBeSolved).toHaveBeenCalledWith(CAPTCHA_SITE_KEY);
expect(mock.history[method]).toHaveLength(1);
@@ -103,7 +120,7 @@ describe('registerCaptchaModalInterceptor', () => {
describe.each(unsupportedMethods)('For HTTP method %s', (method) => {
it('ignores captcha response', async () => {
- await expect(() => axios[method]('/captcha')).rejects.toThrow(
+ await expect(() => axios[method]('/endpoint-with-captcha')).rejects.toThrow(
expect.objectContaining({
response: expect.objectContaining({
status: httpStatusCodes.METHOD_NOT_ALLOWED,
diff --git a/spec/frontend/ci_lint/components/ci_lint_spec.js b/spec/frontend/ci_lint/components/ci_lint_spec.js
index 8a065436da0..36d860b1ccd 100644
--- a/spec/frontend/ci_lint/components/ci_lint_spec.js
+++ b/spec/frontend/ci_lint/components/ci_lint_spec.js
@@ -4,7 +4,7 @@ import waitForPromises from 'helpers/wait_for_promises';
import CiLint from '~/ci_lint/components/ci_lint.vue';
import CiLintResults from '~/pipeline_editor/components/lint/ci_lint_results.vue';
import lintCIMutation from '~/pipeline_editor/graphql/mutations/lint_ci.mutation.graphql';
-import EditorLite from '~/vue_shared/components/editor_lite.vue';
+import SourceEditor from '~/vue_shared/components/source_editor.vue';
import { mockLintDataValid } from '../mock_data';
describe('CI Lint', () => {
@@ -35,7 +35,7 @@ describe('CI Lint', () => {
});
};
- const findEditor = () => wrapper.find(EditorLite);
+ const findEditor = () => wrapper.find(SourceEditor);
const findAlert = () => wrapper.find(GlAlert);
const findCiLintResults = () => wrapper.find(CiLintResults);
const findValidateBtn = () => wrapper.find('[data-testid="ci-lint-validate"]');
diff --git a/spec/frontend/clusters/clusters_bundle_spec.js b/spec/frontend/clusters/clusters_bundle_spec.js
index cd0eda2ab49..42990334f0a 100644
--- a/spec/frontend/clusters/clusters_bundle_spec.js
+++ b/spec/frontend/clusters/clusters_bundle_spec.js
@@ -2,15 +2,12 @@ import MockAdapter from 'axios-mock-adapter';
import { loadHTMLFixture } from 'helpers/fixtures';
import { setTestTimeout } from 'helpers/timeout';
import Clusters from '~/clusters/clusters_bundle';
-import { APPLICATION_STATUS, APPLICATIONS, RUNNER } from '~/clusters/constants';
import axios from '~/lib/utils/axios_utils';
import initProjectSelectDropdown from '~/project_select';
jest.mock('~/lib/utils/poll');
jest.mock('~/project_select');
-const { INSTALLING, INSTALLABLE, INSTALLED, UNINSTALLING } = APPLICATION_STATUS;
-
describe('Clusters', () => {
setTestTimeout(1000);
@@ -57,67 +54,6 @@ describe('Clusters', () => {
});
});
- describe('checkForNewInstalls', () => {
- const INITIAL_APP_MAP = {
- helm: { status: null, title: 'Helm Tiller' },
- ingress: { status: null, title: 'Ingress' },
- runner: { status: null, title: 'GitLab Runner' },
- };
-
- it('does not show alert when things transition from initial null state to something', () => {
- cluster.checkForNewInstalls(INITIAL_APP_MAP, {
- ...INITIAL_APP_MAP,
- helm: { status: INSTALLABLE, title: 'Helm Tiller' },
- });
-
- const flashMessage = document.querySelector('.js-cluster-application-notice .flash-text');
-
- expect(flashMessage).toBeNull();
- });
-
- it('shows an alert when something gets newly installed', () => {
- cluster.checkForNewInstalls(
- {
- ...INITIAL_APP_MAP,
- helm: { status: INSTALLING, title: 'Helm Tiller' },
- },
- {
- ...INITIAL_APP_MAP,
- helm: { status: INSTALLED, title: 'Helm Tiller' },
- },
- );
-
- const flashMessage = document.querySelector('.js-cluster-application-notice .flash-text');
-
- expect(flashMessage).not.toBeNull();
- expect(flashMessage.textContent.trim()).toEqual(
- 'Helm Tiller was successfully installed on your Kubernetes cluster',
- );
- });
-
- it('shows an alert when multiple things gets newly installed', () => {
- cluster.checkForNewInstalls(
- {
- ...INITIAL_APP_MAP,
- helm: { status: INSTALLING, title: 'Helm Tiller' },
- ingress: { status: INSTALLABLE, title: 'Ingress' },
- },
- {
- ...INITIAL_APP_MAP,
- helm: { status: INSTALLED, title: 'Helm Tiller' },
- ingress: { status: INSTALLED, title: 'Ingress' },
- },
- );
-
- const flashMessage = document.querySelector('.js-cluster-application-notice .flash-text');
-
- expect(flashMessage).not.toBeNull();
- expect(flashMessage.textContent.trim()).toEqual(
- 'Helm Tiller, Ingress was successfully installed on your Kubernetes cluster',
- );
- });
- });
-
describe('updateContainer', () => {
const { location } = window;
@@ -237,77 +173,6 @@ describe('Clusters', () => {
});
});
- describe('installApplication', () => {
- it.each(APPLICATIONS)('tries to install %s', (applicationId, done) => {
- jest.spyOn(cluster.service, 'installApplication').mockResolvedValue();
-
- cluster.store.state.applications[applicationId].status = INSTALLABLE;
-
- const params = {};
- if (applicationId === 'knative') {
- params.hostname = 'test-example.com';
- }
-
- // eslint-disable-next-line promise/valid-params
- cluster
- .installApplication({ id: applicationId, params })
- .then(() => {
- expect(cluster.store.state.applications[applicationId].status).toEqual(INSTALLING);
- expect(cluster.store.state.applications[applicationId].requestReason).toEqual(null);
- expect(cluster.service.installApplication).toHaveBeenCalledWith(applicationId, params);
- done();
- })
- .catch();
- });
-
- it('sets error request status when the request fails', () => {
- jest
- .spyOn(cluster.service, 'installApplication')
- .mockRejectedValueOnce(new Error('STUBBED ERROR'));
-
- cluster.store.state.applications.helm.status = INSTALLABLE;
-
- const promise = cluster.installApplication({ id: 'helm' });
-
- return promise.then(() => {
- expect(cluster.store.state.applications.helm.status).toEqual(INSTALLABLE);
- expect(cluster.store.state.applications.helm.installFailed).toBe(true);
-
- expect(cluster.store.state.applications.helm.requestReason).toBeDefined();
- });
- });
- });
-
- describe('uninstallApplication', () => {
- it.each(APPLICATIONS)('tries to uninstall %s', (applicationId) => {
- jest.spyOn(cluster.service, 'uninstallApplication').mockResolvedValueOnce();
-
- cluster.store.state.applications[applicationId].status = INSTALLED;
-
- cluster.uninstallApplication({ id: applicationId });
-
- expect(cluster.store.state.applications[applicationId].status).toEqual(UNINSTALLING);
- expect(cluster.store.state.applications[applicationId].requestReason).toEqual(null);
- expect(cluster.service.uninstallApplication).toHaveBeenCalledWith(applicationId);
- });
-
- it('sets error request status when the uninstall request fails', () => {
- jest
- .spyOn(cluster.service, 'uninstallApplication')
- .mockRejectedValueOnce(new Error('STUBBED ERROR'));
-
- cluster.store.state.applications.helm.status = INSTALLED;
-
- const promise = cluster.uninstallApplication({ id: 'helm' });
-
- return promise.then(() => {
- expect(cluster.store.state.applications.helm.status).toEqual(INSTALLED);
- expect(cluster.store.state.applications.helm.uninstallFailed).toBe(true);
- expect(cluster.store.state.applications.helm.requestReason).toBeDefined();
- });
- });
- });
-
describe('fetch cluster environments success', () => {
beforeEach(() => {
jest.spyOn(cluster.store, 'toggleFetchEnvironments').mockReturnThis();
@@ -328,7 +193,6 @@ describe('Clusters', () => {
describe('handleClusterStatusSuccess', () => {
beforeEach(() => {
jest.spyOn(cluster.store, 'updateStateFromServer').mockReturnThis();
- jest.spyOn(cluster, 'checkForNewInstalls').mockReturnThis();
jest.spyOn(cluster, 'updateContainer').mockReturnThis();
cluster.handleClusterStatusSuccess({ data: {} });
});
@@ -337,38 +201,8 @@ describe('Clusters', () => {
expect(cluster.store.updateStateFromServer).toHaveBeenCalled();
});
- it('checks for new installable apps', () => {
- expect(cluster.checkForNewInstalls).toHaveBeenCalled();
- });
-
it('updates message containers', () => {
expect(cluster.updateContainer).toHaveBeenCalled();
});
});
-
- describe('updateApplication', () => {
- const params = { version: '1.0.0' };
- let storeUpdateApplication;
- let installApplication;
-
- beforeEach(() => {
- storeUpdateApplication = jest.spyOn(cluster.store, 'updateApplication');
- installApplication = jest.spyOn(cluster.service, 'installApplication');
-
- cluster.updateApplication({ id: RUNNER, params });
- });
-
- afterEach(() => {
- storeUpdateApplication.mockRestore();
- installApplication.mockRestore();
- });
-
- it('calls store updateApplication method', () => {
- expect(storeUpdateApplication).toHaveBeenCalledWith(RUNNER);
- });
-
- it('sends installApplication request', () => {
- expect(installApplication).toHaveBeenCalledWith(RUNNER, params);
- });
- });
});
diff --git a/spec/frontend/clusters/components/__snapshots__/applications_spec.js.snap b/spec/frontend/clusters/components/__snapshots__/applications_spec.js.snap
deleted file mode 100644
index c2ace1b4e30..00000000000
--- a/spec/frontend/clusters/components/__snapshots__/applications_spec.js.snap
+++ /dev/null
@@ -1,105 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`Applications Cert-Manager application shows the correct description 1`] = `
-<p
- data-testid="certManagerDescription"
->
- Cert-Manager is a native Kubernetes certificate management controller that helps with issuing certificates. Installing Cert-Manager on your cluster will issue a certificate by
- <a
- class="gl-link"
- href="https://letsencrypt.org/"
- rel="noopener noreferrer"
- target="_blank"
- >
- Let's Encrypt
- </a>
- and ensure that certificates are valid and up-to-date.
-</p>
-`;
-
-exports[`Applications Cilium application shows the correct description 1`] = `
-<p
- data-testid="ciliumDescription"
->
- Protect your clusters with GitLab Container Network Policies by enforcing how pods communicate with each other and other network endpoints.
- <a
- class="gl-link"
- href="cilium-help-path"
- rel="noopener"
- target="_blank"
- >
- Learn more about configuring Network Policies here.
- </a>
-</p>
-`;
-
-exports[`Applications Crossplane application shows the correct description 1`] = `
-<p
- data-testid="crossplaneDescription"
->
- Crossplane enables declarative provisioning of managed services from your cloud of choice using
- <code>
- kubectl
- </code>
- or
- <a
- class="gl-link"
- href="https://docs.gitlab.com/ee/user/clusters/applications.html#crossplane"
- rel="noopener noreferrer"
- target="_blank"
- >
- GitLab Integration
- </a>
- . Crossplane runs inside your Kubernetes cluster and supports secure connectivity and secrets management between app containers and the cloud services they depend on.
-</p>
-`;
-
-exports[`Applications Ingress application shows the correct warning message 1`] = `
-<span
- data-testid="ingressCostWarning"
->
- Installing Ingress may incur additional costs. Learn more about
- <a
- class="gl-link"
- href="https://cloud.google.com/compute/pricing#lb"
- rel="noopener noreferrer"
- target="_blank"
- >
- pricing
- </a>
- .
-</span>
-`;
-
-exports[`Applications Knative application shows the correct description 1`] = `
-<span
- data-testid="installed-via"
->
- installed via
- <a
- class="gl-link"
- href=""
- rel="noopener"
- target="_blank"
- >
- Cloud Run
- </a>
-</span>
-`;
-
-exports[`Applications Prometheus application shows the correct description 1`] = `
-<span
- data-testid="prometheusDescription"
->
- Prometheus is an open-source monitoring system with
- <a
- class="gl-link"
- href="https://docs.gitlab.com/ee/user/project/integrations/prometheus.html"
- rel="noopener noreferrer"
- target="_blank"
- >
- GitLab Integration
- </a>
- to monitor deployed applications.
-</span>
-`;
diff --git a/spec/frontend/clusters/components/__snapshots__/remove_cluster_confirmation_spec.js.snap b/spec/frontend/clusters/components/__snapshots__/remove_cluster_confirmation_spec.js.snap
index e5e336eb3d5..0e1fe790771 100644
--- a/spec/frontend/clusters/components/__snapshots__/remove_cluster_confirmation_spec.js.snap
+++ b/spec/frontend/clusters/components/__snapshots__/remove_cluster_confirmation_spec.js.snap
@@ -156,7 +156,6 @@ exports[`Remove cluster confirmation modal renders splitbutton with modal includ
<!---->
</div>
-
</ul>
</div>
diff --git a/spec/frontend/clusters/components/application_row_spec.js b/spec/frontend/clusters/components/application_row_spec.js
deleted file mode 100644
index 6bad1db542b..00000000000
--- a/spec/frontend/clusters/components/application_row_spec.js
+++ /dev/null
@@ -1,505 +0,0 @@
-import { GlSprintf } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
-import ApplicationRow from '~/clusters/components/application_row.vue';
-import UninstallApplicationConfirmationModal from '~/clusters/components/uninstall_application_confirmation_modal.vue';
-import UpdateApplicationConfirmationModal from '~/clusters/components/update_application_confirmation_modal.vue';
-import { APPLICATION_STATUS, ELASTIC_STACK } from '~/clusters/constants';
-import eventHub from '~/clusters/event_hub';
-
-import { DEFAULT_APPLICATION_STATE } from '../services/mock_data';
-
-describe('Application Row', () => {
- let wrapper;
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- const mountComponent = (data) => {
- wrapper = shallowMount(ApplicationRow, {
- stubs: { GlSprintf },
- propsData: {
- ...DEFAULT_APPLICATION_STATE,
- ...data,
- },
- });
- };
-
- describe('Title', () => {
- it('shows title', () => {
- mountComponent({ titleLink: null });
-
- const title = wrapper.find('.js-cluster-application-title');
-
- expect(title.element).toBeInstanceOf(HTMLSpanElement);
- expect(title.text()).toEqual(DEFAULT_APPLICATION_STATE.title);
- });
-
- it('shows title link', () => {
- expect(DEFAULT_APPLICATION_STATE.titleLink).toBeDefined();
- mountComponent();
- const title = wrapper.find('.js-cluster-application-title');
-
- expect(title.element).toBeInstanceOf(HTMLAnchorElement);
- expect(title.text()).toEqual(DEFAULT_APPLICATION_STATE.title);
- });
- });
-
- describe('Install button', () => {
- const button = () => wrapper.find('.js-cluster-application-install-button');
- const checkButtonState = (label, loading, disabled) => {
- expect(button().text()).toEqual(label);
- expect(button().props('loading')).toEqual(loading);
- expect(button().props('disabled')).toEqual(disabled);
- };
-
- it('has indeterminate state on page load', () => {
- mountComponent({ status: null });
-
- expect(button().text()).toBe('');
- });
-
- it('has install button', () => {
- mountComponent();
-
- expect(button().exists()).toBe(true);
- });
-
- it('has disabled "Install" when APPLICATION_STATUS.NOT_INSTALLABLE', () => {
- mountComponent({ status: APPLICATION_STATUS.NOT_INSTALLABLE });
-
- checkButtonState('Install', false, true);
- });
-
- it('has enabled "Install" when APPLICATION_STATUS.INSTALLABLE', () => {
- mountComponent({ status: APPLICATION_STATUS.INSTALLABLE });
-
- checkButtonState('Install', false, false);
- });
-
- it('has loading "Installing" when APPLICATION_STATUS.INSTALLING', () => {
- mountComponent({ status: APPLICATION_STATUS.INSTALLING });
-
- checkButtonState('Installing', true, true);
- });
-
- it('has disabled "Install" when APPLICATION_STATUS.UNINSTALLED', () => {
- mountComponent({ status: APPLICATION_STATUS.UNINSTALLED });
-
- checkButtonState('Install', false, true);
- });
-
- it('has disabled "Externally installed" when APPLICATION_STATUS.EXTERNALLY_INSTALLED', () => {
- mountComponent({ status: APPLICATION_STATUS.EXTERNALLY_INSTALLED });
-
- checkButtonState('Externally installed', false, true);
- });
-
- it('has disabled "Installed" when application is installed and not uninstallable', () => {
- mountComponent({
- status: APPLICATION_STATUS.INSTALLED,
- installed: true,
- uninstallable: false,
- });
-
- checkButtonState('Installed', false, true);
- });
-
- it('hides when application is installed and uninstallable', () => {
- mountComponent({
- status: APPLICATION_STATUS.INSTALLED,
- installed: true,
- uninstallable: true,
- });
-
- expect(button().exists()).toBe(false);
- });
-
- it('has enabled "Install" when install fails', () => {
- mountComponent({
- status: APPLICATION_STATUS.INSTALLABLE,
- installFailed: true,
- });
-
- checkButtonState('Install', false, false);
- });
-
- it('has disabled "Install" when installation disabled', () => {
- mountComponent({
- status: APPLICATION_STATUS.INSTALLABLE,
- installable: false,
- });
-
- checkButtonState('Install', false, true);
- });
-
- it('has enabled "Install" when REQUEST_FAILURE (so you can try installing again)', () => {
- mountComponent({ status: APPLICATION_STATUS.INSTALLABLE });
-
- checkButtonState('Install', false, false);
- });
-
- it('clicking install button emits event', () => {
- const spy = jest.spyOn(eventHub, '$emit');
- mountComponent({ status: APPLICATION_STATUS.INSTALLABLE });
-
- button().vm.$emit('click');
-
- expect(spy).toHaveBeenCalledWith('installApplication', {
- id: DEFAULT_APPLICATION_STATE.id,
- params: {},
- });
- });
-
- it('clicking install button when installApplicationRequestParams are provided emits event', () => {
- const spy = jest.spyOn(eventHub, '$emit');
- mountComponent({
- status: APPLICATION_STATUS.INSTALLABLE,
- installApplicationRequestParams: { hostname: 'jupyter' },
- });
-
- button().vm.$emit('click');
-
- expect(spy).toHaveBeenCalledWith('installApplication', {
- id: DEFAULT_APPLICATION_STATE.id,
- params: { hostname: 'jupyter' },
- });
- });
-
- it('clicking disabled install button emits nothing', () => {
- const spy = jest.spyOn(eventHub, '$emit');
- mountComponent({ status: APPLICATION_STATUS.INSTALLING });
-
- expect(button().props('disabled')).toEqual(true);
-
- button().vm.$emit('click');
-
- expect(spy).not.toHaveBeenCalled();
- });
- });
-
- describe('Uninstall button', () => {
- it('displays button when app is installed and uninstallable', () => {
- mountComponent({
- installed: true,
- uninstallable: true,
- status: APPLICATION_STATUS.NOT_INSTALLABLE,
- });
- const uninstallButton = wrapper.find('.js-cluster-application-uninstall-button');
-
- expect(uninstallButton.exists()).toBe(true);
- });
-
- it('displays a success toast message if application uninstall was successful', async () => {
- mountComponent({
- title: 'GitLab Runner',
- uninstallSuccessful: false,
- });
-
- wrapper.vm.$toast = { show: jest.fn() };
- wrapper.setProps({ uninstallSuccessful: true });
-
- await wrapper.vm.$nextTick();
- expect(wrapper.vm.$toast.show).toHaveBeenCalledWith(
- 'GitLab Runner uninstalled successfully.',
- );
- });
- });
-
- describe('when confirmation modal triggers confirm event', () => {
- it('triggers uninstallApplication event', () => {
- jest.spyOn(eventHub, '$emit');
- mountComponent();
- wrapper.find(UninstallApplicationConfirmationModal).vm.$emit('confirm');
-
- expect(eventHub.$emit).toHaveBeenCalledWith('uninstallApplication', {
- id: DEFAULT_APPLICATION_STATE.id,
- });
- });
- });
-
- describe('Update button', () => {
- const button = () => wrapper.find('.js-cluster-application-update-button');
-
- it('has indeterminate state on page load', () => {
- mountComponent();
-
- expect(button().exists()).toBe(false);
- });
-
- it('has enabled "Update" when "updateAvailable" is true', () => {
- mountComponent({ updateAvailable: true });
-
- expect(button().exists()).toBe(true);
- expect(button().text()).toContain('Update');
- });
-
- it('has enabled "Retry update" when update process fails', () => {
- mountComponent({
- status: APPLICATION_STATUS.INSTALLED,
- updateFailed: true,
- });
-
- expect(button().exists()).toBe(true);
- expect(button().text()).toContain('Retry update');
- });
-
- it('has disabled "Updating" when APPLICATION_STATUS.UPDATING', () => {
- mountComponent({ status: APPLICATION_STATUS.UPDATING });
-
- expect(button().exists()).toBe(true);
- expect(button().text()).toContain('Updating');
- });
-
- it('clicking update button emits event', () => {
- const spy = jest.spyOn(eventHub, '$emit');
- mountComponent({
- status: APPLICATION_STATUS.INSTALLED,
- updateAvailable: true,
- });
-
- button().vm.$emit('click');
-
- expect(spy).toHaveBeenCalledWith('updateApplication', {
- id: DEFAULT_APPLICATION_STATE.id,
- params: {},
- });
- });
-
- it('clicking disabled update button emits nothing', () => {
- const spy = jest.spyOn(eventHub, '$emit');
- mountComponent({ status: APPLICATION_STATUS.UPDATING });
-
- button().vm.$emit('click');
-
- expect(spy).not.toHaveBeenCalled();
- });
-
- it('displays an error message if application update failed', () => {
- mountComponent({
- title: 'GitLab Runner',
- status: APPLICATION_STATUS.INSTALLED,
- updateFailed: true,
- });
- const failureMessage = wrapper.find('.js-cluster-application-update-details');
-
- expect(failureMessage.exists()).toBe(true);
- expect(failureMessage.text()).toContain(
- 'Update failed. Please check the logs and try again.',
- );
- });
-
- it('displays a success toast message if application update was successful', async () => {
- mountComponent({
- title: 'GitLab Runner',
- updateSuccessful: false,
- });
-
- wrapper.vm.$toast = { show: jest.fn() };
- wrapper.setProps({ updateSuccessful: true });
-
- await wrapper.vm.$nextTick();
- expect(wrapper.vm.$toast.show).toHaveBeenCalledWith('GitLab Runner updated successfully.');
- });
-
- describe('when updating does not require confirmation', () => {
- beforeEach(() => mountComponent({ updateAvailable: true }));
-
- it('the modal is not rendered', () => {
- expect(wrapper.find(UpdateApplicationConfirmationModal).exists()).toBe(false);
- });
-
- it('the correct button is rendered', () => {
- expect(wrapper.find("[data-qa-selector='update_button']").exists()).toBe(true);
- });
- });
-
- describe('when updating requires confirmation', () => {
- beforeEach(() => {
- mountComponent({
- updateAvailable: true,
- id: ELASTIC_STACK,
- version: '1.1.2',
- });
- });
-
- it('displays a modal', () => {
- expect(wrapper.find(UpdateApplicationConfirmationModal).exists()).toBe(true);
- });
-
- it('the correct button is rendered', () => {
- expect(wrapper.find("[data-qa-selector='update_button_with_confirmation']").exists()).toBe(
- true,
- );
- });
-
- it('triggers updateApplication event', () => {
- jest.spyOn(eventHub, '$emit');
- wrapper.find(UpdateApplicationConfirmationModal).vm.$emit('confirm');
-
- expect(eventHub.$emit).toHaveBeenCalledWith('updateApplication', {
- id: ELASTIC_STACK,
- params: {},
- });
- });
- });
-
- describe('updating Elastic Stack special case', () => {
- it('needs confirmation if version is lower than 3.0.0', () => {
- mountComponent({
- updateAvailable: true,
- id: ELASTIC_STACK,
- version: '1.1.2',
- });
-
- expect(wrapper.find("[data-qa-selector='update_button_with_confirmation']").exists()).toBe(
- true,
- );
- expect(wrapper.find(UpdateApplicationConfirmationModal).exists()).toBe(true);
- });
-
- it('does not need confirmation is version is 3.0.0', () => {
- mountComponent({
- updateAvailable: true,
- id: ELASTIC_STACK,
- version: '3.0.0',
- });
-
- expect(wrapper.find("[data-qa-selector='update_button']").exists()).toBe(true);
- expect(wrapper.find(UpdateApplicationConfirmationModal).exists()).toBe(false);
- });
-
- it('does not need confirmation if version is higher than 3.0.0', () => {
- mountComponent({
- updateAvailable: true,
- id: ELASTIC_STACK,
- version: '5.2.1',
- });
-
- expect(wrapper.find("[data-qa-selector='update_button']").exists()).toBe(true);
- expect(wrapper.find(UpdateApplicationConfirmationModal).exists()).toBe(false);
- });
- });
- });
-
- describe('Version', () => {
- const updateDetails = () => wrapper.find('.js-cluster-application-update-details');
- const versionEl = () => wrapper.find('.js-cluster-application-update-version');
-
- it('displays a version number if application has been updated', () => {
- const version = '0.1.45';
- mountComponent({
- status: APPLICATION_STATUS.INSTALLED,
- updateSuccessful: true,
- version,
- });
-
- expect(updateDetails().text()).toBe(`Updated to chart v${version}`);
- });
-
- it('contains a link to the chart repo if application has been updated', () => {
- const version = '0.1.45';
- const chartRepo = 'https://gitlab.com/gitlab-org/charts/gitlab-runner';
- mountComponent({
- status: APPLICATION_STATUS.INSTALLED,
- updateSuccessful: true,
- chartRepo,
- version,
- });
-
- expect(versionEl().attributes('href')).toEqual(chartRepo);
- expect(versionEl().props('target')).toEqual('_blank');
- });
-
- it('does not display a version number if application update failed', () => {
- const version = '0.1.45';
- mountComponent({
- status: APPLICATION_STATUS.INSTALLED,
- updateFailed: true,
- version,
- });
-
- expect(updateDetails().text()).toBe('Update failed');
- expect(versionEl().exists()).toBe(false);
- });
-
- it('displays updating when the application update is currently updating', () => {
- mountComponent({
- status: APPLICATION_STATUS.UPDATING,
- updateSuccessful: true,
- version: '1.2.3',
- });
-
- expect(updateDetails().text()).toBe('Updating');
- expect(versionEl().exists()).toBe(false);
- });
- });
-
- describe('Error block', () => {
- const generalErrorMessage = () => wrapper.find('.js-cluster-application-general-error-message');
-
- describe('when nothing fails', () => {
- it('does not show error block', () => {
- mountComponent();
-
- expect(generalErrorMessage().exists()).toBe(false);
- });
- });
-
- describe('when install or uninstall fails', () => {
- const statusReason = 'We broke it 0.0';
- const requestReason = 'We broke the request 0.0';
-
- beforeEach(() => {
- mountComponent({
- status: APPLICATION_STATUS.ERROR,
- statusReason,
- requestReason,
- installFailed: true,
- });
- });
-
- it('shows status reason if it is available', () => {
- const statusErrorMessage = wrapper.find('.js-cluster-application-status-error-message');
-
- expect(statusErrorMessage.text()).toEqual(statusReason);
- });
-
- it('shows request reason if it is available', () => {
- const requestErrorMessage = wrapper.find('.js-cluster-application-request-error-message');
-
- expect(requestErrorMessage.text()).toEqual(requestReason);
- });
- });
-
- describe('when install fails', () => {
- beforeEach(() => {
- mountComponent({
- status: APPLICATION_STATUS.ERROR,
- installFailed: true,
- });
- });
-
- it('shows a general message indicating the installation failed', () => {
- expect(generalErrorMessage().text()).toEqual(
- `Something went wrong while installing ${DEFAULT_APPLICATION_STATE.title}`,
- );
- });
- });
-
- describe('when uninstall fails', () => {
- beforeEach(() => {
- mountComponent({
- status: APPLICATION_STATUS.ERROR,
- uninstallFailed: true,
- });
- });
-
- it('shows a general message indicating the uninstalling failed', () => {
- expect(generalErrorMessage().text()).toEqual(
- `Something went wrong while uninstalling ${DEFAULT_APPLICATION_STATE.title}`,
- );
- });
- });
- });
-});
diff --git a/spec/frontend/clusters/components/applications_spec.js b/spec/frontend/clusters/components/applications_spec.js
deleted file mode 100644
index 511f5fc1d89..00000000000
--- a/spec/frontend/clusters/components/applications_spec.js
+++ /dev/null
@@ -1,510 +0,0 @@
-import { shallowMount, mount } from '@vue/test-utils';
-import ApplicationRow from '~/clusters/components/application_row.vue';
-import Applications from '~/clusters/components/applications.vue';
-import CrossplaneProviderStack from '~/clusters/components/crossplane_provider_stack.vue';
-import KnativeDomainEditor from '~/clusters/components/knative_domain_editor.vue';
-import { CLUSTER_TYPE, PROVIDER_TYPE } from '~/clusters/constants';
-import eventHub from '~/clusters/event_hub';
-import { APPLICATIONS_MOCK_STATE } from '../services/mock_data';
-
-describe('Applications', () => {
- let wrapper;
-
- beforeEach(() => {
- gon.features = gon.features || {};
- });
-
- const createComponent = ({ applications, type, propsData } = {}, isShallow) => {
- const mountMethod = isShallow ? shallowMount : mount;
-
- wrapper = mountMethod(Applications, {
- stubs: { ApplicationRow },
- propsData: {
- type,
- applications: { ...APPLICATIONS_MOCK_STATE, ...applications },
- ...propsData,
- },
- });
- };
-
- const createShallowComponent = (options) => createComponent(options, true);
- const findByTestId = (id) => wrapper.find(`[data-testid="${id}"]`);
- afterEach(() => {
- wrapper.destroy();
- });
-
- describe('Project cluster applications', () => {
- beforeEach(() => {
- createComponent({ type: CLUSTER_TYPE.PROJECT });
- });
-
- it('renders a row for Ingress', () => {
- expect(wrapper.find('.js-cluster-application-row-ingress').exists()).toBe(true);
- });
-
- it('renders a row for Cert-Manager', () => {
- expect(wrapper.find('.js-cluster-application-row-cert_manager').exists()).toBe(true);
- });
-
- it('renders a row for Crossplane', () => {
- expect(wrapper.find('.js-cluster-application-row-crossplane').exists()).toBe(true);
- });
-
- it('renders a row for Prometheus', () => {
- expect(wrapper.find('.js-cluster-application-row-prometheus').exists()).toBe(true);
- });
-
- it('renders a row for GitLab Runner', () => {
- expect(wrapper.find('.js-cluster-application-row-runner').exists()).toBe(true);
- });
-
- it('renders a row for Jupyter', () => {
- expect(wrapper.find('.js-cluster-application-row-jupyter').exists()).toBe(true);
- });
-
- it('renders a row for Knative', () => {
- expect(wrapper.find('.js-cluster-application-row-knative').exists()).toBe(true);
- });
-
- it('renders a row for Elastic Stack', () => {
- expect(wrapper.find('.js-cluster-application-row-elastic_stack').exists()).toBe(true);
- });
-
- it('renders a row for Cilium', () => {
- expect(wrapper.find('.js-cluster-application-row-cilium').exists()).toBe(true);
- });
- });
-
- describe('Group cluster applications', () => {
- beforeEach(() => {
- createComponent({ type: CLUSTER_TYPE.GROUP });
- });
-
- it('renders a row for Ingress', () => {
- expect(wrapper.find('.js-cluster-application-row-ingress').exists()).toBe(true);
- });
-
- it('renders a row for Cert-Manager', () => {
- expect(wrapper.find('.js-cluster-application-row-cert_manager').exists()).toBe(true);
- });
-
- it('renders a row for Crossplane', () => {
- expect(wrapper.find('.js-cluster-application-row-crossplane').exists()).toBe(true);
- });
-
- it('renders a row for Prometheus', () => {
- expect(wrapper.find('.js-cluster-application-row-prometheus').exists()).toBe(true);
- });
-
- it('renders a row for GitLab Runner', () => {
- expect(wrapper.find('.js-cluster-application-row-runner').exists()).toBe(true);
- });
-
- it('renders a row for Jupyter', () => {
- expect(wrapper.find('.js-cluster-application-row-jupyter').exists()).toBe(true);
- });
-
- it('renders a row for Knative', () => {
- expect(wrapper.find('.js-cluster-application-row-knative').exists()).toBe(true);
- });
-
- it('renders a row for Elastic Stack', () => {
- expect(wrapper.find('.js-cluster-application-row-elastic_stack').exists()).toBe(true);
- });
-
- it('renders a row for Cilium', () => {
- expect(wrapper.find('.js-cluster-application-row-cilium').exists()).toBe(true);
- });
- });
-
- describe('Instance cluster applications', () => {
- beforeEach(() => {
- createComponent({ type: CLUSTER_TYPE.INSTANCE });
- });
-
- it('renders a row for Ingress', () => {
- expect(wrapper.find('.js-cluster-application-row-ingress').exists()).toBe(true);
- });
-
- it('renders a row for Cert-Manager', () => {
- expect(wrapper.find('.js-cluster-application-row-cert_manager').exists()).toBe(true);
- });
-
- it('renders a row for Crossplane', () => {
- expect(wrapper.find('.js-cluster-application-row-crossplane').exists()).toBe(true);
- });
-
- it('renders a row for Prometheus', () => {
- expect(wrapper.find('.js-cluster-application-row-prometheus').exists()).toBe(true);
- });
-
- it('renders a row for GitLab Runner', () => {
- expect(wrapper.find('.js-cluster-application-row-runner').exists()).toBe(true);
- });
-
- it('renders a row for Jupyter', () => {
- expect(wrapper.find('.js-cluster-application-row-jupyter').exists()).toBe(true);
- });
-
- it('renders a row for Knative', () => {
- expect(wrapper.find('.js-cluster-application-row-knative').exists()).toBe(true);
- });
-
- it('renders a row for Elastic Stack', () => {
- expect(wrapper.find('.js-cluster-application-row-elastic_stack').exists()).toBe(true);
- });
-
- it('renders a row for Cilium', () => {
- expect(wrapper.find('.js-cluster-application-row-cilium').exists()).toBe(true);
- });
- });
-
- describe('Helm application', () => {
- it('does not render a row for Helm Tiller', () => {
- createComponent();
- expect(wrapper.find('.js-cluster-application-row-helm').exists()).toBe(false);
- });
- });
-
- describe('Ingress application', () => {
- it('shows the correct warning message', () => {
- createComponent();
- expect(findByTestId('ingressCostWarning').element).toMatchSnapshot();
- });
-
- describe('when installed', () => {
- describe('with ip address', () => {
- it('renders ip address with a clipboard button', () => {
- createComponent({
- applications: {
- ingress: {
- title: 'Ingress',
- status: 'installed',
- externalIp: '0.0.0.0',
- },
- },
- });
-
- expect(wrapper.find('.js-endpoint').element.value).toEqual('0.0.0.0');
- expect(wrapper.find('.js-clipboard-btn').attributes('data-clipboard-text')).toEqual(
- '0.0.0.0',
- );
- });
- });
-
- describe('with hostname', () => {
- it('renders hostname with a clipboard button', () => {
- createComponent({
- applications: {
- ingress: {
- title: 'Ingress',
- status: 'installed',
- externalHostname: 'localhost.localdomain',
- },
- cert_manager: { title: 'Cert-Manager' },
- crossplane: { title: 'Crossplane', stack: '' },
- runner: { title: 'GitLab Runner' },
- prometheus: { title: 'Prometheus' },
- jupyter: { title: 'JupyterHub', hostname: '' },
- knative: { title: 'Knative', hostname: '' },
- elastic_stack: { title: 'Elastic Stack' },
- cilium: { title: 'GitLab Container Network Policies' },
- },
- });
-
- expect(wrapper.find('.js-endpoint').element.value).toEqual('localhost.localdomain');
-
- expect(wrapper.find('.js-clipboard-btn').attributes('data-clipboard-text')).toEqual(
- 'localhost.localdomain',
- );
- });
- });
-
- describe('without ip address', () => {
- it('renders an input text with a loading icon and an alert text', () => {
- createComponent({
- applications: {
- ingress: {
- title: 'Ingress',
- status: 'installed',
- },
- },
- });
-
- expect(wrapper.find('.js-ingress-ip-loading-icon').exists()).toBe(true);
- expect(wrapper.find('.js-no-endpoint-message').exists()).toBe(true);
- });
- });
- });
-
- describe('before installing', () => {
- it('does not render the IP address', () => {
- createComponent();
-
- expect(wrapper.text()).not.toContain('Ingress IP Address');
- expect(wrapper.find('.js-endpoint').exists()).toBe(false);
- });
- });
- });
-
- describe('Cert-Manager application', () => {
- it('shows the correct description', () => {
- createComponent();
- expect(findByTestId('certManagerDescription').element).toMatchSnapshot();
- });
-
- describe('when not installed', () => {
- it('renders email & allows editing', () => {
- createComponent({
- applications: {
- cert_manager: {
- title: 'Cert-Manager',
- email: 'before@example.com',
- status: 'installable',
- },
- },
- });
-
- expect(wrapper.find('.js-email').element.value).toEqual('before@example.com');
- expect(wrapper.find('.js-email').attributes('readonly')).toBe(undefined);
- });
- });
-
- describe('when installed', () => {
- it('renders email in readonly', () => {
- createComponent({
- applications: {
- cert_manager: {
- title: 'Cert-Manager',
- email: 'after@example.com',
- status: 'installed',
- },
- },
- });
-
- expect(wrapper.find('.js-email').element.value).toEqual('after@example.com');
- expect(wrapper.find('.js-email').attributes('readonly')).toEqual('readonly');
- });
- });
- });
-
- describe('Jupyter application', () => {
- describe('with ingress installed with ip & jupyter installable', () => {
- it('renders hostname active input', () => {
- createComponent({
- applications: {
- ingress: {
- title: 'Ingress',
- status: 'installed',
- externalIp: '1.1.1.1',
- },
- },
- });
-
- expect(
- wrapper.find('.js-cluster-application-row-jupyter .js-hostname').attributes('readonly'),
- ).toEqual(undefined);
- });
- });
-
- describe('with ingress installed without external ip', () => {
- it('does not render hostname input', () => {
- createComponent({
- applications: {
- ingress: { title: 'Ingress', status: 'installed' },
- },
- });
-
- expect(wrapper.find('.js-cluster-application-row-jupyter .js-hostname').exists()).toBe(
- false,
- );
- });
- });
-
- describe('with ingress & jupyter installed', () => {
- it('renders readonly input', () => {
- createComponent({
- applications: {
- ingress: {
- title: 'Ingress',
- status: 'installed',
- externalIp: '1.1.1.1',
- },
- jupyter: { title: 'JupyterHub', status: 'installed', hostname: '' },
- },
- });
-
- expect(
- wrapper.find('.js-cluster-application-row-jupyter .js-hostname').attributes('readonly'),
- ).toEqual('readonly');
- });
- });
-
- describe('without ingress installed', () => {
- beforeEach(() => {
- createComponent();
- });
-
- it('does not render input', () => {
- expect(wrapper.find('.js-cluster-application-row-jupyter .js-hostname').exists()).toBe(
- false,
- );
- });
- });
- });
-
- describe('Prometheus application', () => {
- it('shows the correct description', () => {
- createComponent();
- expect(findByTestId('prometheusDescription').element).toMatchSnapshot();
- });
- });
-
- describe('Knative application', () => {
- const availableDomain = {
- id: 4,
- domain: 'newhostname.com',
- };
- const propsData = {
- applications: {
- knative: {
- title: 'Knative',
- hostname: 'example.com',
- status: 'installed',
- externalIp: '1.1.1.1',
- installed: true,
- availableDomains: [availableDomain],
- pagesDomain: null,
- },
- },
- };
- let knativeDomainEditor;
-
- beforeEach(() => {
- createShallowComponent(propsData);
- jest.spyOn(eventHub, '$emit');
-
- knativeDomainEditor = wrapper.find(KnativeDomainEditor);
- });
-
- it('shows the correct description', async () => {
- createComponent();
- wrapper.setProps({
- providerType: PROVIDER_TYPE.GCP,
- preInstalledKnative: true,
- });
-
- await wrapper.vm.$nextTick();
-
- expect(findByTestId('installed-via').element).toMatchSnapshot();
- });
-
- it('emits saveKnativeDomain event when knative domain editor emits save event', () => {
- propsData.applications.knative.hostname = availableDomain.domain;
- propsData.applications.knative.pagesDomain = availableDomain;
- knativeDomainEditor.vm.$emit('save');
-
- expect(eventHub.$emit).toHaveBeenCalledWith('saveKnativeDomain', {
- id: 'knative',
- params: {
- hostname: availableDomain.domain,
- pages_domain_id: availableDomain.id,
- },
- });
- });
-
- it('emits saveKnativeDomain event when knative domain editor emits save event with custom domain', () => {
- const newHostName = 'someothernewhostname.com';
- propsData.applications.knative.hostname = newHostName;
- propsData.applications.knative.pagesDomain = null;
- knativeDomainEditor.vm.$emit('save');
-
- expect(eventHub.$emit).toHaveBeenCalledWith('saveKnativeDomain', {
- id: 'knative',
- params: {
- hostname: newHostName,
- pages_domain_id: undefined,
- },
- });
- });
-
- it('emits setKnativeHostname event when knative domain editor emits change event', () => {
- wrapper.find(KnativeDomainEditor).vm.$emit('set', {
- domain: availableDomain.domain,
- domainId: availableDomain.id,
- });
-
- expect(eventHub.$emit).toHaveBeenCalledWith('setKnativeDomain', {
- id: 'knative',
- domain: availableDomain.domain,
- domainId: availableDomain.id,
- });
- });
- });
-
- describe('Crossplane application', () => {
- const propsData = {
- applications: {
- crossplane: {
- title: 'Crossplane',
- stack: {
- code: '',
- },
- },
- },
- };
-
- beforeEach(() => createShallowComponent(propsData));
-
- it('renders the correct Component', () => {
- const crossplane = wrapper.find(CrossplaneProviderStack);
- expect(crossplane.exists()).toBe(true);
- });
-
- it('shows the correct description', () => {
- createComponent();
- expect(findByTestId('crossplaneDescription').element).toMatchSnapshot();
- });
- });
-
- describe('Elastic Stack application', () => {
- describe('with elastic stack installable', () => {
- it('renders the install button enabled', () => {
- createComponent();
-
- expect(
- wrapper
- .find(
- '.js-cluster-application-row-elastic_stack .js-cluster-application-install-button',
- )
- .attributes('disabled'),
- ).toBeUndefined();
- });
- });
-
- describe('elastic stack installed', () => {
- it('renders uninstall button', () => {
- createComponent({
- applications: {
- elastic_stack: { title: 'Elastic Stack', status: 'installed' },
- },
- });
-
- expect(
- wrapper
- .find(
- '.js-cluster-application-row-elastic_stack .js-cluster-application-install-button',
- )
- .attributes('disabled'),
- ).toEqual('disabled');
- });
- });
- });
-
- describe('Cilium application', () => {
- it('shows the correct description', () => {
- createComponent({ propsData: { ciliumHelpPath: 'cilium-help-path' } });
- expect(findByTestId('ciliumDescription').element).toMatchSnapshot();
- });
- });
-});
diff --git a/spec/frontend/clusters/components/knative_domain_editor_spec.js b/spec/frontend/clusters/components/knative_domain_editor_spec.js
deleted file mode 100644
index 207eb071171..00000000000
--- a/spec/frontend/clusters/components/knative_domain_editor_spec.js
+++ /dev/null
@@ -1,179 +0,0 @@
-import { GlDropdownItem, GlButton } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
-import KnativeDomainEditor from '~/clusters/components/knative_domain_editor.vue';
-import { APPLICATION_STATUS } from '~/clusters/constants';
-
-const { UPDATING } = APPLICATION_STATUS;
-
-describe('KnativeDomainEditor', () => {
- let wrapper;
- let knative;
-
- const createComponent = (props = {}) => {
- wrapper = shallowMount(KnativeDomainEditor, {
- propsData: { ...props },
- });
- };
-
- beforeEach(() => {
- knative = {
- title: 'Knative',
- hostname: 'example.com',
- installed: true,
- };
- });
-
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
- describe('knative has an assigned IP address', () => {
- beforeEach(() => {
- knative.externalIp = '1.1.1.1';
- createComponent({ knative });
- });
-
- it('renders ip address with a clipboard button', () => {
- expect(wrapper.find('.js-knative-endpoint').exists()).toBe(true);
- expect(wrapper.find('.js-knative-endpoint').element.value).toEqual(knative.externalIp);
- });
-
- it('displays ip address clipboard button', () => {
- expect(wrapper.find('.js-knative-endpoint-clipboard-btn').attributes('text')).toEqual(
- knative.externalIp,
- );
- });
-
- it('renders domain & allows editing', () => {
- const domainNameInput = wrapper.find('.js-knative-domainname');
-
- expect(domainNameInput.element.value).toEqual(knative.hostname);
- expect(domainNameInput.attributes('readonly')).toBeFalsy();
- });
-
- it('renders an update/save Knative domain button', () => {
- expect(wrapper.find('.js-knative-save-domain-button').exists()).toBe(true);
- });
- });
-
- describe('knative without ip address', () => {
- beforeEach(() => {
- knative.externalIp = null;
- createComponent({ knative });
- });
-
- it('renders an input text with a loading icon', () => {
- expect(wrapper.find('.js-knative-ip-loading-icon').exists()).toBe(true);
- });
-
- it('renders message indicating there is not IP address assigned', () => {
- expect(wrapper.find('.js-no-knative-endpoint-message').exists()).toBe(true);
- });
- });
-
- describe('clicking save changes button', () => {
- beforeEach(() => {
- createComponent({ knative });
- });
-
- it('triggers save event and pass current knative hostname', () => {
- wrapper.find(GlButton).vm.$emit('click');
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.emitted('save').length).toEqual(1);
- });
- });
- });
-
- describe('when knative domain name was saved successfully', () => {
- beforeEach(() => {
- createComponent({ knative });
- });
-
- it('displays toast indicating a successful update', () => {
- wrapper.vm.$toast = { show: jest.fn() };
- wrapper.setProps({ knative: { updateSuccessful: true, ...knative } });
-
- return wrapper.vm.$nextTick(() => {
- expect(wrapper.vm.$toast.show).toHaveBeenCalledWith(
- 'Knative domain name was updated successfully.',
- );
- });
- });
- });
-
- describe('when knative domain name input changes', () => {
- it('emits "set" event with updated domain name', () => {
- const newDomain = {
- id: 4,
- domain: 'newhostname.com',
- };
-
- createComponent({ knative: { ...knative, availableDomains: [newDomain] } });
- jest.spyOn(wrapper.vm, 'selectDomain');
-
- wrapper.find(GlDropdownItem).vm.$emit('click');
-
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.vm.selectDomain).toHaveBeenCalledWith(newDomain);
- expect(wrapper.emitted('set')[0]).toEqual([
- {
- domain: newDomain.domain,
- domainId: newDomain.id,
- },
- ]);
- });
- });
-
- it('emits "set" event with updated custom domain name', () => {
- const newHostname = 'newhostname.com';
-
- createComponent({ knative });
- jest.spyOn(wrapper.vm, 'selectCustomDomain');
-
- wrapper.setData({ knativeHostname: newHostname });
-
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.vm.selectCustomDomain).toHaveBeenCalledWith(newHostname);
- expect(wrapper.emitted('set')[0]).toEqual([
- {
- domain: newHostname,
- domainId: null,
- },
- ]);
- });
- });
- });
-
- describe('when updating knative domain name failed', () => {
- beforeEach(() => {
- createComponent({ knative });
- });
-
- it('displays an error banner indicating the operation failure', () => {
- wrapper.setProps({ knative: { updateFailed: true, ...knative } });
-
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.find('.js-cluster-knative-domain-name-failure-message').exists()).toBe(true);
- });
- });
- });
-
- describe(`when knative status is ${UPDATING}`, () => {
- beforeEach(() => {
- createComponent({ knative: { status: UPDATING, ...knative } });
- });
-
- it('renders loading spinner in save button', () => {
- expect(wrapper.find(GlButton).props('loading')).toBe(true);
- });
-
- it('renders disabled save button', () => {
- expect(wrapper.find(GlButton).props('disabled')).toBe(true);
- });
-
- it('renders save button with "Saving" label', () => {
- expect(wrapper.find(GlButton).text()).toBe('Saving');
- });
- });
-});
diff --git a/spec/frontend/clusters/components/uninstall_application_button_spec.js b/spec/frontend/clusters/components/uninstall_application_button_spec.js
deleted file mode 100644
index 2596820e5ac..00000000000
--- a/spec/frontend/clusters/components/uninstall_application_button_spec.js
+++ /dev/null
@@ -1,39 +0,0 @@
-import { GlButton } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
-import UninstallApplicationButton from '~/clusters/components/uninstall_application_button.vue';
-import { APPLICATION_STATUS } from '~/clusters/constants';
-
-const { INSTALLED, UPDATING, UNINSTALLING } = APPLICATION_STATUS;
-
-describe('UninstallApplicationButton', () => {
- let wrapper;
-
- const createComponent = (props = {}) => {
- wrapper = shallowMount(UninstallApplicationButton, {
- propsData: { ...props },
- });
- };
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- describe.each`
- status | loading | disabled | text
- ${INSTALLED} | ${false} | ${false} | ${'Uninstall'}
- ${UPDATING} | ${false} | ${true} | ${'Uninstall'}
- ${UNINSTALLING} | ${true} | ${true} | ${'Uninstalling'}
- `('when app status is $status', ({ loading, disabled, status, text }) => {
- beforeEach(() => {
- createComponent({ status });
- });
-
- it(`renders a button with loading=${loading} and disabled=${disabled}`, () => {
- expect(wrapper.find(GlButton).props()).toMatchObject({ loading, disabled });
- });
-
- it(`renders a button with text="${text}"`, () => {
- expect(wrapper.find(GlButton).text()).toBe(text);
- });
- });
-});
diff --git a/spec/frontend/clusters/components/uninstall_application_confirmation_modal_spec.js b/spec/frontend/clusters/components/uninstall_application_confirmation_modal_spec.js
deleted file mode 100644
index 74ae4ecc486..00000000000
--- a/spec/frontend/clusters/components/uninstall_application_confirmation_modal_spec.js
+++ /dev/null
@@ -1,57 +0,0 @@
-import { GlModal } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
-import UninstallApplicationConfirmationModal from '~/clusters/components/uninstall_application_confirmation_modal.vue';
-import { INGRESS } from '~/clusters/constants';
-
-describe('UninstallApplicationConfirmationModal', () => {
- let wrapper;
- const appTitle = 'Ingress';
-
- const createComponent = (props = {}) => {
- wrapper = shallowMount(UninstallApplicationConfirmationModal, {
- propsData: { ...props },
- });
- };
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- beforeEach(() => {
- createComponent({ application: INGRESS, applicationTitle: appTitle });
- });
-
- it(`renders a modal with a title "Uninstall ${appTitle}"`, () => {
- expect(wrapper.find(GlModal).attributes('title')).toEqual(`Uninstall ${appTitle}`);
- });
-
- it(`renders a modal with an ok button labeled "Uninstall ${appTitle}"`, () => {
- expect(wrapper.find(GlModal).attributes('ok-title')).toEqual(`Uninstall ${appTitle}`);
- });
-
- describe('when ok button is clicked', () => {
- beforeEach(() => {
- jest.spyOn(wrapper.vm, 'trackUninstallButtonClick');
- wrapper.find(GlModal).vm.$emit('ok');
- });
-
- it('emits confirm event', () =>
- wrapper.vm.$nextTick().then(() => {
- expect(wrapper.emitted('confirm')).toBeTruthy();
- }));
-
- it('calls track uninstall button click mixin', () => {
- expect(wrapper.vm.trackUninstallButtonClick).toHaveBeenCalledWith(INGRESS);
- });
- });
-
- it('displays a warning text indicating the app will be uninstalled', () => {
- expect(wrapper.text()).toContain(`You are about to uninstall ${appTitle} from your cluster.`);
- });
-
- it('displays a custom warning text depending on the application', () => {
- expect(wrapper.text()).toContain(
- `The associated load balancer and IP will be deleted and cannot be restored.`,
- );
- });
-});
diff --git a/spec/frontend/clusters/components/update_application_confirmation_modal_spec.js b/spec/frontend/clusters/components/update_application_confirmation_modal_spec.js
deleted file mode 100644
index e933f17a980..00000000000
--- a/spec/frontend/clusters/components/update_application_confirmation_modal_spec.js
+++ /dev/null
@@ -1,52 +0,0 @@
-import { GlModal } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
-import UpdateApplicationConfirmationModal from '~/clusters/components/update_application_confirmation_modal.vue';
-import { ELASTIC_STACK } from '~/clusters/constants';
-
-describe('UpdateApplicationConfirmationModal', () => {
- let wrapper;
- const appTitle = 'Elastic stack';
-
- const createComponent = (props = {}) => {
- wrapper = shallowMount(UpdateApplicationConfirmationModal, {
- propsData: { ...props },
- });
- };
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- beforeEach(() => {
- createComponent({ application: ELASTIC_STACK, applicationTitle: appTitle });
- });
-
- it(`renders a modal with a title "Update ${appTitle}"`, () => {
- expect(wrapper.find(GlModal).attributes('title')).toEqual(`Update ${appTitle}`);
- });
-
- it(`renders a modal with an ok button labeled "Update ${appTitle}"`, () => {
- expect(wrapper.find(GlModal).attributes('ok-title')).toEqual(`Update ${appTitle}`);
- });
-
- describe('when ok button is clicked', () => {
- beforeEach(() => {
- wrapper.find(GlModal).vm.$emit('ok');
- });
-
- it('emits confirm event', () =>
- wrapper.vm.$nextTick().then(() => {
- expect(wrapper.emitted('confirm')).toBeTruthy();
- }));
-
- it('displays a warning text indicating the app will be updated', () => {
- expect(wrapper.text()).toContain(`You are about to update ${appTitle} on your cluster.`);
- });
-
- it('displays a custom warning text depending on the application', () => {
- expect(wrapper.text()).toContain(
- `Your Elasticsearch cluster will be re-created during this upgrade. Your logs will be re-indexed, and you will lose historical logs from hosts terminated in the last 30 days.`,
- );
- });
- });
-});
diff --git a/spec/frontend/clusters/services/application_state_machine_spec.js b/spec/frontend/clusters/services/application_state_machine_spec.js
deleted file mode 100644
index 4e731e331c2..00000000000
--- a/spec/frontend/clusters/services/application_state_machine_spec.js
+++ /dev/null
@@ -1,206 +0,0 @@
-import {
- APPLICATION_STATUS,
- UNINSTALL_EVENT,
- UPDATE_EVENT,
- INSTALL_EVENT,
-} from '~/clusters/constants';
-import transitionApplicationState from '~/clusters/services/application_state_machine';
-
-const {
- NO_STATUS,
- SCHEDULED,
- NOT_INSTALLABLE,
- INSTALLABLE,
- INSTALLING,
- INSTALLED,
- ERROR,
- UPDATING,
- UPDATED,
- UPDATE_ERRORED,
- UNINSTALLING,
- UNINSTALL_ERRORED,
- UNINSTALLED,
- PRE_INSTALLED,
- EXTERNALLY_INSTALLED,
-} = APPLICATION_STATUS;
-
-const NO_EFFECTS = 'no effects';
-
-describe('applicationStateMachine', () => {
- const noEffectsToEmptyObject = (effects) => (typeof effects === 'string' ? {} : effects);
-
- describe(`current state is ${NO_STATUS}`, () => {
- it.each`
- expectedState | event | effects
- ${INSTALLING} | ${SCHEDULED} | ${NO_EFFECTS}
- ${NOT_INSTALLABLE} | ${NOT_INSTALLABLE} | ${NO_EFFECTS}
- ${INSTALLABLE} | ${INSTALLABLE} | ${NO_EFFECTS}
- ${INSTALLING} | ${INSTALLING} | ${NO_EFFECTS}
- ${INSTALLED} | ${INSTALLED} | ${NO_EFFECTS}
- ${INSTALLABLE} | ${ERROR} | ${{ installFailed: true }}
- ${UPDATING} | ${UPDATING} | ${NO_EFFECTS}
- ${INSTALLED} | ${UPDATED} | ${NO_EFFECTS}
- ${INSTALLED} | ${UPDATE_ERRORED} | ${{ updateFailed: true }}
- ${UNINSTALLING} | ${UNINSTALLING} | ${NO_EFFECTS}
- ${INSTALLED} | ${UNINSTALL_ERRORED} | ${{ uninstallFailed: true }}
- ${UNINSTALLED} | ${UNINSTALLED} | ${NO_EFFECTS}
- ${PRE_INSTALLED} | ${PRE_INSTALLED} | ${NO_EFFECTS}
- ${EXTERNALLY_INSTALLED} | ${EXTERNALLY_INSTALLED} | ${NO_EFFECTS}
- `(`transitions to $expectedState on $event event and applies $effects`, (data) => {
- const { expectedState, event, effects } = data;
- const currentAppState = {
- status: NO_STATUS,
- };
-
- expect(transitionApplicationState(currentAppState, event)).toEqual({
- status: expectedState,
- ...noEffectsToEmptyObject(effects),
- });
- });
- });
-
- describe(`current state is ${NOT_INSTALLABLE}`, () => {
- it.each`
- expectedState | event | effects
- ${INSTALLABLE} | ${INSTALLABLE} | ${NO_EFFECTS}
- `(`transitions to $expectedState on $event event and applies $effects`, (data) => {
- const { expectedState, event, effects } = data;
- const currentAppState = {
- status: NOT_INSTALLABLE,
- };
-
- expect(transitionApplicationState(currentAppState, event)).toEqual({
- status: expectedState,
- ...noEffectsToEmptyObject(effects),
- });
- });
- });
-
- describe(`current state is ${INSTALLABLE}`, () => {
- it.each`
- expectedState | event | effects
- ${INSTALLING} | ${INSTALL_EVENT} | ${{ installFailed: false }}
- ${INSTALLED} | ${INSTALLED} | ${{ installFailed: false }}
- ${NOT_INSTALLABLE} | ${NOT_INSTALLABLE} | ${NO_EFFECTS}
- ${UNINSTALLED} | ${UNINSTALLED} | ${{ installFailed: false }}
- `(`transitions to $expectedState on $event event and applies $effects`, (data) => {
- const { expectedState, event, effects } = data;
- const currentAppState = {
- status: INSTALLABLE,
- };
-
- expect(transitionApplicationState(currentAppState, event)).toEqual({
- status: expectedState,
- ...noEffectsToEmptyObject(effects),
- });
- });
- });
-
- describe(`current state is ${INSTALLING}`, () => {
- it.each`
- expectedState | event | effects
- ${INSTALLED} | ${INSTALLED} | ${NO_EFFECTS}
- ${INSTALLABLE} | ${ERROR} | ${{ installFailed: true }}
- `(`transitions to $expectedState on $event event and applies $effects`, (data) => {
- const { expectedState, event, effects } = data;
- const currentAppState = {
- status: INSTALLING,
- };
-
- expect(transitionApplicationState(currentAppState, event)).toEqual({
- status: expectedState,
- ...noEffectsToEmptyObject(effects),
- });
- });
- });
-
- describe(`current state is ${INSTALLED}`, () => {
- it.each`
- expectedState | event | effects
- ${UPDATING} | ${UPDATE_EVENT} | ${{ updateFailed: false, updateSuccessful: false }}
- ${UNINSTALLING} | ${UNINSTALL_EVENT} | ${{ uninstallFailed: false, uninstallSuccessful: false }}
- ${NOT_INSTALLABLE} | ${NOT_INSTALLABLE} | ${NO_EFFECTS}
- ${UNINSTALLED} | ${UNINSTALLED} | ${NO_EFFECTS}
- ${INSTALLABLE} | ${ERROR} | ${{ installFailed: true }}
- `(`transitions to $expectedState on $event event and applies $effects`, (data) => {
- const { expectedState, event, effects } = data;
- const currentAppState = {
- status: INSTALLED,
- };
-
- expect(transitionApplicationState(currentAppState, event)).toEqual({
- status: expectedState,
- ...noEffectsToEmptyObject(effects),
- });
- });
- });
-
- describe(`current state is ${UPDATING}`, () => {
- it.each`
- expectedState | event | effects
- ${INSTALLED} | ${UPDATED} | ${{ updateSuccessful: true }}
- ${INSTALLED} | ${UPDATE_ERRORED} | ${{ updateFailed: true }}
- `(`transitions to $expectedState on $event event and applies $effects`, (data) => {
- const { expectedState, event, effects } = data;
- const currentAppState = {
- status: UPDATING,
- };
-
- expect(transitionApplicationState(currentAppState, event)).toEqual({
- status: expectedState,
- ...effects,
- });
- });
- });
-
- describe(`current state is ${UNINSTALLING}`, () => {
- it.each`
- expectedState | event | effects
- ${INSTALLABLE} | ${INSTALLABLE} | ${{ uninstallSuccessful: true }}
- ${INSTALLED} | ${UNINSTALL_ERRORED} | ${{ uninstallFailed: true }}
- `(`transitions to $expectedState on $event event and applies $effects`, (data) => {
- const { expectedState, event, effects } = data;
- const currentAppState = {
- status: UNINSTALLING,
- };
-
- expect(transitionApplicationState(currentAppState, event)).toEqual({
- status: expectedState,
- ...effects,
- });
- });
- });
-
- describe(`current state is ${UNINSTALLED}`, () => {
- it.each`
- expectedState | event | effects
- ${INSTALLED} | ${INSTALLED} | ${NO_EFFECTS}
- ${INSTALLABLE} | ${ERROR} | ${{ installFailed: true }}
- `(`transitions to $expectedState on $event event and applies $effects`, (data) => {
- const { expectedState, event, effects } = data;
- const currentAppState = {
- status: UNINSTALLED,
- };
-
- expect(transitionApplicationState(currentAppState, event)).toEqual({
- status: expectedState,
- ...noEffectsToEmptyObject(effects),
- });
- });
- });
- describe('current state is undefined', () => {
- it('returns the current state without having any effects', () => {
- const currentAppState = {};
- expect(transitionApplicationState(currentAppState, INSTALLABLE)).toEqual(currentAppState);
- });
- });
-
- describe('with event is undefined', () => {
- it('returns the current state without having any effects', () => {
- const currentAppState = {
- status: NO_STATUS,
- };
- expect(transitionApplicationState(currentAppState, undefined)).toEqual(currentAppState);
- });
- });
-});
diff --git a/spec/frontend/clusters/services/crossplane_provider_stack_spec.js b/spec/frontend/clusters/services/crossplane_provider_stack_spec.js
deleted file mode 100644
index f95b175ca64..00000000000
--- a/spec/frontend/clusters/services/crossplane_provider_stack_spec.js
+++ /dev/null
@@ -1,85 +0,0 @@
-import { GlDropdownItem, GlIcon } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
-import CrossplaneProviderStack from '~/clusters/components/crossplane_provider_stack.vue';
-
-describe('CrossplaneProviderStack component', () => {
- let wrapper;
-
- const defaultProps = {
- stacks: [
- {
- name: 'Google Cloud Platform',
- code: 'gcp',
- },
- {
- name: 'Amazon Web Services',
- code: 'aws',
- },
- ],
- };
-
- function createComponent(props = {}) {
- const propsData = {
- ...defaultProps,
- ...props,
- };
-
- wrapper = shallowMount(CrossplaneProviderStack, {
- propsData,
- });
- }
-
- beforeEach(() => {
- const crossplane = {
- title: 'crossplane',
- stack: '',
- };
- createComponent({ crossplane });
- });
-
- const findDropdownElements = () => wrapper.findAll(GlDropdownItem);
- const findFirstDropdownElement = () => findDropdownElements().at(0);
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- it('renders all of the available stacks in the dropdown', () => {
- const dropdownElements = findDropdownElements();
-
- expect(dropdownElements.length).toBe(defaultProps.stacks.length);
-
- defaultProps.stacks.forEach((stack, index) =>
- expect(dropdownElements.at(index).text()).toEqual(stack.name),
- );
- });
-
- it('displays the correct label for the first dropdown item if a stack is selected', () => {
- const crossplane = {
- title: 'crossplane',
- stack: 'gcp',
- };
- createComponent({ crossplane });
- expect(wrapper.vm.dropdownText).toBe('Google Cloud Platform');
- });
-
- it('emits the "set" event with the selected stack value', () => {
- const crossplane = {
- title: 'crossplane',
- stack: 'gcp',
- };
- createComponent({ crossplane });
- findFirstDropdownElement().vm.$emit('click');
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.emitted().set[0][0].code).toEqual('gcp');
- });
- });
-
- it('renders the correct dropdown text when no stack is selected', () => {
- expect(wrapper.vm.dropdownText).toBe('Select Stack');
- });
-
- it('renders an external link', () => {
- expect(wrapper.find(GlIcon).props('name')).toBe('external-link');
- });
-});
diff --git a/spec/frontend/clusters/services/mock_data.js b/spec/frontend/clusters/services/mock_data.js
index a75fcb0cb06..cf63d5452ac 100644
--- a/spec/frontend/clusters/services/mock_data.js
+++ b/spec/frontend/clusters/services/mock_data.js
@@ -1,170 +1,19 @@
-import { APPLICATION_STATUS } from '~/clusters/constants';
-
const CLUSTERS_MOCK_DATA = {
GET: {
'/gitlab-org/gitlab-shell/clusters/1/status.json': {
data: {
status: 'errored',
status_reason: 'Failed to request to CloudPlatform.',
- applications: [
- {
- name: 'helm',
- status: APPLICATION_STATUS.INSTALLABLE,
- status_reason: null,
- can_uninstall: false,
- },
- {
- name: 'ingress',
- status: APPLICATION_STATUS.ERROR,
- status_reason: 'Cannot connect',
- external_ip: null,
- external_hostname: null,
- can_uninstall: false,
- },
- {
- name: 'runner',
- status: APPLICATION_STATUS.INSTALLING,
- status_reason: null,
- can_uninstall: false,
- },
- {
- name: 'prometheus',
- status: APPLICATION_STATUS.ERROR,
- status_reason: 'Cannot connect',
- can_uninstall: false,
- },
- {
- name: 'jupyter',
- status: APPLICATION_STATUS.INSTALLING,
- status_reason: 'Cannot connect',
- can_uninstall: false,
- },
- {
- name: 'knative',
- status: APPLICATION_STATUS.INSTALLING,
- status_reason: 'Cannot connect',
- can_uninstall: false,
- },
- {
- name: 'cert_manager',
- status: APPLICATION_STATUS.ERROR,
- status_reason: 'Cannot connect',
- email: 'test@example.com',
- can_uninstall: false,
- },
- {
- name: 'crossplane',
- status: APPLICATION_STATUS.ERROR,
- status_reason: 'Cannot connect',
- can_uninstall: false,
- },
- {
- name: 'elastic_stack',
- status: APPLICATION_STATUS.ERROR,
- status_reason: 'Cannot connect',
- can_uninstall: false,
- },
- ],
},
},
'/gitlab-org/gitlab-shell/clusters/2/status.json': {
data: {
status: 'errored',
status_reason: 'Failed to request to CloudPlatform.',
- applications: [
- {
- name: 'helm',
- status: APPLICATION_STATUS.INSTALLED,
- status_reason: null,
- },
- {
- name: 'ingress',
- status: APPLICATION_STATUS.INSTALLED,
- status_reason: 'Cannot connect',
- external_ip: '1.1.1.1',
- external_hostname: null,
- },
- {
- name: 'runner',
- status: APPLICATION_STATUS.INSTALLING,
- status_reason: null,
- },
- {
- name: 'prometheus',
- status: APPLICATION_STATUS.ERROR,
- status_reason: 'Cannot connect',
- },
- {
- name: 'jupyter',
- status: APPLICATION_STATUS.INSTALLABLE,
- status_reason: 'Cannot connect',
- },
- {
- name: 'knative',
- status: APPLICATION_STATUS.INSTALLABLE,
- status_reason: 'Cannot connect',
- },
- {
- name: 'cert_manager',
- status: APPLICATION_STATUS.ERROR,
- status_reason: 'Cannot connect',
- email: 'test@example.com',
- },
- {
- name: 'crossplane',
- status: APPLICATION_STATUS.ERROR,
- status_reason: 'Cannot connect',
- stack: 'gcp',
- },
- {
- name: 'elastic_stack',
- status: APPLICATION_STATUS.ERROR,
- status_reason: 'Cannot connect',
- },
- ],
},
},
},
- POST: {
- '/gitlab-org/gitlab-shell/clusters/1/applications/helm': {},
- '/gitlab-org/gitlab-shell/clusters/1/applications/ingress': {},
- '/gitlab-org/gitlab-shell/clusters/1/applications/crossplane': {},
- '/gitlab-org/gitlab-shell/clusters/1/applications/cert_manager': {},
- '/gitlab-org/gitlab-shell/clusters/1/applications/runner': {},
- '/gitlab-org/gitlab-shell/clusters/1/applications/prometheus': {},
- '/gitlab-org/gitlab-shell/clusters/1/applications/jupyter': {},
- '/gitlab-org/gitlab-shell/clusters/1/applications/knative': {},
- '/gitlab-org/gitlab-shell/clusters/1/applications/elastic_stack': {},
- },
-};
-
-const DEFAULT_APPLICATION_STATE = {
- id: 'some-app',
- title: 'My App',
- titleLink: 'https://about.gitlab.com/',
- description: 'Some description about this interesting application!',
- status: null,
- statusReason: null,
- requestReason: null,
-};
-
-const APPLICATIONS_MOCK_STATE = {
- helm: { title: 'Helm Tiller', status: 'installable' },
- ingress: {
- title: 'Ingress',
- status: 'installable',
- },
- crossplane: { title: 'Crossplane', status: 'installable', stack: '' },
- cert_manager: { title: 'Cert-Manager', status: 'installable' },
- runner: { title: 'GitLab Runner' },
- prometheus: { title: 'Prometheus' },
- jupyter: { title: 'JupyterHub', status: 'installable', hostname: '' },
- knative: { title: 'Knative ', status: 'installable', hostname: '' },
- elastic_stack: { title: 'Elastic Stack', status: 'installable' },
- cilium: {
- title: 'GitLab Container Network Policies',
- status: 'not_installable',
- },
+ POST: {},
};
-export { CLUSTERS_MOCK_DATA, DEFAULT_APPLICATION_STATE, APPLICATIONS_MOCK_STATE };
+export { CLUSTERS_MOCK_DATA };
diff --git a/spec/frontend/clusters/stores/clusters_store_spec.js b/spec/frontend/clusters/stores/clusters_store_spec.js
index cdba6fc6ab8..5e797bbf8a8 100644
--- a/spec/frontend/clusters/stores/clusters_store_spec.js
+++ b/spec/frontend/clusters/stores/clusters_store_spec.js
@@ -1,4 +1,3 @@
-import { APPLICATION_INSTALLED_STATUSES, APPLICATION_STATUS, RUNNER } from '~/clusters/constants';
import ClustersStore from '~/clusters/stores/clusters_store';
import { CLUSTERS_MOCK_DATA } from '../services/mock_data';
@@ -31,17 +30,6 @@ describe('Clusters Store', () => {
});
});
- describe('updateAppProperty', () => {
- it('should store new request reason', () => {
- expect(store.state.applications.helm.requestReason).toEqual(null);
-
- const newReason = 'We broke it.';
- store.updateAppProperty('helm', 'requestReason', newReason);
-
- expect(store.state.applications.helm.requestReason).toEqual(newReason);
- });
- });
-
describe('updateStateFromServer', () => {
it('should store new polling data from server', () => {
const mockResponseData =
@@ -50,196 +38,16 @@ describe('Clusters Store', () => {
expect(store.state).toEqual({
helpPath: null,
- helmHelpPath: null,
- ingressHelpPath: null,
environmentsHelpPath: null,
clustersHelpPath: null,
deployBoardsHelpPath: null,
- cloudRunHelpPath: null,
status: mockResponseData.status,
statusReason: mockResponseData.status_reason,
providerType: null,
- preInstalledKnative: false,
rbac: false,
- applications: {
- helm: {
- title: 'Legacy Helm Tiller server',
- status: mockResponseData.applications[0].status,
- statusReason: mockResponseData.applications[0].status_reason,
- requestReason: null,
- installable: true,
- installed: false,
- installFailed: false,
- uninstallable: false,
- uninstallSuccessful: false,
- uninstallFailed: false,
- validationError: null,
- },
- ingress: {
- title: 'Ingress',
- status: APPLICATION_STATUS.INSTALLABLE,
- statusReason: mockResponseData.applications[1].status_reason,
- requestReason: null,
- externalIp: null,
- externalHostname: null,
- installable: true,
- installed: false,
- installFailed: true,
- uninstallable: false,
- updateFailed: false,
- uninstallSuccessful: false,
- uninstallFailed: false,
- validationError: null,
- },
- runner: {
- title: 'GitLab Runner',
- status: mockResponseData.applications[2].status,
- statusReason: mockResponseData.applications[2].status_reason,
- requestReason: null,
- version: mockResponseData.applications[2].version,
- updateAvailable: mockResponseData.applications[2].update_available,
- chartRepo: 'https://gitlab.com/gitlab-org/charts/gitlab-runner',
- installable: true,
- installed: false,
- installFailed: false,
- updateFailed: false,
- updateSuccessful: false,
- uninstallable: false,
- uninstallSuccessful: false,
- uninstallFailed: false,
- validationError: null,
- },
- prometheus: {
- title: 'Prometheus',
- status: APPLICATION_STATUS.INSTALLABLE,
- statusReason: mockResponseData.applications[3].status_reason,
- requestReason: null,
- installable: true,
- installed: false,
- installFailed: true,
- uninstallable: false,
- uninstallSuccessful: false,
- uninstallFailed: false,
- validationError: null,
- },
- jupyter: {
- title: 'JupyterHub',
- status: mockResponseData.applications[4].status,
- statusReason: mockResponseData.applications[4].status_reason,
- requestReason: null,
- hostname: '',
- installable: true,
- installed: false,
- installFailed: false,
- uninstallable: false,
- uninstallSuccessful: false,
- uninstallFailed: false,
- validationError: null,
- },
- knative: {
- title: 'Knative',
- status: mockResponseData.applications[5].status,
- statusReason: mockResponseData.applications[5].status_reason,
- requestReason: null,
- hostname: null,
- isEditingDomain: false,
- externalIp: null,
- externalHostname: null,
- installable: true,
- installed: false,
- installFailed: false,
- uninstallable: false,
- uninstallSuccessful: false,
- uninstallFailed: false,
- updateSuccessful: false,
- updateFailed: false,
- validationError: null,
- },
- cert_manager: {
- title: 'Cert-Manager',
- status: APPLICATION_STATUS.INSTALLABLE,
- installFailed: true,
- statusReason: mockResponseData.applications[6].status_reason,
- requestReason: null,
- email: mockResponseData.applications[6].email,
- installable: true,
- installed: false,
- uninstallable: false,
- uninstallSuccessful: false,
- uninstallFailed: false,
- validationError: null,
- },
- elastic_stack: {
- title: 'Elastic Stack',
- status: APPLICATION_STATUS.INSTALLABLE,
- installFailed: true,
- statusReason: mockResponseData.applications[7].status_reason,
- requestReason: null,
- installable: true,
- installed: false,
- uninstallable: false,
- uninstallSuccessful: false,
- uninstallFailed: false,
- validationError: null,
- },
- crossplane: {
- title: 'Crossplane',
- status: APPLICATION_STATUS.INSTALLABLE,
- installFailed: true,
- statusReason: mockResponseData.applications[8].status_reason,
- requestReason: null,
- installable: true,
- installed: false,
- uninstallable: false,
- uninstallSuccessful: false,
- uninstallFailed: false,
- validationError: null,
- },
- cilium: {
- title: 'GitLab Container Network Policies',
- status: null,
- statusReason: null,
- requestReason: null,
- installable: false,
- installed: false,
- installFailed: false,
- uninstallable: false,
- uninstallSuccessful: false,
- uninstallFailed: false,
- validationError: null,
- },
- },
environments: [],
fetchingEnvironments: false,
});
});
-
- describe.each(APPLICATION_INSTALLED_STATUSES)(
- 'given the current app status is %s',
- (status) => {
- it('marks application as installed', () => {
- const mockResponseData =
- CLUSTERS_MOCK_DATA.GET['/gitlab-org/gitlab-shell/clusters/2/status.json'].data;
- const runnerAppIndex = 2;
-
- mockResponseData.applications[runnerAppIndex].status = status;
-
- store.updateStateFromServer(mockResponseData);
-
- expect(store.state.applications[RUNNER].installed).toBe(true);
- });
- },
- );
-
- it('sets default hostname for jupyter when ingress has a ip address', () => {
- const mockResponseData =
- CLUSTERS_MOCK_DATA.GET['/gitlab-org/gitlab-shell/clusters/2/status.json'].data;
-
- store.updateStateFromServer(mockResponseData);
-
- expect(store.state.applications.jupyter.hostname).toEqual(
- `jupyter.${store.state.applications.ingress.externalIp}.nip.io`,
- );
- });
});
});
diff --git a/spec/frontend/clusters_list/store/actions_spec.js b/spec/frontend/clusters_list/store/actions_spec.js
index b2ef3c2138a..f4b69053e14 100644
--- a/spec/frontend/clusters_list/store/actions_spec.js
+++ b/spec/frontend/clusters_list/store/actions_spec.js
@@ -5,7 +5,7 @@ import waitForPromises from 'helpers/wait_for_promises';
import { MAX_REQUESTS } from '~/clusters_list/constants';
import * as actions from '~/clusters_list/store/actions';
import * as types from '~/clusters_list/store/mutation_types';
-import { deprecatedCreateFlash as flashError } from '~/flash';
+import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import Poll from '~/lib/utils/poll';
import { apiData } from '../mock_data';
@@ -101,7 +101,9 @@ describe('Clusters store actions', () => {
},
],
() => {
- expect(flashError).toHaveBeenCalledWith(expect.stringMatching('error'));
+ expect(createFlash).toHaveBeenCalledWith({
+ message: expect.stringMatching('error'),
+ });
done();
},
);
diff --git a/spec/frontend/code_navigation/components/__snapshots__/popover_spec.js.snap b/spec/frontend/code_navigation/components/__snapshots__/popover_spec.js.snap
index b59d1597a12..118d8ceceb9 100644
--- a/spec/frontend/code_navigation/components/__snapshots__/popover_spec.js.snap
+++ b/spec/frontend/code_navigation/components/__snapshots__/popover_spec.js.snap
@@ -13,7 +13,9 @@ exports[`Code navigation popover component renders popover 1`] = `
<gl-tabs-stub
contentclass="gl-py-0"
navclass="gl-hidden"
+ queryparamname="tab"
theme="indigo"
+ value="0"
>
<gl-tab-stub
title="Definition"
diff --git a/spec/frontend/code_quality_walkthrough/components/step_spec.js b/spec/frontend/code_quality_walkthrough/components/step_spec.js
index c397faf1f35..bdbcda5f902 100644
--- a/spec/frontend/code_quality_walkthrough/components/step_spec.js
+++ b/spec/frontend/code_quality_walkthrough/components/step_spec.js
@@ -4,11 +4,11 @@ import Cookies from 'js-cookie';
import Step from '~/code_quality_walkthrough/components/step.vue';
import { EXPERIMENT_NAME, STEPS } from '~/code_quality_walkthrough/constants';
import { TRACKING_CONTEXT_SCHEMA } from '~/experimentation/constants';
-import { getParameterByName } from '~/lib/utils/common_utils';
+import { getParameterByName } from '~/lib/utils/url_utility';
import Tracking from '~/tracking';
-jest.mock('~/lib/utils/common_utils', () => ({
- ...jest.requireActual('~/lib/utils/common_utils'),
+jest.mock('~/lib/utils/url_utility', () => ({
+ ...jest.requireActual('~/lib/utils/url_utility'),
getParameterByName: jest.fn(),
}));
diff --git a/spec/frontend/collapsed_sidebar_todo_spec.js b/spec/frontend/collapsed_sidebar_todo_spec.js
deleted file mode 100644
index 7c659822672..00000000000
--- a/spec/frontend/collapsed_sidebar_todo_spec.js
+++ /dev/null
@@ -1,171 +0,0 @@
-/* eslint-disable no-new */
-import MockAdapter from 'axios-mock-adapter';
-import { clone } from 'lodash';
-import waitForPromises from 'helpers/wait_for_promises';
-import { TEST_HOST } from 'spec/test_constants';
-import axios from '~/lib/utils/axios_utils';
-import Sidebar from '~/right_sidebar';
-import { fixTitle } from '~/tooltips';
-
-jest.mock('~/tooltips');
-
-describe('Issuable right sidebar collapsed todo toggle', () => {
- const fixtureName = 'issues/open-issue.html';
- const jsonFixtureName = 'todos/todos.json';
- let mock;
-
- beforeEach(() => {
- const todoData = getJSONFixture(jsonFixtureName);
- new Sidebar();
- loadFixtures(fixtureName);
-
- document.querySelector('.js-right-sidebar').classList.toggle('right-sidebar-expanded');
- document.querySelector('.js-right-sidebar').classList.toggle('right-sidebar-collapsed');
-
- mock = new MockAdapter(axios);
-
- mock.onPost(`${TEST_HOST}/frontend-fixtures/issues-project/todos`).reply(() => {
- const response = clone(todoData);
-
- return [200, response];
- });
-
- mock.onDelete(/(.*)\/dashboard\/todos\/\d+$/).reply(() => {
- const response = clone(todoData);
- delete response.delete_path;
-
- return [200, response];
- });
- });
-
- afterEach(() => {
- mock.restore();
- });
-
- it('shows add todo button', () => {
- expect(document.querySelector('.js-issuable-todo.sidebar-collapsed-icon')).not.toBeNull();
-
- expect(
- document
- .querySelector('.js-issuable-todo.sidebar-collapsed-icon svg')
- .getAttribute('data-testid'),
- ).toBe('todo-add-icon');
-
- expect(
- document.querySelector('.js-issuable-todo.sidebar-collapsed-icon .todo-undone'),
- ).toBeNull();
- });
-
- it('sets default tooltip title', () => {
- expect(
- document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').getAttribute('title'),
- ).toBe('Add a to do');
- });
-
- it('toggle todo state', (done) => {
- document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').click();
-
- setImmediate(() => {
- expect(
- document.querySelector('.js-issuable-todo.sidebar-collapsed-icon .todo-undone'),
- ).not.toBeNull();
-
- expect(
- document
- .querySelector('.js-issuable-todo.sidebar-collapsed-icon svg.todo-undone')
- .getAttribute('data-testid'),
- ).toBe('todo-done-icon');
-
- done();
- });
- });
-
- it('toggle todo state of expanded todo toggle', (done) => {
- document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').click();
-
- setImmediate(() => {
- expect(
- document.querySelector('.issuable-sidebar-header .js-issuable-todo').textContent.trim(),
- ).toBe('Mark as done');
-
- done();
- });
- });
-
- it('toggles todo button tooltip', (done) => {
- document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').click();
-
- setImmediate(() => {
- const el = document.querySelector('.js-issuable-todo.sidebar-collapsed-icon');
-
- expect(el.getAttribute('title')).toBe('Mark as done');
- expect(fixTitle).toHaveBeenCalledWith(el);
-
- done();
- });
- });
-
- it('marks todo as done', (done) => {
- document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').click();
-
- waitForPromises()
- .then(() => {
- expect(
- document.querySelector('.js-issuable-todo.sidebar-collapsed-icon .todo-undone'),
- ).not.toBeNull();
-
- document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').click();
- })
- .then(waitForPromises)
- .then(() => {
- expect(
- document.querySelector('.js-issuable-todo.sidebar-collapsed-icon .todo-undone'),
- ).toBeNull();
-
- expect(
- document.querySelector('.issuable-sidebar-header .js-issuable-todo').textContent.trim(),
- ).toBe('Add a to do');
- })
- .then(done)
- .catch(done.fail);
- });
-
- it('updates aria-label to Mark as done', (done) => {
- document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').click();
-
- setImmediate(() => {
- expect(
- document
- .querySelector('.js-issuable-todo.sidebar-collapsed-icon')
- .getAttribute('aria-label'),
- ).toBe('Mark as done');
-
- done();
- });
- });
-
- it('updates aria-label to add todo', (done) => {
- document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').click();
-
- waitForPromises()
- .then(() => {
- expect(
- document
- .querySelector('.js-issuable-todo.sidebar-collapsed-icon')
- .getAttribute('aria-label'),
- ).toBe('Mark as done');
-
- document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').click();
- })
- .then(waitForPromises)
- .then(() => {
- expect(
- document
- .querySelector('.js-issuable-todo.sidebar-collapsed-icon')
- .getAttribute('aria-label'),
- ).toBe('Add a to do');
- })
- .then(done)
- .catch(done.fail);
- });
-});
diff --git a/spec/frontend/commit/commit_pipeline_status_component_spec.js b/spec/frontend/commit/commit_pipeline_status_component_spec.js
index a56f761269a..8082b8524e7 100644
--- a/spec/frontend/commit/commit_pipeline_status_component_spec.js
+++ b/spec/frontend/commit/commit_pipeline_status_component_spec.js
@@ -2,7 +2,7 @@ import { GlLoadingIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import Visibility from 'visibilityjs';
import { getJSONFixture } from 'helpers/fixtures';
-import { deprecatedCreateFlash as flash } from '~/flash';
+import createFlash from '~/flash';
import Poll from '~/lib/utils/poll';
import CommitPipelineStatus from '~/projects/tree/components/commit_pipeline_status_component.vue';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
@@ -170,7 +170,7 @@ describe('Commit pipeline status component', () => {
});
it('displays flash error message', () => {
- expect(flash).toHaveBeenCalled();
+ expect(createFlash).toHaveBeenCalled();
});
});
});
diff --git a/spec/frontend/commit/pipelines/pipelines_table_spec.js b/spec/frontend/commit/pipelines/pipelines_table_spec.js
index 4bf6727af3b..1defb3d586c 100644
--- a/spec/frontend/commit/pipelines/pipelines_table_spec.js
+++ b/spec/frontend/commit/pipelines/pipelines_table_spec.js
@@ -66,7 +66,7 @@ describe('Pipelines table in Commits and Merge requests', () => {
describe('with pipelines', () => {
beforeEach(async () => {
- mock.onGet('endpoint.json').reply(200, [pipeline]);
+ mock.onGet('endpoint.json').reply(200, [pipeline], { 'x-total': 10 });
createComponent();
@@ -110,7 +110,7 @@ describe('Pipelines table in Commits and Merge requests', () => {
document.body.appendChild(element);
element.addEventListener('update-pipelines-count', (event) => {
- expect(event.detail.pipelines).toEqual([pipeline]);
+ expect(event.detail.pipelineCount).toEqual(10);
done();
});
diff --git a/spec/frontend/content_editor/components/content_editor_spec.js b/spec/frontend/content_editor/components/content_editor_spec.js
index 59c4190ad3a..563e80e04c1 100644
--- a/spec/frontend/content_editor/components/content_editor_spec.js
+++ b/spec/frontend/content_editor/components/content_editor_spec.js
@@ -1,5 +1,7 @@
+import { GlAlert } from '@gitlab/ui';
import { EditorContent } from '@tiptap/vue-2';
-import { shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import ContentEditor from '~/content_editor/components/content_editor.vue';
import TopToolbar from '~/content_editor/components/top_toolbar.vue';
import { createContentEditor } from '~/content_editor/services/create_content_editor';
@@ -8,8 +10,11 @@ describe('ContentEditor', () => {
let wrapper;
let editor;
+ const findEditorElement = () => wrapper.findByTestId('content-editor');
+ const findErrorAlert = () => wrapper.findComponent(GlAlert);
+
const createWrapper = async (contentEditor) => {
- wrapper = shallowMount(ContentEditor, {
+ wrapper = shallowMountExtended(ContentEditor, {
propsData: {
contentEditor,
},
@@ -49,7 +54,7 @@ describe('ContentEditor', () => {
editor.tiptapEditor.isFocused = isFocused;
createWrapper(editor);
- expect(wrapper.classes()).toStrictEqual(classes);
+ expect(findEditorElement().classes()).toStrictEqual(classes);
},
);
@@ -57,6 +62,30 @@ describe('ContentEditor', () => {
editor.tiptapEditor.isFocused = true;
createWrapper(editor);
- expect(wrapper.classes()).toContain('is-focused');
+ expect(findEditorElement().classes()).toContain('is-focused');
+ });
+
+ describe('displaying error', () => {
+ const error = 'Content Editor error';
+
+ beforeEach(async () => {
+ createWrapper(editor);
+
+ editor.tiptapEditor.emit('error', error);
+
+ await nextTick();
+ });
+
+ it('displays error notifications from the tiptap editor', () => {
+ expect(findErrorAlert().text()).toBe(error);
+ });
+
+ it('allows dismissing an error alert', async () => {
+ findErrorAlert().vm.$emit('dismiss');
+
+ await nextTick();
+
+ expect(findErrorAlert().exists()).toBe(false);
+ });
});
});
diff --git a/spec/frontend/content_editor/components/toolbar_button_spec.js b/spec/frontend/content_editor/components/toolbar_button_spec.js
index a49efa34017..d848adcbff8 100644
--- a/spec/frontend/content_editor/components/toolbar_button_spec.js
+++ b/spec/frontend/content_editor/components/toolbar_button_spec.js
@@ -1,33 +1,17 @@
import { GlButton } from '@gitlab/ui';
-import { Extension } from '@tiptap/core';
import { shallowMount } from '@vue/test-utils';
import ToolbarButton from '~/content_editor/components/toolbar_button.vue';
-import { createContentEditor } from '~/content_editor/services/create_content_editor';
+import { createTestEditor, mockChainedCommands } from '../test_utils';
describe('content_editor/components/toolbar_button', () => {
let wrapper;
let tiptapEditor;
- let toggleFooSpy;
const CONTENT_TYPE = 'bold';
const ICON_NAME = 'bold';
const LABEL = 'Bold';
const buildEditor = () => {
- toggleFooSpy = jest.fn();
- tiptapEditor = createContentEditor({
- extensions: [
- {
- tiptapExtension: Extension.create({
- addCommands() {
- return {
- toggleFoo: () => toggleFooSpy,
- };
- },
- }),
- },
- ],
- renderMarkdown: () => true,
- }).tiptapEditor;
+ tiptapEditor = createTestEditor();
jest.spyOn(tiptapEditor, 'isActive');
};
@@ -78,20 +62,28 @@ describe('content_editor/components/toolbar_button', () => {
describe('when button is clicked', () => {
it('executes the content type command when executeCommand = true', async () => {
- buildWrapper({ editorCommand: 'toggleFoo' });
+ const editorCommand = 'toggleFoo';
+ const mockCommands = mockChainedCommands(tiptapEditor, [editorCommand, 'focus', 'run']);
+
+ buildWrapper({ editorCommand });
await findButton().trigger('click');
- expect(toggleFooSpy).toHaveBeenCalled();
+ expect(mockCommands[editorCommand]).toHaveBeenCalled();
+ expect(mockCommands.focus).toHaveBeenCalled();
+ expect(mockCommands.run).toHaveBeenCalled();
expect(wrapper.emitted().execute).toHaveLength(1);
});
it('does not executes the content type command when executeCommand = false', async () => {
+ const editorCommand = 'toggleFoo';
+ const mockCommands = mockChainedCommands(tiptapEditor, [editorCommand, 'run']);
+
buildWrapper();
await findButton().trigger('click');
- expect(toggleFooSpy).not.toHaveBeenCalled();
+ expect(mockCommands[editorCommand]).not.toHaveBeenCalled();
expect(wrapper.emitted().execute).toHaveLength(1);
});
});
diff --git a/spec/frontend/content_editor/components/toolbar_image_button_spec.js b/spec/frontend/content_editor/components/toolbar_image_button_spec.js
new file mode 100644
index 00000000000..701dcf83476
--- /dev/null
+++ b/spec/frontend/content_editor/components/toolbar_image_button_spec.js
@@ -0,0 +1,78 @@
+import { GlButton, GlFormInputGroup } from '@gitlab/ui';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import ToolbarImageButton from '~/content_editor/components/toolbar_image_button.vue';
+import { configure as configureImageExtension } from '~/content_editor/extensions/image';
+import { createTestEditor, mockChainedCommands } from '../test_utils';
+
+describe('content_editor/components/toolbar_image_button', () => {
+ let wrapper;
+ let editor;
+
+ const buildWrapper = () => {
+ wrapper = mountExtended(ToolbarImageButton, {
+ propsData: {
+ tiptapEditor: editor,
+ },
+ });
+ };
+
+ const findImageURLInput = () =>
+ wrapper.findComponent(GlFormInputGroup).find('input[type="text"]');
+ const findApplyImageButton = () => wrapper.findComponent(GlButton);
+
+ const selectFile = async (file) => {
+ const input = wrapper.find({ 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(() => {
+ const { tiptapExtension: Image } = configureImageExtension({
+ renderMarkdown: jest.fn(),
+ uploadsPath: '/uploads/',
+ });
+
+ editor = createTestEditor({
+ extensions: [Image],
+ });
+
+ buildWrapper();
+ });
+
+ afterEach(() => {
+ editor.destroy();
+ wrapper.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', 'uploadImage', 'run']);
+ const file = new File(['foo'], 'foo.png', { type: 'image/png' });
+
+ await selectFile(file);
+
+ expect(commands.focus).toHaveBeenCalled();
+ expect(commands.uploadImage).toHaveBeenCalledWith({ file });
+ expect(commands.run).toHaveBeenCalled();
+
+ expect(wrapper.emitted().execute[0]).toEqual([{ contentType: 'image', value: 'upload' }]);
+ });
+});
diff --git a/spec/frontend/content_editor/components/toolbar_link_button_spec.js b/spec/frontend/content_editor/components/toolbar_link_button_spec.js
index 812e769c891..576a2912f72 100644
--- a/spec/frontend/content_editor/components/toolbar_link_button_spec.js
+++ b/spec/frontend/content_editor/components/toolbar_link_button_spec.js
@@ -1,4 +1,4 @@
-import { GlDropdown, GlDropdownDivider, GlFormInputGroup, GlButton } from '@gitlab/ui';
+import { GlDropdown, GlDropdownDivider, GlButton, GlFormInputGroup } from '@gitlab/ui';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import ToolbarLinkButton from '~/content_editor/components/toolbar_link_button.vue';
import { tiptapExtension as Link } from '~/content_editor/extensions/link';
@@ -16,9 +16,6 @@ describe('content_editor/components/toolbar_link_button', () => {
propsData: {
tiptapEditor: editor,
},
- stubs: {
- GlFormInputGroup,
- },
});
};
const findDropdown = () => wrapper.findComponent(GlDropdown);
@@ -45,9 +42,8 @@ describe('content_editor/components/toolbar_link_button', () => {
});
describe('when there is an active link', () => {
- beforeEach(() => {
- jest.spyOn(editor, 'isActive');
- editor.isActive.mockReturnValueOnce(true);
+ beforeEach(async () => {
+ jest.spyOn(editor, 'isActive').mockReturnValueOnce(true);
buildWrapper();
});
@@ -78,8 +74,36 @@ describe('content_editor/components/toolbar_link_button', () => {
expect(commands.focus).toHaveBeenCalled();
expect(commands.unsetLink).toHaveBeenCalled();
- expect(commands.setLink).toHaveBeenCalledWith({ href: 'https://example' });
+ 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 editor.emit('selectionUpdate', { 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 editor.emit('selectionUpdate', { editor });
+
+ expect(findLinkURLInput().element.value).toEqual('https://gitlab.com');
+ });
});
});
@@ -106,8 +130,13 @@ describe('content_editor/components/toolbar_link_button', () => {
await findApplyLinkButton().trigger('click');
expect(commands.focus).toHaveBeenCalled();
- expect(commands.setLink).toHaveBeenCalledWith({ href: 'https://example' });
+ expect(commands.setLink).toHaveBeenCalledWith({
+ href: 'https://example',
+ canonicalSrc: 'https://example',
+ });
expect(commands.run).toHaveBeenCalled();
+
+ expect(wrapper.emitted().execute[0]).toEqual([{ contentType: 'link' }]);
});
});
diff --git a/spec/frontend/content_editor/components/toolbar_table_button_spec.js b/spec/frontend/content_editor/components/toolbar_table_button_spec.js
new file mode 100644
index 00000000000..237b2848246
--- /dev/null
+++ b/spec/frontend/content_editor/components/toolbar_table_button_spec.js
@@ -0,0 +1,109 @@
+import { GlDropdown, GlButton } from '@gitlab/ui';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import ToolbarTableButton from '~/content_editor/components/toolbar_table_button.vue';
+import { tiptapExtension as Table } from '~/content_editor/extensions/table';
+import { tiptapExtension as TableCell } from '~/content_editor/extensions/table_cell';
+import { tiptapExtension as TableHeader } from '~/content_editor/extensions/table_header';
+import { tiptapExtension as TableRow } from '~/content_editor/extensions/table_row';
+import { createTestEditor, mockChainedCommands } from '../test_utils';
+
+describe('content_editor/components/toolbar_table_button', () => {
+ let wrapper;
+ let editor;
+
+ const buildWrapper = () => {
+ wrapper = mountExtended(ToolbarTableButton, {
+ propsData: {
+ tiptapEditor: editor,
+ },
+ });
+ };
+
+ const findDropdown = () => wrapper.findComponent(GlDropdown);
+ const getNumButtons = () => findDropdown().findAllComponents(GlButton).length;
+
+ beforeEach(() => {
+ editor = createTestEditor({
+ extensions: [Table, TableCell, TableRow, TableHeader],
+ });
+
+ buildWrapper();
+ });
+
+ afterEach(() => {
+ editor.destroy();
+ wrapper.destroy();
+ });
+
+ it('renders a grid of 3x3 buttons to create a table', () => {
+ expect(getNumButtons()).toBe(9); // 3 x 3
+ });
+
+ describe.each`
+ row | col | numButtons | tableSize
+ ${1} | ${2} | ${9} | ${'1x2'}
+ ${2} | ${2} | ${9} | ${'2x2'}
+ ${2} | ${3} | ${12} | ${'2x3'}
+ ${3} | ${2} | ${12} | ${'3x2'}
+ ${3} | ${3} | ${16} | ${'3x3'}
+ `('button($row, $col) in the table creator grid', ({ row, col, numButtons, tableSize }) => {
+ describe('on mouse over', () => {
+ beforeEach(async () => {
+ const button = wrapper.findByTestId(`table-${row}-${col}`);
+ await button.trigger('mouseover');
+ });
+
+ it('marks all rows and cols before it as active', () => {
+ const prevRow = Math.max(1, row - 1);
+ const prevCol = Math.max(1, col - 1);
+ expect(wrapper.findByTestId(`table-${prevRow}-${prevCol}`).element).toHaveClass(
+ 'gl-bg-blue-50!',
+ );
+ });
+
+ it('shows a help text indicating the size of the table being inserted', () => {
+ expect(findDropdown().element).toHaveText(`Insert a ${tableSize} table.`);
+ });
+
+ it('adds another row and col of buttons to create a bigger table', () => {
+ expect(getNumButtons()).toBe(numButtons);
+ });
+ });
+
+ describe('on click', () => {
+ let commands;
+
+ beforeEach(async () => {
+ commands = mockChainedCommands(editor, ['focus', 'insertTable', 'run']);
+
+ const button = wrapper.findByTestId(`table-${row}-${col}`);
+ await button.trigger('mouseover');
+ await button.trigger('click');
+ });
+
+ it('inserts a table with $tableSize rows and cols', () => {
+ expect(commands.focus).toHaveBeenCalled();
+ expect(commands.insertTable).toHaveBeenCalledWith({
+ rows: row,
+ cols: col,
+ withHeaderRow: true,
+ });
+ expect(commands.run).toHaveBeenCalled();
+
+ expect(wrapper.emitted().execute).toHaveLength(1);
+ });
+ });
+ });
+
+ it('does not create more buttons than a 8x8 grid', async () => {
+ for (let i = 3; i < 8; i += 1) {
+ expect(getNumButtons()).toBe(i * i);
+
+ // eslint-disable-next-line no-await-in-loop
+ await wrapper.findByTestId(`table-${i}-${i}`).trigger('mouseover');
+ expect(findDropdown().element).toHaveText(`Insert a ${i}x${i} table.`);
+ }
+
+ expect(getNumButtons()).toBe(64); // 8x8 (and not 9x9)
+ });
+});
diff --git a/spec/frontend/content_editor/components/toolbar_text_style_dropdown_spec.js b/spec/frontend/content_editor/components/toolbar_text_style_dropdown_spec.js
index 8c54f6bb8bb..9a46e27404f 100644
--- a/spec/frontend/content_editor/components/toolbar_text_style_dropdown_spec.js
+++ b/spec/frontend/content_editor/components/toolbar_text_style_dropdown_spec.js
@@ -2,21 +2,16 @@ import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import ToolbarTextStyleDropdown from '~/content_editor/components/toolbar_text_style_dropdown.vue';
import { TEXT_STYLE_DROPDOWN_ITEMS } from '~/content_editor/constants';
-import { createTestContentEditorExtension, createTestEditor } from '../test_utils';
+import { tiptapExtension as Heading } from '~/content_editor/extensions/heading';
+import { createTestEditor, mockChainedCommands } from '../test_utils';
describe('content_editor/components/toolbar_headings_dropdown', () => {
let wrapper;
let tiptapEditor;
- let commandMocks;
const buildEditor = () => {
- const testExtension = createTestContentEditorExtension({
- commands: TEXT_STYLE_DROPDOWN_ITEMS.map((item) => item.editorCommand),
- });
-
- commandMocks = testExtension.commandMocks;
tiptapEditor = createTestEditor({
- extensions: [testExtension.tiptapExtension],
+ extensions: [Heading],
});
jest.spyOn(tiptapEditor, 'isActive');
@@ -104,9 +99,12 @@ describe('content_editor/components/toolbar_headings_dropdown', () => {
TEXT_STYLE_DROPDOWN_ITEMS.forEach((textStyle, index) => {
const { editorCommand, commandParams } = textStyle;
+ const commands = mockChainedCommands(tiptapEditor, [editorCommand, 'focus', 'run']);
wrapper.findAllComponents(GlDropdownItem).at(index).vm.$emit('click');
- expect(commandMocks[editorCommand]).toHaveBeenCalledWith(commandParams || {});
+ expect(commands[editorCommand]).toHaveBeenCalledWith(commandParams || {});
+ expect(commands.focus).toHaveBeenCalled();
+ expect(commands.run).toHaveBeenCalled();
});
});
diff --git a/spec/frontend/content_editor/components/top_toolbar_spec.js b/spec/frontend/content_editor/components/top_toolbar_spec.js
index 0d55fa730ae..5411793cd5e 100644
--- a/spec/frontend/content_editor/components/top_toolbar_spec.js
+++ b/spec/frontend/content_editor/components/top_toolbar_spec.js
@@ -39,17 +39,19 @@ describe('content_editor/components/top_toolbar', () => {
});
describe.each`
- testId | controlProps
- ${'bold'} | ${{ contentType: 'bold', iconName: 'bold', label: 'Bold text', editorCommand: 'toggleBold' }}
- ${'italic'} | ${{ contentType: 'italic', iconName: 'italic', label: 'Italic text', editorCommand: 'toggleItalic' }}
- ${'strike'} | ${{ contentType: 'strike', iconName: 'strikethrough', label: 'Strikethrough', editorCommand: 'toggleStrike' }}
- ${'code'} | ${{ contentType: 'code', iconName: 'code', label: 'Code', editorCommand: 'toggleCode' }}
- ${'blockquote'} | ${{ contentType: 'blockquote', iconName: 'quote', label: 'Insert a quote', editorCommand: 'toggleBlockquote' }}
- ${'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' }}
- ${'code-block'} | ${{ contentType: 'codeBlock', iconName: 'doc-code', label: 'Insert a code block', editorCommand: 'toggleCodeBlock' }}
- ${'text-styles'} | ${{}}
- ${'link'} | ${{}}
+ testId | controlProps
+ ${'bold'} | ${{ contentType: 'bold', iconName: 'bold', label: 'Bold text', editorCommand: 'toggleBold' }}
+ ${'italic'} | ${{ contentType: 'italic', iconName: 'italic', label: 'Italic text', editorCommand: 'toggleItalic' }}
+ ${'strike'} | ${{ contentType: 'strike', iconName: 'strikethrough', label: 'Strikethrough', editorCommand: 'toggleStrike' }}
+ ${'code'} | ${{ contentType: 'code', iconName: 'code', label: 'Code', editorCommand: 'toggleCode' }}
+ ${'blockquote'} | ${{ contentType: 'blockquote', iconName: 'quote', label: 'Insert a quote', editorCommand: 'toggleBlockquote' }}
+ ${'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' }}
+ ${'horizontal-rule'} | ${{ contentType: 'horizontalRule', iconName: 'dash', label: 'Add a horizontal rule', editorCommand: 'setHorizontalRule' }}
+ ${'code-block'} | ${{ contentType: 'codeBlock', iconName: 'doc-code', label: 'Insert a code block', editorCommand: 'toggleCodeBlock' }}
+ ${'text-styles'} | ${{}}
+ ${'link'} | ${{}}
+ ${'image'} | ${{}}
`('given a $testId toolbar control', ({ testId, controlProps }) => {
beforeEach(() => {
buildWrapper();
diff --git a/spec/frontend/content_editor/components/wrappers/image_spec.js b/spec/frontend/content_editor/components/wrappers/image_spec.js
new file mode 100644
index 00000000000..7b057f9cabc
--- /dev/null
+++ b/spec/frontend/content_editor/components/wrappers/image_spec.js
@@ -0,0 +1,66 @@
+import { GlLoadingIcon } from '@gitlab/ui';
+import { NodeViewWrapper } from '@tiptap/vue-2';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import ImageWrapper from '~/content_editor/components/wrappers/image.vue';
+
+describe('content/components/wrappers/image', () => {
+ let wrapper;
+
+ const createWrapper = async (nodeAttrs = {}) => {
+ wrapper = shallowMountExtended(ImageWrapper, {
+ propsData: {
+ node: {
+ attrs: nodeAttrs,
+ },
+ },
+ });
+ };
+ const findImage = () => wrapper.findByTestId('image');
+ const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('renders a node-view-wrapper with display-inline-block class', () => {
+ createWrapper();
+
+ expect(wrapper.findComponent(NodeViewWrapper).classes()).toContain('gl-display-inline-block');
+ });
+
+ it('renders an image that displays the node src', () => {
+ const src = 'foobar.png';
+
+ createWrapper({ src });
+
+ expect(findImage().attributes().src).toBe(src);
+ });
+
+ describe('when uploading', () => {
+ beforeEach(() => {
+ createWrapper({ uploading: true });
+ });
+
+ it('renders a gl-loading-icon component', () => {
+ expect(findLoadingIcon().exists()).toBe(true);
+ });
+
+ it('adds gl-opacity-5 class selector to image', () => {
+ expect(findImage().classes()).toContain('gl-opacity-5');
+ });
+ });
+
+ describe('when not uploading', () => {
+ beforeEach(() => {
+ createWrapper({ uploading: false });
+ });
+
+ it('does not render a gl-loading-icon component', () => {
+ expect(findLoadingIcon().exists()).toBe(false);
+ });
+
+ it('does not add gl-opacity-5 class selector to image', () => {
+ expect(findImage().classes()).not.toContain('gl-opacity-5');
+ });
+ });
+});
diff --git a/spec/frontend/content_editor/extensions/hard_break_spec.js b/spec/frontend/content_editor/extensions/hard_break_spec.js
new file mode 100644
index 00000000000..ebd58e60b0c
--- /dev/null
+++ b/spec/frontend/content_editor/extensions/hard_break_spec.js
@@ -0,0 +1,46 @@
+import { tiptapExtension as HardBreak } from '~/content_editor/extensions/hard_break';
+import { createTestEditor, createDocBuilder } from '../test_utils';
+
+describe('content_editor/extensions/hard_break', () => {
+ let tiptapEditor;
+ let eq;
+ let doc;
+ let p;
+ let hardBreak;
+
+ beforeEach(() => {
+ tiptapEditor = createTestEditor({ extensions: [HardBreak] });
+
+ ({
+ builders: { doc, p, hardBreak },
+ eq,
+ } = createDocBuilder({
+ tiptapEditor,
+ names: { hardBreak: { nodeType: HardBreak.name } },
+ }));
+ });
+
+ describe('Shift-Enter shortcut', () => {
+ it('inserts a hard break when shortcut is executed', () => {
+ const initialDoc = doc(p(''));
+ const expectedDoc = doc(p(hardBreak()));
+
+ tiptapEditor.commands.setContent(initialDoc.toJSON());
+ tiptapEditor.commands.keyboardShortcut('Shift-Enter');
+
+ expect(eq(tiptapEditor.state.doc, expectedDoc)).toBe(true);
+ });
+ });
+
+ describe('Mod-Enter shortcut', () => {
+ it('does not insert a hard break when shortcut is executed', () => {
+ const initialDoc = doc(p(''));
+ const expectedDoc = initialDoc;
+
+ tiptapEditor.commands.setContent(initialDoc.toJSON());
+ tiptapEditor.commands.keyboardShortcut('Mod-Enter');
+
+ expect(eq(tiptapEditor.state.doc, expectedDoc)).toBe(true);
+ });
+ });
+});
diff --git a/spec/frontend/content_editor/extensions/horizontal_rule_spec.js b/spec/frontend/content_editor/extensions/horizontal_rule_spec.js
new file mode 100644
index 00000000000..a1bc7f0e8ed
--- /dev/null
+++ b/spec/frontend/content_editor/extensions/horizontal_rule_spec.js
@@ -0,0 +1,20 @@
+import { hrInputRuleRegExp } from '~/content_editor/extensions/horizontal_rule';
+
+describe('content_editor/extensions/horizontal_rule', () => {
+ describe.each`
+ input | matches
+ ${'---'} | ${true}
+ ${'--'} | ${false}
+ ${'---x'} | ${false}
+ ${' ---x'} | ${false}
+ ${' --- '} | ${false}
+ ${'x---x'} | ${false}
+ ${'x---'} | ${false}
+ `('hrInputRuleRegExp', ({ input, matches }) => {
+ it(`${matches ? 'matches' : 'does not match'}: "${input}"`, () => {
+ const match = new RegExp(hrInputRuleRegExp).test(input);
+
+ expect(match).toBe(matches);
+ });
+ });
+});
diff --git a/spec/frontend/content_editor/extensions/image_spec.js b/spec/frontend/content_editor/extensions/image_spec.js
new file mode 100644
index 00000000000..922966b813a
--- /dev/null
+++ b/spec/frontend/content_editor/extensions/image_spec.js
@@ -0,0 +1,193 @@
+import axios from 'axios';
+import MockAdapter from 'axios-mock-adapter';
+import { once } from 'lodash';
+import waitForPromises from 'helpers/wait_for_promises';
+import * as Image from '~/content_editor/extensions/image';
+import httpStatus from '~/lib/utils/http_status';
+import { loadMarkdownApiResult } from '../markdown_processing_examples';
+import { createTestEditor, createDocBuilder } from '../test_utils';
+
+describe('content_editor/extensions/image', () => {
+ let tiptapEditor;
+ let eq;
+ let doc;
+ let p;
+ let image;
+ let renderMarkdown;
+ let mock;
+ const uploadsPath = '/uploads/';
+ const validFile = new File(['foo'], 'foo.png', { type: 'image/png' });
+ const invalidFile = new File(['foo'], 'bar.html', { type: 'text/html' });
+
+ beforeEach(() => {
+ renderMarkdown = jest
+ .fn()
+ .mockResolvedValue(loadMarkdownApiResult('project_wiki_attachment_image').body);
+
+ const { tiptapExtension } = Image.configure({ renderMarkdown, uploadsPath });
+
+ tiptapEditor = createTestEditor({ extensions: [tiptapExtension] });
+
+ ({
+ builders: { doc, p, image },
+ eq,
+ } = createDocBuilder({
+ tiptapEditor,
+ names: { image: { nodeType: tiptapExtension.name } },
+ }));
+
+ mock = new MockAdapter(axios);
+ });
+
+ afterEach(() => {
+ mock.reset();
+ });
+
+ it.each`
+ file | valid | description
+ ${validFile} | ${true} | ${'handles paste event when mime type is valid'}
+ ${invalidFile} | ${false} | ${'does not handle paste event when mime type is invalid'}
+ `('$description', ({ file, valid }) => {
+ const pasteEvent = Object.assign(new Event('paste'), {
+ clipboardData: {
+ files: [file],
+ },
+ });
+ let handled;
+
+ tiptapEditor.view.someProp('handlePaste', (eventHandler) => {
+ handled = eventHandler(tiptapEditor.view, pasteEvent);
+ });
+
+ expect(handled).toBe(valid);
+ });
+
+ it.each`
+ file | valid | description
+ ${validFile} | ${true} | ${'handles drop event when mime type is valid'}
+ ${invalidFile} | ${false} | ${'does not handle drop event when mime type is invalid'}
+ `('$description', ({ file, valid }) => {
+ const dropEvent = Object.assign(new Event('drop'), {
+ dataTransfer: {
+ files: [file],
+ },
+ });
+ let handled;
+
+ tiptapEditor.view.someProp('handleDrop', (eventHandler) => {
+ handled = eventHandler(tiptapEditor.view, dropEvent);
+ });
+
+ expect(handled).toBe(valid);
+ });
+
+ it('handles paste event when mime type is correct', () => {
+ const pasteEvent = Object.assign(new Event('paste'), {
+ clipboardData: {
+ files: [new File(['foo'], 'foo.png', { type: 'image/png' })],
+ },
+ });
+ const handled = tiptapEditor.view.someProp('handlePaste', (eventHandler) => {
+ return eventHandler(tiptapEditor.view, pasteEvent);
+ });
+
+ expect(handled).toBe(true);
+ });
+
+ describe('uploadImage command', () => {
+ describe('when file has correct mime type', () => {
+ let initialDoc;
+ const base64EncodedFile = 'data:image/png;base64,Zm9v';
+
+ beforeEach(() => {
+ initialDoc = doc(p(''));
+ tiptapEditor.commands.setContent(initialDoc.toJSON());
+ });
+
+ describe('when uploading image succeeds', () => {
+ const successResponse = {
+ link: {
+ markdown: '[image](/uploads/25265/image.png)',
+ },
+ };
+
+ beforeEach(() => {
+ mock.onPost().reply(httpStatus.OK, successResponse);
+ });
+
+ it('inserts an image with src set to the encoded image file and uploading true', (done) => {
+ const expectedDoc = doc(p(image({ uploading: true, src: base64EncodedFile })));
+
+ tiptapEditor.on(
+ 'update',
+ once(() => {
+ expect(eq(tiptapEditor.state.doc, expectedDoc)).toBe(true);
+ done();
+ }),
+ );
+
+ tiptapEditor.commands.uploadImage({ file: validFile });
+ });
+
+ it('updates the inserted image with canonicalSrc when upload is successful', async () => {
+ const expectedDoc = doc(
+ p(
+ image({
+ canonicalSrc: 'test-file.png',
+ src: base64EncodedFile,
+ alt: 'test file',
+ uploading: false,
+ }),
+ ),
+ );
+
+ tiptapEditor.commands.uploadImage({ file: validFile });
+
+ await waitForPromises();
+
+ expect(eq(tiptapEditor.state.doc, expectedDoc)).toBe(true);
+ });
+ });
+
+ describe('when uploading image request fails', () => {
+ beforeEach(() => {
+ mock.onPost().reply(httpStatus.INTERNAL_SERVER_ERROR);
+ });
+
+ it('resets the doc to orginal state', async () => {
+ const expectedDoc = doc(p(''));
+
+ tiptapEditor.commands.uploadImage({ file: validFile });
+
+ await waitForPromises();
+
+ expect(eq(tiptapEditor.state.doc, expectedDoc)).toBe(true);
+ });
+
+ it('emits an error event that includes an error message', (done) => {
+ tiptapEditor.commands.uploadImage({ file: validFile });
+
+ tiptapEditor.on('error', (message) => {
+ expect(message).toBe('An error occurred while uploading the image. Please try again.');
+ done();
+ });
+ });
+ });
+ });
+
+ describe('when file does not have correct mime type', () => {
+ let initialDoc;
+
+ beforeEach(() => {
+ initialDoc = doc(p(''));
+ tiptapEditor.commands.setContent(initialDoc.toJSON());
+ });
+
+ it('does not start the upload image process', () => {
+ tiptapEditor.commands.uploadImage({ file: invalidFile });
+
+ expect(eq(tiptapEditor.state.doc, initialDoc)).toBe(true);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/content_editor/markdown_processing_examples.js b/spec/frontend/content_editor/markdown_processing_examples.js
index 12bf2cbb747..12eed00f3c6 100644
--- a/spec/frontend/content_editor/markdown_processing_examples.js
+++ b/spec/frontend/content_editor/markdown_processing_examples.js
@@ -1,7 +1,6 @@
import fs from 'fs';
import path from 'path';
import jsYaml from 'js-yaml';
-import { toArray } from 'lodash';
import { getJSONFixture } from 'helpers/fixtures';
export const loadMarkdownApiResult = (testName) => {
@@ -15,5 +14,5 @@ export const loadMarkdownApiExamples = () => {
const apiMarkdownYamlText = fs.readFileSync(apiMarkdownYamlPath);
const apiMarkdownExampleObjects = jsYaml.safeLoad(apiMarkdownYamlText);
- return apiMarkdownExampleObjects.map((example) => toArray(example));
+ return apiMarkdownExampleObjects.map(({ name, context, markdown }) => [name, context, markdown]);
};
diff --git a/spec/frontend/content_editor/markdown_processing_spec.js b/spec/frontend/content_editor/markdown_processing_spec.js
index cb34476d680..028cd6a8271 100644
--- a/spec/frontend/content_editor/markdown_processing_spec.js
+++ b/spec/frontend/content_editor/markdown_processing_spec.js
@@ -3,11 +3,15 @@ import { loadMarkdownApiExamples, loadMarkdownApiResult } from './markdown_proce
describe('markdown processing', () => {
// Ensure we generate same markdown that was provided to Markdown API.
- it.each(loadMarkdownApiExamples())('correctly handles %s', async (testName, markdown) => {
- const { html } = loadMarkdownApiResult(testName);
- const contentEditor = createContentEditor({ renderMarkdown: () => html });
- await contentEditor.setSerializedContent(markdown);
+ it.each(loadMarkdownApiExamples())(
+ 'correctly handles %s (context: %s)',
+ async (name, context, markdown) => {
+ const testName = context ? `${context}_${name}` : name;
+ const { html, body } = loadMarkdownApiResult(testName);
+ const contentEditor = createContentEditor({ renderMarkdown: () => html || body });
+ await contentEditor.setSerializedContent(markdown);
- expect(contentEditor.getSerializedContent()).toBe(markdown);
- });
+ expect(contentEditor.getSerializedContent()).toBe(markdown);
+ },
+ );
});
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 59b2fab6d54..b614efd954a 100644
--- a/spec/frontend/content_editor/services/create_content_editor_spec.js
+++ b/spec/frontend/content_editor/services/create_content_editor_spec.js
@@ -5,10 +5,11 @@ import { createTestContentEditorExtension } from '../test_utils';
describe('content_editor/services/create_editor', () => {
let renderMarkdown;
let editor;
+ const uploadsPath = '/uploads';
beforeEach(() => {
renderMarkdown = jest.fn();
- editor = createContentEditor({ renderMarkdown });
+ editor = createContentEditor({ renderMarkdown, uploadsPath });
});
it('sets gl-outline-0! class selector to the tiptapEditor instance', () => {
@@ -48,4 +49,13 @@ describe('content_editor/services/create_editor', () => {
it('throws an error when a renderMarkdown fn is not provided', () => {
expect(() => createContentEditor()).toThrow(PROVIDE_SERIALIZER_OR_RENDERER_ERROR);
});
+
+ it('provides uploadsPath and renderMarkdown function to Image extension', () => {
+ expect(
+ editor.tiptapEditor.extensionManager.extensions.find((e) => e.name === 'image').options,
+ ).toMatchObject({
+ uploadsPath,
+ renderMarkdown,
+ });
+ });
});
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 cf74b5c56c9..64f3d8df6e0 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
@@ -1,26 +1,23 @@
-import { BulletList } from '@tiptap/extension-bullet-list';
-import { CodeBlockLowlight } from '@tiptap/extension-code-block-lowlight';
-import { Document } from '@tiptap/extension-document';
-import { Heading } from '@tiptap/extension-heading';
-import { ListItem } from '@tiptap/extension-list-item';
-import { Paragraph } from '@tiptap/extension-paragraph';
-import { Text } from '@tiptap/extension-text';
-import { Editor } from '@tiptap/vue-2';
import { mockTracking } from 'helpers/tracking_helper';
import {
KEYBOARD_SHORTCUT_TRACKING_ACTION,
INPUT_RULE_TRACKING_ACTION,
CONTENT_EDITOR_TRACKING_LABEL,
} from '~/content_editor/constants';
+import { tiptapExtension as BulletList } from '~/content_editor/extensions/bullet_list';
+import { tiptapExtension as CodeBlockLowlight } from '~/content_editor/extensions/code_block_highlight';
+import { tiptapExtension as Heading } from '~/content_editor/extensions/heading';
+import { tiptapExtension as ListItem } from '~/content_editor/extensions/list_item';
import trackInputRulesAndShortcuts from '~/content_editor/services/track_input_rules_and_shortcuts';
import { ENTER_KEY, BACKSPACE_KEY } from '~/lib/utils/keys';
+import { createTestEditor } from '../test_utils';
describe('content_editor/services/track_input_rules_and_shortcuts', () => {
let trackingSpy;
let editor;
let trackedExtensions;
const HEADING_TEXT = 'Heading text';
- const extensions = [Document, Paragraph, Text, Heading, CodeBlockLowlight, BulletList, ListItem];
+ const extensions = [Heading, CodeBlockLowlight, BulletList, ListItem];
beforeEach(() => {
trackingSpy = mockTracking(undefined, null, jest.spyOn);
@@ -29,7 +26,7 @@ describe('content_editor/services/track_input_rules_and_shortcuts', () => {
describe('given the heading extension is instrumented', () => {
beforeEach(() => {
trackedExtensions = extensions.map(trackInputRulesAndShortcuts);
- editor = new Editor({
+ editor = createTestEditor({
extensions: extensions.map(trackInputRulesAndShortcuts),
});
});
diff --git a/spec/frontend/content_editor/services/upload_file_spec.js b/spec/frontend/content_editor/services/upload_file_spec.js
new file mode 100644
index 00000000000..87c5298079e
--- /dev/null
+++ b/spec/frontend/content_editor/services/upload_file_spec.js
@@ -0,0 +1,46 @@
+import axios from 'axios';
+import MockAdapter from 'axios-mock-adapter';
+import { uploadFile } from '~/content_editor/services/upload_file';
+import httpStatus from '~/lib/utils/http_status';
+
+describe('content_editor/services/upload_file', () => {
+ const uploadsPath = '/uploads';
+ const file = new File(['content'], 'file.txt');
+ // TODO: Replace with automated fixture
+ const renderedAttachmentLinkFixture =
+ '<a href="/group1/project1/-/wikis/test-file.png" data-canonical-src="test-file.png"><img data-src="/group1/project1/-/wikis/test-file.png" data-canonical-src="test-file.png"></a></p>';
+ const successResponse = {
+ link: {
+ markdown: '[GitLab](https://gitlab.com)',
+ },
+ };
+ const parseHTML = (html) => new DOMParser().parseFromString(html, 'text/html');
+ let mock;
+ let renderMarkdown;
+ let renderedMarkdown;
+
+ beforeEach(() => {
+ const formData = new FormData();
+ formData.append('file', file);
+
+ renderedMarkdown = parseHTML(renderedAttachmentLinkFixture);
+
+ mock = new MockAdapter(axios);
+ mock.onPost(uploadsPath, formData).reply(httpStatus.OK, successResponse);
+ renderMarkdown = jest.fn().mockResolvedValue(renderedAttachmentLinkFixture);
+ });
+
+ afterEach(() => {
+ mock.restore();
+ });
+
+ it('returns src and canonicalSrc of uploaded file', async () => {
+ const response = await uploadFile({ uploadsPath, renderMarkdown, file });
+
+ expect(renderMarkdown).toHaveBeenCalledWith(successResponse.link.markdown);
+ expect(response).toEqual({
+ src: renderedMarkdown.querySelector('a').getAttribute('href'),
+ canonicalSrc: renderedMarkdown.querySelector('a').dataset.canonicalSrc,
+ });
+ });
+});
diff --git a/spec/frontend/content_editor/test_utils.js b/spec/frontend/content_editor/test_utils.js
index 8e73aef678b..090e1d92218 100644
--- a/spec/frontend/content_editor/test_utils.js
+++ b/spec/frontend/content_editor/test_utils.js
@@ -3,6 +3,16 @@ import { Document } from '@tiptap/extension-document';
import { Paragraph } from '@tiptap/extension-paragraph';
import { Text } from '@tiptap/extension-text';
import { Editor } from '@tiptap/vue-2';
+import { builders, eq } from 'prosemirror-test-builder';
+
+export const createDocBuilder = ({ tiptapEditor, names = {} }) => {
+ const docBuilders = builders(tiptapEditor.schema, {
+ p: { nodeType: 'paragraph' },
+ ...names,
+ });
+
+ return { eq, builders: docBuilders };
+};
/**
* Creates an instance of the Tiptap Editor class
@@ -15,7 +25,7 @@ import { Editor } from '@tiptap/vue-2';
* include in the editor
* @returns An instance of a Tiptap’s Editor class
*/
-export const createTestEditor = ({ extensions = [] }) => {
+export const createTestEditor = ({ extensions = [] } = {}) => {
return new Editor({
extensions: [Document, Text, Paragraph, ...extensions],
});
diff --git a/spec/frontend/contributors/store/actions_spec.js b/spec/frontend/contributors/store/actions_spec.js
index 82b6492b779..a4054ab1fc8 100644
--- a/spec/frontend/contributors/store/actions_spec.js
+++ b/spec/frontend/contributors/store/actions_spec.js
@@ -2,7 +2,7 @@ import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
import * as actions from '~/contributors/stores/actions';
import * as types from '~/contributors/stores/mutation_types';
-import { deprecatedCreateFlash as flashError } from '~/flash';
+import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
jest.mock('~/flash.js');
@@ -47,7 +47,9 @@ describe('Contributors store actions', () => {
[{ type: types.SET_LOADING_STATE, payload: true }],
[],
() => {
- expect(flashError).toHaveBeenCalledWith(expect.stringMatching('error'));
+ expect(createFlash).toHaveBeenCalledWith({
+ message: expect.stringMatching('error'),
+ });
mock.restore();
done();
},
diff --git a/spec/frontend/cycle_analytics/filter_bar_spec.js b/spec/frontend/cycle_analytics/filter_bar_spec.js
new file mode 100644
index 00000000000..407f21bd956
--- /dev/null
+++ b/spec/frontend/cycle_analytics/filter_bar_spec.js
@@ -0,0 +1,224 @@
+import { createLocalVue, shallowMount } from '@vue/test-utils';
+import axios from 'axios';
+import MockAdapter from 'axios-mock-adapter';
+import Vuex from 'vuex';
+import {
+ filterMilestones,
+ filterLabels,
+} from 'jest/vue_shared/components/filtered_search_bar/store/modules/filters/mock_data';
+import FilterBar from '~/cycle_analytics/components/filter_bar.vue';
+import storeConfig from '~/cycle_analytics/store';
+import * as commonUtils from '~/lib/utils/common_utils';
+import * as urlUtils from '~/lib/utils/url_utility';
+import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
+import * as utils from '~/vue_shared/components/filtered_search_bar/filtered_search_utils';
+import initialFiltersState from '~/vue_shared/components/filtered_search_bar/store/modules/filters/state';
+import UrlSync from '~/vue_shared/components/url_sync.vue';
+
+const localVue = createLocalVue();
+localVue.use(Vuex);
+
+const milestoneTokenType = 'milestone';
+const labelsTokenType = 'labels';
+const authorTokenType = 'author';
+const assigneesTokenType = 'assignees';
+
+const initialFilterBarState = {
+ selectedMilestone: null,
+ selectedAuthor: null,
+ selectedAssigneeList: null,
+ selectedLabelList: null,
+};
+
+const defaultParams = {
+ milestone_title: null,
+ 'not[milestone_title]': null,
+ author_username: null,
+ 'not[author_username]': null,
+ assignee_username: null,
+ 'not[assignee_username]': null,
+ label_name: null,
+ 'not[label_name]': null,
+};
+
+async function shouldMergeUrlParams(wrapper, result) {
+ await wrapper.vm.$nextTick();
+ expect(urlUtils.mergeUrlParams).toHaveBeenCalledWith(result, window.location.href, {
+ spreadArrays: true,
+ });
+ expect(commonUtils.historyPushState).toHaveBeenCalled();
+}
+
+describe('Filter bar', () => {
+ let wrapper;
+ let store;
+ let mock;
+
+ let setFiltersMock;
+
+ const createStore = (initialState = {}) => {
+ setFiltersMock = jest.fn();
+
+ return new Vuex.Store({
+ modules: {
+ filters: {
+ namespaced: true,
+ state: {
+ ...initialFiltersState(),
+ ...initialState,
+ },
+ actions: {
+ setFilters: setFiltersMock,
+ },
+ },
+ },
+ });
+ };
+
+ const createComponent = (initialStore) => {
+ return shallowMount(FilterBar, {
+ localVue,
+ store: initialStore,
+ propsData: {
+ groupPath: 'foo',
+ },
+ stubs: {
+ UrlSync,
+ },
+ });
+ };
+
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ mock.restore();
+ });
+
+ const selectedMilestone = [filterMilestones[0]];
+ const selectedLabelList = [filterLabels[0]];
+
+ const findFilteredSearch = () => wrapper.findComponent(FilteredSearchBar);
+ const getSearchToken = (type) =>
+ findFilteredSearch()
+ .props('tokens')
+ .find((token) => token.type === type);
+
+ describe('default', () => {
+ beforeEach(() => {
+ store = createStore();
+ wrapper = createComponent(store);
+ });
+
+ it('renders FilteredSearchBar component', () => {
+ expect(findFilteredSearch().exists()).toBe(true);
+ });
+ });
+
+ describe('when the state has data', () => {
+ beforeEach(() => {
+ store = createStore({
+ milestones: { data: selectedMilestone },
+ labels: { data: selectedLabelList },
+ authors: { data: [] },
+ assignees: { data: [] },
+ });
+ wrapper = createComponent(store);
+ });
+
+ it('displays the milestone and label token', () => {
+ const tokens = findFilteredSearch().props('tokens');
+
+ expect(tokens).toHaveLength(4);
+ expect(tokens[0].type).toBe(milestoneTokenType);
+ expect(tokens[1].type).toBe(labelsTokenType);
+ expect(tokens[2].type).toBe(authorTokenType);
+ expect(tokens[3].type).toBe(assigneesTokenType);
+ });
+
+ it('provides the initial milestone token', () => {
+ const { initialMilestones: milestoneToken } = getSearchToken(milestoneTokenType);
+
+ expect(milestoneToken).toHaveLength(selectedMilestone.length);
+ });
+
+ it('provides the initial label token', () => {
+ const { initialLabels: labelToken } = getSearchToken(labelsTokenType);
+
+ expect(labelToken).toHaveLength(selectedLabelList.length);
+ });
+ });
+
+ describe('when the user interacts', () => {
+ beforeEach(() => {
+ store = createStore({
+ milestones: { data: filterMilestones },
+ labels: { data: filterLabels },
+ });
+ wrapper = createComponent(store);
+ jest.spyOn(utils, 'processFilters');
+ });
+
+ it('clicks on the search button, setFilters is dispatched', () => {
+ const filters = [
+ { type: 'milestone', value: { data: selectedMilestone[0].title, operator: '=' } },
+ { type: 'labels', value: { data: selectedLabelList[0].title, operator: '=' } },
+ ];
+
+ findFilteredSearch().vm.$emit('onFilter', filters);
+
+ expect(utils.processFilters).toHaveBeenCalledWith(filters);
+
+ expect(setFiltersMock).toHaveBeenCalledWith(expect.anything(), {
+ selectedLabelList: [{ value: selectedLabelList[0].title, operator: '=' }],
+ selectedMilestone: { value: selectedMilestone[0].title, operator: '=' },
+ selectedAssigneeList: [],
+ selectedAuthor: null,
+ });
+ });
+ });
+
+ describe.each([
+ ['selectedMilestone', 'milestone_title', { value: '12.0', operator: '=' }, '12.0'],
+ ['selectedAuthor', 'author_username', { value: 'rootUser', operator: '=' }, 'rootUser'],
+ [
+ 'selectedLabelList',
+ 'label_name',
+ [
+ { value: 'Afternix', operator: '=' },
+ { value: 'Brouceforge', operator: '=' },
+ ],
+ ['Afternix', 'Brouceforge'],
+ ],
+ [
+ 'selectedAssigneeList',
+ 'assignee_username',
+ [
+ { value: 'rootUser', operator: '=' },
+ { value: 'secondaryUser', operator: '=' },
+ ],
+ ['rootUser', 'secondaryUser'],
+ ],
+ ])('with a %s updates the %s url parameter', (stateKey, paramKey, payload, result) => {
+ beforeEach(() => {
+ commonUtils.historyPushState = jest.fn();
+ urlUtils.mergeUrlParams = jest.fn();
+
+ mock = new MockAdapter(axios);
+ wrapper = createComponent(storeConfig);
+
+ wrapper.vm.$store.dispatch('filters/setFilters', {
+ ...initialFilterBarState,
+ [stateKey]: payload,
+ });
+ });
+ it(`sets the ${paramKey} url parameter`, () => {
+ return shouldMergeUrlParams(wrapper, {
+ ...defaultParams,
+ [paramKey]: result,
+ });
+ });
+ });
+});
diff --git a/spec/frontend/cycle_analytics/formatted_stage_count_spec.js b/spec/frontend/cycle_analytics/formatted_stage_count_spec.js
new file mode 100644
index 00000000000..1228b8511ea
--- /dev/null
+++ b/spec/frontend/cycle_analytics/formatted_stage_count_spec.js
@@ -0,0 +1,34 @@
+import { shallowMount } from '@vue/test-utils';
+import Component from '~/cycle_analytics/components/formatted_stage_count.vue';
+
+describe('Formatted Stage Count', () => {
+ let wrapper = null;
+
+ const createComponent = (stageCount = null) => {
+ wrapper = shallowMount(Component, {
+ propsData: {
+ stageCount,
+ },
+ });
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it.each`
+ stageCount | expectedOutput
+ ${null} | ${'-'}
+ ${1} | ${'1 item'}
+ ${10} | ${'10 items'}
+ ${1000} | ${'1,000 items'}
+ ${1001} | ${'1,000+ items'}
+ `('returns "$expectedOutput" for stageCount=$stageCount', ({ stageCount, expectedOutput }) => {
+ createComponent(stageCount);
+ expect(wrapper.text()).toContain(expectedOutput);
+ });
+});
diff --git a/spec/frontend/cycle_analytics/mock_data.js b/spec/frontend/cycle_analytics/mock_data.js
index 242ea1932fb..4e6471d5f7b 100644
--- a/spec/frontend/cycle_analytics/mock_data.js
+++ b/spec/frontend/cycle_analytics/mock_data.js
@@ -1,5 +1,10 @@
-import { DEFAULT_VALUE_STREAM } from '~/cycle_analytics/constants';
+import { TEST_HOST } from 'helpers/test_constants';
+import { DEFAULT_VALUE_STREAM, DEFAULT_DAYS_IN_PAST } from '~/cycle_analytics/constants';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+import { getDateInPast } from '~/lib/utils/datetime_utility';
+
+export const createdBefore = new Date(2019, 0, 14);
+export const createdAfter = getDateInPast(createdBefore, DEFAULT_DAYS_IN_PAST);
export const getStageByTitle = (stages, title) =>
stages.find((stage) => stage.title && stage.title.toLowerCase().trim() === title) || {};
@@ -169,6 +174,15 @@ export const stageMedians = {
staging: 388800,
};
+export const formattedStageMedians = {
+ issue: '2d',
+ plan: '1d',
+ review: '1w',
+ code: '1d',
+ test: '3d',
+ staging: '4d',
+};
+
export const allowedStages = [issueStage, planStage, codeStage];
export const transformedProjectStagePathData = [
@@ -212,6 +226,31 @@ export const transformedProjectStagePathData = [
export const selectedValueStream = DEFAULT_VALUE_STREAM;
+export const group = {
+ id: 1,
+ name: 'foo',
+ path: 'foo',
+ full_path: 'foo',
+ avatar_url: `${TEST_HOST}/images/home/nasa.svg`,
+};
+
+export const currentGroup = convertObjectPropsToCamelCase(group, { deep: true });
+
+export const selectedProjects = [
+ {
+ id: 'gid://gitlab/Project/1',
+ name: 'cool project',
+ pathWithNamespace: 'group/cool-project',
+ avatarUrl: null,
+ },
+ {
+ id: 'gid://gitlab/Project/2',
+ name: 'another cool project',
+ pathWithNamespace: 'group/another-cool-project',
+ avatarUrl: null,
+ },
+];
+
export const rawValueStreamStages = [
{
title: 'Issue',
diff --git a/spec/frontend/cycle_analytics/store/actions_spec.js b/spec/frontend/cycle_analytics/store/actions_spec.js
index 4f37e1266fb..8a8dd374f8e 100644
--- a/spec/frontend/cycle_analytics/store/actions_spec.js
+++ b/spec/frontend/cycle_analytics/store/actions_spec.js
@@ -3,7 +3,7 @@ import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
import * as actions from '~/cycle_analytics/store/actions';
import httpStatusCodes from '~/lib/utils/http_status';
-import { selectedStage, selectedValueStream } from '../mock_data';
+import { allowedStages, selectedStage, selectedValueStream } from '../mock_data';
const mockRequestPath = 'some/cool/path';
const mockFullPath = '/namespace/-/analytics/value_stream_analytics/value_streams';
@@ -25,6 +25,10 @@ const mockRequestedDataMutations = [
},
];
+const features = {
+ cycleAnalyticsForGroups: true,
+};
+
describe('Project Value Stream Analytics actions', () => {
let state;
let mock;
@@ -175,6 +179,7 @@ describe('Project Value Stream Analytics actions', () => {
beforeEach(() => {
state = {
+ features,
fullPath: mockFullPath,
};
mock = new MockAdapter(axios);
@@ -187,9 +192,33 @@ describe('Project Value Stream Analytics actions', () => {
state,
payload: {},
expectedMutations: [{ type: 'REQUEST_VALUE_STREAMS' }],
- expectedActions: [{ type: 'receiveValueStreamsSuccess' }, { type: 'setSelectedStage' }],
+ expectedActions: [
+ { type: 'receiveValueStreamsSuccess' },
+ { type: 'setSelectedStage' },
+ { type: 'fetchStageMedians' },
+ ],
}));
+ describe('with cycleAnalyticsForGroups=false', () => {
+ beforeEach(() => {
+ state = {
+ features: { cycleAnalyticsForGroups: false },
+ fullPath: mockFullPath,
+ };
+ mock = new MockAdapter(axios);
+ mock.onGet(mockValueStreamPath).reply(httpStatusCodes.OK);
+ });
+
+ it("does not dispatch the 'fetchStageMedians' request", () =>
+ testAction({
+ action: actions.fetchValueStreams,
+ state,
+ payload: {},
+ expectedMutations: [{ type: 'REQUEST_VALUE_STREAMS' }],
+ expectedActions: [{ type: 'receiveValueStreamsSuccess' }, { type: 'setSelectedStage' }],
+ }));
+ });
+
describe('with a failing request', () => {
beforeEach(() => {
mock = new MockAdapter(axios);
@@ -280,4 +309,59 @@ describe('Project Value Stream Analytics actions', () => {
}));
});
});
+
+ describe('fetchStageMedians', () => {
+ const mockValueStreamPath = /median/;
+
+ const stageMediansPayload = [
+ { id: 'issue', value: null },
+ { id: 'plan', value: null },
+ { id: 'code', value: null },
+ ];
+
+ const stageMedianError = new Error(
+ `Request failed with status code ${httpStatusCodes.BAD_REQUEST}`,
+ );
+
+ beforeEach(() => {
+ state = {
+ fullPath: mockFullPath,
+ selectedValueStream,
+ stages: allowedStages,
+ };
+ mock = new MockAdapter(axios);
+ mock.onGet(mockValueStreamPath).reply(httpStatusCodes.OK);
+ });
+
+ it(`commits the 'REQUEST_STAGE_MEDIANS' and 'RECEIVE_STAGE_MEDIANS_SUCCESS' mutations`, () =>
+ testAction({
+ action: actions.fetchStageMedians,
+ state,
+ payload: {},
+ expectedMutations: [
+ { type: 'REQUEST_STAGE_MEDIANS' },
+ { type: 'RECEIVE_STAGE_MEDIANS_SUCCESS', payload: stageMediansPayload },
+ ],
+ expectedActions: [],
+ }));
+
+ describe('with a failing request', () => {
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+ mock.onGet(mockValueStreamPath).reply(httpStatusCodes.BAD_REQUEST);
+ });
+
+ it(`commits the 'RECEIVE_VALUE_STREAM_STAGES_ERROR' mutation`, () =>
+ testAction({
+ action: actions.fetchStageMedians,
+ state,
+ payload: {},
+ expectedMutations: [
+ { type: 'REQUEST_STAGE_MEDIANS' },
+ { type: 'RECEIVE_STAGE_MEDIANS_ERROR', payload: stageMedianError },
+ ],
+ expectedActions: [],
+ }));
+ });
+ });
});
diff --git a/spec/frontend/cycle_analytics/store/mutations_spec.js b/spec/frontend/cycle_analytics/store/mutations_spec.js
index 88e1a13f506..77b19280517 100644
--- a/spec/frontend/cycle_analytics/store/mutations_spec.js
+++ b/spec/frontend/cycle_analytics/store/mutations_spec.js
@@ -1,3 +1,5 @@
+import { useFakeDate } from 'helpers/fake_date';
+import { DEFAULT_DAYS_TO_DISPLAY } from '~/cycle_analytics/constants';
import * as types from '~/cycle_analytics/store/mutation_types';
import mutations from '~/cycle_analytics/store/mutations';
import {
@@ -9,15 +11,23 @@ import {
selectedValueStream,
rawValueStreamStages,
valueStreamStages,
+ rawStageMedians,
+ formattedStageMedians,
} from '../mock_data';
let state;
const mockRequestPath = 'fake/request/path';
-const mockStartData = '2021-04-20';
+const mockCreatedAfter = '2020-06-18';
+const mockCreatedBefore = '2020-07-18';
+const features = {
+ cycleAnalyticsForGroups: true,
+};
describe('Project Value Stream Analytics mutations', () => {
+ useFakeDate(2020, 6, 18);
+
beforeEach(() => {
- state = {};
+ state = { features };
});
afterEach(() => {
@@ -46,6 +56,8 @@ describe('Project Value Stream Analytics mutations', () => {
${types.RECEIVE_STAGE_DATA_ERROR} | ${'selectedStageEvents'} | ${[]}
${types.RECEIVE_STAGE_DATA_ERROR} | ${'hasError'} | ${true}
${types.RECEIVE_STAGE_DATA_ERROR} | ${'isEmptyStage'} | ${true}
+ ${types.REQUEST_STAGE_MEDIANS} | ${'medians'} | ${{}}
+ ${types.RECEIVE_STAGE_MEDIANS_ERROR} | ${'medians'} | ${{}}
`('$mutation will set $stateKey to $value', ({ mutation, stateKey, value }) => {
mutations[mutation](state, {});
@@ -53,15 +65,19 @@ describe('Project Value Stream Analytics mutations', () => {
});
it.each`
- mutation | payload | stateKey | value
- ${types.INITIALIZE_VSA} | ${{ requestPath: mockRequestPath }} | ${'requestPath'} | ${mockRequestPath}
- ${types.SET_DATE_RANGE} | ${{ startDate: mockStartData }} | ${'startDate'} | ${mockStartData}
- ${types.SET_LOADING} | ${true} | ${'isLoading'} | ${true}
- ${types.SET_LOADING} | ${false} | ${'isLoading'} | ${false}
- ${types.SET_SELECTED_VALUE_STREAM} | ${selectedValueStream} | ${'selectedValueStream'} | ${selectedValueStream}
- ${types.RECEIVE_CYCLE_ANALYTICS_DATA_SUCCESS} | ${rawData} | ${'summary'} | ${convertedData.summary}
- ${types.RECEIVE_VALUE_STREAMS_SUCCESS} | ${[selectedValueStream]} | ${'valueStreams'} | ${[selectedValueStream]}
- ${types.RECEIVE_VALUE_STREAM_STAGES_SUCCESS} | ${{ stages: rawValueStreamStages }} | ${'stages'} | ${valueStreamStages}
+ mutation | payload | stateKey | value
+ ${types.INITIALIZE_VSA} | ${{ requestPath: mockRequestPath }} | ${'requestPath'} | ${mockRequestPath}
+ ${types.SET_DATE_RANGE} | ${{ startDate: DEFAULT_DAYS_TO_DISPLAY }} | ${'startDate'} | ${DEFAULT_DAYS_TO_DISPLAY}
+ ${types.SET_DATE_RANGE} | ${{ startDate: DEFAULT_DAYS_TO_DISPLAY }} | ${'createdAfter'} | ${mockCreatedAfter}
+ ${types.SET_DATE_RANGE} | ${{ startDate: DEFAULT_DAYS_TO_DISPLAY }} | ${'createdBefore'} | ${mockCreatedBefore}
+ ${types.SET_LOADING} | ${true} | ${'isLoading'} | ${true}
+ ${types.SET_LOADING} | ${false} | ${'isLoading'} | ${false}
+ ${types.SET_SELECTED_VALUE_STREAM} | ${selectedValueStream} | ${'selectedValueStream'} | ${selectedValueStream}
+ ${types.RECEIVE_CYCLE_ANALYTICS_DATA_SUCCESS} | ${rawData} | ${'summary'} | ${convertedData.summary}
+ ${types.RECEIVE_VALUE_STREAMS_SUCCESS} | ${[selectedValueStream]} | ${'valueStreams'} | ${[selectedValueStream]}
+ ${types.RECEIVE_VALUE_STREAM_STAGES_SUCCESS} | ${{ stages: rawValueStreamStages }} | ${'stages'} | ${valueStreamStages}
+ ${types.RECEIVE_VALUE_STREAMS_SUCCESS} | ${[selectedValueStream]} | ${'valueStreams'} | ${[selectedValueStream]}
+ ${types.RECEIVE_STAGE_MEDIANS_SUCCESS} | ${rawStageMedians} | ${'medians'} | ${formattedStageMedians}
`(
'$mutation with $payload will set $stateKey to $value',
({ mutation, payload, stateKey, value }) => {
@@ -92,4 +108,35 @@ describe('Project Value Stream Analytics mutations', () => {
},
);
});
+
+ describe('with cycleAnalyticsForGroups=false', () => {
+ useFakeDate(2020, 6, 18);
+
+ beforeEach(() => {
+ state = { features: { cycleAnalyticsForGroups: false } };
+ });
+
+ const formattedMedians = {
+ code: '2d',
+ issue: '-',
+ plan: '21h',
+ review: '-',
+ staging: '2d',
+ test: '4h',
+ };
+
+ it.each`
+ mutation | payload | stateKey | value
+ ${types.RECEIVE_CYCLE_ANALYTICS_DATA_SUCCESS} | ${rawData} | ${'medians'} | ${formattedMedians}
+ ${types.REQUEST_CYCLE_ANALYTICS_DATA} | ${{}} | ${'medians'} | ${{}}
+ ${types.RECEIVE_CYCLE_ANALYTICS_DATA_ERROR} | ${{}} | ${'medians'} | ${{}}
+ `(
+ '$mutation with $payload will set $stateKey to $value',
+ ({ mutation, payload, stateKey, value }) => {
+ mutations[mutation](state, payload);
+
+ expect(state).toMatchObject({ [stateKey]: value });
+ },
+ );
+ });
});
diff --git a/spec/frontend/cycle_analytics/utils_spec.js b/spec/frontend/cycle_analytics/utils_spec.js
index 15137bb0571..1fecdfc0539 100644
--- a/spec/frontend/cycle_analytics/utils_spec.js
+++ b/spec/frontend/cycle_analytics/utils_spec.js
@@ -1,3 +1,4 @@
+import { useFakeDate } from 'helpers/fake_date';
import {
decorateEvents,
decorateData,
@@ -6,6 +7,7 @@ import {
medianTimeToParsedSeconds,
formatMedianValues,
filterStagesByHiddenStatus,
+ calculateFormattedDayInPast,
} from '~/cycle_analytics/utils';
import {
selectedStage,
@@ -149,4 +151,12 @@ describe('Value stream analytics utils', () => {
expect(filterStagesByHiddenStatus(mockStages, isHidden)).toEqual(result);
});
});
+
+ describe('calculateFormattedDayInPast', () => {
+ useFakeDate(1815, 11, 10);
+
+ it('will return 2 dates, now and past', () => {
+ expect(calculateFormattedDayInPast(5)).toEqual({ now: '1815-12-10', past: '1815-12-05' });
+ });
+ });
});
diff --git a/spec/frontend/cycle_analytics/value_stream_filters_spec.js b/spec/frontend/cycle_analytics/value_stream_filters_spec.js
new file mode 100644
index 00000000000..6e96a6d756a
--- /dev/null
+++ b/spec/frontend/cycle_analytics/value_stream_filters_spec.js
@@ -0,0 +1,91 @@
+import { shallowMount } from '@vue/test-utils';
+import Daterange from '~/analytics/shared/components/daterange.vue';
+import ProjectsDropdownFilter from '~/analytics/shared/components/projects_dropdown_filter.vue';
+import FilterBar from '~/cycle_analytics/components/filter_bar.vue';
+import ValueStreamFilters from '~/cycle_analytics/components/value_stream_filters.vue';
+import {
+ createdAfter as startDate,
+ createdBefore as endDate,
+ currentGroup,
+ selectedProjects,
+} from './mock_data';
+
+function createComponent(props = {}) {
+ return shallowMount(ValueStreamFilters, {
+ propsData: {
+ selectedProjects,
+ groupId: currentGroup.id,
+ groupPath: currentGroup.fullPath,
+ startDate,
+ endDate,
+ ...props,
+ },
+ });
+}
+
+describe('ValueStreamFilters', () => {
+ let wrapper;
+
+ const findProjectsDropdown = () => wrapper.findComponent(ProjectsDropdownFilter);
+ const findDateRangePicker = () => wrapper.findComponent(Daterange);
+ const findFilterBar = () => wrapper.findComponent(FilterBar);
+
+ beforeEach(() => {
+ wrapper = createComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ it('will render the filter bar', () => {
+ expect(findFilterBar().exists()).toBe(true);
+ });
+
+ it('will render the projects dropdown', () => {
+ expect(findProjectsDropdown().exists()).toBe(true);
+ expect(wrapper.findComponent(ProjectsDropdownFilter).props()).toEqual(
+ expect.objectContaining({
+ queryParams: wrapper.vm.projectsQueryParams,
+ multiSelect: wrapper.vm.$options.multiProjectSelect,
+ }),
+ );
+ });
+
+ it('will render the date range picker', () => {
+ expect(findDateRangePicker().exists()).toBe(true);
+ });
+
+ it('will emit `selectProject` when a project is selected', () => {
+ findProjectsDropdown().vm.$emit('selected');
+
+ expect(wrapper.emitted('selectProject')).not.toBeUndefined();
+ });
+
+ it('will emit `setDateRange` when the date range changes', () => {
+ findDateRangePicker().vm.$emit('change');
+
+ expect(wrapper.emitted('setDateRange')).not.toBeUndefined();
+ });
+
+ describe('hasDateRangeFilter = false', () => {
+ beforeEach(() => {
+ wrapper = createComponent({ hasDateRangeFilter: false });
+ });
+
+ it('will not render the date range picker', () => {
+ expect(findDateRangePicker().exists()).toBe(false);
+ });
+ });
+
+ describe('hasProjectFilter = false', () => {
+ beforeEach(() => {
+ wrapper = createComponent({ hasProjectFilter: false });
+ });
+
+ it('will not render the project dropdown', () => {
+ expect(findProjectsDropdown().exists()).toBe(false);
+ });
+ });
+});
diff --git a/spec/frontend/design_management/components/design_notes/__snapshots__/design_note_spec.js.snap b/spec/frontend/design_management/components/design_notes/__snapshots__/design_note_spec.js.snap
index 084a7e5d712..4ecf82a4714 100644
--- a/spec/frontend/design_management/components/design_notes/__snapshots__/design_note_spec.js.snap
+++ b/spec/frontend/design_management/components/design_notes/__snapshots__/design_note_spec.js.snap
@@ -6,7 +6,7 @@ exports[`Design note component should match the snapshot 1`] = `
id="note_123"
>
<user-avatar-link-stub
- imgalt=""
+ imgalt="foo-bar"
imgcssclasses=""
imgsize="40"
imgsrc=""
@@ -22,7 +22,8 @@ exports[`Design note component should match the snapshot 1`] = `
<div>
<gl-link-stub
class="js-user-link"
- data-user-id="author-id"
+ data-user-id="1"
+ data-username="foo-bar"
>
<span
class="note-header-author-name gl-font-weight-bold"
@@ -35,7 +36,7 @@ exports[`Design note component should match the snapshot 1`] = `
<span
class="note-headline-light"
>
- @
+ @foo-bar
</span>
</gl-link-stub>
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 1cd556eabb4..3f5f5bcdfa7 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
@@ -9,7 +9,8 @@ const scrollIntoViewMock = jest.fn();
const note = {
id: 'gid://gitlab/DiffNote/123',
author: {
- id: 'author-id',
+ id: 'gid://gitlab/User/1',
+ username: 'foo-bar',
},
body: 'test',
userPermissions: {
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 20686d0ae6c..757bf50c527 100644
--- a/spec/frontend/design_management/components/design_todo_button_spec.js
+++ b/spec/frontend/design_management/components/design_todo_button_spec.js
@@ -2,7 +2,7 @@ import { shallowMount, mount } from '@vue/test-utils';
import DesignTodoButton from '~/design_management/components/design_todo_button.vue';
import createDesignTodoMutation from '~/design_management/graphql/mutations/create_design_todo.mutation.graphql';
import todoMarkDoneMutation from '~/graphql_shared/mutations/todo_mark_done.mutation.graphql';
-import TodoButton from '~/vue_shared/components/todo_button.vue';
+import TodoButton from '~/vue_shared/components/sidebar/todo_toggle/todo_button.vue';
import mockDesign from '../mock_data/design';
const mockDesignWithPendingTodos = {
diff --git a/spec/frontend/design_management/pages/design/index_spec.js b/spec/frontend/design_management/pages/design/index_spec.js
index 11c88c3d0f5..1332e872246 100644
--- a/spec/frontend/design_management/pages/design/index_spec.js
+++ b/spec/frontend/design_management/pages/design/index_spec.js
@@ -20,7 +20,7 @@ import {
import {
DESIGN_TRACKING_PAGE_NAME,
DESIGN_SNOWPLOW_EVENT_TYPES,
- DESIGN_USAGE_PING_EVENT_TYPES,
+ DESIGN_SERVICE_PING_EVENT_TYPES,
} from '~/design_management/utils/tracking';
import createFlash from '~/flash';
import mockAllVersions from '../../mock_data/all_versions';
@@ -391,7 +391,7 @@ describe('Design management design index page', () => {
});
describe('with usage_data_design_action enabled', () => {
- it('tracks design view usage ping', () => {
+ it('tracks design view service ping', () => {
createComponent(
{ loading: true },
{
@@ -402,13 +402,13 @@ describe('Design management design index page', () => {
);
expect(Api.trackRedisHllUserEvent).toHaveBeenCalledTimes(1);
expect(Api.trackRedisHllUserEvent).toHaveBeenCalledWith(
- DESIGN_USAGE_PING_EVENT_TYPES.DESIGN_ACTION,
+ DESIGN_SERVICE_PING_EVENT_TYPES.DESIGN_ACTION,
);
});
});
describe('with usage_data_design_action disabled', () => {
- it("doesn't track design view usage ping", () => {
+ it("doesn't track design view service ping", () => {
createComponent({ loading: true });
expect(Api.trackRedisHllUserEvent).toHaveBeenCalledTimes(0);
});
diff --git a/spec/frontend/diffs/components/app_spec.js b/spec/frontend/diffs/components/app_spec.js
index 8a1c5547581..b5eb3e1713c 100644
--- a/spec/frontend/diffs/components/app_spec.js
+++ b/spec/frontend/diffs/components/app_spec.js
@@ -6,14 +6,19 @@ import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import { TEST_HOST } from 'spec/test_constants';
import App from '~/diffs/components/app.vue';
-import CollapsedFilesWarning from '~/diffs/components/collapsed_files_warning.vue';
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 HiddenFilesWarning from '~/diffs/components/hidden_files_warning.vue';
import NoChanges from '~/diffs/components/no_changes.vue';
import TreeList from '~/diffs/components/tree_list.vue';
+/* eslint-disable import/order */
+/* You know what: sometimes alphabetical isn't the best order */
+import CollapsedFilesWarning from '~/diffs/components/collapsed_files_warning.vue';
+import HiddenFilesWarning from '~/diffs/components/hidden_files_warning.vue';
+import MergeConflictWarning from '~/diffs/components/merge_conflict_warning.vue';
+/* eslint-enable import/order */
+
import axios from '~/lib/utils/axios_utils';
import * as urlUtils from '~/lib/utils/url_utility';
import createDiffsStore from '../create_diffs_store';
@@ -541,6 +546,43 @@ describe('diffs/components/app', () => {
expect(getCollapsedFilesWarning(wrapper).exists()).toBe(false);
});
});
+
+ describe('merge conflicts', () => {
+ it('should render the merge conflicts banner if viewing the whole changeset and there are conflicts', () => {
+ createComponent({}, ({ state }) => {
+ Object.assign(state.diffs, {
+ latestDiff: true,
+ startVersion: null,
+ hasConflicts: true,
+ canMerge: false,
+ conflictResolutionPath: 'path',
+ });
+ });
+
+ expect(wrapper.find(MergeConflictWarning).exists()).toBe(true);
+ });
+
+ it.each`
+ prop | value
+ ${'latestDiff'} | ${false}
+ ${'startVersion'} | ${'notnull'}
+ ${'hasConflicts'} | ${false}
+ `(
+ "should not render if any of the MR properties aren't correct - like $prop: $value",
+ ({ prop, value }) => {
+ createComponent({}, ({ state }) => {
+ Object.assign(state.diffs, {
+ latestDiff: true,
+ startVersion: null,
+ hasConflicts: true,
+ [prop]: value,
+ });
+ });
+
+ expect(wrapper.find(MergeConflictWarning).exists()).toBe(false);
+ },
+ );
+ });
});
it('should display commit widget if store has a commit', () => {
diff --git a/spec/frontend/diffs/components/collapsed_files_warning_spec.js b/spec/frontend/diffs/components/collapsed_files_warning_spec.js
index 77c2e19cb68..46caeb01132 100644
--- a/spec/frontend/diffs/components/collapsed_files_warning_spec.js
+++ b/spec/frontend/diffs/components/collapsed_files_warning_spec.js
@@ -1,10 +1,13 @@
import { shallowMount, mount, createLocalVue } from '@vue/test-utils';
+import { nextTick } from 'vue';
import Vuex from 'vuex';
import CollapsedFilesWarning from '~/diffs/components/collapsed_files_warning.vue';
import { CENTERED_LIMITED_CONTAINER_CLASSES, EVT_EXPAND_ALL_FILES } from '~/diffs/constants';
import eventHub from '~/diffs/event_hub';
import createStore from '~/diffs/store/modules';
+import file from '../mock_data/diff_file';
+
const propsData = {
limited: true,
mergeable: true,
@@ -12,6 +15,13 @@ const propsData = {
};
const limitedClasses = CENTERED_LIMITED_CONTAINER_CLASSES.split(' ');
+async function files(store, count) {
+ const copies = Array(count).fill(file);
+ store.state.diffs.diffFiles.push(...copies);
+
+ return nextTick();
+}
+
describe('CollapsedFilesWarning', () => {
const localVue = createLocalVue();
let store;
@@ -42,48 +52,63 @@ describe('CollapsedFilesWarning', () => {
wrapper.destroy();
});
- it.each`
- limited | containerClasses
- ${true} | ${limitedClasses}
- ${false} | ${[]}
- `(
- 'has the correct container classes when limited is $limited',
- ({ limited, containerClasses }) => {
- createComponent({ limited });
-
- expect(wrapper.classes()).toEqual(['col-12'].concat(containerClasses));
- },
- );
-
- it.each`
- present | dismissed
- ${false} | ${true}
- ${true} | ${false}
- `('toggles the alert when dismissed is $dismissed', ({ present, dismissed }) => {
- createComponent({ dismissed });
-
- expect(wrapper.find('[data-testid="root"]').exists()).toBe(present);
- });
+ describe('when there is more than one file', () => {
+ it.each`
+ limited | containerClasses
+ ${true} | ${limitedClasses}
+ ${false} | ${[]}
+ `(
+ 'has the correct container classes when limited is $limited',
+ async ({ limited, containerClasses }) => {
+ createComponent({ limited });
+ await files(store, 2);
+
+ expect(wrapper.classes()).toEqual(['col-12'].concat(containerClasses));
+ },
+ );
- it('dismisses the component when the alert "x" is clicked', async () => {
- createComponent({}, { full: true });
+ it.each`
+ present | dismissed
+ ${false} | ${true}
+ ${true} | ${false}
+ `('toggles the alert when dismissed is $dismissed', async ({ present, dismissed }) => {
+ createComponent({ dismissed });
+ await files(store, 2);
- expect(wrapper.find('[data-testid="root"]').exists()).toBe(true);
+ expect(wrapper.find('[data-testid="root"]').exists()).toBe(present);
+ });
- getAlertCloseButton().element.click();
+ it('dismisses the component when the alert "x" is clicked', async () => {
+ createComponent({}, { full: true });
+ await files(store, 2);
- await wrapper.vm.$nextTick();
+ expect(wrapper.find('[data-testid="root"]').exists()).toBe(true);
- expect(wrapper.find('[data-testid="root"]').exists()).toBe(false);
- });
+ getAlertCloseButton().element.click();
- it(`emits the \`${EVT_EXPAND_ALL_FILES}\` event when the alert action button is clicked`, () => {
- createComponent({}, { full: true });
+ await wrapper.vm.$nextTick();
- jest.spyOn(eventHub, '$emit');
+ expect(wrapper.find('[data-testid="root"]').exists()).toBe(false);
+ });
- getAlertActionButton().vm.$emit('click');
+ it(`emits the \`${EVT_EXPAND_ALL_FILES}\` event when the alert action button is clicked`, async () => {
+ createComponent({}, { full: true });
+ await files(store, 2);
- expect(eventHub.$emit).toHaveBeenCalledWith(EVT_EXPAND_ALL_FILES);
+ jest.spyOn(eventHub, '$emit');
+
+ getAlertActionButton().vm.$emit('click');
+
+ expect(eventHub.$emit).toHaveBeenCalledWith(EVT_EXPAND_ALL_FILES);
+ });
+ });
+
+ describe('when there is a single file', () => {
+ it('should not display', async () => {
+ createComponent();
+ await files(store, 1);
+
+ expect(wrapper.find('[data-testid="root"]').exists()).toBe(false);
+ });
});
});
diff --git a/spec/frontend/diffs/components/diff_content_spec.js b/spec/frontend/diffs/components/diff_content_spec.js
index 7012889440c..0a7dfc02c65 100644
--- a/spec/frontend/diffs/components/diff_content_spec.js
+++ b/spec/frontend/diffs/components/diff_content_spec.js
@@ -4,8 +4,6 @@ import Vuex from 'vuex';
import DiffContentComponent from '~/diffs/components/diff_content.vue';
import DiffDiscussions from '~/diffs/components/diff_discussions.vue';
import DiffView from '~/diffs/components/diff_view.vue';
-import InlineDiffView from '~/diffs/components/inline_diff_view.vue';
-import ParallelDiffView from '~/diffs/components/parallel_diff_view.vue';
import { IMAGE_DIFF_POSITION_TYPE } from '~/diffs/constants';
import { diffViewerModes } from '~/ide/constants';
import NoteForm from '~/notes/components/note_form.vue';
@@ -107,25 +105,10 @@ describe('DiffContent', () => {
});
const textDiffFile = { ...defaultProps.diffFile, viewer: { name: diffViewerModes.text } };
- it('should render diff inline view if `isInlineView` is true', () => {
- isInlineViewGetterMock.mockReturnValue(true);
- createComponent({ props: { diffFile: textDiffFile } });
-
- expect(wrapper.find(InlineDiffView).exists()).toBe(true);
- });
-
- it('should render parallel view if `isParallelView` getter is true', () => {
- isParallelViewGetterMock.mockReturnValue(true);
- createComponent({ props: { diffFile: textDiffFile } });
-
- expect(wrapper.find(ParallelDiffView).exists()).toBe(true);
- });
it('should render diff view if `unifiedDiffComponents` are true', () => {
- isParallelViewGetterMock.mockReturnValue(true);
createComponent({
props: { diffFile: textDiffFile },
- provide: { glFeatures: { unifiedDiffComponents: true } },
});
expect(wrapper.find(DiffView).exists()).toBe(true);
diff --git a/spec/frontend/diffs/components/diff_file_spec.js b/spec/frontend/diffs/components/diff_file_spec.js
index 1e8ad9344f2..99dda8d5deb 100644
--- a/spec/frontend/diffs/components/diff_file_spec.js
+++ b/spec/frontend/diffs/components/diff_file_spec.js
@@ -110,7 +110,6 @@ const findLoader = (wrapper) => wrapper.find('[data-testid="loader-icon"]');
const findToggleButton = (wrapper) => wrapper.find('[data-testid="expand-button"]');
const toggleFile = (wrapper) => findDiffHeader(wrapper).vm.$emit('toggleFile');
-const isDisplayNone = (element) => element.style.display === 'none';
const getReadableFile = () => JSON.parse(JSON.stringify(diffFileMockDataReadable));
const getUnreadableFile = () => JSON.parse(JSON.stringify(diffFileMockDataUnreadable));
@@ -305,9 +304,7 @@ describe('DiffFile', () => {
it('should not have any content at all', async () => {
await wrapper.vm.$nextTick();
- Array.from(findDiffContentArea(wrapper).element.children).forEach((child) => {
- expect(isDisplayNone(child)).toBe(true);
- });
+ expect(findDiffContentArea(wrapper).element.children.length).toBe(0);
});
it('should not have the class `has-body` to present the header differently', () => {
diff --git a/spec/frontend/diffs/components/diff_row_spec.js b/spec/frontend/diffs/components/diff_row_spec.js
index 137cc7e3f86..c0c92908701 100644
--- a/spec/frontend/diffs/components/diff_row_spec.js
+++ b/spec/frontend/diffs/components/diff_row_spec.js
@@ -8,6 +8,12 @@ import diffsModule from '~/diffs/store/modules';
import { findInteropAttributes } from '../find_interop_attributes';
import diffFileMockData from '../mock_data/diff_file';
+const showCommentForm = jest.fn();
+const enterdragging = jest.fn();
+const stopdragging = jest.fn();
+const setHighlightedRow = jest.fn();
+let wrapper;
+
describe('DiffRow', () => {
const testLines = [
{
@@ -29,7 +35,7 @@ describe('DiffRow', () => {
},
];
- const createWrapper = ({ props, state, actions, isLoggedIn = true }) => {
+ const createWrapper = ({ props, state = {}, actions, isLoggedIn = true }) => {
Vue.use(Vuex);
const diffs = diffsModule();
@@ -43,11 +49,25 @@ describe('DiffRow', () => {
getters,
});
+ window.gon = { current_user_id: isLoggedIn ? 1 : 0 };
+ const coverageFileData = state.coverageFiles?.files ? state.coverageFiles.files : {};
+
const propsData = {
fileHash: 'abc',
filePath: 'abc',
line: {},
index: 0,
+ isHighlighted: false,
+ fileLineCoverage: (file, line) => {
+ const hits = coverageFileData[file]?.[line];
+ if (hits) {
+ return { text: `Test coverage: ${hits} hits`, class: 'coverage' };
+ } else if (hits === 0) {
+ return { text: 'No test coverage', class: 'no-coverage' };
+ }
+
+ return {};
+ },
...props,
};
@@ -55,49 +75,37 @@ describe('DiffRow', () => {
glFeatures: { dragCommentSelection: true },
};
- return shallowMount(DiffRow, { propsData, store, provide });
+ return shallowMount(DiffRow, {
+ propsData,
+ store,
+ provide,
+ listeners: {
+ enterdragging,
+ stopdragging,
+ setHighlightedRow,
+ showCommentForm,
+ },
+ });
};
- it('isHighlighted returns true given line.left', () => {
- const props = {
- line: {
- left: {
- line_code: 'abc',
- },
- },
- };
- const state = { highlightedRow: 'abc' };
- const wrapper = createWrapper({ props, state });
- expect(wrapper.vm.isHighlighted).toBe(true);
- });
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
- it('isHighlighted returns true given line.right', () => {
- const props = {
- line: {
- right: {
- line_code: 'abc',
- },
- },
- };
- const state = { highlightedRow: 'abc' };
- const wrapper = createWrapper({ props, state });
- expect(wrapper.vm.isHighlighted).toBe(true);
- });
+ window.gon = {};
+ showCommentForm.mockReset();
+ enterdragging.mockReset();
+ stopdragging.mockReset();
+ setHighlightedRow.mockReset();
- it('isHighlighted returns false given line.left', () => {
- const props = {
- line: {
- left: {
- line_code: 'abc',
- },
- },
- };
- const wrapper = createWrapper({ props });
- expect(wrapper.vm.isHighlighted).toBe(false);
+ Object.values(DiffRow).forEach(({ cache }) => {
+ if (cache) {
+ cache.clear();
+ }
+ });
});
- const getCommentButton = (wrapper, side) =>
- wrapper.find(`[data-testid="${side}-comment-button"]`);
+ const getCommentButton = (side) => wrapper.find(`[data-testid="${side}-comment-button"]`);
describe.each`
side
@@ -105,33 +113,30 @@ describe('DiffRow', () => {
${'right'}
`('$side side', ({ side }) => {
it(`renders empty cells if ${side} is unavailable`, () => {
- const wrapper = createWrapper({ props: { line: testLines[2], inline: false } });
+ wrapper = createWrapper({ props: { line: testLines[2], inline: false } });
expect(wrapper.find(`[data-testid="${side}-line-number"]`).exists()).toBe(false);
expect(wrapper.find(`[data-testid="${side}-empty-cell"]`).exists()).toBe(true);
});
describe('comment button', () => {
- const showCommentForm = jest.fn();
let line;
beforeEach(() => {
- showCommentForm.mockReset();
// https://eslint.org/docs/rules/prefer-destructuring#when-not-to-use-it
// eslint-disable-next-line prefer-destructuring
line = testLines[3];
});
it('renders', () => {
- const wrapper = createWrapper({ props: { line, inline: false } });
- expect(getCommentButton(wrapper, side).exists()).toBe(true);
+ wrapper = createWrapper({ props: { line, inline: false } });
+ expect(getCommentButton(side).exists()).toBe(true);
});
it('responds to click and keyboard events', async () => {
- const wrapper = createWrapper({
+ wrapper = createWrapper({
props: { line, inline: false },
- actions: { showCommentForm },
});
- const commentButton = getCommentButton(wrapper, side);
+ const commentButton = getCommentButton(side);
await commentButton.trigger('click');
await commentButton.trigger('keydown.enter');
@@ -142,11 +147,10 @@ describe('DiffRow', () => {
it('ignores click and keyboard events when comments are disabled', async () => {
line[side].commentsDisabled = true;
- const wrapper = createWrapper({
+ wrapper = createWrapper({
props: { line, inline: false },
- actions: { showCommentForm },
});
- const commentButton = getCommentButton(wrapper, side);
+ const commentButton = getCommentButton(side);
await commentButton.trigger('click');
await commentButton.trigger('keydown.enter');
@@ -157,19 +161,20 @@ describe('DiffRow', () => {
});
it('renders avatars', () => {
- const wrapper = createWrapper({ props: { line: testLines[0], inline: false } });
+ wrapper = createWrapper({ props: { line: testLines[0], inline: false } });
+
expect(wrapper.find(`[data-testid="${side}-discussions"]`).exists()).toBe(true);
});
});
it('renders left line numbers', () => {
- const wrapper = createWrapper({ props: { line: testLines[0] } });
+ wrapper = createWrapper({ props: { line: testLines[0] } });
const lineNumber = testLines[0].left.old_line;
expect(wrapper.find(`[data-linenumber="${lineNumber}"]`).exists()).toBe(true);
});
it('renders right line numbers', () => {
- const wrapper = createWrapper({ props: { line: testLines[0] } });
+ wrapper = createWrapper({ props: { line: testLines[0] } });
const lineNumber = testLines[0].right.new_line;
expect(wrapper.find(`[data-linenumber="${lineNumber}"]`).exists()).toBe(true);
});
@@ -186,12 +191,10 @@ describe('DiffRow', () => {
${'left'}
${'right'}
`('emits `enterdragging` onDragEnter $side side', ({ side }) => {
- const expectation = { ...line[side], index: 0 };
- const wrapper = createWrapper({ props: { line } });
+ wrapper = createWrapper({ props: { line } });
fireEvent.dragEnter(getByTestId(wrapper.element, `${side}-side`));
- expect(wrapper.emitted().enterdragging).toBeTruthy();
- expect(wrapper.emitted().enterdragging[0]).toEqual([expectation]);
+ expect(enterdragging).toHaveBeenCalledWith({ ...line[side], index: 0 });
});
it.each`
@@ -199,10 +202,10 @@ describe('DiffRow', () => {
${'left'}
${'right'}
`('emits `stopdragging` onDrop $side side', ({ side }) => {
- const wrapper = createWrapper({ props: { line } });
+ wrapper = createWrapper({ props: { line } });
fireEvent.dragEnd(getByTestId(wrapper.element, `${side}-side`));
- expect(wrapper.emitted().stopdragging).toBeTruthy();
+ expect(stopdragging).toHaveBeenCalled();
});
});
@@ -231,7 +234,7 @@ describe('DiffRow', () => {
it('for lines with coverage', () => {
const coverageFiles = { files: { [name]: { [line]: 5 } } };
- const wrapper = createWrapper({ props, state: { coverageFiles } });
+ wrapper = createWrapper({ props, state: { coverageFiles } });
const coverage = wrapper.find('.line-coverage.right-side');
expect(coverage.attributes('title')).toContain('Test coverage: 5 hits');
@@ -240,7 +243,7 @@ describe('DiffRow', () => {
it('for lines without coverage', () => {
const coverageFiles = { files: { [name]: { [line]: 0 } } };
- const wrapper = createWrapper({ props, state: { coverageFiles } });
+ wrapper = createWrapper({ props, state: { coverageFiles } });
const coverage = wrapper.find('.line-coverage.right-side');
expect(coverage.attributes('title')).toContain('No test coverage');
@@ -249,7 +252,7 @@ describe('DiffRow', () => {
it('for unknown lines', () => {
const coverageFiles = {};
- const wrapper = createWrapper({ props, state: { coverageFiles } });
+ wrapper = createWrapper({ props, state: { coverageFiles } });
const coverage = wrapper.find('.line-coverage.right-side');
expect(coverage.attributes('title')).toBeFalsy();
@@ -267,7 +270,7 @@ describe('DiffRow', () => {
${'with parallel and no left side'} | ${{ right: { old_line: 3, new_line: 5 } }} | ${false} | ${null} | ${{ type: 'new', line: '5', newLine: '5' }}
${'with parallel and right side'} | ${{ left: { old_line: 3 }, right: { new_line: 5 } }} | ${false} | ${{ type: 'old', line: '3', oldLine: '3' }} | ${{ type: 'new', line: '5', newLine: '5' }}
`('$desc, sets interop data attributes', ({ line, inline, leftSide, rightSide }) => {
- const wrapper = createWrapper({ props: { line, inline } });
+ wrapper = createWrapper({ props: { line, inline } });
expect(findInteropAttributes(wrapper, '[data-testid="left-side"]')).toEqual(leftSide);
expect(findInteropAttributes(wrapper, '[data-testid="right-side"]')).toEqual(rightSide);
diff --git a/spec/frontend/diffs/components/diff_row_utils_spec.js b/spec/frontend/diffs/components/diff_row_utils_spec.js
index 47ae3cd5867..930b8bcdb08 100644
--- a/spec/frontend/diffs/components/diff_row_utils_spec.js
+++ b/spec/frontend/diffs/components/diff_row_utils_spec.js
@@ -11,24 +11,21 @@ const LINE_CODE = 'abc123';
describe('isHighlighted', () => {
it('should return true if line is highlighted', () => {
- const state = { diffs: { highlightedRow: LINE_CODE } };
const line = { line_code: LINE_CODE };
const isCommented = false;
- expect(utils.isHighlighted(state, line, isCommented)).toBe(true);
+ expect(utils.isHighlighted(LINE_CODE, line, isCommented)).toBe(true);
});
it('should return false if line is not highlighted', () => {
- const state = { diffs: { highlightedRow: 'xxx' } };
const line = { line_code: LINE_CODE };
const isCommented = false;
- expect(utils.isHighlighted(state, line, isCommented)).toBe(false);
+ expect(utils.isHighlighted('xxx', line, isCommented)).toBe(false);
});
it('should return true if isCommented is true', () => {
- const state = { diffs: { highlightedRow: 'xxx' } };
const line = { line_code: LINE_CODE };
const isCommented = true;
- expect(utils.isHighlighted(state, line, isCommented)).toBe(true);
+ expect(utils.isHighlighted('xxx', line, isCommented)).toBe(true);
});
});
@@ -143,19 +140,14 @@ describe('addCommentTooltip', () => {
'Commenting on symbolic links that replace or are replaced by files is currently not supported.';
const brokenRealTooltip =
'Commenting on files that replace or are replaced by symbolic links is currently not supported.';
- const commentTooltip = 'Add a comment to this line';
const dragTooltip = 'Add a comment to this line or drag for multiple lines';
it('should return default tooltip', () => {
expect(utils.addCommentTooltip()).toBeUndefined();
});
- it('should return comment tooltip', () => {
- expect(utils.addCommentTooltip({})).toEqual(commentTooltip);
- });
-
it('should return drag comment tooltip when dragging is enabled', () => {
- expect(utils.addCommentTooltip({}, true)).toEqual(dragTooltip);
+ expect(utils.addCommentTooltip({})).toEqual(dragTooltip);
});
it('should return broken symlink tooltip', () => {
@@ -258,30 +250,3 @@ describe('mapParallel', () => {
expect(mapped.right).toMatchObject(rightExpectation);
});
});
-
-describe('mapInline', () => {
- it('should assign computed properties to the line object', () => {
- const content = {
- diffFile: {},
- shouldRenderDraftRow: () => false,
- };
- const line = {
- discussions: [{}],
- discussionsExpanded: true,
- hasForm: true,
- };
- const expectation = {
- commentRowClasses: '',
- hasDiscussions: true,
- isContextLine: false,
- isMatchLine: false,
- isMetaLine: false,
- renderDiscussion: true,
- hasDraft: false,
- hasCommentForm: true,
- };
- const mapped = utils.mapInline(content)(line);
-
- expect(mapped).toMatchObject(expectation);
- });
-});
diff --git a/spec/frontend/diffs/components/diff_view_spec.js b/spec/frontend/diffs/components/diff_view_spec.js
index 83b173c1f5d..3af66526050 100644
--- a/spec/frontend/diffs/components/diff_view_spec.js
+++ b/spec/frontend/diffs/components/diff_view_spec.js
@@ -28,7 +28,7 @@ describe('DiffView', () => {
};
const diffs = {
actions: { showCommentForm },
- getters: { commitId: () => 'abc123' },
+ getters: { commitId: () => 'abc123', fileLineCoverage: () => ({}) },
namespaced: true,
};
const notes = {
@@ -41,7 +41,7 @@ describe('DiffView', () => {
});
const propsData = {
- diffFile: {},
+ diffFile: { file_hash: '123' },
diffLines: [],
...props,
};
@@ -84,15 +84,15 @@ describe('DiffView', () => {
it('sets `dragStart` onStartDragging', () => {
const wrapper = createWrapper({ diffLines: [{}] });
- wrapper.findComponent(DiffRow).vm.$emit('startdragging', { test: true });
- expect(wrapper.vm.dragStart).toEqual({ test: true });
+ wrapper.findComponent(DiffRow).vm.$emit('startdragging', { line: { test: true } });
+ expect(wrapper.vm.idState.dragStart).toEqual({ test: true });
});
it('does not call `setSelectedCommentPosition` on different chunks onDragOver', () => {
const wrapper = createWrapper({ diffLines: [{}] });
const diffRow = getDiffRow(wrapper);
- diffRow.$emit('startdragging', { chunk: 0 });
+ diffRow.$emit('startdragging', { line: { chunk: 0 } });
diffRow.$emit('enterdragging', { chunk: 1 });
expect(setSelectedCommentPosition).not.toHaveBeenCalled();
@@ -109,7 +109,7 @@ describe('DiffView', () => {
const wrapper = createWrapper({ diffLines: [{}] });
const diffRow = getDiffRow(wrapper);
- diffRow.$emit('startdragging', { chunk: 1, index: start });
+ diffRow.$emit('startdragging', { line: { chunk: 1, index: start } });
diffRow.$emit('enterdragging', { chunk: 1, index: end });
const arg = setSelectedCommentPosition.mock.calls[0][1];
@@ -122,11 +122,11 @@ describe('DiffView', () => {
const wrapper = createWrapper({ diffLines: [{}] });
const diffRow = getDiffRow(wrapper);
- diffRow.$emit('startdragging', { test: true });
- expect(wrapper.vm.dragStart).toEqual({ test: true });
+ diffRow.$emit('startdragging', { line: { test: true } });
+ expect(wrapper.vm.idState.dragStart).toEqual({ test: true });
diffRow.$emit('stopdragging');
- expect(wrapper.vm.dragStart).toBeNull();
+ expect(wrapper.vm.idState.dragStart).toBeNull();
expect(showCommentForm).toHaveBeenCalled();
});
});
diff --git a/spec/frontend/diffs/components/inline_diff_table_row_spec.js b/spec/frontend/diffs/components/inline_diff_table_row_spec.js
deleted file mode 100644
index 9c3e00cd6cf..00000000000
--- a/spec/frontend/diffs/components/inline_diff_table_row_spec.js
+++ /dev/null
@@ -1,325 +0,0 @@
-import { shallowMount } from '@vue/test-utils';
-import DiffGutterAvatars from '~/diffs/components/diff_gutter_avatars.vue';
-import { mapInline } from '~/diffs/components/diff_row_utils';
-import InlineDiffTableRow from '~/diffs/components/inline_diff_table_row.vue';
-import { createStore } from '~/mr_notes/stores';
-import { findInteropAttributes } from '../find_interop_attributes';
-import discussionsMockData from '../mock_data/diff_discussions';
-import diffFileMockData from '../mock_data/diff_file';
-
-const TEST_USER_ID = 'abc123';
-const TEST_USER = { id: TEST_USER_ID };
-
-describe('InlineDiffTableRow', () => {
- let wrapper;
- let store;
- const mockDiffContent = {
- diffFile: diffFileMockData,
- shouldRenderDraftRow: jest.fn(),
- hasParallelDraftLeft: jest.fn(),
- hasParallelDraftRight: jest.fn(),
- draftForLine: jest.fn(),
- };
-
- const applyMap = mapInline(mockDiffContent);
- const thisLine = applyMap(diffFileMockData.highlighted_diff_lines[0]);
-
- const createComponent = (props = {}, propsStore = store) => {
- wrapper = shallowMount(InlineDiffTableRow, {
- store: propsStore,
- propsData: {
- line: thisLine,
- fileHash: diffFileMockData.file_hash,
- filePath: diffFileMockData.file_path,
- contextLinesPath: 'contextLinesPath',
- isHighlighted: false,
- ...props,
- },
- });
- };
-
- beforeEach(() => {
- store = createStore();
- store.state.notes.userData = TEST_USER;
- });
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- it('does not add hll class to line content when line does not match highlighted row', () => {
- createComponent();
- expect(wrapper.find('.line_content').classes('hll')).toBe(false);
- });
-
- it('adds hll class to lineContent when line is the highlighted row', () => {
- store.state.diffs.highlightedRow = thisLine.line_code;
- createComponent({}, store);
- expect(wrapper.find('.line_content').classes('hll')).toBe(true);
- });
-
- it('adds hll class to lineContent when line is part of a multiline comment', () => {
- createComponent({ isCommented: true });
- expect(wrapper.find('.line_content').classes('hll')).toBe(true);
- });
-
- describe('sets coverage title and class', () => {
- it('for lines with coverage', () => {
- const name = diffFileMockData.file_path;
- const line = thisLine.new_line;
-
- store.state.diffs.coverageFiles = { files: { [name]: { [line]: 5 } } };
- createComponent({}, store);
- const coverage = wrapper.find('.line-coverage');
-
- expect(coverage.attributes('title')).toContain('Test coverage: 5 hits');
- expect(coverage.classes('coverage')).toBe(true);
- });
-
- it('for lines without coverage', () => {
- const name = diffFileMockData.file_path;
- const line = thisLine.new_line;
-
- store.state.diffs.coverageFiles = { files: { [name]: { [line]: 0 } } };
- createComponent({}, store);
- const coverage = wrapper.find('.line-coverage');
-
- expect(coverage.attributes('title')).toContain('No test coverage');
- expect(coverage.classes('no-coverage')).toBe(true);
- });
-
- it('for unknown lines', () => {
- store.state.diffs.coverageFiles = {};
- createComponent({}, store);
-
- const coverage = wrapper.find('.line-coverage');
-
- expect(coverage.attributes('title')).toBeUndefined();
- expect(coverage.classes('coverage')).toBe(false);
- expect(coverage.classes('no-coverage')).toBe(false);
- });
- });
-
- describe('Table Cells', () => {
- const findNewTd = () => wrapper.find({ ref: 'newTd' });
- const findOldTd = () => wrapper.find({ ref: 'oldTd' });
-
- describe('td', () => {
- it('highlights when isHighlighted true', () => {
- store.state.diffs.highlightedRow = thisLine.line_code;
- createComponent({}, store);
-
- expect(findNewTd().classes()).toContain('hll');
- expect(findOldTd().classes()).toContain('hll');
- });
-
- it('does not highlight when isHighlighted false', () => {
- createComponent();
-
- expect(findNewTd().classes()).not.toContain('hll');
- expect(findOldTd().classes()).not.toContain('hll');
- });
- });
-
- describe('comment button', () => {
- const findNoteButton = () => wrapper.find({ ref: 'addDiffNoteButton' });
-
- it.each`
- userData | expectation
- ${TEST_USER} | ${true}
- ${null} | ${false}
- `('exists is $expectation - with userData ($userData)', ({ userData, expectation }) => {
- store.state.notes.userData = userData;
- createComponent({}, store);
-
- expect(findNoteButton().exists()).toBe(expectation);
- });
-
- it.each`
- isHover | line | expectation
- ${true} | ${{ ...thisLine, discussions: [] }} | ${true}
- ${false} | ${{ ...thisLine, discussions: [] }} | ${false}
- ${true} | ${{ ...thisLine, type: 'context', discussions: [] }} | ${false}
- ${true} | ${{ ...thisLine, type: 'old-nonewline', discussions: [] }} | ${false}
- ${true} | ${{ ...thisLine, discussions: [{}] }} | ${false}
- `('visible is $expectation - line ($line)', ({ isHover, line, expectation }) => {
- createComponent({ line: applyMap(line) });
- wrapper.setData({ isHover });
-
- return wrapper.vm.$nextTick().then(() => {
- expect(findNoteButton().isVisible()).toBe(expectation);
- });
- });
-
- it.each`
- disabled | commentsDisabled
- ${'disabled'} | ${true}
- ${undefined} | ${false}
- `(
- 'has attribute disabled=$disabled when the outer component has prop commentsDisabled=$commentsDisabled',
- ({ disabled, commentsDisabled }) => {
- createComponent({
- line: applyMap({ ...thisLine, commentsDisabled }),
- });
-
- wrapper.setData({ isHover: true });
-
- return wrapper.vm.$nextTick().then(() => {
- expect(findNoteButton().attributes('disabled')).toBe(disabled);
- });
- },
- );
-
- const symlinkishFileTooltip =
- 'Commenting on symbolic links that replace or are replaced by files is currently not supported.';
- const realishFileTooltip =
- 'Commenting on files that replace or are replaced by symbolic links is currently not supported.';
- const otherFileTooltip = 'Add a comment to this line';
- const findTooltip = () => wrapper.find({ ref: 'addNoteTooltip' });
-
- it.each`
- tooltip | commentsDisabled
- ${symlinkishFileTooltip} | ${{ wasSymbolic: true }}
- ${symlinkishFileTooltip} | ${{ isSymbolic: true }}
- ${realishFileTooltip} | ${{ wasReal: true }}
- ${realishFileTooltip} | ${{ isReal: true }}
- ${otherFileTooltip} | ${false}
- `(
- 'has the correct tooltip when commentsDisabled=$commentsDisabled',
- ({ tooltip, commentsDisabled }) => {
- createComponent({
- line: applyMap({ ...thisLine, commentsDisabled }),
- });
-
- wrapper.setData({ isHover: true });
-
- return wrapper.vm.$nextTick().then(() => {
- expect(findTooltip().attributes('title')).toBe(tooltip);
- });
- },
- );
- });
-
- describe('line number', () => {
- const findLineNumberOld = () => wrapper.find({ ref: 'lineNumberRefOld' });
- const findLineNumberNew = () => wrapper.find({ ref: 'lineNumberRefNew' });
-
- it('renders line numbers in correct cells', () => {
- createComponent();
-
- expect(findLineNumberOld().exists()).toBe(false);
- expect(findLineNumberNew().exists()).toBe(true);
- });
-
- describe('with lineNumber prop', () => {
- const TEST_LINE_CODE = 'LC_42';
- const TEST_LINE_NUMBER = 1;
-
- describe.each`
- lineProps | findLineNumber | expectedHref | expectedClickArg
- ${{ line_code: TEST_LINE_CODE, old_line: TEST_LINE_NUMBER }} | ${findLineNumberOld} | ${`#${TEST_LINE_CODE}`} | ${TEST_LINE_CODE}
- ${{ line_code: undefined, old_line: TEST_LINE_NUMBER }} | ${findLineNumberOld} | ${'#'} | ${undefined}
- ${{ line_code: undefined, left: { line_code: TEST_LINE_CODE }, old_line: TEST_LINE_NUMBER }} | ${findLineNumberOld} | ${'#'} | ${TEST_LINE_CODE}
- ${{ line_code: undefined, right: { line_code: TEST_LINE_CODE }, new_line: TEST_LINE_NUMBER }} | ${findLineNumberNew} | ${'#'} | ${TEST_LINE_CODE}
- `(
- 'with line ($lineProps)',
- ({ lineProps, findLineNumber, expectedHref, expectedClickArg }) => {
- beforeEach(() => {
- jest.spyOn(store, 'dispatch').mockImplementation();
- createComponent({
- line: applyMap({ ...thisLine, ...lineProps }),
- });
- });
-
- it('renders', () => {
- expect(findLineNumber().exists()).toBe(true);
- expect(findLineNumber().attributes()).toEqual({
- href: expectedHref,
- 'data-linenumber': TEST_LINE_NUMBER.toString(),
- });
- });
-
- it('on click, dispatches setHighlightedRow', () => {
- expect(store.dispatch).toHaveBeenCalledTimes(1);
-
- findLineNumber().trigger('click');
-
- expect(store.dispatch).toHaveBeenCalledWith(
- 'diffs/setHighlightedRow',
- expectedClickArg,
- );
- expect(store.dispatch).toHaveBeenCalledTimes(2);
- });
- },
- );
- });
- });
-
- describe('diff-gutter-avatars', () => {
- const TEST_LINE_CODE = 'LC_42';
- const TEST_FILE_HASH = diffFileMockData.file_hash;
- const findAvatars = () => wrapper.find(DiffGutterAvatars);
- let line;
-
- beforeEach(() => {
- jest.spyOn(store, 'dispatch').mockImplementation();
-
- line = {
- line_code: TEST_LINE_CODE,
- type: 'new',
- old_line: null,
- new_line: 1,
- discussions: [{ ...discussionsMockData }],
- discussionsExpanded: true,
- text: '+<span id="LC1" class="line" lang="plaintext"> - Bad dates</span>\n',
- rich_text: '+<span id="LC1" class="line" lang="plaintext"> - Bad dates</span>\n',
- meta_data: null,
- };
- });
-
- describe('with showCommentButton', () => {
- it('renders if line has discussions', () => {
- createComponent({ line: applyMap(line) });
-
- expect(findAvatars().props()).toEqual({
- discussions: line.discussions,
- discussionsExpanded: line.discussionsExpanded,
- });
- });
-
- it('does notrender if line has no discussions', () => {
- line.discussions = [];
- createComponent({ line: applyMap(line) });
-
- expect(findAvatars().exists()).toEqual(false);
- });
-
- it('toggles line discussion', () => {
- createComponent({ line: applyMap(line) });
-
- expect(store.dispatch).toHaveBeenCalledTimes(1);
-
- findAvatars().vm.$emit('toggleLineDiscussions');
-
- expect(store.dispatch).toHaveBeenCalledWith('diffs/toggleLineDiscussions', {
- lineCode: TEST_LINE_CODE,
- fileHash: TEST_FILE_HASH,
- expanded: !line.discussionsExpanded,
- });
- });
- });
- });
- });
-
- describe('interoperability', () => {
- it.each`
- desc | line | expectation
- ${'with type old'} | ${{ ...thisLine, type: 'old', old_line: 3, new_line: 5 }} | ${{ type: 'old', line: '3', oldLine: '3', newLine: '5' }}
- ${'with type new'} | ${{ ...thisLine, type: 'new', old_line: 3, new_line: 5 }} | ${{ type: 'new', line: '5', oldLine: '3', newLine: '5' }}
- `('$desc, sets interop data attributes', ({ line, expectation }) => {
- createComponent({ line });
-
- expect(findInteropAttributes(wrapper)).toEqual(expectation);
- });
- });
-});
diff --git a/spec/frontend/diffs/components/inline_diff_view_spec.js b/spec/frontend/diffs/components/inline_diff_view_spec.js
deleted file mode 100644
index 27834804f77..00000000000
--- a/spec/frontend/diffs/components/inline_diff_view_spec.js
+++ /dev/null
@@ -1,57 +0,0 @@
-import '~/behaviors/markdown/render_gfm';
-import { getByText } from '@testing-library/dom';
-import { mount } from '@vue/test-utils';
-import { mapInline } from '~/diffs/components/diff_row_utils';
-import InlineDiffView from '~/diffs/components/inline_diff_view.vue';
-import { createStore } from '~/mr_notes/stores';
-import discussionsMockData from '../mock_data/diff_discussions';
-import diffFileMockData from '../mock_data/diff_file';
-
-describe('InlineDiffView', () => {
- let wrapper;
- const getDiffFileMock = () => ({ ...diffFileMockData });
- const getDiscussionsMockData = () => [{ ...discussionsMockData }];
- const notesLength = getDiscussionsMockData()[0].notes.length;
-
- const setup = (diffFile, diffLines) => {
- const mockDiffContent = {
- diffFile,
- shouldRenderDraftRow: jest.fn(),
- };
-
- const store = createStore();
-
- store.dispatch('diffs/setInlineDiffViewType');
- wrapper = mount(InlineDiffView, {
- store,
- propsData: {
- diffFile,
- diffLines: diffLines.map(mapInline(mockDiffContent)),
- },
- });
- };
-
- describe('template', () => {
- it('should have rendered diff lines', () => {
- const diffFile = getDiffFileMock();
- setup(diffFile, diffFile.highlighted_diff_lines);
-
- expect(wrapper.findAll('tr.line_holder').length).toEqual(8);
- expect(wrapper.findAll('tr.line_holder.new').length).toEqual(4);
- expect(wrapper.findAll('tr.line_expansion.match').length).toEqual(1);
- getByText(wrapper.element, /Bad dates/i);
- });
-
- it('should render discussions', () => {
- const diffFile = getDiffFileMock();
- diffFile.highlighted_diff_lines[1].discussions = getDiscussionsMockData();
- diffFile.highlighted_diff_lines[1].discussionsExpanded = true;
- setup(diffFile, diffFile.highlighted_diff_lines);
-
- expect(wrapper.findAll('.notes_holder').length).toEqual(1);
- expect(wrapper.findAll('.notes_holder .note').length).toEqual(notesLength + 1);
- getByText(wrapper.element, 'comment 5');
- wrapper.vm.$store.dispatch('setInitialNotes', []);
- });
- });
-});
diff --git a/spec/frontend/diffs/components/parallel_diff_table_row_spec.js b/spec/frontend/diffs/components/parallel_diff_table_row_spec.js
deleted file mode 100644
index ed191d849fd..00000000000
--- a/spec/frontend/diffs/components/parallel_diff_table_row_spec.js
+++ /dev/null
@@ -1,445 +0,0 @@
-import { shallowMount } from '@vue/test-utils';
-import Vue from 'vue';
-import { createComponentWithStore } from 'helpers/vue_mount_component_helper';
-import DiffGutterAvatars from '~/diffs/components/diff_gutter_avatars.vue';
-import { mapParallel } from '~/diffs/components/diff_row_utils';
-import ParallelDiffTableRow from '~/diffs/components/parallel_diff_table_row.vue';
-import { createStore } from '~/mr_notes/stores';
-import { findInteropAttributes } from '../find_interop_attributes';
-import discussionsMockData from '../mock_data/diff_discussions';
-import diffFileMockData from '../mock_data/diff_file';
-
-describe('ParallelDiffTableRow', () => {
- const mockDiffContent = {
- diffFile: diffFileMockData,
- shouldRenderDraftRow: jest.fn(),
- hasParallelDraftLeft: jest.fn(),
- hasParallelDraftRight: jest.fn(),
- draftForLine: jest.fn(),
- };
-
- const applyMap = mapParallel(mockDiffContent);
-
- describe('when one side is empty', () => {
- let wrapper;
- let vm;
- const thisLine = diffFileMockData.parallel_diff_lines[0];
- const rightLine = diffFileMockData.parallel_diff_lines[0].right;
-
- beforeEach(() => {
- wrapper = shallowMount(ParallelDiffTableRow, {
- store: createStore(),
- propsData: {
- line: applyMap(thisLine),
- fileHash: diffFileMockData.file_hash,
- filePath: diffFileMockData.file_path,
- contextLinesPath: 'contextLinesPath',
- isHighlighted: false,
- },
- });
-
- vm = wrapper.vm;
- });
-
- it('does not highlight non empty line content when line does not match highlighted row', (done) => {
- vm.$nextTick()
- .then(() => {
- expect(vm.$el.querySelector('.line_content.right-side').classList).not.toContain('hll');
- })
- .then(done)
- .catch(done.fail);
- });
-
- it('highlights nonempty line content when line is the highlighted row', (done) => {
- vm.$nextTick()
- .then(() => {
- vm.$store.state.diffs.highlightedRow = rightLine.line_code;
-
- return vm.$nextTick();
- })
- .then(() => {
- expect(vm.$el.querySelector('.line_content.right-side').classList).toContain('hll');
- })
- .then(done)
- .catch(done.fail);
- });
-
- it('highlights nonempty line content when line is part of a multiline comment', () => {
- wrapper.setProps({ isCommented: true });
- return vm.$nextTick().then(() => {
- expect(vm.$el.querySelector('.line_content.right-side').classList).toContain('hll');
- });
- });
- });
-
- describe('when both sides have content', () => {
- let vm;
- const thisLine = diffFileMockData.parallel_diff_lines[2];
- const rightLine = diffFileMockData.parallel_diff_lines[2].right;
-
- beforeEach(() => {
- vm = createComponentWithStore(Vue.extend(ParallelDiffTableRow), createStore(), {
- line: applyMap(thisLine),
- fileHash: diffFileMockData.file_hash,
- filePath: diffFileMockData.file_path,
- contextLinesPath: 'contextLinesPath',
- isHighlighted: false,
- }).$mount();
- });
-
- it('does not highlight either line when line does not match highlighted row', (done) => {
- vm.$nextTick()
- .then(() => {
- expect(vm.$el.querySelector('.line_content.right-side').classList).not.toContain('hll');
- expect(vm.$el.querySelector('.line_content.left-side').classList).not.toContain('hll');
- })
- .then(done)
- .catch(done.fail);
- });
-
- it('adds hll class to lineContent when line is the highlighted row', (done) => {
- vm.$nextTick()
- .then(() => {
- vm.$store.state.diffs.highlightedRow = rightLine.line_code;
-
- return vm.$nextTick();
- })
- .then(() => {
- expect(vm.$el.querySelector('.line_content.right-side').classList).toContain('hll');
- expect(vm.$el.querySelector('.line_content.left-side').classList).toContain('hll');
- })
- .then(done)
- .catch(done.fail);
- });
-
- describe('sets coverage title and class', () => {
- it('for lines with coverage', (done) => {
- vm.$nextTick()
- .then(() => {
- const name = diffFileMockData.file_path;
- const line = rightLine.new_line;
-
- vm.$store.state.diffs.coverageFiles = { files: { [name]: { [line]: 5 } } };
-
- return vm.$nextTick();
- })
- .then(() => {
- const coverage = vm.$el.querySelector('.line-coverage.right-side');
-
- expect(coverage.title).toContain('Test coverage: 5 hits');
- expect(coverage.classList).toContain('coverage');
- })
- .then(done)
- .catch(done.fail);
- });
-
- it('for lines without coverage', (done) => {
- vm.$nextTick()
- .then(() => {
- const name = diffFileMockData.file_path;
- const line = rightLine.new_line;
-
- vm.$store.state.diffs.coverageFiles = { files: { [name]: { [line]: 0 } } };
-
- return vm.$nextTick();
- })
- .then(() => {
- const coverage = vm.$el.querySelector('.line-coverage.right-side');
-
- expect(coverage.title).toContain('No test coverage');
- expect(coverage.classList).toContain('no-coverage');
- })
- .then(done)
- .catch(done.fail);
- });
-
- it('for unknown lines', (done) => {
- vm.$nextTick()
- .then(() => {
- vm.$store.state.diffs.coverageFiles = {};
-
- return vm.$nextTick();
- })
- .then(() => {
- const coverage = vm.$el.querySelector('.line-coverage.right-side');
-
- expect(coverage.title).not.toContain('Coverage');
- expect(coverage.classList).not.toContain('coverage');
- expect(coverage.classList).not.toContain('no-coverage');
- })
- .then(done)
- .catch(done.fail);
- });
- });
- });
-
- describe('Table Cells', () => {
- let wrapper;
- let store;
- let thisLine;
- const TEST_USER_ID = 'abc123';
- const TEST_USER = { id: TEST_USER_ID };
-
- const createComponent = (props = {}, propsStore = store, data = {}) => {
- wrapper = shallowMount(ParallelDiffTableRow, {
- store: propsStore,
- propsData: {
- line: thisLine,
- fileHash: diffFileMockData.file_hash,
- filePath: diffFileMockData.file_path,
- contextLinesPath: 'contextLinesPath',
- isHighlighted: false,
- ...props,
- },
- data() {
- return data;
- },
- });
- };
-
- beforeEach(() => {
- // eslint-disable-next-line prefer-destructuring
- thisLine = diffFileMockData.parallel_diff_lines[2];
- store = createStore();
- store.state.notes.userData = TEST_USER;
- });
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- const findNewTd = () => wrapper.find({ ref: 'newTd' });
- const findOldTd = () => wrapper.find({ ref: 'oldTd' });
-
- describe('td', () => {
- it('highlights when isHighlighted true', () => {
- store.state.diffs.highlightedRow = thisLine.left.line_code;
- createComponent({}, store);
-
- expect(findNewTd().classes()).toContain('hll');
- expect(findOldTd().classes()).toContain('hll');
- });
-
- it('does not highlight when isHighlighted false', () => {
- createComponent();
-
- expect(findNewTd().classes()).not.toContain('hll');
- expect(findOldTd().classes()).not.toContain('hll');
- });
- });
-
- describe('comment button', () => {
- const findNoteButton = () => wrapper.find({ ref: 'addDiffNoteButtonLeft' });
-
- it.each`
- hover | line | userData | expectation
- ${true} | ${{}} | ${TEST_USER} | ${true}
- ${true} | ${{ line: { left: null } }} | ${TEST_USER} | ${false}
- ${true} | ${{}} | ${null} | ${false}
- ${false} | ${{}} | ${TEST_USER} | ${false}
- `(
- 'exists is $expectation - with userData ($userData)',
- async ({ hover, line, userData, expectation }) => {
- store.state.notes.userData = userData;
- createComponent(line, store);
- if (hover) await wrapper.find('.line_holder').trigger('mouseover');
-
- expect(findNoteButton().exists()).toBe(expectation);
- },
- );
-
- it.each`
- line | expectation
- ${{ ...thisLine, left: { discussions: [] } }} | ${true}
- ${{ ...thisLine, left: { type: 'context', discussions: [] } }} | ${false}
- ${{ ...thisLine, left: { type: 'old-nonewline', discussions: [] } }} | ${false}
- ${{ ...thisLine, left: { discussions: [{}] } }} | ${false}
- `('visible is $expectation - line ($line)', async ({ line, expectation }) => {
- createComponent({ line: applyMap(line) }, store, {
- isLeftHover: true,
- isCommentButtonRendered: true,
- });
-
- expect(findNoteButton().isVisible()).toBe(expectation);
- });
-
- it.each`
- disabled | commentsDisabled
- ${'disabled'} | ${true}
- ${undefined} | ${false}
- `(
- 'has attribute disabled=$disabled when the outer component has prop commentsDisabled=$commentsDisabled',
- ({ disabled, commentsDisabled }) => {
- thisLine.left.commentsDisabled = commentsDisabled;
- createComponent({ line: { ...thisLine } }, store, {
- isLeftHover: true,
- isCommentButtonRendered: true,
- });
-
- expect(findNoteButton().attributes('disabled')).toBe(disabled);
- },
- );
-
- const symlinkishFileTooltip =
- 'Commenting on symbolic links that replace or are replaced by files is currently not supported.';
- const realishFileTooltip =
- 'Commenting on files that replace or are replaced by symbolic links is currently not supported.';
- const otherFileTooltip = 'Add a comment to this line';
- const findTooltip = () => wrapper.find({ ref: 'addNoteTooltipLeft' });
-
- it.each`
- tooltip | commentsDisabled
- ${symlinkishFileTooltip} | ${{ wasSymbolic: true }}
- ${symlinkishFileTooltip} | ${{ isSymbolic: true }}
- ${realishFileTooltip} | ${{ wasReal: true }}
- ${realishFileTooltip} | ${{ isReal: true }}
- ${otherFileTooltip} | ${false}
- `(
- 'has the correct tooltip when commentsDisabled=$commentsDisabled',
- ({ tooltip, commentsDisabled }) => {
- thisLine.left.commentsDisabled = commentsDisabled;
- createComponent({ line: { ...thisLine } }, store, {
- isLeftHover: true,
- isCommentButtonRendered: true,
- });
-
- expect(findTooltip().attributes('title')).toBe(tooltip);
- },
- );
- });
-
- describe('line number', () => {
- const findLineNumberOld = () => wrapper.find({ ref: 'lineNumberRefOld' });
- const findLineNumberNew = () => wrapper.find({ ref: 'lineNumberRefNew' });
-
- it('renders line numbers in correct cells', () => {
- createComponent();
-
- expect(findLineNumberOld().exists()).toBe(true);
- expect(findLineNumberNew().exists()).toBe(true);
- });
-
- describe('with lineNumber prop', () => {
- const TEST_LINE_CODE = 'LC_42';
- const TEST_LINE_NUMBER = 1;
-
- describe.each`
- lineProps | findLineNumber | expectedHref | expectedClickArg
- ${{ line_code: TEST_LINE_CODE, old_line: TEST_LINE_NUMBER }} | ${findLineNumberOld} | ${`#${TEST_LINE_CODE}`} | ${TEST_LINE_CODE}
- ${{ line_code: undefined, old_line: TEST_LINE_NUMBER }} | ${findLineNumberOld} | ${'#'} | ${undefined}
- `(
- 'with line ($lineProps)',
- ({ lineProps, findLineNumber, expectedHref, expectedClickArg }) => {
- beforeEach(() => {
- jest.spyOn(store, 'dispatch').mockImplementation();
- Object.assign(thisLine.left, lineProps);
- Object.assign(thisLine.right, lineProps);
- createComponent({
- line: applyMap({ ...thisLine }),
- });
- });
-
- it('renders', () => {
- expect(findLineNumber().exists()).toBe(true);
- expect(findLineNumber().attributes()).toEqual({
- href: expectedHref,
- 'data-linenumber': TEST_LINE_NUMBER.toString(),
- });
- });
-
- it('on click, dispatches setHighlightedRow', () => {
- expect(store.dispatch).toHaveBeenCalledTimes(1);
-
- findLineNumber().trigger('click');
-
- expect(store.dispatch).toHaveBeenCalledWith(
- 'diffs/setHighlightedRow',
- expectedClickArg,
- );
- expect(store.dispatch).toHaveBeenCalledTimes(2);
- });
- },
- );
- });
- });
-
- describe('diff-gutter-avatars', () => {
- const TEST_LINE_CODE = 'LC_42';
- const TEST_FILE_HASH = diffFileMockData.file_hash;
- const findAvatars = () => wrapper.find(DiffGutterAvatars);
- let line;
-
- beforeEach(() => {
- jest.spyOn(store, 'dispatch').mockImplementation();
-
- line = applyMap({
- left: {
- line_code: TEST_LINE_CODE,
- type: 'new',
- old_line: null,
- new_line: 1,
- discussions: [{ ...discussionsMockData }],
- discussionsExpanded: true,
- text: '+<span id="LC1" class="line" lang="plaintext"> - Bad dates</span>\n',
- rich_text: '+<span id="LC1" class="line" lang="plaintext"> - Bad dates</span>\n',
- meta_data: null,
- },
- });
- });
-
- describe('with showCommentButton', () => {
- it('renders if line has discussions', () => {
- createComponent({ line });
-
- expect(findAvatars().props()).toEqual({
- discussions: line.left.discussions,
- discussionsExpanded: line.left.discussionsExpanded,
- });
- });
-
- it('does notrender if line has no discussions', () => {
- line.left.discussions = [];
- createComponent({ line: applyMap(line) });
-
- expect(findAvatars().exists()).toEqual(false);
- });
-
- it('toggles line discussion', () => {
- createComponent({ line });
-
- expect(store.dispatch).toHaveBeenCalledTimes(1);
-
- findAvatars().vm.$emit('toggleLineDiscussions');
-
- expect(store.dispatch).toHaveBeenCalledWith('diffs/toggleLineDiscussions', {
- lineCode: TEST_LINE_CODE,
- fileHash: TEST_FILE_HASH,
- expanded: !line.left.discussionsExpanded,
- });
- });
- });
- });
-
- describe('interoperability', () => {
- beforeEach(() => {
- createComponent();
- });
-
- it('adds old side interoperability data attributes', () => {
- expect(findInteropAttributes(wrapper, '.line_content.left-side')).toEqual({
- type: 'old',
- line: thisLine.left.old_line.toString(),
- oldLine: thisLine.left.old_line.toString(),
- });
- });
-
- it('adds new side interoperability data attributes', () => {
- expect(findInteropAttributes(wrapper, '.line_content.right-side')).toEqual({
- type: 'new',
- line: thisLine.right.new_line.toString(),
- newLine: thisLine.right.new_line.toString(),
- });
- });
- });
- });
-});
diff --git a/spec/frontend/diffs/components/parallel_diff_view_spec.js b/spec/frontend/diffs/components/parallel_diff_view_spec.js
deleted file mode 100644
index 452e1f58551..00000000000
--- a/spec/frontend/diffs/components/parallel_diff_view_spec.js
+++ /dev/null
@@ -1,37 +0,0 @@
-import { shallowMount, createLocalVue } from '@vue/test-utils';
-import Vuex from 'vuex';
-import parallelDiffTableRow from '~/diffs/components/parallel_diff_table_row.vue';
-import ParallelDiffView from '~/diffs/components/parallel_diff_view.vue';
-import { createStore } from '~/mr_notes/stores';
-import diffFileMockData from '../mock_data/diff_file';
-
-let wrapper;
-const localVue = createLocalVue();
-
-localVue.use(Vuex);
-
-function factory() {
- const diffFile = { ...diffFileMockData };
- const store = createStore();
-
- wrapper = shallowMount(ParallelDiffView, {
- localVue,
- store,
- propsData: {
- diffFile,
- diffLines: diffFile.parallel_diff_lines,
- },
- });
-}
-
-describe('ParallelDiffView', () => {
- afterEach(() => {
- wrapper.destroy();
- });
-
- it('renders diff lines', () => {
- factory();
-
- expect(wrapper.findAll(parallelDiffTableRow).length).toBe(8);
- });
-});
diff --git a/spec/frontend/diffs/store/actions_spec.js b/spec/frontend/diffs/store/actions_spec.js
index 14f8e090be9..c2e5d07bcfd 100644
--- a/spec/frontend/diffs/store/actions_spec.js
+++ b/spec/frontend/diffs/store/actions_spec.js
@@ -8,7 +8,6 @@ import {
DIFF_VIEW_COOKIE_NAME,
INLINE_DIFF_VIEW_TYPE,
PARALLEL_DIFF_VIEW_TYPE,
- DIFFS_PER_PAGE,
} from '~/diffs/constants';
import {
setBaseConfig,
@@ -154,16 +153,16 @@ describe('DiffsStoreActions', () => {
it('should fetch batch diff files', (done) => {
const endpointBatch = '/fetch/diffs_batch';
- const res1 = { diff_files: [{ file_hash: 'test' }], pagination: { next_page: 2 } };
- const res2 = { diff_files: [{ file_hash: 'test2' }], pagination: {} };
+ const res1 = { diff_files: [{ file_hash: 'test' }], pagination: { total_pages: 7 } };
+ const res2 = { diff_files: [{ file_hash: 'test2' }], pagination: { total_pages: 7 } };
mock
.onGet(
mergeUrlParams(
{
w: '1',
view: 'inline',
- page: 1,
- per_page: DIFFS_PER_PAGE,
+ page: 0,
+ per_page: 5,
},
endpointBatch,
),
@@ -174,8 +173,8 @@ describe('DiffsStoreActions', () => {
{
w: '1',
view: 'inline',
- page: 2,
- per_page: DIFFS_PER_PAGE,
+ page: 5,
+ per_page: 7,
},
endpointBatch,
),
@@ -1020,10 +1019,12 @@ describe('DiffsStoreActions', () => {
const endpointUpdateUser = 'user/prefs';
let putSpy;
let mock;
+ let gon;
beforeEach(() => {
mock = new MockAdapter(axios);
putSpy = jest.spyOn(axios, 'put');
+ gon = window.gon;
mock.onPut(endpointUpdateUser).reply(200, {});
jest.spyOn(eventHub, '$emit').mockImplementation();
@@ -1031,6 +1032,7 @@ describe('DiffsStoreActions', () => {
afterEach(() => {
mock.restore();
+ window.gon = gon;
});
it('commits SET_SHOW_WHITESPACE', (done) => {
@@ -1044,7 +1046,9 @@ describe('DiffsStoreActions', () => {
);
});
- it('saves to the database', async () => {
+ it('saves to the database when the user is logged in', async () => {
+ window.gon = { current_user_id: 12345 };
+
await setShowWhitespace(
{ state: { endpointUpdateUser }, commit() {} },
{ showWhitespace: true, updateDatabase: true },
@@ -1053,6 +1057,17 @@ describe('DiffsStoreActions', () => {
expect(putSpy).toHaveBeenCalledWith(endpointUpdateUser, { show_whitespace_in_diffs: true });
});
+ it('does not try to save to the API if the user is not logged in', async () => {
+ window.gon = {};
+
+ await setShowWhitespace(
+ { state: { endpointUpdateUser }, commit() {} },
+ { showWhitespace: true, updateDatabase: true },
+ );
+
+ expect(putSpy).not.toHaveBeenCalled();
+ });
+
it('emits eventHub event', async () => {
await setShowWhitespace(
{ state: {}, commit() {} },
diff --git a/spec/frontend/diffs/store/getters_versions_dropdowns_spec.js b/spec/frontend/diffs/store/getters_versions_dropdowns_spec.js
index dbef547c297..99f13a1c84c 100644
--- a/spec/frontend/diffs/store/getters_versions_dropdowns_spec.js
+++ b/spec/frontend/diffs/store/getters_versions_dropdowns_spec.js
@@ -54,7 +54,7 @@ describe('Compare diff version dropdowns', () => {
Object.defineProperty(window, 'location', {
writable: true,
- value: { href: `https://example.gitlab.com${diffHeadParam}` },
+ value: { search: diffHeadParam },
});
expectedFirstVersion = {
diff --git a/spec/frontend/editor/editor_ci_schema_ext_spec.js b/spec/frontend/editor/source_editor_ci_schema_ext_spec.js
index 2f0ecfb151e..07ac080fe08 100644
--- a/spec/frontend/editor/editor_ci_schema_ext_spec.js
+++ b/spec/frontend/editor/source_editor_ci_schema_ext_spec.js
@@ -1,8 +1,8 @@
import { languages } from 'monaco-editor';
import { TEST_HOST } from 'helpers/test_constants';
import { EXTENSION_CI_SCHEMA_FILE_NAME_MATCH } from '~/editor/constants';
-import EditorLite from '~/editor/editor_lite';
-import { CiSchemaExtension } from '~/editor/extensions/editor_ci_schema_ext';
+import { CiSchemaExtension } from '~/editor/extensions/source_editor_ci_schema_ext';
+import SourceEditor from '~/editor/source_editor';
const mockRef = 'AABBCCDD';
@@ -17,7 +17,7 @@ describe('~/editor/editor_ci_config_ext', () => {
const createMockEditor = ({ blobPath = defaultBlobPath } = {}) => {
setFixtures('<div id="editor"></div>');
editorEl = document.getElementById('editor');
- editor = new EditorLite();
+ editor = new SourceEditor();
instance = editor.createInstance({
el: editorEl,
blobPath,
diff --git a/spec/frontend/editor/editor_lite_extension_base_spec.js b/spec/frontend/editor/source_editor_extension_base_spec.js
index 59e1b8968eb..352db9d0d51 100644
--- a/spec/frontend/editor/editor_lite_extension_base_spec.js
+++ b/spec/frontend/editor/source_editor_extension_base_spec.js
@@ -5,7 +5,7 @@ import {
EDITOR_TYPE_CODE,
EDITOR_TYPE_DIFF,
} from '~/editor/constants';
-import { EditorLiteExtension } from '~/editor/extensions/editor_lite_extension_base';
+import { SourceEditorExtension } from '~/editor/extensions/source_editor_extension_base';
jest.mock('~/helpers/startup_css_helper', () => {
return {
@@ -22,7 +22,7 @@ jest.mock('~/helpers/startup_css_helper', () => {
};
});
-describe('The basis for an Editor Lite extension', () => {
+describe('The basis for an Source Editor extension', () => {
const defaultLine = 3;
let ext;
let event;
@@ -63,7 +63,7 @@ describe('The basis for an Editor Lite extension', () => {
const instance = {
layout: jest.fn(),
};
- ext = new EditorLiteExtension({ instance });
+ ext = new SourceEditorExtension({ instance });
expect(instance.layout).not.toHaveBeenCalled();
// We're waiting for the waitForCSSLoaded mock to kick in
@@ -79,7 +79,7 @@ describe('The basis for an Editor Lite extension', () => {
${'does not fail if both instance and the options are omitted'} | ${undefined} | ${undefined}
${'throws if only options are passed'} | ${undefined} | ${defaultOptions}
`('$description', ({ instance, options } = {}) => {
- EditorLiteExtension.deferRerender = jest.fn();
+ SourceEditorExtension.deferRerender = jest.fn();
const originalInstance = { ...instance };
if (instance) {
@@ -88,54 +88,54 @@ describe('The basis for an Editor Lite extension', () => {
expect(instance[prop]).toBeUndefined();
});
// Both instance and options are passed
- ext = new EditorLiteExtension({ instance, ...options });
+ ext = new SourceEditorExtension({ instance, ...options });
Object.entries(options).forEach(([prop, value]) => {
expect(ext[prop]).toBeUndefined();
expect(instance[prop]).toBe(value);
});
} else {
- ext = new EditorLiteExtension({ instance });
+ ext = new SourceEditorExtension({ instance });
expect(instance).toEqual(originalInstance);
}
} else if (options) {
// Options are passed without instance
expect(() => {
- ext = new EditorLiteExtension({ ...options });
+ ext = new SourceEditorExtension({ ...options });
}).toThrow(ERROR_INSTANCE_REQUIRED_FOR_EXTENSION);
} else {
// Neither options nor instance are passed
expect(() => {
- ext = new EditorLiteExtension();
+ ext = new SourceEditorExtension();
}).not.toThrow();
}
});
it('initializes the line highlighting', () => {
- EditorLiteExtension.deferRerender = jest.fn();
- const spy = jest.spyOn(EditorLiteExtension, 'highlightLines');
- ext = new EditorLiteExtension({ instance: {} });
+ SourceEditorExtension.deferRerender = jest.fn();
+ const spy = jest.spyOn(SourceEditorExtension, 'highlightLines');
+ ext = new SourceEditorExtension({ instance: {} });
expect(spy).toHaveBeenCalled();
});
it('sets up the line linking for code instance', () => {
- EditorLiteExtension.deferRerender = jest.fn();
- const spy = jest.spyOn(EditorLiteExtension, 'setupLineLinking');
+ SourceEditorExtension.deferRerender = jest.fn();
+ const spy = jest.spyOn(SourceEditorExtension, 'setupLineLinking');
const instance = {
getEditorType: jest.fn().mockReturnValue(EDITOR_TYPE_CODE),
onMouseMove: jest.fn(),
onMouseDown: jest.fn(),
};
- ext = new EditorLiteExtension({ instance });
+ ext = new SourceEditorExtension({ instance });
expect(spy).toHaveBeenCalledWith(instance);
});
it('does not set up the line linking for diff instance', () => {
- EditorLiteExtension.deferRerender = jest.fn();
- const spy = jest.spyOn(EditorLiteExtension, 'setupLineLinking');
+ SourceEditorExtension.deferRerender = jest.fn();
+ const spy = jest.spyOn(SourceEditorExtension, 'setupLineLinking');
const instance = {
getEditorType: jest.fn().mockReturnValue(EDITOR_TYPE_DIFF),
};
- ext = new EditorLiteExtension({ instance });
+ ext = new SourceEditorExtension({ instance });
expect(spy).not.toHaveBeenCalled();
});
});
@@ -172,7 +172,7 @@ describe('The basis for an Editor Lite extension', () => {
${'does not highlight if hash is incomplete 2'} | ${'#L-'} | ${false} | ${null}
`('$desc', ({ hash, shouldReveal, expectedRange } = {}) => {
window.location.hash = hash;
- EditorLiteExtension.highlightLines(instance);
+ SourceEditorExtension.highlightLines(instance);
if (!shouldReveal) {
expect(revealSpy).not.toHaveBeenCalled();
expect(decorationsSpy).not.toHaveBeenCalled();
@@ -194,7 +194,7 @@ describe('The basis for an Editor Lite extension', () => {
decorationsSpy.mockReturnValue('foo');
window.location.hash = '#L10';
expect(instance.lineDecorations).toBeUndefined();
- EditorLiteExtension.highlightLines(instance);
+ SourceEditorExtension.highlightLines(instance);
expect(instance.lineDecorations).toBe('foo');
});
});
@@ -208,7 +208,7 @@ describe('The basis for an Editor Lite extension', () => {
};
beforeEach(() => {
- EditorLiteExtension.onMouseMoveHandler(event); // generate the anchor
+ SourceEditorExtension.onMouseMoveHandler(event); // generate the anchor
});
it.each`
@@ -216,7 +216,7 @@ describe('The basis for an Editor Lite extension', () => {
${'onMouseMove'} | ${instance.onMouseMove}
${'onMouseDown'} | ${instance.onMouseDown}
`('sets up the $desc listener', ({ spy } = {}) => {
- EditorLiteExtension.setupLineLinking(instance);
+ SourceEditorExtension.setupLineLinking(instance);
expect(spy).toHaveBeenCalled();
});
@@ -230,7 +230,7 @@ describe('The basis for an Editor Lite extension', () => {
fn(event);
});
- EditorLiteExtension.setupLineLinking(instance);
+ SourceEditorExtension.setupLineLinking(instance);
if (shouldRemove) {
expect(instance.deltaDecorations).toHaveBeenCalledWith(instance.lineDecorations, []);
} else {
@@ -241,7 +241,7 @@ describe('The basis for an Editor Lite extension', () => {
describe('onMouseMoveHandler', () => {
it('stops propagation for contextmenu event on the generated anchor', () => {
- EditorLiteExtension.onMouseMoveHandler(event);
+ SourceEditorExtension.onMouseMoveHandler(event);
const anchor = findLine(defaultLine).querySelector('a');
const contextMenuEvent = new Event('contextmenu');
@@ -253,27 +253,27 @@ describe('The basis for an Editor Lite extension', () => {
it('creates an anchor if it does not exist yet', () => {
expect(findLine(defaultLine).querySelector('a')).toBe(null);
- EditorLiteExtension.onMouseMoveHandler(event);
+ SourceEditorExtension.onMouseMoveHandler(event);
expect(findLine(defaultLine).querySelector('a')).not.toBe(null);
});
it('does not create a new anchor if it exists', () => {
- EditorLiteExtension.onMouseMoveHandler(event);
+ SourceEditorExtension.onMouseMoveHandler(event);
expect(findLine(defaultLine).querySelector('a')).not.toBe(null);
- EditorLiteExtension.createAnchor = jest.fn();
- EditorLiteExtension.onMouseMoveHandler(event);
- expect(EditorLiteExtension.createAnchor).not.toHaveBeenCalled();
+ SourceEditorExtension.createAnchor = jest.fn();
+ SourceEditorExtension.onMouseMoveHandler(event);
+ expect(SourceEditorExtension.createAnchor).not.toHaveBeenCalled();
expect(findLine(defaultLine).querySelectorAll('a')).toHaveLength(1);
});
it('does not create a link if the event is triggered on a wrong node', () => {
setFixtures('<div class="wrong-class">3</div>');
- EditorLiteExtension.createAnchor = jest.fn();
+ SourceEditorExtension.createAnchor = jest.fn();
const wrongEvent = generateEventMock({ el: document.querySelector('.wrong-class') });
- EditorLiteExtension.onMouseMoveHandler(wrongEvent);
- expect(EditorLiteExtension.createAnchor).not.toHaveBeenCalled();
+ SourceEditorExtension.onMouseMoveHandler(wrongEvent);
+ expect(SourceEditorExtension.createAnchor).not.toHaveBeenCalled();
});
});
});
diff --git a/spec/frontend/editor/editor_markdown_ext_spec.js b/spec/frontend/editor/source_editor_markdown_ext_spec.js
index 3f64dcfd7a0..943e21250b4 100644
--- a/spec/frontend/editor/editor_markdown_ext_spec.js
+++ b/spec/frontend/editor/source_editor_markdown_ext_spec.js
@@ -1,8 +1,8 @@
import { Range, Position } from 'monaco-editor';
-import EditorLite from '~/editor/editor_lite';
-import { EditorMarkdownExtension } from '~/editor/extensions/editor_markdown_ext';
+import { EditorMarkdownExtension } from '~/editor/extensions/source_editor_markdown_ext';
+import SourceEditor from '~/editor/source_editor';
-describe('Markdown Extension for Editor Lite', () => {
+describe('Markdown Extension for Source Editor', () => {
let editor;
let instance;
let editorEl;
@@ -25,7 +25,7 @@ describe('Markdown Extension for Editor Lite', () => {
beforeEach(() => {
setFixtures('<div id="editor" data-editor-loading></div>');
editorEl = document.getElementById('editor');
- editor = new EditorLite();
+ editor = new SourceEditor();
instance = editor.createInstance({
el: editorEl,
blobPath: filePath,
diff --git a/spec/frontend/editor/editor_lite_spec.js b/spec/frontend/editor/source_editor_spec.js
index 815457e012f..d87d373c952 100644
--- a/spec/frontend/editor/editor_lite_spec.js
+++ b/spec/frontend/editor/source_editor_spec.js
@@ -2,12 +2,12 @@
import { editor as monacoEditor, languages as monacoLanguages } from 'monaco-editor';
import waitForPromises from 'helpers/wait_for_promises';
import {
- EDITOR_LITE_INSTANCE_ERROR_NO_EL,
+ SOURCE_EDITOR_INSTANCE_ERROR_NO_EL,
URI_PREFIX,
EDITOR_READY_EVENT,
} from '~/editor/constants';
-import EditorLite from '~/editor/editor_lite';
-import { EditorLiteExtension } from '~/editor/extensions/editor_lite_extension_base';
+import { SourceEditorExtension } from '~/editor/extensions/source_editor_extension_base';
+import SourceEditor from '~/editor/source_editor';
import { DEFAULT_THEME, themes } from '~/ide/lib/themes';
import { joinPaths } from '~/lib/utils/url_utility';
@@ -25,7 +25,7 @@ describe('Base editor', () => {
setFixtures('<div id="editor" data-editor-loading></div>');
editorEl = document.getElementById('editor');
defaultArguments = { el: editorEl, blobPath, blobContent, blobGlobalId };
- editor = new EditorLite();
+ editor = new SourceEditor();
});
afterEach(() => {
@@ -49,7 +49,7 @@ describe('Base editor', () => {
expect(editorEl.dataset.editorLoading).toBeUndefined();
});
- describe('instance of the Editor Lite', () => {
+ describe('instance of the Source Editor', () => {
let modelSpy;
let instanceSpy;
const setModel = jest.fn();
@@ -58,7 +58,7 @@ describe('Base editor', () => {
modelSpy = jest.spyOn(monacoEditor, 'createModel').mockImplementation(() => res);
};
const mockDecorateInstance = (decorations = {}) => {
- jest.spyOn(EditorLite, 'convertMonacoToELInstance').mockImplementation((inst) => {
+ jest.spyOn(SourceEditor, 'convertMonacoToELInstance').mockImplementation((inst) => {
return Object.assign(inst, decorations);
});
};
@@ -76,11 +76,11 @@ describe('Base editor', () => {
mockDecorateInstance();
expect(() => {
editor.createInstance();
- }).toThrow(EDITOR_LITE_INSTANCE_ERROR_NO_EL);
+ }).toThrow(SOURCE_EDITOR_INSTANCE_ERROR_NO_EL);
expect(modelSpy).not.toHaveBeenCalled();
expect(instanceSpy).not.toHaveBeenCalled();
- expect(EditorLite.convertMonacoToELInstance).not.toHaveBeenCalled();
+ expect(SourceEditor.convertMonacoToELInstance).not.toHaveBeenCalled();
});
it('creates model to be supplied to Monaco editor', () => {
@@ -246,7 +246,7 @@ describe('Base editor', () => {
let editorEl2;
let inst1;
let inst2;
- const readOnlyIndex = '68'; // readOnly option has the internal index of 68 in the editor's options
+ const readOnlyIndex = '78'; // readOnly option has the internal index of 78 in the editor's options
beforeEach(() => {
setFixtures('<div id="editor1"></div><div id="editor2"></div>');
@@ -261,7 +261,7 @@ describe('Base editor', () => {
blobPath,
};
- editor = new EditorLite();
+ editor = new SourceEditor();
instanceSpy = jest.spyOn(monacoEditor, 'create');
});
@@ -304,7 +304,7 @@ describe('Base editor', () => {
});
it('shares global editor options among all instances', () => {
- editor = new EditorLite({
+ editor = new SourceEditor({
readOnly: true,
});
@@ -316,7 +316,7 @@ describe('Base editor', () => {
});
it('allows overriding editor options on the instance level', () => {
- editor = new EditorLite({
+ editor = new SourceEditor({
readOnly: true,
});
inst1 = editor.createInstance({
@@ -410,7 +410,7 @@ describe('Base editor', () => {
return WithStaticMethod.computeBoo(this.base);
}
}
- class WithStaticMethodExtended extends EditorLiteExtension {
+ class WithStaticMethodExtended extends SourceEditorExtension {
static computeBoo(a) {
return a + 1;
}
@@ -546,7 +546,7 @@ describe('Base editor', () => {
beforeEach(() => {
editorExtensionSpy = jest
- .spyOn(EditorLite, 'pushToImportsArray')
+ .spyOn(SourceEditor, 'pushToImportsArray')
.mockImplementation((arr) => {
arr.push(
Promise.resolve({
@@ -593,7 +593,7 @@ describe('Base editor', () => {
const useSpy = jest.fn().mockImplementation(() => {
calls.push('use');
});
- jest.spyOn(EditorLite, 'convertMonacoToELInstance').mockImplementation((inst) => {
+ jest.spyOn(SourceEditor, 'convertMonacoToELInstance').mockImplementation((inst) => {
const decoratedInstance = inst;
decoratedInstance.use = useSpy;
return decoratedInstance;
@@ -664,7 +664,7 @@ describe('Base editor', () => {
it('sets default syntax highlighting theme', () => {
const expectedTheme = themes.find((t) => t.name === DEFAULT_THEME);
- editor = new EditorLite();
+ editor = new SourceEditor();
expect(themeDefineSpy).toHaveBeenCalledWith(DEFAULT_THEME, expectedTheme.data);
expect(themeSetSpy).toHaveBeenCalledWith(DEFAULT_THEME);
@@ -676,7 +676,7 @@ describe('Base editor', () => {
expect(expectedTheme.name).not.toBe(DEFAULT_THEME);
window.gon.user_color_scheme = expectedTheme.name;
- editor = new EditorLite();
+ editor = new SourceEditor();
expect(themeDefineSpy).toHaveBeenCalledWith(expectedTheme.name, expectedTheme.data);
expect(themeSetSpy).toHaveBeenCalledWith(expectedTheme.name);
@@ -687,7 +687,7 @@ describe('Base editor', () => {
const nonExistentTheme = { name };
window.gon.user_color_scheme = nonExistentTheme.name;
- editor = new EditorLite();
+ editor = new SourceEditor();
expect(themeDefineSpy).not.toHaveBeenCalled();
expect(themeSetSpy).toHaveBeenCalledWith(DEFAULT_THEME);
diff --git a/spec/frontend/emoji/awards_app/store/actions_spec.js b/spec/frontend/emoji/awards_app/store/actions_spec.js
index e96920d1112..02b643244d2 100644
--- a/spec/frontend/emoji/awards_app/store/actions_spec.js
+++ b/spec/frontend/emoji/awards_app/store/actions_spec.js
@@ -5,6 +5,7 @@ import * as actions from '~/emoji/awards_app/store/actions';
import axios from '~/lib/utils/axios_utils';
jest.mock('@sentry/browser');
+jest.mock('~/vue_shared/plugins/global_toast');
describe('Awards app actions', () => {
afterEach(() => {
diff --git a/spec/frontend/environment.js b/spec/frontend/environment.js
index d1bc11538a3..29aa416149c 100644
--- a/spec/frontend/environment.js
+++ b/spec/frontend/environment.js
@@ -43,6 +43,9 @@ class CustomEnvironment extends JSDOMEnvironment {
};
this.global.IS_EE = IS_EE;
+ // Set up global `gl` object
+ this.global.gl = {};
+
this.rejectedPromises = [];
this.global.promiseRejectionHandler = (error) => {
@@ -67,6 +70,24 @@ class CustomEnvironment extends JSDOMEnvironment {
getEntriesByName: () => [],
});
+ //
+ // Monaco-related environment variables
+ //
+ this.global.MonacoEnvironment = { globalAPI: true };
+ Object.defineProperty(this.global, 'matchMedia', {
+ writable: true,
+ value: (query) => ({
+ matches: false,
+ media: query,
+ onchange: null,
+ addListener: () => null, // deprecated
+ removeListener: () => null, // deprecated
+ addEventListener: () => null,
+ removeEventListener: () => null,
+ dispatchEvent: () => null,
+ }),
+ });
+
this.global.PerformanceObserver = class {
/* eslint-disable no-useless-constructor, no-unused-vars, no-empty-function, class-methods-use-this */
constructor(callback) {}
diff --git a/spec/frontend/environments/environment_item_spec.js b/spec/frontend/environments/environment_item_spec.js
index 09ab1223fd1..62806c9e44c 100644
--- a/spec/frontend/environments/environment_item_spec.js
+++ b/spec/frontend/environments/environment_item_spec.js
@@ -285,6 +285,17 @@ describe('Environment item', () => {
it('should not render the "Upcoming deployment" column', () => {
expect(findUpcomingDeployment().exists()).toBe(false);
});
+
+ it('should set the name cell to be full width', () => {
+ expect(wrapper.find('[data-testid="environment-name-cell"]').classes('section-100')).toBe(
+ true,
+ );
+ });
+
+ it('should hide non-folder properties', () => {
+ expect(wrapper.find('[data-testid="environment-deployment-id-cell"]').exists()).toBe(false);
+ expect(wrapper.find('[data-testid="environment-build-cell"]').exists()).toBe(false);
+ });
});
describe('When environment can be deleted', () => {
diff --git a/spec/frontend/environments/environments_app_spec.js b/spec/frontend/environments/environments_app_spec.js
index 542cf58b079..1abdeff614c 100644
--- a/spec/frontend/environments/environments_app_spec.js
+++ b/spec/frontend/environments/environments_app_spec.js
@@ -1,3 +1,4 @@
+import { GlTabs } from '@gitlab/ui';
import { mount, shallowMount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
@@ -7,6 +8,7 @@ import EmptyState from '~/environments/components/empty_state.vue';
import EnableReviewAppModal from '~/environments/components/enable_review_app_modal.vue';
import EnvironmentsApp from '~/environments/components/environments_app.vue';
import axios from '~/lib/utils/axios_utils';
+import * as urlUtils from '~/lib/utils/url_utility';
import { environment, folder } from './mock_data';
describe('Environment', () => {
@@ -264,4 +266,18 @@ describe('Environment', () => {
});
});
});
+
+ describe('tabs', () => {
+ beforeEach(() => {
+ mockRequest(200, { environments: [] });
+ jest
+ .spyOn(urlUtils, 'getParameterByName')
+ .mockImplementation((param) => (param === 'scope' ? 'stopped' : null));
+ return createWrapper(true);
+ });
+
+ it('selects the tab for the parameter', () => {
+ expect(wrapper.findComponent(GlTabs).attributes('value')).toBe('1');
+ });
+ });
});
diff --git a/spec/frontend/feature_flags/components/edit_feature_flag_spec.js b/spec/frontend/feature_flags/components/edit_feature_flag_spec.js
index 0948b08f942..799b567a2c0 100644
--- a/spec/frontend/feature_flags/components/edit_feature_flag_spec.js
+++ b/spec/frontend/feature_flags/components/edit_feature_flag_spec.js
@@ -1,21 +1,16 @@
import { GlToggle, GlAlert } from '@gitlab/ui';
-import { createLocalVue, shallowMount } from '@vue/test-utils';
+import { shallowMount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
+import Vue from 'vue';
import Vuex from 'vuex';
import { mockTracking } from 'helpers/tracking_helper';
import { TEST_HOST } from 'spec/test_constants';
import EditFeatureFlag from '~/feature_flags/components/edit_feature_flag.vue';
import Form from '~/feature_flags/components/form.vue';
-import { LEGACY_FLAG, NEW_VERSION_FLAG } from '~/feature_flags/constants';
import createStore from '~/feature_flags/store/edit';
import axios from '~/lib/utils/axios_utils';
-const localVue = createLocalVue();
-localVue.use(Vuex);
-
-const userCalloutId = 'feature_flags_new_version';
-const userCalloutsPath = `${TEST_HOST}/user_callouts`;
-
+Vue.use(Vuex);
describe('Edit feature flag form', () => {
let wrapper;
let mock;
@@ -25,20 +20,14 @@ describe('Edit feature flag form', () => {
endpoint: `${TEST_HOST}/feature_flags.json`,
});
- const factory = (opts = {}) => {
+ const factory = (provide = {}) => {
if (wrapper) {
wrapper.destroy();
wrapper = null;
}
wrapper = shallowMount(EditFeatureFlag, {
- localVue,
store,
- provide: {
- showUserCallout: true,
- userCalloutId,
- userCalloutsPath,
- ...opts,
- },
+ provide,
});
};
@@ -52,18 +41,8 @@ describe('Edit feature flag form', () => {
updated_at: '2019-01-17T17:27:39.778Z',
name: 'feature_flag',
description: '',
- version: LEGACY_FLAG,
edit_path: '/h5bp/html5-boilerplate/-/feature_flags/21/edit',
destroy_path: '/h5bp/html5-boilerplate/-/feature_flags/21',
- scopes: [
- {
- id: 21,
- active: false,
- environment_scope: '*',
- created_at: '2019-01-17T17:27:39.778Z',
- updated_at: '2019-01-17T17:27:39.778Z',
- },
- ],
});
factory();
setImmediate(() => done());
@@ -74,9 +53,7 @@ describe('Edit feature flag form', () => {
mock.restore();
});
- const findAlert = () => wrapper.find(GlAlert);
- const findWarningGlAlert = () =>
- wrapper.findAll(GlAlert).filter((c) => c.props('variant') === 'warning');
+ const findWarningGlAlert = () => wrapper.findComponent(GlAlert);
it('should display the iid', () => {
expect(wrapper.find('h3').text()).toContain('^5');
@@ -86,21 +63,13 @@ describe('Edit feature flag form', () => {
expect(wrapper.find(GlToggle).exists()).toBe(true);
});
- it('should set the value of the toggle to whether or not the flag is active', () => {
- expect(wrapper.find(GlToggle).props('value')).toBe(true);
- });
-
- it('should alert users the flag is read-only', () => {
- expect(findAlert().text()).toContain('GitLab is moving to a new way of managing feature flags');
- });
-
describe('with error', () => {
it('should render the error', () => {
store.dispatch('receiveUpdateFeatureFlagError', { message: ['The name is required'] });
return wrapper.vm.$nextTick(() => {
const warningGlAlert = findWarningGlAlert();
- expect(warningGlAlert.at(1).exists()).toEqual(true);
- expect(warningGlAlert.at(1).text()).toContain('The name is required');
+ expect(warningGlAlert.exists()).toEqual(true);
+ expect(warningGlAlert.text()).toContain('The name is required');
});
});
});
@@ -114,32 +83,6 @@ describe('Edit feature flag form', () => {
expect(wrapper.find(Form).exists()).toEqual(true);
});
- it('should set the version of the form from the feature flag', () => {
- expect(wrapper.find(Form).props('version')).toBe(LEGACY_FLAG);
-
- mock.resetHandlers();
-
- mock.onGet(`${TEST_HOST}/feature_flags.json`).replyOnce(200, {
- id: 21,
- iid: 5,
- active: true,
- created_at: '2019-01-17T17:27:39.778Z',
- updated_at: '2019-01-17T17:27:39.778Z',
- name: 'feature_flag',
- description: '',
- version: NEW_VERSION_FLAG,
- edit_path: '/h5bp/html5-boilerplate/-/feature_flags/21/edit',
- destroy_path: '/h5bp/html5-boilerplate/-/feature_flags/21',
- strategies: [],
- });
-
- factory();
-
- return axios.waitForAll().then(() => {
- expect(wrapper.find(Form).props('version')).toBe(NEW_VERSION_FLAG);
- });
- });
-
it('should track when the toggle is clicked', () => {
const toggle = wrapper.find(GlToggle);
const spy = mockTracking('_category_', toggle.element, jest.spyOn);
diff --git a/spec/frontend/feature_flags/components/feature_flags_table_spec.js b/spec/frontend/feature_flags/components/feature_flags_table_spec.js
index 816bc9b9707..d06d60ae310 100644
--- a/spec/frontend/feature_flags/components/feature_flags_table_spec.js
+++ b/spec/frontend/feature_flags/components/feature_flags_table_spec.js
@@ -8,9 +8,6 @@ import {
ROLLOUT_STRATEGY_PERCENT_ROLLOUT,
ROLLOUT_STRATEGY_USER_ID,
ROLLOUT_STRATEGY_GITLAB_USER_LIST,
- NEW_VERSION_FLAG,
- LEGACY_FLAG,
- DEFAULT_PERCENT_ROLLOUT,
} from '~/feature_flags/constants';
const getDefaultProps = () => ({
@@ -23,17 +20,28 @@ const getDefaultProps = () => ({
description: 'flag description',
destroy_path: 'destroy/path',
edit_path: 'edit/path',
- version: LEGACY_FLAG,
- scopes: [
+ scopes: [],
+ strategies: [
{
- id: 1,
- active: true,
- environmentScope: 'scope',
- canUpdate: true,
- protected: false,
- rolloutStrategy: ROLLOUT_STRATEGY_ALL_USERS,
- rolloutPercentage: DEFAULT_PERCENT_ROLLOUT,
- shouldBeDestroyed: false,
+ name: ROLLOUT_STRATEGY_ALL_USERS,
+ parameters: {},
+ scopes: [{ environment_scope: '*' }],
+ },
+ {
+ name: ROLLOUT_STRATEGY_PERCENT_ROLLOUT,
+ parameters: { percentage: '50' },
+ scopes: [{ environment_scope: 'production' }, { environment_scope: 'staging' }],
+ },
+ {
+ name: ROLLOUT_STRATEGY_USER_ID,
+ parameters: { userIds: '1,2,3,4' },
+ scopes: [{ environment_scope: 'review/*' }],
+ },
+ {
+ name: ROLLOUT_STRATEGY_GITLAB_USER_LIST,
+ parameters: {},
+ user_list: { name: 'test list' },
+ scopes: [{ environment_scope: '*' }],
},
],
},
@@ -43,6 +51,7 @@ const getDefaultProps = () => ({
describe('Feature flag table', () => {
let wrapper;
let props;
+ let badges;
const createWrapper = (propsData, opts = {}) => {
wrapper = shallowMount(FeatureFlagsTable, {
@@ -56,6 +65,15 @@ describe('Feature flag table', () => {
beforeEach(() => {
props = getDefaultProps();
+ createWrapper(props, {
+ provide: { csrfToken: 'fakeToken' },
+ });
+
+ badges = wrapper.findAll('[data-testid="strategy-badge"]');
+ });
+
+ beforeEach(() => {
+ props = getDefaultProps();
});
afterEach(() => {
@@ -97,17 +115,10 @@ describe('Feature flag table', () => {
);
});
- it('should render an environments specs column', () => {
- const envColumn = wrapper.find('.js-feature-flag-environments');
-
- expect(envColumn).toBeDefined();
- expect(trimText(envColumn.text())).toBe('scope');
- });
-
it('should render an environments specs badge with active class', () => {
const envColumn = wrapper.find('.js-feature-flag-environments');
- expect(trimText(envColumn.find(GlBadge).text())).toBe('scope');
+ expect(trimText(envColumn.find(GlBadge).text())).toBe('All Users: All Environments');
});
it('should render an actions column', () => {
@@ -120,11 +131,13 @@ describe('Feature flag table', () => {
describe('when active and with an update toggle', () => {
let toggle;
+ let spy;
beforeEach(() => {
props.featureFlags[0].update_path = props.featureFlags[0].destroy_path;
createWrapper(props);
toggle = wrapper.find(GlToggle);
+ spy = mockTracking('_category_', toggle.element, jest.spyOn);
});
it('should have a toggle', () => {
@@ -143,123 +156,40 @@ describe('Feature flag table', () => {
expect(wrapper.emitted('toggle-flag')).toEqual([[flag]]);
});
});
- });
-
- describe('with an active scope and a percentage rollout strategy', () => {
- beforeEach(() => {
- props.featureFlags[0].scopes[0].rolloutStrategy = ROLLOUT_STRATEGY_PERCENT_ROLLOUT;
- props.featureFlags[0].scopes[0].rolloutPercentage = '54';
- createWrapper(props);
- });
- it('should render an environments specs badge with percentage', () => {
- const envColumn = wrapper.find('.js-feature-flag-environments');
+ it('tracks a click', () => {
+ toggle.trigger('click');
- expect(trimText(envColumn.find(GlBadge).text())).toBe('scope: 54%');
+ expect(spy).toHaveBeenCalledWith('_category_', 'click_button', {
+ label: 'feature_flag_toggle',
+ });
});
});
- describe('with an inactive scope', () => {
- beforeEach(() => {
- props.featureFlags[0].scopes[0].active = false;
- createWrapper(props);
- });
-
- it('should render an environments specs badge with inactive class', () => {
- const envColumn = wrapper.find('.js-feature-flag-environments');
-
- expect(trimText(envColumn.find(GlBadge).text())).toBe('scope');
- });
+ it('shows All Environments if the environment scope is *', () => {
+ expect(badges.at(0).text()).toContain('All Environments');
});
- describe('with a new version flag', () => {
- let toggle;
- let spy;
- let badges;
-
- beforeEach(() => {
- const newVersionProps = {
- ...props,
- featureFlags: [
- {
- id: 1,
- iid: 1,
- active: true,
- name: 'flag name',
- description: 'flag description',
- destroy_path: 'destroy/path',
- edit_path: 'edit/path',
- update_path: 'update/path',
- version: NEW_VERSION_FLAG,
- scopes: [],
- strategies: [
- {
- name: ROLLOUT_STRATEGY_ALL_USERS,
- parameters: {},
- scopes: [{ environment_scope: '*' }],
- },
- {
- name: ROLLOUT_STRATEGY_PERCENT_ROLLOUT,
- parameters: { percentage: '50' },
- scopes: [{ environment_scope: 'production' }, { environment_scope: 'staging' }],
- },
- {
- name: ROLLOUT_STRATEGY_USER_ID,
- parameters: { userIds: '1,2,3,4' },
- scopes: [{ environment_scope: 'review/*' }],
- },
- {
- name: ROLLOUT_STRATEGY_GITLAB_USER_LIST,
- parameters: {},
- user_list: { name: 'test list' },
- scopes: [{ environment_scope: '*' }],
- },
- ],
- },
- ],
- };
- createWrapper(newVersionProps, {
- provide: { csrfToken: 'fakeToken', glFeatures: { featureFlagsNewVersion: true } },
- });
-
- toggle = wrapper.find(GlToggle);
- spy = mockTracking('_category_', toggle.element, jest.spyOn);
- badges = wrapper.findAll('[data-testid="strategy-badge"]');
- });
-
- it('shows All Environments if the environment scope is *', () => {
- expect(badges.at(0).text()).toContain('All Environments');
- });
-
- it('shows the environment scope if another is set', () => {
- expect(badges.at(1).text()).toContain('production');
- expect(badges.at(1).text()).toContain('staging');
- expect(badges.at(2).text()).toContain('review/*');
- });
-
- it('shows All Users for the default strategy', () => {
- expect(badges.at(0).text()).toContain('All Users');
- });
-
- it('shows the percent for a percent rollout', () => {
- expect(badges.at(1).text()).toContain('Percent of users - 50%');
- });
+ it('shows the environment scope if another is set', () => {
+ expect(badges.at(1).text()).toContain('production');
+ expect(badges.at(1).text()).toContain('staging');
+ expect(badges.at(2).text()).toContain('review/*');
+ });
- it('shows the number of users for users with ID', () => {
- expect(badges.at(2).text()).toContain('User IDs - 4 users');
- });
+ it('shows All Users for the default strategy', () => {
+ expect(badges.at(0).text()).toContain('All Users');
+ });
- it('shows the name of a user list for user list', () => {
- expect(badges.at(3).text()).toContain('User List - test list');
- });
+ it('shows the percent for a percent rollout', () => {
+ expect(badges.at(1).text()).toContain('Percent of users - 50%');
+ });
- it('tracks a click', () => {
- toggle.trigger('click');
+ it('shows the number of users for users with ID', () => {
+ expect(badges.at(2).text()).toContain('User IDs - 4 users');
+ });
- expect(spy).toHaveBeenCalledWith('_category_', 'click_button', {
- label: 'feature_flag_toggle',
- });
- });
+ it('shows the name of a user list for user list', () => {
+ expect(badges.at(3).text()).toContain('User List - test list');
});
it('renders a feature flag without an iid', () => {
diff --git a/spec/frontend/feature_flags/components/form_spec.js b/spec/frontend/feature_flags/components/form_spec.js
index 6c3fce68618..c0f9638390a 100644
--- a/spec/frontend/feature_flags/components/form_spec.js
+++ b/spec/frontend/feature_flags/components/form_spec.js
@@ -1,18 +1,12 @@
-import { GlFormTextarea, GlFormCheckbox, GlButton, GlToggle } from '@gitlab/ui';
+import { GlButton } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import { uniqueId } from 'lodash';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import Api from '~/api';
-import EnvironmentsDropdown from '~/feature_flags/components/environments_dropdown.vue';
import Form from '~/feature_flags/components/form.vue';
import Strategy from '~/feature_flags/components/strategy.vue';
import {
ROLLOUT_STRATEGY_ALL_USERS,
ROLLOUT_STRATEGY_PERCENT_ROLLOUT,
- INTERNAL_ID_PREFIX,
- DEFAULT_PERCENT_ROLLOUT,
- LEGACY_FLAG,
- NEW_VERSION_FLAG,
} from '~/feature_flags/constants';
import RelatedIssuesRoot from '~/related_issues/components/related_issues_root.vue';
import { featureFlag, userList, allUsersStrategy } from '../mock_data';
@@ -29,15 +23,8 @@ describe('feature flag form', () => {
const requiredInjections = {
environmentsEndpoint: '/environments.json',
projectId: '1',
- glFeatures: {
- featureFlagPermissions: true,
- featureFlagsNewVersion: true,
- },
};
- const findAddNewScopeRow = () => wrapper.findByTestId('add-new-scope');
- const findGlToggle = () => wrapper.find(GlToggle);
-
const factory = (props = {}, provide = {}) => {
wrapper = extendedWrapper(
shallowMount(Form, {
@@ -100,328 +87,6 @@ describe('feature flag form', () => {
it('should render description textarea', () => {
expect(wrapper.find('#feature-flag-description').exists()).toBe(true);
});
-
- describe('scopes', () => {
- it('should render scopes table', () => {
- expect(wrapper.find('.js-scopes-table').exists()).toBe(true);
- });
-
- it('should render scopes table with a new row ', () => {
- expect(findAddNewScopeRow().exists()).toBe(true);
- });
-
- describe('status toggle', () => {
- describe('without filled text input', () => {
- it('should add a new scope with the text value empty and the status', () => {
- findGlToggle().vm.$emit('change', true);
-
- expect(wrapper.vm.formScopes).toHaveLength(1);
- expect(wrapper.vm.formScopes[0].active).toEqual(true);
- expect(wrapper.vm.formScopes[0].environmentScope).toEqual('');
-
- expect(wrapper.vm.newScope).toEqual('');
- });
- });
-
- it('has label', () => {
- expect(findGlToggle().props('label')).toBe(Form.i18n.statusLabel);
- });
-
- it('should be disabled if the feature flag is not active', (done) => {
- wrapper.setProps({ active: false });
- wrapper.vm.$nextTick(() => {
- expect(findGlToggle().props('disabled')).toBe(true);
- done();
- });
- });
- });
- });
- });
-
- describe('with provided data', () => {
- beforeEach(() => {
- factory({
- ...requiredProps,
- name: featureFlag.name,
- description: featureFlag.description,
- active: true,
- version: LEGACY_FLAG,
- scopes: [
- {
- id: 1,
- active: true,
- environmentScope: 'scope',
- canUpdate: true,
- protected: false,
- rolloutStrategy: ROLLOUT_STRATEGY_PERCENT_ROLLOUT,
- rolloutPercentage: '54',
- rolloutUserIds: '123',
- shouldIncludeUserIds: true,
- },
- {
- id: 2,
- active: true,
- environmentScope: 'scope',
- canUpdate: false,
- protected: true,
- rolloutStrategy: ROLLOUT_STRATEGY_PERCENT_ROLLOUT,
- rolloutPercentage: '54',
- rolloutUserIds: '123',
- shouldIncludeUserIds: true,
- },
- ],
- });
- });
-
- describe('scopes', () => {
- it('should be possible to remove a scope', () => {
- expect(wrapper.findByTestId('feature-flag-delete').exists()).toEqual(true);
- });
-
- it('renders empty row to add a new scope', () => {
- expect(findAddNewScopeRow().exists()).toEqual(true);
- });
-
- it('renders the user id checkbox', () => {
- expect(wrapper.find(GlFormCheckbox).exists()).toBe(true);
- });
-
- it('renders the user id text area', () => {
- expect(wrapper.find(GlFormTextarea).exists()).toBe(true);
-
- expect(wrapper.find(GlFormTextarea).vm.value).toBe('123');
- });
-
- describe('update scope', () => {
- describe('on click on toggle', () => {
- it('should update the scope', () => {
- findGlToggle().vm.$emit('change', false);
-
- expect(wrapper.vm.formScopes[0].active).toBe(false);
- });
-
- it('should be disabled if the feature flag is not active', (done) => {
- wrapper.setProps({ active: false });
-
- wrapper.vm.$nextTick(() => {
- expect(findGlToggle().props('disabled')).toBe(true);
- done();
- });
- });
- });
- describe('on strategy change', () => {
- it('should not include user IDs if All Users is selected', () => {
- const scope = wrapper.find({ ref: 'scopeRow' });
- scope.find('select').setValue(ROLLOUT_STRATEGY_ALL_USERS);
- return wrapper.vm.$nextTick().then(() => {
- expect(scope.find('#rollout-user-id-0').exists()).toBe(false);
- });
- });
- });
- });
-
- describe('deleting an existing scope', () => {
- beforeEach(() => {
- wrapper.find('.js-delete-scope').vm.$emit('click');
- });
-
- it('should add `shouldBeDestroyed` key the clicked scope', () => {
- expect(wrapper.vm.formScopes[0].shouldBeDestroyed).toBe(true);
- });
-
- it('should not render deleted scopes', () => {
- expect(wrapper.vm.filteredScopes).toEqual([expect.objectContaining({ id: 2 })]);
- });
- });
-
- describe('deleting a new scope', () => {
- it('should remove the scope from formScopes', () => {
- factory({
- ...requiredProps,
- name: 'feature_flag_1',
- description: 'this is a feature flag',
- scopes: [
- {
- environmentScope: 'new_scope',
- active: false,
- id: uniqueId(INTERNAL_ID_PREFIX),
- canUpdate: true,
- protected: false,
- strategies: [
- {
- name: ROLLOUT_STRATEGY_ALL_USERS,
- parameters: {},
- },
- ],
- },
- ],
- });
-
- wrapper.find('.js-delete-scope').vm.$emit('click');
-
- expect(wrapper.vm.formScopes).toEqual([]);
- });
- });
-
- describe('with * scope', () => {
- beforeEach(() => {
- factory({
- ...requiredProps,
- name: 'feature_flag_1',
- description: 'this is a feature flag',
- scopes: [
- {
- environmentScope: '*',
- active: false,
- canUpdate: false,
- rolloutStrategy: ROLLOUT_STRATEGY_ALL_USERS,
- rolloutPercentage: DEFAULT_PERCENT_ROLLOUT,
- },
- ],
- });
- });
-
- it('renders read-only name', () => {
- expect(wrapper.find('.js-scope-all').exists()).toEqual(true);
- });
- });
-
- describe('without permission to update', () => {
- it('should have the flag name input disabled', () => {
- const input = wrapper.find('#feature-flag-name');
-
- expect(input.element.disabled).toBe(true);
- });
-
- it('should have the flag discription text area disabled', () => {
- const textarea = wrapper.find('#feature-flag-description');
-
- expect(textarea.element.disabled).toBe(true);
- });
-
- it('should have the scope that cannot be updated be disabled', () => {
- const row = wrapper.findAll('.gl-responsive-table-row').at(2);
-
- expect(row.find(EnvironmentsDropdown).vm.disabled).toBe(true);
- expect(row.find(GlToggle).props('disabled')).toBe(true);
- expect(row.find('.js-delete-scope').exists()).toBe(false);
- });
- });
- });
-
- describe('on submit', () => {
- const selectFirstRolloutStrategyOption = (dropdownIndex) => {
- wrapper
- .findAll('select.js-rollout-strategy')
- .at(dropdownIndex)
- .findAll('option')
- .at(1)
- .setSelected();
- };
-
- beforeEach(() => {
- factory({
- ...requiredProps,
- name: 'feature_flag_1',
- active: true,
- description: 'this is a feature flag',
- scopes: [
- {
- id: 1,
- environmentScope: 'production',
- canUpdate: true,
- protected: true,
- active: false,
- rolloutStrategy: ROLLOUT_STRATEGY_ALL_USERS,
- rolloutPercentage: DEFAULT_PERCENT_ROLLOUT,
- rolloutUserIds: '',
- },
- ],
- });
-
- return wrapper.vm.$nextTick();
- });
-
- it('should emit handleSubmit with the updated data', () => {
- wrapper.find('#feature-flag-name').setValue('feature_flag_2');
-
- return wrapper.vm
- .$nextTick()
- .then(() => {
- wrapper
- .find('.js-new-scope-name')
- .find(EnvironmentsDropdown)
- .vm.$emit('selectEnvironment', 'review');
-
- return wrapper.vm.$nextTick();
- })
- .then(() => {
- findAddNewScopeRow().find(GlToggle).vm.$emit('change', true);
- })
- .then(() => {
- findGlToggle().vm.$emit('change', true);
- return wrapper.vm.$nextTick();
- })
-
- .then(() => {
- selectFirstRolloutStrategyOption(0);
- return wrapper.vm.$nextTick();
- })
- .then(() => {
- selectFirstRolloutStrategyOption(2);
- return wrapper.vm.$nextTick();
- })
- .then(() => {
- wrapper.find('.js-rollout-percentage').setValue('55');
-
- return wrapper.vm.$nextTick();
- })
- .then(() => {
- wrapper.find({ ref: 'submitButton' }).vm.$emit('click');
-
- const data = wrapper.emitted().handleSubmit[0][0];
-
- expect(data.name).toEqual('feature_flag_2');
- expect(data.description).toEqual('this is a feature flag');
- expect(data.active).toBe(true);
-
- expect(data.scopes).toEqual([
- {
- id: 1,
- active: true,
- environmentScope: 'production',
- canUpdate: true,
- protected: true,
- rolloutStrategy: ROLLOUT_STRATEGY_PERCENT_ROLLOUT,
- rolloutPercentage: '55',
- rolloutUserIds: '',
- shouldIncludeUserIds: false,
- },
- {
- id: expect.any(String),
- active: false,
- environmentScope: 'review',
- canUpdate: true,
- protected: false,
- rolloutStrategy: ROLLOUT_STRATEGY_ALL_USERS,
- rolloutPercentage: DEFAULT_PERCENT_ROLLOUT,
- rolloutUserIds: '',
- },
- {
- id: expect.any(String),
- active: true,
- environmentScope: '',
- canUpdate: true,
- protected: false,
- rolloutStrategy: ROLLOUT_STRATEGY_PERCENT_ROLLOUT,
- rolloutPercentage: DEFAULT_PERCENT_ROLLOUT,
- rolloutUserIds: '',
- shouldIncludeUserIds: false,
- },
- ]);
- });
- });
- });
});
describe('with strategies', () => {
@@ -432,7 +97,6 @@ describe('feature flag form', () => {
name: featureFlag.name,
description: featureFlag.description,
active: true,
- version: NEW_VERSION_FLAG,
strategies: [
{
type: ROLLOUT_STRATEGY_PERCENT_ROLLOUT,
diff --git a/spec/frontend/feature_flags/components/new_feature_flag_spec.js b/spec/frontend/feature_flags/components/new_feature_flag_spec.js
index e209c14d8c7..fe98b6421d4 100644
--- a/spec/frontend/feature_flags/components/new_feature_flag_spec.js
+++ b/spec/frontend/feature_flags/components/new_feature_flag_spec.js
@@ -4,7 +4,6 @@ import Vuex from 'vuex';
import { TEST_HOST } from 'spec/test_constants';
import Form from '~/feature_flags/components/form.vue';
import NewFeatureFlag from '~/feature_flags/components/new_feature_flag.vue';
-import { ROLLOUT_STRATEGY_ALL_USERS, DEFAULT_PERCENT_ROLLOUT } from '~/feature_flags/constants';
import createStore from '~/feature_flags/store/new';
import { allUsersStrategy } from '../mock_data';
@@ -71,20 +70,6 @@ describe('New feature flag form', () => {
expect(wrapper.find(Form).exists()).toEqual(true);
});
- it('should render default * row', () => {
- const defaultScope = {
- id: expect.any(String),
- environmentScope: '*',
- active: true,
- rolloutStrategy: ROLLOUT_STRATEGY_ALL_USERS,
- rolloutPercentage: DEFAULT_PERCENT_ROLLOUT,
- rolloutUserIds: '',
- };
- expect(wrapper.vm.scopes).toEqual([defaultScope]);
-
- expect(wrapper.find(Form).props('scopes')).toContainEqual(defaultScope);
- });
-
it('has an all users strategy by default', () => {
const strategies = wrapper.find(Form).props('strategies');
diff --git a/spec/frontend/feature_flags/mock_data.js b/spec/frontend/feature_flags/mock_data.js
index 11a91e5b2a8..b5f09ac1957 100644
--- a/spec/frontend/feature_flags/mock_data.js
+++ b/spec/frontend/feature_flags/mock_data.js
@@ -16,86 +16,24 @@ export const featureFlag = {
destroy_path: 'feature_flags/1',
update_path: 'feature_flags/1',
edit_path: 'feature_flags/1/edit',
- scopes: [
+ strategies: [
{
- id: 1,
- active: true,
- environment_scope: '*',
- can_update: true,
- protected: false,
- created_at: '2019-01-14T06:41:40.987Z',
- updated_at: '2019-01-14T06:41:40.987Z',
- strategies: [
- {
- name: ROLLOUT_STRATEGY_ALL_USERS,
- parameters: {},
- },
- ],
+ id: 9,
+ name: ROLLOUT_STRATEGY_ALL_USERS,
+ parameters: {},
+ scopes: [{ id: 17, environment_scope: '*' }],
},
{
- id: 2,
- active: false,
- environment_scope: 'production',
- can_update: true,
- protected: false,
- created_at: '2019-01-14T06:41:40.987Z',
- updated_at: '2019-01-14T06:41:40.987Z',
- strategies: [
- {
- name: ROLLOUT_STRATEGY_ALL_USERS,
- parameters: {},
- },
- ],
+ id: 8,
+ name: ROLLOUT_STRATEGY_PERCENT_ROLLOUT,
+ parameters: {},
+ scopes: [{ id: 18, environment_scope: 'review/*' }],
},
{
- id: 3,
- active: false,
- environment_scope: 'review/*',
- can_update: true,
- protected: false,
- created_at: '2019-01-14T06:41:40.987Z',
- updated_at: '2019-01-14T06:41:40.987Z',
- strategies: [
- {
- name: ROLLOUT_STRATEGY_ALL_USERS,
- parameters: {},
- },
- ],
- },
- {
- id: 4,
- active: true,
- environment_scope: 'development',
- can_update: true,
- protected: false,
- created_at: '2019-01-14T06:41:40.987Z',
- updated_at: '2019-01-14T06:41:40.987Z',
- strategies: [
- {
- name: ROLLOUT_STRATEGY_PERCENT_ROLLOUT,
- parameters: {
- percentage: '86',
- },
- },
- ],
- },
- {
- id: 5,
- active: true,
- environment_scope: 'development',
- can_update: true,
- protected: false,
- created_at: '2019-01-14T06:41:40.987Z',
- updated_at: '2019-01-14T06:41:40.987Z',
- strategies: [
- {
- name: ROLLOUT_STRATEGY_FLEXIBLE_ROLLOUT,
- parameters: {
- rollout: '42',
- stickiness: 'DEFAULT',
- },
- },
- ],
+ id: 7,
+ name: ROLLOUT_STRATEGY_USER_ID,
+ parameters: { userIds: '1,2,3,4' },
+ scopes: [{ id: 19, environment_scope: 'production' }],
},
],
};
diff --git a/spec/frontend/feature_flags/store/edit/actions_spec.js b/spec/frontend/feature_flags/store/edit/actions_spec.js
index afcac53468c..12fccd79170 100644
--- a/spec/frontend/feature_flags/store/edit/actions_spec.js
+++ b/spec/frontend/feature_flags/store/edit/actions_spec.js
@@ -1,11 +1,7 @@
import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
import { TEST_HOST } from 'spec/test_constants';
-import {
- NEW_VERSION_FLAG,
- LEGACY_FLAG,
- ROLLOUT_STRATEGY_ALL_USERS,
-} from '~/feature_flags/constants';
+import { ROLLOUT_STRATEGY_ALL_USERS } from '~/feature_flags/constants';
import {
updateFeatureFlag,
requestUpdateFeatureFlag,
@@ -19,7 +15,7 @@ import {
} from '~/feature_flags/store/edit/actions';
import * as types from '~/feature_flags/store/edit/mutation_types';
import state from '~/feature_flags/store/edit/state';
-import { mapStrategiesToRails, mapFromScopesViewModel } from '~/feature_flags/store/helpers';
+import { mapStrategiesToRails } from '~/feature_flags/store/helpers';
import axios from '~/lib/utils/axios_utils';
jest.mock('~/lib/utils/url_utility');
@@ -46,46 +42,9 @@ describe('Feature flags Edit Module actions', () => {
describe('success', () => {
it('dispatches requestUpdateFeatureFlag and receiveUpdateFeatureFlagSuccess ', (done) => {
const featureFlag = {
- name: 'feature_flag',
- description: 'feature flag',
- scopes: [
- {
- id: '1',
- environmentScope: '*',
- active: true,
- shouldBeDestroyed: false,
- canUpdate: true,
- protected: false,
- rolloutStrategy: ROLLOUT_STRATEGY_ALL_USERS,
- },
- ],
- version: LEGACY_FLAG,
- active: true,
- };
- mock.onPut(mockedState.endpoint, mapFromScopesViewModel(featureFlag)).replyOnce(200);
-
- testAction(
- updateFeatureFlag,
- featureFlag,
- mockedState,
- [],
- [
- {
- type: 'requestUpdateFeatureFlag',
- },
- {
- type: 'receiveUpdateFeatureFlagSuccess',
- },
- ],
- done,
- );
- });
- it('handles new version flags as well', (done) => {
- const featureFlag = {
name: 'name',
description: 'description',
active: true,
- version: NEW_VERSION_FLAG,
strategies: [
{
name: ROLLOUT_STRATEGY_ALL_USERS,
diff --git a/spec/frontend/feature_flags/store/helpers_spec.js b/spec/frontend/feature_flags/store/helpers_spec.js
index 711e2a1286e..2a6211c8cc1 100644
--- a/spec/frontend/feature_flags/store/helpers_spec.js
+++ b/spec/frontend/feature_flags/store/helpers_spec.js
@@ -1,351 +1,7 @@
-import { uniqueId } from 'lodash';
-import {
- ROLLOUT_STRATEGY_ALL_USERS,
- ROLLOUT_STRATEGY_PERCENT_ROLLOUT,
- ROLLOUT_STRATEGY_USER_ID,
- PERCENT_ROLLOUT_GROUP_ID,
- INTERNAL_ID_PREFIX,
- DEFAULT_PERCENT_ROLLOUT,
- LEGACY_FLAG,
- NEW_VERSION_FLAG,
-} from '~/feature_flags/constants';
-import {
- mapToScopesViewModel,
- mapFromScopesViewModel,
- createNewEnvironmentScope,
- mapStrategiesToViewModel,
- mapStrategiesToRails,
-} from '~/feature_flags/store/helpers';
+import { NEW_VERSION_FLAG } from '~/feature_flags/constants';
+import { mapStrategiesToViewModel, mapStrategiesToRails } from '~/feature_flags/store/helpers';
describe('feature flags helpers spec', () => {
- describe('mapToScopesViewModel', () => {
- it('converts the data object from the Rails API into something more usable by Vue', () => {
- const input = [
- {
- id: 3,
- environment_scope: 'environment_scope',
- active: true,
- can_update: true,
- protected: true,
- strategies: [
- {
- name: ROLLOUT_STRATEGY_PERCENT_ROLLOUT,
- parameters: {
- percentage: '56',
- },
- },
- {
- name: ROLLOUT_STRATEGY_USER_ID,
- parameters: {
- userIds: '123,234',
- },
- },
- ],
-
- _destroy: true,
- },
- ];
-
- const expected = [
- expect.objectContaining({
- id: 3,
- environmentScope: 'environment_scope',
- active: true,
- canUpdate: true,
- protected: true,
- rolloutStrategy: ROLLOUT_STRATEGY_PERCENT_ROLLOUT,
- rolloutPercentage: '56',
- rolloutUserIds: '123, 234',
- shouldBeDestroyed: true,
- }),
- ];
-
- const actual = mapToScopesViewModel(input);
-
- expect(actual).toEqual(expected);
- });
-
- it('returns Boolean properties even when their Rails counterparts were not provided (are `undefined`)', () => {
- const input = [
- {
- id: 3,
- environment_scope: 'environment_scope',
- },
- ];
-
- const [result] = mapToScopesViewModel(input);
-
- expect(result).toEqual(
- expect.objectContaining({
- active: false,
- canUpdate: false,
- protected: false,
- shouldBeDestroyed: false,
- }),
- );
- });
-
- it('returns an empty array if null or undefined is provided as a parameter', () => {
- expect(mapToScopesViewModel(null)).toEqual([]);
- expect(mapToScopesViewModel(undefined)).toEqual([]);
- });
-
- describe('with user IDs per environment', () => {
- let oldGon;
-
- beforeEach(() => {
- oldGon = window.gon;
- window.gon = { features: { featureFlagsUsersPerEnvironment: true } };
- });
-
- afterEach(() => {
- window.gon = oldGon;
- });
-
- it('sets the user IDs as a comma separated string', () => {
- const input = [
- {
- id: 3,
- environment_scope: 'environment_scope',
- active: true,
- can_update: true,
- protected: true,
- strategies: [
- {
- name: ROLLOUT_STRATEGY_PERCENT_ROLLOUT,
- parameters: {
- percentage: '56',
- },
- },
- {
- name: ROLLOUT_STRATEGY_USER_ID,
- parameters: {
- userIds: '123,234',
- },
- },
- ],
-
- _destroy: true,
- },
- ];
-
- const expected = [
- {
- id: 3,
- environmentScope: 'environment_scope',
- active: true,
- canUpdate: true,
- protected: true,
- rolloutStrategy: ROLLOUT_STRATEGY_PERCENT_ROLLOUT,
- rolloutPercentage: '56',
- rolloutUserIds: '123, 234',
- shouldBeDestroyed: true,
- shouldIncludeUserIds: true,
- },
- ];
-
- const actual = mapToScopesViewModel(input);
-
- expect(actual).toEqual(expected);
- });
- });
- });
-
- describe('mapFromScopesViewModel', () => {
- it('converts the object emitted from the Vue component into an object than is in the right format to be submitted to the Rails API', () => {
- const input = {
- name: 'name',
- description: 'description',
- active: true,
- scopes: [
- {
- id: 4,
- environmentScope: 'environmentScope',
- active: true,
- canUpdate: true,
- protected: true,
- shouldBeDestroyed: true,
- shouldIncludeUserIds: true,
- rolloutStrategy: ROLLOUT_STRATEGY_PERCENT_ROLLOUT,
- rolloutPercentage: '48',
- rolloutUserIds: '123, 234',
- },
- ],
- };
-
- const expected = {
- operations_feature_flag: {
- name: 'name',
- description: 'description',
- active: true,
- version: LEGACY_FLAG,
- scopes_attributes: [
- {
- id: 4,
- environment_scope: 'environmentScope',
- active: true,
- can_update: true,
- protected: true,
- _destroy: true,
- strategies: [
- {
- name: ROLLOUT_STRATEGY_PERCENT_ROLLOUT,
- parameters: {
- groupId: PERCENT_ROLLOUT_GROUP_ID,
- percentage: '48',
- },
- },
- {
- name: ROLLOUT_STRATEGY_USER_ID,
- parameters: {
- userIds: '123,234',
- },
- },
- ],
- },
- ],
- },
- };
-
- const actual = mapFromScopesViewModel(input);
-
- expect(actual).toEqual(expected);
- });
-
- it('should strip out internal IDs', () => {
- const input = {
- scopes: [{ id: 3 }, { id: uniqueId(INTERNAL_ID_PREFIX) }],
- };
-
- const result = mapFromScopesViewModel(input);
- const [realId, internalId] = result.operations_feature_flag.scopes_attributes;
-
- expect(realId.id).toBe(3);
- expect(internalId.id).toBeUndefined();
- });
-
- it('returns scopes_attributes as [] if param.scopes is null or undefined', () => {
- let {
- operations_feature_flag: { scopes_attributes: actualScopes },
- } = mapFromScopesViewModel({ scopes: null });
-
- expect(actualScopes).toEqual([]);
-
- ({
- operations_feature_flag: { scopes_attributes: actualScopes },
- } = mapFromScopesViewModel({ scopes: undefined }));
-
- expect(actualScopes).toEqual([]);
- });
- describe('with user IDs per environment', () => {
- it('sets the user IDs as a comma separated string', () => {
- const input = {
- name: 'name',
- description: 'description',
- active: true,
- scopes: [
- {
- id: 4,
- environmentScope: 'environmentScope',
- active: true,
- canUpdate: true,
- protected: true,
- shouldBeDestroyed: true,
- rolloutStrategy: ROLLOUT_STRATEGY_PERCENT_ROLLOUT,
- rolloutPercentage: '48',
- rolloutUserIds: '123, 234',
- shouldIncludeUserIds: true,
- },
- ],
- };
-
- const expected = {
- operations_feature_flag: {
- name: 'name',
- description: 'description',
- version: LEGACY_FLAG,
- active: true,
- scopes_attributes: [
- {
- id: 4,
- environment_scope: 'environmentScope',
- active: true,
- can_update: true,
- protected: true,
- _destroy: true,
- strategies: [
- {
- name: ROLLOUT_STRATEGY_PERCENT_ROLLOUT,
- parameters: {
- groupId: PERCENT_ROLLOUT_GROUP_ID,
- percentage: '48',
- },
- },
- {
- name: ROLLOUT_STRATEGY_USER_ID,
- parameters: {
- userIds: '123,234',
- },
- },
- ],
- },
- ],
- },
- };
-
- const actual = mapFromScopesViewModel(input);
-
- expect(actual).toEqual(expected);
- });
- });
- });
-
- describe('createNewEnvironmentScope', () => {
- it('should return a new environment scope object populated with the default options', () => {
- const expected = {
- environmentScope: '',
- active: false,
- id: expect.stringContaining(INTERNAL_ID_PREFIX),
- rolloutStrategy: ROLLOUT_STRATEGY_ALL_USERS,
- rolloutPercentage: DEFAULT_PERCENT_ROLLOUT,
- rolloutUserIds: '',
- };
-
- const actual = createNewEnvironmentScope();
-
- expect(actual).toEqual(expected);
- });
-
- it('should return a new environment scope object with overrides applied', () => {
- const overrides = {
- environmentScope: 'environmentScope',
- active: true,
- };
-
- const expected = {
- environmentScope: 'environmentScope',
- active: true,
- id: expect.stringContaining(INTERNAL_ID_PREFIX),
- rolloutStrategy: ROLLOUT_STRATEGY_ALL_USERS,
- rolloutPercentage: DEFAULT_PERCENT_ROLLOUT,
- rolloutUserIds: '',
- };
-
- const actual = createNewEnvironmentScope(overrides);
-
- expect(actual).toEqual(expected);
- });
-
- it('sets canUpdate and protected when called with featureFlagPermissions=true', () => {
- expect(createNewEnvironmentScope({}, true)).toEqual(
- expect.objectContaining({
- canUpdate: true,
- protected: false,
- }),
- );
- });
- });
-
describe('mapStrategiesToViewModel', () => {
it('should map rails casing to view model casing', () => {
expect(
@@ -380,14 +36,14 @@ describe('feature flags helpers spec', () => {
});
it('inserts spaces between user ids', () => {
- const strategy = mapStrategiesToViewModel([
+ const [strategy] = mapStrategiesToViewModel([
{
id: '1',
name: 'userWithId',
parameters: { userIds: 'user1,user2,user3' },
scopes: [],
},
- ])[0];
+ ]);
expect(strategy.parameters).toEqual({ userIds: 'user1, user2, user3' });
});
@@ -399,7 +55,6 @@ describe('feature flags helpers spec', () => {
mapStrategiesToRails({
name: 'test',
description: 'test description',
- version: NEW_VERSION_FLAG,
active: true,
strategies: [
{
@@ -421,8 +76,8 @@ describe('feature flags helpers spec', () => {
operations_feature_flag: {
name: 'test',
description: 'test description',
- version: NEW_VERSION_FLAG,
active: true,
+ version: NEW_VERSION_FLAG,
strategies_attributes: [
{
id: '1',
@@ -447,7 +102,6 @@ describe('feature flags helpers spec', () => {
mapStrategiesToRails({
name: 'test',
description: 'test description',
- version: NEW_VERSION_FLAG,
active: true,
strategies: [
{
@@ -462,8 +116,8 @@ describe('feature flags helpers spec', () => {
operations_feature_flag: {
name: 'test',
description: 'test description',
- version: NEW_VERSION_FLAG,
active: true,
+ version: NEW_VERSION_FLAG,
strategies_attributes: [
{
id: '1',
@@ -483,7 +137,6 @@ describe('feature flags helpers spec', () => {
it('removes white space between user ids', () => {
const result = mapStrategiesToRails({
name: 'test',
- version: NEW_VERSION_FLAG,
active: true,
strategies: [
{
@@ -503,7 +156,6 @@ describe('feature flags helpers spec', () => {
it('preserves the value of active', () => {
const result = mapStrategiesToRails({
name: 'test',
- version: NEW_VERSION_FLAG,
active: false,
strategies: [],
});
diff --git a/spec/frontend/feature_flags/store/index/actions_spec.js b/spec/frontend/feature_flags/store/index/actions_spec.js
index ec311ef92a3..a59f99f538c 100644
--- a/spec/frontend/feature_flags/store/index/actions_spec.js
+++ b/spec/frontend/feature_flags/store/index/actions_spec.js
@@ -1,7 +1,6 @@
import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
import { TEST_HOST } from 'spec/test_constants';
-import { mapToScopesViewModel } from '~/feature_flags/store/helpers';
import {
requestFeatureFlags,
receiveFeatureFlagsSuccess,
@@ -255,7 +254,6 @@ describe('Feature flags actions', () => {
beforeEach(() => {
mockedState.featureFlags = getRequestData.feature_flags.map((flag) => ({
...flag,
- scopes: mapToScopesViewModel(flag.scopes || []),
}));
mock = new MockAdapter(axios);
});
@@ -314,7 +312,6 @@ describe('Feature flags actions', () => {
beforeEach(() => {
mockedState.featureFlags = getRequestData.feature_flags.map((f) => ({
...f,
- scopes: mapToScopesViewModel(f.scopes || []),
}));
});
@@ -338,7 +335,6 @@ describe('Feature flags actions', () => {
beforeEach(() => {
mockedState.featureFlags = getRequestData.feature_flags.map((f) => ({
...f,
- scopes: mapToScopesViewModel(f.scopes || []),
}));
});
@@ -362,7 +358,6 @@ describe('Feature flags actions', () => {
beforeEach(() => {
mockedState.featureFlags = getRequestData.feature_flags.map((f) => ({
...f,
- scopes: mapToScopesViewModel(f.scopes || []),
}));
});
diff --git a/spec/frontend/feature_flags/store/index/mutations_spec.js b/spec/frontend/feature_flags/store/index/mutations_spec.js
index b9354196c68..c19f459e124 100644
--- a/spec/frontend/feature_flags/store/index/mutations_spec.js
+++ b/spec/frontend/feature_flags/store/index/mutations_spec.js
@@ -1,4 +1,3 @@
-import { mapToScopesViewModel } from '~/feature_flags/store/helpers';
import * as types from '~/feature_flags/store/index/mutation_types';
import mutations from '~/feature_flags/store/index/mutations';
import state from '~/feature_flags/store/index/state';
@@ -49,15 +48,6 @@ describe('Feature flags store Mutations', () => {
expect(stateCopy.hasError).toEqual(false);
});
- it('should set featureFlags with the transformed data', () => {
- const expected = getRequestData.feature_flags.map((flag) => ({
- ...flag,
- scopes: mapToScopesViewModel(flag.scopes || []),
- }));
-
- expect(stateCopy.featureFlags).toEqual(expected);
- });
-
it('should set count with the given data', () => {
expect(stateCopy.count).toEqual(37);
});
@@ -131,13 +121,11 @@ describe('Feature flags store Mutations', () => {
beforeEach(() => {
stateCopy.featureFlags = getRequestData.feature_flags.map((flag) => ({
...flag,
- scopes: mapToScopesViewModel(flag.scopes || []),
}));
stateCopy.count = { featureFlags: 1, userLists: 0 };
mutations[types.UPDATE_FEATURE_FLAG](stateCopy, {
...featureFlag,
- scopes: mapToScopesViewModel(featureFlag.scopes || []),
active: false,
});
});
@@ -146,7 +134,6 @@ describe('Feature flags store Mutations', () => {
expect(stateCopy.featureFlags).toEqual([
{
...featureFlag,
- scopes: mapToScopesViewModel(featureFlag.scopes || []),
active: false,
},
]);
@@ -158,7 +145,6 @@ describe('Feature flags store Mutations', () => {
stateCopy.featureFlags = getRequestData.feature_flags.map((flag) => ({
...flag,
...flagState,
- scopes: mapToScopesViewModel(flag.scopes || []),
}));
stateCopy.count = stateCount;
@@ -174,7 +160,6 @@ describe('Feature flags store Mutations', () => {
expect(stateCopy.featureFlags).toEqual([
{
...featureFlag,
- scopes: mapToScopesViewModel(featureFlag.scopes || []),
active: false,
},
]);
@@ -185,7 +170,6 @@ describe('Feature flags store Mutations', () => {
beforeEach(() => {
stateCopy.featureFlags = getRequestData.feature_flags.map((flag) => ({
...flag,
- scopes: mapToScopesViewModel(flag.scopes || []),
}));
mutations[types.RECEIVE_UPDATE_FEATURE_FLAG_ERROR](stateCopy, featureFlag.id);
});
@@ -194,7 +178,6 @@ describe('Feature flags store Mutations', () => {
expect(stateCopy.featureFlags).toEqual([
{
...featureFlag,
- scopes: mapToScopesViewModel(featureFlag.scopes || []),
active: false,
},
]);
diff --git a/spec/frontend/feature_flags/store/new/actions_spec.js b/spec/frontend/feature_flags/store/new/actions_spec.js
index 00dfb982ded..7900b200eb2 100644
--- a/spec/frontend/feature_flags/store/new/actions_spec.js
+++ b/spec/frontend/feature_flags/store/new/actions_spec.js
@@ -1,13 +1,7 @@
import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
-import { TEST_HOST } from 'spec/test_constants';
-import {
- ROLLOUT_STRATEGY_ALL_USERS,
- ROLLOUT_STRATEGY_PERCENT_ROLLOUT,
- LEGACY_FLAG,
- NEW_VERSION_FLAG,
-} from '~/feature_flags/constants';
-import { mapFromScopesViewModel, mapStrategiesToRails } from '~/feature_flags/store/helpers';
+import { ROLLOUT_STRATEGY_ALL_USERS } from '~/feature_flags/constants';
+import { mapStrategiesToRails } from '~/feature_flags/store/helpers';
import {
createFeatureFlag,
requestCreateFeatureFlag,
@@ -24,33 +18,13 @@ describe('Feature flags New Module Actions', () => {
let mockedState;
beforeEach(() => {
- mockedState = state({ endpoint: 'feature_flags.json', path: '/feature_flags' });
+ mockedState = state({ endpoint: '/feature_flags.json', path: '/feature_flags' });
});
describe('createFeatureFlag', () => {
let mock;
- const actionParams = {
- name: 'name',
- description: 'description',
- active: true,
- version: LEGACY_FLAG,
- scopes: [
- {
- id: 1,
- environmentScope: 'environmentScope',
- active: true,
- canUpdate: true,
- protected: true,
- shouldBeDestroyed: false,
- rolloutStrategy: ROLLOUT_STRATEGY_ALL_USERS,
- rolloutPercentage: ROLLOUT_STRATEGY_PERCENT_ROLLOUT,
- },
- ],
- };
-
beforeEach(() => {
- mockedState.endpoint = `${TEST_HOST}/endpoint.json`;
mock = new MockAdapter(axios);
});
@@ -60,33 +34,10 @@ describe('Feature flags New Module Actions', () => {
describe('success', () => {
it('dispatches requestCreateFeatureFlag and receiveCreateFeatureFlagSuccess ', (done) => {
- const convertedActionParams = mapFromScopesViewModel(actionParams);
-
- mock.onPost(`${TEST_HOST}/endpoint.json`, convertedActionParams).replyOnce(200);
-
- testAction(
- createFeatureFlag,
- actionParams,
- mockedState,
- [],
- [
- {
- type: 'requestCreateFeatureFlag',
- },
- {
- type: 'receiveCreateFeatureFlagSuccess',
- },
- ],
- done,
- );
- });
-
- it('sends strategies for new style feature flags', (done) => {
- const newVersionFlagParams = {
+ const actionParams = {
name: 'name',
description: 'description',
active: true,
- version: NEW_VERSION_FLAG,
strategies: [
{
name: ROLLOUT_STRATEGY_ALL_USERS,
@@ -97,13 +48,11 @@ describe('Feature flags New Module Actions', () => {
},
],
};
- mock
- .onPost(`${TEST_HOST}/endpoint.json`, mapStrategiesToRails(newVersionFlagParams))
- .replyOnce(200);
+ mock.onPost(mockedState.endpoint, mapStrategiesToRails(actionParams)).replyOnce(200);
testAction(
createFeatureFlag,
- newVersionFlagParams,
+ actionParams,
mockedState,
[],
[
@@ -121,10 +70,22 @@ describe('Feature flags New Module Actions', () => {
describe('error', () => {
it('dispatches requestCreateFeatureFlag and receiveCreateFeatureFlagError ', (done) => {
- const convertedActionParams = mapFromScopesViewModel(actionParams);
-
+ const actionParams = {
+ name: 'name',
+ description: 'description',
+ active: true,
+ strategies: [
+ {
+ name: ROLLOUT_STRATEGY_ALL_USERS,
+ parameters: {},
+ id: 1,
+ scopes: [{ id: 1, environmentScope: 'environmentScope', shouldBeDestroyed: false }],
+ shouldBeDestroyed: false,
+ },
+ ],
+ };
mock
- .onPost(`${TEST_HOST}/endpoint.json`, convertedActionParams)
+ .onPost(mockedState.endpoint, mapStrategiesToRails(actionParams))
.replyOnce(500, { message: [] });
testAction(
diff --git a/spec/frontend/feature_highlight/feature_highlight_helper_spec.js b/spec/frontend/feature_highlight/feature_highlight_helper_spec.js
index 1b5bffc1f9b..b87571830ca 100644
--- a/spec/frontend/feature_highlight/feature_highlight_helper_spec.js
+++ b/spec/frontend/feature_highlight/feature_highlight_helper_spec.js
@@ -1,6 +1,6 @@
import MockAdapter from 'axios-mock-adapter';
import { dismiss } from '~/feature_highlight/feature_highlight_helper';
-import { deprecatedCreateFlash as Flash } from '~/flash';
+import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import httpStatusCodes from '~/lib/utils/http_status';
@@ -32,9 +32,10 @@ describe('feature highlight helper', () => {
await dismiss(endpoint, highlightId);
- expect(Flash).toHaveBeenCalledWith(
- 'An error occurred while dismissing the feature highlight. Refresh the page and try dismissing again.',
- );
+ expect(createFlash).toHaveBeenCalledWith({
+ message:
+ 'An error occurred while dismissing the feature highlight. Refresh the page and try dismissing again.',
+ });
});
});
});
diff --git a/spec/frontend/filtered_search/filtered_search_manager_spec.js b/spec/frontend/filtered_search/filtered_search_manager_spec.js
index c03c8f6c529..83e7f6c9b3f 100644
--- a/spec/frontend/filtered_search/filtered_search_manager_spec.js
+++ b/spec/frontend/filtered_search/filtered_search_manager_spec.js
@@ -8,12 +8,14 @@ import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered
import RecentSearchesRoot from '~/filtered_search/recent_searches_root';
import RecentSearchesService from '~/filtered_search/services/recent_searches_service';
import RecentSearchesServiceError from '~/filtered_search/services/recent_searches_service_error';
-import * as commonUtils from '~/lib/utils/common_utils';
+import createFlash from '~/flash';
import { BACKSPACE_KEY_CODE, DELETE_KEY_CODE } from '~/lib/utils/keycodes';
-import { visitUrl } from '~/lib/utils/url_utility';
+import { visitUrl, getParameterByName } from '~/lib/utils/url_utility';
+jest.mock('~/flash');
jest.mock('~/lib/utils/url_utility', () => ({
...jest.requireActual('~/lib/utils/url_utility'),
+ getParameterByName: jest.fn(),
visitUrl: jest.fn(),
}));
@@ -84,9 +86,10 @@ describe('Filtered Search Manager', () => {
jest
.spyOn(FilteredSearchDropdownManager.prototype, 'updateDropdownOffset')
.mockImplementation();
- jest.spyOn(commonUtils, 'getParameterByName').mockReturnValue(null);
jest.spyOn(FilteredSearchVisualTokens, 'unselectTokens');
+ getParameterByName.mockReturnValue(null);
+
input = document.querySelector('.filtered-search');
tokensContainer = document.querySelector('.tokens-container');
manager = new FilteredSearchManager({ page, useDefaultState });
@@ -127,11 +130,10 @@ describe('Filtered Search Manager', () => {
jest
.spyOn(RecentSearchesService.prototype, 'fetch')
.mockImplementation(() => Promise.reject(new RecentSearchesServiceError()));
- jest.spyOn(window, 'Flash').mockImplementation();
manager.setup();
- expect(window.Flash).not.toHaveBeenCalled();
+ expect(createFlash).not.toHaveBeenCalled();
});
});
diff --git a/spec/frontend/filtered_search/visual_token_value_spec.js b/spec/frontend/filtered_search/visual_token_value_spec.js
index 772fa7d07ed..7185f382fc1 100644
--- a/spec/frontend/filtered_search/visual_token_value_spec.js
+++ b/spec/frontend/filtered_search/visual_token_value_spec.js
@@ -1,11 +1,14 @@
import { escape } from 'lodash';
import FilteredSearchSpecHelper from 'helpers/filtered_search_spec_helper';
import { TEST_HOST } from 'helpers/test_constants';
-import DropdownUtils from '~/filtered_search//dropdown_utils';
+import DropdownUtils from '~/filtered_search/dropdown_utils';
import VisualTokenValue from '~/filtered_search/visual_token_value';
+import createFlash from '~/flash';
import AjaxCache from '~/lib/utils/ajax_cache';
import UsersCache from '~/lib/utils/users_cache';
+jest.mock('~/flash');
+
describe('Filtered Search Visual Tokens', () => {
const findElements = (tokenElement) => {
const tokenNameElement = tokenElement.querySelector('.name');
@@ -43,7 +46,6 @@ describe('Filtered Search Visual Tokens', () => {
});
it('ignores error if UsersCache throws', (done) => {
- jest.spyOn(window, 'Flash').mockImplementation(() => {});
const dummyError = new Error('Earth rotated backwards');
const { subject, tokenValueContainer, tokenValueElement } = findElements(authorToken);
const tokenValue = tokenValueElement.innerText;
@@ -55,7 +57,7 @@ describe('Filtered Search Visual Tokens', () => {
subject
.updateUserTokenAppearance(tokenValueContainer, tokenValueElement, tokenValue)
.then(() => {
- expect(window.Flash.mock.calls.length).toBe(0);
+ expect(createFlash.mock.calls.length).toBe(0);
})
.then(done)
.catch(done.fail);
diff --git a/spec/frontend/fixtures/api_markdown.rb b/spec/frontend/fixtures/api_markdown.rb
index 1c3967b2c36..94db262e4fd 100644
--- a/spec/frontend/fixtures/api_markdown.rb
+++ b/spec/frontend/fixtures/api_markdown.rb
@@ -4,12 +4,29 @@ require 'spec_helper'
RSpec.describe API::MergeRequests, '(JavaScript fixtures)', type: :request do
include ApiHelpers
+ include WikiHelpers
include JavaScriptFixturesHelpers
+ let_it_be(:user) { create(:user) }
+
+ let_it_be(:group) { create(:group, :public) }
+ let_it_be(:project) { create(:project, :public, :repository, group: group) }
+
+ let_it_be(:project_wiki) { create(:project_wiki, user: user) }
+
+ let(:project_wiki_page) { create(:wiki_page, wiki: project_wiki) }
+
fixture_subdir = 'api/markdown'
before(:all) do
clean_frontend_fixtures(fixture_subdir)
+
+ group.add_owner(user)
+ project.add_maintainer(user)
+ end
+
+ before do
+ sign_in(user)
end
markdown_examples = begin
@@ -19,14 +36,27 @@ RSpec.describe API::MergeRequests, '(JavaScript fixtures)', type: :request do
end
markdown_examples.each do |markdown_example|
+ context = markdown_example.fetch(:context, '')
name = markdown_example.fetch(:name)
- context "for #{name}" do
+ context "for #{name}#{!context.empty? ? " (context: #{context})" : ''}" do
let(:markdown) { markdown_example.fetch(:markdown) }
+ name = "#{context}_#{name}" unless context.empty?
+
it "#{fixture_subdir}/#{name}.json" do
- post api("/markdown"), params: { text: markdown, gfm: true }
+ api_url = case context
+ when 'project'
+ "/#{project.full_path}/preview_markdown"
+ when 'group'
+ "/groups/#{group.full_path}/preview_markdown"
+ when 'project_wiki'
+ "/#{project.full_path}/-/wikis/#{project_wiki_page.slug}/preview_markdown"
+ else
+ api "/markdown"
+ end
+ post api_url, params: { text: markdown, gfm: true }
expect(response).to be_successful
end
end
diff --git a/spec/frontend/fixtures/api_markdown.yml b/spec/frontend/fixtures/api_markdown.yml
index 3274e914f03..8d8c9a1d902 100644
--- a/spec/frontend/fixtures/api_markdown.yml
+++ b/spec/frontend/fixtures/api_markdown.yml
@@ -10,8 +10,28 @@
markdown: '`code`'
- name: strike
markdown: '~~del~~'
+- name: horizontal_rule
+ markdown: '---'
- name: link
markdown: '[GitLab](https://gitlab.com)'
+- name: attachment_link
+ context: project_wiki
+ markdown: '[test-file](test-file.zip)'
+- name: attachment_link
+ context: project
+ markdown: '[test-file](/uploads/aa45a38ec2cfe97433281b10bbff042c/test-file.zip)'
+- name: attachment_link
+ context: group
+ markdown: '[test-file](/uploads/aa45a38ec2cfe97433281b10bbff042c/test-file.zip)'
+- name: attachment_image
+ context: project_wiki
+ markdown: '![test-file](test-file.png)'
+- name: attachment_image
+ context: project
+ markdown: '![test-file](/uploads/aa45a38ec2cfe97433281b10bbff042c/test-file.png)'
+- name: attachment_image
+ context: group
+ markdown: '![test-file](/uploads/aa45a38ec2cfe97433281b10bbff042c/test-file.png)'
- name: code_block
markdown: |-
```javascript
@@ -54,3 +74,16 @@
markdown: |-
This is a line after a\
hard break
+- name: table
+ markdown: |-
+ | header | header |
+ |--------|--------|
+ | cell | cell |
+ | cell | cell |
+- name: table_with_alignment
+ markdown: |-
+ | header | : header : | header : |
+ |--------|------------|----------|
+ | cell | cell | cell |
+ | cell | cell | cell |
+
diff --git a/spec/frontend/fixtures/application_settings.rb b/spec/frontend/fixtures/application_settings.rb
index ebccecb32ba..b09bea56b94 100644
--- a/spec/frontend/fixtures/application_settings.rb
+++ b/spec/frontend/fixtures/application_settings.rb
@@ -34,4 +34,12 @@ RSpec.describe Admin::ApplicationSettingsController, '(JavaScript fixtures)', ty
expect(response).to be_successful
end
+
+ it 'application_settings/usage.html' do
+ stub_application_setting(usage_ping_enabled: false)
+
+ get :metrics_and_profiling
+
+ expect(response).to be_successful
+ end
end
diff --git a/spec/frontend/fixtures/pipelines.rb b/spec/frontend/fixtures/pipelines.rb
index 2a538352abe..f695b74ec87 100644
--- a/spec/frontend/fixtures/pipelines.rb
+++ b/spec/frontend/fixtures/pipelines.rb
@@ -13,6 +13,7 @@ RSpec.describe Projects::PipelinesController, '(JavaScript fixtures)', type: :co
let!(:build_pipeline_without_author) { create(:ci_build, pipeline: pipeline_without_author, stage: 'test') }
let_it_be(:pipeline_without_commit) { create(:ci_pipeline, status: :success, project: project, sha: '0000') }
+
let!(:build_pipeline_without_commit) { create(:ci_build, pipeline: pipeline_without_commit, stage: 'test') }
let(:commit) { create(:commit, project: project) }
diff --git a/spec/frontend/fixtures/projects.rb b/spec/frontend/fixtures/projects.rb
index 778ae218160..7873d59dbad 100644
--- a/spec/frontend/fixtures/projects.rb
+++ b/spec/frontend/fixtures/projects.rb
@@ -61,13 +61,12 @@ RSpec.describe 'Projects (JavaScript fixtures)', type: :controller do
clean_frontend_fixtures('graphql/projects/access_tokens')
end
- fragment_paths = ['graphql_shared/fragments/pageInfo.fragment.graphql']
base_input_path = 'access_tokens/graphql/queries/'
base_output_path = 'graphql/projects/access_tokens/'
query_name = 'get_projects.query.graphql'
it "#{base_output_path}#{query_name}.json" do
- query = get_graphql_query_as_string("#{base_input_path}#{query_name}", fragment_paths)
+ query = get_graphql_query_as_string("#{base_input_path}#{query_name}")
post_graphql(query, current_user: user, variables: { search: '', first: 2 })
diff --git a/spec/frontend/fixtures/prometheus_service.rb b/spec/frontend/fixtures/prometheus_service.rb
index 3a59ecf3868..c349f2a24bc 100644
--- a/spec/frontend/fixtures/prometheus_service.rb
+++ b/spec/frontend/fixtures/prometheus_service.rb
@@ -7,7 +7,7 @@ RSpec.describe Projects::ServicesController, '(JavaScript fixtures)', type: :con
let(:namespace) { create(:namespace, name: 'frontend-fixtures' )}
let(:project) { create(:project_empty_repo, namespace: namespace, path: 'services-project') }
- let!(:service) { create(:prometheus_service, project: project) }
+ let!(:integration) { create(:prometheus_integration, project: project) }
let(:user) { project.owner }
render_views
@@ -28,7 +28,7 @@ RSpec.describe Projects::ServicesController, '(JavaScript fixtures)', type: :con
get :edit, params: {
namespace_id: namespace,
project_id: project,
- id: service.to_param
+ id: integration.to_param
}
expect(response).to be_successful
diff --git a/spec/frontend/fixtures/releases.rb b/spec/frontend/fixtures/releases.rb
index ac34400bc01..e8f259fba15 100644
--- a/spec/frontend/fixtures/releases.rb
+++ b/spec/frontend/fixtures/releases.rb
@@ -133,15 +133,13 @@ RSpec.describe 'Releases (JavaScript fixtures)' do
all_releases_query_path = 'releases/graphql/queries/all_releases.query.graphql'
one_release_query_path = 'releases/graphql/queries/one_release.query.graphql'
one_release_for_editing_query_path = 'releases/graphql/queries/one_release_for_editing.query.graphql'
- release_fragment_path = 'releases/graphql/fragments/release.fragment.graphql'
- release_for_editing_fragment_path = 'releases/graphql/fragments/release_for_editing.fragment.graphql'
before(:all) do
clean_frontend_fixtures('graphql/releases/')
end
it "graphql/#{all_releases_query_path}.json" do
- query = get_graphql_query_as_string(all_releases_query_path, [release_fragment_path])
+ query = get_graphql_query_as_string(all_releases_query_path)
post_graphql(query, current_user: admin, variables: { fullPath: project.full_path })
@@ -150,7 +148,7 @@ RSpec.describe 'Releases (JavaScript fixtures)' do
end
it "graphql/#{one_release_query_path}.json" do
- query = get_graphql_query_as_string(one_release_query_path, [release_fragment_path])
+ query = get_graphql_query_as_string(one_release_query_path)
post_graphql(query, current_user: admin, variables: { fullPath: project.full_path, tagName: release.tag })
@@ -159,7 +157,7 @@ RSpec.describe 'Releases (JavaScript fixtures)' do
end
it "graphql/#{one_release_for_editing_query_path}.json" do
- query = get_graphql_query_as_string(one_release_for_editing_query_path, [release_for_editing_fragment_path])
+ query = get_graphql_query_as_string(one_release_for_editing_query_path)
post_graphql(query, current_user: admin, variables: { fullPath: project.full_path, tagName: release.tag })
diff --git a/spec/frontend/fixtures/runner.rb b/spec/frontend/fixtures/runner.rb
index b88fb840137..e29a58f43b9 100644
--- a/spec/frontend/fixtures/runner.rb
+++ b/spec/frontend/fixtures/runner.rb
@@ -36,10 +36,7 @@ RSpec.describe 'Runner (JavaScript fixtures)' do
get_runners_query_name = 'get_runners.query.graphql'
let_it_be(:query) do
- get_graphql_query_as_string("#{query_path}#{get_runners_query_name}", [
- 'runner/graphql/runner_node.fragment.graphql',
- 'graphql_shared/fragments/pageInfo.fragment.graphql'
- ])
+ get_graphql_query_as_string("#{query_path}#{get_runners_query_name}")
end
it "#{fixtures_path}#{get_runners_query_name}.json" do
@@ -59,9 +56,7 @@ RSpec.describe 'Runner (JavaScript fixtures)' do
get_runner_query_name = 'get_runner.query.graphql'
let_it_be(:query) do
- get_graphql_query_as_string("#{query_path}#{get_runner_query_name}", [
- 'runner/graphql/runner_details.fragment.graphql'
- ])
+ get_graphql_query_as_string("#{query_path}#{get_runner_query_name}")
end
it "#{fixtures_path}#{get_runner_query_name}.json" do
diff --git a/spec/frontend/flash_spec.js b/spec/frontend/flash_spec.js
index 28e8522cc12..96e5202780b 100644
--- a/spec/frontend/flash_spec.js
+++ b/spec/frontend/flash_spec.js
@@ -1,5 +1,4 @@
import createFlash, {
- deprecatedCreateFlash,
createFlashEl,
createAction,
hideFlash,
@@ -125,120 +124,6 @@ describe('Flash', () => {
});
});
- describe('deprecatedCreateFlash', () => {
- const message = 'test';
- const type = 'alert';
- const parent = document;
- const actionConfig = null;
- const fadeTransition = false;
- const addBodyClass = true;
- const defaultParams = [message, type, parent, actionConfig, fadeTransition, addBodyClass];
-
- describe('no flash-container', () => {
- it('does not add to the DOM', () => {
- const flashEl = deprecatedCreateFlash(message);
-
- expect(flashEl).toBeNull();
-
- expect(document.querySelector('.flash-alert')).toBeNull();
- });
- });
-
- describe('with flash-container', () => {
- beforeEach(() => {
- setFixtures(
- '<div class="content-wrapper js-content-wrapper"><div class="flash-container"></div></div>',
- );
- });
-
- afterEach(() => {
- document.querySelector('.js-content-wrapper').remove();
- });
-
- it('adds flash element into container', () => {
- deprecatedCreateFlash(...defaultParams);
-
- expect(document.querySelector('.flash-alert')).not.toBeNull();
-
- expect(document.body.className).toContain('flash-shown');
- });
-
- it('adds flash into specified parent', () => {
- deprecatedCreateFlash(
- message,
- type,
- document.querySelector('.content-wrapper'),
- actionConfig,
- fadeTransition,
- addBodyClass,
- );
-
- expect(document.querySelector('.content-wrapper .flash-alert')).not.toBeNull();
- expect(document.querySelector('.content-wrapper').innerText.trim()).toEqual(message);
- });
-
- it('adds container classes when inside content-wrapper', () => {
- deprecatedCreateFlash(...defaultParams);
-
- expect(document.querySelector('.flash-text').className).toBe('flash-text');
- expect(document.querySelector('.content-wrapper').innerText.trim()).toEqual(message);
- });
-
- it('does not add container when outside of content-wrapper', () => {
- document.querySelector('.content-wrapper').className = 'js-content-wrapper';
- deprecatedCreateFlash(...defaultParams);
-
- expect(document.querySelector('.flash-text').className.trim()).toContain('flash-text');
- });
-
- it('removes element after clicking', () => {
- deprecatedCreateFlash(...defaultParams);
-
- document.querySelector('.flash-alert .js-close-icon').click();
-
- expect(document.querySelector('.flash-alert')).toBeNull();
-
- expect(document.body.className).not.toContain('flash-shown');
- });
-
- describe('with actionConfig', () => {
- it('adds action link', () => {
- const newActionConfig = { title: 'test' };
- deprecatedCreateFlash(
- message,
- type,
- parent,
- newActionConfig,
- fadeTransition,
- addBodyClass,
- );
-
- expect(document.querySelector('.flash-action')).not.toBeNull();
- });
-
- it('calls actionConfig clickHandler on click', () => {
- const newActionConfig = {
- title: 'test',
- clickHandler: jest.fn(),
- };
-
- deprecatedCreateFlash(
- message,
- type,
- parent,
- newActionConfig,
- fadeTransition,
- addBodyClass,
- );
-
- document.querySelector('.flash-action').click();
-
- expect(newActionConfig.clickHandler).toHaveBeenCalled();
- });
- });
- });
- });
-
describe('createFlash', () => {
const message = 'test';
const type = 'alert';
diff --git a/spec/frontend/frequent_items/components/frequent_items_list_item_spec.js b/spec/frontend/frequent_items/components/frequent_items_list_item_spec.js
index 9a68115e4f6..5a05265afdc 100644
--- a/spec/frontend/frequent_items/components/frequent_items_list_item_spec.js
+++ b/spec/frontend/frequent_items/components/frequent_items_list_item_spec.js
@@ -1,9 +1,11 @@
+import { GlButton } from '@gitlab/ui';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex';
import { trimText } from 'helpers/text_helper';
import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
import frequentItemsListItemComponent from '~/frequent_items/components/frequent_items_list_item.vue';
import { createStore } from '~/frequent_items/store';
+import ProjectAvatar from '~/vue_shared/components/project_avatar.vue';
import { mockProject } from '../mock_data';
const localVue = createLocalVue();
@@ -15,12 +17,12 @@ describe('FrequentItemsListItemComponent', () => {
let store;
const findTitle = () => wrapper.find({ ref: 'frequentItemsItemTitle' });
- const findAvatar = () => wrapper.find({ ref: 'frequentItemsItemAvatar' });
+ const findAvatar = () => wrapper.findComponent(ProjectAvatar);
const findAllTitles = () => wrapper.findAll({ ref: 'frequentItemsItemTitle' });
const findNamespace = () => wrapper.find({ ref: 'frequentItemsItemNamespace' });
- const findAllAnchors = () => wrapper.findAll('a');
+ const findAllButtons = () => wrapper.findAllComponents(GlButton);
const findAllNamespace = () => wrapper.findAll({ ref: 'frequentItemsItemNamespace' });
- const findAvatarContainer = () => wrapper.findAll({ ref: 'frequentItemsItemAvatarContainer' });
+ const findAllAvatars = () => wrapper.findAllComponents(ProjectAvatar);
const findAllMetadataContainers = () =>
wrapper.findAll({ ref: 'frequentItemsItemMetadataContainer' });
@@ -91,16 +93,8 @@ describe('FrequentItemsListItemComponent', () => {
createComponent();
});
- it('should render avatar if avatarUrl is present', () => {
- wrapper.setProps({ avatarUrl: 'path/to/avatar.png' });
-
- return wrapper.vm.$nextTick(() => {
- expect(findAvatar().exists()).toBe(true);
- });
- });
-
- it('should not render avatar if avatarUrl is not present', () => {
- expect(findAvatar().exists()).toBe(false);
+ it('renders avatar', () => {
+ expect(findAvatar().exists()).toBe(true);
});
it('renders root element with the right classes', () => {
@@ -109,8 +103,8 @@ describe('FrequentItemsListItemComponent', () => {
it.each`
name | selector | expected
- ${'anchor'} | ${findAllAnchors} | ${1}
- ${'avatar container'} | ${findAvatarContainer} | ${1}
+ ${'button'} | ${findAllButtons} | ${1}
+ ${'avatar container'} | ${findAllAvatars} | ${1}
${'metadata container'} | ${findAllMetadataContainers} | ${1}
${'title'} | ${findAllTitles} | ${1}
${'namespace'} | ${findAllNamespace} | ${1}
@@ -119,13 +113,10 @@ describe('FrequentItemsListItemComponent', () => {
});
it('tracks when item link is clicked', () => {
- const link = wrapper.find('a');
- // NOTE: this listener is required to prevent the click from going through and causing:
- // `Error: Not implemented: navigation ...`
- link.element.addEventListener('click', (e) => {
- e.preventDefault();
- });
- link.trigger('click');
+ const link = wrapper.findComponent(GlButton);
+
+ link.vm.$emit('click');
+
expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_link', {
label: 'projects_dropdown_frequent_items_list_item',
});
diff --git a/spec/frontend/gpg_badges_spec.js b/spec/frontend/gpg_badges_spec.js
index cd2cc88fa5a..44c70f1ad4d 100644
--- a/spec/frontend/gpg_badges_spec.js
+++ b/spec/frontend/gpg_badges_spec.js
@@ -17,19 +17,23 @@ describe('GpgBadges', () => {
};
const dummyUrl = `${TEST_HOST}/dummy/signatures`;
- beforeEach(() => {
- mock = new MockAdapter(axios);
+ const setForm = ({ utf8 = '✓', search = '' } = {}) => {
setFixtures(`
<form
class="commits-search-form js-signature-container" data-signatures-path="${dummyUrl}" action="${dummyUrl}"
method="get">
- <input name="utf8" type="hidden" value="✓">
- <input type="search" name="search" id="commits-search"class="form-control search-text-input input-short">
+ <input name="utf8" type="hidden" value="${utf8}">
+ <input type="search" name="search" value="${search}" id="commits-search"class="form-control search-text-input input-short">
</form>
<div class="parent-container">
<div class="js-loading-gpg-badge" data-commit-sha="${dummyCommitSha}"></div>
</div>
`);
+ };
+
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+ setForm();
});
afterEach(() => {
@@ -62,6 +66,44 @@ describe('GpgBadges', () => {
.catch(done.fail);
});
+ it('fetches commit signatures', async () => {
+ mock.onGet(dummyUrl).replyOnce(200);
+
+ await GpgBadges.fetch();
+
+ expect(mock.history.get).toHaveLength(1);
+ expect(mock.history.get[0]).toMatchObject({
+ params: { search: '', utf8: '✓' },
+ url: dummyUrl,
+ });
+ });
+
+ it('fetches commit signatures with search parameters with spaces', async () => {
+ mock.onGet(dummyUrl).replyOnce(200);
+ setForm({ search: 'my search' });
+
+ await GpgBadges.fetch();
+
+ expect(mock.history.get).toHaveLength(1);
+ expect(mock.history.get[0]).toMatchObject({
+ params: { search: 'my search', utf8: '✓' },
+ url: dummyUrl,
+ });
+ });
+
+ it('fetches commit signatures with search parameters with plus symbols', async () => {
+ mock.onGet(dummyUrl).replyOnce(200);
+ setForm({ search: 'my+search' });
+
+ await GpgBadges.fetch();
+
+ expect(mock.history.get).toHaveLength(1);
+ expect(mock.history.get[0]).toMatchObject({
+ params: { search: 'my+search', utf8: '✓' },
+ url: dummyUrl,
+ });
+ });
+
it('displays a loading spinner', (done) => {
mock.onGet(dummyUrl).replyOnce(200);
diff --git a/spec/frontend/grafana_integration/components/grafana_integration_spec.js b/spec/frontend/grafana_integration/components/grafana_integration_spec.js
index 3cb4dd41574..d5338430054 100644
--- a/spec/frontend/grafana_integration/components/grafana_integration_spec.js
+++ b/spec/frontend/grafana_integration/components/grafana_integration_spec.js
@@ -114,7 +114,6 @@ describe('grafana integration component', () => {
.then(() =>
expect(createFlash).toHaveBeenCalledWith({
message: `There was an error saving your changes. ${message}`,
- type: 'alert',
}),
);
});
diff --git a/spec/frontend/groups/components/app_spec.js b/spec/frontend/groups/components/app_spec.js
index e559c9519f2..da0ff2a64ec 100644
--- a/spec/frontend/groups/components/app_spec.js
+++ b/spec/frontend/groups/components/app_spec.js
@@ -1,9 +1,9 @@
-import '~/flash';
import { GlModal, GlLoadingIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import AxiosMockAdapter from 'axios-mock-adapter';
import Vue from 'vue';
import waitForPromises from 'helpers/wait_for_promises';
+import createFlash from '~/flash';
import appComponent from '~/groups/components/app.vue';
import groupFolderComponent from '~/groups/components/group_folder.vue';
import groupItemComponent from '~/groups/components/group_item.vue';
@@ -27,6 +27,7 @@ import {
const $toast = {
show: jest.fn(),
};
+jest.mock('~/flash');
describe('AppComponent', () => {
let wrapper;
@@ -123,12 +124,12 @@ describe('AppComponent', () => {
mock.onGet('/dashboard/groups.json').reply(400);
jest.spyOn(window, 'scrollTo').mockImplementation(() => {});
- jest.spyOn(window, 'Flash').mockImplementation(() => {});
-
return vm.fetchGroups({}).then(() => {
expect(vm.isLoading).toBe(false);
expect(window.scrollTo).toHaveBeenCalledWith({ behavior: 'smooth', top: 0 });
- expect(window.Flash).toHaveBeenCalledWith('An error occurred. Please try again.');
+ expect(createFlash).toHaveBeenCalledWith({
+ message: 'An error occurred. Please try again.',
+ });
});
});
});
@@ -324,15 +325,13 @@ describe('AppComponent', () => {
const message = 'An error occurred. Please try again.';
jest.spyOn(vm.service, 'leaveGroup').mockRejectedValue({ status: 500 });
jest.spyOn(vm.store, 'removeGroup');
- jest.spyOn(window, 'Flash').mockImplementation(() => {});
-
vm.leaveGroup();
expect(vm.targetGroup.isBeingRemoved).toBe(true);
expect(vm.service.leaveGroup).toHaveBeenCalledWith(childGroupItem.leavePath);
return waitForPromises().then(() => {
expect(vm.store.removeGroup).not.toHaveBeenCalled();
- expect(window.Flash).toHaveBeenCalledWith(message);
+ expect(createFlash).toHaveBeenCalledWith({ message });
expect(vm.targetGroup.isBeingRemoved).toBe(false);
});
});
@@ -341,15 +340,13 @@ describe('AppComponent', () => {
const message = 'Failed to leave the group. Please make sure you are not the only owner.';
jest.spyOn(vm.service, 'leaveGroup').mockRejectedValue({ status: 403 });
jest.spyOn(vm.store, 'removeGroup');
- jest.spyOn(window, 'Flash').mockImplementation(() => {});
-
vm.leaveGroup(childGroupItem, groupItem);
expect(vm.targetGroup.isBeingRemoved).toBe(true);
expect(vm.service.leaveGroup).toHaveBeenCalledWith(childGroupItem.leavePath);
return waitForPromises().then(() => {
expect(vm.store.removeGroup).not.toHaveBeenCalled();
- expect(window.Flash).toHaveBeenCalledWith(message);
+ expect(createFlash).toHaveBeenCalledWith({ message });
expect(vm.targetGroup.isBeingRemoved).toBe(false);
});
});
diff --git a/spec/frontend/groups/components/group_item_spec.js b/spec/frontend/groups/components/group_item_spec.js
index 546cdd3cd6f..2369685f506 100644
--- a/spec/frontend/groups/components/group_item_spec.js
+++ b/spec/frontend/groups/components/group_item_spec.js
@@ -162,11 +162,11 @@ describe('GroupItemComponent', () => {
wrapper = createComponent({ group });
});
- it('renders the group pending removal badge', () => {
+ it('renders the group pending deletion badge', () => {
const badgeEl = wrapper.vm.$el.querySelector('.badge-warning');
expect(badgeEl).toBeDefined();
- expect(badgeEl.innerHTML).toContain('pending removal');
+ expect(badgeEl.innerHTML).toContain('pending deletion');
});
});
@@ -176,10 +176,10 @@ describe('GroupItemComponent', () => {
wrapper = createComponent({ group });
});
- it('does not render the group pending removal badge', () => {
+ it('does not render the group pending deletion badge', () => {
const groupTextContainer = wrapper.vm.$el.querySelector('.group-text-container');
- expect(groupTextContainer).not.toContain('pending removal');
+ expect(groupTextContainer).not.toContain('pending deletion');
});
it('renders `item-actions` component and passes correct props to it', () => {
@@ -236,13 +236,13 @@ describe('GroupItemComponent', () => {
describe('schema.org props', () => {
describe('when showSchemaMarkup is disabled on the group', () => {
it.each(['itemprop', 'itemtype', 'itemscope'], 'it does not set %s', (attr) => {
- expect(wrapper.vm.$el.getAttribute(attr)).toBeNull();
+ expect(wrapper.attributes(attr)).toBeUndefined();
});
it.each(
['.js-group-avatar', '.js-group-name', '.js-group-description'],
'it does not set `itemprop` on sub-nodes',
(selector) => {
- expect(wrapper.vm.$el.querySelector(selector).getAttribute('itemprop')).toBeNull();
+ expect(wrapper.find(selector).attributes('itemprop')).toBeUndefined();
},
);
});
@@ -263,16 +263,16 @@ describe('GroupItemComponent', () => {
${'itemtype'} | ${'https://schema.org/Organization'}
${'itemprop'} | ${'subOrganization'}
`('it does set correct $attr', ({ attr, value } = {}) => {
- expect(wrapper.vm.$el.getAttribute(attr)).toBe(value);
+ expect(wrapper.attributes(attr)).toBe(value);
});
it.each`
selector | propValue
- ${'[data-testid="group-avatar"]'} | ${'logo'}
+ ${'img'} | ${'logo'}
${'[data-testid="group-name"]'} | ${'name'}
${'[data-testid="group-description"]'} | ${'description'}
`('it does set correct $selector', ({ selector, propValue } = {}) => {
- expect(wrapper.vm.$el.querySelector(selector).getAttribute('itemprop')).toBe(propValue);
+ expect(wrapper.find(selector).attributes('itemprop')).toBe(propValue);
});
});
});
diff --git a/spec/frontend/ide/components/ide_project_header_spec.js b/spec/frontend/ide/components/ide_project_header_spec.js
new file mode 100644
index 00000000000..fc39651c661
--- /dev/null
+++ b/spec/frontend/ide/components/ide_project_header_spec.js
@@ -0,0 +1,44 @@
+import { shallowMount } from '@vue/test-utils';
+import IDEProjectHeader from '~/ide/components/ide_project_header.vue';
+import ProjectAvatar from '~/vue_shared/components/project_avatar.vue';
+
+const mockProject = {
+ name: 'test proj',
+ avatar_url: 'https://gitlab.com',
+ path_with_namespace: 'path/with-namespace',
+ web_url: 'https://gitlab.com/project',
+};
+
+describe('IDE project header', () => {
+ let wrapper;
+
+ const findProjectAvatar = () => wrapper.findComponent(ProjectAvatar);
+ const findProjectLink = () => wrapper.find('[data-testid="go-to-project-link"');
+
+ const createComponent = () => {
+ wrapper = shallowMount(IDEProjectHeader, { propsData: { project: mockProject } });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('template', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders ProjectAvatar with correct props', () => {
+ expect(findProjectAvatar().props()).toMatchObject({
+ projectName: mockProject.name,
+ projectAvatarUrl: mockProject.avatar_url,
+ });
+ });
+
+ it('renders a link to the project URL', () => {
+ const link = findProjectLink();
+ expect(link.exists()).toBe(true);
+ expect(link.attributes('href')).toBe(mockProject.web_url);
+ });
+ });
+});
diff --git a/spec/frontend/ide/components/new_dropdown/modal_spec.js b/spec/frontend/ide/components/new_dropdown/modal_spec.js
index fce6ccf4b58..41111f5dbb4 100644
--- a/spec/frontend/ide/components/new_dropdown/modal_spec.js
+++ b/spec/frontend/ide/components/new_dropdown/modal_spec.js
@@ -184,9 +184,6 @@ describe('new file modal component', () => {
expect(createFlash).toHaveBeenCalledWith({
message: 'The name "test-path/test" is already taken in this directory.',
- type: 'alert',
- parent: expect.anything(),
- actionConfig: null,
fadeTransition: false,
addBodyClass: true,
});
diff --git a/spec/frontend/ide/components/repo_editor_spec.js b/spec/frontend/ide/components/repo_editor_spec.js
index 8e8fb31b15a..4bf3334ae6b 100644
--- a/spec/frontend/ide/components/repo_editor_spec.js
+++ b/spec/frontend/ide/components/repo_editor_spec.js
@@ -8,8 +8,8 @@ import waitForPromises from 'helpers/wait_for_promises';
import waitUsingRealTimer from 'helpers/wait_using_real_timer';
import { exampleConfigs, exampleFiles } from 'jest/ide/lib/editorconfig/mock_data';
import { EDITOR_CODE_INSTANCE_FN, EDITOR_DIFF_INSTANCE_FN } from '~/editor/constants';
-import EditorLite from '~/editor/editor_lite';
-import { EditorWebIdeExtension } from '~/editor/extensions/editor_lite_webide_ext';
+import { EditorWebIdeExtension } from '~/editor/extensions/source_editor_webide_ext';
+import SourceEditor from '~/editor/source_editor';
import RepoEditor from '~/ide/components/repo_editor.vue';
import {
leftSidebarViews,
@@ -123,8 +123,8 @@ describe('RepoEditor', () => {
const findPreviewTab = () => wrapper.find('[data-testid="preview-tab"]');
beforeEach(() => {
- createInstanceSpy = jest.spyOn(EditorLite.prototype, EDITOR_CODE_INSTANCE_FN);
- createDiffInstanceSpy = jest.spyOn(EditorLite.prototype, EDITOR_DIFF_INSTANCE_FN);
+ createInstanceSpy = jest.spyOn(SourceEditor.prototype, EDITOR_CODE_INSTANCE_FN);
+ createDiffInstanceSpy = jest.spyOn(SourceEditor.prototype, EDITOR_DIFF_INSTANCE_FN);
createModelSpy = jest.spyOn(monacoEditor, 'createModel');
jest.spyOn(service, 'getFileData').mockResolvedValue();
jest.spyOn(service, 'getRawFileData').mockResolvedValue();
@@ -252,7 +252,7 @@ describe('RepoEditor', () => {
);
it('installs the WebIDE extension', async () => {
- const extensionSpy = jest.spyOn(EditorLite, 'instanceApplyExtension');
+ const extensionSpy = jest.spyOn(SourceEditor, 'instanceApplyExtension');
await createComponent();
expect(extensionSpy).toHaveBeenCalled();
Reflect.ownKeys(EditorWebIdeExtension.prototype)
@@ -640,11 +640,12 @@ describe('RepoEditor', () => {
pasteImage();
await waitForFileContentChange();
+ expect(vm.$store.state.entries['foo/foo.png'].rawPath.startsWith('blob:')).toBe(true);
expect(vm.$store.state.entries['foo/foo.png']).toMatchObject({
path: 'foo/foo.png',
type: 'blob',
- content: 'Zm9v',
- rawPath: 'data:image/png;base64,Zm9v',
+ content: 'foo',
+ rawPath: vm.$store.state.entries['foo/foo.png'].rawPath,
});
});
diff --git a/spec/frontend/ide/services/index_spec.js b/spec/frontend/ide/services/index_spec.js
index 925446aa280..eacf1244d55 100644
--- a/spec/frontend/ide/services/index_spec.js
+++ b/spec/frontend/ide/services/index_spec.js
@@ -292,7 +292,7 @@ describe('IDE services', () => {
it('posts to usage endpoint', () => {
const TEST_PROJECT_PATH = 'foo/bar';
- const axiosURL = `${TEST_RELATIVE_URL_ROOT}/${TEST_PROJECT_PATH}/usage_ping/web_ide_pipelines_count`;
+ const axiosURL = `${TEST_RELATIVE_URL_ROOT}/${TEST_PROJECT_PATH}/service_ping/web_ide_pipelines_count`;
mock.onPost(axiosURL).reply(200);
diff --git a/spec/frontend/ide/stores/modules/clientside/actions_spec.js b/spec/frontend/ide/stores/modules/clientside/actions_spec.js
index c167d056039..88d7a630a90 100644
--- a/spec/frontend/ide/stores/modules/clientside/actions_spec.js
+++ b/spec/frontend/ide/stores/modules/clientside/actions_spec.js
@@ -5,7 +5,7 @@ import * as actions from '~/ide/stores/modules/clientside/actions';
import axios from '~/lib/utils/axios_utils';
const TEST_PROJECT_URL = `${TEST_HOST}/lorem/ipsum`;
-const TEST_USAGE_URL = `${TEST_PROJECT_URL}/usage_ping/web_ide_clientside_preview`;
+const TEST_USAGE_URL = `${TEST_PROJECT_URL}/service_ping/web_ide_clientside_preview`;
describe('IDE store module clientside actions', () => {
let rootGetters;
diff --git a/spec/frontend/ide/stores/utils_spec.js b/spec/frontend/ide/stores/utils_spec.js
index 8f7b8c5e311..79b6b66319e 100644
--- a/spec/frontend/ide/stores/utils_spec.js
+++ b/spec/frontend/ide/stores/utils_spec.js
@@ -604,7 +604,7 @@ describe('Multi-file store utils', () => {
let entries;
beforeEach(() => {
- const img = { content: '/base64/encoded/image+' };
+ const img = { content: 'png-gibberish', rawPath: 'blob:1234' };
mdFile = { path: 'path/to/some/directory/myfile.md' };
entries = {
// invalid (or lack of) extensions are also supported as long as there's
@@ -637,14 +637,14 @@ describe('Multi-file store utils', () => {
${'* ![img](img.png "title here")'} | ${'png'} | ${'img'} | ${'title here'}
`(
'correctly transforms markdown with uncommitted images: $markdownBefore',
- ({ markdownBefore, ext, imgAlt, imgTitle }) => {
+ ({ markdownBefore, imgAlt, imgTitle }) => {
mdFile.content = markdownBefore;
expect(utils.extractMarkdownImagesFromEntries(mdFile, entries)).toEqual({
content: '* {{gl_md_img_1}}',
images: {
'{{gl_md_img_1}}': {
- src: `data:image/${ext};base64,/base64/encoded/image+`,
+ src: 'blob:1234',
alt: imgAlt,
title: imgTitle,
},
diff --git a/spec/frontend/import_entities/components/group_dropdown_spec.js b/spec/frontend/import_entities/components/group_dropdown_spec.js
new file mode 100644
index 00000000000..f7aa0e889ea
--- /dev/null
+++ b/spec/frontend/import_entities/components/group_dropdown_spec.js
@@ -0,0 +1,44 @@
+import { GlSearchBoxByType, GlDropdown } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
+import GroupDropdown from '~/import_entities/components/group_dropdown.vue';
+
+describe('Import entities group dropdown component', () => {
+ let wrapper;
+ let namespacesTracker;
+
+ const createComponent = (propsData) => {
+ namespacesTracker = jest.fn();
+
+ wrapper = shallowMount(GroupDropdown, {
+ scopedSlots: {
+ default: namespacesTracker,
+ },
+ stubs: { GlDropdown },
+ propsData,
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('passes namespaces from props to default slot', () => {
+ const namespaces = ['ns1', 'ns2'];
+ createComponent({ namespaces });
+
+ expect(namespacesTracker).toHaveBeenCalledWith({ namespaces });
+ });
+
+ it('filters namespaces based on user input', async () => {
+ const namespaces = ['match1', 'some unrelated', 'match2'];
+ createComponent({ namespaces });
+
+ namespacesTracker.mockReset();
+ wrapper.find(GlSearchBoxByType).vm.$emit('input', 'match');
+
+ await nextTick();
+
+ expect(namespacesTracker).toHaveBeenCalledWith({ namespaces: ['match1', 'match2'] });
+ });
+});
diff --git a/spec/frontend/import_entities/import_groups/components/import_table_row_spec.js b/spec/frontend/import_entities/import_groups/components/import_table_row_spec.js
index aa6a40cad18..654a8fd00d3 100644
--- a/spec/frontend/import_entities/import_groups/components/import_table_row_spec.js
+++ b/spec/frontend/import_entities/import_groups/components/import_table_row_spec.js
@@ -1,8 +1,9 @@
-import { GlButton, GlDropdown, GlDropdownItem, GlLink, GlFormInput } from '@gitlab/ui';
+import { GlButton, GlDropdownItem, GlLink, GlFormInput } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
+import ImportGroupDropdown from '~/import_entities/components/group_dropdown.vue';
import { STATUSES } from '~/import_entities/constants';
import ImportTableRow from '~/import_entities/import_groups/components/import_table_row.vue';
import addValidationErrorMutation from '~/import_entities/import_groups/graphql/mutations/add_validation_error.mutation.graphql';
@@ -41,7 +42,7 @@ describe('import table row', () => {
};
const findImportButton = () => findByText(GlButton, 'Import');
const findNameInput = () => wrapper.find(GlFormInput);
- const findNamespaceDropdown = () => wrapper.find(GlDropdown);
+ const findNamespaceDropdown = () => wrapper.find(ImportGroupDropdown);
const createComponent = (props) => {
apolloProvider = createMockApollo([
@@ -65,6 +66,7 @@ describe('import table row', () => {
wrapper = shallowMount(ImportTableRow, {
apolloProvider,
+ stubs: { ImportGroupDropdown },
propsData: {
availableNamespaces: availableNamespacesFixture,
groupPathRegex: /.*/,
diff --git a/spec/frontend/import_entities/import_projects/components/import_projects_table_spec.js b/spec/frontend/import_entities/import_projects/components/import_projects_table_spec.js
index d9f4168f1a5..0e748baa313 100644
--- a/spec/frontend/import_entities/import_projects/components/import_projects_table_spec.js
+++ b/spec/frontend/import_entities/import_projects/components/import_projects_table_spec.js
@@ -11,6 +11,8 @@ import state from '~/import_entities/import_projects/store/state';
describe('ImportProjectsTable', () => {
let wrapper;
+ const USER_NAMESPACE = 'root';
+
const findFilterField = () =>
wrapper
.findAllComponents(GlFormInput)
@@ -48,7 +50,7 @@ describe('ImportProjectsTable', () => {
localVue.use(Vuex);
const store = new Vuex.Store({
- state: { ...state(), ...initialState },
+ state: { ...state(), defaultTargetNamespace: USER_NAMESPACE, ...initialState },
getters: {
...getters,
...customGetters,
diff --git a/spec/frontend/import_entities/import_projects/components/provider_repo_table_row_spec.js b/spec/frontend/import_entities/import_projects/components/provider_repo_table_row_spec.js
index e15389be53a..72640f3d601 100644
--- a/spec/frontend/import_entities/import_projects/components/provider_repo_table_row_spec.js
+++ b/spec/frontend/import_entities/import_projects/components/provider_repo_table_row_spec.js
@@ -1,11 +1,11 @@
-import { GlBadge, GlButton } from '@gitlab/ui';
+import { GlBadge, GlButton, GlDropdown } from '@gitlab/ui';
import { createLocalVue, shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
import Vuex from 'vuex';
import { STATUSES } from '~/import_entities//constants';
+import ImportGroupDropdown from '~/import_entities/components/group_dropdown.vue';
import ImportStatus from '~/import_entities/components/import_status.vue';
import ProviderRepoTableRow from '~/import_entities/import_projects/components/provider_repo_table_row.vue';
-import Select2Select from '~/vue_shared/components/select2_select.vue';
describe('ProviderRepoTableRow', () => {
let wrapper;
@@ -16,10 +16,8 @@ describe('ProviderRepoTableRow', () => {
newName: 'newName',
};
- const availableNamespaces = [
- { text: 'Groups', children: [{ id: 'test', text: 'test' }] },
- { text: 'Users', children: [{ id: 'root', text: 'root' }] },
- ];
+ const availableNamespaces = ['test'];
+ const userNamespace = 'root';
function initStore(initialState) {
const store = new Vuex.Store({
@@ -48,7 +46,7 @@ describe('ProviderRepoTableRow', () => {
wrapper = shallowMount(ProviderRepoTableRow, {
localVue,
store,
- propsData: { availableNamespaces, ...props },
+ propsData: { availableNamespaces, userNamespace, ...props },
});
}
@@ -81,9 +79,8 @@ describe('ProviderRepoTableRow', () => {
expect(wrapper.find(ImportStatus).props().status).toBe(STATUSES.NONE);
});
- it('renders a select2 namespace select', () => {
- expect(wrapper.find(Select2Select).exists()).toBe(true);
- expect(wrapper.find(Select2Select).props().options.data).toBe(availableNamespaces);
+ it('renders a group namespace select', () => {
+ expect(wrapper.find(ImportGroupDropdown).props().namespaces).toBe(availableNamespaces);
});
it('renders import button', () => {
@@ -133,7 +130,7 @@ describe('ProviderRepoTableRow', () => {
});
it('does not renders a namespace select', () => {
- expect(wrapper.find(Select2Select).exists()).toBe(false);
+ expect(wrapper.find(GlDropdown).exists()).toBe(false);
});
it('does not render import button', () => {
diff --git a/spec/frontend/incidents_settings/components/__snapshots__/incidents_settings_tabs_spec.js.snap b/spec/frontend/incidents_settings/components/__snapshots__/incidents_settings_tabs_spec.js.snap
index 4f70f908c4a..1e3c344ce65 100644
--- a/spec/frontend/incidents_settings/components/__snapshots__/incidents_settings_tabs_spec.js.snap
+++ b/spec/frontend/incidents_settings/components/__snapshots__/incidents_settings_tabs_spec.js.snap
@@ -39,7 +39,9 @@ exports[`IncidentsSettingTabs should render the component 1`] = `
class="settings-content"
>
<gl-tabs-stub
+ queryparamname="tab"
theme="indigo"
+ value="0"
>
<!---->
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 f4342c56f98..1b0253480e0 100644
--- a/spec/frontend/incidents_settings/components/incidents_settings_service_spec.js
+++ b/spec/frontend/incidents_settings/components/incidents_settings_service_spec.js
@@ -39,7 +39,6 @@ describe('IncidentsSettingsService', () => {
return service.updateSettings({}).then(() => {
expect(createFlash).toHaveBeenCalledWith({
message: expect.stringContaining(ERROR_MSG),
- type: 'alert',
});
});
});
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 eb5f7e9fe40..2860d3cc37a 100644
--- a/spec/frontend/integrations/edit/components/jira_issues_fields_spec.js
+++ b/spec/frontend/integrations/edit/components/jira_issues_fields_spec.js
@@ -2,7 +2,6 @@ import { GlFormCheckbox, GlFormInput } from '@gitlab/ui';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import JiraIssuesFields from '~/integrations/edit/components/jira_issues_fields.vue';
-import JiraUpgradeCta from '~/integrations/edit/components/jira_upgrade_cta.vue';
import eventHub from '~/integrations/edit/event_hub';
import { createStore } from '~/integrations/edit/store';
@@ -14,6 +13,7 @@ describe('JiraIssuesFields', () => {
editProjectPath: '/edit',
showJiraIssuesIntegration: true,
showJiraVulnerabilitiesIntegration: true,
+ upgradePlanPath: 'https://gitlab.com',
};
const createComponent = ({ isInheriting = false, props, ...options } = {}) => {
@@ -37,60 +37,79 @@ describe('JiraIssuesFields', () => {
const findEnableCheckboxDisabled = () =>
findEnableCheckbox().find('[type=checkbox]').attributes('disabled');
const findProjectKey = () => wrapper.findComponent(GlFormInput);
- const findJiraUpgradeCta = () => wrapper.findComponent(JiraUpgradeCta);
+ const findPremiumUpgradeCTA = () => wrapper.findByTestId('premium-upgrade-cta');
+ const findUltimateUpgradeCTA = () => wrapper.findByTestId('ultimate-upgrade-cta');
const findJiraForVulnerabilities = () => wrapper.findByTestId('jira-for-vulnerabilities');
const setEnableCheckbox = async (isEnabled = true) =>
findEnableCheckbox().vm.$emit('input', isEnabled);
- describe('jira issues call to action', () => {
- it('shows the premium message', () => {
- createComponent({
- props: { showJiraIssuesIntegration: false },
- });
-
- expect(findJiraUpgradeCta().props()).toMatchObject({
- showPremiumMessage: true,
- showUltimateMessage: false,
- });
- });
-
- it('shows the ultimate message', () => {
- createComponent({
- props: {
- showJiraIssuesIntegration: true,
- showJiraVulnerabilitiesIntegration: false,
- },
- });
-
- expect(findJiraUpgradeCta().props()).toMatchObject({
- showPremiumMessage: false,
- showUltimateMessage: true,
- });
- });
- });
-
describe('template', () => {
- describe('upgrade banner for non-Premium user', () => {
- beforeEach(() => {
- createComponent({ props: { initialProjectKey: '', showJiraIssuesIntegration: false } });
- });
+ describe.each`
+ showJiraIssuesIntegration | showJiraVulnerabilitiesIntegration
+ ${false} | ${false}
+ ${false} | ${true}
+ ${true} | ${false}
+ ${true} | ${true}
+ `(
+ 'when `showJiraIssuesIntegration` is $jiraIssues and `showJiraVulnerabilitiesIntegration` is $jiraVulnerabilities',
+ ({ showJiraIssuesIntegration, showJiraVulnerabilitiesIntegration }) => {
+ beforeEach(() => {
+ createComponent({
+ props: {
+ showJiraIssuesIntegration,
+ showJiraVulnerabilitiesIntegration,
+ },
+ });
+ });
- it('does not show checkbox and input field', () => {
- expect(findEnableCheckbox().exists()).toBe(false);
- expect(findProjectKey().exists()).toBe(false);
- });
- });
+ if (showJiraIssuesIntegration) {
+ it('renders checkbox and input field', () => {
+ expect(findEnableCheckbox().exists()).toBe(true);
+ expect(findEnableCheckboxDisabled()).toBeUndefined();
+ expect(findProjectKey().exists()).toBe(true);
+ });
+
+ it('does not render the Premium CTA', () => {
+ expect(findPremiumUpgradeCTA().exists()).toBe(false);
+ });
+
+ if (!showJiraVulnerabilitiesIntegration) {
+ it.each`
+ scenario | enableJiraIssues
+ ${'when "Enable Jira issues" is checked, renders Ultimate upgrade CTA'} | ${true}
+ ${'when "Enable Jira issues" is unchecked, does not render Ultimate upgrade CTA'} | ${false}
+ `('$scenario', async ({ enableJiraIssues }) => {
+ if (enableJiraIssues) {
+ await setEnableCheckbox();
+ }
+ expect(findUltimateUpgradeCTA().exists()).toBe(enableJiraIssues);
+ });
+ }
+ } else {
+ it('does not render checkbox and input field', () => {
+ expect(findEnableCheckbox().exists()).toBe(false);
+ expect(findProjectKey().exists()).toBe(false);
+ });
+
+ it('renders the Premium CTA', () => {
+ const premiumUpgradeCTA = findPremiumUpgradeCTA();
+
+ expect(premiumUpgradeCTA.exists()).toBe(true);
+ expect(premiumUpgradeCTA.props('upgradePlanPath')).toBe(defaultProps.upgradePlanPath);
+ });
+ }
+
+ it('does not render the Ultimate CTA', () => {
+ expect(findUltimateUpgradeCTA().exists()).toBe(false);
+ });
+ },
+ );
describe('Enable Jira issues checkbox', () => {
beforeEach(() => {
createComponent({ props: { initialProjectKey: '' } });
});
- it('renders enabled checkbox', () => {
- expect(findEnableCheckbox().exists()).toBe(true);
- expect(findEnableCheckboxDisabled()).toBeUndefined();
- });
-
it('renders disabled project_key input', () => {
const projectKey = findProjectKey();
@@ -99,10 +118,6 @@ describe('JiraIssuesFields', () => {
expect(projectKey.attributes('required')).toBeUndefined();
});
- it('does not show upgrade banner', () => {
- expect(findJiraUpgradeCta().exists()).toBe(false);
- });
-
// As per https://vuejs.org/v2/guide/forms.html#Checkbox-1,
// browsers don't include unchecked boxes in form submissions.
it('includes issues_enabled as false even if unchecked', () => {
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 eabbea84234..b828b5d8a04 100644
--- a/spec/frontend/invite_members/components/invite_members_modal_spec.js
+++ b/spec/frontend/invite_members/components/invite_members_modal_spec.js
@@ -1,11 +1,27 @@
-import { GlDropdown, GlDropdownItem, GlDatepicker, GlSprintf, GlLink, GlModal } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
+import {
+ GlDropdown,
+ GlDropdownItem,
+ GlDatepicker,
+ GlFormGroup,
+ GlSprintf,
+ GlLink,
+ GlModal,
+} from '@gitlab/ui';
+import MockAdapter from 'axios-mock-adapter';
import { stubComponent } from 'helpers/stub_component';
+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 MembersTokenSelect from '~/invite_members/components/members_token_select.vue';
import { INVITE_MEMBERS_IN_COMMENT } from '~/invite_members/constants';
+import axios from '~/lib/utils/axios_utils';
+import httpStatus from '~/lib/utils/http_status';
+import { apiPaths, membersApiResponse, invitationsApiResponse } from '../mock_data/api_responses';
+
+let wrapper;
+let mock;
jest.mock('~/experimentation/experiment_tracking');
@@ -26,10 +42,16 @@ const user3 = {
username: 'one_2',
avatar_url: '',
};
+const user4 = {
+ id: 'user-defined-token',
+ name: 'email4@example.com',
+ username: 'one_4',
+ avatar_url: '',
+};
const sharedGroup = { id: '981' };
const createComponent = (data = {}, props = {}) => {
- return shallowMount(InviteMembersModal, {
+ wrapper = shallowMountExtended(InviteMembersModal, {
propsData: {
id,
name,
@@ -51,46 +73,56 @@ const createComponent = (data = {}, props = {}) => {
GlDropdown: true,
GlDropdownItem: true,
GlSprintf,
+ GlFormGroup: stubComponent(GlFormGroup, {
+ props: ['state', 'invalidFeedback'],
+ }),
},
});
};
const createInviteMembersToProjectWrapper = () => {
- return createComponent({ inviteeType: 'members' }, { isProject: true });
+ createComponent({ inviteeType: 'members' }, { isProject: true });
};
const createInviteMembersToGroupWrapper = () => {
- return createComponent({ inviteeType: 'members' }, { isProject: false });
+ createComponent({ inviteeType: 'members' }, { isProject: false });
};
const createInviteGroupToProjectWrapper = () => {
- return createComponent({ inviteeType: 'group' }, { isProject: true });
+ createComponent({ inviteeType: 'group' }, { isProject: true });
};
const createInviteGroupToGroupWrapper = () => {
- return createComponent({ inviteeType: 'group' }, { isProject: false });
+ createComponent({ inviteeType: 'group' }, { isProject: false });
};
-describe('InviteMembersModal', () => {
- let wrapper;
+beforeEach(() => {
+ gon.api_version = 'v4';
+ mock = new MockAdapter(axios);
+});
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
+afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ mock.restore();
+});
+describe('InviteMembersModal', () => {
const findDropdown = () => wrapper.findComponent(GlDropdown);
const findDropdownItems = () => findDropdown().findAllComponents(GlDropdownItem);
const findDatepicker = () => wrapper.findComponent(GlDatepicker);
const findLink = () => wrapper.findComponent(GlLink);
const findIntroText = () => wrapper.find({ ref: 'introText' }).text();
- const findCancelButton = () => wrapper.findComponent({ ref: 'cancelButton' });
- const findInviteButton = () => wrapper.findComponent({ ref: 'inviteButton' });
+ const findCancelButton = () => wrapper.findByTestId('cancel-button');
+ const findInviteButton = () => wrapper.findByTestId('invite-button');
const clickInviteButton = () => findInviteButton().vm.$emit('click');
+ const findMembersFormGroup = () => wrapper.findByTestId('members-form-group');
+ const membersFormGroupInvalidFeedback = () => findMembersFormGroup().props('invalidFeedback');
+ const findMembersSelect = () => wrapper.findComponent(MembersTokenSelect);
describe('rendering the modal', () => {
beforeEach(() => {
- wrapper = createComponent();
+ createComponent();
});
it('renders the modal with the correct title', () => {
@@ -132,7 +164,7 @@ describe('InviteMembersModal', () => {
describe('when inviting to a project', () => {
describe('when inviting members', () => {
it('includes the correct invitee, type, and formatted name', () => {
- wrapper = createInviteMembersToProjectWrapper();
+ createInviteMembersToProjectWrapper();
expect(findIntroText()).toBe("You're inviting members to the test name project.");
});
@@ -140,7 +172,7 @@ describe('InviteMembersModal', () => {
describe('when sharing with a group', () => {
it('includes the correct invitee, type, and formatted name', () => {
- wrapper = createInviteGroupToProjectWrapper();
+ createInviteGroupToProjectWrapper();
expect(findIntroText()).toBe("You're inviting a group to the test name project.");
});
@@ -150,7 +182,7 @@ describe('InviteMembersModal', () => {
describe('when inviting to a group', () => {
describe('when inviting members', () => {
it('includes the correct invitee, type, and formatted name', () => {
- wrapper = createInviteMembersToGroupWrapper();
+ createInviteMembersToGroupWrapper();
expect(findIntroText()).toBe("You're inviting members to the test name group.");
});
@@ -158,7 +190,7 @@ describe('InviteMembersModal', () => {
describe('when sharing with a group', () => {
it('includes the correct invitee, type, and formatted name', () => {
- wrapper = createInviteGroupToGroupWrapper();
+ createInviteGroupToGroupWrapper();
expect(findIntroText()).toBe("You're inviting a group to the test name group.");
});
@@ -167,22 +199,30 @@ describe('InviteMembersModal', () => {
});
describe('submitting the invite form', () => {
- const apiErrorMessage = 'Member already exists';
+ const mockMembersApi = (code, data) => {
+ mock.onPost(apiPaths.GROUPS_MEMBERS).reply(code, data);
+ };
+ const mockInvitationsApi = (code, data) => {
+ mock.onPost(apiPaths.GROUPS_INVITATIONS).reply(code, data);
+ };
+
+ const expectedEmailRestrictedError =
+ "email 'email@example.com' does not match the allowed domains: example1.org";
+ const expectedSyntaxError = 'email contains an invalid email address';
describe('when inviting an existing user to group by user ID', () => {
const postData = {
- user_id: '1',
+ user_id: '1,2',
access_level: defaultAccessLevel,
expires_at: undefined,
invite_source: inviteSource,
format: 'json',
};
- describe('when invites are sent successfully', () => {
+ describe('when member is added successfully', () => {
beforeEach(() => {
- wrapper = createInviteMembersToGroupWrapper();
+ createComponent({ newUsersToInvite: [user1, user2] });
- wrapper.setData({ newUsersToInvite: [user1] });
wrapper.vm.$toast = { show: jest.fn() };
jest.spyOn(Api, 'addGroupMembersByUserId').mockResolvedValue({ data: postData });
jest.spyOn(wrapper.vm, 'showToastMessageSuccess');
@@ -190,54 +230,102 @@ describe('InviteMembersModal', () => {
clickInviteButton();
});
- it('calls Api addGroupMembersByUserId with the correct params', () => {
+ it('calls Api addGroupMembersByUserId with the correct params', async () => {
+ await waitForPromises;
+
expect(Api.addGroupMembersByUserId).toHaveBeenCalledWith(id, postData);
});
- it('displays the successful toastMessage', () => {
+ it('displays the successful toastMessage', async () => {
+ await waitForPromises;
+
expect(wrapper.vm.showToastMessageSuccess).toHaveBeenCalled();
});
});
- describe('when the invite received an api error message', () => {
+ describe('when member is not added successfully', () => {
beforeEach(() => {
- wrapper = createComponent({ newUsersToInvite: [user1] });
+ createInviteMembersToGroupWrapper();
- wrapper.vm.$toast = { show: jest.fn() };
- jest
- .spyOn(Api, 'addGroupMembersByUserId')
- .mockRejectedValue({ response: { data: { message: apiErrorMessage } } });
- jest.spyOn(wrapper.vm, 'showToastMessageError');
+ wrapper.setData({ newUsersToInvite: [user1] });
+ });
+
+ it('displays "Member already exists" api message for http status conflict', async () => {
+ mockMembersApi(httpStatus.CONFLICT, membersApiResponse.MEMBER_ALREADY_EXISTS);
clickInviteButton();
+
+ await waitForPromises();
+
+ expect(membersFormGroupInvalidFeedback()).toBe('Member already exists');
+ expect(findMembersFormGroup().props('state')).toBe(false);
+ expect(findMembersSelect().props('validationState')).toBe(false);
});
- it('displays the apiErrorMessage in the toastMessage', async () => {
+ it('clears the invalid state and message once the list of members to invite is cleared', async () => {
+ mockMembersApi(httpStatus.CONFLICT, membersApiResponse.MEMBER_ALREADY_EXISTS);
+
+ clickInviteButton();
+
await waitForPromises();
- expect(wrapper.vm.showToastMessageError).toHaveBeenCalledWith({
- response: { data: { message: apiErrorMessage } },
- });
+ expect(membersFormGroupInvalidFeedback()).toBe('Member already exists');
+ expect(findMembersFormGroup().props('state')).toBe(false);
+ expect(findMembersSelect().props('validationState')).toBe(false);
+
+ findMembersSelect().vm.$emit('clear');
+
+ await waitForPromises();
+
+ expect(membersFormGroupInvalidFeedback()).toBe('');
+ expect(findMembersFormGroup().props('state')).not.toBe(false);
+ expect(findMembersSelect().props('validationState')).not.toBe(false);
});
- });
- describe('when any invite failed for any other reason', () => {
- beforeEach(() => {
- wrapper = createComponent({ newUsersToInvite: [user1, user2] });
+ it('displays the generic error for http server error', async () => {
+ mockMembersApi(httpStatus.INTERNAL_SERVER_ERROR, 'Request failed with status code 500');
- wrapper.vm.$toast = { show: jest.fn() };
- jest
- .spyOn(Api, 'addGroupMembersByUserId')
- .mockRejectedValue({ response: { data: { success: false } } });
- jest.spyOn(wrapper.vm, 'showToastMessageError');
+ clickInviteButton();
+
+ await waitForPromises();
+
+ expect(membersFormGroupInvalidFeedback()).toBe('Something went wrong');
+ });
+
+ it('displays the restricted user api message for response with bad request', async () => {
+ mockMembersApi(httpStatus.BAD_REQUEST, membersApiResponse.SINGLE_USER_RESTRICTED);
clickInviteButton();
+
+ await waitForPromises();
+
+ expect(membersFormGroupInvalidFeedback()).toBe(expectedEmailRestrictedError);
});
- it('displays the generic error toastMessage', async () => {
+ it('displays the first part of the error when multiple existing users are restricted by email', async () => {
+ mockMembersApi(httpStatus.CREATED, membersApiResponse.MULTIPLE_USERS_RESTRICTED);
+
+ clickInviteButton();
+
await waitForPromises();
- expect(wrapper.vm.showToastMessageError).toHaveBeenCalled();
+ expect(membersFormGroupInvalidFeedback()).toBe(
+ "root: User email 'admin@example.com' does not match the allowed domain of example2.com",
+ );
+ expect(findMembersSelect().props('validationState')).toBe(false);
+ });
+
+ it('displays an access_level error message received for the existing user', async () => {
+ mockMembersApi(httpStatus.BAD_REQUEST, membersApiResponse.SINGLE_USER_ACCESS_LEVEL);
+
+ clickInviteButton();
+
+ await waitForPromises();
+
+ expect(membersFormGroupInvalidFeedback()).toBe(
+ 'should be greater than or equal to Owner inherited membership from group Gitlab Org',
+ );
+ expect(findMembersSelect().props('validationState')).toBe(false);
});
});
});
@@ -253,7 +341,7 @@ describe('InviteMembersModal', () => {
describe('when invites are sent successfully', () => {
beforeEach(() => {
- wrapper = createComponent({ newUsersToInvite: [user3] });
+ createComponent({ newUsersToInvite: [user3] });
wrapper.vm.$toast = { show: jest.fn() };
jest.spyOn(Api, 'inviteGroupMembersByEmail').mockResolvedValue({ data: postData });
@@ -271,23 +359,84 @@ describe('InviteMembersModal', () => {
});
});
- describe('when any invite failed for any reason', () => {
+ describe('when invites are not sent successfully', () => {
beforeEach(() => {
- wrapper = createComponent({ newUsersToInvite: [user1, user2] });
+ createInviteMembersToGroupWrapper();
+
+ wrapper.setData({ newUsersToInvite: [user3] });
+ });
+
+ it('displays the api error for invalid email syntax', async () => {
+ mockInvitationsApi(httpStatus.BAD_REQUEST, invitationsApiResponse.EMAIL_INVALID);
+
+ clickInviteButton();
+
+ await waitForPromises();
+
+ expect(membersFormGroupInvalidFeedback()).toBe(expectedSyntaxError);
+ expect(findMembersSelect().props('validationState')).toBe(false);
+ });
+ it('displays the restricted email error when restricted email is invited', async () => {
+ mockInvitationsApi(httpStatus.CREATED, invitationsApiResponse.EMAIL_RESTRICTED);
+
+ clickInviteButton();
+
+ await waitForPromises();
+
+ expect(membersFormGroupInvalidFeedback()).toContain(expectedEmailRestrictedError);
+ expect(findMembersSelect().props('validationState')).toBe(false);
+ });
+
+ it('displays the successful toast message when email has already been invited', async () => {
+ mockInvitationsApi(httpStatus.CREATED, invitationsApiResponse.EMAIL_TAKEN);
wrapper.vm.$toast = { show: jest.fn() };
- jest
- .spyOn(Api, 'addGroupMembersByUserId')
- .mockRejectedValue({ response: { data: { success: false } } });
- jest.spyOn(wrapper.vm, 'showToastMessageError');
+ jest.spyOn(wrapper.vm, 'showToastMessageSuccess');
+
+ clickInviteButton();
+
+ await waitForPromises();
+
+ expect(wrapper.vm.showToastMessageSuccess).toHaveBeenCalled();
+ expect(findMembersSelect().props('validationState')).toBe(null);
+ });
+
+ it('displays the first error message when multiple emails return a restricted error message', async () => {
+ mockInvitationsApi(httpStatus.CREATED, invitationsApiResponse.MULTIPLE_EMAIL_RESTRICTED);
clickInviteButton();
+
+ await waitForPromises();
+
+ expect(membersFormGroupInvalidFeedback()).toContain(expectedEmailRestrictedError);
+ expect(findMembersSelect().props('validationState')).toBe(false);
+ });
+
+ it('displays the invalid syntax error for bad request', async () => {
+ mockInvitationsApi(httpStatus.BAD_REQUEST, invitationsApiResponse.ERROR_EMAIL_INVALID);
+
+ clickInviteButton();
+
+ await waitForPromises();
+
+ expect(membersFormGroupInvalidFeedback()).toBe(expectedSyntaxError);
+ expect(findMembersSelect().props('validationState')).toBe(false);
});
+ });
+
+ describe('when multiple emails are invited at the same time', () => {
+ it('displays the invalid syntax error if one of the emails is invalid', async () => {
+ createInviteMembersToGroupWrapper();
+
+ wrapper.setData({ newUsersToInvite: [user3, user4] });
+ mockInvitationsApi(httpStatus.CREATED, invitationsApiResponse.ERROR_EMAIL_INVALID);
+
+ clickInviteButton();
- it('displays the generic error toastMessage', async () => {
await waitForPromises();
- expect(wrapper.vm.showToastMessageError).toHaveBeenCalled();
+ expect(membersFormGroupInvalidFeedback()).toBe(expectedSyntaxError);
+ expect(findMembersSelect().props('validationState')).toBe(false);
});
});
});
@@ -305,7 +454,7 @@ describe('InviteMembersModal', () => {
describe('when invites are sent successfully', () => {
beforeEach(() => {
- wrapper = createComponent({ newUsersToInvite: [user1, user3] });
+ createComponent({ newUsersToInvite: [user1, user3] });
wrapper.vm.$toast = { show: jest.fn() };
jest.spyOn(Api, 'inviteGroupMembersByEmail').mockResolvedValue({ data: postData });
@@ -350,24 +499,20 @@ describe('InviteMembersModal', () => {
describe('when any invite failed for any reason', () => {
beforeEach(() => {
- wrapper = createComponent({ newUsersToInvite: [user1, user3] });
+ createInviteMembersToGroupWrapper();
- wrapper.vm.$toast = { show: jest.fn() };
-
- jest
- .spyOn(Api, 'inviteGroupMembersByEmail')
- .mockRejectedValue({ response: { data: { success: false } } });
+ wrapper.setData({ newUsersToInvite: [user1, user3] });
- jest.spyOn(Api, 'addGroupMembersByUserId').mockResolvedValue({ data: postData });
- jest.spyOn(wrapper.vm, 'showToastMessageError');
+ mockInvitationsApi(httpStatus.BAD_REQUEST, invitationsApiResponse.EMAIL_INVALID);
+ mockMembersApi(httpStatus.OK, '200 OK');
clickInviteButton();
});
- it('displays the generic error toastMessage', async () => {
+ it('displays the first error message', async () => {
await waitForPromises();
- expect(wrapper.vm.showToastMessageError).toHaveBeenCalled();
+ expect(membersFormGroupInvalidFeedback()).toBe(expectedSyntaxError);
});
});
});
@@ -382,7 +527,7 @@ describe('InviteMembersModal', () => {
};
beforeEach(() => {
- wrapper = createComponent({ groupToBeSharedWith: sharedGroup });
+ createComponent({ groupToBeSharedWith: sharedGroup });
wrapper.setData({ inviteeType: 'group' });
wrapper.vm.$toast = { show: jest.fn() };
@@ -403,7 +548,7 @@ describe('InviteMembersModal', () => {
describe('when sharing the group fails', () => {
beforeEach(() => {
- wrapper = createComponent({ groupToBeSharedWith: sharedGroup });
+ createComponent({ groupToBeSharedWith: sharedGroup });
wrapper.setData({ inviteeType: 'group' });
wrapper.vm.$toast = { show: jest.fn() };
@@ -412,22 +557,20 @@ describe('InviteMembersModal', () => {
.spyOn(Api, 'groupShareWithGroup')
.mockRejectedValue({ response: { data: { success: false } } });
- jest.spyOn(wrapper.vm, 'showToastMessageError');
-
clickInviteButton();
});
- it('displays the generic error toastMessage', async () => {
+ it('displays the generic error message', async () => {
await waitForPromises();
- expect(wrapper.vm.showToastMessageError).toHaveBeenCalled();
+ expect(membersFormGroupInvalidFeedback()).toBe('Something went wrong');
});
});
});
describe('tracking', () => {
beforeEach(() => {
- wrapper = createComponent({ newUsersToInvite: [user3] });
+ createComponent({ newUsersToInvite: [user3] });
wrapper.vm.$toast = { show: jest.fn() };
jest.spyOn(Api, 'inviteGroupMembersByEmail').mockResolvedValue({});
diff --git a/spec/frontend/invite_members/components/members_token_select_spec.js b/spec/frontend/invite_members/components/members_token_select_spec.js
index f6e79d3607f..12db7e42464 100644
--- a/spec/frontend/invite_members/components/members_token_select_spec.js
+++ b/spec/frontend/invite_members/components/members_token_select_spec.js
@@ -115,6 +115,21 @@ describe('MembersTokenSelect', () => {
expect(wrapper.emitted().input[0][0]).toEqual([user1, user2]);
});
});
+
+ describe('when user is removed', () => {
+ it('emits `clear` event', () => {
+ findTokenSelector().vm.$emit('token-remove', [user1]);
+
+ expect(wrapper.emitted('clear')).toEqual([[]]);
+ });
+
+ it('does not emit `clear` event when there are still tokens selected', () => {
+ findTokenSelector().vm.$emit('input', [user1, user2]);
+ findTokenSelector().vm.$emit('token-remove', [user1]);
+
+ expect(wrapper.emitted('clear')).toBeUndefined();
+ });
+ });
});
describe('when text input is blurred', () => {
diff --git a/spec/frontend/invite_members/mock_data/api_responses.js b/spec/frontend/invite_members/mock_data/api_responses.js
new file mode 100644
index 00000000000..79b56a33708
--- /dev/null
+++ b/spec/frontend/invite_members/mock_data/api_responses.js
@@ -0,0 +1,74 @@
+const INVITATIONS_API_EMAIL_INVALID = {
+ message: { error: 'email contains an invalid email address' },
+};
+
+const INVITATIONS_API_ERROR_EMAIL_INVALID = {
+ error: 'email contains an invalid email address',
+};
+
+const INVITATIONS_API_EMAIL_RESTRICTED = {
+ message: {
+ 'email@example.com':
+ "Invite email 'email@example.com' does not match the allowed domains: example1.org",
+ },
+ status: 'error',
+};
+
+const INVITATIONS_API_MULTIPLE_EMAIL_RESTRICTED = {
+ message: {
+ 'email@example.com':
+ "Invite email email 'email@example.com' does not match the allowed domains: example1.org",
+ 'email4@example.com':
+ "Invite email email 'email4@example.com' does not match the allowed domains: example1.org",
+ },
+ status: 'error',
+};
+
+const INVITATIONS_API_EMAIL_TAKEN = {
+ message: {
+ 'email@example2.com': 'Invite email has already been taken',
+ },
+ status: 'error',
+};
+
+const MEMBERS_API_MEMBER_ALREADY_EXISTS = {
+ message: 'Member already exists',
+};
+
+const MEMBERS_API_SINGLE_USER_RESTRICTED = {
+ message: { user: ["email 'email@example.com' does not match the allowed domains: example1.org"] },
+};
+
+const MEMBERS_API_SINGLE_USER_ACCESS_LEVEL = {
+ message: {
+ access_level: [
+ 'should be greater than or equal to Owner inherited membership from group Gitlab Org',
+ ],
+ },
+};
+
+const MEMBERS_API_MULTIPLE_USERS_RESTRICTED = {
+ message:
+ "root: User email 'admin@example.com' does not match the allowed domain of example2.com and user18: User email 'user18@example.org' does not match the allowed domain of example2.com",
+ status: 'error',
+};
+
+export const apiPaths = {
+ GROUPS_MEMBERS: '/api/v4/groups/1/members',
+ GROUPS_INVITATIONS: '/api/v4/groups/1/invitations',
+};
+
+export const membersApiResponse = {
+ MEMBER_ALREADY_EXISTS: MEMBERS_API_MEMBER_ALREADY_EXISTS,
+ SINGLE_USER_ACCESS_LEVEL: MEMBERS_API_SINGLE_USER_ACCESS_LEVEL,
+ SINGLE_USER_RESTRICTED: MEMBERS_API_SINGLE_USER_RESTRICTED,
+ MULTIPLE_USERS_RESTRICTED: MEMBERS_API_MULTIPLE_USERS_RESTRICTED,
+};
+
+export const invitationsApiResponse = {
+ EMAIL_INVALID: INVITATIONS_API_EMAIL_INVALID,
+ ERROR_EMAIL_INVALID: INVITATIONS_API_ERROR_EMAIL_INVALID,
+ EMAIL_RESTRICTED: INVITATIONS_API_EMAIL_RESTRICTED,
+ MULTIPLE_EMAIL_RESTRICTED: INVITATIONS_API_MULTIPLE_EMAIL_RESTRICTED,
+ EMAIL_TAKEN: INVITATIONS_API_EMAIL_TAKEN,
+};
diff --git a/spec/frontend/invite_members/utils/response_message_parser_spec.js b/spec/frontend/invite_members/utils/response_message_parser_spec.js
new file mode 100644
index 00000000000..3c88b5a2418
--- /dev/null
+++ b/spec/frontend/invite_members/utils/response_message_parser_spec.js
@@ -0,0 +1,36 @@
+import {
+ responseMessageFromSuccess,
+ responseMessageFromError,
+} from '~/invite_members/utils/response_message_parser';
+
+describe('Response message parser', () => {
+ const expectedMessage = 'expected display message';
+
+ describe('parse message from successful response', () => {
+ const exampleKeyedMsg = { 'email@example.com': expectedMessage };
+ const exampleUserMsgMultiple =
+ ' and username1: id not found and username2: email is restricted';
+
+ it.each([
+ [[{ data: { message: expectedMessage } }]],
+ [[{ data: { message: expectedMessage + exampleUserMsgMultiple } }]],
+ [[{ data: { error: expectedMessage } }]],
+ [[{ data: { message: [expectedMessage] } }]],
+ [[{ data: { message: exampleKeyedMsg } }]],
+ ])(`returns "${expectedMessage}" from success response: %j`, (successResponse) => {
+ expect(responseMessageFromSuccess(successResponse)).toBe(expectedMessage);
+ });
+ });
+
+ describe('message from error response', () => {
+ it.each([
+ [{ response: { data: { error: expectedMessage } } }],
+ [{ response: { data: { message: { user: [expectedMessage] } } } }],
+ [{ response: { data: { message: { access_level: [expectedMessage] } } } }],
+ [{ response: { data: { message: { error: expectedMessage } } } }],
+ [{ response: { data: { message: expectedMessage } } }],
+ ])(`returns "${expectedMessage}" from error response: %j`, (errorResponse) => {
+ expect(responseMessageFromError(errorResponse)).toBe(expectedMessage);
+ });
+ });
+});
diff --git a/spec/frontend/issuable/components/issuable_by_email_spec.js b/spec/frontend/issuable/components/issuable_by_email_spec.js
index f11c41fe25d..01abf239e57 100644
--- a/spec/frontend/issuable/components/issuable_by_email_spec.js
+++ b/spec/frontend/issuable/components/issuable_by_email_spec.js
@@ -154,10 +154,7 @@ describe('IssuableByEmail', () => {
await clickResetEmail();
- expect(mockToastShow).toHaveBeenCalledWith(
- 'There was an error when reseting email token.',
- { type: 'error' },
- );
+ expect(mockToastShow).toHaveBeenCalledWith('There was an error when reseting email token.');
expect(findFormInputGroup().props('value')).toBe('user@gitlab.com');
});
});
diff --git a/spec/frontend/issuable_bulk_update_sidebar/components/status_select_spec.js b/spec/frontend/issuable_bulk_update_sidebar/components/status_select_spec.js
new file mode 100644
index 00000000000..09dcb963154
--- /dev/null
+++ b/spec/frontend/issuable_bulk_update_sidebar/components/status_select_spec.js
@@ -0,0 +1,77 @@
+import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import StatusSelect from '~/issuable_bulk_update_sidebar/components/status_select.vue';
+import { ISSUE_STATUS_SELECT_OPTIONS } from '~/issuable_bulk_update_sidebar/constants';
+
+describe('StatusSelect', () => {
+ let wrapper;
+
+ const findDropdown = () => wrapper.findComponent(GlDropdown);
+ const findAllDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
+ const findHiddenInput = () => wrapper.find('input');
+
+ function createComponent() {
+ wrapper = shallowMount(StatusSelect);
+ }
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('with no value selected', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders default text', () => {
+ expect(findDropdown().props('text')).toBe('Select status');
+ });
+
+ it('renders dropdown items with `is-checked` prop set to `false`', () => {
+ const dropdownItems = findAllDropdownItems();
+
+ expect(dropdownItems.at(0).props('isChecked')).toBe(false);
+ expect(dropdownItems.at(1).props('isChecked')).toBe(false);
+ });
+ });
+
+ describe('when selecting a value', () => {
+ const selectItemAtIndex = 0;
+
+ beforeEach(async () => {
+ createComponent();
+ await findAllDropdownItems().at(selectItemAtIndex).vm.$emit('click');
+ });
+
+ it('updates value of the hidden input', () => {
+ expect(findHiddenInput().attributes('value')).toBe(
+ ISSUE_STATUS_SELECT_OPTIONS[selectItemAtIndex].value,
+ );
+ });
+
+ it('updates the dropdown text prop', () => {
+ expect(findDropdown().props('text')).toBe(
+ ISSUE_STATUS_SELECT_OPTIONS[selectItemAtIndex].text,
+ );
+ });
+
+ it('sets dropdown item `is-checked` prop to `true`', () => {
+ const dropdownItems = findAllDropdownItems();
+
+ expect(dropdownItems.at(0).props('isChecked')).toBe(true);
+ expect(dropdownItems.at(1).props('isChecked')).toBe(false);
+ });
+
+ describe('when selecting the value that is already selected', () => {
+ it('clears dropdown selection', async () => {
+ await findAllDropdownItems().at(selectItemAtIndex).vm.$emit('click');
+
+ const dropdownItems = findAllDropdownItems();
+
+ expect(dropdownItems.at(0).props('isChecked')).toBe(false);
+ expect(dropdownItems.at(1).props('isChecked')).toBe(false);
+ expect(findDropdown().props('text')).toBe('Select status');
+ });
+ });
+ });
+});
diff --git a/spec/frontend/issuable_create/components/issuable_form_spec.js b/spec/frontend/issuable_create/components/issuable_form_spec.js
index a074fddf091..30b116bc35c 100644
--- a/spec/frontend/issuable_create/components/issuable_form_spec.js
+++ b/spec/frontend/issuable_create/components/issuable_form_spec.js
@@ -23,6 +23,9 @@ const createComponent = ({
<button class="js-issuable-save">Submit issuable</button>
`,
},
+ stubs: {
+ MarkdownField,
+ },
});
};
diff --git a/spec/frontend/issuable_show/components/issuable_show_root_spec.js b/spec/frontend/issuable_show/components/issuable_show_root_spec.js
index b4c125f4910..7ad409c3a74 100644
--- a/spec/frontend/issuable_show/components/issuable_show_root_spec.js
+++ b/spec/frontend/issuable_show/components/issuable_show_root_spec.js
@@ -133,14 +133,6 @@ describe('IssuableShowRoot', () => {
expect(wrapper.emitted('task-list-update-failure')).toBeTruthy();
});
- it('component emits `sidebar-toggle` event bubbled via issuable-sidebar', () => {
- const issuableSidebar = wrapper.find(IssuableSidebar);
-
- issuableSidebar.vm.$emit('sidebar-toggle', true);
-
- expect(wrapper.emitted('sidebar-toggle')).toBeTruthy();
- });
-
it.each(['keydown-title', 'keydown-description'])(
'component emits `%s` event with event object and issuableMeta params via issuable-body',
(eventName) => {
diff --git a/spec/frontend/issuable_sidebar/components/issuable_sidebar_root_spec.js b/spec/frontend/issuable_sidebar/components/issuable_sidebar_root_spec.js
index 62a0016d67b..c872925cca2 100644
--- a/spec/frontend/issuable_sidebar/components/issuable_sidebar_root_spec.js
+++ b/spec/frontend/issuable_sidebar/components/issuable_sidebar_root_spec.js
@@ -1,88 +1,80 @@
import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
-import { shallowMount } from '@vue/test-utils';
import Cookies from 'js-cookie';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import IssuableSidebarRoot from '~/issuable_sidebar/components/issuable_sidebar_root.vue';
+import { USER_COLLAPSED_GUTTER_COOKIE } from '~/issuable_sidebar/constants';
-const createComponent = (expanded = true) =>
- shallowMount(IssuableSidebarRoot, {
- propsData: {
- expanded,
- },
+const MOCK_LAYOUT_PAGE_CLASS = 'layout-page';
+
+const createComponent = () => {
+ setFixtures(`<div class="${MOCK_LAYOUT_PAGE_CLASS}"></div>`);
+
+ return shallowMountExtended(IssuableSidebarRoot, {
slots: {
'right-sidebar-items': `
<button class="js-todo">Todo</button>
`,
},
});
+};
describe('IssuableSidebarRoot', () => {
let wrapper;
- beforeEach(() => {
- wrapper = createComponent();
- });
+ const findToggleSidebarButton = () => wrapper.findByTestId('toggle-right-sidebar-button');
+
+ const assertPageLayoutClasses = ({ isExpanded }) => {
+ const { classList } = document.querySelector(`.${MOCK_LAYOUT_PAGE_CLASS}`);
+ if (isExpanded) {
+ expect(classList).toContain('right-sidebar-expanded');
+ expect(classList).not.toContain('right-sidebar-collapsed');
+ } else {
+ expect(classList).toContain('right-sidebar-collapsed');
+ expect(classList).not.toContain('right-sidebar-expanded');
+ }
+ };
afterEach(() => {
wrapper.destroy();
});
- describe('watch', () => {
- describe('isExpanded', () => {
- it('emits `sidebar-toggle` event on component', async () => {
- wrapper.setData({
- isExpanded: false,
- });
-
- await wrapper.vm.$nextTick();
-
- expect(wrapper.emitted('sidebar-toggle')).toBeTruthy();
- expect(wrapper.emitted('sidebar-toggle')[0]).toEqual([
- {
- expanded: false,
- },
- ]);
- });
- });
- });
+ describe('when sidebar is expanded', () => {
+ beforeEach(() => {
+ jest.spyOn(Cookies, 'set').mockImplementation(jest.fn());
+ jest.spyOn(Cookies, 'get').mockReturnValue(false);
+ jest.spyOn(bp, 'isDesktop').mockReturnValue(true);
- describe('methods', () => {
- describe('updatePageContainerClass', () => {
- beforeEach(() => {
- setFixtures('<div class="layout-page"></div>');
- });
+ wrapper = createComponent();
+ });
- it.each`
- isExpanded | layoutPageClass
- ${true} | ${'right-sidebar-expanded'}
- ${false} | ${'right-sidebar-collapsed'}
- `(
- 'set class $layoutPageClass to container element when `isExpanded` prop is $isExpanded',
- async ({ isExpanded, layoutPageClass }) => {
- wrapper.setData({
- isExpanded,
- });
+ it('renders component container element with class `right-sidebar-expanded`', () => {
+ expect(wrapper.classes()).toContain('right-sidebar-expanded');
+ });
- await wrapper.vm.$nextTick();
+ it('sets layout class to reflect expanded state', () => {
+ assertPageLayoutClasses({ isExpanded: true });
+ });
- wrapper.vm.updatePageContainerClass();
+ it('renders sidebar toggle button with text and icon', () => {
+ const buttonEl = findToggleSidebarButton();
- expect(document.querySelector('.layout-page').classList.contains(layoutPageClass)).toBe(
- true,
- );
- },
- );
+ expect(buttonEl.exists()).toBe(true);
+ expect(buttonEl.attributes('title')).toBe('Toggle sidebar');
+ expect(buttonEl.find('span').text()).toBe('Collapse sidebar');
+ expect(wrapper.findByTestId('icon-collapse').isVisible()).toBe(true);
});
- describe('handleWindowResize', () => {
- beforeEach(async () => {
- wrapper.setData({
- userExpanded: true,
- });
+ describe('when collapsing the sidebar', () => {
+ it('updates "collapsed_gutter" cookie value and layout classes', async () => {
+ await findToggleSidebarButton().trigger('click');
- await wrapper.vm.$nextTick();
+ expect(Cookies.set).toHaveBeenCalledWith(USER_COLLAPSED_GUTTER_COOKIE, true);
+ assertPageLayoutClasses({ isExpanded: false });
});
+ });
+ describe('when window `resize` event is triggered', () => {
it.each`
breakpoint | isExpandedValue
${'xs'} | ${false}
@@ -91,109 +83,49 @@ describe('IssuableSidebarRoot', () => {
${'lg'} | ${true}
${'xl'} | ${true}
`(
- 'sets `isExpanded` prop to $isExpandedValue only when current screen size is `lg` or `xl`',
+ 'sets page layout classes correctly when current screen size is `$breakpoint`',
async ({ breakpoint, isExpandedValue }) => {
jest.spyOn(bp, 'isDesktop').mockReturnValue(breakpoint === 'lg' || breakpoint === 'xl');
- wrapper.vm.handleWindowResize();
+ window.dispatchEvent(new Event('resize'));
+ await wrapper.vm.$nextTick();
- expect(wrapper.vm.isExpanded).toBe(isExpandedValue);
+ assertPageLayoutClasses({ isExpanded: isExpandedValue });
},
);
-
- it('calls `updatePageContainerClass` method', () => {
- jest.spyOn(wrapper.vm, 'updatePageContainerClass');
-
- wrapper.vm.handleWindowResize();
-
- expect(wrapper.vm.updatePageContainerClass).toHaveBeenCalled();
- });
- });
-
- describe('handleToggleSidebarClick', () => {
- beforeEach(async () => {
- jest.spyOn(Cookies, 'set').mockImplementation(jest.fn());
- wrapper.setData({
- isExpanded: true,
- });
-
- await wrapper.vm.$nextTick();
- });
-
- it('flips value of `isExpanded`', () => {
- wrapper.vm.handleToggleSidebarClick();
-
- expect(wrapper.vm.isExpanded).toBe(false);
- expect(wrapper.vm.userExpanded).toBe(false);
- });
-
- it('updates "collapsed_gutter" cookie value', () => {
- wrapper.vm.handleToggleSidebarClick();
-
- expect(Cookies.set).toHaveBeenCalledWith('collapsed_gutter', true);
- });
-
- it('calls `updatePageContainerClass` method', () => {
- jest.spyOn(wrapper.vm, 'updatePageContainerClass');
-
- wrapper.vm.handleWindowResize();
-
- expect(wrapper.vm.updatePageContainerClass).toHaveBeenCalled();
- });
});
});
- describe('template', () => {
- describe('sidebar expanded', () => {
- beforeEach(async () => {
- wrapper.setData({
- isExpanded: true,
- });
+ describe('when sidebar is collapsed', () => {
+ beforeEach(() => {
+ jest.spyOn(Cookies, 'get').mockReturnValue(true);
- await wrapper.vm.$nextTick();
- });
-
- it('renders component container element with class `right-sidebar-expanded` when `isExpanded` prop is true', () => {
- expect(wrapper.classes()).toContain('right-sidebar-expanded');
- });
-
- it('renders sidebar toggle button with text and icon', () => {
- const buttonEl = wrapper.find('button');
-
- expect(buttonEl.exists()).toBe(true);
- expect(buttonEl.attributes('title')).toBe('Toggle sidebar');
- expect(buttonEl.find('span').text()).toBe('Collapse sidebar');
- expect(buttonEl.find('[data-testid="icon-collapse"]').isVisible()).toBe(true);
- });
+ wrapper = createComponent();
});
- describe('sidebar collapsed', () => {
- beforeEach(async () => {
- wrapper.setData({
- isExpanded: false,
- });
-
- await wrapper.vm.$nextTick();
- });
+ it('renders component container element with class `right-sidebar-collapsed`', () => {
+ expect(wrapper.classes()).toContain('right-sidebar-collapsed');
+ });
- it('renders component container element with class `right-sidebar-collapsed` when `isExpanded` prop is false', () => {
- expect(wrapper.classes()).toContain('right-sidebar-collapsed');
- });
+ it('sets layout class to reflect collapsed state', () => {
+ assertPageLayoutClasses({ isExpanded: false });
+ });
- it('renders sidebar toggle button with text and icon', () => {
- const buttonEl = wrapper.find('button');
+ it('renders sidebar toggle button with text and icon', () => {
+ const buttonEl = findToggleSidebarButton();
- expect(buttonEl.exists()).toBe(true);
- expect(buttonEl.attributes('title')).toBe('Toggle sidebar');
- expect(buttonEl.find('[data-testid="icon-expand"]').isVisible()).toBe(true);
- });
+ expect(buttonEl.exists()).toBe(true);
+ expect(buttonEl.attributes('title')).toBe('Toggle sidebar');
+ expect(wrapper.findByTestId('icon-expand').isVisible()).toBe(true);
});
+ });
- it('renders sidebar items', () => {
- const sidebarItemsEl = wrapper.find('[data-testid="sidebar-items"]');
+ it('renders slotted sidebar items', () => {
+ wrapper = createComponent();
- expect(sidebarItemsEl.exists()).toBe(true);
- expect(sidebarItemsEl.find('button.js-todo').exists()).toBe(true);
- });
+ const sidebarItemsEl = wrapper.findByTestId('sidebar-items');
+
+ expect(sidebarItemsEl.exists()).toBe(true);
+ expect(sidebarItemsEl.find('button.js-todo').exists()).toBe(true);
});
});
diff --git a/spec/frontend/issuable_spec.js b/spec/frontend/issuable_spec.js
index 9c8f1e04609..e0bd7b802c9 100644
--- a/spec/frontend/issuable_spec.js
+++ b/spec/frontend/issuable_spec.js
@@ -1,5 +1,5 @@
+import issuableInitBulkUpdateSidebar from '~/issuable_bulk_update_sidebar/issuable_init_bulk_update_sidebar';
import IssuableIndex from '~/issuable_index';
-import issuableInitBulkUpdateSidebar from '~/issuable_init_bulk_update_sidebar';
describe('Issuable', () => {
describe('initBulkUpdate', () => {
diff --git a/spec/frontend/issues_list/components/issuables_list_app_spec.js b/spec/frontend/issues_list/components/issuables_list_app_spec.js
index a7f3dd81517..86112dad444 100644
--- a/spec/frontend/issues_list/components/issuables_list_app_spec.js
+++ b/spec/frontend/issues_list/components/issuables_list_app_spec.js
@@ -8,7 +8,7 @@ import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import { TEST_HOST } from 'helpers/test_constants';
import waitForPromises from 'helpers/wait_for_promises';
-import { deprecatedCreateFlash as flash } from '~/flash';
+import createFlash from '~/flash';
import Issuable from '~/issues_list/components/issuable.vue';
import IssuablesListApp from '~/issues_list/components/issuables_list_app.vue';
import { PAGE_SIZE, PAGE_SIZE_MANUAL, RELATIVE_POSITION } from '~/issues_list/constants';
@@ -104,7 +104,7 @@ describe('Issuables list component', () => {
});
it('flashes an error', () => {
- expect(flash).toHaveBeenCalledTimes(1);
+ expect(createFlash).toHaveBeenCalledTimes(1);
});
});
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 a3ac57ee1bb..846236e1fb5 100644
--- a/spec/frontend/issues_list/components/issues_list_app_spec.js
+++ b/spec/frontend/issues_list/components/issues_list_app_spec.js
@@ -5,6 +5,7 @@ import { cloneDeep } from 'lodash';
import { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import getIssuesQuery from 'ee_else_ce/issues_list/queries/get_issues.query.graphql';
+import getIssuesCountQuery from 'ee_else_ce/issues_list/queries/get_issues_count.query.graphql';
import createMockApollo from 'helpers/mock_apollo_helper';
import { TEST_HOST } from 'helpers/test_constants';
import waitForPromises from 'helpers/wait_for_promises';
@@ -13,15 +14,16 @@ import {
filteredTokens,
locationSearch,
urlParams,
+ getIssuesCountQueryResponse,
} from 'jest/issues_list/mock_data';
import createFlash from '~/flash';
+import { convertToGraphQLId } from '~/graphql_shared/utils';
import CsvImportExportButtons from '~/issuable/components/csv_import_export_buttons.vue';
import IssuableByEmail from '~/issuable/components/issuable_by_email.vue';
import IssuableList from '~/issuable_list/components/issuable_list_root.vue';
import { IssuableListTabs, IssuableStates } from '~/issuable_list/constants';
import IssuesListApp from '~/issues_list/components/issues_list_app.vue';
import {
- apiSortParams,
CREATED_DESC,
DUE_DATE_OVERDUE,
PARAM_DUE_DATE,
@@ -55,19 +57,18 @@ describe('IssuesListApp component', () => {
localVue.use(VueApollo);
const defaultProvide = {
- autocompleteUsersPath: 'autocomplete/users/path',
calendarPath: 'calendar/path',
canBulkUpdate: false,
emptyStateSvgPath: 'empty-state.svg',
exportCsvPath: 'export/csv/path',
hasBlockedIssuesFeature: true,
hasIssueWeightsFeature: true,
+ hasIterationsFeature: true,
hasProjectIssues: true,
- isSignedIn: false,
+ isSignedIn: true,
issuesPath: 'path/to/issues',
jiraIntegrationPath: 'jira/integration/path',
newIssuePath: 'new/issue/path',
- projectLabelsPath: 'project/labels/path',
projectPath: 'path/to/project',
rssPath: 'rss/path',
showNewIssueLink: true,
@@ -77,7 +78,7 @@ describe('IssuesListApp component', () => {
let defaultQueryResponse = getIssuesQueryResponse;
if (IS_EE) {
defaultQueryResponse = cloneDeep(getIssuesQueryResponse);
- defaultQueryResponse.data.project.issues.nodes[0].blockedByCount = 1;
+ defaultQueryResponse.data.project.issues.nodes[0].blockingCount = 1;
defaultQueryResponse.data.project.issues.nodes[0].healthStatus = null;
defaultQueryResponse.data.project.issues.nodes[0].weight = 5;
}
@@ -93,10 +94,14 @@ describe('IssuesListApp component', () => {
const mountComponent = ({
provide = {},
- response = defaultQueryResponse,
+ issuesQueryResponse = jest.fn().mockResolvedValue(defaultQueryResponse),
+ issuesQueryCountResponse = jest.fn().mockResolvedValue(getIssuesCountQueryResponse),
mountFn = shallowMount,
} = {}) => {
- const requestHandlers = [[getIssuesQuery, jest.fn().mockResolvedValue(response)]];
+ const requestHandlers = [
+ [getIssuesQuery, issuesQueryResponse],
+ [getIssuesCountQuery, issuesQueryCountResponse],
+ ];
const apolloProvider = createMockApollo(requestHandlers);
return mountFn(IssuesListApp, {
@@ -137,8 +142,8 @@ describe('IssuesListApp component', () => {
currentTab: IssuableStates.Opened,
tabCounts: {
opened: 1,
- closed: undefined,
- all: undefined,
+ closed: 1,
+ all: 1,
},
issuablesLoading: false,
isManualOrdering: false,
@@ -148,8 +153,8 @@ describe('IssuesListApp component', () => {
hasPreviousPage: getIssuesQueryResponse.data.project.issues.pageInfo.hasPreviousPage,
hasNextPage: getIssuesQueryResponse.data.project.issues.pageInfo.hasNextPage,
urlParams: {
+ sort: urlSortParams[CREATED_DESC],
state: IssuableStates.Opened,
- ...urlSortParams[CREATED_DESC],
},
});
});
@@ -178,7 +183,7 @@ describe('IssuesListApp component', () => {
describe('csv import/export component', () => {
describe('when user is signed in', () => {
- const search = '?search=refactor&state=opened&sort=created_date';
+ const search = '?search=refactor&sort=created_date&state=opened';
beforeEach(() => {
global.jsdom.reconfigure({ url: `${TEST_HOST}${search}` });
@@ -273,13 +278,17 @@ describe('IssuesListApp component', () => {
describe('sort', () => {
it.each(Object.keys(urlSortParams))('is set as %s from the url params', (sortKey) => {
- global.jsdom.reconfigure({ url: setUrlParams(urlSortParams[sortKey], TEST_HOST) });
+ global.jsdom.reconfigure({
+ url: setUrlParams({ sort: urlSortParams[sortKey] }, TEST_HOST),
+ });
wrapper = mountComponent();
expect(findIssuableList().props()).toMatchObject({
initialSortBy: sortKey,
- urlParams: urlSortParams[sortKey],
+ urlParams: {
+ sort: urlSortParams[sortKey],
+ },
});
});
});
@@ -542,9 +551,13 @@ describe('IssuesListApp component', () => {
});
it('renders all tokens', () => {
+ const preloadedAuthors = [
+ { ...mockCurrentUser, id: convertToGraphQLId('User', mockCurrentUser.id) },
+ ];
+
expect(findIssuableList().props('searchTokens')).toMatchObject([
- { type: TOKEN_TYPE_AUTHOR, preloadedAuthors: [mockCurrentUser] },
- { type: TOKEN_TYPE_ASSIGNEE, preloadedAuthors: [mockCurrentUser] },
+ { type: TOKEN_TYPE_AUTHOR, preloadedAuthors },
+ { type: TOKEN_TYPE_ASSIGNEE, preloadedAuthors },
{ type: TOKEN_TYPE_MILESTONE },
{ type: TOKEN_TYPE_LABEL },
{ type: TOKEN_TYPE_MY_REACTION },
@@ -557,6 +570,29 @@ describe('IssuesListApp component', () => {
});
});
+ describe('errors', () => {
+ describe.each`
+ error | mountOption | message
+ ${'fetching issues'} | ${'issuesQueryResponse'} | ${IssuesListApp.i18n.errorFetchingIssues}
+ ${'fetching issue counts'} | ${'issuesQueryCountResponse'} | ${IssuesListApp.i18n.errorFetchingCounts}
+ `('when there is an error $error', ({ mountOption, message }) => {
+ beforeEach(() => {
+ wrapper = mountComponent({
+ [mountOption]: jest.fn().mockRejectedValue(new Error('ERROR')),
+ });
+ jest.runOnlyPendingTimers();
+ });
+
+ it('shows an error message', () => {
+ expect(createFlash).toHaveBeenCalledWith({
+ captureError: true,
+ error: new Error('Network error: ERROR'),
+ message,
+ });
+ });
+ });
+ });
+
describe('events', () => {
describe('when "click-tab" event is emitted by IssuableList', () => {
beforeEach(() => {
@@ -622,7 +658,7 @@ describe('IssuesListApp component', () => {
};
beforeEach(() => {
- wrapper = mountComponent({ response });
+ wrapper = mountComponent({ issuesQueryResponse: jest.fn().mockResolvedValue(response) });
jest.runOnlyPendingTimers();
});
@@ -640,7 +676,7 @@ describe('IssuesListApp component', () => {
});
describe('when "sort" event is emitted by IssuableList', () => {
- it.each(Object.keys(apiSortParams))(
+ it.each(Object.keys(urlSortParams))(
'updates to the new sort when payload is `%s`',
async (sortKey) => {
wrapper = mountComponent();
@@ -650,7 +686,9 @@ describe('IssuesListApp component', () => {
jest.runOnlyPendingTimers();
await nextTick();
- expect(findIssuableList().props('urlParams')).toMatchObject(urlSortParams[sortKey]);
+ expect(findIssuableList().props('urlParams')).toMatchObject({
+ sort: urlSortParams[sortKey],
+ });
},
);
});
diff --git a/spec/frontend/issues_list/mock_data.js b/spec/frontend/issues_list/mock_data.js
index 6c669e02070..fd59241fd1d 100644
--- a/spec/frontend/issues_list/mock_data.js
+++ b/spec/frontend/issues_list/mock_data.js
@@ -7,9 +7,8 @@ export const getIssuesQueryResponse = {
data: {
project: {
issues: {
- count: 1,
pageInfo: {
- hasNextPage: false,
+ hasNextPage: true,
hasPreviousPage: false,
startCursor: 'startcursor',
endCursor: 'endcursor',
@@ -70,6 +69,16 @@ export const getIssuesQueryResponse = {
},
};
+export const getIssuesCountQueryResponse = {
+ data: {
+ project: {
+ issues: {
+ count: 1,
+ },
+ },
+ },
+};
+
export const locationSearch = [
'?search=find+issues',
'author_username=homer',
@@ -86,10 +95,10 @@ export const locationSearch = [
'not[label_name][]=drama',
'my_reaction_emoji=thumbsup',
'confidential=no',
- 'iteration_title=season:+%234',
- 'not[iteration_title]=season:+%2320',
- 'epic_id=gitlab-org%3A%3A%2612',
- 'not[epic_id]=gitlab-org%3A%3A%2634',
+ 'iteration_id=4',
+ 'not[iteration_id]=20',
+ 'epic_id=12',
+ 'not[epic_id]=34',
'weight=1',
'not[weight]=3',
].join('&');
@@ -118,10 +127,10 @@ export const filteredTokens = [
{ type: 'labels', value: { data: 'drama', operator: OPERATOR_IS_NOT } },
{ type: 'my_reaction_emoji', value: { data: 'thumbsup', operator: OPERATOR_IS } },
{ type: 'confidential', value: { data: 'no', operator: OPERATOR_IS } },
- { type: 'iteration', value: { data: 'season: #4', operator: OPERATOR_IS } },
- { type: 'iteration', value: { data: 'season: #20', operator: OPERATOR_IS_NOT } },
- { type: 'epic_id', value: { data: 'gitlab-org::&12', operator: OPERATOR_IS } },
- { type: 'epic_id', value: { data: 'gitlab-org::&34', operator: OPERATOR_IS_NOT } },
+ { type: 'iteration', value: { data: '4', operator: OPERATOR_IS } },
+ { type: 'iteration', value: { data: '20', operator: OPERATOR_IS_NOT } },
+ { type: 'epic_id', value: { data: '12', operator: OPERATOR_IS } },
+ { type: 'epic_id', value: { data: '34', operator: OPERATOR_IS_NOT } },
{ type: 'weight', value: { data: '1', operator: OPERATOR_IS } },
{ type: 'weight', value: { data: '3', operator: OPERATOR_IS_NOT } },
{ type: 'filtered-search-term', value: { data: 'find' } },
@@ -138,30 +147,32 @@ export const filteredTokensWithSpecialValues = [
];
export const apiParams = {
- author_username: 'homer',
- 'not[author_username]': 'marge',
- assignee_username: ['bart', 'lisa'],
- 'not[assignee_username]': ['patty', 'selma'],
- milestone: 'season 4',
- 'not[milestone]': 'season 20',
- labels: ['cartoon', 'tv'],
- 'not[labels]': ['live action', 'drama'],
- my_reaction_emoji: 'thumbsup',
+ authorUsername: 'homer',
+ assigneeUsernames: ['bart', 'lisa'],
+ milestoneTitle: 'season 4',
+ labelName: ['cartoon', 'tv'],
+ myReactionEmoji: 'thumbsup',
confidential: 'no',
- iteration_title: 'season: #4',
- 'not[iteration_title]': 'season: #20',
- epic_id: '12',
- 'not[epic_id]': 'gitlab-org::&34',
+ iterationId: '4',
+ epicId: '12',
weight: '1',
- 'not[weight]': '3',
+ not: {
+ authorUsername: 'marge',
+ assigneeUsernames: ['patty', 'selma'],
+ milestoneTitle: 'season 20',
+ labelName: ['live action', 'drama'],
+ iterationId: '20',
+ epicId: '34',
+ weight: '3',
+ },
};
export const apiParamsWithSpecialValues = {
- assignee_id: '123',
- assignee_username: 'bart',
- my_reaction_emoji: 'None',
- iteration_id: 'Current',
- epic_id: 'None',
+ assigneeId: '123',
+ assigneeUsernames: 'bart',
+ myReactionEmoji: 'None',
+ iterationWildcardId: 'CURRENT',
+ epicId: 'None',
weight: 'None',
};
@@ -176,10 +187,10 @@ export const urlParams = {
'not[label_name][]': ['live action', 'drama'],
my_reaction_emoji: 'thumbsup',
confidential: 'no',
- iteration_title: 'season: #4',
- 'not[iteration_title]': 'season: #20',
- epic_id: 'gitlab-org%3A%3A%2612',
- 'not[epic_id]': 'gitlab-org::&34',
+ iteration_id: '4',
+ 'not[iteration_id]': '20',
+ epic_id: '12',
+ 'not[epic_id]': '34',
weight: '1',
'not[weight]': '3',
};
diff --git a/spec/frontend/issues_list/utils_spec.js b/spec/frontend/issues_list/utils_spec.js
index e377c35a0aa..b7863068570 100644
--- a/spec/frontend/issues_list/utils_spec.js
+++ b/spec/frontend/issues_list/utils_spec.js
@@ -8,10 +8,11 @@ import {
urlParams,
urlParamsWithSpecialValues,
} from 'jest/issues_list/mock_data';
-import { API_PARAM, DUE_DATE_VALUES, URL_PARAM, urlSortParams } from '~/issues_list/constants';
+import { DUE_DATE_VALUES, urlSortParams } from '~/issues_list/constants';
import {
- convertToParams,
+ convertToApiParams,
convertToSearchQuery,
+ convertToUrlParams,
getDueDateValue,
getFilterTokens,
getSortKey,
@@ -20,7 +21,7 @@ import {
describe('getSortKey', () => {
it.each(Object.keys(urlSortParams))('returns %s given the correct inputs', (sortKey) => {
- const { sort } = urlSortParams[sortKey];
+ const sort = urlSortParams[sortKey];
expect(getSortKey(sort)).toBe(sortKey);
});
});
@@ -80,31 +81,23 @@ describe('getFilterTokens', () => {
});
});
-describe('convertToParams', () => {
+describe('convertToApiParams', () => {
it('returns api params given filtered tokens', () => {
- expect(convertToParams(filteredTokens, API_PARAM)).toEqual({
- ...apiParams,
- epic_id: 'gitlab-org::&12',
- });
+ expect(convertToApiParams(filteredTokens)).toEqual(apiParams);
});
it('returns api params given filtered tokens with special values', () => {
- expect(convertToParams(filteredTokensWithSpecialValues, API_PARAM)).toEqual(
- apiParamsWithSpecialValues,
- );
+ expect(convertToApiParams(filteredTokensWithSpecialValues)).toEqual(apiParamsWithSpecialValues);
});
+});
+describe('convertToUrlParams', () => {
it('returns url params given filtered tokens', () => {
- expect(convertToParams(filteredTokens, URL_PARAM)).toEqual({
- ...urlParams,
- epic_id: 'gitlab-org::&12',
- });
+ expect(convertToUrlParams(filteredTokens)).toEqual(urlParams);
});
it('returns url params given filtered tokens with special values', () => {
- expect(convertToParams(filteredTokensWithSpecialValues, URL_PARAM)).toEqual(
- urlParamsWithSpecialValues,
- );
+ expect(convertToUrlParams(filteredTokensWithSpecialValues)).toEqual(urlParamsWithSpecialValues);
});
});
diff --git a/spec/frontend/jira_connect/branches/components/project_dropdown_spec.js b/spec/frontend/jira_connect/branches/components/project_dropdown_spec.js
new file mode 100644
index 00000000000..ec4cb2739f8
--- /dev/null
+++ b/spec/frontend/jira_connect/branches/components/project_dropdown_spec.js
@@ -0,0 +1,180 @@
+import { GlDropdown, GlDropdownItem, GlLoadingIcon, GlSearchBoxByType } from '@gitlab/ui';
+import { mount, shallowMount, createLocalVue } from '@vue/test-utils';
+import VueApollo from 'vue-apollo';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import ProjectDropdown from '~/jira_connect/branches/components/project_dropdown.vue';
+import { PROJECTS_PER_PAGE } from '~/jira_connect/branches/constants';
+import getProjectsQuery from '~/jira_connect/branches/graphql/queries/get_projects.query.graphql';
+
+const localVue = createLocalVue();
+
+const mockProjects = [
+ {
+ id: 'test',
+ name: 'test',
+ nameWithNamespace: 'test',
+ avatarUrl: 'https://gitlab.com',
+ path: 'test-path',
+ fullPath: 'test-path',
+ repository: {
+ empty: false,
+ },
+ },
+ {
+ id: 'gitlab',
+ name: 'GitLab',
+ nameWithNamespace: 'gitlab-org/gitlab',
+ avatarUrl: 'https://gitlab.com',
+ path: 'gitlab',
+ fullPath: 'gitlab-org/gitlab',
+ repository: {
+ empty: false,
+ },
+ },
+];
+
+const mockProjectsQueryResponse = {
+ data: {
+ projects: {
+ nodes: mockProjects,
+ pageInfo: {
+ hasNextPage: false,
+ hasPreviousPage: false,
+ startCursor: '',
+ endCursor: '',
+ },
+ },
+ },
+};
+const mockGetProjectsQuerySuccess = jest.fn().mockResolvedValue(mockProjectsQueryResponse);
+const mockGetProjectsQueryFailed = jest.fn().mockRejectedValue(new Error('GraphQL error'));
+const mockQueryLoading = jest.fn().mockReturnValue(new Promise(() => {}));
+
+describe('ProjectDropdown', () => {
+ let wrapper;
+
+ const findDropdown = () => wrapper.findComponent(GlDropdown);
+ const findAllDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
+ const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
+ const findDropdownItemByText = (text) =>
+ findAllDropdownItems().wrappers.find((item) => item.text() === text);
+ const findSearchBox = () => wrapper.findComponent(GlSearchBoxByType);
+
+ function createMockApolloProvider({ mockGetProjectsQuery = mockGetProjectsQuerySuccess } = {}) {
+ localVue.use(VueApollo);
+
+ const mockApollo = createMockApollo([[getProjectsQuery, mockGetProjectsQuery]]);
+
+ return mockApollo;
+ }
+
+ function createComponent({ mockApollo, props, mountFn = shallowMount } = {}) {
+ wrapper = mountFn(ProjectDropdown, {
+ localVue,
+ apolloProvider: mockApollo || createMockApolloProvider(),
+ propsData: props,
+ });
+ }
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('when loading projects', () => {
+ beforeEach(() => {
+ createComponent({
+ mockApollo: createMockApolloProvider({ mockGetProjectsQuery: mockQueryLoading }),
+ });
+ });
+
+ it('sets dropdown `loading` prop to `true`', () => {
+ expect(findDropdown().props('loading')).toBe(true);
+ });
+
+ it('renders loading icon in dropdown', () => {
+ expect(findLoadingIcon().isVisible()).toBe(true);
+ });
+ });
+
+ describe('when projects query succeeds', () => {
+ beforeEach(async () => {
+ createComponent();
+ await waitForPromises();
+ await wrapper.vm.$nextTick();
+ });
+
+ it('sets dropdown `loading` prop to `false`', () => {
+ expect(findDropdown().props('loading')).toBe(false);
+ });
+
+ it('renders dropdown items', () => {
+ const dropdownItems = findAllDropdownItems();
+ expect(dropdownItems.wrappers).toHaveLength(mockProjects.length);
+ expect(dropdownItems.wrappers.map((item) => item.text())).toEqual(
+ mockProjects.map((project) => project.nameWithNamespace),
+ );
+ });
+
+ describe('when selecting a dropdown item', () => {
+ it('emits `change` event with the selected project name', async () => {
+ const mockProject = mockProjects[0];
+ const itemToSelect = findDropdownItemByText(mockProject.nameWithNamespace);
+ await itemToSelect.vm.$emit('click');
+
+ expect(wrapper.emitted('change')[0]).toEqual([mockProject]);
+ });
+ });
+
+ describe('when `selectedProject` prop is specified', () => {
+ const mockProject = mockProjects[0];
+
+ beforeEach(async () => {
+ wrapper.setProps({
+ selectedProject: mockProject,
+ });
+ });
+
+ it('sets `isChecked` prop of the corresponding dropdown item to `true`', () => {
+ expect(findDropdownItemByText(mockProject.nameWithNamespace).props('isChecked')).toBe(true);
+ });
+
+ it('sets dropdown text to `selectedBranchName` value', () => {
+ expect(findDropdown().props('text')).toBe(mockProject.nameWithNamespace);
+ });
+ });
+ });
+
+ describe('when projects query fails', () => {
+ beforeEach(async () => {
+ createComponent({
+ mockApollo: createMockApolloProvider({ mockGetProjectsQuery: mockGetProjectsQueryFailed }),
+ });
+ await waitForPromises();
+ });
+
+ it('emits `error` event', () => {
+ expect(wrapper.emitted('error')).toBeTruthy();
+ });
+ });
+
+ describe('when searching branches', () => {
+ it('triggers a refetch', async () => {
+ createComponent({ mountFn: mount });
+ await waitForPromises();
+ jest.clearAllMocks();
+
+ const mockSearchTerm = 'gitl';
+ await findSearchBox().vm.$emit('input', mockSearchTerm);
+
+ expect(mockGetProjectsQuerySuccess).toHaveBeenCalledWith({
+ after: '',
+ first: PROJECTS_PER_PAGE,
+ membership: true,
+ search: mockSearchTerm,
+ searchNamespaces: true,
+ sort: 'similarity',
+ });
+ });
+ });
+});
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
new file mode 100644
index 00000000000..9dd11dd6345
--- /dev/null
+++ b/spec/frontend/jira_connect/branches/components/source_branch_dropdown_spec.js
@@ -0,0 +1,192 @@
+import { GlDropdown, GlDropdownItem, GlLoadingIcon, GlSearchBoxByType } from '@gitlab/ui';
+import { mount, shallowMount, createLocalVue } from '@vue/test-utils';
+import VueApollo from 'vue-apollo';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import SourceBranchDropdown from '~/jira_connect/branches/components/source_branch_dropdown.vue';
+import { BRANCHES_PER_PAGE } from '~/jira_connect/branches/constants';
+import getProjectQuery from '~/jira_connect/branches/graphql/queries/get_project.query.graphql';
+
+const localVue = createLocalVue();
+
+const mockProject = {
+ id: 'test',
+ fullPath: 'test-path',
+ repository: {
+ branchNames: ['main', 'f-test', 'release'],
+ rootRef: 'main',
+ },
+};
+
+const mockProjectQueryResponse = {
+ data: {
+ project: mockProject,
+ },
+};
+const mockGetProjectQuery = jest.fn().mockResolvedValue(mockProjectQueryResponse);
+const mockQueryLoading = jest.fn().mockReturnValue(new Promise(() => {}));
+
+describe('SourceBranchDropdown', () => {
+ let wrapper;
+
+ const findDropdown = () => wrapper.findComponent(GlDropdown);
+ const findAllDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
+ const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
+ const findDropdownItemByText = (text) =>
+ findAllDropdownItems().wrappers.find((item) => item.text() === text);
+ const findSearchBox = () => wrapper.findComponent(GlSearchBoxByType);
+
+ const assertDropdownItems = () => {
+ const dropdownItems = findAllDropdownItems();
+ expect(dropdownItems.wrappers).toHaveLength(mockProject.repository.branchNames.length);
+ expect(dropdownItems.wrappers.map((item) => item.text())).toEqual(
+ mockProject.repository.branchNames,
+ );
+ };
+
+ function createMockApolloProvider({ getProjectQueryLoading = false } = {}) {
+ localVue.use(VueApollo);
+
+ const mockApollo = createMockApollo([
+ [getProjectQuery, getProjectQueryLoading ? mockQueryLoading : mockGetProjectQuery],
+ ]);
+
+ return mockApollo;
+ }
+
+ function createComponent({ mockApollo, props, mountFn = shallowMount } = {}) {
+ wrapper = mountFn(SourceBranchDropdown, {
+ localVue,
+ apolloProvider: mockApollo || createMockApolloProvider(),
+ propsData: props,
+ });
+ }
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('when `selectedProject` prop is not specified', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('sets dropdown `disabled` prop to `true`', () => {
+ expect(findDropdown().props('disabled')).toBe(true);
+ });
+
+ describe('when `selectedProject` becomes specified', () => {
+ beforeEach(async () => {
+ wrapper.setProps({
+ selectedProject: mockProject,
+ });
+
+ await waitForPromises();
+ });
+
+ it('sets dropdown props correctly', () => {
+ expect(findDropdown().props()).toMatchObject({
+ loading: false,
+ disabled: false,
+ text: 'Select a branch',
+ });
+ });
+
+ it('renders available source branches as dropdown items', () => {
+ assertDropdownItems();
+ });
+ });
+ });
+
+ describe('when `selectedProject` prop is specified', () => {
+ describe('when branches are loading', () => {
+ it('renders loading icon in dropdown', () => {
+ createComponent({
+ mockApollo: createMockApolloProvider({ getProjectQueryLoading: true }),
+ props: { selectedProject: mockProject },
+ });
+
+ expect(findLoadingIcon().isVisible()).toBe(true);
+ });
+ });
+
+ describe('when branches have loaded', () => {
+ describe('when searching branches', () => {
+ it('triggers a refetch', async () => {
+ createComponent({ mountFn: mount, props: { selectedProject: mockProject } });
+ await waitForPromises();
+ jest.clearAllMocks();
+
+ const mockSearchTerm = 'mai';
+ await findSearchBox().vm.$emit('input', mockSearchTerm);
+
+ expect(mockGetProjectQuery).toHaveBeenCalledWith({
+ branchNamesLimit: BRANCHES_PER_PAGE,
+ branchNamesOffset: 0,
+ branchNamesSearchPattern: `*${mockSearchTerm}*`,
+ projectPath: 'test-path',
+ });
+ });
+ });
+
+ describe('template', () => {
+ beforeEach(async () => {
+ createComponent({ props: { selectedProject: mockProject } });
+ await waitForPromises();
+ });
+
+ it('sets dropdown props correctly', () => {
+ expect(findDropdown().props()).toMatchObject({
+ loading: false,
+ disabled: false,
+ text: 'Select a branch',
+ });
+ });
+
+ it('omits monospace styling from dropdown', () => {
+ expect(findDropdown().classes()).not.toContain('gl-font-monospace');
+ });
+
+ it('renders available source branches as dropdown items', () => {
+ assertDropdownItems();
+ });
+
+ it("emits `change` event with the repository's `rootRef` by default", () => {
+ expect(wrapper.emitted('change')[0]).toEqual([mockProject.repository.rootRef]);
+ });
+
+ describe('when selecting a dropdown item', () => {
+ it('emits `change` event with the selected branch name', async () => {
+ const mockBranchName = mockProject.repository.branchNames[1];
+ const itemToSelect = findDropdownItemByText(mockBranchName);
+ await itemToSelect.vm.$emit('click');
+
+ expect(wrapper.emitted('change')[1]).toEqual([mockBranchName]);
+ });
+ });
+
+ describe('when `selectedBranchName` prop is specified', () => {
+ const mockBranchName = mockProject.repository.branchNames[2];
+
+ beforeEach(async () => {
+ wrapper.setProps({
+ selectedBranchName: mockBranchName,
+ });
+ });
+
+ it('sets `isChecked` prop of the corresponding dropdown item to `true`', () => {
+ expect(findDropdownItemByText(mockBranchName).props('isChecked')).toBe(true);
+ });
+
+ it('sets dropdown text to `selectedBranchName` value', () => {
+ expect(findDropdown().props('text')).toBe(mockBranchName);
+ });
+
+ it('adds monospace styling to dropdown', () => {
+ expect(findDropdown().classes()).toContain('gl-font-monospace');
+ });
+ });
+ });
+ });
+ });
+});
diff --git a/spec/frontend/jira_connect/components/groups_list_spec.js b/spec/frontend/jira_connect/components/groups_list_spec.js
index 4b875928a90..d583fb68771 100644
--- a/spec/frontend/jira_connect/components/groups_list_spec.js
+++ b/spec/frontend/jira_connect/components/groups_list_spec.js
@@ -160,9 +160,13 @@ describe('GroupsList', () => {
expect(findGroupsList().classes()).toContain('gl-opacity-5');
});
- it('sets loading prop of ths search box', () => {
+ it('sets loading prop of the search box', () => {
expect(findSearchBox().props('isLoading')).toBe(true);
});
+
+ it('sets value prop of the search box to the search term', () => {
+ expect(findSearchBox().props('value')).toBe(mockSearchTeam);
+ });
});
describe('when group search finishes loading', () => {
diff --git a/spec/frontend/jira_import/components/__snapshots__/jira_import_form_spec.js.snap b/spec/frontend/jira_import/components/__snapshots__/jira_import_form_spec.js.snap
index 172b6e4831c..f2142ce1fcf 100644
--- a/spec/frontend/jira_import/components/__snapshots__/jira_import_form_spec.js.snap
+++ b/spec/frontend/jira_import/components/__snapshots__/jira_import_form_spec.js.snap
@@ -176,7 +176,6 @@ exports[`JiraImportForm table body shows correct information in each cell 1`] =
<!---->
</div>
-
</ul>
</div>
</td>
@@ -304,7 +303,6 @@ exports[`JiraImportForm table body shows correct information in each cell 1`] =
<!---->
</div>
-
</ul>
</div>
</td>
diff --git a/spec/frontend/jobs/components/empty_state_spec.js b/spec/frontend/jobs/components/empty_state_spec.js
index c9de110ce06..9738fd14275 100644
--- a/spec/frontend/jobs/components/empty_state_spec.js
+++ b/spec/frontend/jobs/components/empty_state_spec.js
@@ -9,7 +9,6 @@ describe('Empty State', () => {
illustrationSizeClass: 'svg-430',
title: 'This job has not started yet',
playable: false,
- variablesSettingsUrl: '',
};
const createWrapper = (props) => {
diff --git a/spec/frontend/jobs/components/job_app_spec.js b/spec/frontend/jobs/components/job_app_spec.js
index 3fcefde1aba..1f4dd7d6216 100644
--- a/spec/frontend/jobs/components/job_app_spec.js
+++ b/spec/frontend/jobs/components/job_app_spec.js
@@ -24,6 +24,7 @@ describe('Job App', () => {
let store;
let wrapper;
let mock;
+ let origGon;
const initSettings = {
endpoint: `${TEST_HOST}jobs/123.json`,
@@ -37,7 +38,6 @@ describe('Job App', () => {
deploymentHelpUrl: 'help/deployment',
codeQualityHelpPath: '/help/code_quality',
runnerSettingsUrl: 'settings/ci-cd/runners',
- variablesSettingsUrl: 'settings/ci-cd/variables',
terminalPath: 'jobs/123/terminal',
projectPath: 'user-name/project-name',
subscriptionsMoreMinutesUrl: 'https://customers.gitlab.com/buy_pipeline_minutes',
@@ -86,11 +86,17 @@ describe('Job App', () => {
beforeEach(() => {
mock = new MockAdapter(axios);
store = createStore();
+
+ origGon = window.gon;
+
+ window.gon = { features: { infinitelyCollapsibleSections: false } }; // NOTE: All of this passes with the feature flag
});
afterEach(() => {
wrapper.destroy();
mock.restore();
+
+ window.gon = origGon;
});
describe('while loading', () => {
diff --git a/spec/frontend/jobs/components/log/collapsible_section_spec.js b/spec/frontend/jobs/components/log/collapsible_section_spec.js
index 66f22162c97..4e23a3ba7b8 100644
--- a/spec/frontend/jobs/components/log/collapsible_section_spec.js
+++ b/spec/frontend/jobs/components/log/collapsible_section_spec.js
@@ -4,6 +4,7 @@ import { collapsibleSectionClosed, collapsibleSectionOpened } from './mock_data'
describe('Job Log Collapsible Section', () => {
let wrapper;
+ let origGon;
const traceEndpoint = 'jobs/335';
@@ -18,8 +19,16 @@ describe('Job Log Collapsible Section', () => {
});
};
+ beforeEach(() => {
+ origGon = window.gon;
+
+ window.gon = { features: { infinitelyCollapsibleSections: false } }; // NOTE: This also works with true
+ });
+
afterEach(() => {
wrapper.destroy();
+
+ window.gon = origGon;
});
describe('with closed section', () => {
diff --git a/spec/frontend/jobs/components/log/line_spec.js b/spec/frontend/jobs/components/log/line_spec.js
index 367154e7f82..d184696cd1f 100644
--- a/spec/frontend/jobs/components/log/line_spec.js
+++ b/spec/frontend/jobs/components/log/line_spec.js
@@ -94,6 +94,16 @@ describe('Job Log Line', () => {
expect(findLinkAttributeByIndex(0).href).toBe(queryUrl);
});
+ it('renders links that have brackets `[]` in their parameters', () => {
+ const url = `${httpUrl}?label_name[]=frontend`;
+
+ createComponent(mockProps({ text: url }));
+
+ expect(findLine().text()).toBe(url);
+ expect(findLinks().at(0).text()).toBe(url);
+ expect(findLinks().at(0).attributes('href')).toBe(url);
+ });
+
it('renders multiple links surrounded by text', () => {
createComponent(
mockProps({ text: `Well, my HTTP url: ${httpUrl} and my HTTPS url: ${httpsUrl}` }),
@@ -125,6 +135,26 @@ describe('Job Log Line', () => {
expect(findLinkAttributeByIndex(4).href).toBe(httpsUrl);
});
+ it('renders multiple links surrounded by brackets', () => {
+ createComponent(mockProps({ text: `(${httpUrl}) <${httpUrl}> {${httpsUrl}}` }));
+ expect(findLine().text()).toBe(
+ '(http://example.com) <http://example.com> {https://example.com}',
+ );
+
+ const links = findLinks();
+
+ expect(links).toHaveLength(3);
+
+ expect(links.at(0).text()).toBe(httpUrl);
+ expect(links.at(0).attributes('href')).toBe(httpUrl);
+
+ expect(links.at(1).text()).toBe(httpUrl);
+ expect(links.at(1).attributes('href')).toBe(httpUrl);
+
+ expect(links.at(2).text()).toBe(httpsUrl);
+ expect(links.at(2).attributes('href')).toBe(httpsUrl);
+ });
+
it('renders text with symbols in it', () => {
const text = 'apt-get update < /dev/null > /dev/null';
createComponent(mockProps({ text }));
diff --git a/spec/frontend/jobs/components/log/log_spec.js b/spec/frontend/jobs/components/log/log_spec.js
index b7aff1f3e3b..99fb6846ce5 100644
--- a/spec/frontend/jobs/components/log/log_spec.js
+++ b/spec/frontend/jobs/components/log/log_spec.js
@@ -1,7 +1,7 @@
import { mount, createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex';
import Log from '~/jobs/components/log/log.vue';
-import { logLinesParser } from '~/jobs/store/utils';
+import { logLinesParserLegacy, logLinesParser } from '~/jobs/store/utils';
import { jobLog } from './mock_data';
describe('Job Log', () => {
@@ -9,6 +9,7 @@ describe('Job Log', () => {
let actions;
let state;
let store;
+ let origGon;
const localVue = createLocalVue();
localVue.use(Vuex);
@@ -25,8 +26,12 @@ describe('Job Log', () => {
toggleCollapsibleLine: () => {},
};
+ origGon = window.gon;
+
+ window.gon = { features: { infinitelyCollapsibleSections: false } };
+
state = {
- trace: logLinesParser(jobLog),
+ trace: logLinesParserLegacy(jobLog),
traceEndpoint: 'jobs/id',
};
@@ -40,6 +45,88 @@ describe('Job Log', () => {
afterEach(() => {
wrapper.destroy();
+
+ window.gon = origGon;
+ });
+
+ const findCollapsibleLine = () => wrapper.find('.collapsible-line');
+
+ describe('line numbers', () => {
+ it('renders a line number for each open line', () => {
+ expect(wrapper.find('#L1').text()).toBe('1');
+ expect(wrapper.find('#L2').text()).toBe('2');
+ expect(wrapper.find('#L3').text()).toBe('3');
+ });
+
+ it('links to the provided path and correct line number', () => {
+ expect(wrapper.find('#L1').attributes('href')).toBe(`${state.traceEndpoint}#L1`);
+ });
+ });
+
+ describe('collapsible sections', () => {
+ it('renders a clickable header section', () => {
+ expect(findCollapsibleLine().attributes('role')).toBe('button');
+ });
+
+ it('renders an icon with the open state', () => {
+ expect(findCollapsibleLine().find('[data-testid="angle-down-icon"]').exists()).toBe(true);
+ });
+
+ describe('on click header section', () => {
+ it('calls toggleCollapsibleLine', () => {
+ jest.spyOn(wrapper.vm, 'toggleCollapsibleLine');
+
+ findCollapsibleLine().trigger('click');
+
+ expect(wrapper.vm.toggleCollapsibleLine).toHaveBeenCalled();
+ });
+ });
+ });
+});
+
+describe('Job Log, infinitelyCollapsibleSections feature flag enabled', () => {
+ let wrapper;
+ let actions;
+ let state;
+ let store;
+ let origGon;
+
+ const localVue = createLocalVue();
+ localVue.use(Vuex);
+
+ const createComponent = () => {
+ wrapper = mount(Log, {
+ localVue,
+ store,
+ });
+ };
+
+ beforeEach(() => {
+ actions = {
+ toggleCollapsibleLine: () => {},
+ };
+
+ origGon = window.gon;
+
+ window.gon = { features: { infinitelyCollapsibleSections: true } };
+
+ state = {
+ trace: logLinesParser(jobLog).parsedLines,
+ traceEndpoint: 'jobs/id',
+ };
+
+ store = new Vuex.Store({
+ actions,
+ state,
+ });
+
+ createComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+
+ window.gon = origGon;
});
const findCollapsibleLine = () => wrapper.find('.collapsible-line');
diff --git a/spec/frontend/jobs/components/log/mock_data.js b/spec/frontend/jobs/components/log/mock_data.js
index eb8c4fe8bc9..76c35703106 100644
--- a/spec/frontend/jobs/components/log/mock_data.js
+++ b/spec/frontend/jobs/components/log/mock_data.js
@@ -58,6 +58,71 @@ export const utilsMockData = [
},
];
+export const multipleCollapsibleSectionsMockData = [
+ {
+ offset: 1001,
+ content: [{ text: ' on docker-auto-scale-com 8a6210b8' }],
+ },
+ {
+ offset: 1002,
+ content: [
+ {
+ text: 'Executing "step_script" stage of the job script',
+ },
+ ],
+ section: 'step-script',
+ section_header: true,
+ },
+ {
+ offset: 1003,
+ content: [{ text: 'sleep 60' }],
+ section: 'step-script',
+ },
+ {
+ offset: 1004,
+ content: [
+ {
+ text:
+ 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam lorem dolor, congue ac condimentum vitae',
+ },
+ ],
+ section: 'step-script',
+ },
+ {
+ offset: 1005,
+ content: [{ text: 'executing...' }],
+ section: 'step-script',
+ },
+ {
+ offset: 1006,
+ content: [{ text: '1st collapsible section' }],
+ section: 'collapsible-1',
+ section_header: true,
+ },
+ {
+ offset: 1007,
+ content: [
+ {
+ text:
+ 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam lorem dolor, congue ac condimentum vitae',
+ },
+ ],
+ section: 'collapsible-1',
+ },
+ {
+ offset: 1008,
+ content: [],
+ section: 'collapsible-1',
+ section_duration: '01:00',
+ },
+ {
+ offset: 1009,
+ content: [],
+ section: 'step-script',
+ section_duration: '10:00',
+ },
+];
+
export const originalTrace = [
{
offset: 1,
diff --git a/spec/frontend/jobs/components/manual_variables_form_spec.js b/spec/frontend/jobs/components/manual_variables_form_spec.js
index 376a822dde5..7e42ee957d3 100644
--- a/spec/frontend/jobs/components/manual_variables_form_spec.js
+++ b/spec/frontend/jobs/components/manual_variables_form_spec.js
@@ -1,3 +1,4 @@
+import { GlSprintf, GlLink } from '@gitlab/ui';
import { createLocalVue, mount, shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import Vuex from 'vuex';
@@ -18,7 +19,6 @@ describe('Manual Variables Form', () => {
method: 'post',
button_title: 'Trigger this manual action',
},
- variablesSettingsUrl: '/settings',
};
const createComponent = ({ props = {}, mountFn = shallowMount } = {}) => {
@@ -33,15 +33,19 @@ describe('Manual Variables Form', () => {
propsData: { ...requiredProps, ...props },
localVue,
store,
+ stubs: {
+ GlSprintf,
+ },
}),
);
};
const findInputKey = () => wrapper.findComponent({ ref: 'inputKey' });
const findInputValue = () => wrapper.findComponent({ ref: 'inputSecretValue' });
+ const findHelpText = () => wrapper.findComponent(GlSprintf);
+ const findHelpLink = () => wrapper.findComponent(GlLink);
const findTriggerBtn = () => wrapper.findByTestId('trigger-manual-job-btn');
- const findHelpText = () => wrapper.findByTestId('form-help-text');
const findDeleteVarBtn = () => wrapper.findByTestId('delete-variable-btn');
const findCiVariableKey = () => wrapper.findByTestId('ci-variable-key');
const findCiVariableValue = () => wrapper.findByTestId('ci-variable-value');
@@ -62,11 +66,10 @@ describe('Manual Variables Form', () => {
});
it('renders help text with provided link', () => {
- expect(findHelpText().text()).toBe(
- 'Specify variable values to be used in this run. The values specified in CI/CD settings will be used as default',
+ expect(findHelpText().exists()).toBe(true);
+ expect(findHelpLink().attributes('href')).toBe(
+ '/help/ci/variables/index#add-a-cicd-variable-to-a-project',
);
-
- expect(wrapper.find('a').attributes('href')).toBe(requiredProps.variablesSettingsUrl);
});
describe('when adding a new variable', () => {
diff --git a/spec/frontend/jobs/components/sidebar_detail_row_spec.js b/spec/frontend/jobs/components/sidebar_detail_row_spec.js
index bae4d6cf837..43f2e022dd8 100644
--- a/spec/frontend/jobs/components/sidebar_detail_row_spec.js
+++ b/spec/frontend/jobs/components/sidebar_detail_row_spec.js
@@ -7,7 +7,7 @@ describe('Sidebar detail row', () => {
const title = 'this is the title';
const value = 'this is the value';
- const helpUrl = '/help/ci/runners/README.html';
+ const helpUrl = '/help/ci/runners/index.html';
const findHelpLink = () => wrapper.findComponent(GlLink);
diff --git a/spec/frontend/jobs/store/mutations_spec.js b/spec/frontend/jobs/store/mutations_spec.js
index 1c7e45dfb3d..159315330e4 100644
--- a/spec/frontend/jobs/store/mutations_spec.js
+++ b/spec/frontend/jobs/store/mutations_spec.js
@@ -4,12 +4,21 @@ import state from '~/jobs/store/state';
describe('Jobs Store Mutations', () => {
let stateCopy;
+ let origGon;
const html =
'I, [2018-08-17T22:57:45.707325 #1841] INFO -- : Writing /builds/ab89e95b0fa0b9272ea0c797b76908f24d36992630e9325273a4ce3.png<br>I';
beforeEach(() => {
stateCopy = state();
+
+ origGon = window.gon;
+
+ window.gon = { features: { infinitelyCollapsibleSections: false } };
+ });
+
+ afterEach(() => {
+ window.gon = origGon;
});
describe('SET_JOB_ENDPOINT', () => {
@@ -267,3 +276,88 @@ describe('Jobs Store Mutations', () => {
});
});
});
+
+describe('Job Store mutations, feature flag ON', () => {
+ let stateCopy;
+ let origGon;
+
+ const html =
+ 'I, [2018-08-17T22:57:45.707325 #1841] INFO -- : Writing /builds/ab89e95b0fa0b9272ea0c797b76908f24d36992630e9325273a4ce3.png<br>I';
+
+ beforeEach(() => {
+ stateCopy = state();
+
+ origGon = window.gon;
+
+ window.gon = { features: { infinitelyCollapsibleSections: true } };
+ });
+
+ afterEach(() => {
+ window.gon = origGon;
+ });
+
+ describe('RECEIVE_TRACE_SUCCESS', () => {
+ describe('with new job log', () => {
+ describe('log.lines', () => {
+ describe('when append is true', () => {
+ it('sets the parsed log ', () => {
+ mutations[types.RECEIVE_TRACE_SUCCESS](stateCopy, {
+ append: true,
+ size: 511846,
+ complete: true,
+ lines: [
+ {
+ offset: 1,
+ content: [{ text: 'Running with gitlab-runner 11.12.1 (5a147c92)' }],
+ },
+ ],
+ });
+
+ expect(stateCopy.trace).toEqual([
+ {
+ offset: 1,
+ content: [{ text: 'Running with gitlab-runner 11.12.1 (5a147c92)' }],
+ lineNumber: 1,
+ },
+ ]);
+ });
+ });
+
+ describe('when lines are defined', () => {
+ it('sets the parsed log ', () => {
+ mutations[types.RECEIVE_TRACE_SUCCESS](stateCopy, {
+ append: false,
+ size: 511846,
+ complete: true,
+ lines: [
+ { offset: 0, content: [{ text: 'Running with gitlab-runner 11.11.1 (5a147c92)' }] },
+ ],
+ });
+
+ expect(stateCopy.trace).toEqual([
+ {
+ offset: 0,
+ content: [{ text: 'Running with gitlab-runner 11.11.1 (5a147c92)' }],
+ lineNumber: 1,
+ },
+ ]);
+ });
+ });
+
+ describe('when lines are null', () => {
+ it('sets the default value', () => {
+ mutations[types.RECEIVE_TRACE_SUCCESS](stateCopy, {
+ append: true,
+ html,
+ size: 511846,
+ complete: false,
+ lines: null,
+ });
+
+ expect(stateCopy.trace).toEqual([]);
+ });
+ });
+ });
+ });
+ });
+});
diff --git a/spec/frontend/jobs/store/utils_spec.js b/spec/frontend/jobs/store/utils_spec.js
index e50d304bb08..35ac2945ab5 100644
--- a/spec/frontend/jobs/store/utils_spec.js
+++ b/spec/frontend/jobs/store/utils_spec.js
@@ -1,5 +1,6 @@
import {
logLinesParser,
+ logLinesParserLegacy,
updateIncrementalTrace,
parseHeaderLine,
parseLine,
@@ -17,6 +18,7 @@ import {
headerTraceIncremental,
collapsibleTrace,
collapsibleTraceIncremental,
+ multipleCollapsibleSectionsMockData,
} from '../components/log/mock_data';
describe('Jobs Store Utils', () => {
@@ -175,11 +177,11 @@ describe('Jobs Store Utils', () => {
expect(isCollapsibleSection()).toEqual(false);
});
});
- describe('logLinesParser', () => {
+ describe('logLinesParserLegacy', () => {
let result;
beforeEach(() => {
- result = logLinesParser(utilsMockData);
+ result = logLinesParserLegacy(utilsMockData);
});
describe('regular line', () => {
@@ -216,6 +218,87 @@ describe('Jobs Store Utils', () => {
});
});
+ describe('logLinesParser', () => {
+ let result;
+
+ beforeEach(() => {
+ result = logLinesParser(utilsMockData);
+ });
+
+ describe('regular line', () => {
+ it('adds a lineNumber property with correct index', () => {
+ expect(result.parsedLines[0].lineNumber).toEqual(1);
+ expect(result.parsedLines[1].line.lineNumber).toEqual(2);
+ });
+ });
+
+ describe('collapsible section', () => {
+ it('adds a `isClosed` property', () => {
+ expect(result.parsedLines[1].isClosed).toEqual(false);
+ });
+
+ it('adds a `isHeader` property', () => {
+ expect(result.parsedLines[1].isHeader).toEqual(true);
+ });
+
+ it('creates a lines array property with the content of the collapsible section', () => {
+ expect(result.parsedLines[1].lines.length).toEqual(2);
+ expect(result.parsedLines[1].lines[0].content).toEqual(utilsMockData[2].content);
+ expect(result.parsedLines[1].lines[1].content).toEqual(utilsMockData[3].content);
+ });
+ });
+
+ describe('section duration', () => {
+ it('adds the section information to the header section', () => {
+ expect(result.parsedLines[1].line.section_duration).toEqual(
+ utilsMockData[4].section_duration,
+ );
+ });
+
+ it('does not add section duration as a line', () => {
+ expect(result.parsedLines[1].lines.includes(utilsMockData[4])).toEqual(false);
+ });
+ });
+
+ describe('multiple collapsible sections', () => {
+ beforeEach(() => {
+ result = logLinesParser(multipleCollapsibleSectionsMockData);
+ });
+
+ it('should contain a section inside another section', () => {
+ const innerSection = [
+ {
+ isClosed: false,
+ isHeader: true,
+ line: {
+ content: [{ text: '1st collapsible section' }],
+ lineNumber: 6,
+ offset: 1006,
+ section: 'collapsible-1',
+ section_duration: '01:00',
+ section_header: true,
+ },
+ lines: [
+ {
+ content: [
+ {
+ text:
+ 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam lorem dolor, congue ac condimentum vitae',
+ },
+ ],
+ lineNumber: 7,
+ offset: 1007,
+ section: 'collapsible-1',
+ },
+ ],
+ },
+ ];
+
+ expect(result.parsedLines[1].lines).toEqual(expect.arrayContaining(innerSection));
+ });
+ });
+ });
+
describe('findOffsetAndRemove', () => {
describe('when last item is header', () => {
const existingLog = [
@@ -391,7 +474,7 @@ describe('Jobs Store Utils', () => {
describe('updateIncrementalTrace', () => {
describe('without repeated section', () => {
it('concats and parses both arrays', () => {
- const oldLog = logLinesParser(originalTrace);
+ const oldLog = logLinesParserLegacy(originalTrace);
const result = updateIncrementalTrace(regularIncremental, oldLog);
expect(result).toEqual([
@@ -419,7 +502,7 @@ describe('Jobs Store Utils', () => {
describe('with regular line repeated offset', () => {
it('updates the last line and formats with the incremental part', () => {
- const oldLog = logLinesParser(originalTrace);
+ const oldLog = logLinesParserLegacy(originalTrace);
const result = updateIncrementalTrace(regularIncrementalRepeated, oldLog);
expect(result).toEqual([
@@ -438,7 +521,7 @@ describe('Jobs Store Utils', () => {
describe('with header line repeated', () => {
it('updates the header line and formats with the incremental part', () => {
- const oldLog = logLinesParser(headerTrace);
+ const oldLog = logLinesParserLegacy(headerTrace);
const result = updateIncrementalTrace(headerTraceIncremental, oldLog);
expect(result).toEqual([
@@ -464,7 +547,7 @@ describe('Jobs Store Utils', () => {
describe('with collapsible line repeated', () => {
it('updates the collapsible line and formats with the incremental part', () => {
- const oldLog = logLinesParser(collapsibleTrace);
+ const oldLog = logLinesParserLegacy(collapsibleTrace);
const result = updateIncrementalTrace(collapsibleTraceIncremental, oldLog);
expect(result).toEqual([
diff --git a/spec/frontend/lib/dompurify_spec.js b/spec/frontend/lib/dompurify_spec.js
index a01f86678e9..fa8dbb12a08 100644
--- a/spec/frontend/lib/dompurify_spec.js
+++ b/spec/frontend/lib/dompurify_spec.js
@@ -30,6 +30,9 @@ const unsafeUrls = [
`https://evil.url/${absoluteGon.sprite_file_icons}`,
];
+const forbiddenDataAttrs = ['data-remote', 'data-url', 'data-type', 'data-method'];
+const acceptedDataAttrs = ['data-random', 'data-custom'];
+
describe('~/lib/dompurify', () => {
let originalGon;
@@ -95,4 +98,17 @@ describe('~/lib/dompurify', () => {
expect(sanitize(htmlXlink)).toBe(expectedSanitized);
});
});
+
+ describe('handles data attributes correctly', () => {
+ it.each(forbiddenDataAttrs)('removes %s attributes', (attr) => {
+ const htmlHref = `<a ${attr}="true">hello</a>`;
+ expect(sanitize(htmlHref)).toBe('<a>hello</a>');
+ });
+
+ it.each(acceptedDataAttrs)('does not remove %s attributes', (attr) => {
+ const attrWithValue = `${attr}="true"`;
+ const htmlHref = `<a ${attrWithValue}>hello</a>`;
+ expect(sanitize(htmlHref)).toBe(`<a ${attrWithValue}>hello</a>`);
+ });
+ });
});
diff --git a/spec/frontend/lib/graphql_spec.js b/spec/frontend/lib/graphql_spec.js
new file mode 100644
index 00000000000..a39ce2ffd99
--- /dev/null
+++ b/spec/frontend/lib/graphql_spec.js
@@ -0,0 +1,54 @@
+import getPipelineDetails from 'shared_queries/pipelines/get_pipeline_details.query.graphql';
+import { stripWhitespaceFromQuery } from '~/lib/graphql';
+import { queryToObject } from '~/lib/utils/url_utility';
+
+describe('stripWhitespaceFromQuery', () => {
+ const operationName = 'getPipelineDetails';
+ const variables = `{
+ projectPath: 'root/abcd-dag',
+ iid: '44'
+ }`;
+
+ const testQuery = getPipelineDetails.loc.source.body;
+ const defaultPath = '/api/graphql';
+ const encodedVariables = encodeURIComponent(variables);
+
+ it('shortens the query argument by replacing multiple spaces and newlines with a single space', () => {
+ const testString = `${defaultPath}?query=${encodeURIComponent(testQuery)}`;
+ expect(testString.length > stripWhitespaceFromQuery(testString, defaultPath).length).toBe(true);
+ });
+
+ it('does not contract a single space', () => {
+ const simpleSingleString = `${defaultPath}?query=${encodeURIComponent('fragment Nonsense')}`;
+ expect(stripWhitespaceFromQuery(simpleSingleString, defaultPath)).toEqual(simpleSingleString);
+ });
+
+ it('works with a non-default path', () => {
+ const newPath = 'another/graphql/path';
+ const newPathSingleString = `${newPath}?query=${encodeURIComponent('fragment Nonsense')}`;
+ expect(stripWhitespaceFromQuery(newPathSingleString, newPath)).toEqual(newPathSingleString);
+ });
+
+ it('does not alter other arguments', () => {
+ const bareParams = `?query=${encodeURIComponent(
+ testQuery,
+ )}&operationName=${operationName}&variables=${encodedVariables}`;
+ const testLongString = `${defaultPath}${bareParams}`;
+
+ const processed = stripWhitespaceFromQuery(testLongString, defaultPath);
+ const decoded = decodeURIComponent(processed);
+ const params = queryToObject(decoded);
+
+ expect(params.operationName).toBe(operationName);
+ expect(params.variables).toBe(variables);
+ });
+
+ it('works when there are no query params', () => {
+ expect(stripWhitespaceFromQuery(defaultPath, defaultPath)).toEqual(defaultPath);
+ });
+
+ it('works when the params do not include a query', () => {
+ const paramsWithoutQuery = `${defaultPath}&variables=${encodedVariables}`;
+ expect(stripWhitespaceFromQuery(paramsWithoutQuery, defaultPath)).toEqual(paramsWithoutQuery);
+ });
+});
diff --git a/spec/frontend/lib/utils/common_utils_spec.js b/spec/frontend/lib/utils/common_utils_spec.js
index e03d1ef7295..f5a74ee7f09 100644
--- a/spec/frontend/lib/utils/common_utils_spec.js
+++ b/spec/frontend/lib/utils/common_utils_spec.js
@@ -1,6 +1,56 @@
import * as commonUtils from '~/lib/utils/common_utils';
describe('common_utils', () => {
+ describe('getPagePath', () => {
+ const { getPagePath } = commonUtils;
+
+ let originalBody;
+
+ beforeEach(() => {
+ originalBody = document.body;
+ document.body = document.createElement('body');
+ });
+
+ afterEach(() => {
+ document.body = originalBody;
+ });
+
+ it('returns an empty path if none is defined', () => {
+ expect(getPagePath()).toBe('');
+ expect(getPagePath(0)).toBe('');
+ });
+
+ describe('returns a path', () => {
+ const mockSection = 'my_section';
+ const mockSubSection = 'my_sub_section';
+ const mockPage = 'my_page';
+
+ it('returns a page', () => {
+ document.body.dataset.page = mockPage;
+
+ expect(getPagePath()).toBe(mockPage);
+ expect(getPagePath(0)).toBe(mockPage);
+ });
+
+ it('returns a section and page', () => {
+ document.body.dataset.page = `${mockSection}:${mockPage}`;
+
+ expect(getPagePath()).toBe(mockSection);
+ expect(getPagePath(0)).toBe(mockSection);
+ expect(getPagePath(1)).toBe(mockPage);
+ });
+
+ it('returns a section and subsection', () => {
+ document.body.dataset.page = `${mockSection}:${mockSubSection}:${mockPage}`;
+
+ expect(getPagePath()).toBe(mockSection);
+ expect(getPagePath(0)).toBe(mockSection);
+ expect(getPagePath(1)).toBe(mockSubSection);
+ expect(getPagePath(2)).toBe(mockPage);
+ });
+ });
+ });
+
describe('parseUrl', () => {
it('returns an anchor tag with url', () => {
expect(commonUtils.parseUrl('/some/absolute/url').pathname).toContain('some/absolute/url');
@@ -26,42 +76,6 @@ describe('common_utils', () => {
});
});
- describe('urlParamsToArray', () => {
- it('returns empty array for empty querystring', () => {
- expect(commonUtils.urlParamsToArray('')).toEqual([]);
- });
-
- it('should decode params', () => {
- expect(commonUtils.urlParamsToArray('?label_name%5B%5D=test')[0]).toBe('label_name[]=test');
- });
-
- it('should remove the question mark from the search params', () => {
- const paramsArray = commonUtils.urlParamsToArray('?test=thing');
-
- expect(paramsArray[0][0]).not.toBe('?');
- });
- });
-
- describe('urlParamsToObject', () => {
- it('parses path for label with trailing +', () => {
- expect(commonUtils.urlParamsToObject('label_name[]=label%2B', {})).toEqual({
- label_name: ['label+'],
- });
- });
-
- it('parses path for milestone with trailing +', () => {
- expect(commonUtils.urlParamsToObject('milestone_title=A%2B', {})).toEqual({
- milestone_title: 'A+',
- });
- });
-
- it('parses path for search terms with spaces', () => {
- expect(commonUtils.urlParamsToObject('search=two+words', {})).toEqual({
- search: 'two words',
- });
- });
- });
-
describe('handleLocationHash', () => {
beforeEach(() => {
jest.spyOn(window.document, 'getElementById');
@@ -175,33 +189,6 @@ describe('common_utils', () => {
});
});
- describe('parseQueryStringIntoObject', () => {
- it('should return object with query parameters', () => {
- expect(commonUtils.parseQueryStringIntoObject('scope=all&page=2')).toEqual({
- scope: 'all',
- page: '2',
- });
-
- expect(commonUtils.parseQueryStringIntoObject('scope=all')).toEqual({ scope: 'all' });
- expect(commonUtils.parseQueryStringIntoObject()).toEqual({});
- });
- });
-
- describe('objectToQueryString', () => {
- it('returns empty string when `param` is undefined, null or empty string', () => {
- expect(commonUtils.objectToQueryString()).toBe('');
- expect(commonUtils.objectToQueryString('')).toBe('');
- });
-
- it('returns query string with values of `params`', () => {
- const singleQueryParams = { foo: true };
- const multipleQueryParams = { foo: true, bar: true };
-
- expect(commonUtils.objectToQueryString(singleQueryParams)).toBe('foo=true');
- expect(commonUtils.objectToQueryString(multipleQueryParams)).toBe('foo=true&bar=true');
- });
- });
-
describe('buildUrlWithCurrentLocation', () => {
it('should build an url with current location and given parameters', () => {
expect(commonUtils.buildUrlWithCurrentLocation()).toEqual(window.location.pathname);
@@ -310,39 +297,6 @@ describe('common_utils', () => {
});
});
- describe('getParameterByName', () => {
- beforeEach(() => {
- window.history.pushState({}, null, '?scope=all&p=2');
- });
-
- afterEach(() => {
- window.history.replaceState({}, null, null);
- });
-
- it('should return valid parameter', () => {
- const value = commonUtils.getParameterByName('scope');
-
- expect(commonUtils.getParameterByName('p')).toEqual('2');
- expect(value).toBe('all');
- });
-
- it('should return invalid parameter', () => {
- const value = commonUtils.getParameterByName('fakeParameter');
-
- expect(value).toBe(null);
- });
-
- it('should return valid paramentes if URL is provided', () => {
- let value = commonUtils.getParameterByName('foo', 'http://cocteau.twins/?foo=bar');
-
- expect(value).toBe('bar');
-
- value = commonUtils.getParameterByName('manan', 'http://cocteau.twins/?foo=bar&manan=canchu');
-
- expect(value).toBe('canchu');
- });
- });
-
describe('normalizedHeaders', () => {
it('should upperCase all the header keys to keep them consistent', () => {
const apiHeaders = {
diff --git a/spec/frontend/lib/utils/datetime/timeago_utility_spec.js b/spec/frontend/lib/utils/datetime/timeago_utility_spec.js
new file mode 100644
index 00000000000..2314ec678d3
--- /dev/null
+++ b/spec/frontend/lib/utils/datetime/timeago_utility_spec.js
@@ -0,0 +1,103 @@
+import { getTimeago, localTimeAgo, timeFor } from '~/lib/utils/datetime/timeago_utility';
+import { s__ } from '~/locale';
+import '~/commons/bootstrap';
+
+describe('TimeAgo utils', () => {
+ let oldGon;
+
+ afterEach(() => {
+ window.gon = oldGon;
+ });
+
+ beforeEach(() => {
+ oldGon = window.gon;
+ });
+
+ describe('getTimeago', () => {
+ describe('with User Setting timeDisplayRelative: true', () => {
+ beforeEach(() => {
+ window.gon = { time_display_relative: true };
+ });
+
+ it.each([
+ [new Date().toISOString(), 'just now'],
+ [new Date().getTime(), 'just now'],
+ [new Date(), 'just now'],
+ [null, 'just now'],
+ ])('formats date `%p` as `%p`', (date, result) => {
+ expect(getTimeago().format(date)).toEqual(result);
+ });
+ });
+
+ describe('with User Setting timeDisplayRelative: false', () => {
+ beforeEach(() => {
+ window.gon = { time_display_relative: false };
+ });
+
+ it.each([
+ [new Date().toISOString(), 'Jul 6, 2020, 12:00 AM'],
+ [new Date(), 'Jul 6, 2020, 12:00 AM'],
+ [new Date().getTime(), 'Jul 6, 2020, 12:00 AM'],
+ // Slightly different behaviour when `null` is passed :see_no_evil`
+ [null, 'Jan 1, 1970, 12:00 AM'],
+ ])('formats date `%p` as `%p`', (date, result) => {
+ expect(getTimeago().format(date)).toEqual(result);
+ });
+ });
+ });
+
+ describe('timeFor', () => {
+ it('returns localize `past due` when in past', () => {
+ const date = new Date();
+ date.setFullYear(date.getFullYear() - 1);
+
+ expect(timeFor(date)).toBe(s__('Timeago|Past due'));
+ });
+
+ it('returns localized remaining time when in the future', () => {
+ const date = new Date();
+ date.setFullYear(date.getFullYear() + 1);
+
+ // Add a day to prevent a transient error. If date is even 1 second
+ // short of a full year, timeFor will return '11 months remaining'
+ date.setDate(date.getDate() + 1);
+
+ expect(timeFor(date)).toBe(s__('Timeago|1 year remaining'));
+ });
+ });
+
+ describe('localTimeAgo', () => {
+ beforeEach(() => {
+ document.body.innerHTML =
+ '<time title="some time" datetime="2020-02-18T22:22:32Z">1 hour ago</time>';
+ });
+
+ describe.each`
+ timeDisplayRelative | text
+ ${true} | ${'4 months ago'}
+ ${false} | ${'Feb 18, 2020, 10:22 PM'}
+ `(
+ `With User Setting timeDisplayRelative: $timeDisplayRelative`,
+ ({ timeDisplayRelative, text }) => {
+ it.each`
+ updateTooltip | title
+ ${false} | ${'some time'}
+ ${true} | ${'Feb 18, 2020 10:22pm UTC'}
+ `(
+ `has content: '${text}' and tooltip: '$title' with updateTooltip = $updateTooltip`,
+ ({ updateTooltip, title }) => {
+ window.gon = { time_display_relative: timeDisplayRelative };
+
+ const element = document.querySelector('time');
+ localTimeAgo([element], updateTooltip);
+
+ jest.runAllTimers();
+
+ expect(element.getAttribute('title')).toBe(title);
+ expect(element.innerText).toBe(text);
+ },
+ );
+ },
+ );
+ });
+});
diff --git a/spec/frontend/lib/utils/datetime_utility_spec.js b/spec/frontend/lib/utils/datetime_utility_spec.js
index df0ccb19cb7..f6ad41d5478 100644
--- a/spec/frontend/lib/utils/datetime_utility_spec.js
+++ b/spec/frontend/lib/utils/datetime_utility_spec.js
@@ -1,30 +1,9 @@
-import $ from 'jquery';
import timezoneMock from 'timezone-mock';
import * as datetimeUtility from '~/lib/utils/datetime_utility';
import { __, s__ } from '~/locale';
import '~/commons/bootstrap';
describe('Date time utils', () => {
- describe('timeFor', () => {
- it('returns localize `past due` when in past', () => {
- const date = new Date();
- date.setFullYear(date.getFullYear() - 1);
-
- expect(datetimeUtility.timeFor(date)).toBe(s__('Timeago|Past due'));
- });
-
- it('returns localized remaining time when in the future', () => {
- const date = new Date();
- date.setFullYear(date.getFullYear() + 1);
-
- // Add a day to prevent a transient error. If date is even 1 second
- // short of a full year, timeFor will return '11 months remaining'
- date.setDate(date.getDate() + 1);
-
- expect(datetimeUtility.timeFor(date)).toBe(s__('Timeago|1 year remaining'));
- });
- });
-
describe('get localized day name', () => {
it('should return Sunday', () => {
const day = datetimeUtility.getDayName(new Date('07/17/2016'));
@@ -870,25 +849,6 @@ describe('approximateDuration', () => {
});
});
-describe('localTimeAgo', () => {
- beforeEach(() => {
- document.body.innerHTML = `<time title="some time" datetime="2020-02-18T22:22:32Z">1 hour ago</time>`;
- });
-
- it.each`
- timeagoArg | title
- ${false} | ${'some time'}
- ${true} | ${'Feb 18, 2020 10:22pm UTC'}
- `('converts $seconds seconds to $approximation', ({ timeagoArg, title }) => {
- const element = document.querySelector('time');
- datetimeUtility.localTimeAgo($(element), timeagoArg);
-
- jest.runAllTimers();
-
- expect(element.getAttribute('title')).toBe(title);
- });
-});
-
describe('differenceInSeconds', () => {
const startDateTime = new Date('2019-07-17T00:00:00.000Z');
diff --git a/spec/frontend/lib/utils/finite_state_machine_spec.js b/spec/frontend/lib/utils/finite_state_machine_spec.js
new file mode 100644
index 00000000000..441dd24c758
--- /dev/null
+++ b/spec/frontend/lib/utils/finite_state_machine_spec.js
@@ -0,0 +1,293 @@
+import { machine, transition } from '~/lib/utils/finite_state_machine';
+
+describe('Finite State Machine', () => {
+ const STATE_IDLE = 'idle';
+ const STATE_LOADING = 'loading';
+ const STATE_ERRORED = 'errored';
+
+ const TRANSITION_START_LOAD = 'START_LOAD';
+ const TRANSITION_LOAD_ERROR = 'LOAD_ERROR';
+ const TRANSITION_LOAD_SUCCESS = 'LOAD_SUCCESS';
+ const TRANSITION_ACKNOWLEDGE_ERROR = 'ACKNOWLEDGE_ERROR';
+
+ const definition = {
+ initial: STATE_IDLE,
+ states: {
+ [STATE_IDLE]: {
+ on: {
+ [TRANSITION_START_LOAD]: STATE_LOADING,
+ },
+ },
+ [STATE_LOADING]: {
+ on: {
+ [TRANSITION_LOAD_ERROR]: STATE_ERRORED,
+ [TRANSITION_LOAD_SUCCESS]: STATE_IDLE,
+ },
+ },
+ [STATE_ERRORED]: {
+ on: {
+ [TRANSITION_ACKNOWLEDGE_ERROR]: STATE_IDLE,
+ [TRANSITION_START_LOAD]: STATE_LOADING,
+ },
+ },
+ },
+ };
+
+ describe('machine', () => {
+ const STATE_IMPOSSIBLE = 'impossible';
+ const badDefinition = {
+ init: definition.initial,
+ badKeyShouldBeStates: definition.states,
+ };
+ const unstartableDefinition = {
+ initial: STATE_IMPOSSIBLE,
+ states: definition.states,
+ };
+ let liveMachine;
+
+ beforeEach(() => {
+ liveMachine = machine(definition);
+ });
+
+ it('throws an error if the machine definition is invalid', () => {
+ expect(() => machine(badDefinition)).toThrowError(
+ 'A state machine must have an initial state (`.initial`) and a dictionary of possible states (`.states`)',
+ );
+ });
+
+ it('throws an error if the initial state is invalid', () => {
+ expect(() => machine(unstartableDefinition)).toThrowError(
+ `Cannot initialize the state machine to state '${STATE_IMPOSSIBLE}'. Is that one of the machine's defined states?`,
+ );
+ });
+
+ it.each`
+ partOfMachine | equals | description | eqDescription
+ ${'keys'} | ${['is', 'send', 'value', 'states']} | ${'keys'} | ${'the correct array'}
+ ${'is'} | ${expect.any(Function)} | ${'`is` property'} | ${'a function'}
+ ${'send'} | ${expect.any(Function)} | ${'`send` property'} | ${'a function'}
+ ${'value'} | ${definition.initial} | ${'`value` property'} | ${'the same as the `initial` value of the machine definition'}
+ ${'states'} | ${definition.states} | ${'`states` property'} | ${'the same as the `states` value of the machine definition'}
+ `("The machine's $description should be $eqDescription", ({ partOfMachine, equals }) => {
+ const test = partOfMachine === 'keys' ? Object.keys(liveMachine) : liveMachine[partOfMachine];
+
+ expect(test).toEqual(equals);
+ });
+
+ it.each`
+ initialState | transitionEvent | expectedState
+ ${definition.initial} | ${TRANSITION_START_LOAD} | ${STATE_LOADING}
+ ${STATE_LOADING} | ${TRANSITION_LOAD_ERROR} | ${STATE_ERRORED}
+ ${STATE_ERRORED} | ${TRANSITION_ACKNOWLEDGE_ERROR} | ${STATE_IDLE}
+ ${STATE_IDLE} | ${TRANSITION_START_LOAD} | ${STATE_LOADING}
+ ${STATE_LOADING} | ${TRANSITION_LOAD_SUCCESS} | ${STATE_IDLE}
+ `(
+ 'properly steps from $initialState to $expectedState when the event "$transitionEvent" is sent',
+ ({ initialState, transitionEvent, expectedState }) => {
+ liveMachine.value = initialState;
+
+ liveMachine.send(transitionEvent);
+
+ expect(liveMachine.is(expectedState)).toBe(true);
+ expect(liveMachine.value).toBe(expectedState);
+ },
+ );
+
+ it.each`
+ initialState | transitionEvent
+ ${STATE_IDLE} | ${TRANSITION_ACKNOWLEDGE_ERROR}
+ ${STATE_IDLE} | ${TRANSITION_LOAD_SUCCESS}
+ ${STATE_IDLE} | ${TRANSITION_LOAD_ERROR}
+ ${STATE_IDLE} | ${'RANDOM_FOO'}
+ ${STATE_LOADING} | ${TRANSITION_START_LOAD}
+ ${STATE_LOADING} | ${TRANSITION_ACKNOWLEDGE_ERROR}
+ ${STATE_LOADING} | ${'RANDOM_FOO'}
+ ${STATE_ERRORED} | ${TRANSITION_LOAD_ERROR}
+ ${STATE_ERRORED} | ${TRANSITION_LOAD_SUCCESS}
+ ${STATE_ERRORED} | ${'RANDOM_FOO'}
+ `(
+ `does not perform any transition if the machine can't move from "$initialState" using the "$transitionEvent" event`,
+ ({ initialState, transitionEvent }) => {
+ liveMachine.value = initialState;
+
+ liveMachine.send(transitionEvent);
+
+ expect(liveMachine.is(initialState)).toBe(true);
+ expect(liveMachine.value).toBe(initialState);
+ },
+ );
+
+ describe('send', () => {
+ it.each`
+ startState | transitionEvent | result
+ ${STATE_IDLE} | ${TRANSITION_START_LOAD} | ${STATE_LOADING}
+ ${STATE_LOADING} | ${TRANSITION_LOAD_SUCCESS} | ${STATE_IDLE}
+ ${STATE_LOADING} | ${TRANSITION_LOAD_ERROR} | ${STATE_ERRORED}
+ ${STATE_ERRORED} | ${TRANSITION_ACKNOWLEDGE_ERROR} | ${STATE_IDLE}
+ ${STATE_ERRORED} | ${TRANSITION_START_LOAD} | ${STATE_LOADING}
+ `(
+ 'successfully transitions to $result from $startState when the transition $transitionEvent is received',
+ ({ startState, transitionEvent, result }) => {
+ liveMachine.value = startState;
+
+ expect(liveMachine.send(transitionEvent)).toEqual(result);
+ },
+ );
+
+ it.each`
+ startState | transitionEvent
+ ${STATE_IDLE} | ${TRANSITION_ACKNOWLEDGE_ERROR}
+ ${STATE_IDLE} | ${TRANSITION_LOAD_SUCCESS}
+ ${STATE_IDLE} | ${TRANSITION_LOAD_ERROR}
+ ${STATE_IDLE} | ${'RANDOM_FOO'}
+ ${STATE_LOADING} | ${TRANSITION_START_LOAD}
+ ${STATE_LOADING} | ${TRANSITION_ACKNOWLEDGE_ERROR}
+ ${STATE_LOADING} | ${'RANDOM_FOO'}
+ ${STATE_ERRORED} | ${TRANSITION_LOAD_ERROR}
+ ${STATE_ERRORED} | ${TRANSITION_LOAD_SUCCESS}
+ ${STATE_ERRORED} | ${'RANDOM_FOO'}
+ `(
+ 'remains as $startState if an undefined transition ($transitionEvent) is received',
+ ({ startState, transitionEvent }) => {
+ liveMachine.value = startState;
+
+ expect(liveMachine.send(transitionEvent)).toEqual(startState);
+ },
+ );
+
+ describe('detached', () => {
+ it.each`
+ startState | transitionEvent | result
+ ${STATE_IDLE} | ${TRANSITION_START_LOAD} | ${STATE_LOADING}
+ ${STATE_LOADING} | ${TRANSITION_LOAD_SUCCESS} | ${STATE_IDLE}
+ ${STATE_LOADING} | ${TRANSITION_LOAD_ERROR} | ${STATE_ERRORED}
+ ${STATE_ERRORED} | ${TRANSITION_ACKNOWLEDGE_ERROR} | ${STATE_IDLE}
+ ${STATE_ERRORED} | ${TRANSITION_START_LOAD} | ${STATE_LOADING}
+ `(
+ 'successfully transitions to $result from $startState when the transition $transitionEvent is received outside the context of the machine',
+ ({ startState, transitionEvent, result }) => {
+ const liveSend = machine({
+ ...definition,
+ initial: startState,
+ }).send;
+
+ expect(liveSend(transitionEvent)).toEqual(result);
+ },
+ );
+
+ it.each`
+ startState | transitionEvent
+ ${STATE_IDLE} | ${TRANSITION_ACKNOWLEDGE_ERROR}
+ ${STATE_IDLE} | ${TRANSITION_LOAD_SUCCESS}
+ ${STATE_IDLE} | ${TRANSITION_LOAD_ERROR}
+ ${STATE_IDLE} | ${'RANDOM_FOO'}
+ ${STATE_LOADING} | ${TRANSITION_START_LOAD}
+ ${STATE_LOADING} | ${TRANSITION_ACKNOWLEDGE_ERROR}
+ ${STATE_LOADING} | ${'RANDOM_FOO'}
+ ${STATE_ERRORED} | ${TRANSITION_LOAD_ERROR}
+ ${STATE_ERRORED} | ${TRANSITION_LOAD_SUCCESS}
+ ${STATE_ERRORED} | ${'RANDOM_FOO'}
+ `(
+ 'remains as $startState if an undefined transition ($transitionEvent) is received',
+ ({ startState, transitionEvent }) => {
+ const liveSend = machine({
+ ...definition,
+ initial: startState,
+ }).send;
+
+ expect(liveSend(transitionEvent)).toEqual(startState);
+ },
+ );
+ });
+ });
+
+ describe('is', () => {
+ it.each`
+ bool | test | actual
+ ${true} | ${STATE_IDLE} | ${STATE_IDLE}
+ ${false} | ${STATE_LOADING} | ${STATE_IDLE}
+ ${false} | ${STATE_ERRORED} | ${STATE_IDLE}
+ ${true} | ${STATE_LOADING} | ${STATE_LOADING}
+ ${false} | ${STATE_IDLE} | ${STATE_LOADING}
+ ${false} | ${STATE_ERRORED} | ${STATE_LOADING}
+ ${true} | ${STATE_ERRORED} | ${STATE_ERRORED}
+ ${false} | ${STATE_IDLE} | ${STATE_ERRORED}
+ ${false} | ${STATE_LOADING} | ${STATE_ERRORED}
+ `(
+ 'returns "$bool" for "$test" when the current state is "$actual"',
+ ({ bool, test, actual }) => {
+ liveMachine = machine({
+ ...definition,
+ initial: actual,
+ });
+
+ expect(liveMachine.is(test)).toEqual(bool);
+ },
+ );
+
+ describe('detached', () => {
+ it.each`
+ bool | test | actual
+ ${true} | ${STATE_IDLE} | ${STATE_IDLE}
+ ${false} | ${STATE_LOADING} | ${STATE_IDLE}
+ ${false} | ${STATE_ERRORED} | ${STATE_IDLE}
+ ${true} | ${STATE_LOADING} | ${STATE_LOADING}
+ ${false} | ${STATE_IDLE} | ${STATE_LOADING}
+ ${false} | ${STATE_ERRORED} | ${STATE_LOADING}
+ ${true} | ${STATE_ERRORED} | ${STATE_ERRORED}
+ ${false} | ${STATE_IDLE} | ${STATE_ERRORED}
+ ${false} | ${STATE_LOADING} | ${STATE_ERRORED}
+ `(
+ 'returns "$bool" for "$test" when the current state is "$actual"',
+ ({ bool, test, actual }) => {
+ const liveIs = machine({
+ ...definition,
+ initial: actual,
+ }).is;
+
+ expect(liveIs(test)).toEqual(bool);
+ },
+ );
+ });
+ });
+ });
+
+ describe('transition', () => {
+ it.each`
+ startState | transitionEvent | result
+ ${STATE_IDLE} | ${TRANSITION_START_LOAD} | ${STATE_LOADING}
+ ${STATE_LOADING} | ${TRANSITION_LOAD_SUCCESS} | ${STATE_IDLE}
+ ${STATE_LOADING} | ${TRANSITION_LOAD_ERROR} | ${STATE_ERRORED}
+ ${STATE_ERRORED} | ${TRANSITION_ACKNOWLEDGE_ERROR} | ${STATE_IDLE}
+ ${STATE_ERRORED} | ${TRANSITION_START_LOAD} | ${STATE_LOADING}
+ `(
+ 'successfully transitions to $result from $startState when the transition $transitionEvent is received',
+ ({ startState, transitionEvent, result }) => {
+ expect(transition(definition, startState, transitionEvent)).toEqual(result);
+ },
+ );
+
+ it.each`
+ startState | transitionEvent
+ ${STATE_IDLE} | ${TRANSITION_ACKNOWLEDGE_ERROR}
+ ${STATE_IDLE} | ${TRANSITION_LOAD_SUCCESS}
+ ${STATE_IDLE} | ${TRANSITION_LOAD_ERROR}
+ ${STATE_IDLE} | ${'RANDOM_FOO'}
+ ${STATE_LOADING} | ${TRANSITION_START_LOAD}
+ ${STATE_LOADING} | ${TRANSITION_ACKNOWLEDGE_ERROR}
+ ${STATE_LOADING} | ${'RANDOM_FOO'}
+ ${STATE_ERRORED} | ${TRANSITION_LOAD_ERROR}
+ ${STATE_ERRORED} | ${TRANSITION_LOAD_SUCCESS}
+ ${STATE_ERRORED} | ${'RANDOM_FOO'}
+ `(
+ 'remains as $startState if an undefined transition ($transitionEvent) is received',
+ ({ startState, transitionEvent }) => {
+ expect(transition(definition, startState, transitionEvent)).toEqual(startState);
+ },
+ );
+
+ it('remains as the provided starting state if it is an unrecognized state', () => {
+ expect(transition(definition, 'RANDOM_FOO', TRANSITION_START_LOAD)).toEqual('RANDOM_FOO');
+ });
+ });
+});
diff --git a/spec/frontend/lib/utils/text_markdown_spec.js b/spec/frontend/lib/utils/text_markdown_spec.js
index cad500039c0..beedb9b2eba 100644
--- a/spec/frontend/lib/utils/text_markdown_spec.js
+++ b/spec/frontend/lib/utils/text_markdown_spec.js
@@ -300,7 +300,7 @@ describe('init markdown', () => {
});
});
- describe('Editor Lite', () => {
+ describe('Source Editor', () => {
let editor;
beforeEach(() => {
diff --git a/spec/frontend/lib/utils/url_utility_spec.js b/spec/frontend/lib/utils/url_utility_spec.js
index 31c78681994..66d0faa95e7 100644
--- a/spec/frontend/lib/utils/url_utility_spec.js
+++ b/spec/frontend/lib/utils/url_utility_spec.js
@@ -24,6 +24,16 @@ const setWindowLocation = (value) => {
};
describe('URL utility', () => {
+ let originalLocation;
+
+ beforeAll(() => {
+ originalLocation = window.location;
+ });
+
+ afterAll(() => {
+ window.location = originalLocation;
+ });
+
describe('webIDEUrl', () => {
afterEach(() => {
gon.relative_url_root = '';
@@ -319,19 +329,17 @@ describe('URL utility', () => {
});
describe('doesHashExistInUrl', () => {
- it('should return true when the given string exists in the URL hash', () => {
+ beforeEach(() => {
setWindowLocation({
- href: 'https://gitlab.com/gitlab-org/gitlab-test/issues/1#note_1',
+ hash: 'https://gitlab.com/gitlab-org/gitlab-test/issues/1#note_1',
});
+ });
+ it('should return true when the given string exists in the URL hash', () => {
expect(urlUtils.doesHashExistInUrl('note_')).toBe(true);
});
it('should return false when the given string does not exist in the URL hash', () => {
- setWindowLocation({
- href: 'https://gitlab.com/gitlab-org/gitlab-test/issues/1#note_1',
- });
-
expect(urlUtils.doesHashExistInUrl('doesnotexist')).toBe(false);
});
});
@@ -651,6 +659,45 @@ describe('URL utility', () => {
});
});
+ describe('urlParamsToArray', () => {
+ it('returns empty array for empty querystring', () => {
+ expect(urlUtils.urlParamsToArray('')).toEqual([]);
+ });
+
+ it('should decode params', () => {
+ expect(urlUtils.urlParamsToArray('?label_name%5B%5D=test')[0]).toBe('label_name[]=test');
+ });
+
+ it('should remove the question mark from the search params', () => {
+ const paramsArray = urlUtils.urlParamsToArray('?test=thing');
+
+ expect(paramsArray[0][0]).not.toBe('?');
+ });
+ });
+
+ describe('urlParamsToObject', () => {
+ it('parses path for label with trailing +', () => {
+ // eslint-disable-next-line import/no-deprecated
+ expect(urlUtils.urlParamsToObject('label_name[]=label%2B', {})).toEqual({
+ label_name: ['label+'],
+ });
+ });
+
+ it('parses path for milestone with trailing +', () => {
+ // eslint-disable-next-line import/no-deprecated
+ expect(urlUtils.urlParamsToObject('milestone_title=A%2B', {})).toEqual({
+ milestone_title: 'A+',
+ });
+ });
+
+ it('parses path for search terms with spaces', () => {
+ // eslint-disable-next-line import/no-deprecated
+ expect(urlUtils.urlParamsToObject('search=two+words', {})).toEqual({
+ search: 'two words',
+ });
+ });
+ });
+
describe('queryToObject', () => {
it.each`
case | query | options | result
@@ -673,12 +720,68 @@ describe('URL utility', () => {
});
});
+ describe('getParameterByName', () => {
+ const { getParameterByName } = urlUtils;
+
+ it('should return valid parameter', () => {
+ setWindowLocation({ search: '?scope=all&p=2' });
+
+ expect(getParameterByName('p')).toEqual('2');
+ expect(getParameterByName('scope')).toBe('all');
+ });
+
+ it('should return invalid parameter', () => {
+ setWindowLocation({ search: '?scope=all&p=2' });
+
+ expect(getParameterByName('fakeParameter')).toBe(null);
+ });
+
+ it('should return a parameter with spaces', () => {
+ setWindowLocation({ search: '?search=my terms' });
+
+ expect(getParameterByName('search')).toBe('my terms');
+ });
+
+ it('should return a parameter with encoded spaces', () => {
+ setWindowLocation({ search: '?search=my%20terms' });
+
+ expect(getParameterByName('search')).toBe('my terms');
+ });
+
+ it('should return a parameter with plus signs as spaces', () => {
+ setWindowLocation({ search: '?search=my+terms' });
+
+ expect(getParameterByName('search')).toBe('my terms');
+ });
+
+ it('should return valid parameters if search is provided', () => {
+ expect(getParameterByName('foo', 'foo=bar')).toBe('bar');
+ expect(getParameterByName('foo', '?foo=bar')).toBe('bar');
+
+ expect(getParameterByName('manan', 'foo=bar&manan=canchu')).toBe('canchu');
+ expect(getParameterByName('manan', '?foo=bar&manan=canchu')).toBe('canchu');
+ });
+ });
+
describe('objectToQuery', () => {
it('converts search query object back into a search query', () => {
const searchQueryObject = { one: '1', two: '2' };
expect(urlUtils.objectToQuery(searchQueryObject)).toEqual('one=1&two=2');
});
+
+ it('returns empty string when `params` is undefined, null or empty string', () => {
+ expect(urlUtils.objectToQuery()).toBe('');
+ expect(urlUtils.objectToQuery('')).toBe('');
+ });
+
+ it('returns query string with values of `params`', () => {
+ const singleQueryParams = { foo: true };
+ const multipleQueryParams = { foo: true, bar: true };
+
+ expect(urlUtils.objectToQuery(singleQueryParams)).toBe('foo=true');
+ expect(urlUtils.objectToQuery(multipleQueryParams)).toBe('foo=true&bar=true');
+ });
});
describe('cleanLeadingSeparator', () => {
diff --git a/spec/frontend/line_highlighter_spec.js b/spec/frontend/line_highlighter_spec.js
index b5a0adc9d49..97ae6c0e3b7 100644
--- a/spec/frontend/line_highlighter_spec.js
+++ b/spec/frontend/line_highlighter_spec.js
@@ -49,6 +49,15 @@ describe('LineHighlighter', () => {
}
});
+ it('highlights a range of lines given in the URL hash using GitHub format', () => {
+ new LineHighlighter({ hash: '#L5-L25' });
+
+ expect($(`.${testContext.css}`).length).toBe(21);
+ for (let line = 5; line <= 25; line += 1) {
+ expect($(`#LC${line}`)).toHaveClass(testContext.css);
+ }
+ });
+
it('scrolls to the first highlighted line on initial load', () => {
jest.spyOn(utils, 'scrollToElement');
new LineHighlighter({ hash: '#L5-25' });
diff --git a/spec/frontend/locale/index_spec.js b/spec/frontend/locale/index_spec.js
index a08be502735..220061fc64a 100644
--- a/spec/frontend/locale/index_spec.js
+++ b/spec/frontend/locale/index_spec.js
@@ -1,5 +1,5 @@
import { setLanguage } from 'helpers/locale_helper';
-import { createDateTimeFormat, formatNumber, languageCode } from '~/locale';
+import { createDateTimeFormat, formatNumber, languageCode, getPreferredLocales } from '~/locale';
describe('locale', () => {
afterEach(() => setLanguage(null));
@@ -18,13 +18,91 @@ describe('locale', () => {
});
});
+ describe('getPreferredLocales', () => {
+ beforeEach(() => {
+ // Need to spy on window.navigator.languages as it is read-only
+ jest
+ .spyOn(window.navigator, 'languages', 'get')
+ .mockReturnValueOnce(['en-GB', 'en-US', 'de-AT']);
+ });
+
+ it('filters navigator.languages by GitLab language', () => {
+ setLanguage('en');
+
+ expect(getPreferredLocales()).toEqual(['en-GB', 'en-US', 'en']);
+ });
+
+ it('filters navigator.languages by GitLab language without locale and sets English Fallback', () => {
+ setLanguage('de');
+
+ expect(getPreferredLocales()).toEqual(['de-AT', 'de', 'en']);
+ });
+
+ it('filters navigator.languages by GitLab language with locale and sets English Fallback', () => {
+ setLanguage('de-DE');
+
+ expect(getPreferredLocales()).toEqual(['de-AT', 'de-DE', 'de', 'en']);
+ });
+
+ it('adds GitLab language if navigator.languages does not contain it', () => {
+ setLanguage('es-ES');
+
+ expect(getPreferredLocales()).toEqual(['es-ES', 'es', 'en']);
+ });
+ });
+
describe('createDateTimeFormat', () => {
- beforeEach(() => setLanguage('en'));
+ const date = new Date(2015, 0, 3, 15, 13, 22);
+ const formatOptions = { dateStyle: 'long', timeStyle: 'medium' };
it('creates an instance of Intl.DateTimeFormat', () => {
- const dateFormat = createDateTimeFormat({ year: 'numeric', month: 'long', day: 'numeric' });
+ const dateFormat = createDateTimeFormat(formatOptions);
+
+ expect(dateFormat).toBeInstanceOf(Intl.DateTimeFormat);
+ });
+
+ it('falls back to `en` and GitLab language is default', () => {
+ setLanguage(null);
+ jest.spyOn(window.navigator, 'languages', 'get').mockReturnValueOnce(['de-AT', 'en-GB']);
+
+ const dateFormat = createDateTimeFormat(formatOptions);
+ expect(dateFormat.format(date)).toBe(
+ new Intl.DateTimeFormat('en-GB', formatOptions).format(date),
+ );
+ });
+
+ it('falls back to `en` locale if browser languages are empty', () => {
+ setLanguage('en');
+ jest.spyOn(window.navigator, 'languages', 'get').mockReturnValueOnce([]);
+
+ const dateFormat = createDateTimeFormat(formatOptions);
+ expect(dateFormat.format(date)).toBe(
+ new Intl.DateTimeFormat('en', formatOptions).format(date),
+ );
+ });
+
+ it('prefers `en-GB` if it is the preferred language and GitLab language is `en`', () => {
+ setLanguage('en');
+ jest
+ .spyOn(window.navigator, 'languages', 'get')
+ .mockReturnValueOnce(['en-GB', 'en-US', 'en']);
+
+ const dateFormat = createDateTimeFormat(formatOptions);
+ expect(dateFormat.format(date)).toBe(
+ new Intl.DateTimeFormat('en-GB', formatOptions).format(date),
+ );
+ });
+
+ it('prefers `de-AT` if it is GitLab language and not part of the browser languages', () => {
+ setLanguage('de-AT');
+ jest
+ .spyOn(window.navigator, 'languages', 'get')
+ .mockReturnValueOnce(['en-GB', 'en-US', 'en']);
- expect(dateFormat.format(new Date(2015, 6, 3))).toBe('July 3, 2015');
+ const dateFormat = createDateTimeFormat(formatOptions);
+ expect(dateFormat.format(date)).toBe(
+ new Intl.DateTimeFormat('de-AT', formatOptions).format(date),
+ );
});
});
diff --git a/spec/frontend/logs/stores/actions_spec.js b/spec/frontend/logs/stores/actions_spec.js
index 9307a3b62fb..46ef1500a20 100644
--- a/spec/frontend/logs/stores/actions_spec.js
+++ b/spec/frontend/logs/stores/actions_spec.js
@@ -1,6 +1,5 @@
import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
-import { deprecatedCreateFlash as flash } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { convertToFixedRange } from '~/lib/utils/datetime_range';
import { TOKEN_TYPE_POD_NAME } from '~/logs/constants';
@@ -32,7 +31,6 @@ import {
mockNextCursor,
} from '../mock_data';
-jest.mock('~/flash');
jest.mock('~/lib/utils/datetime_range');
jest.mock('~/logs/utils');
@@ -75,10 +73,6 @@ describe('Logs Store actions', () => {
state = logsPageState();
});
- afterEach(() => {
- flash.mockClear();
- });
-
describe('setInitData', () => {
it('should commit environment and pod name mutation', () =>
testAction(
diff --git a/spec/frontend/members/components/app_spec.js b/spec/frontend/members/components/app_spec.js
index b9fdf8792fd..9590cd9d8d4 100644
--- a/spec/frontend/members/components/app_spec.js
+++ b/spec/frontend/members/components/app_spec.js
@@ -5,7 +5,8 @@ import Vuex from 'vuex';
import * as commonUtils from '~/lib/utils/common_utils';
import MembersApp from '~/members/components/app.vue';
import FilterSortContainer from '~/members/components/filter_sort/filter_sort_container.vue';
-import { MEMBER_TYPES } from '~/members/constants';
+import MembersTable from '~/members/components/table/members_table.vue';
+import { MEMBER_TYPES, TAB_QUERY_PARAM_VALUES } from '~/members/constants';
import { RECEIVE_MEMBER_ROLE_ERROR, HIDE_ERROR } from '~/members/store/mutation_types';
import mutations from '~/members/store/mutations';
@@ -19,7 +20,7 @@ describe('MembersApp', () => {
const createComponent = (state = {}, options = {}) => {
store = new Vuex.Store({
modules: {
- [MEMBER_TYPES.user]: {
+ [MEMBER_TYPES.group]: {
namespaced: true,
state: {
showError: true,
@@ -34,7 +35,8 @@ describe('MembersApp', () => {
wrapper = shallowMount(MembersApp, {
localVue,
propsData: {
- namespace: MEMBER_TYPES.user,
+ namespace: MEMBER_TYPES.group,
+ tabQueryParamValue: TAB_QUERY_PARAM_VALUES.group,
},
store,
...options,
@@ -57,7 +59,7 @@ describe('MembersApp', () => {
it('renders and scrolls to error alert', async () => {
createComponent({ showError: false, errorMessage: '' });
- store.commit(`${MEMBER_TYPES.user}/${RECEIVE_MEMBER_ROLE_ERROR}`, {
+ store.commit(`${MEMBER_TYPES.group}/${RECEIVE_MEMBER_ROLE_ERROR}`, {
error: new Error('Network Error'),
});
@@ -77,7 +79,7 @@ describe('MembersApp', () => {
it('does not render and scroll to error alert', async () => {
createComponent();
- store.commit(`${MEMBER_TYPES.user}/${HIDE_ERROR}`);
+ store.commit(`${MEMBER_TYPES.group}/${HIDE_ERROR}`);
await nextTick();
@@ -103,4 +105,13 @@ describe('MembersApp', () => {
expect(findFilterSortContainer().exists()).toBe(true);
});
+
+ it('renders `MembersTable` component and passes `tabQueryParamValue` prop', () => {
+ createComponent();
+
+ const membersTableComponent = wrapper.findComponent(MembersTable);
+
+ expect(membersTableComponent.exists()).toBe(true);
+ expect(membersTableComponent.props('tabQueryParamValue')).toBe(TAB_QUERY_PARAM_VALUES.group);
+ });
});
diff --git a/spec/frontend/members/components/filter_sort/members_filtered_search_bar_spec.js b/spec/frontend/members/components/filter_sort/members_filtered_search_bar_spec.js
index 5e04e20801a..a3b91cb20bb 100644
--- a/spec/frontend/members/components/filter_sort/members_filtered_search_bar_spec.js
+++ b/spec/frontend/members/components/filter_sort/members_filtered_search_bar_spec.js
@@ -216,5 +216,17 @@ describe('MembersFilteredSearchBar', () => {
'https://localhost/?two_factor=enabled&search=foobar&sort=name_asc',
);
});
+
+ it('adds active tab query param', () => {
+ window.location.search = '?tab=invited';
+
+ createComponent();
+
+ findFilteredSearchBar().vm.$emit('onFilter', [
+ { type: 'filtered-search-term', value: { data: 'foobar' } },
+ ]);
+
+ expect(window.location.href).toBe('https://localhost/?search=foobar&tab=invited');
+ });
});
});
diff --git a/spec/frontend/members/components/members_tabs_spec.js b/spec/frontend/members/components/members_tabs_spec.js
index 6f1a6d0c223..33d8eebf7eb 100644
--- a/spec/frontend/members/components/members_tabs_spec.js
+++ b/spec/frontend/members/components/members_tabs_spec.js
@@ -1,9 +1,14 @@
+import { GlTabs } from '@gitlab/ui';
import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import MembersApp from '~/members/components/app.vue';
import MembersTabs from '~/members/components/members_tabs.vue';
-import { MEMBER_TYPES } from '~/members/constants';
+import {
+ MEMBER_TYPES,
+ TAB_QUERY_PARAM_VALUES,
+ ACTIVE_TAB_QUERY_PARAM_NAME,
+} from '~/members/constants';
import { pagination } from '../mock_data';
describe('MembersTabs', () => {
@@ -93,6 +98,18 @@ describe('MembersTabs', () => {
wrapper.destroy();
});
+ it('renders `GlTabs` with `syncActiveTabWithQueryParams` and `queryParamName` props set', async () => {
+ await createComponent();
+
+ const glTabsComponent = wrapper.findComponent(GlTabs);
+
+ expect(glTabsComponent.exists()).toBe(true);
+ expect(glTabsComponent.props()).toMatchObject({
+ syncActiveTabWithQueryParams: true,
+ queryParamName: ACTIVE_TAB_QUERY_PARAM_NAME,
+ });
+ });
+
describe('when tabs have a count', () => {
it('renders tabs with count', async () => {
await createComponent();
@@ -106,7 +123,7 @@ describe('MembersTabs', () => {
expect(findActiveTab().text()).toContain('Members');
});
- it('renders `MembersApp` and passes `namespace` prop', async () => {
+ it('renders `MembersApp` and passes `namespace` and `tabQueryParamValue` props', async () => {
await createComponent();
const membersApps = wrapper.findAllComponents(MembersApp).wrappers;
@@ -115,6 +132,10 @@ describe('MembersTabs', () => {
expect(membersApps[1].props('namespace')).toBe(MEMBER_TYPES.group);
expect(membersApps[2].props('namespace')).toBe(MEMBER_TYPES.invite);
expect(membersApps[3].props('namespace')).toBe(MEMBER_TYPES.accessRequest);
+
+ expect(membersApps[1].props('tabQueryParamValue')).toBe(TAB_QUERY_PARAM_VALUES.group);
+ expect(membersApps[2].props('tabQueryParamValue')).toBe(TAB_QUERY_PARAM_VALUES.invite);
+ expect(membersApps[3].props('tabQueryParamValue')).toBe(TAB_QUERY_PARAM_VALUES.accessRequest);
});
});
@@ -127,56 +148,16 @@ describe('MembersTabs', () => {
expect(findTabByText('Invited')).toBeUndefined();
expect(findTabByText('Access requests')).toBeUndefined();
});
- });
- describe('when url param matches `filteredSearchBar.searchParam`', () => {
- beforeEach(() => {
- window.location.search = '?search_groups=foo+bar';
- });
-
- const expectGroupsTabActive = () => {
- expect(findActiveTab().text()).toContain('Groups');
- };
-
- describe('when tab has a count', () => {
- it('sets tab that corresponds to search param as active tab', async () => {
- await createComponent();
-
- expectGroupsTabActive();
+ describe('when url param matches `filteredSearchBar.searchParam`', () => {
+ beforeEach(() => {
+ window.location.search = '?search_groups=foo+bar';
});
- });
-
- describe('when tab does not have a count', () => {
- it('sets tab that corresponds to search param as active tab', async () => {
- await createComponent({ totalItems: 0 });
-
- expectGroupsTabActive();
- });
- });
- });
-
- describe('when url param matches `pagination.paramName`', () => {
- beforeEach(() => {
- window.location.search = '?invited_page=2';
- });
-
- const expectInvitedTabActive = () => {
- expect(findActiveTab().text()).toContain('Invited');
- };
-
- describe('when tab has a count', () => {
- it('sets tab that corresponds to pagination param as active tab', async () => {
- await createComponent();
-
- expectInvitedTabActive();
- });
- });
- describe('when tab does not have a count', () => {
- it('sets tab that corresponds to pagination param as active tab', async () => {
+ it('shows tab that corresponds to search param', async () => {
await createComponent({ totalItems: 0 });
- expectInvitedTabActive();
+ expect(findTabByText('Groups')).not.toBeUndefined();
});
});
});
diff --git a/spec/frontend/members/components/table/members_table_spec.js b/spec/frontend/members/components/table/members_table_spec.js
index 5308d7651a3..3a17d78bd17 100644
--- a/spec/frontend/members/components/table/members_table_spec.js
+++ b/spec/frontend/members/components/table/members_table_spec.js
@@ -15,7 +15,7 @@ import MemberAvatar from '~/members/components/table/member_avatar.vue';
import MemberSource from '~/members/components/table/member_source.vue';
import MembersTable from '~/members/components/table/members_table.vue';
import RoleDropdown from '~/members/components/table/role_dropdown.vue';
-import { MEMBER_TYPES } from '~/members/constants';
+import { MEMBER_TYPES, TAB_QUERY_PARAM_VALUES } from '~/members/constants';
import * as initUserPopovers from '~/user_popovers';
import {
member as memberMock,
@@ -34,7 +34,7 @@ describe('MembersTable', () => {
const createStore = (state = {}) => {
return new Vuex.Store({
modules: {
- [MEMBER_TYPES.user]: {
+ [MEMBER_TYPES.invite]: {
namespaced: true,
state: {
members: [],
@@ -54,11 +54,14 @@ describe('MembersTable', () => {
const createComponent = (state, provide = {}) => {
wrapper = mount(MembersTable, {
localVue,
+ propsData: {
+ tabQueryParamValue: TAB_QUERY_PARAM_VALUES.invite,
+ },
store: createStore(state),
provide: {
sourceId: 1,
currentUserId: 1,
- namespace: MEMBER_TYPES.user,
+ namespace: MEMBER_TYPES.invite,
...provide,
},
stubs: [
@@ -74,7 +77,7 @@ describe('MembersTable', () => {
});
};
- const url = 'https://localhost/foo-bar/-/project_members';
+ const url = 'https://localhost/foo-bar/-/project_members?tab=invited';
const getByText = (text, options) =>
createWrapper(getByTextHelper(wrapper.element, text, options));
@@ -92,7 +95,7 @@ describe('MembersTable', () => {
const expectCorrectLinkToPage2 = () => {
expect(findPagination().findByText('2', { selector: 'a' }).attributes('href')).toBe(
- `${url}?page=2`,
+ `${url}&invited_members_page=2`,
);
};
@@ -271,7 +274,7 @@ describe('MembersTable', () => {
currentPage: 1,
perPage: 5,
totalItems: 10,
- paramName: 'page',
+ paramName: 'invited_members_page',
},
});
@@ -279,14 +282,14 @@ describe('MembersTable', () => {
});
it('removes any url params defined as `null` in the `params` attribute', () => {
- window.location = new URL(`${url}?search_groups=foo`);
+ window.location = new URL(`${url}&search_groups=foo`);
createComponent({
pagination: {
currentPage: 1,
perPage: 5,
totalItems: 10,
- paramName: 'page',
+ paramName: 'invited_members_page',
params: { search_groups: null },
},
});
diff --git a/spec/frontend/milestones/milestone_utils_spec.js b/spec/frontend/milestones/milestone_utils_spec.js
new file mode 100644
index 00000000000..f863f31e5a9
--- /dev/null
+++ b/spec/frontend/milestones/milestone_utils_spec.js
@@ -0,0 +1,47 @@
+import { useFakeDate } from 'helpers/fake_date';
+import { sortMilestonesByDueDate } from '~/milestones/milestone_utils';
+
+describe('sortMilestonesByDueDate', () => {
+ useFakeDate(2021, 6, 22);
+ const mockMilestones = [
+ {
+ id: 2,
+ },
+ {
+ id: 1,
+ dueDate: '2021-01-01',
+ },
+ {
+ id: 4,
+ dueDate: '2021-02-01',
+ expired: true,
+ },
+ {
+ id: 3,
+ dueDate: `2021-08-01`,
+ },
+ ];
+
+ describe('sorts milestones', () => {
+ it('expired milestones are kept at the bottom of the list', () => {
+ const sortedMilestones = [...mockMilestones].sort(sortMilestonesByDueDate);
+
+ expect(sortedMilestones[2].id).toBe(mockMilestones[1].id); // milestone with id `1` is expired
+ expect(sortedMilestones[3].id).toBe(mockMilestones[2].id); // milestone with id `4` is expired
+ });
+
+ it('milestones with closest due date are kept at the top of the list', () => {
+ const sortedMilestones = [...mockMilestones].sort(sortMilestonesByDueDate);
+
+ // milestone with id `3` & 2021-08-01 is closest to current date i.e. 2021-07-22
+ expect(sortedMilestones[0].id).toBe(mockMilestones[3].id);
+ });
+
+ it('milestones with no due date are kept between milestones with closest due date and expired milestones', () => {
+ const sortedMilestones = [...mockMilestones].sort(sortMilestonesByDueDate);
+
+ // milestone with id `2` has no due date
+ expect(sortedMilestones[1].id).toBe(mockMilestones[0].id);
+ });
+ });
+});
diff --git a/spec/frontend/monitoring/components/charts/time_series_spec.js b/spec/frontend/monitoring/components/charts/time_series_spec.js
index 754ddd96c9b..ea6e4f4a5ed 100644
--- a/spec/frontend/monitoring/components/charts/time_series_spec.js
+++ b/spec/frontend/monitoring/components/charts/time_series_spec.js
@@ -51,6 +51,8 @@ describe('Time series component', () => {
},
stubs: {
GlPopover: true,
+ GlLineChart,
+ GlAreaChart,
},
attachTo: document.body,
});
@@ -202,7 +204,7 @@ describe('Time series component', () => {
describe('when series is of line type', () => {
beforeEach(() => {
- createWrapper();
+ createWrapper({}, mount);
wrapper.vm.formatTooltipText(mockLineSeriesData());
return wrapper.vm.$nextTick();
});
diff --git a/spec/frontend/monitoring/components/dashboard_actions_menu_spec.js b/spec/frontend/monitoring/components/dashboard_actions_menu_spec.js
index 6e98ca28071..dbb9fd5f603 100644
--- a/spec/frontend/monitoring/components/dashboard_actions_menu_spec.js
+++ b/spec/frontend/monitoring/components/dashboard_actions_menu_spec.js
@@ -1,4 +1,4 @@
-import { GlDropdownItem } from '@gitlab/ui';
+import { GlDropdownItem, GlModal } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import CustomMetricsFormFields from '~/custom_metrics/components/custom_metrics_form_fields.vue';
import { redirectTo } from '~/lib/utils/url_utility';
@@ -43,6 +43,9 @@ describe('Actions menu', () => {
wrapper = shallowMount(ActionsMenu, {
propsData: { ...dashboardActionsMenuProps, ...props },
store,
+ stubs: {
+ GlModal,
+ },
...options,
});
};
@@ -82,7 +85,7 @@ describe('Actions menu', () => {
it('modal for custom metrics form is rendered', () => {
expect(findAddMetricModal().exists()).toBe(true);
- expect(findAddMetricModal().attributes().modalid).toBe('addMetric');
+ expect(findAddMetricModal().props('modalId')).toBe('addMetric');
});
it('add metric modal submit button exists', () => {
diff --git a/spec/frontend/nav/components/top_nav_menu_item_spec.js b/spec/frontend/nav/components/top_nav_menu_item_spec.js
index fd2b4d3b056..71154e18915 100644
--- a/spec/frontend/nav/components/top_nav_menu_item_spec.js
+++ b/spec/frontend/nav/components/top_nav_menu_item_spec.js
@@ -73,7 +73,7 @@ describe('~/nav/components/top_nav_menu_item.vue', () => {
expect(findButtonIcons()).toEqual([
{
name: TEST_MENU_ITEM.icon,
- classes: ['gl-mr-2!'],
+ classes: ['gl-mr-3!'],
},
{
name: 'chevron-right',
diff --git a/spec/frontend/notes/components/comment_form_spec.js b/spec/frontend/notes/components/comment_form_spec.js
index 537622b7918..bb79b43205b 100644
--- a/spec/frontend/notes/components/comment_form_spec.js
+++ b/spec/frontend/notes/components/comment_form_spec.js
@@ -7,7 +7,7 @@ import Vuex from 'vuex';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import batchComments from '~/batch_comments/stores/modules/batch_comments';
import { refreshUserMergeRequestCounts } from '~/commons/nav/user_merge_requests';
-import { deprecatedCreateFlash as flash } from '~/flash';
+import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import CommentForm from '~/notes/components/comment_form.vue';
import * as constants from '~/notes/constants';
@@ -464,9 +464,9 @@ describe('issue_comment_form component', () => {
await wrapper.vm.$nextTick;
await wrapper.vm.$nextTick;
- expect(flash).toHaveBeenCalledWith(
- `Something went wrong while closing the ${type}. Please try again later.`,
- );
+ expect(createFlash).toHaveBeenCalledWith({
+ message: `Something went wrong while closing the ${type}. Please try again later.`,
+ });
});
});
@@ -500,9 +500,9 @@ describe('issue_comment_form component', () => {
await wrapper.vm.$nextTick;
await wrapper.vm.$nextTick;
- expect(flash).toHaveBeenCalledWith(
- `Something went wrong while reopening the ${type}. Please try again later.`,
- );
+ expect(createFlash).toHaveBeenCalledWith({
+ message: `Something went wrong while reopening the ${type}. Please try again later.`,
+ });
});
});
diff --git a/spec/frontend/notes/components/discussion_notes_spec.js b/spec/frontend/notes/components/discussion_notes_spec.js
index cd24b9afbdf..59ac75f00e6 100644
--- a/spec/frontend/notes/components/discussion_notes_spec.js
+++ b/spec/frontend/notes/components/discussion_notes_spec.js
@@ -1,5 +1,5 @@
import { getByRole } from '@testing-library/dom';
-import { shallowMount } from '@vue/test-utils';
+import { shallowMount, mount } from '@vue/test-utils';
import '~/behaviors/markdown/render_gfm';
import DiscussionNotes from '~/notes/components/discussion_notes.vue';
import NoteableNote from '~/notes/components/noteable_note.vue';
@@ -23,8 +23,8 @@ describe('DiscussionNotes', () => {
let wrapper;
const getList = () => getByRole(wrapper.element, 'list');
- const createComponent = (props) => {
- wrapper = shallowMount(DiscussionNotes, {
+ const createComponent = (props, mountingMethod = shallowMount) => {
+ wrapper = mountingMethod(DiscussionNotes, {
store,
propsData: {
discussion: discussionMock,
@@ -33,7 +33,11 @@ describe('DiscussionNotes', () => {
...props,
},
scopedSlots: {
- footer: '<p slot-scope="{ showReplies }">showReplies:{{showReplies}}</p>',
+ footer: `
+ <template #default="{ showReplies }">
+ <p>showReplies:{{ showReplies }}</p>,
+ </template>
+ `,
},
slots: {
'avatar-badge': '<span class="avatar-badge-slot-content" />',
@@ -112,7 +116,7 @@ describe('DiscussionNotes', () => {
});
it('passes down avatar-badge slot content', () => {
- createComponent();
+ createComponent({}, mount);
expect(wrapper.find('.avatar-badge-slot-content').exists()).toBe(true);
});
});
diff --git a/spec/frontend/notes/components/noteable_note_spec.js b/spec/frontend/notes/components/noteable_note_spec.js
index 7444c441e06..f217dfd2e48 100644
--- a/spec/frontend/notes/components/noteable_note_spec.js
+++ b/spec/frontend/notes/components/noteable_note_spec.js
@@ -1,5 +1,4 @@
import { mount } from '@vue/test-utils';
-import { escape } from 'lodash';
import Vue from 'vue';
import Vuex from 'vuex';
@@ -263,7 +262,9 @@ describe('issue_note', () => {
await waitForPromises();
expect(alertSpy).not.toHaveBeenCalled();
- expect(wrapper.vm.note.note_html).toBe(escape(noteBody));
+ expect(wrapper.vm.note.note_html).toBe(
+ '<p><img src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"></p>\n',
+ );
});
});
@@ -291,7 +292,7 @@ describe('issue_note', () => {
await wrapper.vm.$nextTick();
let noteBodyProps = noteBody.props();
- expect(noteBodyProps.note.note_html).toBe(updatedText);
+ expect(noteBodyProps.note.note_html).toBe(`<p>${updatedText}</p>\n`);
noteBody.vm.$emit('cancelForm');
await wrapper.vm.$nextTick();
diff --git a/spec/frontend/notes/stores/actions_spec.js b/spec/frontend/notes/stores/actions_spec.js
index 7eef2017dfb..2ff65d3f47e 100644
--- a/spec/frontend/notes/stores/actions_spec.js
+++ b/spec/frontend/notes/stores/actions_spec.js
@@ -2,7 +2,7 @@ import AxiosMockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
import { TEST_HOST } from 'spec/test_constants';
import Api from '~/api';
-import { deprecatedCreateFlash as Flash } from '~/flash';
+import createFlash from '~/flash';
import { EVENT_ISSUABLE_VUE_APP_CHANGE } from '~/issuable/constants';
import axios from '~/lib/utils/axios_utils';
import * as notesConstants from '~/notes/constants';
@@ -33,10 +33,7 @@ jest.mock('~/flash', () => {
};
});
- return {
- createFlash: flash,
- deprecatedCreateFlash: flash,
- };
+ return flash;
});
describe('Actions Notes Store', () => {
@@ -348,13 +345,13 @@ describe('Actions Notes Store', () => {
await startPolling();
expect(axiosMock.history.get).toHaveLength(1);
- expect(Flash).not.toHaveBeenCalled();
+ expect(createFlash).not.toHaveBeenCalled();
await advanceXMoreIntervals(1);
expect(axiosMock.history.get).toHaveLength(2);
- expect(Flash).toHaveBeenCalled();
- expect(Flash).toHaveBeenCalledTimes(1);
+ expect(createFlash).toHaveBeenCalled();
+ expect(createFlash).toHaveBeenCalledTimes(1);
});
it('resets the failure counter on success', async () => {
@@ -375,14 +372,14 @@ describe('Actions Notes Store', () => {
await advanceXMoreIntervals(1); // Failure #2
// That was the first failure AFTER a success, so we should NOT see the error displayed
- expect(Flash).not.toHaveBeenCalled();
+ expect(createFlash).not.toHaveBeenCalled();
// Now we'll allow another failure
await advanceXMoreIntervals(1); // Failure #3
// Since this is the second failure in a row, the error should happen
- expect(Flash).toHaveBeenCalled();
- expect(Flash).toHaveBeenCalledTimes(1);
+ expect(createFlash).toHaveBeenCalled();
+ expect(createFlash).toHaveBeenCalledTimes(1);
});
it('hides the error display if it exists on success', async () => {
@@ -393,8 +390,8 @@ describe('Actions Notes Store', () => {
await advanceXMoreIntervals(2);
// After two errors, the error should be displayed
- expect(Flash).toHaveBeenCalled();
- expect(Flash).toHaveBeenCalledTimes(1);
+ expect(createFlash).toHaveBeenCalled();
+ expect(createFlash).toHaveBeenCalledTimes(1);
axiosMock.reset();
successMock();
@@ -906,7 +903,7 @@ describe('Actions Notes Store', () => {
.then(() => done.fail('Expected error to be thrown!'))
.catch((err) => {
expect(err).toBe(error);
- expect(Flash).not.toHaveBeenCalled();
+ expect(createFlash).not.toHaveBeenCalled();
})
.then(done)
.catch(done.fail);
@@ -928,11 +925,10 @@ describe('Actions Notes Store', () => {
)
.then((resp) => {
expect(resp.hasFlash).toBe(true);
- expect(Flash).toHaveBeenCalledWith(
- 'Your comment could not be submitted because something went wrong',
- 'alert',
- flashContainer,
- );
+ expect(createFlash).toHaveBeenCalledWith({
+ message: 'Your comment could not be submitted because something went wrong',
+ parent: flashContainer,
+ });
})
.catch(() => done.fail('Expected success response!'))
.then(done)
@@ -954,7 +950,7 @@ describe('Actions Notes Store', () => {
)
.then((data) => {
expect(data).toBe(res);
- expect(Flash).not.toHaveBeenCalled();
+ expect(createFlash).not.toHaveBeenCalled();
})
.then(done)
.catch(done.fail);
@@ -997,7 +993,7 @@ describe('Actions Notes Store', () => {
['resolveDiscussion', { discussionId }],
['restartPolling'],
]);
- expect(Flash).not.toHaveBeenCalled();
+ expect(createFlash).not.toHaveBeenCalled();
});
});
@@ -1012,7 +1008,10 @@ describe('Actions Notes Store', () => {
[mutationTypes.SET_RESOLVING_DISCUSSION, false],
]);
expect(dispatch.mock.calls).toEqual([['stopPolling'], ['restartPolling']]);
- expect(Flash).toHaveBeenCalledWith(TEST_ERROR_MESSAGE, 'alert', flashContainer);
+ expect(createFlash).toHaveBeenCalledWith({
+ message: TEST_ERROR_MESSAGE,
+ parent: flashContainer,
+ });
});
});
@@ -1027,11 +1026,10 @@ describe('Actions Notes Store', () => {
[mutationTypes.SET_RESOLVING_DISCUSSION, false],
]);
expect(dispatch.mock.calls).toEqual([['stopPolling'], ['restartPolling']]);
- expect(Flash).toHaveBeenCalledWith(
- 'Something went wrong while applying the suggestion. Please try again.',
- 'alert',
- flashContainer,
- );
+ expect(createFlash).toHaveBeenCalledWith({
+ message: 'Something went wrong while applying the suggestion. Please try again.',
+ parent: flashContainer,
+ });
});
});
@@ -1039,7 +1037,7 @@ describe('Actions Notes Store', () => {
dispatch.mockReturnValue(Promise.reject());
testSubmitSuggestion(done, () => {
- expect(Flash).not.toHaveBeenCalled();
+ expect(createFlash).not.toHaveBeenCalled();
});
});
});
@@ -1083,7 +1081,7 @@ describe('Actions Notes Store', () => {
['restartPolling'],
]);
- expect(Flash).not.toHaveBeenCalled();
+ expect(createFlash).not.toHaveBeenCalled();
});
});
@@ -1101,7 +1099,10 @@ describe('Actions Notes Store', () => {
]);
expect(dispatch.mock.calls).toEqual([['stopPolling'], ['restartPolling']]);
- expect(Flash).toHaveBeenCalledWith(TEST_ERROR_MESSAGE, 'alert', flashContainer);
+ expect(createFlash).toHaveBeenCalledWith({
+ message: TEST_ERROR_MESSAGE,
+ parent: flashContainer,
+ });
});
});
@@ -1119,11 +1120,11 @@ describe('Actions Notes Store', () => {
]);
expect(dispatch.mock.calls).toEqual([['stopPolling'], ['restartPolling']]);
- expect(Flash).toHaveBeenCalledWith(
- 'Something went wrong while applying the batch of suggestions. Please try again.',
- 'alert',
- flashContainer,
- );
+ expect(createFlash).toHaveBeenCalledWith({
+ message:
+ 'Something went wrong while applying the batch of suggestions. Please try again.',
+ parent: flashContainer,
+ });
});
});
@@ -1139,7 +1140,7 @@ describe('Actions Notes Store', () => {
[mutationTypes.SET_RESOLVING_DISCUSSION, false],
]);
- expect(Flash).not.toHaveBeenCalled();
+ expect(createFlash).not.toHaveBeenCalled();
});
});
});
@@ -1283,7 +1284,7 @@ describe('Actions Notes Store', () => {
)
.then(() => done.fail('Expected error to be thrown'))
.catch(() => {
- expect(Flash).toHaveBeenCalled();
+ expect(createFlash).toHaveBeenCalled();
done();
});
});
diff --git a/spec/frontend/notifications/components/custom_notifications_modal_spec.js b/spec/frontend/notifications/components/custom_notifications_modal_spec.js
index 5e4114d91f5..0782ec7cdd5 100644
--- a/spec/frontend/notifications/components/custom_notifications_modal_spec.js
+++ b/spec/frontend/notifications/components/custom_notifications_modal_spec.js
@@ -177,11 +177,8 @@ describe('CustomNotificationsModal', () => {
await waitForPromises();
- expect(
- mockToastShow,
- ).toHaveBeenCalledWith(
+ expect(mockToastShow).toHaveBeenCalledWith(
'An error occurred while loading the notification settings. Please try again.',
- { type: 'error' },
);
});
});
@@ -255,11 +252,8 @@ describe('CustomNotificationsModal', () => {
await waitForPromises();
- expect(
- mockToastShow,
- ).toHaveBeenCalledWith(
+ expect(mockToastShow).toHaveBeenCalledWith(
'An error occurred while updating the notification settings. Please try again.',
- { type: 'error' },
);
});
});
diff --git a/spec/frontend/notifications/components/notifications_dropdown_spec.js b/spec/frontend/notifications/components/notifications_dropdown_spec.js
index e90bd68d067..e12251ce6d9 100644
--- a/spec/frontend/notifications/components/notifications_dropdown_spec.js
+++ b/spec/frontend/notifications/components/notifications_dropdown_spec.js
@@ -242,11 +242,8 @@ describe('NotificationsDropdown', () => {
await clickDropdownItemAt(1);
expect(wrapper.vm.selectedNotificationLevel).toBe('global');
- expect(
- mockToastShow,
- ).toHaveBeenCalledWith(
+ expect(mockToastShow).toHaveBeenCalledWith(
'An error occurred while updating the notification settings. Please try again.',
- { type: 'error' },
);
});
diff --git a/spec/frontend/operation_settings/components/metrics_settings_spec.js b/spec/frontend/operation_settings/components/metrics_settings_spec.js
index 5eecfd395e2..258c6eae692 100644
--- a/spec/frontend/operation_settings/components/metrics_settings_spec.js
+++ b/spec/frontend/operation_settings/components/metrics_settings_spec.js
@@ -205,7 +205,6 @@ describe('operation settings external dashboard component', () => {
.then(() =>
expect(createFlash).toHaveBeenCalledWith({
message: `There was an error saving your changes. ${message}`,
- type: 'alert',
}),
);
});
diff --git a/spec/frontend/packages/list/components/__snapshots__/packages_list_app_spec.js.snap b/spec/frontend/packages/list/components/__snapshots__/packages_list_app_spec.js.snap
index 07aba62fef6..dbebdeeb452 100644
--- a/spec/frontend/packages/list/components/__snapshots__/packages_list_app_spec.js.snap
+++ b/spec/frontend/packages/list/components/__snapshots__/packages_list_app_spec.js.snap
@@ -21,6 +21,7 @@ exports[`packages_list_app renders 1`] = `
<img
alt=""
class="gl-max-w-full"
+ role="img"
src="helpSvg"
/>
</div>
diff --git a/spec/frontend/packages/shared/utils_spec.js b/spec/frontend/packages/shared/utils_spec.js
index 463e4a4febb..a1076b729f8 100644
--- a/spec/frontend/packages/shared/utils_spec.js
+++ b/spec/frontend/packages/shared/utils_spec.js
@@ -40,6 +40,8 @@ describe('Packages shared utils', () => {
${'pypi'} | ${'PyPI'}
${'rubygems'} | ${'RubyGems'}
${'composer'} | ${'Composer'}
+ ${'debian'} | ${'Debian'}
+ ${'helm'} | ${'Helm'}
${'foo'} | ${null}
`(`package type`, ({ packageType, expectedResult }) => {
it(`${packageType} should show as ${expectedResult}`, () => {
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/app_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/app_spec.js
new file mode 100644
index 00000000000..97444ec108f
--- /dev/null
+++ b/spec/frontend/packages_and_registries/package_registry/components/details/app_spec.js
@@ -0,0 +1,35 @@
+import { GlEmptyState } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+
+import PackagesApp from '~/packages_and_registries/package_registry/components/details/app.vue';
+
+describe('PackagesApp', () => {
+ let wrapper;
+
+ function createComponent() {
+ wrapper = shallowMount(PackagesApp, {
+ provide: {
+ titleComponent: 'titleComponent',
+ projectName: 'projectName',
+ canDelete: 'canDelete',
+ svgPath: 'svgPath',
+ npmPath: 'npmPath',
+ npmHelpPath: 'npmHelpPath',
+ projectListUrl: 'projectListUrl',
+ groupListUrl: 'groupListUrl',
+ },
+ });
+ }
+
+ const emptyState = () => wrapper.findComponent(GlEmptyState);
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('renders an empty state component', () => {
+ createComponent();
+
+ expect(emptyState().exists()).toBe(true);
+ });
+});
diff --git a/spec/frontend/packages_and_registries/settings/group/components/group_settings_app_spec.js b/spec/frontend/packages_and_registries/settings/group/components/group_settings_app_spec.js
index 14ee3f3e3b8..f2877a1f2a5 100644
--- a/spec/frontend/packages_and_registries/settings/group/components/group_settings_app_spec.js
+++ b/spec/frontend/packages_and_registries/settings/group/components/group_settings_app_spec.js
@@ -137,7 +137,7 @@ describe('Group Settings App', () => {
href: PACKAGES_DOCS_PATH,
target: '_blank',
});
- expect(findLink().text()).toBe('More Information');
+ expect(findLink().text()).toBe('Learn more.');
});
it('calls the graphql API with the proper variables', () => {
@@ -244,9 +244,7 @@ describe('Group Settings App', () => {
await waitForPromises();
- expect(show).toHaveBeenCalledWith(SUCCESS_UPDATING_SETTINGS, {
- type: 'success',
- });
+ expect(show).toHaveBeenCalledWith(SUCCESS_UPDATING_SETTINGS);
});
it('has an optimistic response', async () => {
diff --git a/spec/frontend/packages_and_registries/settings/project/settings/__snapshots__/utils_spec.js.snap b/spec/frontend/packages_and_registries/settings/project/settings/__snapshots__/utils_spec.js.snap
index 7062773b46b..cf554717127 100644
--- a/spec/frontend/packages_and_registries/settings/project/settings/__snapshots__/utils_spec.js.snap
+++ b/spec/frontend/packages_and_registries/settings/project/settings/__snapshots__/utils_spec.js.snap
@@ -33,6 +33,10 @@ Array [
exports[`Utils formOptionsGenerator returns an object containing keepN 1`] = `
Array [
Object {
+ "key": null,
+ "label": "",
+ },
+ Object {
"default": false,
"key": "ONE_TAG",
"label": "1 tag per image name",
@@ -74,6 +78,10 @@ Array [
exports[`Utils formOptionsGenerator returns an object containing olderThan 1`] = `
Array [
Object {
+ "key": null,
+ "label": "",
+ },
+ Object {
"default": false,
"key": "SEVEN_DAYS",
"label": "7 days",
diff --git a/spec/frontend/packages_and_registries/settings/project/settings/components/__snapshots__/settings_form_spec.js.snap b/spec/frontend/packages_and_registries/settings/project/settings/components/__snapshots__/settings_form_spec.js.snap
index 7a52b4a5d0f..1009db46401 100644
--- a/spec/frontend/packages_and_registries/settings/project/settings/components/__snapshots__/settings_form_spec.js.snap
+++ b/spec/frontend/packages_and_registries/settings/project/settings/components/__snapshots__/settings_form_spec.js.snap
@@ -22,7 +22,7 @@ exports[`Settings Form Enable matches snapshot 1`] = `
exports[`Settings Form Keep N matches snapshot 1`] = `
<expiration-dropdown-stub
data-testid="keep-n-dropdown"
- formoptions="[object Object],[object Object],[object Object],[object Object],[object Object],[object Object]"
+ formoptions="[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object]"
label="Keep the most recent:"
name="keep-n"
value="TEN_TAGS"
@@ -44,7 +44,7 @@ exports[`Settings Form Keep Regex matches snapshot 1`] = `
exports[`Settings Form OlderThan matches snapshot 1`] = `
<expiration-dropdown-stub
data-testid="older-than-dropdown"
- formoptions="[object Object],[object Object],[object Object],[object Object]"
+ formoptions="[object Object],[object Object],[object Object],[object Object],[object Object]"
label="Remove tags older than:"
name="older-than"
value="FOURTEEN_DAYS"
diff --git a/spec/frontend/packages_and_registries/settings/project/settings/components/settings_form_spec.js b/spec/frontend/packages_and_registries/settings/project/settings/components/settings_form_spec.js
index 7e5383d7ff1..3a71af94d5a 100644
--- a/spec/frontend/packages_and_registries/settings/project/settings/components/settings_form_spec.js
+++ b/spec/frontend/packages_and_registries/settings/project/settings/components/settings_form_spec.js
@@ -132,9 +132,9 @@ describe('Settings Form', () => {
model | finder | fieldName | type | defaultValue
${'enabled'} | ${findEnableToggle} | ${'Enable'} | ${'toggle'} | ${false}
${'cadence'} | ${findCadenceDropdown} | ${'Cadence'} | ${'dropdown'} | ${'EVERY_DAY'}
- ${'keepN'} | ${findKeepNDropdown} | ${'Keep N'} | ${'dropdown'} | ${'TEN_TAGS'}
+ ${'keepN'} | ${findKeepNDropdown} | ${'Keep N'} | ${'dropdown'} | ${''}
${'nameRegexKeep'} | ${findKeepRegexInput} | ${'Keep Regex'} | ${'textarea'} | ${''}
- ${'olderThan'} | ${findOlderThanDropdown} | ${'OlderThan'} | ${'dropdown'} | ${'NINETY_DAYS'}
+ ${'olderThan'} | ${findOlderThanDropdown} | ${'OlderThan'} | ${'dropdown'} | ${''}
${'nameRegex'} | ${findRemoveRegexInput} | ${'Remove regex'} | ${'textarea'} | ${''}
`('$fieldName', ({ model, finder, type, defaultValue }) => {
it('matches snapshot', () => {
@@ -293,10 +293,10 @@ describe('Settings Form', () => {
input: {
cadence: 'EVERY_DAY',
enabled: true,
- keepN: 'TEN_TAGS',
+ keepN: null,
nameRegex: 'asdasdssssdfdf',
nameRegexKeep: 'sss',
- olderThan: 'NINETY_DAYS',
+ olderThan: null,
projectPath: 'path',
},
});
@@ -321,9 +321,7 @@ describe('Settings Form', () => {
await waitForPromises();
await wrapper.vm.$nextTick();
- expect(wrapper.vm.$toast.show).toHaveBeenCalledWith(UPDATE_SETTINGS_SUCCESS_MESSAGE, {
- type: 'success',
- });
+ expect(wrapper.vm.$toast.show).toHaveBeenCalledWith(UPDATE_SETTINGS_SUCCESS_MESSAGE);
});
describe('when submit fails', () => {
@@ -339,9 +337,7 @@ describe('Settings Form', () => {
await waitForPromises();
await wrapper.vm.$nextTick();
- expect(wrapper.vm.$toast.show).toHaveBeenCalledWith('foo', {
- type: 'error',
- });
+ expect(wrapper.vm.$toast.show).toHaveBeenCalledWith('foo');
});
});
@@ -355,9 +351,7 @@ describe('Settings Form', () => {
await waitForPromises();
await wrapper.vm.$nextTick();
- expect(wrapper.vm.$toast.show).toHaveBeenCalledWith(UPDATE_SETTINGS_ERROR_MESSAGE, {
- type: 'error',
- });
+ expect(wrapper.vm.$toast.show).toHaveBeenCalledWith(UPDATE_SETTINGS_ERROR_MESSAGE);
});
it('parses the error messages', async () => {
diff --git a/spec/frontend/packages_and_registries/settings/project/settings/utils_spec.js b/spec/frontend/packages_and_registries/settings/project/settings/utils_spec.js
index 4c81671cd46..ed126d87ae3 100644
--- a/spec/frontend/packages_and_registries/settings/project/settings/utils_spec.js
+++ b/spec/frontend/packages_and_registries/settings/project/settings/utils_spec.js
@@ -12,6 +12,7 @@ describe('Utils', () => {
olderThanTranslationGenerator,
);
expect(result).toEqual([
+ { key: null, label: '' },
{ variable: 1, label: '1 day' },
{ variable: 2, label: '2 days' },
]);
diff --git a/spec/frontend/pager_spec.js b/spec/frontend/pager_spec.js
index 95679a51c6d..ff352303143 100644
--- a/spec/frontend/pager_spec.js
+++ b/spec/frontend/pager_spec.js
@@ -6,6 +6,7 @@ import { removeParams } from '~/lib/utils/url_utility';
import Pager from '~/pager';
jest.mock('~/lib/utils/url_utility', () => ({
+ ...jest.requireActual('~/lib/utils/url_utility'),
removeParams: jest.fn().mockName('removeParams'),
}));
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
new file mode 100644
index 00000000000..858c7b76ac8
--- /dev/null
+++ b/spec/frontend/pages/admin/application_settings/metrics_and_profiling/usage_statistics_spec.js
@@ -0,0 +1,57 @@
+import initSetHelperText, {
+ HELPER_TEXT_SERVICE_PING_DISABLED,
+ HELPER_TEXT_SERVICE_PING_ENABLED,
+} from '~/pages/admin/application_settings/metrics_and_profiling/usage_statistics';
+
+describe('UsageStatistics', () => {
+ const FIXTURE = 'application_settings/usage.html';
+ let usagePingCheckBox;
+ let usagePingFeaturesCheckBox;
+ let usagePingFeaturesLabel;
+ let usagePingFeaturesHelperText;
+
+ beforeEach(() => {
+ loadFixtures(FIXTURE);
+ initSetHelperText();
+ usagePingCheckBox = document.getElementById('application_setting_usage_ping_enabled');
+ usagePingFeaturesCheckBox = document.getElementById(
+ 'application_setting_usage_ping_features_enabled',
+ );
+ usagePingFeaturesLabel = document.getElementById('service_ping_features_label');
+ usagePingFeaturesHelperText = document.getElementById('service_ping_features_helper_text');
+ });
+
+ const expectEnabledUsagePingFeaturesCheckBox = () => {
+ expect(usagePingFeaturesCheckBox.classList.contains('gl-cursor-not-allowed')).toBe(false);
+ expect(usagePingFeaturesHelperText.textContent).toEqual(HELPER_TEXT_SERVICE_PING_ENABLED);
+ };
+
+ const expectDisabledUsagePingFeaturesCheckBox = () => {
+ expect(usagePingFeaturesLabel.classList.contains('gl-cursor-not-allowed')).toBe(true);
+ expect(usagePingFeaturesHelperText.textContent).toEqual(HELPER_TEXT_SERVICE_PING_DISABLED);
+ };
+
+ describe('Registration Features checkbox', () => {
+ it('is disabled when Usage Ping checkbox is unchecked', () => {
+ expect(usagePingCheckBox.checked).toBe(false);
+ expectDisabledUsagePingFeaturesCheckBox();
+ });
+
+ it('is enabled when Usage Ping checkbox is checked', () => {
+ usagePingCheckBox.click();
+ expect(usagePingCheckBox.checked).toBe(true);
+ expectEnabledUsagePingFeaturesCheckBox();
+ });
+
+ it('is switched to disabled when Usage Ping checkbox is unchecked ', () => {
+ usagePingCheckBox.click();
+ usagePingFeaturesCheckBox.click();
+ expectEnabledUsagePingFeaturesCheckBox();
+
+ usagePingCheckBox.click();
+ expect(usagePingCheckBox.checked).toBe(false);
+ expect(usagePingFeaturesCheckBox.checked).toBe(false);
+ expectDisabledUsagePingFeaturesCheckBox();
+ });
+ });
+});
diff --git a/spec/frontend/pages/projects/forks/new/components/fork_form_spec.js b/spec/frontend/pages/projects/forks/new/components/fork_form_spec.js
index c80ccfa8256..dd617b1ffc2 100644
--- a/spec/frontend/pages/projects/forks/new/components/fork_form_spec.js
+++ b/spec/frontend/pages/projects/forks/new/components/fork_form_spec.js
@@ -29,10 +29,12 @@ describe('ForkForm component', () => {
const MOCK_NAMESPACES_RESPONSE = [
{
name: 'one',
+ full_name: 'one-group/one',
id: 1,
},
{
name: 'two',
+ full_name: 'two-group/two',
id: 2,
},
];
@@ -155,7 +157,7 @@ describe('ForkForm component', () => {
describe('forks namespaces', () => {
beforeEach(() => {
mockGetRequest({ namespaces: MOCK_NAMESPACES_RESPONSE });
- createComponent();
+ createFullComponent();
});
it('make GET request from endpoint', async () => {
@@ -178,8 +180,23 @@ describe('ForkForm component', () => {
const optionsArray = findForkUrlInput().findAll('option');
expect(optionsArray).toHaveLength(MOCK_NAMESPACES_RESPONSE.length + 1);
- expect(optionsArray.at(1).text()).toBe(MOCK_NAMESPACES_RESPONSE[0].name);
- expect(optionsArray.at(2).text()).toBe(MOCK_NAMESPACES_RESPONSE[1].name);
+ expect(optionsArray.at(1).text()).toBe(MOCK_NAMESPACES_RESPONSE[0].full_name);
+ expect(optionsArray.at(2).text()).toBe(MOCK_NAMESPACES_RESPONSE[1].full_name);
+ });
+
+ it('set namespaces in alphabetical order', async () => {
+ const namespace = {
+ name: 'three',
+ full_name: 'aaa/three',
+ id: 3,
+ };
+ mockGetRequest({
+ namespaces: [...MOCK_NAMESPACES_RESPONSE, namespace],
+ });
+ createComponent();
+ await axios.waitForAll();
+
+ expect(wrapper.vm.namespaces).toEqual([namespace, ...MOCK_NAMESPACES_RESPONSE]);
});
});
diff --git a/spec/frontend/pages/projects/forks/new/components/fork_groups_list_item_spec.js b/spec/frontend/pages/projects/forks/new/components/fork_groups_list_item_spec.js
index b5425fa6f2e..490dafed4ae 100644
--- a/spec/frontend/pages/projects/forks/new/components/fork_groups_list_item_spec.js
+++ b/spec/frontend/pages/projects/forks/new/components/fork_groups_list_item_spec.js
@@ -34,10 +34,10 @@ describe('Fork groups list item component', () => {
});
};
- it('renders pending removal badge if applicable', () => {
+ it('renders pending deletion badge if applicable', () => {
createWrapper({ group: { ...DEFAULT_GROUP_DATA, marked_for_deletion: true } });
- expect(wrapper.find(GlBadge).text()).toBe('pending removal');
+ expect(wrapper.find(GlBadge).text()).toBe('pending deletion');
});
it('renders go to fork button if has forked project', () => {
diff --git a/spec/frontend/pages/projects/new/components/app_spec.js b/spec/frontend/pages/projects/new/components/app_spec.js
index b604e636243..ab8c6d529a8 100644
--- a/spec/frontend/pages/projects/new/components/app_spec.js
+++ b/spec/frontend/pages/projects/new/components/app_spec.js
@@ -1,13 +1,10 @@
import { shallowMount } from '@vue/test-utils';
-import { assignGitlabExperiment } from 'helpers/experimentation_helper';
import App from '~/pages/projects/new/components/app.vue';
import NewNamespacePage from '~/vue_shared/new_namespace/new_namespace_page.vue';
describe('Experimental new project creation app', () => {
let wrapper;
- const findNewNamespacePage = () => wrapper.findComponent(NewNamespacePage);
-
const createComponent = (propsData) => {
wrapper = shallowMount(App, { propsData });
};
@@ -16,36 +13,6 @@ describe('Experimental new project creation app', () => {
wrapper.destroy();
});
- describe('new_repo experiment', () => {
- it('passes new_repo experiment', () => {
- createComponent();
-
- expect(findNewNamespacePage().props().experiment).toBe('new_repo');
- });
-
- describe('when in the candidate variant', () => {
- assignGitlabExperiment('new_repo', 'candidate');
-
- it('has "repository" in the panel title', () => {
- createComponent();
-
- expect(findNewNamespacePage().props().panels[0].title).toBe(
- 'Create blank project/repository',
- );
- });
- });
-
- describe('when in the control variant', () => {
- assignGitlabExperiment('new_repo', 'control');
-
- it('has "project" in the panel title', () => {
- createComponent();
-
- expect(findNewNamespacePage().props().panels[0].title).toBe('Create blank project');
- });
- });
- });
-
it('passes custom new project guideline text to underlying component', () => {
const DEMO_GUIDELINES = 'Demo guidelines';
const guidelineSelector = '#new-project-guideline';
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 878721666ff..4c253f0610b 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
@@ -94,6 +94,8 @@ describe('Settings Panel', () => {
const findPackageSettings = () => wrapper.find({ ref: 'package-settings' });
const findPackagesEnabledInput = () => wrapper.find('[name="project[packages_enabled]"]');
const findPagesSettings = () => wrapper.find({ ref: 'pages-settings' });
+ const findPagesAccessLevels = () =>
+ wrapper.find('[name="project[project_feature_attributes][pages_access_level]"]');
const findEmailSettings = () => wrapper.find({ ref: 'email-settings' });
const findShowDefaultAwardEmojis = () =>
wrapper.find('input[name="project[project_setting_attributes][show_default_award_emojis]"]');
@@ -479,6 +481,29 @@ describe('Settings Panel', () => {
describe('Pages', () => {
it.each`
+ visibilityLevel | pagesAccessControlForced | output
+ ${visibilityOptions.PRIVATE} | ${true} | ${[[visibilityOptions.INTERNAL, 'Only Project Members'], [visibilityOptions.PUBLIC, 'Everyone With Access']]}
+ ${visibilityOptions.PRIVATE} | ${false} | ${[[visibilityOptions.INTERNAL, 'Only Project Members'], [visibilityOptions.PUBLIC, 'Everyone With Access'], [30, 'Everyone']]}
+ ${visibilityOptions.INTERNAL} | ${true} | ${[[visibilityOptions.INTERNAL, 'Only Project Members'], [visibilityOptions.PUBLIC, 'Everyone With Access']]}
+ ${visibilityOptions.INTERNAL} | ${false} | ${[[visibilityOptions.INTERNAL, 'Only Project Members'], [visibilityOptions.PUBLIC, 'Everyone With Access'], [30, 'Everyone']]}
+ ${visibilityOptions.PUBLIC} | ${true} | ${[[visibilityOptions.INTERNAL, 'Only Project Members'], [visibilityOptions.PUBLIC, 'Everyone With Access']]}
+ ${visibilityOptions.PUBLIC} | ${false} | ${[[visibilityOptions.INTERNAL, 'Only Project Members'], [visibilityOptions.PUBLIC, 'Everyone With Access'], [30, 'Everyone']]}
+ `(
+ 'renders correct options when pagesAccessControlForced is $pagesAccessControlForced and visibilityLevel is $visibilityLevel',
+ async ({ visibilityLevel, pagesAccessControlForced, output }) => {
+ wrapper = mountComponent({
+ pagesAvailable: true,
+ pagesAccessControlEnabled: true,
+ pagesAccessControlForced,
+ });
+
+ await findProjectVisibilityLevelInput().trigger('change', visibilityLevel);
+
+ expect(findPagesAccessLevels().props('options')).toStrictEqual(output);
+ },
+ );
+
+ it.each`
pagesAvailable | pagesAccessControlEnabled | visibility
${true} | ${true} | ${'show'}
${true} | ${false} | ${'hide'}
diff --git a/spec/frontend/persistent_user_callout_spec.js b/spec/frontend/persistent_user_callout_spec.js
index 403142d7ff7..1e51ddf909a 100644
--- a/spec/frontend/persistent_user_callout_spec.js
+++ b/spec/frontend/persistent_user_callout_spec.js
@@ -1,6 +1,6 @@
import MockAdapter from 'axios-mock-adapter';
import waitForPromises from 'helpers/wait_for_promises';
-import { deprecatedCreateFlash as Flash } from '~/flash';
+import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import PersistentUserCallout from '~/persistent_user_callout';
@@ -96,9 +96,9 @@ describe('PersistentUserCallout', () => {
return waitForPromises().then(() => {
expect(persistentUserCallout.container.remove).not.toHaveBeenCalled();
- expect(Flash).toHaveBeenCalledWith(
- 'An error occurred while dismissing the alert. Refresh the page and try again.',
- );
+ expect(createFlash).toHaveBeenCalledWith({
+ message: 'An error occurred while dismissing the alert. Refresh the page and try again.',
+ });
});
});
});
@@ -203,9 +203,10 @@ describe('PersistentUserCallout', () => {
return waitForPromises().then(() => {
expect(window.location.assign).not.toHaveBeenCalled();
- expect(Flash).toHaveBeenCalledWith(
- 'An error occurred while acknowledging the notification. Refresh the page and try again.',
- );
+ expect(createFlash).toHaveBeenCalledWith({
+ message:
+ 'An error occurred while acknowledging the notification. Refresh the page and try again.',
+ });
});
});
});
diff --git a/spec/frontend/pipeline_editor/components/editor/ci_config_merged_preview_spec.js b/spec/frontend/pipeline_editor/components/editor/ci_config_merged_preview_spec.js
index fb191fccb0d..7dd8a77d055 100644
--- a/spec/frontend/pipeline_editor/components/editor/ci_config_merged_preview_spec.js
+++ b/spec/frontend/pipeline_editor/components/editor/ci_config_merged_preview_spec.js
@@ -8,7 +8,7 @@ import { mockLintResponse, mockCiConfigPath } from '../../mock_data';
describe('Text editor component', () => {
let wrapper;
- const MockEditorLite = {
+ const MockSourceEditor = {
template: '<div/>',
props: ['value', 'fileName', 'editorOptions'],
mounted() {
@@ -26,13 +26,13 @@ describe('Text editor component', () => {
ciConfigPath: mockCiConfigPath,
},
stubs: {
- EditorLite: MockEditorLite,
+ SourceEditor: MockSourceEditor,
},
});
};
const findIcon = () => wrapper.findComponent(GlIcon);
- const findEditor = () => wrapper.findComponent(MockEditorLite);
+ const findEditor = () => wrapper.findComponent(MockSourceEditor);
afterEach(() => {
wrapper.destroy();
diff --git a/spec/frontend/pipeline_editor/components/editor/ci_editor_header_spec.js b/spec/frontend/pipeline_editor/components/editor/ci_editor_header_spec.js
new file mode 100644
index 00000000000..3ee53d4a055
--- /dev/null
+++ b/spec/frontend/pipeline_editor/components/editor/ci_editor_header_spec.js
@@ -0,0 +1,53 @@
+import { GlButton } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
+import CiEditorHeader from '~/pipeline_editor/components/editor/ci_editor_header.vue';
+import {
+ pipelineEditorTrackingOptions,
+ TEMPLATE_REPOSITORY_URL,
+} from '~/pipeline_editor/constants';
+
+describe('CI Editor Header', () => {
+ let wrapper;
+ let trackingSpy = null;
+
+ const createComponent = () => {
+ wrapper = shallowMount(CiEditorHeader, {});
+ };
+
+ const findLinkBtn = () => wrapper.findComponent(GlButton);
+
+ afterEach(() => {
+ wrapper.destroy();
+ unmockTracking();
+ });
+
+ describe('link button', () => {
+ beforeEach(() => {
+ createComponent();
+ trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
+ });
+
+ it('finds the browse template button', () => {
+ expect(findLinkBtn().exists()).toBe(true);
+ });
+
+ it('contains the link to the template repo', () => {
+ expect(findLinkBtn().attributes('href')).toBe(TEMPLATE_REPOSITORY_URL);
+ });
+
+ it('has the external-link icon', () => {
+ expect(findLinkBtn().props('icon')).toBe('external-link');
+ });
+
+ it('tracks the click on the browse button', async () => {
+ const { label, actions } = pipelineEditorTrackingOptions;
+
+ await findLinkBtn().vm.$emit('click');
+
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, actions.browse_templates, {
+ label,
+ });
+ });
+ });
+});
diff --git a/spec/frontend/pipeline_editor/components/editor/text_editor_spec.js b/spec/frontend/pipeline_editor/components/editor/text_editor_spec.js
index 6f9245e39aa..c6c7f593cc5 100644
--- a/spec/frontend/pipeline_editor/components/editor/text_editor_spec.js
+++ b/spec/frontend/pipeline_editor/components/editor/text_editor_spec.js
@@ -1,7 +1,7 @@
import { shallowMount } from '@vue/test-utils';
import { EDITOR_READY_EVENT } from '~/editor/constants';
-import { EditorLiteExtension } from '~/editor/extensions/editor_lite_extension_base';
+import { SourceEditorExtension } from '~/editor/extensions/source_editor_extension_base';
import TextEditor from '~/pipeline_editor/components/editor/text_editor.vue';
import {
mockCiConfigPath,
@@ -19,7 +19,7 @@ describe('Pipeline Editor | Text editor component', () => {
let mockUse;
let mockRegisterCiSchema;
- const MockEditorLite = {
+ const MockSourceEditor = {
template: '<div/>',
props: ['value', 'fileName'],
mounted() {
@@ -55,15 +55,15 @@ describe('Pipeline Editor | Text editor component', () => {
[EDITOR_READY_EVENT]: editorReadyListener,
},
stubs: {
- EditorLite: MockEditorLite,
+ SourceEditor: MockSourceEditor,
},
});
};
- const findEditor = () => wrapper.findComponent(MockEditorLite);
+ const findEditor = () => wrapper.findComponent(MockSourceEditor);
beforeEach(() => {
- EditorLiteExtension.deferRerender = jest.fn();
+ SourceEditorExtension.deferRerender = jest.fn();
});
afterEach(() => {
diff --git a/spec/frontend/pipeline_editor/components/file-nav/branch_switcher_spec.js b/spec/frontend/pipeline_editor/components/file-nav/branch_switcher_spec.js
index e731ad8695e..85b51d08f88 100644
--- a/spec/frontend/pipeline_editor/components/file-nav/branch_switcher_spec.js
+++ b/spec/frontend/pipeline_editor/components/file-nav/branch_switcher_spec.js
@@ -207,7 +207,8 @@ describe('Pipeline editor branch switcher', () => {
it('updates session history when selecting a different branch', async () => {
const branch = findDropdownItems().at(1);
- await branch.vm.$emit('click');
+ branch.vm.$emit('click');
+ await waitForPromises();
expect(window.history.pushState).toHaveBeenCalled();
expect(window.history.pushState.mock.calls[0][2]).toContain(`?branch_name=${branch.text()}`);
@@ -215,7 +216,8 @@ describe('Pipeline editor branch switcher', () => {
it('does not update session history when selecting current branch', async () => {
const branch = findDropdownItems().at(0);
- await branch.vm.$emit('click');
+ branch.vm.$emit('click');
+ await waitForPromises();
expect(branch.text()).toBe(mockDefaultBranch);
expect(window.history.pushState).not.toHaveBeenCalled();
@@ -227,7 +229,8 @@ describe('Pipeline editor branch switcher', () => {
expect(branch.text()).not.toBe(mockDefaultBranch);
expect(wrapper.emitted('refetchContent')).toBeUndefined();
- await branch.vm.$emit('click');
+ branch.vm.$emit('click');
+ await waitForPromises();
expect(wrapper.emitted('refetchContent')).toBeDefined();
expect(wrapper.emitted('refetchContent')).toHaveLength(1);
@@ -239,10 +242,20 @@ describe('Pipeline editor branch switcher', () => {
expect(branch.text()).toBe(mockDefaultBranch);
expect(wrapper.emitted('refetchContent')).toBeUndefined();
- await branch.vm.$emit('click');
+ branch.vm.$emit('click');
+ await waitForPromises();
expect(wrapper.emitted('refetchContent')).toBeUndefined();
});
+
+ it('emits the updateCommitSha event when selecting a different branch', async () => {
+ expect(wrapper.emitted('updateCommitSha')).toBeUndefined();
+
+ const branch = findDropdownItems().at(1);
+ branch.vm.$emit('click');
+
+ expect(wrapper.emitted('updateCommitSha')).toHaveLength(1);
+ });
});
describe('when searching', () => {
diff --git a/spec/frontend/pipeline_editor/components/ui/editor_tab_spec.js b/spec/frontend/pipeline_editor/components/ui/editor_tab_spec.js
index 8def83d578b..3becf82ed6e 100644
--- a/spec/frontend/pipeline_editor/components/ui/editor_tab_spec.js
+++ b/spec/frontend/pipeline_editor/components/ui/editor_tab_spec.js
@@ -6,7 +6,7 @@ import EditorTab from '~/pipeline_editor/components/ui/editor_tab.vue';
const mockContent1 = 'MOCK CONTENT 1';
const mockContent2 = 'MOCK CONTENT 2';
-const MockEditorLite = {
+const MockSourceEditor = {
template: '<div>EDITOR</div>',
};
@@ -48,12 +48,12 @@ describe('~/pipeline_editor/components/ui/editor_tab.vue', () => {
wrapper = mount(EditorTab, {
propsData: props,
slots: {
- default: MockEditorLite,
+ default: MockSourceEditor,
},
});
};
- const findSlotComponent = () => wrapper.findComponent(MockEditorLite);
+ const findSlotComponent = () => wrapper.findComponent(MockSourceEditor);
const findAlert = () => wrapper.findComponent(GlAlert);
beforeEach(() => {
diff --git a/spec/frontend/pipeline_editor/graphql/resolvers_spec.js b/spec/frontend/pipeline_editor/graphql/resolvers_spec.js
index d39c0d80296..76ae96c623a 100644
--- a/spec/frontend/pipeline_editor/graphql/resolvers_spec.js
+++ b/spec/frontend/pipeline_editor/graphql/resolvers_spec.js
@@ -1,15 +1,8 @@
import MockAdapter from 'axios-mock-adapter';
-import Api from '~/api';
import axios from '~/lib/utils/axios_utils';
import httpStatus from '~/lib/utils/http_status';
import { resolvers } from '~/pipeline_editor/graphql/resolvers';
-import {
- mockCiConfigPath,
- mockCiYml,
- mockDefaultBranch,
- mockLintResponse,
- mockProjectFullPath,
-} from '../mock_data';
+import { mockLintResponse } from '../mock_data';
jest.mock('~/api', () => {
return {
@@ -18,36 +11,6 @@ jest.mock('~/api', () => {
});
describe('~/pipeline_editor/graphql/resolvers', () => {
- describe('Query', () => {
- describe('blobContent', () => {
- beforeEach(() => {
- Api.getRawFile.mockResolvedValue({
- data: mockCiYml,
- });
- });
-
- afterEach(() => {
- Api.getRawFile.mockReset();
- });
-
- it('resolves lint data with type names', async () => {
- const result = resolvers.Query.blobContent(null, {
- projectPath: mockProjectFullPath,
- path: mockCiConfigPath,
- ref: mockDefaultBranch,
- });
-
- expect(Api.getRawFile).toHaveBeenCalledWith(mockProjectFullPath, mockCiConfigPath, {
- ref: mockDefaultBranch,
- });
-
- // eslint-disable-next-line no-underscore-dangle
- expect(result.__typename).toBe('BlobContent');
- await expect(result.rawData).resolves.toBe(mockCiYml);
- });
- });
- });
-
describe('Mutation', () => {
describe('lintCI', () => {
let mock;
diff --git a/spec/frontend/pipeline_editor/mock_data.js b/spec/frontend/pipeline_editor/mock_data.js
index cadcdf6ae2e..4d4a8c21d78 100644
--- a/spec/frontend/pipeline_editor/mock_data.js
+++ b/spec/frontend/pipeline_editor/mock_data.js
@@ -35,6 +35,23 @@ job_build:
- echo "build"
needs: ["job_test_2"]
`;
+export const mockBlobContentQueryResponse = {
+ data: {
+ project: { repository: { blobs: { nodes: [{ rawBlob: mockCiYml }] } } },
+ },
+};
+
+export const mockBlobContentQueryResponseNoCiFile = {
+ data: {
+ project: { repository: { blobs: { nodes: [] } } },
+ },
+};
+
+export const mockBlobContentQueryResponseEmptyCiFile = {
+ data: {
+ project: { repository: { blobs: { nodes: [{ rawBlob: '' }] } } },
+ },
+};
const mockJobFields = {
beforeScript: [],
@@ -139,6 +156,35 @@ export const mergeUnwrappedCiConfig = (mergedConfig) => {
};
};
+export const mockNewCommitShaResults = {
+ data: {
+ project: {
+ pipelines: {
+ nodes: [
+ {
+ id: 'gid://gitlab/Ci::Pipeline/1',
+ sha: 'd0d56d363d8a3f67a8ab9fc00207d468f30032ca',
+ path: `/${mockProjectFullPath}/-/pipelines/488`,
+ commitPath: `/${mockProjectFullPath}/-/commit/d0d56d363d8a3f67a8ab9fc00207d468f30032ca`,
+ },
+ {
+ id: 'gid://gitlab/Ci::Pipeline/2',
+ sha: 'fcab2ece40b26f428dfa3aa288b12c3c5bdb06aa',
+ path: `/${mockProjectFullPath}/-/pipelines/487`,
+ commitPath: `/${mockProjectFullPath}/-/commit/fcab2ece40b26f428dfa3aa288b12c3c5bdb06aa`,
+ },
+ {
+ id: 'gid://gitlab/Ci::Pipeline/3',
+ sha: '6c16b17c7f94a438ae19a96c285bb49e3c632cf4',
+ path: `/${mockProjectFullPath}/-/pipelines/433`,
+ commitPath: `/${mockProjectFullPath}/-/commit/6c16b17c7f94a438ae19a96c285bb49e3c632cf4`,
+ },
+ ],
+ },
+ },
+ },
+};
+
export const mockProjectBranches = {
data: {
project: {
diff --git a/spec/frontend/pipeline_editor/pipeline_editor_app_spec.js b/spec/frontend/pipeline_editor/pipeline_editor_app_spec.js
index c88fe159c0d..b0d1a69ee56 100644
--- a/spec/frontend/pipeline_editor/pipeline_editor_app_spec.js
+++ b/spec/frontend/pipeline_editor/pipeline_editor_app_spec.js
@@ -3,7 +3,6 @@ import { shallowMount, createLocalVue } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import httpStatusCodes from '~/lib/utils/http_status';
import CommitForm from '~/pipeline_editor/components/commit/commit_form.vue';
import TextEditor from '~/pipeline_editor/components/editor/text_editor.vue';
@@ -11,21 +10,30 @@ import PipelineEditorTabs from '~/pipeline_editor/components/pipeline_editor_tab
import PipelineEditorEmptyState from '~/pipeline_editor/components/ui/pipeline_editor_empty_state.vue';
import PipelineEditorMessages from '~/pipeline_editor/components/ui/pipeline_editor_messages.vue';
import { COMMIT_SUCCESS, COMMIT_FAILURE } from '~/pipeline_editor/constants';
+import getBlobContent from '~/pipeline_editor/graphql/queries/blob_content.graphql';
import getCiConfigData from '~/pipeline_editor/graphql/queries/ci_config.graphql';
+import getPipelineQuery from '~/pipeline_editor/graphql/queries/client/pipeline.graphql';
+import getTemplate from '~/pipeline_editor/graphql/queries/get_starter_template.query.graphql';
+import getLatestCommitShaQuery from '~/pipeline_editor/graphql/queries/latest_commit_sha.query.graphql';
import PipelineEditorApp from '~/pipeline_editor/pipeline_editor_app.vue';
import PipelineEditorHome from '~/pipeline_editor/pipeline_editor_home.vue';
import {
mockCiConfigPath,
mockCiConfigQueryResponse,
+ mockBlobContentQueryResponse,
+ mockBlobContentQueryResponseEmptyCiFile,
+ mockBlobContentQueryResponseNoCiFile,
mockCiYml,
+ mockCommitSha,
mockDefaultBranch,
mockProjectFullPath,
+ mockNewCommitShaResults,
} from './mock_data';
const localVue = createLocalVue();
localVue.use(VueApollo);
-const MockEditorLite = {
+const MockSourceEditor = {
template: '<div/>',
};
@@ -44,6 +52,10 @@ describe('Pipeline editor app component', () => {
let mockApollo;
let mockBlobContentData;
let mockCiConfigData;
+ let mockGetTemplate;
+ let mockUpdateCommitSha;
+ let mockLatestCommitShaQuery;
+ let mockPipelineQuery;
const createComponent = ({ blobLoading = false, options = {}, provide = {} } = {}) => {
wrapper = shallowMount(PipelineEditorApp, {
@@ -55,7 +67,7 @@ describe('Pipeline editor app component', () => {
PipelineEditorHome,
PipelineEditorTabs,
PipelineEditorMessages,
- EditorLite: MockEditorLite,
+ SourceEditor: MockSourceEditor,
PipelineEditorEmptyState,
},
mocks: {
@@ -75,16 +87,23 @@ describe('Pipeline editor app component', () => {
};
const createComponentWithApollo = async ({ props = {}, provide = {} } = {}) => {
- const handlers = [[getCiConfigData, mockCiConfigData]];
+ const handlers = [
+ [getBlobContent, mockBlobContentData],
+ [getCiConfigData, mockCiConfigData],
+ [getTemplate, mockGetTemplate],
+ [getLatestCommitShaQuery, mockLatestCommitShaQuery],
+ [getPipelineQuery, mockPipelineQuery],
+ ];
+
const resolvers = {
Query: {
- blobContent() {
- return {
- __typename: 'BlobContent',
- rawData: mockBlobContentData(),
- };
+ commitSha() {
+ return mockCommitSha;
},
},
+ Mutation: {
+ updateCommitSha: mockUpdateCommitSha,
+ },
};
mockApollo = createMockApollo(handlers, resolvers);
@@ -116,6 +135,10 @@ describe('Pipeline editor app component', () => {
beforeEach(() => {
mockBlobContentData = jest.fn();
mockCiConfigData = jest.fn();
+ mockGetTemplate = jest.fn();
+ mockUpdateCommitSha = jest.fn();
+ mockLatestCommitShaQuery = jest.fn();
+ mockPipelineQuery = jest.fn();
});
afterEach(() => {
@@ -133,7 +156,7 @@ describe('Pipeline editor app component', () => {
describe('when queries are called', () => {
beforeEach(() => {
- mockBlobContentData.mockResolvedValue(mockCiYml);
+ mockBlobContentData.mockResolvedValue(mockBlobContentQueryResponse);
mockCiConfigData.mockResolvedValue(mockCiConfigQueryResponse);
});
@@ -154,39 +177,19 @@ describe('Pipeline editor app component', () => {
expect(mockCiConfigData).toHaveBeenCalledWith({
content: mockCiYml,
projectPath: mockProjectFullPath,
+ sha: mockCommitSha,
});
});
});
describe('when no CI config file exists', () => {
- describe('in a project without a repository', () => {
- it('shows an empty state and does not show editor home component', async () => {
- mockBlobContentData.mockRejectedValueOnce({
- response: {
- status: httpStatusCodes.BAD_REQUEST,
- },
- });
- await createComponentWithApollo();
-
- expect(findEmptyState().exists()).toBe(true);
- expect(findAlert().exists()).toBe(false);
- expect(findEditorHome().exists()).toBe(false);
- });
- });
-
- describe('in a project with a repository', () => {
- it('shows an empty state and does not show editor home component', async () => {
- mockBlobContentData.mockRejectedValueOnce({
- response: {
- status: httpStatusCodes.NOT_FOUND,
- },
- });
- await createComponentWithApollo();
+ it('shows an empty state and does not show editor home component', async () => {
+ mockBlobContentData.mockResolvedValue(mockBlobContentQueryResponseNoCiFile);
+ await createComponentWithApollo();
- expect(findEmptyState().exists()).toBe(true);
- expect(findAlert().exists()).toBe(false);
- expect(findEditorHome().exists()).toBe(false);
- });
+ expect(findEmptyState().exists()).toBe(true);
+ expect(findAlert().exists()).toBe(false);
+ expect(findEditorHome().exists()).toBe(false);
});
describe('because of a fetching error', () => {
@@ -204,13 +207,28 @@ describe('Pipeline editor app component', () => {
});
});
+ describe('with an empty CI config file', () => {
+ describe('with empty state feature flag on', () => {
+ it('does not show the empty screen state', async () => {
+ mockBlobContentData.mockResolvedValue(mockBlobContentQueryResponseEmptyCiFile);
+
+ await createComponentWithApollo({
+ provide: {
+ glFeatures: {
+ pipelineEditorEmptyStateAction: true,
+ },
+ },
+ });
+
+ expect(findEmptyState().exists()).toBe(false);
+ expect(findTextEditor().exists()).toBe(true);
+ });
+ });
+ });
+
describe('when landing on the empty state with feature flag on', () => {
it('user can click on CTA button and see an empty editor', async () => {
- mockBlobContentData.mockRejectedValueOnce({
- response: {
- status: httpStatusCodes.NOT_FOUND,
- },
- });
+ mockBlobContentData.mockResolvedValue(mockBlobContentQueryResponseNoCiFile);
await createComponentWithApollo({
provide: {
@@ -315,21 +333,83 @@ describe('Pipeline editor app component', () => {
});
it('hides start screen when refetch fetches CI file', async () => {
- mockBlobContentData.mockRejectedValue({
- response: {
- status: httpStatusCodes.NOT_FOUND,
- },
- });
+ mockBlobContentData.mockResolvedValue(mockBlobContentQueryResponseNoCiFile);
await createComponentWithApollo();
expect(findEmptyState().exists()).toBe(true);
expect(findEditorHome().exists()).toBe(false);
- mockBlobContentData.mockResolvedValue(mockCiYml);
+ mockBlobContentData.mockResolvedValue(mockBlobContentQueryResponse);
await wrapper.vm.$apollo.queries.initialCiFileContent.refetch();
expect(findEmptyState().exists()).toBe(false);
expect(findEditorHome().exists()).toBe(true);
});
});
+
+ describe('when a template parameter is present in the URL', () => {
+ const { location } = window;
+
+ beforeEach(() => {
+ delete window.location;
+ window.location = new URL('https://localhost?template=Android');
+ });
+
+ afterEach(() => {
+ window.location = location;
+ });
+
+ it('renders the given template', async () => {
+ await createComponentWithApollo();
+
+ expect(mockGetTemplate).toHaveBeenCalledWith({
+ projectPath: mockProjectFullPath,
+ templateName: 'Android',
+ });
+
+ expect(findEmptyState().exists()).toBe(false);
+ expect(findTextEditor().exists()).toBe(true);
+ });
+ });
+
+ describe('when updating commit sha', () => {
+ const newCommitSha = mockNewCommitShaResults.data.project.pipelines.nodes[0].sha;
+
+ beforeEach(async () => {
+ mockUpdateCommitSha.mockResolvedValue(newCommitSha);
+ mockLatestCommitShaQuery.mockResolvedValue(mockNewCommitShaResults);
+ await createComponentWithApollo();
+ });
+
+ it('fetches updated commit sha for the new branch', async () => {
+ expect(mockLatestCommitShaQuery).not.toHaveBeenCalled();
+
+ wrapper
+ .findComponent(PipelineEditorHome)
+ .vm.$emit('updateCommitSha', { newBranch: 'new-branch' });
+ await waitForPromises();
+
+ expect(mockLatestCommitShaQuery).toHaveBeenCalledWith({
+ projectPath: mockProjectFullPath,
+ ref: 'new-branch',
+ });
+ });
+
+ it('updates commit sha with the newly fetched commit sha', async () => {
+ expect(mockUpdateCommitSha).not.toHaveBeenCalled();
+
+ wrapper
+ .findComponent(PipelineEditorHome)
+ .vm.$emit('updateCommitSha', { newBranch: 'new-branch' });
+ await waitForPromises();
+
+ expect(mockUpdateCommitSha).toHaveBeenCalled();
+ expect(mockUpdateCommitSha).toHaveBeenCalledWith(
+ expect.any(Object),
+ { commitSha: mockNewCommitShaResults.data.project.pipelines.nodes[0].sha },
+ expect.any(Object),
+ expect.any(Object),
+ );
+ });
+ });
});
diff --git a/spec/frontend/pipelines/empty_state_spec.js b/spec/frontend/pipelines/empty_state_spec.js
index 912bc7a104a..1af3065477d 100644
--- a/spec/frontend/pipelines/empty_state_spec.js
+++ b/spec/frontend/pipelines/empty_state_spec.js
@@ -1,14 +1,21 @@
+import '~/commons';
import { mount } from '@vue/test-utils';
import EmptyState from '~/pipelines/components/pipelines_list/empty_state.vue';
+import PipelinesCiTemplates from '~/pipelines/components/pipelines_list/pipelines_ci_templates.vue';
describe('Pipelines Empty State', () => {
let wrapper;
const findIllustration = () => wrapper.find('img');
const findButton = () => wrapper.find('a');
+ const pipelinesCiTemplates = () => wrapper.findComponent(PipelinesCiTemplates);
const createWrapper = (props = {}) => {
wrapper = mount(EmptyState, {
+ provide: {
+ pipelineEditorPath: '',
+ suggestedCiTemplates: [],
+ },
propsData: {
emptyStateSvgPath: 'foo.svg',
canSetCi: true,
@@ -27,27 +34,8 @@ describe('Pipelines Empty State', () => {
wrapper = null;
});
- it('should render empty state SVG', () => {
- expect(findIllustration().attributes('src')).toBe('foo.svg');
- });
-
- it('should render empty state header', () => {
- expect(wrapper.text()).toContain('Build with confidence');
- });
-
- it('should render empty state information', () => {
- expect(wrapper.text()).toContain(
- 'GitLab CI/CD can automatically build, test, and deploy your code. Let GitLab take care of time',
- 'consuming tasks, so you can spend more time creating',
- );
- });
-
- it('should render button with help path', () => {
- expect(findButton().attributes('href')).toBe('/help/ci/quick_start/index.md');
- });
-
- it('should render button text', () => {
- expect(findButton().text()).toBe('Get started with CI/CD');
+ it('should render the CI/CD templates', () => {
+ expect(pipelinesCiTemplates()).toExist();
});
});
diff --git a/spec/frontend/pipelines/graph/mock_data.js b/spec/frontend/pipelines/graph/mock_data.js
index 28fe3b67e7b..3812483766d 100644
--- a/spec/frontend/pipelines/graph/mock_data.js
+++ b/spec/frontend/pipelines/graph/mock_data.js
@@ -12,6 +12,10 @@ export const mockPipelineResponse = {
usesNeeds: true,
downstream: null,
upstream: null,
+ userPermissions: {
+ __typename: 'PipelinePermissions',
+ updatePipeline: true,
+ },
stages: {
__typename: 'CiStageConnection',
nodes: [
@@ -573,6 +577,10 @@ export const wrappedPipelineReturn = {
iid: '38',
complete: true,
usesNeeds: true,
+ userPermissions: {
+ __typename: 'PipelinePermissions',
+ updatePipeline: true,
+ },
downstream: {
__typename: 'PipelineConnection',
nodes: [],
diff --git a/spec/frontend/pipelines/graph/stage_column_component_spec.js b/spec/frontend/pipelines/graph/stage_column_component_spec.js
index f9f6c96a1a6..99e8ea9d0a4 100644
--- a/spec/frontend/pipelines/graph/stage_column_component_spec.js
+++ b/spec/frontend/pipelines/graph/stage_column_component_spec.js
@@ -31,6 +31,9 @@ const defaultProps = {
name: 'Fish',
groups: mockGroups,
pipelineId: 159,
+ userPermissions: {
+ updatePipeline: true,
+ },
};
describe('stage column component', () => {
@@ -53,7 +56,6 @@ describe('stage column component', () => {
afterEach(() => {
wrapper.destroy();
- wrapper = null;
});
describe('when mounted', () => {
@@ -152,36 +154,52 @@ describe('stage column component', () => {
});
describe('with action', () => {
- beforeEach(() => {
+ const defaults = {
+ groups: [
+ {
+ id: 4259,
+ name: '<img src=x onerror=alert(document.domain)>',
+ status: {
+ icon: 'status_success',
+ label: 'success',
+ tooltip: '<img src=x onerror=alert(document.domain)>',
+ },
+ jobs: [mockJob],
+ },
+ ],
+ title: 'test',
+ hasTriggeredBy: false,
+ action: {
+ icon: 'play',
+ title: 'Play all',
+ path: 'action',
+ },
+ };
+
+ it('renders action button if permissions are permitted', () => {
createComponent({
method: mount,
props: {
- groups: [
- {
- id: 4259,
- name: '<img src=x onerror=alert(document.domain)>',
- status: {
- icon: 'status_success',
- label: 'success',
- tooltip: '<img src=x onerror=alert(document.domain)>',
- },
- jobs: [mockJob],
- },
- ],
- title: 'test',
- hasTriggeredBy: false,
- action: {
- icon: 'play',
- title: 'Play all',
- path: 'action',
- },
+ ...defaults,
},
});
- });
- it('renders action button', () => {
expect(findActionComponent().exists()).toBe(true);
});
+
+ it('does not render action button if permissions are not permitted', () => {
+ createComponent({
+ method: mount,
+ props: {
+ ...defaults,
+ userPermissions: {
+ updatePipeline: false,
+ },
+ },
+ });
+
+ expect(findActionComponent().exists()).toBe(false);
+ });
});
describe('without action', () => {
diff --git a/spec/frontend/pipelines/graph_shared/__snapshots__/links_inner_spec.js.snap b/spec/frontend/pipelines/graph_shared/__snapshots__/links_inner_spec.js.snap
index 16c28791514..82206e907ff 100644
--- a/spec/frontend/pipelines/graph_shared/__snapshots__/links_inner_spec.js.snap
+++ b/spec/frontend/pipelines/graph_shared/__snapshots__/links_inner_spec.js.snap
@@ -2,29 +2,29 @@
exports[`Links Inner component with a large number of needs matches snapshot and has expected path 1`] = `
"<div class=\\"gl-display-flex gl-relative\\" totalgroups=\\"10\\"><svg id=\\"link-svg\\" viewBox=\\"0,0,1019,445\\" width=\\"1019px\\" height=\\"445px\\" class=\\"gl-absolute gl-pointer-events-none\\">
- <path d=\\"M202,118L42,118C72,118,72,138,102,138\\" stroke-width=\\"2\\" class=\\"gl-fill-transparent gl-transition-duration-slow gl-transition-timing-function-ease gl-stroke-gray-200\\"></path>
- <path d=\\"M202,118L52,118C82,118,82,148,112,148\\" stroke-width=\\"2\\" class=\\"gl-fill-transparent gl-transition-duration-slow gl-transition-timing-function-ease gl-stroke-gray-200\\"></path>
- <path d=\\"M222,138L62,138C92,138,92,158,122,158\\" stroke-width=\\"2\\" class=\\"gl-fill-transparent gl-transition-duration-slow gl-transition-timing-function-ease gl-stroke-gray-200\\"></path>
- <path d=\\"M212,128L72,128C102,128,102,168,132,168\\" stroke-width=\\"2\\" class=\\"gl-fill-transparent gl-transition-duration-slow gl-transition-timing-function-ease gl-stroke-gray-200\\"></path>
- <path d=\\"M232,148L82,148C112,148,112,178,142,178\\" stroke-width=\\"2\\" class=\\"gl-fill-transparent gl-transition-duration-slow gl-transition-timing-function-ease gl-stroke-gray-200\\"></path>
+ <path d=\\"M202,118C52,118,52,138,102,138\\" stroke-width=\\"2\\" class=\\"gl-fill-transparent gl-transition-duration-slow gl-transition-timing-function-ease gl-stroke-gray-200\\"></path>
+ <path d=\\"M202,118C62,118,62,148,112,148\\" stroke-width=\\"2\\" class=\\"gl-fill-transparent gl-transition-duration-slow gl-transition-timing-function-ease gl-stroke-gray-200\\"></path>
+ <path d=\\"M222,138C72,138,72,158,122,158\\" stroke-width=\\"2\\" class=\\"gl-fill-transparent gl-transition-duration-slow gl-transition-timing-function-ease gl-stroke-gray-200\\"></path>
+ <path d=\\"M212,128C82,128,82,168,132,168\\" stroke-width=\\"2\\" class=\\"gl-fill-transparent gl-transition-duration-slow gl-transition-timing-function-ease gl-stroke-gray-200\\"></path>
+ <path d=\\"M232,148C92,148,92,178,142,178\\" stroke-width=\\"2\\" class=\\"gl-fill-transparent gl-transition-duration-slow gl-transition-timing-function-ease gl-stroke-gray-200\\"></path>
</svg> </div>"
`;
exports[`Links Inner component with a parallel need matches snapshot and has expected path 1`] = `
"<div class=\\"gl-display-flex gl-relative\\" totalgroups=\\"10\\"><svg id=\\"link-svg\\" viewBox=\\"0,0,1019,445\\" width=\\"1019px\\" height=\\"445px\\" class=\\"gl-absolute gl-pointer-events-none\\">
- <path d=\\"M192,108L22,108C52,108,52,118,82,118\\" stroke-width=\\"2\\" class=\\"gl-fill-transparent gl-transition-duration-slow gl-transition-timing-function-ease gl-stroke-gray-200\\"></path>
+ <path d=\\"M192,108C32,108,32,118,82,118\\" stroke-width=\\"2\\" class=\\"gl-fill-transparent gl-transition-duration-slow gl-transition-timing-function-ease gl-stroke-gray-200\\"></path>
</svg> </div>"
`;
exports[`Links Inner component with one need matches snapshot and has expected path 1`] = `
"<div class=\\"gl-display-flex gl-relative\\" totalgroups=\\"10\\"><svg id=\\"link-svg\\" viewBox=\\"0,0,1019,445\\" width=\\"1019px\\" height=\\"445px\\" class=\\"gl-absolute gl-pointer-events-none\\">
- <path d=\\"M202,118L42,118C72,118,72,138,102,138\\" stroke-width=\\"2\\" class=\\"gl-fill-transparent gl-transition-duration-slow gl-transition-timing-function-ease gl-stroke-gray-200\\"></path>
+ <path d=\\"M202,118C52,118,52,138,102,138\\" stroke-width=\\"2\\" class=\\"gl-fill-transparent gl-transition-duration-slow gl-transition-timing-function-ease gl-stroke-gray-200\\"></path>
</svg> </div>"
`;
exports[`Links Inner component with same stage needs matches snapshot and has expected path 1`] = `
"<div class=\\"gl-display-flex gl-relative\\" totalgroups=\\"10\\"><svg id=\\"link-svg\\" viewBox=\\"0,0,1019,445\\" width=\\"1019px\\" height=\\"445px\\" class=\\"gl-absolute gl-pointer-events-none\\">
- <path d=\\"M192,108L22,108C52,108,52,118,82,118\\" stroke-width=\\"2\\" class=\\"gl-fill-transparent gl-transition-duration-slow gl-transition-timing-function-ease gl-stroke-gray-200\\"></path>
- <path d=\\"M202,118L32,118C62,118,62,128,92,128\\" stroke-width=\\"2\\" class=\\"gl-fill-transparent gl-transition-duration-slow gl-transition-timing-function-ease gl-stroke-gray-200\\"></path>
+ <path d=\\"M192,108C32,108,32,118,82,118\\" stroke-width=\\"2\\" class=\\"gl-fill-transparent gl-transition-duration-slow gl-transition-timing-function-ease gl-stroke-gray-200\\"></path>
+ <path d=\\"M202,118C42,118,42,128,92,128\\" stroke-width=\\"2\\" class=\\"gl-fill-transparent gl-transition-duration-slow gl-transition-timing-function-ease gl-stroke-gray-200\\"></path>
</svg> </div>"
`;
diff --git a/spec/frontend/pipelines/pipeline_graph/pipeline_graph_spec.js b/spec/frontend/pipelines/pipeline_graph/pipeline_graph_spec.js
index 7bac7036f46..1b89e322d31 100644
--- a/spec/frontend/pipelines/pipeline_graph/pipeline_graph_spec.js
+++ b/spec/frontend/pipelines/pipeline_graph/pipeline_graph_spec.js
@@ -6,7 +6,7 @@ import LinksInner from '~/pipelines/components/graph_shared/links_inner.vue';
import LinksLayer from '~/pipelines/components/graph_shared/links_layer.vue';
import JobPill from '~/pipelines/components/pipeline_graph/job_pill.vue';
import PipelineGraph from '~/pipelines/components/pipeline_graph/pipeline_graph.vue';
-import StagePill from '~/pipelines/components/pipeline_graph/stage_pill.vue';
+import StageName from '~/pipelines/components/pipeline_graph/stage_name.vue';
import { pipelineData, singleStageData } from './mock_data';
describe('pipeline graph component', () => {
@@ -35,11 +35,9 @@ describe('pipeline graph component', () => {
const findAlert = () => wrapper.findComponent(GlAlert);
const findAllJobPills = () => wrapper.findAll(JobPill);
- const findAllStageBackgroundElements = () => wrapper.findAll('[data-testid="stage-background"]');
- const findAllStagePills = () => wrapper.findAllComponents(StagePill);
+ const findAllStageNames = () => wrapper.findAllComponents(StageName);
const findLinksLayer = () => wrapper.findComponent(LinksLayer);
const findPipelineGraph = () => wrapper.find('[data-testid="graph-container"]');
- const findStageBackgroundElementAt = (index) => findAllStageBackgroundElements().at(index);
afterEach(() => {
wrapper.destroy();
@@ -67,10 +65,10 @@ describe('pipeline graph component', () => {
wrapper = createComponent({ pipelineData: singleStageData });
});
- it('renders the right number of stage pills', () => {
+ it('renders the right number of stage titles', () => {
const expectedStagesLength = singleStageData.stages.length;
- expect(findAllStagePills()).toHaveLength(expectedStagesLength);
+ expect(findAllStageNames()).toHaveLength(expectedStagesLength);
});
it('renders the right number of job pills', () => {
@@ -81,20 +79,6 @@ describe('pipeline graph component', () => {
expect(findAllJobPills()).toHaveLength(expectedJobsLength);
});
-
- describe('rounds corner', () => {
- it.each`
- cssClass | expectedState
- ${'gl-rounded-bottom-left-6'} | ${true}
- ${'gl-rounded-top-left-6'} | ${true}
- ${'gl-rounded-top-right-6'} | ${true}
- ${'gl-rounded-bottom-right-6'} | ${true}
- `('$cssClass should be $expectedState on the only element', ({ cssClass, expectedState }) => {
- const classes = findStageBackgroundElementAt(0).classes();
-
- expect(classes.includes(cssClass)).toBe(expectedState);
- });
- });
});
describe('with multiple stages and jobs', () => {
@@ -102,10 +86,10 @@ describe('pipeline graph component', () => {
wrapper = createComponent();
});
- it('renders the right number of stage pills', () => {
+ it('renders the right number of stage titles', () => {
const expectedStagesLength = pipelineData.stages.length;
- expect(findAllStagePills()).toHaveLength(expectedStagesLength);
+ expect(findAllStageNames()).toHaveLength(expectedStagesLength);
});
it('renders the right number of job pills', () => {
@@ -116,34 +100,5 @@ describe('pipeline graph component', () => {
expect(findAllJobPills()).toHaveLength(expectedJobsLength);
});
-
- describe('rounds corner', () => {
- it.each`
- cssClass | expectedState
- ${'gl-rounded-bottom-left-6'} | ${true}
- ${'gl-rounded-top-left-6'} | ${true}
- ${'gl-rounded-top-right-6'} | ${false}
- ${'gl-rounded-bottom-right-6'} | ${false}
- `(
- '$cssClass should be $expectedState on the first element',
- ({ cssClass, expectedState }) => {
- const classes = findStageBackgroundElementAt(0).classes();
-
- expect(classes.includes(cssClass)).toBe(expectedState);
- },
- );
-
- it.each`
- cssClass | expectedState
- ${'gl-rounded-bottom-left-6'} | ${false}
- ${'gl-rounded-top-left-6'} | ${false}
- ${'gl-rounded-top-right-6'} | ${true}
- ${'gl-rounded-bottom-right-6'} | ${true}
- `('$cssClass should be $expectedState on the last element', ({ cssClass, expectedState }) => {
- const classes = findStageBackgroundElementAt(pipelineData.stages.length - 1).classes();
-
- expect(classes.includes(cssClass)).toBe(expectedState);
- });
- });
});
});
diff --git a/spec/frontend/pipelines/pipelines_ci_templates_spec.js b/spec/frontend/pipelines/pipelines_ci_templates_spec.js
index 0c37bf2d84a..db66b675fb9 100644
--- a/spec/frontend/pipelines/pipelines_ci_templates_spec.js
+++ b/spec/frontend/pipelines/pipelines_ci_templates_spec.js
@@ -1,30 +1,25 @@
+import '~/commons';
import { shallowMount } from '@vue/test-utils';
-import ExperimentTracking from '~/experimentation/experiment_tracking';
+import { mockTracking } from 'helpers/tracking_helper';
import PipelinesCiTemplate from '~/pipelines/components/pipelines_list/pipelines_ci_templates.vue';
-const addCiYmlPath = "/-/new/main?commit_message='Add%20.gitlab-ci.yml'";
+const pipelineEditorPath = '/-/ci/editor';
const suggestedCiTemplates = [
{ name: 'Android', logo: '/assets/illustrations/logos/android.svg' },
{ name: 'Bash', logo: '/assets/illustrations/logos/bash.svg' },
{ name: 'C++', logo: '/assets/illustrations/logos/c_plus_plus.svg' },
];
-jest.mock('~/experimentation/experiment_tracking');
-
describe('Pipelines CI Templates', () => {
let wrapper;
-
- const GlEmoji = { template: '<img/>' };
+ let trackingSpy;
const createWrapper = () => {
return shallowMount(PipelinesCiTemplate, {
provide: {
- addCiYmlPath,
+ pipelineEditorPath,
suggestedCiTemplates,
},
- stubs: {
- GlEmoji,
- },
});
};
@@ -44,9 +39,9 @@ describe('Pipelines CI Templates', () => {
wrapper = createWrapper();
});
- it('links to the hello world template', () => {
+ it('links to the getting started template', () => {
expect(findTestTemplateLinks().at(0).attributes('href')).toBe(
- addCiYmlPath.concat('&template=Hello-World'),
+ pipelineEditorPath.concat('?template=Getting-Started'),
);
});
});
@@ -68,7 +63,7 @@ describe('Pipelines CI Templates', () => {
it('links to the correct template', () => {
expect(findTemplateLinks().at(0).attributes('href')).toBe(
- addCiYmlPath.concat('&template=Android'),
+ pipelineEditorPath.concat('?template=Android'),
);
});
@@ -88,24 +83,25 @@ describe('Pipelines CI Templates', () => {
describe('tracking', () => {
beforeEach(() => {
wrapper = createWrapper();
+ trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
});
it('sends an event when template is clicked', () => {
findTemplateLinks().at(0).vm.$emit('click');
- expect(ExperimentTracking).toHaveBeenCalledWith('pipeline_empty_state_templates', {
+ expect(trackingSpy).toHaveBeenCalledTimes(1);
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, 'template_clicked', {
label: 'Android',
});
- expect(ExperimentTracking.prototype.event).toHaveBeenCalledWith('template_clicked');
});
- it('sends an event when Hello-World template is clicked', () => {
+ it('sends an event when Getting-Started template is clicked', () => {
findTestTemplateLinks().at(0).vm.$emit('click');
- expect(ExperimentTracking).toHaveBeenCalledWith('pipeline_empty_state_templates', {
- label: 'Hello-World',
+ expect(trackingSpy).toHaveBeenCalledTimes(1);
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, 'template_clicked', {
+ label: 'Getting-Started',
});
- expect(ExperimentTracking.prototype.event).toHaveBeenCalledWith('template_clicked');
});
});
});
diff --git a/spec/frontend/pipelines/pipelines_spec.js b/spec/frontend/pipelines/pipelines_spec.js
index 874ecbccf82..2166961cedd 100644
--- a/spec/frontend/pipelines/pipelines_spec.js
+++ b/spec/frontend/pipelines/pipelines_spec.js
@@ -12,6 +12,7 @@ import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import NavigationControls from '~/pipelines/components/pipelines_list/nav_controls.vue';
import PipelinesComponent from '~/pipelines/components/pipelines_list/pipelines.vue';
+import PipelinesCiTemplates from '~/pipelines/components/pipelines_list/pipelines_ci_templates.vue';
import PipelinesTableComponent from '~/pipelines/components/pipelines_list/pipelines_table.vue';
import { RAW_TEXT_WARNING } from '~/pipelines/constants';
import Store from '~/pipelines/stores/pipelines_store';
@@ -82,6 +83,10 @@ describe('Pipelines', () => {
const createComponent = (props = defaultProps) => {
wrapper = extendedWrapper(
mount(PipelinesComponent, {
+ provide: {
+ pipelineEditorPath: '',
+ suggestedCiTemplates: [],
+ },
propsData: {
store: new Store(),
projectId: mockProjectId,
@@ -551,52 +556,74 @@ describe('Pipelines', () => {
await waitForPromises();
});
- it('renders empty state', () => {
- expect(findEmptyState().text()).toContain('Build with confidence');
- expect(findEmptyState().text()).toContain(
- 'GitLab CI/CD can automatically build, test, and deploy your code.',
- );
-
- expect(findEmptyState().find(GlButton).text()).toBe('Get started with CI/CD');
- expect(findEmptyState().find(GlButton).attributes('href')).toBe(
- '/help/ci/quick_start/index.md',
- );
+ it('renders the CI/CD templates', () => {
+ expect(wrapper.find(PipelinesCiTemplates)).toExist();
});
describe('when the code_quality_walkthrough experiment is active', () => {
beforeAll(() => {
getExperimentData.mockImplementation((name) => name === 'code_quality_walkthrough');
- getExperimentVariant.mockReturnValue('candidate');
});
- it('renders another CTA button', () => {
- expect(findEmptyState().findComponent(GlButton).text()).toBe('Add a code quality job');
- expect(findEmptyState().findComponent(GlButton).attributes('href')).toBe(
- paths.codeQualityPagePath,
- );
+ describe('the control state', () => {
+ beforeAll(() => {
+ getExperimentVariant.mockReturnValue('control');
+ });
+
+ it('renders the CI/CD templates', () => {
+ expect(wrapper.find(PipelinesCiTemplates)).toExist();
+ });
+ });
+
+ describe('the candidate state', () => {
+ beforeAll(() => {
+ getExperimentVariant.mockReturnValue('candidate');
+ });
+
+ it('renders another CTA button', () => {
+ expect(findEmptyState().findComponent(GlButton).text()).toBe('Add a code quality job');
+ expect(findEmptyState().findComponent(GlButton).attributes('href')).toBe(
+ paths.codeQualityPagePath,
+ );
+ });
});
});
describe('when the ci_runner_templates experiment is active', () => {
beforeAll(() => {
getExperimentData.mockImplementation((name) => name === 'ci_runner_templates');
- getExperimentVariant.mockReturnValue('candidate');
});
- it('renders two buttons', () => {
- expect(findEmptyState().findAllComponents(GlButton).length).toBe(2);
- expect(findEmptyState().findAllComponents(GlButton).at(0).text()).toBe(
- 'Install GitLab Runners',
- );
- expect(findEmptyState().findAllComponents(GlButton).at(0).attributes('href')).toBe(
- paths.ciRunnerSettingsPath,
- );
- expect(findEmptyState().findAllComponents(GlButton).at(1).text()).toBe(
- 'Learn about Runners',
- );
- expect(findEmptyState().findAllComponents(GlButton).at(1).attributes('href')).toBe(
- '/help/ci/quick_start/index.md',
- );
+ describe('the control state', () => {
+ beforeAll(() => {
+ getExperimentVariant.mockReturnValue('control');
+ });
+
+ it('renders the CI/CD templates', () => {
+ expect(wrapper.find(PipelinesCiTemplates)).toExist();
+ });
+ });
+
+ describe('the candidate state', () => {
+ beforeAll(() => {
+ getExperimentVariant.mockReturnValue('candidate');
+ });
+
+ it('renders two buttons', () => {
+ expect(findEmptyState().findAllComponents(GlButton).length).toBe(2);
+ expect(findEmptyState().findAllComponents(GlButton).at(0).text()).toBe(
+ 'Install GitLab Runners',
+ );
+ expect(findEmptyState().findAllComponents(GlButton).at(0).attributes('href')).toBe(
+ paths.ciRunnerSettingsPath,
+ );
+ expect(findEmptyState().findAllComponents(GlButton).at(1).text()).toBe(
+ 'Learn about Runners',
+ );
+ expect(findEmptyState().findAllComponents(GlButton).at(1).attributes('href')).toBe(
+ '/help/ci/quick_start/index.md',
+ );
+ });
});
});
diff --git a/spec/frontend/profile/preferences/components/profile_preferences_spec.js b/spec/frontend/profile/preferences/components/profile_preferences_spec.js
index 9e6f5594d26..f1172a73d36 100644
--- a/spec/frontend/profile/preferences/components/profile_preferences_spec.js
+++ b/spec/frontend/profile/preferences/components/profile_preferences_spec.js
@@ -2,6 +2,7 @@ import { GlButton } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import createFlash from '~/flash';
import IntegrationView from '~/profile/preferences/components/integration_view.vue';
import ProfilePreferences from '~/profile/preferences/components/profile_preferences.vue';
import { i18n } from '~/profile/preferences/constants';
@@ -15,6 +16,7 @@ import {
lightModeThemeId2,
} from '../mock_data';
+jest.mock('~/flash');
const expectedUrl = '/foo';
describe('ProfilePreferences component', () => {
@@ -54,10 +56,6 @@ describe('ProfilePreferences component', () => {
return wrapper.findComponent(GlButton);
}
- function findFlashError() {
- return document.querySelector('.flash-container .flash-text');
- }
-
function createThemeInput(themeId = lightModeThemeId1) {
const input = document.createElement('input');
input.setAttribute('name', 'user[theme_id]');
@@ -82,10 +80,6 @@ describe('ProfilePreferences component', () => {
document.body.classList.add('content-wrapper');
}
- beforeEach(() => {
- setFixtures('<div class="flash-container"></div>');
- });
-
afterEach(() => {
wrapper.destroy();
wrapper = null;
@@ -152,7 +146,7 @@ describe('ProfilePreferences component', () => {
const successEvent = new CustomEvent('ajax:success');
form.dispatchEvent(successEvent);
- expect(findFlashError().innerText.trim()).toEqual(i18n.defaultSuccess);
+ expect(createFlash).toHaveBeenCalledWith({ message: i18n.defaultSuccess, type: 'notice' });
});
it('displays the custom success message', () => {
@@ -160,14 +154,14 @@ describe('ProfilePreferences component', () => {
const successEvent = new CustomEvent('ajax:success', { detail: [{ message }] });
form.dispatchEvent(successEvent);
- expect(findFlashError().innerText.trim()).toEqual(message);
+ expect(createFlash).toHaveBeenCalledWith({ message, type: 'notice' });
});
it('displays the default error message', () => {
const errorEvent = new CustomEvent('ajax:error');
form.dispatchEvent(errorEvent);
- expect(findFlashError().innerText.trim()).toEqual(i18n.defaultError);
+ expect(createFlash).toHaveBeenCalledWith({ message: i18n.defaultError, type: 'alert' });
});
it('displays the custom error message', () => {
@@ -175,7 +169,7 @@ describe('ProfilePreferences component', () => {
const errorEvent = new CustomEvent('ajax:error', { detail: [{ message }] });
form.dispatchEvent(errorEvent);
- expect(findFlashError().innerText.trim()).toEqual(message);
+ expect(createFlash).toHaveBeenCalledWith({ message, type: 'alert' });
});
});
diff --git a/spec/frontend/projects/commit/components/branches_dropdown_spec.js b/spec/frontend/projects/commit/components/branches_dropdown_spec.js
index ab84c3768d0..30556cdeae1 100644
--- a/spec/frontend/projects/commit/components/branches_dropdown_spec.js
+++ b/spec/frontend/projects/commit/components/branches_dropdown_spec.js
@@ -3,6 +3,7 @@ import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import Vuex from 'vuex';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
import BranchesDropdown from '~/projects/commit/components/branches_dropdown.vue';
Vue.use(Vuex);
@@ -82,7 +83,7 @@ describe('BranchesDropdown', () => {
expect(findSearchBoxByType().exists()).toBe(true);
expect(findSearchBoxByType().vm.$attrs).toMatchObject({
placeholder: 'Search branches',
- debounce: 250,
+ debounce: DEFAULT_DEBOUNCE_AND_THROTTLE_MS,
});
});
});
diff --git a/spec/frontend/projects/components/__snapshots__/project_delete_button_spec.js.snap b/spec/frontend/projects/components/__snapshots__/project_delete_button_spec.js.snap
index f0d72124379..c255fcce321 100644
--- a/spec/frontend/projects/components/__snapshots__/project_delete_button_spec.js.snap
+++ b/spec/frontend/projects/components/__snapshots__/project_delete_button_spec.js.snap
@@ -57,10 +57,6 @@ exports[`Project remove modal initialized matches the snapshot 1`] = `
/>
</gl-alert-stub>
- <p>
- This action cannot be undone. You will lose this project's repository and all related resources, including issues, merge requests, etc.
- </p>
-
<p
class="gl-mb-1"
>
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 c37f6415898..fc51825f15b 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
@@ -21,11 +21,7 @@ exports[`CiCdAnalyticsAreaChart matches the snapshot 1`] = `
option="[object Object]"
thresholds=""
width="0"
- >
- <template />
-
- <template />
- </glareachart-stub>
+ />
</div>
</div>
`;
diff --git a/spec/frontend/projects/settings/components/shared_runners_toggle_spec.js b/spec/frontend/projects/settings/components/shared_runners_toggle_spec.js
index 2d6efe7ae83..0c5bbe2a115 100644
--- a/spec/frontend/projects/settings/components/shared_runners_toggle_spec.js
+++ b/spec/frontend/projects/settings/components/shared_runners_toggle_spec.js
@@ -20,6 +20,7 @@ describe('projects/settings/components/shared_runners', () => {
isDisabledAndUnoverridable: false,
isLoading: false,
updatePath: TEST_UPDATE_PATH,
+ isCreditCardValidationRequired: false,
...props,
},
});
diff --git a/spec/frontend/projects/terraform_notification/terraform_notification_spec.js b/spec/frontend/projects/terraform_notification/terraform_notification_spec.js
new file mode 100644
index 00000000000..be34b207c4b
--- /dev/null
+++ b/spec/frontend/projects/terraform_notification/terraform_notification_spec.js
@@ -0,0 +1,62 @@
+import { GlBanner } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import { setCookie, parseBoolean } from '~/lib/utils/common_utils';
+import TerraformNotification from '~/projects/terraform_notification/components/terraform_notification.vue';
+
+jest.mock('~/lib/utils/common_utils');
+
+const bannerDissmisedKey = 'terraform_notification_dismissed_for_project_1';
+
+describe('TerraformNotificationBanner', () => {
+ let wrapper;
+
+ const propsData = {
+ projectId: 1,
+ };
+ const findBanner = () => wrapper.findComponent(GlBanner);
+
+ beforeEach(() => {
+ wrapper = shallowMount(TerraformNotification, {
+ propsData,
+ stubs: { GlBanner },
+ });
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ parseBoolean.mockReturnValue(false);
+ });
+
+ describe('when the dismiss cookie is set', () => {
+ beforeEach(() => {
+ parseBoolean.mockReturnValue(true);
+ wrapper = shallowMount(TerraformNotification, {
+ propsData,
+ });
+ });
+
+ it('should not render the banner', () => {
+ expect(findBanner().exists()).toBe(false);
+ });
+ });
+
+ describe('when the dismiss cookie is not set', () => {
+ it('should render the banner', () => {
+ expect(findBanner().exists()).toBe(true);
+ });
+ });
+
+ describe('when close button is clicked', () => {
+ beforeEach(async () => {
+ await findBanner().vm.$emit('close');
+ });
+
+ it('should set the cookie with the bannerDissmisedKey', () => {
+ expect(setCookie).toHaveBeenCalledWith(bannerDissmisedKey, true);
+ });
+
+ it('should remove the banner', () => {
+ expect(findBanner().exists()).toBe(false);
+ });
+ });
+});
diff --git a/spec/frontend/registry/explorer/components/list_page/cleanup_status_spec.js b/spec/frontend/registry/explorer/components/list_page/cleanup_status_spec.js
new file mode 100644
index 00000000000..c89bb874a7f
--- /dev/null
+++ b/spec/frontend/registry/explorer/components/list_page/cleanup_status_spec.js
@@ -0,0 +1,87 @@
+import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import CleanupStatus from '~/registry/explorer/components/list_page/cleanup_status.vue';
+import {
+ ASYNC_DELETE_IMAGE_ERROR_MESSAGE,
+ CLEANUP_STATUS_SCHEDULED,
+ CLEANUP_STATUS_ONGOING,
+ CLEANUP_STATUS_UNFINISHED,
+ UNFINISHED_STATUS,
+ UNSCHEDULED_STATUS,
+ SCHEDULED_STATUS,
+ ONGOING_STATUS,
+} from '~/registry/explorer/constants';
+
+describe('cleanup_status', () => {
+ let wrapper;
+
+ const findMainIcon = () => wrapper.findByTestId('main-icon');
+ const findExtraInfoIcon = () => wrapper.findByTestId('extra-info');
+
+ const mountComponent = (propsData = { status: SCHEDULED_STATUS }) => {
+ wrapper = shallowMountExtended(CleanupStatus, {
+ propsData,
+ directives: {
+ GlTooltip: createMockDirective(),
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it.each`
+ status | visible | text
+ ${UNFINISHED_STATUS} | ${true} | ${CLEANUP_STATUS_UNFINISHED}
+ ${SCHEDULED_STATUS} | ${true} | ${CLEANUP_STATUS_SCHEDULED}
+ ${ONGOING_STATUS} | ${true} | ${CLEANUP_STATUS_ONGOING}
+ ${UNSCHEDULED_STATUS} | ${false} | ${''}
+ `(
+ 'when the status is $status is $visible that the component is mounted and has the correct text',
+ ({ status, visible, text }) => {
+ mountComponent({ status });
+
+ expect(findMainIcon().exists()).toBe(visible);
+ expect(wrapper.text()).toBe(text);
+ },
+ );
+
+ describe('main icon', () => {
+ it('exists', () => {
+ mountComponent();
+
+ expect(findMainIcon().exists()).toBe(true);
+ });
+
+ it(`has the orange class when the status is ${UNFINISHED_STATUS}`, () => {
+ mountComponent({ status: UNFINISHED_STATUS });
+
+ expect(findMainIcon().classes('gl-text-orange-500')).toBe(true);
+ });
+ });
+
+ describe('extra info icon', () => {
+ it.each`
+ status | visible
+ ${UNFINISHED_STATUS} | ${true}
+ ${SCHEDULED_STATUS} | ${false}
+ ${ONGOING_STATUS} | ${false}
+ `(
+ 'when the status is $status is $visible that the extra icon is visible',
+ ({ status, visible }) => {
+ mountComponent({ status });
+
+ expect(findExtraInfoIcon().exists()).toBe(visible);
+ },
+ );
+
+ it(`has a tooltip`, () => {
+ mountComponent({ status: UNFINISHED_STATUS });
+
+ const tooltip = getBinding(findExtraInfoIcon().element, 'gl-tooltip');
+
+ expect(tooltip.value.title).toBe(ASYNC_DELETE_IMAGE_ERROR_MESSAGE);
+ });
+ });
+});
diff --git a/spec/frontend/registry/explorer/components/list_page/image_list_row_spec.js b/spec/frontend/registry/explorer/components/list_page/image_list_row_spec.js
index 323d7b177e7..db0f869ab52 100644
--- a/spec/frontend/registry/explorer/components/list_page/image_list_row_spec.js
+++ b/spec/frontend/registry/explorer/components/list_page/image_list_row_spec.js
@@ -3,15 +3,14 @@ import { shallowMount } from '@vue/test-utils';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import DeleteButton from '~/registry/explorer/components/delete_button.vue';
+import CleanupStatus from '~/registry/explorer/components/list_page/cleanup_status.vue';
import Component from '~/registry/explorer/components/list_page/image_list_row.vue';
import {
ROW_SCHEDULED_FOR_DELETION,
LIST_DELETE_BUTTON_DISABLED,
REMOVE_REPOSITORY_LABEL,
- ASYNC_DELETE_IMAGE_ERROR_MESSAGE,
- CLEANUP_TIMED_OUT_ERROR_MESSAGE,
IMAGE_DELETE_SCHEDULED_STATUS,
- IMAGE_FAILED_DELETED_STATUS,
+ SCHEDULED_STATUS,
ROOT_IMAGE_TEXT,
} from '~/registry/explorer/constants';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
@@ -27,7 +26,7 @@ describe('Image List Row', () => {
const findTagsCount = () => wrapper.find('[data-testid="tags-count"]');
const findDeleteBtn = () => wrapper.findComponent(DeleteButton);
const findClipboardButton = () => wrapper.findComponent(ClipboardButton);
- const findWarningIcon = () => wrapper.find('[data-testid="warning-icon"]');
+ const findCleanupStatus = () => wrapper.findComponent(CleanupStatus);
const findSkeletonLoader = () => wrapper.findComponent(GlSkeletonLoader);
const findListItemComponent = () => wrapper.findComponent(ListItem);
@@ -106,23 +105,22 @@ describe('Image List Row', () => {
expect(button.props('title')).toBe(item.location);
});
- describe('warning icon', () => {
+ describe('cleanup status component', () => {
it.each`
- status | expirationPolicyStartedAt | shown | title
- ${IMAGE_FAILED_DELETED_STATUS} | ${true} | ${true} | ${ASYNC_DELETE_IMAGE_ERROR_MESSAGE}
- ${''} | ${true} | ${true} | ${CLEANUP_TIMED_OUT_ERROR_MESSAGE}
- ${''} | ${false} | ${false} | ${''}
+ expirationPolicyCleanupStatus | shown
+ ${null} | ${false}
+ ${SCHEDULED_STATUS} | ${true}
`(
- 'when status is $status and expirationPolicyStartedAt is $expirationPolicyStartedAt',
- ({ expirationPolicyStartedAt, status, shown, title }) => {
- mountComponent({ item: { ...item, status, expirationPolicyStartedAt } });
+ 'when expirationPolicyCleanupStatus is $expirationPolicyCleanupStatus it is $shown that the component exists',
+ ({ expirationPolicyCleanupStatus, shown }) => {
+ mountComponent({ item: { ...item, expirationPolicyCleanupStatus } });
- const icon = findWarningIcon();
- expect(icon.exists()).toBe(shown);
+ expect(findCleanupStatus().exists()).toBe(shown);
if (shown) {
- const tooltip = getBinding(icon.element, 'gl-tooltip');
- expect(tooltip.value.title).toBe(title);
+ expect(findCleanupStatus().props()).toMatchObject({
+ status: expirationPolicyCleanupStatus,
+ });
}
},
);
diff --git a/spec/frontend/registry/explorer/mock_data.js b/spec/frontend/registry/explorer/mock_data.js
index fe258dcd4e8..27246cf2364 100644
--- a/spec/frontend/registry/explorer/mock_data.js
+++ b/spec/frontend/registry/explorer/mock_data.js
@@ -9,6 +9,7 @@ export const imagesListResponse = [
canDelete: true,
createdAt: '2020-11-03T13:29:21Z',
expirationPolicyStartedAt: null,
+ expirationPolicyCleanupStatus: 'UNSCHEDULED',
},
{
__typename: 'ContainerRepository',
@@ -20,6 +21,7 @@ export const imagesListResponse = [
canDelete: true,
createdAt: '2020-09-21T06:57:43Z',
expirationPolicyStartedAt: null,
+ expirationPolicyCleanupStatus: 'UNSCHEDULED',
},
];
diff --git a/spec/frontend/releases/__snapshots__/util_spec.js.snap b/spec/frontend/releases/__snapshots__/util_spec.js.snap
index e0a1343c39c..b2580d47549 100644
--- a/spec/frontend/releases/__snapshots__/util_spec.js.snap
+++ b/spec/frontend/releases/__snapshots__/util_spec.js.snap
@@ -5,6 +5,7 @@ Object {
"data": Array [
Object {
"_links": Object {
+ "__typename": "ReleaseLinks",
"closedIssuesUrl": "http://localhost/releases-namespace/releases-project/-/issues?release_tag=v1.2&scope=all&state=closed",
"closedMergeRequestsUrl": "http://localhost/releases-namespace/releases-project/-/merge_requests?release_tag=v1.2&scope=all&state=closed",
"editUrl": "http://localhost/releases-namespace/releases-project/-/releases/v1.2/edit",
@@ -19,24 +20,29 @@ Object {
"links": Array [],
"sources": Array [
Object {
+ "__typename": "ReleaseSource",
"format": "zip",
"url": "http://localhost/releases-namespace/releases-project/-/archive/v1.2/releases-project-v1.2.zip",
},
Object {
+ "__typename": "ReleaseSource",
"format": "tar.gz",
"url": "http://localhost/releases-namespace/releases-project/-/archive/v1.2/releases-project-v1.2.tar.gz",
},
Object {
+ "__typename": "ReleaseSource",
"format": "tar.bz2",
"url": "http://localhost/releases-namespace/releases-project/-/archive/v1.2/releases-project-v1.2.tar.bz2",
},
Object {
+ "__typename": "ReleaseSource",
"format": "tar",
"url": "http://localhost/releases-namespace/releases-project/-/archive/v1.2/releases-project-v1.2.tar",
},
],
},
"author": Object {
+ "__typename": "UserCore",
"avatarUrl": "https://www.gravatar.com/avatar/16f8e2050ce10180ca571c2eb19cfce2?s=80&d=identicon",
"username": "administrator",
"webUrl": "http://localhost/administrator",
@@ -57,6 +63,7 @@ Object {
},
Object {
"_links": Object {
+ "__typename": "ReleaseLinks",
"closedIssuesUrl": "http://localhost/releases-namespace/releases-project/-/issues?release_tag=v1.1&scope=all&state=closed",
"closedMergeRequestsUrl": "http://localhost/releases-namespace/releases-project/-/merge_requests?release_tag=v1.1&scope=all&state=closed",
"editUrl": "http://localhost/releases-namespace/releases-project/-/releases/v1.1/edit",
@@ -70,6 +77,7 @@ Object {
"count": 8,
"links": Array [
Object {
+ "__typename": "ReleaseAssetLink",
"directAssetUrl": "http://localhost/releases-namespace/releases-project/-/releases/v1.1/downloads/binaries/awesome-app-3",
"external": true,
"id": "gid://gitlab/Releases::Link/13",
@@ -78,6 +86,7 @@ Object {
"url": "https://example.com/image",
},
Object {
+ "__typename": "ReleaseAssetLink",
"directAssetUrl": "http://localhost/releases-namespace/releases-project/-/releases/v1.1/downloads/binaries/awesome-app-2",
"external": true,
"id": "gid://gitlab/Releases::Link/12",
@@ -86,6 +95,7 @@ Object {
"url": "https://example.com/package",
},
Object {
+ "__typename": "ReleaseAssetLink",
"directAssetUrl": "http://localhost/releases-namespace/releases-project/-/releases/v1.1/downloads/binaries/awesome-app-1",
"external": false,
"id": "gid://gitlab/Releases::Link/11",
@@ -94,6 +104,7 @@ Object {
"url": "http://localhost/releases-namespace/releases-project/runbook",
},
Object {
+ "__typename": "ReleaseAssetLink",
"directAssetUrl": "http://localhost/releases-namespace/releases-project/-/releases/v1.1/downloads/binaries/linux-amd64",
"external": true,
"id": "gid://gitlab/Releases::Link/10",
@@ -104,24 +115,29 @@ Object {
],
"sources": Array [
Object {
+ "__typename": "ReleaseSource",
"format": "zip",
"url": "http://localhost/releases-namespace/releases-project/-/archive/v1.1/releases-project-v1.1.zip",
},
Object {
+ "__typename": "ReleaseSource",
"format": "tar.gz",
"url": "http://localhost/releases-namespace/releases-project/-/archive/v1.1/releases-project-v1.1.tar.gz",
},
Object {
+ "__typename": "ReleaseSource",
"format": "tar.bz2",
"url": "http://localhost/releases-namespace/releases-project/-/archive/v1.1/releases-project-v1.1.tar.bz2",
},
Object {
+ "__typename": "ReleaseSource",
"format": "tar",
"url": "http://localhost/releases-namespace/releases-project/-/archive/v1.1/releases-project-v1.1.tar",
},
],
},
"author": Object {
+ "__typename": "UserCore",
"avatarUrl": "https://www.gravatar.com/avatar/16f8e2050ce10180ca571c2eb19cfce2?s=80&d=identicon",
"username": "administrator",
"webUrl": "http://localhost/administrator",
@@ -134,6 +150,7 @@ Object {
"descriptionHtml": "<p data-sourcepos=\\"1:1-1:33\\" dir=\\"auto\\">Best. Release. <strong>Ever.</strong> <gl-emoji title=\\"rocket\\" data-name=\\"rocket\\" data-unicode-version=\\"6.0\\">🚀</gl-emoji></p>",
"evidences": Array [
Object {
+ "__typename": "ReleaseEvidence",
"collectedAt": "2018-12-03T00:00:00Z",
"filepath": "http://localhost/releases-namespace/releases-project/-/releases/v1.1/evidences/1.json",
"sha": "760d6cdfb0879c3ffedec13af470e0f71cf52c6cde4d",
@@ -141,6 +158,7 @@ Object {
],
"milestones": Array [
Object {
+ "__typename": "Milestone",
"description": "The 12.3 milestone",
"id": "gid://gitlab/Milestone/123",
"issueStats": Object {
@@ -153,6 +171,7 @@ Object {
"webUrl": "/releases-namespace/releases-project/-/milestones/1",
},
Object {
+ "__typename": "Milestone",
"description": "The 12.4 milestone",
"id": "gid://gitlab/Milestone/124",
"issueStats": Object {
@@ -173,6 +192,7 @@ Object {
},
],
"paginationInfo": Object {
+ "__typename": "PageInfo",
"endCursor": "eyJyZWxlYXNlZF9hdCI6IjIwMTgtMTItMTAgMDA6MDA6MDAuMDAwMDAwMDAwIFVUQyIsImlkIjoiMSJ9",
"hasNextPage": false,
"hasPreviousPage": false,
@@ -192,24 +212,28 @@ Object {
"count": undefined,
"links": Array [
Object {
+ "directAssetPath": "/binaries/awesome-app-3",
"id": "gid://gitlab/Releases::Link/13",
"linkType": "image",
"name": "Image",
"url": "https://example.com/image",
},
Object {
+ "directAssetPath": "/binaries/awesome-app-2",
"id": "gid://gitlab/Releases::Link/12",
"linkType": "package",
"name": "Package",
"url": "https://example.com/package",
},
Object {
+ "directAssetPath": "/binaries/awesome-app-1",
"id": "gid://gitlab/Releases::Link/11",
"linkType": "runbook",
"name": "Runbook",
"url": "http://localhost/releases-namespace/releases-project/runbook",
},
Object {
+ "directAssetPath": "/binaries/linux-amd64",
"id": "gid://gitlab/Releases::Link/10",
"linkType": "other",
"name": "linux-amd64 binaries",
@@ -247,6 +271,7 @@ exports[`releases/util.js convertOneReleaseGraphQLResponse matches snapshot 1`]
Object {
"data": Object {
"_links": Object {
+ "__typename": "ReleaseLinks",
"closedIssuesUrl": "http://localhost/releases-namespace/releases-project/-/issues?release_tag=v1.1&scope=all&state=closed",
"closedMergeRequestsUrl": "http://localhost/releases-namespace/releases-project/-/merge_requests?release_tag=v1.1&scope=all&state=closed",
"editUrl": "http://localhost/releases-namespace/releases-project/-/releases/v1.1/edit",
@@ -260,6 +285,7 @@ Object {
"count": 8,
"links": Array [
Object {
+ "__typename": "ReleaseAssetLink",
"directAssetUrl": "http://localhost/releases-namespace/releases-project/-/releases/v1.1/downloads/binaries/awesome-app-3",
"external": true,
"id": "gid://gitlab/Releases::Link/13",
@@ -268,6 +294,7 @@ Object {
"url": "https://example.com/image",
},
Object {
+ "__typename": "ReleaseAssetLink",
"directAssetUrl": "http://localhost/releases-namespace/releases-project/-/releases/v1.1/downloads/binaries/awesome-app-2",
"external": true,
"id": "gid://gitlab/Releases::Link/12",
@@ -276,6 +303,7 @@ Object {
"url": "https://example.com/package",
},
Object {
+ "__typename": "ReleaseAssetLink",
"directAssetUrl": "http://localhost/releases-namespace/releases-project/-/releases/v1.1/downloads/binaries/awesome-app-1",
"external": false,
"id": "gid://gitlab/Releases::Link/11",
@@ -284,6 +312,7 @@ Object {
"url": "http://localhost/releases-namespace/releases-project/runbook",
},
Object {
+ "__typename": "ReleaseAssetLink",
"directAssetUrl": "http://localhost/releases-namespace/releases-project/-/releases/v1.1/downloads/binaries/linux-amd64",
"external": true,
"id": "gid://gitlab/Releases::Link/10",
@@ -294,24 +323,29 @@ Object {
],
"sources": Array [
Object {
+ "__typename": "ReleaseSource",
"format": "zip",
"url": "http://localhost/releases-namespace/releases-project/-/archive/v1.1/releases-project-v1.1.zip",
},
Object {
+ "__typename": "ReleaseSource",
"format": "tar.gz",
"url": "http://localhost/releases-namespace/releases-project/-/archive/v1.1/releases-project-v1.1.tar.gz",
},
Object {
+ "__typename": "ReleaseSource",
"format": "tar.bz2",
"url": "http://localhost/releases-namespace/releases-project/-/archive/v1.1/releases-project-v1.1.tar.bz2",
},
Object {
+ "__typename": "ReleaseSource",
"format": "tar",
"url": "http://localhost/releases-namespace/releases-project/-/archive/v1.1/releases-project-v1.1.tar",
},
],
},
"author": Object {
+ "__typename": "UserCore",
"avatarUrl": "https://www.gravatar.com/avatar/16f8e2050ce10180ca571c2eb19cfce2?s=80&d=identicon",
"username": "administrator",
"webUrl": "http://localhost/administrator",
@@ -324,6 +358,7 @@ Object {
"descriptionHtml": "<p data-sourcepos=\\"1:1-1:33\\" dir=\\"auto\\">Best. Release. <strong>Ever.</strong> <gl-emoji title=\\"rocket\\" data-name=\\"rocket\\" data-unicode-version=\\"6.0\\">🚀</gl-emoji></p>",
"evidences": Array [
Object {
+ "__typename": "ReleaseEvidence",
"collectedAt": "2018-12-03T00:00:00Z",
"filepath": "http://localhost/releases-namespace/releases-project/-/releases/v1.1/evidences/1.json",
"sha": "760d6cdfb0879c3ffedec13af470e0f71cf52c6cde4d",
@@ -331,6 +366,7 @@ Object {
],
"milestones": Array [
Object {
+ "__typename": "Milestone",
"description": "The 12.3 milestone",
"id": "gid://gitlab/Milestone/123",
"issueStats": Object {
@@ -343,6 +379,7 @@ Object {
"webUrl": "/releases-namespace/releases-project/-/milestones/1",
},
Object {
+ "__typename": "Milestone",
"description": "The 12.4 milestone",
"id": "gid://gitlab/Milestone/124",
"issueStats": Object {
diff --git a/spec/frontend/releases/components/app_index_apollo_client_spec.js b/spec/frontend/releases/components/app_index_apollo_client_spec.js
index 002d8939058..096d319c82f 100644
--- a/spec/frontend/releases/components/app_index_apollo_client_spec.js
+++ b/spec/frontend/releases/components/app_index_apollo_client_spec.js
@@ -3,6 +3,7 @@ import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import allReleasesQuery from 'shared_queries/releases/all_releases.query.graphql';
import createFlash from '~/flash';
import { historyPushState } from '~/lib/utils/common_utils';
import ReleasesIndexApolloClientApp from '~/releases/components/app_index_apollo_client.vue';
@@ -12,7 +13,6 @@ import ReleasesEmptyState from '~/releases/components/releases_empty_state.vue';
import ReleasesPaginationApolloClient from '~/releases/components/releases_pagination_apollo_client.vue';
import ReleasesSortApolloClient from '~/releases/components/releases_sort_apollo_client.vue';
import { PAGE_SIZE, CREATED_ASC, DEFAULT_SORT } from '~/releases/constants';
-import allReleasesQuery from '~/releases/graphql/queries/all_releases.query.graphql';
Vue.use(VueApollo);
@@ -21,10 +21,14 @@ jest.mock('~/flash');
let mockQueryParams;
jest.mock('~/lib/utils/common_utils', () => ({
...jest.requireActual('~/lib/utils/common_utils'),
+ historyPushState: jest.fn(),
+}));
+
+jest.mock('~/lib/utils/url_utility', () => ({
+ ...jest.requireActual('~/lib/utils/url_utility'),
getParameterByName: jest
.fn()
.mockImplementation((parameterName) => mockQueryParams[parameterName]),
- historyPushState: jest.fn(),
}));
describe('app_index_apollo_client.vue', () => {
diff --git a/spec/frontend/releases/components/app_index_spec.js b/spec/frontend/releases/components/app_index_spec.js
index 3a28020c284..43e88650ae3 100644
--- a/spec/frontend/releases/components/app_index_spec.js
+++ b/spec/frontend/releases/components/app_index_spec.js
@@ -2,14 +2,14 @@ import { shallowMount } from '@vue/test-utils';
import { merge } from 'lodash';
import Vue from 'vue';
import Vuex from 'vuex';
-import { getParameterByName } from '~/lib/utils/common_utils';
+import { getParameterByName } from '~/lib/utils/url_utility';
import AppIndex from '~/releases/components/app_index.vue';
import ReleaseSkeletonLoader from '~/releases/components/release_skeleton_loader.vue';
import ReleasesPagination from '~/releases/components/releases_pagination.vue';
import ReleasesSort from '~/releases/components/releases_sort.vue';
-jest.mock('~/lib/utils/common_utils', () => ({
- ...jest.requireActual('~/lib/utils/common_utils'),
+jest.mock('~/lib/utils/url_utility', () => ({
+ ...jest.requireActual('~/lib/utils/url_utility'),
getParameterByName: jest.fn(),
}));
diff --git a/spec/frontend/reports/components/__snapshots__/grouped_issues_list_spec.js.snap b/spec/frontend/reports/components/__snapshots__/grouped_issues_list_spec.js.snap
index c932379a253..111757e2d30 100644
--- a/spec/frontend/reports/components/__snapshots__/grouped_issues_list_spec.js.snap
+++ b/spec/frontend/reports/components/__snapshots__/grouped_issues_list_spec.js.snap
@@ -14,6 +14,7 @@ Object {
exports[`Grouped Issues List with data renders a report item with the correct props 1`] = `
Object {
"component": "TestIssueBody",
+ "iconComponent": "IssueStatusIcon",
"isNew": false,
"issue": Object {
"name": "foo",
diff --git a/spec/frontend/reports/grouped_test_report/grouped_test_reports_app_spec.js b/spec/frontend/reports/grouped_test_report/grouped_test_reports_app_spec.js
index d29048d640c..0f7c2559e8b 100644
--- a/spec/frontend/reports/grouped_test_report/grouped_test_reports_app_spec.js
+++ b/spec/frontend/reports/grouped_test_report/grouped_test_reports_app_spec.js
@@ -114,7 +114,7 @@ describe('Grouped test reports app', () => {
setReports(newFailedTestReports);
});
- it('tracks usage ping metric when enabled', () => {
+ it('tracks service ping metric when enabled', () => {
mountComponent({ glFeatures: { usageDataITestingSummaryWidgetTotal: true } });
findExpandButton().trigger('click');
@@ -132,7 +132,7 @@ describe('Grouped test reports app', () => {
expect(Api.trackRedisHllUserEvent).toHaveBeenCalledTimes(1);
});
- it('does not track usage ping metric when disabled', () => {
+ it('does not track service ping metric when disabled', () => {
mountComponent({ glFeatures: { usageDataITestingSummaryWidgetTotal: false } });
findExpandButton().trigger('click');
diff --git a/spec/frontend/repository/components/blob_button_group_spec.js b/spec/frontend/repository/components/blob_button_group_spec.js
new file mode 100644
index 00000000000..a449fd6f06c
--- /dev/null
+++ b/spec/frontend/repository/components/blob_button_group_spec.js
@@ -0,0 +1,117 @@
+import { GlButton } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
+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';
+
+const DEFAULT_PROPS = {
+ name: 'some name',
+ path: 'some/path',
+ canPushCode: true,
+ replacePath: 'some/replace/path',
+ deletePath: 'some/delete/path',
+ emptyRepo: false,
+};
+
+const DEFAULT_INJECT = {
+ targetBranch: 'master',
+ originalBranch: 'master',
+};
+
+describe('BlobButtonGroup component', () => {
+ let wrapper;
+
+ const createComponent = (props = {}) => {
+ wrapper = shallowMount(BlobButtonGroup, {
+ propsData: {
+ ...DEFAULT_PROPS,
+ ...props,
+ },
+ provide: {
+ ...DEFAULT_INJECT,
+ },
+ directives: {
+ GlModal: createMockDirective(),
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ const findDeleteBlobModal = () => wrapper.findComponent(DeleteBlobModal);
+ const findUploadBlobModal = () => wrapper.findComponent(UploadBlobModal);
+ const findReplaceButton = () => wrapper.findAll(GlButton).at(0);
+
+ it('renders component', () => {
+ createComponent();
+
+ const { name, path } = DEFAULT_PROPS;
+
+ expect(wrapper.props()).toMatchObject({
+ name,
+ path,
+ });
+ });
+
+ describe('buttons', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders both the replace and delete button', () => {
+ expect(wrapper.findAll(GlButton)).toHaveLength(2);
+ });
+
+ it('renders the buttons in the correct order', () => {
+ expect(wrapper.findAll(GlButton).at(0).text()).toBe('Replace');
+ expect(wrapper.findAll(GlButton).at(1).text()).toBe('Delete');
+ });
+
+ it('triggers the UploadBlobModal from the replace button', () => {
+ const { value } = getBinding(findReplaceButton().element, 'gl-modal');
+ const modalId = findUploadBlobModal().props('modalId');
+
+ expect(modalId).toEqual(value);
+ });
+ });
+
+ it('renders UploadBlobModal', () => {
+ createComponent();
+
+ const { targetBranch, originalBranch } = DEFAULT_INJECT;
+ const { name, path, canPushCode, replacePath } = DEFAULT_PROPS;
+ const title = `Replace ${name}`;
+
+ expect(findUploadBlobModal().props()).toMatchObject({
+ modalTitle: title,
+ commitMessage: title,
+ targetBranch,
+ originalBranch,
+ canPushCode,
+ path,
+ replacePath,
+ primaryBtnText: 'Replace file',
+ });
+ });
+
+ it('renders DeleteBlobModel', () => {
+ createComponent();
+
+ const { targetBranch, originalBranch } = DEFAULT_INJECT;
+ const { name, canPushCode, deletePath, emptyRepo } = DEFAULT_PROPS;
+ const title = `Delete ${name}`;
+
+ expect(findDeleteBlobModal().props()).toMatchObject({
+ modalTitle: title,
+ commitMessage: title,
+ targetBranch,
+ originalBranch,
+ canPushCode,
+ deletePath,
+ emptyRepo,
+ });
+ });
+});
diff --git a/spec/frontend/repository/components/blob_content_viewer_spec.js b/spec/frontend/repository/components/blob_content_viewer_spec.js
index 495039b4ccb..a83d0a607f2 100644
--- a/spec/frontend/repository/components/blob_content_viewer_spec.js
+++ b/spec/frontend/repository/components/blob_content_viewer_spec.js
@@ -1,11 +1,23 @@
import { GlLoadingIcon } from '@gitlab/ui';
-import { shallowMount, mount } from '@vue/test-utils';
+import { shallowMount, mount, createLocalVue } from '@vue/test-utils';
+import axios from 'axios';
+import MockAdapter from 'axios-mock-adapter';
import { nextTick } from 'vue';
+import VueApollo from 'vue-apollo';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
import BlobContent from '~/blob/components/blob_content.vue';
import BlobHeader from '~/blob/components/blob_header.vue';
+import BlobButtonGroup from '~/repository/components/blob_button_group.vue';
import BlobContentViewer from '~/repository/components/blob_content_viewer.vue';
-import BlobHeaderEdit from '~/repository/components/blob_header_edit.vue';
-import BlobReplace from '~/repository/components/blob_replace.vue';
+import BlobEdit from '~/repository/components/blob_edit.vue';
+import { loadViewer, viewerProps } from '~/repository/components/blob_viewers';
+import DownloadViewer from '~/repository/components/blob_viewers/download_viewer.vue';
+import EmptyViewer from '~/repository/components/blob_viewers/empty_viewer.vue';
+import TextViewer from '~/repository/components/blob_viewers/text_viewer.vue';
+import blobInfoQuery from '~/repository/queries/blob_info.query.graphql';
+
+jest.mock('~/repository/components/blob_viewers');
let wrapper;
const simpleMockData = {
@@ -17,6 +29,7 @@ const simpleMockData = {
fileType: 'text',
tooLarge: false,
path: 'some_file.js',
+ webPath: 'some_file.js',
editBlobPath: 'some_file.js/edit',
ideEditPath: 'some_file.js/ide/edit',
storedExternally: false,
@@ -27,7 +40,6 @@ const simpleMockData = {
canLock: true,
isLocked: false,
lockLink: 'some_file.js/lock',
- canModifyBlob: true,
forkPath: 'some_file.js/fork',
simpleViewer: {
fileType: 'text',
@@ -47,6 +59,51 @@ const richMockData = {
},
};
+const projectMockData = {
+ userPermissions: {
+ pushCode: true,
+ },
+ repository: {
+ empty: false,
+ },
+};
+
+const localVue = createLocalVue();
+const mockAxios = new MockAdapter(axios);
+
+const createComponentWithApollo = (mockData = {}) => {
+ localVue.use(VueApollo);
+
+ const defaultPushCode = projectMockData.userPermissions.pushCode;
+ const defaultEmptyRepo = projectMockData.repository.empty;
+ const { blobs, emptyRepo = defaultEmptyRepo, canPushCode = defaultPushCode } = mockData;
+
+ const mockResolver = jest.fn().mockResolvedValue({
+ data: {
+ project: {
+ userPermissions: { pushCode: canPushCode },
+ repository: {
+ empty: emptyRepo,
+ blobs: {
+ nodes: [blobs],
+ },
+ },
+ },
+ },
+ });
+
+ const fakeApollo = createMockApollo([[blobInfoQuery, mockResolver]]);
+
+ wrapper = shallowMount(BlobContentViewer, {
+ localVue,
+ apolloProvider: fakeApollo,
+ propsData: {
+ path: 'some_file.js',
+ projectPath: 'some/path',
+ },
+ });
+};
+
const createFactory = (mountFn) => (
{ props = {}, mockData = {}, stubs = {} } = {},
loading = false,
@@ -78,9 +135,9 @@ const fullFactory = createFactory(mount);
describe('Blob content viewer component', () => {
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findBlobHeader = () => wrapper.findComponent(BlobHeader);
- const findBlobHeaderEdit = () => wrapper.findComponent(BlobHeaderEdit);
+ const findBlobEdit = () => wrapper.findComponent(BlobEdit);
const findBlobContent = () => wrapper.findComponent(BlobContent);
- const findBlobReplace = () => wrapper.findComponent(BlobReplace);
+ const findBlobButtonGroup = () => wrapper.findComponent(BlobButtonGroup);
afterEach(() => {
wrapper.destroy();
@@ -163,6 +220,67 @@ describe('Blob content viewer component', () => {
});
});
+ describe('legacy viewers', () => {
+ it('does not load a legacy viewer when a rich viewer is not available', async () => {
+ createComponentWithApollo({ blobs: simpleMockData });
+ await waitForPromises();
+
+ expect(mockAxios.history.get).toHaveLength(0);
+ });
+
+ it('loads a legacy viewer when a rich viewer is available', async () => {
+ createComponentWithApollo({ blobs: richMockData });
+ await waitForPromises();
+
+ expect(mockAxios.history.get).toHaveLength(1);
+ });
+ });
+
+ describe('Blob viewer', () => {
+ afterEach(() => {
+ loadViewer.mockRestore();
+ viewerProps.mockRestore();
+ });
+
+ it('does not render a BlobContent component if a Blob viewer is available', () => {
+ loadViewer.mockReturnValueOnce(() => true);
+ factory({ mockData: { blobInfo: richMockData } });
+
+ expect(findBlobContent().exists()).toBe(false);
+ });
+
+ it.each`
+ viewer | loadViewerReturnValue | viewerPropsReturnValue
+ ${'empty'} | ${EmptyViewer} | ${{}}
+ ${'download'} | ${DownloadViewer} | ${{ filePath: '/some/file/path', fileName: 'test.js', fileSize: 100 }}
+ ${'text'} | ${TextViewer} | ${{ content: 'test', fileName: 'test.js', readOnly: true }}
+ `(
+ 'renders viewer component for $viewer files',
+ async ({ viewer, loadViewerReturnValue, viewerPropsReturnValue }) => {
+ loadViewer.mockReturnValue(loadViewerReturnValue);
+ viewerProps.mockReturnValue(viewerPropsReturnValue);
+
+ factory({
+ mockData: {
+ blobInfo: {
+ ...simpleMockData,
+ fileType: null,
+ simpleViewer: {
+ ...simpleMockData.simpleViewer,
+ fileType: viewer,
+ },
+ },
+ },
+ });
+
+ await nextTick();
+
+ expect(loadViewer).toHaveBeenCalledWith(viewer);
+ expect(wrapper.findComponent(loadViewerReturnValue).exists()).toBe(true);
+ },
+ );
+ });
+
describe('BlobHeader action slot', () => {
const { ideEditPath, editBlobPath } = simpleMockData;
@@ -177,7 +295,7 @@ describe('Blob content viewer component', () => {
await nextTick();
- expect(findBlobHeaderEdit().props()).toMatchObject({
+ expect(findBlobEdit().props()).toMatchObject({
editPath: editBlobPath,
webIdePath: ideEditPath,
});
@@ -194,31 +312,56 @@ describe('Blob content viewer component', () => {
await nextTick();
- expect(findBlobHeaderEdit().props()).toMatchObject({
+ expect(findBlobEdit().props()).toMatchObject({
editPath: editBlobPath,
webIdePath: ideEditPath,
});
});
- describe('BlobReplace', () => {
- const { name, path } = simpleMockData;
+ it('does not render BlobHeaderEdit button when viewing a binary file', async () => {
+ fullFactory({
+ mockData: { blobInfo: richMockData, isBinary: true },
+ stubs: {
+ BlobContent: true,
+ BlobReplace: true,
+ },
+ });
+
+ await nextTick();
+
+ expect(findBlobEdit().exists()).toBe(false);
+ });
+
+ describe('BlobButtonGroup', () => {
+ const { name, path, replacePath, webPath } = simpleMockData;
+ const {
+ userPermissions: { pushCode },
+ repository: { empty },
+ } = projectMockData;
it('renders component', async () => {
window.gon.current_user_id = 1;
fullFactory({
- mockData: { blobInfo: simpleMockData },
+ mockData: {
+ blobInfo: simpleMockData,
+ project: { userPermissions: { pushCode }, repository: { empty } },
+ },
stubs: {
BlobContent: true,
- BlobReplace: true,
+ BlobButtonGroup: true,
},
});
await nextTick();
- expect(findBlobReplace().props()).toMatchObject({
+ expect(findBlobButtonGroup().props()).toMatchObject({
name,
path,
+ replacePath,
+ deletePath: webPath,
+ canPushCode: pushCode,
+ emptyRepo: empty,
});
});
@@ -235,7 +378,7 @@ describe('Blob content viewer component', () => {
await nextTick();
- expect(findBlobReplace().exists()).toBe(false);
+ expect(findBlobButtonGroup().exists()).toBe(false);
});
});
});
diff --git a/spec/frontend/repository/components/blob_header_edit_spec.js b/spec/frontend/repository/components/blob_edit_spec.js
index c0eb7c523c4..e6e69cd8549 100644
--- a/spec/frontend/repository/components/blob_header_edit_spec.js
+++ b/spec/frontend/repository/components/blob_edit_spec.js
@@ -1,6 +1,6 @@
import { GlButton } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import BlobHeaderEdit from '~/repository/components/blob_header_edit.vue';
+import BlobEdit from '~/repository/components/blob_edit.vue';
import WebIdeLink from '~/vue_shared/components/web_ide_link.vue';
const DEFAULT_PROPS = {
@@ -8,11 +8,11 @@ const DEFAULT_PROPS = {
webIdePath: 'some_file.js/ide/edit',
};
-describe('BlobHeaderEdit component', () => {
+describe('BlobEdit component', () => {
let wrapper;
const createComponent = (consolidatedEditButton = false, props = {}) => {
- wrapper = shallowMount(BlobHeaderEdit, {
+ wrapper = shallowMount(BlobEdit, {
propsData: {
...DEFAULT_PROPS,
...props,
diff --git a/spec/frontend/repository/components/blob_replace_spec.js b/spec/frontend/repository/components/blob_replace_spec.js
deleted file mode 100644
index 4a6f147da22..00000000000
--- a/spec/frontend/repository/components/blob_replace_spec.js
+++ /dev/null
@@ -1,67 +0,0 @@
-import { shallowMount } from '@vue/test-utils';
-import BlobReplace from '~/repository/components/blob_replace.vue';
-import UploadBlobModal from '~/repository/components/upload_blob_modal.vue';
-
-const DEFAULT_PROPS = {
- name: 'some name',
- path: 'some/path',
- canPushCode: true,
- replacePath: 'some/replace/path',
-};
-
-const DEFAULT_INJECT = {
- targetBranch: 'master',
- originalBranch: 'master',
-};
-
-describe('BlobReplace component', () => {
- let wrapper;
-
- const createComponent = (props = {}) => {
- wrapper = shallowMount(BlobReplace, {
- propsData: {
- ...DEFAULT_PROPS,
- ...props,
- },
- provide: {
- ...DEFAULT_INJECT,
- },
- });
- };
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- const findUploadBlobModal = () => wrapper.findComponent(UploadBlobModal);
-
- it('renders component', () => {
- createComponent();
-
- const { name, path } = DEFAULT_PROPS;
-
- expect(wrapper.props()).toMatchObject({
- name,
- path,
- });
- });
-
- it('renders UploadBlobModal', () => {
- createComponent();
-
- const { targetBranch, originalBranch } = DEFAULT_INJECT;
- const { name, path, canPushCode, replacePath } = DEFAULT_PROPS;
- const title = `Replace ${name}`;
-
- expect(findUploadBlobModal().props()).toMatchObject({
- modalTitle: title,
- commitMessage: title,
- targetBranch,
- originalBranch,
- canPushCode,
- path,
- replacePath,
- primaryBtnText: 'Replace file',
- });
- });
-});
diff --git a/spec/frontend/repository/components/blob_viewers/__snapshots__/empty_viewer_spec.js.snap b/spec/frontend/repository/components/blob_viewers/__snapshots__/empty_viewer_spec.js.snap
new file mode 100644
index 00000000000..e702ea5fd00
--- /dev/null
+++ b/spec/frontend/repository/components/blob_viewers/__snapshots__/empty_viewer_spec.js.snap
@@ -0,0 +1,9 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Empty Viewer matches the snapshot 1`] = `
+<div
+ class="nothing-here-block"
+>
+ Empty file
+</div>
+`;
diff --git a/spec/frontend/repository/components/blob_viewers/download_viewer_spec.js b/spec/frontend/repository/components/blob_viewers/download_viewer_spec.js
new file mode 100644
index 00000000000..c71b2b3c55c
--- /dev/null
+++ b/spec/frontend/repository/components/blob_viewers/download_viewer_spec.js
@@ -0,0 +1,70 @@
+import { GlLink, GlIcon } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import { numberToHumanSize } from '~/lib/utils/number_utils';
+import DownloadViewer from '~/repository/components/blob_viewers/download_viewer.vue';
+
+describe('Text Viewer', () => {
+ let wrapper;
+
+ const DEFAULT_PROPS = {
+ fileName: 'file_name.js',
+ filePath: '/some/file/path',
+ fileSize: 2269674,
+ };
+
+ const createComponent = (props = {}) => {
+ wrapper = shallowMount(DownloadViewer, {
+ propsData: {
+ ...DEFAULT_PROPS,
+ ...props,
+ },
+ });
+ };
+
+ it('renders component', () => {
+ createComponent();
+
+ const { fileName, filePath, fileSize } = DEFAULT_PROPS;
+ expect(wrapper.props()).toMatchObject({
+ fileName,
+ filePath,
+ fileSize,
+ });
+ });
+
+ it('renders download human readable file size text', () => {
+ createComponent();
+
+ const downloadText = `Download (${numberToHumanSize(DEFAULT_PROPS.fileSize)})`;
+ expect(wrapper.text()).toBe(downloadText);
+ });
+
+ it('renders download text', () => {
+ createComponent({
+ fileSize: 0,
+ });
+
+ expect(wrapper.text()).toBe('Download');
+ });
+
+ it('renders download link', () => {
+ createComponent();
+ const { filePath, fileName } = DEFAULT_PROPS;
+
+ expect(wrapper.findComponent(GlLink).attributes()).toMatchObject({
+ rel: 'nofollow',
+ target: '_blank',
+ href: filePath,
+ download: fileName,
+ });
+ });
+
+ it('renders download icon', () => {
+ createComponent();
+
+ expect(wrapper.findComponent(GlIcon).props()).toMatchObject({
+ name: 'download',
+ size: 16,
+ });
+ });
+});
diff --git a/spec/frontend/repository/components/blob_viewers/empty_viewer_spec.js b/spec/frontend/repository/components/blob_viewers/empty_viewer_spec.js
new file mode 100644
index 00000000000..e65f20ea0af
--- /dev/null
+++ b/spec/frontend/repository/components/blob_viewers/empty_viewer_spec.js
@@ -0,0 +1,14 @@
+import { shallowMount } from '@vue/test-utils';
+import EmptyViewer from '~/repository/components/blob_viewers/empty_viewer.vue';
+
+describe('Empty Viewer', () => {
+ let wrapper;
+
+ beforeEach(() => {
+ wrapper = shallowMount(EmptyViewer);
+ });
+
+ it('matches the snapshot', () => {
+ expect(wrapper.element).toMatchSnapshot();
+ });
+});
diff --git a/spec/frontend/repository/components/blob_viewers/text_viewer_spec.js b/spec/frontend/repository/components/blob_viewers/text_viewer_spec.js
new file mode 100644
index 00000000000..88c5bee6564
--- /dev/null
+++ b/spec/frontend/repository/components/blob_viewers/text_viewer_spec.js
@@ -0,0 +1,30 @@
+import { shallowMount } from '@vue/test-utils';
+import waitForPromises from 'helpers/wait_for_promises';
+import TextViewer from '~/repository/components/blob_viewers/text_viewer.vue';
+import SourceEditor from '~/vue_shared/components/source_editor.vue';
+
+describe('Text Viewer', () => {
+ let wrapper;
+ const propsData = {
+ content: 'Some content',
+ fileName: 'file_name.js',
+ readOnly: true,
+ };
+
+ const createComponent = () => {
+ wrapper = shallowMount(TextViewer, { propsData });
+ };
+
+ const findEditor = () => wrapper.findComponent(SourceEditor);
+
+ it('renders a Source Editor component', async () => {
+ createComponent();
+
+ await waitForPromises();
+
+ expect(findEditor().exists()).toBe(true);
+ expect(findEditor().props('value')).toBe(propsData.content);
+ expect(findEditor().props('fileName')).toBe(propsData.fileName);
+ expect(findEditor().props('editorOptions')).toEqual({ readOnly: propsData.readOnly });
+ });
+});
diff --git a/spec/frontend/repository/components/delete_blob_modal_spec.js b/spec/frontend/repository/components/delete_blob_modal_spec.js
new file mode 100644
index 00000000000..a74e3e6d325
--- /dev/null
+++ b/spec/frontend/repository/components/delete_blob_modal_spec.js
@@ -0,0 +1,130 @@
+import { GlFormTextarea, GlModal, GlFormInput, GlToggle } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
+import DeleteBlobModal from '~/repository/components/delete_blob_modal.vue';
+
+jest.mock('~/lib/utils/csrf', () => ({ token: 'mock-csrf-token' }));
+
+const initialProps = {
+ modalId: 'Delete-blob',
+ modalTitle: 'Delete File',
+ deletePath: 'some/path',
+ commitMessage: 'Delete File',
+ targetBranch: 'some-target-branch',
+ originalBranch: 'main',
+ canPushCode: true,
+ emptyRepo: false,
+};
+
+describe('DeleteBlobModal', () => {
+ let wrapper;
+
+ const createComponent = (props = {}) => {
+ wrapper = shallowMount(DeleteBlobModal, {
+ propsData: {
+ ...initialProps,
+ ...props,
+ },
+ });
+ };
+
+ const findModal = () => wrapper.findComponent(GlModal);
+ const findForm = () => wrapper.findComponent({ ref: 'form' });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('renders Modal component', () => {
+ createComponent();
+
+ const { modalTitle: title } = initialProps;
+
+ expect(findModal().props()).toMatchObject({
+ title,
+ size: 'md',
+ actionPrimary: {
+ text: 'Delete file',
+ },
+ actionCancel: {
+ text: 'Cancel',
+ },
+ });
+ });
+
+ describe('form', () => {
+ it('gets passed the path for action attribute', () => {
+ createComponent();
+ expect(findForm().attributes('action')).toBe(initialProps.deletePath);
+ });
+
+ it('submits the form', async () => {
+ createComponent();
+
+ const submitSpy = jest.spyOn(findForm().element, 'submit');
+ findModal().vm.$emit('primary', { preventDefault: () => {} });
+ await nextTick();
+
+ expect(submitSpy).toHaveBeenCalled();
+ submitSpy.mockRestore();
+ });
+
+ it.each`
+ component | defaultValue | canPushCode | targetBranch | originalBranch | exist
+ ${GlFormTextarea} | ${initialProps.commitMessage} | ${true} | ${initialProps.targetBranch} | ${initialProps.originalBranch} | ${true}
+ ${GlFormInput} | ${initialProps.targetBranch} | ${true} | ${initialProps.targetBranch} | ${initialProps.originalBranch} | ${true}
+ ${GlFormInput} | ${undefined} | ${false} | ${initialProps.targetBranch} | ${initialProps.originalBranch} | ${false}
+ ${GlToggle} | ${'true'} | ${true} | ${initialProps.targetBranch} | ${initialProps.originalBranch} | ${true}
+ ${GlToggle} | ${undefined} | ${true} | ${'same-branch'} | ${'same-branch'} | ${false}
+ `(
+ 'has the correct form fields ',
+ ({ component, defaultValue, canPushCode, targetBranch, originalBranch, exist }) => {
+ createComponent({
+ canPushCode,
+ targetBranch,
+ originalBranch,
+ });
+ const formField = wrapper.findComponent(component);
+
+ if (!exist) {
+ expect(formField.exists()).toBe(false);
+ return;
+ }
+
+ expect(formField.exists()).toBe(true);
+ expect(formField.attributes('value')).toBe(defaultValue);
+ },
+ );
+
+ it.each`
+ input | value | emptyRepo | canPushCode | exist
+ ${'authenticity_token'} | ${'mock-csrf-token'} | ${false} | ${true} | ${true}
+ ${'authenticity_token'} | ${'mock-csrf-token'} | ${true} | ${false} | ${true}
+ ${'_method'} | ${'delete'} | ${false} | ${true} | ${true}
+ ${'_method'} | ${'delete'} | ${true} | ${false} | ${true}
+ ${'original_branch'} | ${initialProps.originalBranch} | ${false} | ${true} | ${true}
+ ${'original_branch'} | ${undefined} | ${true} | ${true} | ${false}
+ ${'create_merge_request'} | ${'1'} | ${false} | ${false} | ${true}
+ ${'create_merge_request'} | ${'1'} | ${false} | ${true} | ${true}
+ ${'create_merge_request'} | ${undefined} | ${true} | ${false} | ${false}
+ `(
+ 'passes $input as a hidden input with the correct value',
+ ({ input, value, emptyRepo, canPushCode, exist }) => {
+ createComponent({
+ emptyRepo,
+ canPushCode,
+ });
+
+ const inputMethod = findForm().find(`input[name="${input}"]`);
+
+ if (!exist) {
+ expect(inputMethod.exists()).toBe(false);
+ return;
+ }
+
+ expect(inputMethod.attributes('type')).toBe('hidden');
+ expect(inputMethod.attributes('value')).toBe(value);
+ },
+ );
+ });
+});
diff --git a/spec/frontend/repository/components/table/__snapshots__/row_spec.js.snap b/spec/frontend/repository/components/table/__snapshots__/row_spec.js.snap
index ac60fc4917d..6f461f4c69b 100644
--- a/spec/frontend/repository/components/table/__snapshots__/row_spec.js.snap
+++ b/spec/frontend/repository/components/table/__snapshots__/row_spec.js.snap
@@ -11,6 +11,7 @@ exports[`Repository table row component renders a symlink table row 1`] = `
class="tree-item-link str-truncated"
data-qa-selector="file_name_link"
href="https://test.com"
+ title="test"
>
<file-icon-stub
class="mr-1 position-relative text-secondary"
@@ -64,6 +65,7 @@ exports[`Repository table row component renders table row 1`] = `
class="tree-item-link str-truncated"
data-qa-selector="file_name_link"
href="https://test.com"
+ title="test"
>
<file-icon-stub
class="mr-1 position-relative text-secondary"
@@ -117,6 +119,7 @@ exports[`Repository table row component renders table row for path with special
class="tree-item-link str-truncated"
data-qa-selector="file_name_link"
href="https://test.com"
+ title="test"
>
<file-icon-stub
class="mr-1 position-relative text-secondary"
diff --git a/spec/frontend/repository/components/tree_content_spec.js b/spec/frontend/repository/components/tree_content_spec.js
index d397bc185e2..1d1ec58100f 100644
--- a/spec/frontend/repository/components/tree_content_spec.js
+++ b/spec/frontend/repository/components/tree_content_spec.js
@@ -1,8 +1,8 @@
import { shallowMount } from '@vue/test-utils';
+import filesQuery from 'shared_queries/repository/files.query.graphql';
import FilePreview from '~/repository/components/preview/index.vue';
import FileTable from '~/repository/components/table/index.vue';
import TreeContent from '~/repository/components/tree_content.vue';
-import { TREE_INITIAL_FETCH_COUNT } from '~/repository/constants';
let vm;
let $apollo;
@@ -19,10 +19,17 @@ function factory(path, data = () => ({})) {
mocks: {
$apollo,
},
+ provide: {
+ glFeatures: {
+ increasePageSizeExponentially: true,
+ },
+ },
});
}
describe('Repository table component', () => {
+ const findFileTable = () => vm.find(FileTable);
+
afterEach(() => {
vm.destroy();
});
@@ -85,14 +92,12 @@ describe('Repository table component', () => {
describe('FileTable showMore', () => {
describe('when is present', () => {
- const fileTable = () => vm.find(FileTable);
-
beforeEach(async () => {
factory('/');
});
it('is changes hasShowMore to false when "showMore" event is emitted', async () => {
- fileTable().vm.$emit('showMore');
+ findFileTable().vm.$emit('showMore');
await vm.vm.$nextTick();
@@ -100,7 +105,7 @@ describe('Repository table component', () => {
});
it('changes clickedShowMore when "showMore" event is emitted', async () => {
- fileTable().vm.$emit('showMore');
+ findFileTable().vm.$emit('showMore');
await vm.vm.$nextTick();
@@ -110,7 +115,7 @@ describe('Repository table component', () => {
it('triggers fetchFiles when "showMore" event is emitted', () => {
jest.spyOn(vm.vm, 'fetchFiles');
- fileTable().vm.$emit('showMore');
+ findFileTable().vm.$emit('showMore');
expect(vm.vm.fetchFiles).toHaveBeenCalled();
});
@@ -126,10 +131,52 @@ describe('Repository table component', () => {
expect(vm.vm.hasShowMore).toBe(false);
});
- it('has limit of 1000 files on initial load', () => {
+ it.each`
+ totalBlobs | pagesLoaded | limitReached
+ ${900} | ${1} | ${false}
+ ${1000} | ${1} | ${true}
+ ${1002} | ${1} | ${true}
+ ${1002} | ${2} | ${false}
+ ${1900} | ${2} | ${false}
+ ${2000} | ${2} | ${true}
+ `('has limit of 1000 entries per page', async ({ totalBlobs, pagesLoaded, limitReached }) => {
factory('/');
- expect(TREE_INITIAL_FETCH_COUNT * vm.vm.pageSize).toBe(1000);
+ const blobs = new Array(totalBlobs).fill('fakeBlob');
+ vm.setData({ entries: { blobs }, pagesLoaded });
+
+ await vm.vm.$nextTick();
+
+ expect(findFileTable().props('hasMore')).toBe(limitReached);
+ });
+
+ it.each`
+ fetchCounter | pageSize
+ ${0} | ${10}
+ ${2} | ${30}
+ ${4} | ${50}
+ ${6} | ${70}
+ ${8} | ${90}
+ ${10} | ${100}
+ ${20} | ${100}
+ ${100} | ${100}
+ ${200} | ${100}
+ `('exponentially increases page size, to a maximum of 100', ({ fetchCounter, pageSize }) => {
+ factory('/');
+ vm.setData({ fetchCounter });
+
+ vm.vm.fetchFiles();
+
+ expect($apollo.query).toHaveBeenCalledWith({
+ query: filesQuery,
+ variables: {
+ pageSize,
+ nextPageCursor: '',
+ path: '/',
+ projectPath: '',
+ ref: '',
+ },
+ });
});
});
});
diff --git a/spec/frontend/repository/components/upload_blob_modal_spec.js b/spec/frontend/repository/components/upload_blob_modal_spec.js
index d93b1d7e5f1..08a6583b60c 100644
--- a/spec/frontend/repository/components/upload_blob_modal_spec.js
+++ b/spec/frontend/repository/components/upload_blob_modal_spec.js
@@ -190,7 +190,9 @@ describe('UploadBlobModal', () => {
});
it('creates a flash error', () => {
- expect(createFlash).toHaveBeenCalledWith('Error uploading file. Please try again.');
+ expect(createFlash).toHaveBeenCalledWith({
+ message: 'Error uploading file. Please try again.',
+ });
});
afterEach(() => {
diff --git a/spec/frontend/repository/log_tree_spec.js b/spec/frontend/repository/log_tree_spec.js
index 8cabf902a4f..5186c9a8992 100644
--- a/spec/frontend/repository/log_tree_spec.js
+++ b/spec/frontend/repository/log_tree_spec.js
@@ -1,6 +1,10 @@
import MockAdapter from 'axios-mock-adapter';
+import { createMockClient } from 'helpers/mock_apollo_helper';
import axios from '~/lib/utils/axios_utils';
import { resolveCommit, fetchLogsTree } from '~/repository/log_tree';
+import commitsQuery from '~/repository/queries/commits.query.graphql';
+import projectPathQuery from '~/repository/queries/project_path.query.graphql';
+import refQuery from '~/repository/queries/ref.query.graphql';
const mockData = [
{
@@ -10,6 +14,7 @@ const mockData = [
committed_date: '2019-01-01',
},
commit_path: `https://test.com`,
+ commit_title_html: 'commit title',
file_name: 'index.js',
type: 'blob',
},
@@ -50,19 +55,15 @@ describe('fetchLogsTree', () => {
global.gon = { relative_url_root: '' };
- client = {
- readQuery: () => ({
- projectPath: 'gitlab-org/gitlab-foss',
- escapedRef: 'main',
- commits: [],
- }),
- writeQuery: jest.fn(),
- };
-
resolver = {
entry: { name: 'index.js', type: 'blob' },
resolve: jest.fn(),
};
+
+ client = createMockClient();
+ client.writeQuery({ query: projectPathQuery, data: { projectPath: 'gitlab-org/gitlab-foss' } });
+ client.writeQuery({ query: refQuery, data: { ref: 'main', escapedRef: 'main' } });
+ client.writeQuery({ query: commitsQuery, data: { commits: [] } });
});
afterEach(() => {
@@ -125,25 +126,19 @@ describe('fetchLogsTree', () => {
it('writes query to client', async () => {
await fetchLogsTree(client, '', '0', resolver);
- expect(client.writeQuery).toHaveBeenCalledWith({
- query: expect.anything(),
- data: {
- projectPath: 'gitlab-org/gitlab-foss',
- escapedRef: 'main',
- commits: [
- expect.objectContaining({
- __typename: 'LogTreeCommit',
- commitPath: 'https://test.com',
- committedDate: '2019-01-01',
- fileName: 'index.js',
- filePath: '/index.js',
- message: 'testing message',
- sha: '123',
- titleHtml: undefined,
- type: 'blob',
- }),
- ],
- },
+ expect(client.readQuery({ query: commitsQuery })).toEqual({
+ commits: [
+ expect.objectContaining({
+ commitPath: 'https://test.com',
+ committedDate: '2019-01-01',
+ fileName: 'index.js',
+ filePath: '/index.js',
+ message: 'testing message',
+ sha: '123',
+ titleHtml: 'commit title',
+ type: 'blob',
+ }),
+ ],
});
});
});
diff --git a/spec/frontend/right_sidebar_spec.js b/spec/frontend/right_sidebar_spec.js
index 8699e1cf420..d1f861669a0 100644
--- a/spec/frontend/right_sidebar_spec.js
+++ b/spec/frontend/right_sidebar_spec.js
@@ -66,22 +66,6 @@ describe('RightSidebar', () => {
assertSidebarState('collapsed');
});
- it('should broadcast todo:toggle event when add todo clicked', (done) => {
- const todos = getJSONFixture('todos/todos.json');
- mock.onPost(/(.*)\/todos$/).reply(200, todos);
-
- const todoToggleSpy = jest.fn();
- $(document).on('todo:toggle', todoToggleSpy);
-
- $('.issuable-sidebar-header .js-issuable-todo').click();
-
- setImmediate(() => {
- expect(todoToggleSpy.mock.calls.length).toEqual(1);
-
- done();
- });
- });
-
it('should not hide collapsed icons', () => {
[].forEach.call(document.querySelectorAll('.sidebar-collapsed-icon'), (el) => {
expect(el.querySelector('.fa, svg').classList.contains('hidden')).toBeFalsy();
diff --git a/spec/frontend/runner/components/cells/runner_actions_cell_spec.js b/spec/frontend/runner/components/cells/runner_actions_cell_spec.js
index 12651a82a0c..95f7c38cafc 100644
--- a/spec/frontend/runner/components/cells/runner_actions_cell_spec.js
+++ b/spec/frontend/runner/components/cells/runner_actions_cell_spec.js
@@ -1,18 +1,30 @@
-import { shallowMount } from '@vue/test-utils';
+import { createLocalVue, shallowMount } from '@vue/test-utils';
+import VueApollo from 'vue-apollo';
+import createMockApollo from 'helpers/mock_apollo_helper';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
+import createFlash from '~/flash';
import RunnerActionCell from '~/runner/components/cells/runner_actions_cell.vue';
-import deleteRunnerMutation from '~/runner/graphql/delete_runner.mutation.graphql';
import getRunnersQuery from '~/runner/graphql/get_runners.query.graphql';
+import runnerDeleteMutation from '~/runner/graphql/runner_delete.mutation.graphql';
import runnerUpdateMutation from '~/runner/graphql/runner_update.mutation.graphql';
+import { captureException } from '~/runner/sentry_utils';
+import { runnerData } from '../../mock_data';
-const mockId = '1';
+const mockRunner = runnerData.data.runner;
const getRunnersQueryName = getRunnersQuery.definitions[0].name.value;
+const localVue = createLocalVue();
+localVue.use(VueApollo);
+
+jest.mock('~/flash');
+jest.mock('~/runner/sentry_utils');
+
describe('RunnerTypeCell', () => {
let wrapper;
- let mutate;
+ const runnerDeleteMutationHandler = jest.fn();
+ const runnerUpdateMutationHandler = jest.fn();
const findEditBtn = () => wrapper.findByTestId('edit-runner');
const findToggleActiveBtn = () => wrapper.findByTestId('toggle-active-runner');
@@ -23,26 +35,43 @@ describe('RunnerTypeCell', () => {
shallowMount(RunnerActionCell, {
propsData: {
runner: {
- id: `gid://gitlab/Ci::Runner/${mockId}`,
+ id: mockRunner.id,
active,
},
},
- mocks: {
- $apollo: {
- mutate,
- },
- },
+ localVue,
+ apolloProvider: createMockApollo([
+ [runnerDeleteMutation, runnerDeleteMutationHandler],
+ [runnerUpdateMutation, runnerUpdateMutationHandler],
+ ]),
...options,
}),
);
};
beforeEach(() => {
- mutate = jest.fn();
+ runnerDeleteMutationHandler.mockResolvedValue({
+ data: {
+ runnerDelete: {
+ errors: [],
+ },
+ },
+ });
+
+ runnerUpdateMutationHandler.mockResolvedValue({
+ data: {
+ runnerUpdate: {
+ runner: runnerData.data.runner,
+ errors: [],
+ },
+ },
+ });
});
afterEach(() => {
- mutate.mockReset();
+ runnerDeleteMutationHandler.mockReset();
+ runnerUpdateMutationHandler.mockReset();
+
wrapper.destroy();
});
@@ -58,17 +87,6 @@ describe('RunnerTypeCell', () => {
${'paused'} | ${'Resume'} | ${'play'} | ${false} | ${true}
`('When the runner is $state', ({ label, icon, isActive, newActiveValue }) => {
beforeEach(() => {
- mutate.mockResolvedValue({
- data: {
- runnerUpdate: {
- runner: {
- id: `gid://gitlab/Ci::Runner/1`,
- __typename: 'CiRunner',
- },
- },
- },
- });
-
createComponent({ active: isActive });
});
@@ -93,46 +111,93 @@ describe('RunnerTypeCell', () => {
});
describe(`When clicking on the ${icon} button`, () => {
- beforeEach(async () => {
+ it(`The apollo mutation to set active to ${newActiveValue} is called`, async () => {
+ expect(runnerUpdateMutationHandler).toHaveBeenCalledTimes(0);
+
await findToggleActiveBtn().vm.$emit('click');
- await waitForPromises();
- });
- it(`The apollo mutation to set active to ${newActiveValue} is called`, () => {
- expect(mutate).toHaveBeenCalledTimes(1);
- expect(mutate).toHaveBeenCalledWith({
- mutation: runnerUpdateMutation,
- variables: {
- input: {
- id: `gid://gitlab/Ci::Runner/${mockId}`,
- active: newActiveValue,
- },
+ expect(runnerUpdateMutationHandler).toHaveBeenCalledTimes(1);
+ expect(runnerUpdateMutationHandler).toHaveBeenCalledWith({
+ input: {
+ id: mockRunner.id,
+ active: newActiveValue,
},
});
});
- it('The button does not have a loading state', () => {
+ it('The button does not have a loading state after the mutation occurs', async () => {
+ await findToggleActiveBtn().vm.$emit('click');
+
+ expect(findToggleActiveBtn().props('loading')).toBe(true);
+
+ await waitForPromises();
+
expect(findToggleActiveBtn().props('loading')).toBe(false);
});
});
- });
- describe('When the user clicks a runner', () => {
- beforeEach(() => {
- createComponent();
+ describe('When update fails', () => {
+ describe('On a network error', () => {
+ const mockErrorMsg = 'Update error!';
+
+ beforeEach(async () => {
+ runnerUpdateMutationHandler.mockRejectedValueOnce(new Error(mockErrorMsg));
+
+ await findToggleActiveBtn().vm.$emit('click');
+ });
+
+ it('error is reported to sentry', () => {
+ expect(captureException).toHaveBeenCalledWith({
+ error: new Error(`Network error: ${mockErrorMsg}`),
+ component: 'RunnerActionsCell',
+ });
+ });
- mutate.mockResolvedValue({
- data: {
- runnerDelete: {
- runner: {
- id: `gid://gitlab/Ci::Runner/1`,
- __typename: 'CiRunner',
+ it('error is shown to the user', () => {
+ expect(createFlash).toHaveBeenCalledTimes(1);
+ });
+ });
+
+ describe('On a validation error', () => {
+ const mockErrorMsg = 'Runner not found!';
+ const mockErrorMsg2 = 'User not allowed!';
+
+ beforeEach(async () => {
+ runnerUpdateMutationHandler.mockResolvedValue({
+ data: {
+ runnerUpdate: {
+ runner: runnerData.data.runner,
+ errors: [mockErrorMsg, mockErrorMsg2],
+ },
},
- },
- },
+ });
+
+ await findToggleActiveBtn().vm.$emit('click');
+ });
+
+ it('error is reported to sentry', () => {
+ expect(captureException).toHaveBeenCalledWith({
+ error: new Error(`${mockErrorMsg} ${mockErrorMsg2}`),
+ component: 'RunnerActionsCell',
+ });
+ });
+
+ it('error is shown to the user', () => {
+ expect(createFlash).toHaveBeenCalledTimes(1);
+ });
});
+ });
+ });
+ describe('When the user clicks a runner', () => {
+ beforeEach(() => {
jest.spyOn(window, 'confirm');
+
+ createComponent();
+ });
+
+ afterEach(() => {
+ window.confirm.mockRestore();
});
describe('When the user confirms deletion', () => {
@@ -141,18 +206,28 @@ describe('RunnerTypeCell', () => {
await findDeleteBtn().vm.$emit('click');
});
- it('The user sees a confirmation alert', async () => {
+ it('The user sees a confirmation alert', () => {
expect(window.confirm).toHaveBeenCalledTimes(1);
expect(window.confirm).toHaveBeenCalledWith(expect.any(String));
});
it('The delete mutation is called correctly', () => {
- expect(mutate).toHaveBeenCalledTimes(1);
- expect(mutate).toHaveBeenCalledWith({
- mutation: deleteRunnerMutation,
+ expect(runnerDeleteMutationHandler).toHaveBeenCalledTimes(1);
+ expect(runnerDeleteMutationHandler).toHaveBeenCalledWith({
+ input: { id: mockRunner.id },
+ });
+ });
+
+ it('When delete mutation is called, current runners are refetched', async () => {
+ jest.spyOn(wrapper.vm.$apollo, 'mutate');
+
+ await findDeleteBtn().vm.$emit('click');
+
+ expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({
+ mutation: runnerDeleteMutation,
variables: {
input: {
- id: `gid://gitlab/Ci::Runner/${mockId}`,
+ id: mockRunner.id,
},
},
awaitRefetchQueries: true,
@@ -176,6 +251,57 @@ describe('RunnerTypeCell', () => {
expect(findDeleteBtn().attributes('title')).toBe('');
});
+
+ describe('When delete fails', () => {
+ describe('On a network error', () => {
+ const mockErrorMsg = 'Delete error!';
+
+ beforeEach(async () => {
+ runnerDeleteMutationHandler.mockRejectedValueOnce(new Error(mockErrorMsg));
+
+ await findDeleteBtn().vm.$emit('click');
+ });
+
+ it('error is reported to sentry', () => {
+ expect(captureException).toHaveBeenCalledWith({
+ error: new Error(`Network error: ${mockErrorMsg}`),
+ component: 'RunnerActionsCell',
+ });
+ });
+
+ it('error is shown to the user', () => {
+ expect(createFlash).toHaveBeenCalledTimes(1);
+ });
+ });
+
+ describe('On a validation error', () => {
+ const mockErrorMsg = 'Runner not found!';
+ const mockErrorMsg2 = 'User not allowed!';
+
+ beforeEach(async () => {
+ runnerDeleteMutationHandler.mockResolvedValue({
+ data: {
+ runnerDelete: {
+ errors: [mockErrorMsg, mockErrorMsg2],
+ },
+ },
+ });
+
+ await findDeleteBtn().vm.$emit('click');
+ });
+
+ it('error is reported to sentry', () => {
+ expect(captureException).toHaveBeenCalledWith({
+ error: new Error(`${mockErrorMsg} ${mockErrorMsg2}`),
+ component: 'RunnerActionsCell',
+ });
+ });
+
+ it('error is shown to the user', () => {
+ expect(createFlash).toHaveBeenCalledTimes(1);
+ });
+ });
+ });
});
describe('When the user does not confirm deletion', () => {
@@ -189,7 +315,7 @@ describe('RunnerTypeCell', () => {
});
it('The delete mutation is not called', () => {
- expect(mutate).toHaveBeenCalledTimes(0);
+ expect(runnerDeleteMutationHandler).toHaveBeenCalledTimes(0);
});
it('The delete button does not have a loading state', () => {
diff --git a/spec/frontend/runner/components/helpers/masked_value_spec.js b/spec/frontend/runner/components/helpers/masked_value_spec.js
new file mode 100644
index 00000000000..f87315057ec
--- /dev/null
+++ b/spec/frontend/runner/components/helpers/masked_value_spec.js
@@ -0,0 +1,51 @@
+import { GlButton } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import MaskedValue from '~/runner/components/helpers/masked_value.vue';
+
+const mockSecret = '01234567890';
+const mockMasked = '***********';
+
+describe('MaskedValue', () => {
+ let wrapper;
+
+ const findButton = () => wrapper.findComponent(GlButton);
+
+ const createComponent = ({ props = {} } = {}) => {
+ wrapper = shallowMount(MaskedValue, {
+ propsData: {
+ value: mockSecret,
+ ...props,
+ },
+ });
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('Displays masked value by default', () => {
+ expect(wrapper.text()).toBe(mockMasked);
+ });
+
+ describe('When the icon is clicked', () => {
+ beforeEach(() => {
+ findButton().vm.$emit('click');
+ });
+
+ it('Displays the actual value', () => {
+ expect(wrapper.text()).toBe(mockSecret);
+ expect(wrapper.text()).not.toBe(mockMasked);
+ });
+
+ it('When user clicks again, displays masked value', async () => {
+ await findButton().vm.$emit('click');
+
+ expect(wrapper.text()).toBe(mockMasked);
+ expect(wrapper.text()).not.toBe(mockSecret);
+ });
+ });
+});
diff --git a/spec/frontend/runner/components/runner_filtered_search_bar_spec.js b/spec/frontend/runner/components/runner_filtered_search_bar_spec.js
index 61a8f821b30..85cf7ea92df 100644
--- a/spec/frontend/runner/components/runner_filtered_search_bar_spec.js
+++ b/spec/frontend/runner/components/runner_filtered_search_bar_spec.js
@@ -2,8 +2,10 @@ import { GlFilteredSearch, GlDropdown, GlDropdownItem } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import RunnerFilteredSearchBar from '~/runner/components/runner_filtered_search_bar.vue';
-import { PARAM_KEY_STATUS, PARAM_KEY_RUNNER_TYPE } from '~/runner/constants';
+import TagToken from '~/runner/components/search_tokens/tag_token.vue';
+import { PARAM_KEY_STATUS, PARAM_KEY_RUNNER_TYPE, PARAM_KEY_TAG } from '~/runner/constants';
import FilteredSearch from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
+import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_token.vue';
describe('RunnerList', () => {
let wrapper;
@@ -11,6 +13,7 @@ describe('RunnerList', () => {
const findFilteredSearch = () => wrapper.findComponent(FilteredSearch);
const findGlFilteredSearch = () => wrapper.findComponent(GlFilteredSearch);
const findSortOptions = () => wrapper.findAllComponents(GlDropdownItem);
+ const findActiveRunnersMessage = () => wrapper.findByTestId('active-runners-message');
const mockDefaultSort = 'CREATED_DESC';
const mockOtherSort = 'CONTACTED_DESC';
@@ -18,18 +21,20 @@ describe('RunnerList', () => {
{ type: PARAM_KEY_STATUS, value: { data: 'ACTIVE', operator: '=' } },
{ type: 'filtered-search-term', value: { data: '' } },
];
+ const mockActiveRunnersCount = 2;
const createComponent = ({ props = {}, options = {} } = {}) => {
wrapper = extendedWrapper(
shallowMount(RunnerFilteredSearchBar, {
propsData: {
+ namespace: 'runners',
value: {
filters: [],
sort: mockDefaultSort,
},
+ activeRunnersCount: mockActiveRunnersCount,
...props,
},
- attrs: { namespace: 'runners' },
stubs: {
FilteredSearch,
GlFilteredSearch,
@@ -53,6 +58,18 @@ describe('RunnerList', () => {
expect(findFilteredSearch().props('namespace')).toBe('runners');
});
+ it('Displays an active runner count', () => {
+ expect(findActiveRunnersMessage().text()).toBe(
+ `Runners currently online: ${mockActiveRunnersCount}`,
+ );
+ });
+
+ it('Displays a large active runner count', () => {
+ createComponent({ props: { activeRunnersCount: 2000 } });
+
+ expect(findActiveRunnersMessage().text()).toBe('Runners currently online: 2,000');
+ });
+
it('sets sorting options', () => {
const SORT_OPTIONS_COUNT = 2;
@@ -65,12 +82,18 @@ describe('RunnerList', () => {
expect(findFilteredSearch().props('tokens')).toEqual([
expect.objectContaining({
type: PARAM_KEY_STATUS,
+ token: BaseToken,
options: expect.any(Array),
}),
expect.objectContaining({
type: PARAM_KEY_RUNNER_TYPE,
+ token: BaseToken,
options: expect.any(Array),
}),
+ expect.objectContaining({
+ type: PARAM_KEY_TAG,
+ token: TagToken,
+ }),
]);
});
diff --git a/spec/frontend/runner/components/runner_list_spec.js b/spec/frontend/runner/components/runner_list_spec.js
index d88d7b3fbee..5fff3581e39 100644
--- a/spec/frontend/runner/components/runner_list_spec.js
+++ b/spec/frontend/runner/components/runner_list_spec.js
@@ -1,5 +1,6 @@
import { GlLink, GlTable, GlSkeletonLoader } from '@gitlab/ui';
import { mount, shallowMount } from '@vue/test-utils';
+import { cloneDeep } from 'lodash';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import RunnerList from '~/runner/components/runner_list.vue';
@@ -11,7 +12,6 @@ const mockActiveRunnersCount = mockRunners.length;
describe('RunnerList', () => {
let wrapper;
- const findActiveRunnersMessage = () => wrapper.findByTestId('active-runners-message');
const findSkeletonLoader = () => wrapper.findComponent(GlSkeletonLoader);
const findTable = () => wrapper.findComponent(GlTable);
const findHeaders = () => wrapper.findAll('th');
@@ -39,18 +39,6 @@ describe('RunnerList', () => {
wrapper.destroy();
});
- it('Displays active runner count', () => {
- expect(findActiveRunnersMessage().text()).toBe(
- `Runners currently online: ${mockActiveRunnersCount}`,
- );
- });
-
- it('Displays a large active runner count', () => {
- createComponent({ props: { activeRunnersCount: 2000 } });
-
- expect(findActiveRunnersMessage().text()).toBe('Runners currently online: 2,000');
- });
-
it('Displays headers', () => {
const headerLabels = findHeaders().wrappers.map((w) => w.text());
@@ -85,12 +73,11 @@ describe('RunnerList', () => {
);
expect(findCell({ fieldKey: 'name' }).text()).toContain(description);
- // Other fields: some cells are empty in the first iteration
- // See https://gitlab.com/gitlab-org/gitlab/-/issues/329658#pending-features
+ // Other fields
expect(findCell({ fieldKey: 'version' }).text()).toBe(version);
expect(findCell({ fieldKey: 'ipAddress' }).text()).toBe(ipAddress);
- expect(findCell({ fieldKey: 'projectCount' }).text()).toBe('');
- expect(findCell({ fieldKey: 'jobCount' }).text()).toBe('');
+ expect(findCell({ fieldKey: 'projectCount' }).text()).toBe('1');
+ expect(findCell({ fieldKey: 'jobCount' }).text()).toBe('0');
expect(findCell({ fieldKey: 'tagList' }).text()).toBe('');
expect(findCell({ fieldKey: 'contactedAt' }).text()).toEqual(expect.any(String));
@@ -101,6 +88,54 @@ describe('RunnerList', () => {
expect(actions.findByTestId('toggle-active-runner').exists()).toBe(true);
});
+ describe('Table data formatting', () => {
+ let mockRunnersCopy;
+
+ beforeEach(() => {
+ mockRunnersCopy = cloneDeep(mockRunners);
+ });
+
+ it('Formats null project counts', () => {
+ mockRunnersCopy[0].projectCount = null;
+
+ createComponent({ props: { runners: mockRunnersCopy } }, mount);
+
+ expect(findCell({ fieldKey: 'projectCount' }).text()).toBe('n/a');
+ });
+
+ it('Formats 0 project counts', () => {
+ mockRunnersCopy[0].projectCount = 0;
+
+ createComponent({ props: { runners: mockRunnersCopy } }, mount);
+
+ expect(findCell({ fieldKey: 'projectCount' }).text()).toBe('0');
+ });
+
+ it('Formats big project counts', () => {
+ mockRunnersCopy[0].projectCount = 1000;
+
+ createComponent({ props: { runners: mockRunnersCopy } }, mount);
+
+ expect(findCell({ fieldKey: 'projectCount' }).text()).toBe('1,000');
+ });
+
+ it('Formats job counts', () => {
+ mockRunnersCopy[0].jobCount = 1000;
+
+ createComponent({ props: { runners: mockRunnersCopy } }, mount);
+
+ expect(findCell({ fieldKey: 'jobCount' }).text()).toBe('1,000');
+ });
+
+ it('Formats big job counts with a plus symbol', () => {
+ mockRunnersCopy[0].jobCount = 1001;
+
+ createComponent({ props: { runners: mockRunnersCopy } }, mount);
+
+ expect(findCell({ fieldKey: 'jobCount' }).text()).toBe('1,000+');
+ });
+ });
+
it('Links to the runner page', () => {
const { id } = mockRunners[0];
diff --git a/spec/frontend/runner/components/runner_manual_setup_help_spec.js b/spec/frontend/runner/components/runner_manual_setup_help_spec.js
index add595d784e..effef0e7ebf 100644
--- a/spec/frontend/runner/components/runner_manual_setup_help_spec.js
+++ b/spec/frontend/runner/components/runner_manual_setup_help_spec.js
@@ -3,6 +3,7 @@ import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
import { TEST_HOST } from 'helpers/test_constants';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import MaskedValue from '~/runner/components/helpers/masked_value.vue';
import RunnerManualSetupHelp from '~/runner/components/runner_manual_setup_help.vue';
import RunnerRegistrationTokenReset from '~/runner/components/runner_registration_token_reset.vue';
import { INSTANCE_TYPE, GROUP_TYPE, PROJECT_TYPE } from '~/runner/constants';
@@ -37,6 +38,7 @@ describe('RunnerManualSetupHelp', () => {
...props,
},
stubs: {
+ MaskedValue,
GlSprintf,
},
}),
@@ -93,7 +95,11 @@ describe('RunnerManualSetupHelp', () => {
expect(findRunnerInstructions().exists()).toBe(true);
});
- it('Displays the registration token', () => {
+ it('Displays the registration token', async () => {
+ findRegistrationToken().find('[data-testid="toggle-masked"]').vm.$emit('click');
+
+ await nextTick();
+
expect(findRegistrationToken().text()).toBe(mockRegistrationToken);
expect(findClipboardButtons().at(1).props('text')).toBe(mockRegistrationToken);
});
@@ -105,6 +111,7 @@ describe('RunnerManualSetupHelp', () => {
it('Replaces the runner reset button', async () => {
const mockNewRegistrationToken = 'NEW_MOCK_REGISTRATION_TOKEN';
+ findRegistrationToken().find('[data-testid="toggle-masked"]').vm.$emit('click');
findRunnerRegistrationTokenReset().vm.$emit('tokenReset', mockNewRegistrationToken);
await nextTick();
diff --git a/spec/frontend/runner/components/runner_registration_token_reset_spec.js b/spec/frontend/runner/components/runner_registration_token_reset_spec.js
index fa5751b380f..6dc207e369c 100644
--- a/spec/frontend/runner/components/runner_registration_token_reset_spec.js
+++ b/spec/frontend/runner/components/runner_registration_token_reset_spec.js
@@ -7,8 +7,10 @@ import createFlash, { FLASH_TYPES } from '~/flash';
import RunnerRegistrationTokenReset from '~/runner/components/runner_registration_token_reset.vue';
import { INSTANCE_TYPE } from '~/runner/constants';
import runnersRegistrationTokenResetMutation from '~/runner/graphql/runners_registration_token_reset.mutation.graphql';
+import { captureException } from '~/runner/sentry_utils';
jest.mock('~/flash');
+jest.mock('~/runner/sentry_utils');
const localVue = createLocalVue();
localVue.use(VueApollo);
@@ -111,25 +113,32 @@ describe('RunnerRegistrationTokenReset', () => {
describe('On error', () => {
it('On network error, error message is shown', async () => {
- runnersRegistrationTokenResetMutationHandler.mockRejectedValueOnce(
- new Error('Something went wrong'),
- );
+ const mockErrorMsg = 'Token reset failed!';
+
+ runnersRegistrationTokenResetMutationHandler.mockRejectedValueOnce(new Error(mockErrorMsg));
window.confirm.mockReturnValueOnce(true);
await findButton().vm.$emit('click');
await waitForPromises();
expect(createFlash).toHaveBeenLastCalledWith({
- message: 'Network error: Something went wrong',
+ message: `Network error: ${mockErrorMsg}`,
+ });
+ expect(captureException).toHaveBeenCalledWith({
+ error: new Error(`Network error: ${mockErrorMsg}`),
+ component: 'RunnerRegistrationTokenReset',
});
});
it('On validation error, error message is shown', async () => {
+ const mockErrorMsg = 'User not allowed!';
+ const mockErrorMsg2 = 'Type is not valid!';
+
runnersRegistrationTokenResetMutationHandler.mockResolvedValue({
data: {
runnersRegistrationTokenReset: {
token: null,
- errors: ['Token reset failed'],
+ errors: [mockErrorMsg, mockErrorMsg2],
},
},
});
@@ -139,7 +148,11 @@ describe('RunnerRegistrationTokenReset', () => {
await waitForPromises();
expect(createFlash).toHaveBeenLastCalledWith({
- message: 'Token reset failed',
+ message: `${mockErrorMsg} ${mockErrorMsg2}`,
+ });
+ expect(captureException).toHaveBeenCalledWith({
+ error: new Error(`${mockErrorMsg} ${mockErrorMsg2}`),
+ component: 'RunnerRegistrationTokenReset',
});
});
});
diff --git a/spec/frontend/runner/components/runner_tag_spec.js b/spec/frontend/runner/components/runner_tag_spec.js
new file mode 100644
index 00000000000..dda318f8153
--- /dev/null
+++ b/spec/frontend/runner/components/runner_tag_spec.js
@@ -0,0 +1,45 @@
+import { GlBadge } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import RunnerTag from '~/runner/components/runner_tag.vue';
+
+describe('RunnerTag', () => {
+ let wrapper;
+
+ const findBadge = () => wrapper.findComponent(GlBadge);
+
+ const createComponent = ({ props = {} } = {}) => {
+ wrapper = shallowMount(RunnerTag, {
+ propsData: {
+ tag: 'tag1',
+ ...props,
+ },
+ });
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('Displays tag text', () => {
+ expect(wrapper.text()).toBe('tag1');
+ });
+
+ it('Displays tags with correct style', () => {
+ expect(findBadge().props()).toMatchObject({
+ size: 'md',
+ variant: 'info',
+ });
+ });
+
+ it('Displays tags with small size', () => {
+ createComponent({
+ props: { size: 'sm' },
+ });
+
+ expect(findBadge().props('size')).toBe('sm');
+ });
+});
diff --git a/spec/frontend/runner/components/runner_tags_spec.js b/spec/frontend/runner/components/runner_tags_spec.js
index 7bb3f65e4ba..b6487ade0d6 100644
--- a/spec/frontend/runner/components/runner_tags_spec.js
+++ b/spec/frontend/runner/components/runner_tags_spec.js
@@ -1,5 +1,5 @@
import { GlBadge } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
+import { mount } from '@vue/test-utils';
import RunnerTags from '~/runner/components/runner_tags.vue';
describe('RunnerTags', () => {
@@ -9,7 +9,7 @@ describe('RunnerTags', () => {
const findBadgesAt = (i = 0) => wrapper.findAllComponents(GlBadge).at(i);
const createComponent = ({ props = {} } = {}) => {
- wrapper = shallowMount(RunnerTags, {
+ wrapper = mount(RunnerTags, {
propsData: {
tagList: ['tag1', 'tag2'],
...props,
@@ -45,14 +45,6 @@ describe('RunnerTags', () => {
expect(findBadge().props('size')).toBe('sm');
});
- it('Displays tags with a variant', () => {
- createComponent({
- props: { variant: 'warning' },
- });
-
- expect(findBadge().props('variant')).toBe('warning');
- });
-
it('Is empty when there are no tags', () => {
createComponent({
props: { tagList: null },
diff --git a/spec/frontend/runner/components/runner_update_form_spec.js b/spec/frontend/runner/components/runner_update_form_spec.js
index 6333ed7118a..15029d7a911 100644
--- a/spec/frontend/runner/components/runner_update_form_spec.js
+++ b/spec/frontend/runner/components/runner_update_form_spec.js
@@ -15,9 +15,11 @@ import {
ACCESS_LEVEL_NOT_PROTECTED,
} from '~/runner/constants';
import runnerUpdateMutation from '~/runner/graphql/runner_update.mutation.graphql';
+import { captureException } from '~/runner/sentry_utils';
import { runnerData } from '../mock_data';
jest.mock('~/flash');
+jest.mock('~/runner/sentry_utils');
const mockRunner = runnerData.data.runner;
@@ -205,13 +207,11 @@ describe('RunnerUpdateForm', () => {
});
it.each`
- value | submitted
- ${''} | ${{ tagList: [] }}
- ${'tag1, tag2'} | ${{ tagList: ['tag1', 'tag2'] }}
- ${'with spaces'} | ${{ tagList: ['with spaces'] }}
- ${',,,,, commas'} | ${{ tagList: ['commas'] }}
- ${'more ,,,,, commas'} | ${{ tagList: ['more', 'commas'] }}
- ${' trimmed , trimmed2 '} | ${{ tagList: ['trimmed', 'trimmed2'] }}
+ value | submitted
+ ${''} | ${{ tagList: [] }}
+ ${'tag1, tag2'} | ${{ tagList: ['tag1', 'tag2'] }}
+ ${'with spaces'} | ${{ tagList: ['with spaces'] }}
+ ${'more ,,,,, commas'} | ${{ tagList: ['more', 'commas'] }}
`('Field updates runner\'s tags for "$value"', async ({ value, submitted }) => {
const runner = { ...mockRunner, tagList: ['tag1'] };
createComponent({ props: { runner } });
@@ -232,22 +232,30 @@ describe('RunnerUpdateForm', () => {
});
it('On network error, error message is shown', async () => {
- runnerUpdateHandler.mockRejectedValue(new Error('Something went wrong'));
+ const mockErrorMsg = 'Update error!';
+
+ runnerUpdateHandler.mockRejectedValue(new Error(mockErrorMsg));
await submitFormAndWait();
expect(createFlash).toHaveBeenLastCalledWith({
- message: 'Network error: Something went wrong',
+ message: `Network error: ${mockErrorMsg}`,
+ });
+ expect(captureException).toHaveBeenCalledWith({
+ component: 'RunnerUpdateForm',
+ error: new Error(`Network error: ${mockErrorMsg}`),
});
expect(findSubmitDisabledAttr()).toBeUndefined();
});
- it('On validation error, error message is shown', async () => {
+ it('On validation error, error message is shown and it is not sent to sentry', async () => {
+ const mockErrorMsg = 'Invalid value!';
+
runnerUpdateHandler.mockResolvedValue({
data: {
runnerUpdate: {
runner: mockRunner,
- errors: ['A value is invalid'],
+ errors: [mockErrorMsg],
},
},
});
@@ -255,8 +263,9 @@ describe('RunnerUpdateForm', () => {
await submitFormAndWait();
expect(createFlash).toHaveBeenLastCalledWith({
- message: 'A value is invalid',
+ message: mockErrorMsg,
});
+ expect(captureException).not.toHaveBeenCalled();
expect(findSubmitDisabledAttr()).toBeUndefined();
});
});
diff --git a/spec/frontend/runner/components/search_tokens/tag_token_spec.js b/spec/frontend/runner/components/search_tokens/tag_token_spec.js
new file mode 100644
index 00000000000..52b87542243
--- /dev/null
+++ b/spec/frontend/runner/components/search_tokens/tag_token_spec.js
@@ -0,0 +1,188 @@
+import { GlFilteredSearchSuggestion, GlLoadingIcon, GlToken } from '@gitlab/ui';
+import { mount } from '@vue/test-utils';
+import MockAdapter from 'axios-mock-adapter';
+import { nextTick } from 'vue';
+import waitForPromises from 'helpers/wait_for_promises';
+import createFlash from '~/flash';
+import axios from '~/lib/utils/axios_utils';
+
+import TagToken, { TAG_SUGGESTIONS_PATH } from '~/runner/components/search_tokens/tag_token.vue';
+import { OPERATOR_IS_ONLY } from '~/vue_shared/components/filtered_search_bar/constants';
+import { getRecentlyUsedSuggestions } from '~/vue_shared/components/filtered_search_bar/filtered_search_utils';
+
+jest.mock('~/flash');
+
+jest.mock('~/vue_shared/components/filtered_search_bar/filtered_search_utils', () => ({
+ ...jest.requireActual('~/vue_shared/components/filtered_search_bar/filtered_search_utils'),
+ getRecentlyUsedSuggestions: jest.fn(),
+}));
+
+const mockStorageKey = 'stored-recent-tags';
+
+const mockTags = [
+ { id: 1, name: 'linux' },
+ { id: 2, name: 'windows' },
+ { id: 3, name: 'mac' },
+];
+
+const mockTagsFiltered = [mockTags[0]];
+
+const mockSearchTerm = mockTags[0].name;
+
+const GlFilteredSearchTokenStub = {
+ template: `<div>
+ <slot name="view-token"></slot>
+ <slot name="suggestions"></slot>
+ </div>`,
+};
+
+const mockTagTokenConfig = {
+ icon: 'tag',
+ title: 'Tags',
+ type: 'tag',
+ token: TagToken,
+ recentTokenValuesStorageKey: mockStorageKey,
+ operators: OPERATOR_IS_ONLY,
+};
+
+describe('TagToken', () => {
+ let mock;
+ let wrapper;
+
+ const createComponent = (props = {}) => {
+ wrapper = mount(TagToken, {
+ propsData: {
+ config: mockTagTokenConfig,
+ value: { data: '' },
+ active: false,
+ ...props,
+ },
+ provide: {
+ portalName: 'fake target',
+ alignSuggestions: function fakeAlignSuggestions() {},
+ filteredSearchSuggestionListInstance: {
+ register: jest.fn(),
+ unregister: jest.fn(),
+ },
+ },
+ stubs: {
+ GlFilteredSearchToken: GlFilteredSearchTokenStub,
+ },
+ });
+ };
+
+ const findGlFilteredSearchSuggestions = () =>
+ wrapper.findAllComponents(GlFilteredSearchSuggestion);
+ const findGlFilteredSearchToken = () => wrapper.findComponent(GlFilteredSearchTokenStub);
+ const findToken = () => wrapper.findComponent(GlToken);
+ const findGlLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
+
+ beforeEach(async () => {
+ mock = new MockAdapter(axios);
+
+ mock.onGet(TAG_SUGGESTIONS_PATH, { params: { search: '' } }).reply(200, mockTags);
+ mock
+ .onGet(TAG_SUGGESTIONS_PATH, { params: { search: mockSearchTerm } })
+ .reply(200, mockTagsFiltered);
+
+ getRecentlyUsedSuggestions.mockReturnValue([]);
+
+ createComponent();
+ await waitForPromises();
+ });
+
+ afterEach(() => {
+ getRecentlyUsedSuggestions.mockReset();
+ wrapper.destroy();
+ });
+
+ describe('when the tags token is displayed', () => {
+ it('requests tags suggestions', () => {
+ expect(mock.history.get[0].params).toEqual({ search: '' });
+ });
+
+ it('displays tags suggestions', () => {
+ mockTags.forEach(({ name }, i) => {
+ expect(findGlFilteredSearchSuggestions().at(i).text()).toBe(name);
+ });
+ });
+ });
+
+ describe('when suggestions are stored', () => {
+ const storedSuggestions = [{ id: 4, value: 'docker', text: 'docker' }];
+
+ beforeEach(async () => {
+ getRecentlyUsedSuggestions.mockReturnValue(storedSuggestions);
+
+ createComponent();
+ await waitForPromises();
+ });
+
+ it('suggestions are loaded from a correct key', () => {
+ expect(getRecentlyUsedSuggestions).toHaveBeenCalledWith(mockStorageKey);
+ });
+
+ it('displays stored tags suggestions', () => {
+ expect(findGlFilteredSearchSuggestions()).toHaveLength(
+ mockTags.length + storedSuggestions.length,
+ );
+
+ expect(findGlFilteredSearchSuggestions().at(0).text()).toBe(storedSuggestions[0].text);
+ });
+ });
+
+ describe('when the users filters suggestions', () => {
+ beforeEach(async () => {
+ findGlFilteredSearchToken().vm.$emit('input', { data: mockSearchTerm });
+
+ jest.runAllTimers();
+ });
+
+ it('requests filtered tags suggestions', async () => {
+ await waitForPromises();
+
+ expect(mock.history.get[1].params).toEqual({ search: mockSearchTerm });
+ });
+
+ it('shows the loading icon', async () => {
+ await nextTick();
+
+ expect(findGlLoadingIcon().exists()).toBe(true);
+ });
+
+ it('displays filtered tags suggestions', async () => {
+ await waitForPromises();
+
+ expect(findGlFilteredSearchSuggestions()).toHaveLength(mockTagsFiltered.length);
+
+ expect(findGlFilteredSearchSuggestions().at(0).text()).toBe(mockTagsFiltered[0].name);
+ });
+ });
+
+ describe('when suggestions cannot be loaded', () => {
+ beforeEach(async () => {
+ mock.onGet(TAG_SUGGESTIONS_PATH, { params: { search: '' } }).reply(500);
+
+ createComponent();
+ await waitForPromises();
+ });
+
+ it('error is shown', async () => {
+ expect(createFlash).toHaveBeenCalledTimes(1);
+ expect(createFlash).toHaveBeenCalledWith({ message: expect.any(String) });
+ });
+ });
+
+ describe('when the user selects a value', () => {
+ beforeEach(async () => {
+ createComponent({ value: { data: mockTags[0].name } });
+ findGlFilteredSearchToken().vm.$emit('select');
+
+ await waitForPromises();
+ });
+
+ it('selected tag is displayed', async () => {
+ expect(findToken().exists()).toBe(true);
+ });
+ });
+});
diff --git a/spec/frontend/runner/runner_detail/runner_details_app_spec.js b/spec/frontend/runner/runner_detail/runner_details_app_spec.js
index d0bd701458d..1a1428e8cb1 100644
--- a/spec/frontend/runner/runner_detail/runner_details_app_spec.js
+++ b/spec/frontend/runner/runner_detail/runner_details_app_spec.js
@@ -2,14 +2,19 @@ import { createLocalVue, mount, shallowMount } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
+import createFlash from '~/flash';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import RunnerTypeBadge from '~/runner/components/runner_type_badge.vue';
import getRunnerQuery from '~/runner/graphql/get_runner.query.graphql';
import RunnerDetailsApp from '~/runner/runner_details/runner_details_app.vue';
+import { captureException } from '~/runner/sentry_utils';
import { runnerData } from '../mock_data';
+jest.mock('~/flash');
+jest.mock('~/runner/sentry_utils');
+
const mockRunnerGraphqlId = runnerData.data.runner.id;
const mockRunnerId = `${getIdFromGraphQLId(mockRunnerGraphqlId)}`;
@@ -23,11 +28,9 @@ describe('RunnerDetailsApp', () => {
const findRunnerTypeBadge = () => wrapper.findComponent(RunnerTypeBadge);
const createComponentWithApollo = ({ props = {}, mountFn = shallowMount } = {}) => {
- const handlers = [[getRunnerQuery, mockRunnerQuery]];
-
wrapper = mountFn(RunnerDetailsApp, {
localVue,
- apolloProvider: createMockApollo(handlers),
+ apolloProvider: createMockApollo([[getRunnerQuery, mockRunnerQuery]]),
propsData: {
runnerId: mockRunnerId,
...props,
@@ -63,4 +66,22 @@ describe('RunnerDetailsApp', () => {
expect(findRunnerTypeBadge().text()).toBe('shared');
});
+
+ describe('When there is an error', () => {
+ beforeEach(async () => {
+ mockRunnerQuery = jest.fn().mockRejectedValueOnce(new Error('Error!'));
+ await createComponentWithApollo();
+ });
+
+ it('error is reported to sentry', async () => {
+ expect(captureException).toHaveBeenCalledWith({
+ error: new Error('Network error: Error!'),
+ component: 'RunnerDetailsApp',
+ });
+ });
+
+ it('error is shown to the user', async () => {
+ expect(createFlash).toHaveBeenCalled();
+ });
+ });
});
diff --git a/spec/frontend/runner/runner_detail/runner_update_form_utils_spec.js b/spec/frontend/runner/runner_detail/runner_update_form_utils_spec.js
new file mode 100644
index 00000000000..510b4e604ac
--- /dev/null
+++ b/spec/frontend/runner/runner_detail/runner_update_form_utils_spec.js
@@ -0,0 +1,96 @@
+import { ACCESS_LEVEL_NOT_PROTECTED } from '~/runner/constants';
+import {
+ modelToUpdateMutationVariables,
+ runnerToModel,
+} from '~/runner/runner_details/runner_update_form_utils';
+
+const mockId = 'gid://gitlab/Ci::Runner/1';
+const mockDescription = 'Runner Desc.';
+
+const mockRunner = {
+ id: mockId,
+ description: mockDescription,
+ maximumTimeout: 100,
+ accessLevel: ACCESS_LEVEL_NOT_PROTECTED,
+ active: true,
+ locked: true,
+ runUntagged: true,
+ tagList: ['tag-1', 'tag-2'],
+};
+
+const mockModel = {
+ ...mockRunner,
+ tagList: 'tag-1, tag-2',
+};
+
+describe('~/runner/runner_details/runner_update_form_utils', () => {
+ describe('runnerToModel', () => {
+ it('collects all model data', () => {
+ expect(runnerToModel(mockRunner)).toEqual(mockModel);
+ });
+
+ it('does not collect other data', () => {
+ const model = runnerToModel({
+ ...mockRunner,
+ unrelated: 'unrelatedValue',
+ });
+
+ expect(model.unrelated).toEqual(undefined);
+ });
+
+ it('tag list defaults to an empty string', () => {
+ const model = runnerToModel({
+ ...mockRunner,
+ tagList: undefined,
+ });
+
+ expect(model.tagList).toEqual('');
+ });
+ });
+
+ describe('modelToUpdateMutationVariables', () => {
+ it('collects all model data', () => {
+ expect(modelToUpdateMutationVariables(mockModel)).toEqual({
+ input: {
+ ...mockRunner,
+ },
+ });
+ });
+
+ it('collects a nullable timeout from the model', () => {
+ const variables = modelToUpdateMutationVariables({
+ ...mockModel,
+ maximumTimeout: '',
+ });
+
+ expect(variables).toEqual({
+ input: {
+ ...mockRunner,
+ maximumTimeout: null,
+ },
+ });
+ });
+
+ it.each`
+ tagList | tagListInput
+ ${''} | ${[]}
+ ${'tag1, tag2'} | ${['tag1', 'tag2']}
+ ${'with spaces'} | ${['with spaces']}
+ ${',,,,, commas'} | ${['commas']}
+ ${'more ,,,,, commas'} | ${['more', 'commas']}
+ ${' trimmed , trimmed2 '} | ${['trimmed', 'trimmed2']}
+ `('collect tags separated by commas for "$value"', ({ tagList, tagListInput }) => {
+ const variables = modelToUpdateMutationVariables({
+ ...mockModel,
+ tagList,
+ });
+
+ expect(variables).toEqual({
+ input: {
+ ...mockRunner,
+ tagList: tagListInput,
+ },
+ });
+ });
+ });
+});
diff --git a/spec/frontend/runner/runner_list/runner_list_app_spec.js b/spec/frontend/runner/runner_list/runner_list_app_spec.js
index dd913df7143..54b7d1f1bdb 100644
--- a/spec/frontend/runner/runner_list/runner_list_app_spec.js
+++ b/spec/frontend/runner/runner_list/runner_list_app_spec.js
@@ -1,9 +1,9 @@
-import * as Sentry from '@sentry/browser';
import { createLocalVue, mount, shallowMount } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import { TEST_HOST } from 'helpers/test_constants';
import waitForPromises from 'helpers/wait_for_promises';
+import createFlash from '~/flash';
import { updateHistory } from '~/lib/utils/url_utility';
import RunnerFilteredSearchBar from '~/runner/components/runner_filtered_search_bar.vue';
@@ -23,13 +23,15 @@ import {
} from '~/runner/constants';
import getRunnersQuery from '~/runner/graphql/get_runners.query.graphql';
import RunnerListApp from '~/runner/runner_list/runner_list_app.vue';
+import { captureException } from '~/runner/sentry_utils';
import { runnersData, runnersDataPaginated } from '../mock_data';
const mockRegistrationToken = 'MOCK_REGISTRATION_TOKEN';
const mockActiveRunnersCount = 2;
-jest.mock('@sentry/browser');
+jest.mock('~/flash');
+jest.mock('~/runner/sentry_utils');
jest.mock('~/lib/utils/url_utility', () => ({
...jest.requireActual('~/lib/utils/url_utility'),
updateHistory: jest.fn(),
@@ -64,7 +66,7 @@ describe('RunnerListApp', () => {
};
const setQuery = (query) => {
- window.location.href = `${TEST_HOST}/admin/runners/${query}`;
+ window.location.href = `${TEST_HOST}/admin/runners?${query}`;
window.location.search = query;
};
@@ -80,11 +82,6 @@ describe('RunnerListApp', () => {
beforeEach(async () => {
setQuery('');
- Sentry.withScope.mockImplementation((fn) => {
- const scope = { setTag: jest.fn() };
- fn(scope);
- });
-
mockRunnersQuery = jest.fn().mockResolvedValue(runnersData);
createComponentWithApollo();
await waitForPromises();
@@ -119,7 +116,7 @@ describe('RunnerListApp', () => {
describe('when a filter is preselected', () => {
beforeEach(async () => {
- window.location.search = `?status[]=${STATUS_ACTIVE}&runner_type[]=${INSTANCE_TYPE}`;
+ setQuery(`?status[]=${STATUS_ACTIVE}&runner_type[]=${INSTANCE_TYPE}&tag[]=tag1`);
createComponentWithApollo();
await waitForPromises();
@@ -130,6 +127,7 @@ describe('RunnerListApp', () => {
filters: [
{ type: 'status', value: { data: STATUS_ACTIVE, operator: '=' } },
{ type: 'runner_type', value: { data: INSTANCE_TYPE, operator: '=' } },
+ { type: 'tag', value: { data: 'tag1', operator: '=' } },
],
sort: 'CREATED_DESC',
pagination: { page: 1 },
@@ -140,6 +138,7 @@ describe('RunnerListApp', () => {
expect(mockRunnersQuery).toHaveBeenLastCalledWith({
status: STATUS_ACTIVE,
type: INSTANCE_TYPE,
+ tagList: ['tag1'],
sort: DEFAULT_SORT,
first: RUNNER_PAGE_SIZE,
});
@@ -157,7 +156,7 @@ describe('RunnerListApp', () => {
it('updates the browser url', () => {
expect(updateHistory).toHaveBeenLastCalledWith({
title: expect.any(String),
- url: 'http://test.host/admin/runners/?status[]=ACTIVE&sort=CREATED_ASC',
+ url: 'http://test.host/admin/runners?status[]=ACTIVE&sort=CREATED_ASC',
});
});
@@ -189,15 +188,21 @@ describe('RunnerListApp', () => {
describe('when runners query fails', () => {
beforeEach(async () => {
- mockRunnersQuery = jest.fn().mockRejectedValue(new Error());
+ mockRunnersQuery = jest.fn().mockRejectedValue(new Error('Error!'));
createComponentWithApollo();
await waitForPromises();
});
it('error is reported to sentry', async () => {
- expect(Sentry.withScope).toHaveBeenCalled();
- expect(Sentry.captureException).toHaveBeenCalled();
+ expect(captureException).toHaveBeenCalledWith({
+ error: new Error('Network error: Error!'),
+ component: 'RunnerListApp',
+ });
+ });
+
+ it('error is shown to the user', async () => {
+ expect(createFlash).toHaveBeenCalledTimes(1);
});
});
diff --git a/spec/frontend/runner/runner_list/runner_search_utils_spec.js b/spec/frontend/runner/runner_list/runner_search_utils_spec.js
index a1f33e9c880..e7969676549 100644
--- a/spec/frontend/runner/runner_list/runner_search_utils_spec.js
+++ b/spec/frontend/runner/runner_list/runner_search_utils_spec.js
@@ -99,6 +99,37 @@ describe('search_params.js', () => {
},
},
{
+ name: 'a tag',
+ urlQuery: '?tag[]=tag-1',
+ search: {
+ filters: [{ type: 'tag', value: { data: 'tag-1', operator: '=' } }],
+ pagination: { page: 1 },
+ sort: 'CREATED_DESC',
+ },
+ graphqlVariables: {
+ tagList: ['tag-1'],
+ first: 20,
+ sort: 'CREATED_DESC',
+ },
+ },
+ {
+ name: 'two tags',
+ urlQuery: '?tag[]=tag-1&tag[]=tag-2',
+ search: {
+ filters: [
+ { type: 'tag', value: { data: 'tag-1', operator: '=' } },
+ { type: 'tag', value: { data: 'tag-2', operator: '=' } },
+ ],
+ pagination: { page: 1 },
+ sort: 'CREATED_DESC',
+ },
+ graphqlVariables: {
+ tagList: ['tag-1', 'tag-2'],
+ first: 20,
+ sort: 'CREATED_DESC',
+ },
+ },
+ {
name: 'the next page',
urlQuery: '?page=2&after=AFTER_CURSOR',
search: { filters: [], pagination: { page: 2, after: 'AFTER_CURSOR' }, sort: 'CREATED_DESC' },
@@ -115,14 +146,15 @@ describe('search_params.js', () => {
graphqlVariables: { sort: 'CREATED_DESC', before: 'BEFORE_CURSOR', last: RUNNER_PAGE_SIZE },
},
{
- name:
- 'the next page filtered by multiple status, a single instance type and a non default sort',
+ name: 'the next page filtered by a status, an instance type, tags and a non default sort',
urlQuery:
- '?status[]=ACTIVE&runner_type[]=INSTANCE_TYPE&sort=CREATED_ASC&page=2&after=AFTER_CURSOR',
+ '?status[]=ACTIVE&runner_type[]=INSTANCE_TYPE&tag[]=tag-1&tag[]=tag-2&sort=CREATED_ASC&page=2&after=AFTER_CURSOR',
search: {
filters: [
{ type: 'status', value: { data: 'ACTIVE', operator: '=' } },
{ type: 'runner_type', value: { data: 'INSTANCE_TYPE', operator: '=' } },
+ { type: 'tag', value: { data: 'tag-1', operator: '=' } },
+ { type: 'tag', value: { data: 'tag-2', operator: '=' } },
],
pagination: { page: 2, after: 'AFTER_CURSOR' },
sort: 'CREATED_ASC',
@@ -130,6 +162,7 @@ describe('search_params.js', () => {
graphqlVariables: {
status: 'ACTIVE',
type: 'INSTANCE_TYPE',
+ tagList: ['tag-1', 'tag-2'],
sort: 'CREATED_ASC',
after: 'AFTER_CURSOR',
first: RUNNER_PAGE_SIZE,
diff --git a/spec/frontend/runner/sentry_utils_spec.js b/spec/frontend/runner/sentry_utils_spec.js
new file mode 100644
index 00000000000..b61eb63961e
--- /dev/null
+++ b/spec/frontend/runner/sentry_utils_spec.js
@@ -0,0 +1,39 @@
+import * as Sentry from '@sentry/browser';
+import { captureException } from '~/runner/sentry_utils';
+
+jest.mock('@sentry/browser');
+
+describe('~/runner/sentry_utils', () => {
+ let mockSetTag;
+
+ beforeEach(async () => {
+ mockSetTag = jest.fn();
+
+ Sentry.withScope.mockImplementation((fn) => {
+ const scope = { setTag: mockSetTag };
+ fn(scope);
+ });
+ });
+
+ describe('captureException', () => {
+ const mockError = new Error('Something went wrong!');
+
+ it('error is reported to sentry', () => {
+ captureException({ error: mockError });
+
+ expect(Sentry.withScope).toHaveBeenCalled();
+ expect(Sentry.captureException).toHaveBeenCalledWith(mockError);
+ });
+
+ it('error is reported to sentry with a component name', () => {
+ const mockComponentName = 'MyComponent';
+
+ captureException({ error: mockError, component: mockComponentName });
+
+ expect(Sentry.withScope).toHaveBeenCalled();
+ expect(Sentry.captureException).toHaveBeenCalledWith(mockError);
+
+ expect(mockSetTag).toHaveBeenCalledWith('vue_component', mockComponentName);
+ });
+ });
+});
diff --git a/spec/frontend/search/mock_data.js b/spec/frontend/search/mock_data.js
index fbe01f372b0..24ce45e8a09 100644
--- a/spec/frontend/search/mock_data.js
+++ b/spec/frontend/search/mock_data.js
@@ -1,3 +1,6 @@
+import { GROUPS_LOCAL_STORAGE_KEY, PROJECTS_LOCAL_STORAGE_KEY } from '~/search/store/constants';
+import * as types from '~/search/store/mutation_types';
+
export const MOCK_QUERY = {
scope: 'issues',
state: 'all',
@@ -6,45 +9,45 @@ export const MOCK_QUERY = {
};
export const MOCK_GROUP = {
+ id: 1,
name: 'test group',
full_name: 'full name / test group',
- id: 1,
};
export const MOCK_GROUPS = [
{
+ id: 1,
avatar_url: null,
name: 'test group',
full_name: 'full name / test group',
- id: 1,
},
{
+ id: 2,
avatar_url: 'https://avatar.com',
name: 'test group 2',
full_name: 'full name / test group 2',
- id: 2,
},
];
export const MOCK_PROJECT = {
+ id: 1,
name: 'test project',
namespace: MOCK_GROUP,
nameWithNamespace: 'test group / test project',
- id: 1,
};
export const MOCK_PROJECTS = [
{
+ id: 1,
name: 'test project',
namespace: MOCK_GROUP,
name_with_namespace: 'test group / test project',
- id: 1,
},
{
+ id: 2,
name: 'test project 2',
namespace: MOCK_GROUP,
name_with_namespace: 'test group / test project 2',
- id: 2,
},
];
@@ -63,3 +66,41 @@ export const MOCK_SORT_OPTIONS = [
},
},
];
+
+export const MOCK_LS_KEY = 'mock-ls-key';
+
+export const MOCK_INFLATED_DATA = [
+ { id: 1, name: 'test 1' },
+ { id: 2, name: 'test 2' },
+];
+
+export const FRESH_STORED_DATA = [
+ { id: 1, name: 'test 1', frequency: 1 },
+ { id: 2, name: 'test 2', frequency: 2 },
+];
+
+export const STALE_STORED_DATA = [
+ { id: 1, name: 'blah 1', frequency: 1 },
+ { id: 2, name: 'blah 2', frequency: 2 },
+];
+
+export const MOCK_FRESH_DATA_RES = { name: 'fresh' };
+
+export const PROMISE_ALL_EXPECTED_MUTATIONS = {
+ initGroups: {
+ type: types.LOAD_FREQUENT_ITEMS,
+ payload: { key: GROUPS_LOCAL_STORAGE_KEY, data: FRESH_STORED_DATA },
+ },
+ resGroups: {
+ type: types.LOAD_FREQUENT_ITEMS,
+ payload: { key: GROUPS_LOCAL_STORAGE_KEY, data: [MOCK_FRESH_DATA_RES, MOCK_FRESH_DATA_RES] },
+ },
+ initProjects: {
+ type: types.LOAD_FREQUENT_ITEMS,
+ payload: { key: PROJECTS_LOCAL_STORAGE_KEY, data: FRESH_STORED_DATA },
+ },
+ resProjects: {
+ type: types.LOAD_FREQUENT_ITEMS,
+ payload: { key: PROJECTS_LOCAL_STORAGE_KEY, data: [MOCK_FRESH_DATA_RES, MOCK_FRESH_DATA_RES] },
+ },
+};
diff --git a/spec/frontend/search/store/actions_spec.js b/spec/frontend/search/store/actions_spec.js
index 634661c5843..3755f8ffae7 100644
--- a/spec/frontend/search/store/actions_spec.js
+++ b/spec/frontend/search/store/actions_spec.js
@@ -5,9 +5,20 @@ import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import * as urlUtils from '~/lib/utils/url_utility';
import * as actions from '~/search/store/actions';
+import { GROUPS_LOCAL_STORAGE_KEY, PROJECTS_LOCAL_STORAGE_KEY } from '~/search/store/constants';
import * as types from '~/search/store/mutation_types';
import createState from '~/search/store/state';
-import { MOCK_QUERY, MOCK_GROUPS, MOCK_PROJECT, MOCK_PROJECTS } from '../mock_data';
+import * as storeUtils from '~/search/store/utils';
+import {
+ MOCK_QUERY,
+ MOCK_GROUPS,
+ MOCK_PROJECT,
+ MOCK_PROJECTS,
+ MOCK_GROUP,
+ FRESH_STORED_DATA,
+ MOCK_FRESH_DATA_RES,
+ PROMISE_ALL_EXPECTED_MUTATIONS,
+} from '../mock_data';
jest.mock('~/flash');
jest.mock('~/lib/utils/url_utility', () => ({
@@ -56,6 +67,46 @@ describe('Global Search Store Actions', () => {
});
});
+ describe.each`
+ action | axiosMock | type | expectedMutations | flashCallCount | lsKey
+ ${actions.loadFrequentGroups} | ${{ method: 'onGet', code: 200 }} | ${'success'} | ${[PROMISE_ALL_EXPECTED_MUTATIONS.initGroups, PROMISE_ALL_EXPECTED_MUTATIONS.resGroups]} | ${0} | ${GROUPS_LOCAL_STORAGE_KEY}
+ ${actions.loadFrequentGroups} | ${{ method: 'onGet', code: 500 }} | ${'error'} | ${[PROMISE_ALL_EXPECTED_MUTATIONS.initGroups]} | ${1} | ${GROUPS_LOCAL_STORAGE_KEY}
+ ${actions.loadFrequentProjects} | ${{ method: 'onGet', code: 200 }} | ${'success'} | ${[PROMISE_ALL_EXPECTED_MUTATIONS.initProjects, PROMISE_ALL_EXPECTED_MUTATIONS.resProjects]} | ${0} | ${PROJECTS_LOCAL_STORAGE_KEY}
+ ${actions.loadFrequentProjects} | ${{ method: 'onGet', code: 500 }} | ${'error'} | ${[PROMISE_ALL_EXPECTED_MUTATIONS.initProjects]} | ${1} | ${PROJECTS_LOCAL_STORAGE_KEY}
+ `(
+ 'Promise.all calls',
+ ({ action, axiosMock, type, expectedMutations, flashCallCount, lsKey }) => {
+ describe(action.name, () => {
+ describe(`on ${type}`, () => {
+ beforeEach(() => {
+ storeUtils.loadDataFromLS = jest.fn().mockReturnValue(FRESH_STORED_DATA);
+ mock[axiosMock.method]().reply(axiosMock.code, MOCK_FRESH_DATA_RES);
+ });
+
+ it(`should dispatch the correct mutations`, () => {
+ return testAction({ action, state, expectedMutations }).then(() => {
+ expect(storeUtils.loadDataFromLS).toHaveBeenCalledWith(lsKey);
+ flashCallback(flashCallCount);
+ });
+ });
+ });
+ });
+ },
+ );
+
+ describe('getGroupsData', () => {
+ const mockCommit = () => {};
+ beforeEach(() => {
+ jest.spyOn(Api, 'groups').mockResolvedValue(MOCK_GROUPS);
+ });
+
+ it('calls Api.groups with order_by set to similarity', () => {
+ actions.fetchGroups({ commit: mockCommit }, 'test');
+
+ expect(Api.groups).toHaveBeenCalledWith('test', { order_by: 'similarity' });
+ });
+ });
+
describe('getProjectsData', () => {
const mockCommit = () => {};
beforeEach(() => {
@@ -64,10 +115,19 @@ describe('Global Search Store Actions', () => {
});
describe('when groupId is set', () => {
- it('calls Api.groupProjects', () => {
+ it('calls Api.groupProjects with expected parameters', () => {
actions.fetchProjects({ commit: mockCommit, state });
- expect(Api.groupProjects).toHaveBeenCalled();
+ expect(Api.groupProjects).toHaveBeenCalledWith(
+ state.query.group_id,
+ state.query.search,
+ {
+ order_by: 'similarity',
+ include_subgroups: true,
+ with_shared: false,
+ },
+ expect.any(Function),
+ );
expect(Api.projects).not.toHaveBeenCalled();
});
});
@@ -121,4 +181,44 @@ describe('Global Search Store Actions', () => {
});
});
});
+
+ describe('setFrequentGroup', () => {
+ beforeEach(() => {
+ storeUtils.setFrequentItemToLS = jest.fn();
+ });
+
+ it(`calls setFrequentItemToLS with ${GROUPS_LOCAL_STORAGE_KEY} and item data`, async () => {
+ await testAction({
+ action: actions.setFrequentGroup,
+ payload: MOCK_GROUP,
+ state,
+ });
+
+ expect(storeUtils.setFrequentItemToLS).toHaveBeenCalledWith(
+ GROUPS_LOCAL_STORAGE_KEY,
+ state.frequentItems,
+ MOCK_GROUP,
+ );
+ });
+ });
+
+ describe('setFrequentProject', () => {
+ beforeEach(() => {
+ storeUtils.setFrequentItemToLS = jest.fn();
+ });
+
+ it(`calls setFrequentItemToLS with ${PROJECTS_LOCAL_STORAGE_KEY} and item data`, async () => {
+ await testAction({
+ action: actions.setFrequentProject,
+ payload: MOCK_PROJECT,
+ state,
+ });
+
+ expect(storeUtils.setFrequentItemToLS).toHaveBeenCalledWith(
+ PROJECTS_LOCAL_STORAGE_KEY,
+ state.frequentItems,
+ MOCK_PROJECT,
+ );
+ });
+ });
});
diff --git a/spec/frontend/search/store/getters_spec.js b/spec/frontend/search/store/getters_spec.js
new file mode 100644
index 00000000000..081e6a986eb
--- /dev/null
+++ b/spec/frontend/search/store/getters_spec.js
@@ -0,0 +1,32 @@
+import { GROUPS_LOCAL_STORAGE_KEY, PROJECTS_LOCAL_STORAGE_KEY } from '~/search/store/constants';
+import * as getters from '~/search/store/getters';
+import createState from '~/search/store/state';
+import { MOCK_QUERY, MOCK_GROUPS, MOCK_PROJECTS } from '../mock_data';
+
+describe('Global Search Store Getters', () => {
+ let state;
+
+ beforeEach(() => {
+ state = createState({ query: MOCK_QUERY });
+ });
+
+ describe('frequentGroups', () => {
+ beforeEach(() => {
+ state.frequentItems[GROUPS_LOCAL_STORAGE_KEY] = MOCK_GROUPS;
+ });
+
+ it('returns the correct data', () => {
+ expect(getters.frequentGroups(state)).toStrictEqual(MOCK_GROUPS);
+ });
+ });
+
+ describe('frequentProjects', () => {
+ beforeEach(() => {
+ state.frequentItems[PROJECTS_LOCAL_STORAGE_KEY] = MOCK_PROJECTS;
+ });
+
+ it('returns the correct data', () => {
+ expect(getters.frequentProjects(state)).toStrictEqual(MOCK_PROJECTS);
+ });
+ });
+});
diff --git a/spec/frontend/search/store/mutations_spec.js b/spec/frontend/search/store/mutations_spec.js
index df94ba40ff2..a60718a972d 100644
--- a/spec/frontend/search/store/mutations_spec.js
+++ b/spec/frontend/search/store/mutations_spec.js
@@ -71,4 +71,13 @@ describe('Global Search Store Mutations', () => {
expect(state.query[payload.key]).toBe(payload.value);
});
});
+
+ describe('LOAD_FREQUENT_ITEMS', () => {
+ it('sets frequentItems[key] to data', () => {
+ const payload = { key: 'test-key', data: [1, 2, 3] };
+ mutations[types.LOAD_FREQUENT_ITEMS](state, payload);
+
+ expect(state.frequentItems[payload.key]).toStrictEqual(payload.data);
+ });
+ });
});
diff --git a/spec/frontend/search/store/utils_spec.js b/spec/frontend/search/store/utils_spec.js
new file mode 100644
index 00000000000..5055fa2cc3d
--- /dev/null
+++ b/spec/frontend/search/store/utils_spec.js
@@ -0,0 +1,197 @@
+import { useLocalStorageSpy } from 'helpers/local_storage_helper';
+import { MAX_FREQUENCY } from '~/search/store/constants';
+import { loadDataFromLS, setFrequentItemToLS, mergeById } from '~/search/store/utils';
+import {
+ MOCK_LS_KEY,
+ MOCK_GROUPS,
+ MOCK_INFLATED_DATA,
+ FRESH_STORED_DATA,
+ STALE_STORED_DATA,
+} from '../mock_data';
+
+const PREV_TIME = new Date().getTime() - 1;
+const CURRENT_TIME = new Date().getTime();
+
+useLocalStorageSpy();
+jest.mock('~/lib/utils/accessor', () => ({
+ isLocalStorageAccessSafe: jest.fn().mockReturnValue(true),
+}));
+
+describe('Global Search Store Utils', () => {
+ afterEach(() => {
+ localStorage.clear();
+ });
+
+ describe('loadDataFromLS', () => {
+ let res;
+
+ describe('with valid data', () => {
+ beforeEach(() => {
+ localStorage.setItem(MOCK_LS_KEY, JSON.stringify(MOCK_GROUPS));
+ res = loadDataFromLS(MOCK_LS_KEY);
+ });
+
+ it('returns parsed array', () => {
+ expect(res).toStrictEqual(MOCK_GROUPS);
+ });
+ });
+
+ describe('with invalid data', () => {
+ beforeEach(() => {
+ localStorage.setItem(MOCK_LS_KEY, '[}');
+ res = loadDataFromLS(MOCK_LS_KEY);
+ });
+
+ it('wipes local storage and returns an empty array', () => {
+ expect(localStorage.removeItem).toHaveBeenCalledWith(MOCK_LS_KEY);
+ expect(res).toStrictEqual([]);
+ });
+ });
+ });
+
+ describe('setFrequentItemToLS', () => {
+ const frequentItems = {};
+
+ describe('with existing data', () => {
+ describe(`when frequency is less than ${MAX_FREQUENCY}`, () => {
+ beforeEach(() => {
+ frequentItems[MOCK_LS_KEY] = [{ ...MOCK_GROUPS[0], frequency: 1, lastUsed: PREV_TIME }];
+ setFrequentItemToLS(MOCK_LS_KEY, frequentItems, MOCK_GROUPS[0]);
+ });
+
+ it('adds 1 to the frequency, tracks lastUsed, and calls localStorage.setItem', () => {
+ expect(localStorage.setItem).toHaveBeenCalledWith(
+ MOCK_LS_KEY,
+ JSON.stringify([{ ...MOCK_GROUPS[0], frequency: 2, lastUsed: CURRENT_TIME }]),
+ );
+ });
+ });
+
+ describe(`when frequency is equal to ${MAX_FREQUENCY}`, () => {
+ beforeEach(() => {
+ frequentItems[MOCK_LS_KEY] = [
+ { ...MOCK_GROUPS[0], frequency: MAX_FREQUENCY, lastUsed: PREV_TIME },
+ ];
+ setFrequentItemToLS(MOCK_LS_KEY, frequentItems, MOCK_GROUPS[0]);
+ });
+
+ it(`does not further increase frequency past ${MAX_FREQUENCY}, tracks lastUsed, and calls localStorage.setItem`, () => {
+ expect(localStorage.setItem).toHaveBeenCalledWith(
+ MOCK_LS_KEY,
+ JSON.stringify([
+ { ...MOCK_GROUPS[0], frequency: MAX_FREQUENCY, lastUsed: CURRENT_TIME },
+ ]),
+ );
+ });
+ });
+ });
+
+ describe('with no existing data', () => {
+ beforeEach(() => {
+ frequentItems[MOCK_LS_KEY] = [];
+ setFrequentItemToLS(MOCK_LS_KEY, frequentItems, MOCK_GROUPS[0]);
+ });
+
+ it('adds a new entry with frequency 1, tracks lastUsed, and calls localStorage.setItem', () => {
+ expect(localStorage.setItem).toHaveBeenCalledWith(
+ MOCK_LS_KEY,
+ JSON.stringify([{ ...MOCK_GROUPS[0], frequency: 1, lastUsed: CURRENT_TIME }]),
+ );
+ });
+ });
+
+ describe('with multiple entries', () => {
+ beforeEach(() => {
+ frequentItems[MOCK_LS_KEY] = [
+ { id: 1, frequency: 2, lastUsed: PREV_TIME },
+ { id: 2, frequency: 1, lastUsed: PREV_TIME },
+ { id: 3, frequency: 1, lastUsed: PREV_TIME },
+ ];
+ setFrequentItemToLS(MOCK_LS_KEY, frequentItems, { id: 3 });
+ });
+
+ it('sorts the array by most frequent and lastUsed', () => {
+ expect(localStorage.setItem).toHaveBeenCalledWith(
+ MOCK_LS_KEY,
+ JSON.stringify([
+ { id: 3, frequency: 2, lastUsed: CURRENT_TIME },
+ { id: 1, frequency: 2, lastUsed: PREV_TIME },
+ { id: 2, frequency: 1, lastUsed: PREV_TIME },
+ ]),
+ );
+ });
+ });
+
+ describe('with max entries', () => {
+ beforeEach(() => {
+ frequentItems[MOCK_LS_KEY] = [
+ { id: 1, frequency: 5, lastUsed: PREV_TIME },
+ { id: 2, frequency: 4, lastUsed: PREV_TIME },
+ { id: 3, frequency: 3, lastUsed: PREV_TIME },
+ { id: 4, frequency: 2, lastUsed: PREV_TIME },
+ { id: 5, frequency: 1, lastUsed: PREV_TIME },
+ ];
+ setFrequentItemToLS(MOCK_LS_KEY, frequentItems, { id: 6 });
+ });
+
+ it('removes the last item in the array', () => {
+ expect(localStorage.setItem).toHaveBeenCalledWith(
+ MOCK_LS_KEY,
+ JSON.stringify([
+ { id: 1, frequency: 5, lastUsed: PREV_TIME },
+ { id: 2, frequency: 4, lastUsed: PREV_TIME },
+ { id: 3, frequency: 3, lastUsed: PREV_TIME },
+ { id: 4, frequency: 2, lastUsed: PREV_TIME },
+ { id: 6, frequency: 1, lastUsed: CURRENT_TIME },
+ ]),
+ );
+ });
+ });
+
+ describe('with null data loaded in', () => {
+ beforeEach(() => {
+ frequentItems[MOCK_LS_KEY] = null;
+ setFrequentItemToLS(MOCK_LS_KEY, frequentItems, MOCK_GROUPS[0]);
+ });
+
+ it('wipes local storage', () => {
+ expect(localStorage.removeItem).toHaveBeenCalledWith(MOCK_LS_KEY);
+ });
+ });
+
+ describe('with additional data', () => {
+ beforeEach(() => {
+ const MOCK_ADDITIONAL_DATA_GROUP = { ...MOCK_GROUPS[0], extraData: 'test' };
+ frequentItems[MOCK_LS_KEY] = [];
+ setFrequentItemToLS(MOCK_LS_KEY, frequentItems, MOCK_ADDITIONAL_DATA_GROUP);
+ });
+
+ it('parses out extra data for LS', () => {
+ expect(localStorage.setItem).toHaveBeenCalledWith(
+ MOCK_LS_KEY,
+ JSON.stringify([{ ...MOCK_GROUPS[0], frequency: 1, lastUsed: CURRENT_TIME }]),
+ );
+ });
+ });
+ });
+
+ describe.each`
+ description | inflatedData | storedData | response
+ ${'identical'} | ${MOCK_INFLATED_DATA} | ${FRESH_STORED_DATA} | ${FRESH_STORED_DATA}
+ ${'stale'} | ${MOCK_INFLATED_DATA} | ${STALE_STORED_DATA} | ${FRESH_STORED_DATA}
+ ${'empty'} | ${MOCK_INFLATED_DATA} | ${[]} | ${MOCK_INFLATED_DATA}
+ ${'null'} | ${MOCK_INFLATED_DATA} | ${null} | ${MOCK_INFLATED_DATA}
+ `('mergeById', ({ description, inflatedData, storedData, response }) => {
+ describe(`with ${description} storedData`, () => {
+ let res;
+
+ beforeEach(() => {
+ res = mergeById(inflatedData, storedData);
+ });
+
+ it('prioritizes inflatedData and preserves frequency count', () => {
+ expect(response).toStrictEqual(res);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/search/topbar/components/group_filter_spec.js b/spec/frontend/search/topbar/components/group_filter_spec.js
index 15b46f9c058..fbd7ad6bb57 100644
--- a/spec/frontend/search/topbar/components/group_filter_spec.js
+++ b/spec/frontend/search/topbar/components/group_filter_spec.js
@@ -1,13 +1,14 @@
-import { createLocalVue, shallowMount } from '@vue/test-utils';
+import { shallowMount } from '@vue/test-utils';
+import Vue from 'vue';
import Vuex from 'vuex';
import { MOCK_GROUP, MOCK_QUERY } from 'jest/search/mock_data';
import { visitUrl, setUrlParams } from '~/lib/utils/url_utility';
+import { GROUPS_LOCAL_STORAGE_KEY } from '~/search/store/constants';
import GroupFilter from '~/search/topbar/components/group_filter.vue';
import SearchableDropdown from '~/search/topbar/components/searchable_dropdown.vue';
import { ANY_OPTION, GROUP_DATA, PROJECT_DATA } from '~/search/topbar/constants';
-const localVue = createLocalVue();
-localVue.use(Vuex);
+Vue.use(Vuex);
jest.mock('~/lib/utils/url_utility', () => ({
visitUrl: jest.fn(),
@@ -19,6 +20,8 @@ describe('GroupFilter', () => {
const actionSpies = {
fetchGroups: jest.fn(),
+ setFrequentGroup: jest.fn(),
+ loadFrequentGroups: jest.fn(),
};
const defaultProps = {
@@ -32,10 +35,12 @@ describe('GroupFilter', () => {
...initialState,
},
actions: actionSpies,
+ getters: {
+ frequentGroups: () => [],
+ },
});
wrapper = shallowMount(GroupFilter, {
- localVue,
store,
propsData: {
...defaultProps,
@@ -62,12 +67,14 @@ describe('GroupFilter', () => {
});
describe('events', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
describe('when @search is emitted', () => {
const search = 'test';
beforeEach(() => {
- createComponent();
-
findSearchableDropdown().vm.$emit('search', search);
});
@@ -77,14 +84,31 @@ describe('GroupFilter', () => {
});
});
- describe('when @change is emitted', () => {
+ describe('when @change is emitted with Any', () => {
beforeEach(() => {
- createComponent();
+ findSearchableDropdown().vm.$emit('change', ANY_OPTION);
+ });
+
+ it('calls setUrlParams with group null, project id null, and then calls visitUrl', () => {
+ expect(setUrlParams).toHaveBeenCalledWith({
+ [GROUP_DATA.queryParam]: null,
+ [PROJECT_DATA.queryParam]: null,
+ });
+
+ expect(visitUrl).toHaveBeenCalled();
+ });
+
+ it('does not call setFrequentGroup', () => {
+ expect(actionSpies.setFrequentGroup).not.toHaveBeenCalled();
+ });
+ });
+ describe('when @change is emitted with a group', () => {
+ beforeEach(() => {
findSearchableDropdown().vm.$emit('change', MOCK_GROUP);
});
- it('calls calls setUrlParams with group id, project id null, and visitUrl', () => {
+ it('calls setUrlParams with group id, project id null, and then calls visitUrl', () => {
expect(setUrlParams).toHaveBeenCalledWith({
[GROUP_DATA.queryParam]: MOCK_GROUP.id,
[PROJECT_DATA.queryParam]: null,
@@ -92,6 +116,20 @@ describe('GroupFilter', () => {
expect(visitUrl).toHaveBeenCalled();
});
+
+ it(`calls setFrequentGroup with the group and ${GROUPS_LOCAL_STORAGE_KEY}`, () => {
+ expect(actionSpies.setFrequentGroup).toHaveBeenCalledWith(expect.any(Object), MOCK_GROUP);
+ });
+ });
+
+ describe('when @first-open is emitted', () => {
+ beforeEach(() => {
+ findSearchableDropdown().vm.$emit('first-open');
+ });
+
+ it('calls loadFrequentGroups', () => {
+ expect(actionSpies.loadFrequentGroups).toHaveBeenCalledTimes(1);
+ });
});
});
diff --git a/spec/frontend/search/topbar/components/project_filter_spec.js b/spec/frontend/search/topbar/components/project_filter_spec.js
index 3bd0769b34a..63b0f882ca4 100644
--- a/spec/frontend/search/topbar/components/project_filter_spec.js
+++ b/spec/frontend/search/topbar/components/project_filter_spec.js
@@ -1,13 +1,14 @@
-import { createLocalVue, shallowMount } from '@vue/test-utils';
+import { shallowMount } from '@vue/test-utils';
+import Vue from 'vue';
import Vuex from 'vuex';
import { MOCK_PROJECT, MOCK_QUERY } from 'jest/search/mock_data';
import { visitUrl, setUrlParams } from '~/lib/utils/url_utility';
+import { PROJECTS_LOCAL_STORAGE_KEY } from '~/search/store/constants';
import ProjectFilter from '~/search/topbar/components/project_filter.vue';
import SearchableDropdown from '~/search/topbar/components/searchable_dropdown.vue';
import { ANY_OPTION, GROUP_DATA, PROJECT_DATA } from '~/search/topbar/constants';
-const localVue = createLocalVue();
-localVue.use(Vuex);
+Vue.use(Vuex);
jest.mock('~/lib/utils/url_utility', () => ({
visitUrl: jest.fn(),
@@ -19,6 +20,8 @@ describe('ProjectFilter', () => {
const actionSpies = {
fetchProjects: jest.fn(),
+ setFrequentProject: jest.fn(),
+ loadFrequentProjects: jest.fn(),
};
const defaultProps = {
@@ -32,10 +35,12 @@ describe('ProjectFilter', () => {
...initialState,
},
actions: actionSpies,
+ getters: {
+ frequentProjects: () => [],
+ },
});
wrapper = shallowMount(ProjectFilter, {
- localVue,
store,
propsData: {
...defaultProps,
@@ -84,12 +89,16 @@ describe('ProjectFilter', () => {
findSearchableDropdown().vm.$emit('change', ANY_OPTION);
});
- it('calls setUrlParams with project id, not group id, then calls visitUrl', () => {
+ it('calls setUrlParams with null, no group id, then calls visitUrl', () => {
expect(setUrlParams).toHaveBeenCalledWith({
- [PROJECT_DATA.queryParam]: ANY_OPTION.id,
+ [PROJECT_DATA.queryParam]: null,
});
expect(visitUrl).toHaveBeenCalled();
});
+
+ it('does not call setFrequentProject', () => {
+ expect(actionSpies.setFrequentProject).not.toHaveBeenCalled();
+ });
});
describe('with a Project', () => {
@@ -104,6 +113,23 @@ describe('ProjectFilter', () => {
});
expect(visitUrl).toHaveBeenCalled();
});
+
+ it(`calls setFrequentProject with the group and ${PROJECTS_LOCAL_STORAGE_KEY}`, () => {
+ expect(actionSpies.setFrequentProject).toHaveBeenCalledWith(
+ expect.any(Object),
+ MOCK_PROJECT,
+ );
+ });
+ });
+ });
+
+ describe('when @first-open is emitted', () => {
+ beforeEach(() => {
+ findSearchableDropdown().vm.$emit('first-open');
+ });
+
+ it('calls loadFrequentProjects', () => {
+ expect(actionSpies.loadFrequentProjects).toHaveBeenCalledTimes(1);
});
});
});
diff --git a/spec/frontend/search/topbar/components/searchable_dropdown_spec.js b/spec/frontend/search/topbar/components/searchable_dropdown_spec.js
index 10d779f0f90..b21cf5c6b79 100644
--- a/spec/frontend/search/topbar/components/searchable_dropdown_spec.js
+++ b/spec/frontend/search/topbar/components/searchable_dropdown_spec.js
@@ -2,9 +2,9 @@ import { GlDropdown, GlDropdownItem, GlSearchBoxByType, GlSkeletonLoader } from
import { shallowMount, mount } from '@vue/test-utils';
import Vue from 'vue';
import Vuex from 'vuex';
+import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import { MOCK_GROUPS, MOCK_GROUP, MOCK_QUERY } from 'jest/search/mock_data';
import SearchableDropdown from '~/search/topbar/components/searchable_dropdown.vue';
-import SearchableDropdownItem from '~/search/topbar/components/searchable_dropdown_item.vue';
import { ANY_OPTION, GROUP_DATA } from '~/search/topbar/constants';
Vue.use(Vuex);
@@ -29,13 +29,15 @@ describe('Global Search Searchable Dropdown', () => {
},
});
- wrapper = mountFn(SearchableDropdown, {
- store,
- propsData: {
- ...defaultProps,
- ...props,
- },
- });
+ wrapper = extendedWrapper(
+ mountFn(SearchableDropdown, {
+ store,
+ propsData: {
+ ...defaultProps,
+ ...props,
+ },
+ }),
+ );
};
afterEach(() => {
@@ -45,10 +47,11 @@ describe('Global Search Searchable Dropdown', () => {
const findGlDropdown = () => wrapper.findComponent(GlDropdown);
const findGlDropdownSearch = () => findGlDropdown().findComponent(GlSearchBoxByType);
const findDropdownText = () => findGlDropdown().find('.dropdown-toggle-text');
- const findSearchableDropdownItems = () =>
- findGlDropdown().findAllComponents(SearchableDropdownItem);
+ const findSearchableDropdownItems = () => wrapper.findAllByTestId('searchable-items');
+ const findFrequentDropdownItems = () => wrapper.findAllByTestId('frequent-items');
const findAnyDropdownItem = () => findGlDropdown().findComponent(GlDropdownItem);
- const findFirstGroupDropdownItem = () => findSearchableDropdownItems().at(0);
+ const findFirstSearchableDropdownItem = () => findSearchableDropdownItems().at(0);
+ const findFirstFrequentDropdownItem = () => findFrequentDropdownItems().at(0);
const findLoader = () => wrapper.findComponent(GlSkeletonLoader);
describe('template', () => {
@@ -82,7 +85,7 @@ describe('Global Search Searchable Dropdown', () => {
});
});
- describe('findDropdownItems', () => {
+ describe('Searchable Dropdown Items', () => {
describe('when loading is false', () => {
beforeEach(() => {
createComponent({}, { items: MOCK_GROUPS });
@@ -96,7 +99,7 @@ describe('Global Search Searchable Dropdown', () => {
expect(findAnyDropdownItem().exists()).toBe(true);
});
- it('renders SearchableDropdownItem for each item', () => {
+ it('renders searchable dropdown item for each item', () => {
expect(findSearchableDropdownItems()).toHaveLength(MOCK_GROUPS.length);
});
});
@@ -114,12 +117,31 @@ describe('Global Search Searchable Dropdown', () => {
expect(findAnyDropdownItem().exists()).toBe(true);
});
- it('does not render SearchableDropdownItem', () => {
+ it('does not render searchable dropdown items', () => {
expect(findSearchableDropdownItems()).toHaveLength(0);
});
});
});
+ describe.each`
+ searchText | frequentItems | length
+ ${''} | ${[]} | ${0}
+ ${''} | ${MOCK_GROUPS} | ${MOCK_GROUPS.length}
+ ${'test'} | ${[]} | ${0}
+ ${'test'} | ${MOCK_GROUPS} | ${0}
+ `('Frequent Dropdown Items', ({ searchText, frequentItems, length }) => {
+ describe(`when search is ${searchText} and frequentItems length is ${frequentItems.length}`, () => {
+ beforeEach(() => {
+ createComponent({}, { frequentItems });
+ wrapper.setData({ searchText });
+ });
+
+ it(`should${length ? '' : ' not'} render frequent dropdown items`, () => {
+ expect(findFrequentDropdownItems()).toHaveLength(length);
+ });
+ });
+ });
+
describe('Dropdown Text', () => {
describe('when selectedItem is any', () => {
beforeEach(() => {
@@ -145,7 +167,7 @@ describe('Global Search Searchable Dropdown', () => {
describe('actions', () => {
beforeEach(() => {
- createComponent({}, { items: MOCK_GROUPS });
+ createComponent({}, { items: MOCK_GROUPS, frequentItems: MOCK_GROUPS });
});
it('clicking "Any" dropdown item $emits @change with ANY_OPTION', () => {
@@ -154,10 +176,41 @@ describe('Global Search Searchable Dropdown', () => {
expect(wrapper.emitted('change')[0]).toEqual([ANY_OPTION]);
});
- it('on SearchableDropdownItem @change, the wrapper $emits change with the item', () => {
- findFirstGroupDropdownItem().vm.$emit('change', MOCK_GROUPS[0]);
+ it('on searchable item @change, the wrapper $emits change with the item', () => {
+ findFirstSearchableDropdownItem().vm.$emit('change', MOCK_GROUPS[0]);
+
+ expect(wrapper.emitted('change')[0]).toEqual([MOCK_GROUPS[0]]);
+ });
+
+ it('on frequent item @change, the wrapper $emits change with the item', () => {
+ findFirstFrequentDropdownItem().vm.$emit('change', MOCK_GROUPS[0]);
expect(wrapper.emitted('change')[0]).toEqual([MOCK_GROUPS[0]]);
});
+
+ describe('opening the dropdown', () => {
+ describe('for the first time', () => {
+ beforeEach(() => {
+ findGlDropdown().vm.$emit('show');
+ });
+
+ it('$emits @search and @first-open', () => {
+ expect(wrapper.emitted('search')[0]).toStrictEqual([wrapper.vm.searchText]);
+ expect(wrapper.emitted('first-open')[0]).toStrictEqual([]);
+ });
+ });
+
+ describe('not for the first time', () => {
+ beforeEach(() => {
+ wrapper.setData({ hasBeenOpened: true });
+ findGlDropdown().vm.$emit('show');
+ });
+
+ it('$emits @search and not @first-open', () => {
+ expect(wrapper.emitted('search')[0]).toStrictEqual([wrapper.vm.searchText]);
+ expect(wrapper.emitted('first-open')).toBeUndefined();
+ });
+ });
+ });
});
});
diff --git a/spec/frontend/search_autocomplete_spec.js b/spec/frontend/search_autocomplete_spec.js
index 5aca07d59e4..c643cf6557d 100644
--- a/spec/frontend/search_autocomplete_spec.js
+++ b/spec/frontend/search_autocomplete_spec.js
@@ -1,4 +1,3 @@
-/* eslint-disable no-unused-expressions, consistent-return, no-param-reassign, default-case, no-return-assign */
import AxiosMockAdapter from 'axios-mock-adapter';
import $ from 'jquery';
import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
@@ -22,31 +21,33 @@ describe('Search autocomplete dropdown', () => {
const groupName = 'Gitlab Org';
const removeBodyAttributes = () => {
- const $body = $('body');
+ const { body } = document;
- $body.removeAttr('data-page');
- $body.removeAttr('data-project');
- $body.removeAttr('data-group');
+ 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) => {
- if (section == null) {
- section = 'dashboard';
- }
-
- const $body = $('body');
+ const addBodyAttributes = (section = 'dashboard') => {
removeBodyAttributes();
+
+ const { body } = document;
switch (section) {
case 'dashboard':
- return $body.attr('data-page', 'root:index');
+ body.dataset.page = 'root:index';
+ break;
case 'group':
- $body.attr('data-page', 'groups:show');
- return $body.data('group', 'gitlab-org');
+ body.dataset.page = 'groups:show';
+ body.dataset.group = 'gitlab-org';
+ break;
case 'project':
- $body.attr('data-page', 'projects:show');
- return $body.data('project', 'gitlab-ce');
+ body.dataset.page = 'projects:show';
+ body.dataset.project = 'gitlab-ce';
+ break;
+ default:
+ break;
}
};
@@ -56,34 +57,31 @@ describe('Search autocomplete dropdown', () => {
// Mock `gl` object in window for dashboard specific page. App code will need it.
const mockDashboardOptions = () => {
- window.gl || (window.gl = {});
- return (window.gl.dashboardOptions = {
+ 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 || (window.gl = {});
- return (window.gl.projectOptions = {
+ window.gl.projectOptions = {
'gitlab-ce': {
issuesPath: projectIssuesPath,
mrPath: projectMRsPath,
projectName,
},
- });
+ };
};
const mockGroupOptions = () => {
- window.gl || (window.gl = {});
- return (window.gl.groupOptions = {
+ window.gl.groupOptions = {
'gitlab-org': {
issuesPath: groupIssuesPath,
mrPath: groupMRsPath,
projectName: groupName,
},
- });
+ };
};
const assertLinks = (list, issuesPath, mrsPath) => {
@@ -113,7 +111,7 @@ describe('Search autocomplete dropdown', () => {
window.gon.current_username = userName;
window.gl = window.gl || (window.gl = {});
- return (widget = initSearchAutocomplete({ autocompletePath }));
+ widget = initSearchAutocomplete({ autocompletePath });
});
afterEach(() => {
diff --git a/spec/frontend/search_autocomplete_utils_spec.js b/spec/frontend/search_autocomplete_utils_spec.js
new file mode 100644
index 00000000000..4fdec717e93
--- /dev/null
+++ b/spec/frontend/search_autocomplete_utils_spec.js
@@ -0,0 +1,114 @@
+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/auto_dev_ops_alert_spec.js b/spec/frontend/security_configuration/components/auto_dev_ops_alert_spec.js
new file mode 100644
index 00000000000..467ae35408c
--- /dev/null
+++ b/spec/frontend/security_configuration/components/auto_dev_ops_alert_spec.js
@@ -0,0 +1,55 @@
+import { GlAlert } from '@gitlab/ui';
+import { mount } from '@vue/test-utils';
+import AutoDevopsAlert from '~/security_configuration/components/auto_dev_ops_alert.vue';
+
+const autoDevopsHelpPagePath = '/autoDevopsHelpPagePath';
+const autoDevopsPath = '/enableAutoDevopsPath';
+
+describe('AutoDevopsAlert component', () => {
+ let wrapper;
+
+ const createComponent = () => {
+ wrapper = mount(AutoDevopsAlert, {
+ provide: {
+ autoDevopsHelpPagePath,
+ autoDevopsPath,
+ },
+ });
+ };
+
+ const findAlert = () => wrapper.findComponent(GlAlert);
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('contains correct body text', () => {
+ expect(wrapper.text()).toContain('Quickly enable all');
+ });
+
+ it('renders the link correctly', () => {
+ const link = wrapper.find('a');
+
+ expect(link.attributes('href')).toBe(autoDevopsHelpPagePath);
+ expect(link.text()).toBe('Auto DevOps');
+ });
+
+ it('bubbles up dismiss events from the GlAlert', () => {
+ expect(wrapper.emitted('dismiss')).toBe(undefined);
+
+ findAlert().vm.$emit('dismiss');
+
+ expect(wrapper.emitted('dismiss')).toEqual([[]]);
+ });
+
+ it('has a button pointing to autoDevopsPath', () => {
+ expect(findAlert().props()).toMatchObject({
+ primaryButtonText: 'Enable Auto DevOps',
+ primaryButtonLink: autoDevopsPath,
+ });
+ });
+});
diff --git a/spec/frontend/security_configuration/components/feature_card_spec.js b/spec/frontend/security_configuration/components/feature_card_spec.js
index c69e135012e..3658dbb5ef2 100644
--- a/spec/frontend/security_configuration/components/feature_card_spec.js
+++ b/spec/frontend/security_configuration/components/feature_card_spec.js
@@ -3,6 +3,7 @@ import { mount } from '@vue/test-utils';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import FeatureCard from '~/security_configuration/components/feature_card.vue';
import ManageViaMr from '~/vue_shared/security_configuration/components/manage_via_mr.vue';
+import { REPORT_TYPE_SAST } from '~/vue_shared/security_reports/constants';
import { makeFeature } from './utils';
describe('FeatureCard component', () => {
@@ -126,21 +127,23 @@ describe('FeatureCard component', () => {
describe('actions', () => {
describe.each`
- context | available | configured | configurationPath | canEnableByMergeRequest | action
- ${'unavailable'} | ${false} | ${false} | ${null} | ${false} | ${null}
- ${'available'} | ${true} | ${false} | ${null} | ${false} | ${'guide'}
- ${'configured'} | ${true} | ${true} | ${null} | ${false} | ${'guide'}
- ${'available, can enable by MR'} | ${true} | ${false} | ${null} | ${true} | ${'create-mr'}
- ${'configured, can enable by MR'} | ${true} | ${true} | ${null} | ${true} | ${'guide'}
- ${'available with config path'} | ${true} | ${false} | ${'foo'} | ${false} | ${'enable'}
- ${'available with config path, can enable by MR'} | ${true} | ${false} | ${'foo'} | ${true} | ${'enable'}
- ${'configured with config path'} | ${true} | ${true} | ${'foo'} | ${false} | ${'configure'}
- ${'configured with config path, can enable by MR'} | ${true} | ${true} | ${'foo'} | ${true} | ${'configure'}
+ context | type | available | configured | configurationPath | canEnableByMergeRequest | action
+ ${'unavailable'} | ${REPORT_TYPE_SAST} | ${false} | ${false} | ${null} | ${false} | ${null}
+ ${'available'} | ${REPORT_TYPE_SAST} | ${true} | ${false} | ${null} | ${false} | ${'guide'}
+ ${'configured'} | ${REPORT_TYPE_SAST} | ${true} | ${true} | ${null} | ${false} | ${'guide'}
+ ${'available, can enable by MR'} | ${REPORT_TYPE_SAST} | ${true} | ${false} | ${null} | ${true} | ${'create-mr'}
+ ${'available, can enable by MR, unknown type'} | ${'foo'} | ${true} | ${false} | ${null} | ${true} | ${'guide'}
+ ${'configured, can enable by MR'} | ${REPORT_TYPE_SAST} | ${true} | ${true} | ${null} | ${true} | ${'guide'}
+ ${'available with config path'} | ${REPORT_TYPE_SAST} | ${true} | ${false} | ${'foo'} | ${false} | ${'enable'}
+ ${'available with config path, can enable by MR'} | ${REPORT_TYPE_SAST} | ${true} | ${false} | ${'foo'} | ${true} | ${'enable'}
+ ${'configured with config path'} | ${REPORT_TYPE_SAST} | ${true} | ${true} | ${'foo'} | ${false} | ${'configure'}
+ ${'configured with config path, can enable by MR'} | ${REPORT_TYPE_SAST} | ${true} | ${true} | ${'foo'} | ${true} | ${'configure'}
`(
'given $context feature',
- ({ available, configured, configurationPath, canEnableByMergeRequest, action }) => {
+ ({ type, available, configured, configurationPath, canEnableByMergeRequest, action }) => {
beforeEach(() => {
feature = makeFeature({
+ type,
available,
configured,
configurationPath,
diff --git a/spec/frontend/security_configuration/components/redesigned_app_spec.js b/spec/frontend/security_configuration/components/redesigned_app_spec.js
index 7e27a3e1108..119a25a77c1 100644
--- a/spec/frontend/security_configuration/components/redesigned_app_spec.js
+++ b/spec/frontend/security_configuration/components/redesigned_app_spec.js
@@ -2,6 +2,7 @@ import { GlTab } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import { makeMockUserCalloutDismisser } from 'helpers/mock_user_callout_dismisser';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import AutoDevopsAlert from '~/security_configuration/components/auto_dev_ops_alert.vue';
import {
SAST_NAME,
SAST_SHORT_NAME,
@@ -13,6 +14,7 @@ import {
LICENSE_COMPLIANCE_HELP_PATH,
} from '~/security_configuration/components/constants';
import FeatureCard from '~/security_configuration/components/feature_card.vue';
+
import RedesignedSecurityConfigurationApp, {
i18n,
} from '~/security_configuration/components/redesigned_app.vue';
@@ -23,6 +25,9 @@ import {
} from '~/vue_shared/security_reports/constants';
const upgradePath = '/upgrade';
+const autoDevopsHelpPagePath = '/autoDevopsHelpPagePath';
+const autoDevopsPath = '/autoDevopsPath';
+const gitlabCiHistoryPath = 'test/historyPath';
describe('redesigned App component', () => {
let wrapper;
@@ -36,6 +41,8 @@ describe('redesigned App component', () => {
propsData,
provide: {
upgradePath,
+ autoDevopsHelpPagePath,
+ autoDevopsPath,
},
stubs: {
UserCalloutDismisser: makeMockUserCalloutDismisser({
@@ -52,9 +59,30 @@ describe('redesigned App component', () => {
const findTabs = () => wrapper.findAllComponents(GlTab);
const findByTestId = (id) => wrapper.findByTestId(id);
const findFeatureCards = () => wrapper.findAllComponents(FeatureCard);
- const findComplianceViewHistoryLink = () => findByTestId('compliance-view-history-link');
- const findSecurityViewHistoryLink = () => findByTestId('security-view-history-link');
+ const findLink = ({ href, text, container = wrapper }) => {
+ const selector = `a[href="${href}"]`;
+ const link = container.find(selector);
+
+ if (link.exists() && link.text() === text) {
+ return link;
+ }
+
+ return wrapper.find(`${selector} does not exist`);
+ };
+ const findSecurityViewHistoryLink = () =>
+ findLink({
+ href: gitlabCiHistoryPath,
+ text: i18n.configurationHistory,
+ container: findByTestId('security-testing-tab'),
+ });
+ const findComplianceViewHistoryLink = () =>
+ findLink({
+ href: gitlabCiHistoryPath,
+ text: i18n.configurationHistory,
+ container: findByTestId('compliance-testing-tab'),
+ });
const findUpgradeBanner = () => wrapper.findComponent(UpgradeBanner);
+ const findAutoDevopsAlert = () => wrapper.findComponent(AutoDevopsAlert);
const securityFeaturesMock = [
{
@@ -119,6 +147,10 @@ describe('redesigned App component', () => {
expect(cards.at(1).props()).toEqual({ feature: complianceFeaturesMock[0] });
});
+ it('renders a basic description', () => {
+ expect(wrapper.text()).toContain(i18n.description);
+ });
+
it('should not show latest pipeline link when latestPipelinePath is not defined', () => {
expect(findByTestId('latest-pipeline-info').exists()).toBe(false);
});
@@ -129,6 +161,44 @@ describe('redesigned App component', () => {
});
});
+ describe('autoDevOpsAlert', () => {
+ describe('given the right props', () => {
+ beforeEach(() => {
+ createComponent({
+ augmentedSecurityFeatures: securityFeaturesMock,
+ augmentedComplianceFeatures: complianceFeaturesMock,
+ autoDevopsEnabled: false,
+ gitlabCiPresent: false,
+ canEnableAutoDevops: true,
+ });
+ });
+
+ it('should show AutoDevopsAlert', () => {
+ expect(findAutoDevopsAlert().exists()).toBe(true);
+ });
+
+ it('calls the dismiss callback when closing the AutoDevopsAlert', () => {
+ expect(userCalloutDismissSpy).not.toHaveBeenCalled();
+
+ findAutoDevopsAlert().vm.$emit('dismiss');
+
+ expect(userCalloutDismissSpy).toHaveBeenCalledTimes(1);
+ });
+ });
+
+ describe('given the wrong props', () => {
+ beforeEach(() => {
+ createComponent({
+ augmentedSecurityFeatures: securityFeaturesMock,
+ augmentedComplianceFeatures: complianceFeaturesMock,
+ });
+ });
+ it('should not show AutoDevopsAlert', () => {
+ expect(findAutoDevopsAlert().exists()).toBe(false);
+ });
+ });
+ });
+
describe('upgrade banner', () => {
const makeAvailable = (available) => (feature) => ({ ...feature, available });
@@ -193,9 +263,8 @@ describe('redesigned App component', () => {
it('should show latest pipeline info on the security tab with correct link when latestPipelinePath is defined', () => {
const latestPipelineInfoSecurity = findByTestId('latest-pipeline-info-security');
- expect(latestPipelineInfoSecurity.exists()).toBe(true);
expect(latestPipelineInfoSecurity.text()).toMatchInterpolatedText(
- i18n.securityTestingDescription,
+ i18n.latestPipelineDescription,
);
expect(latestPipelineInfoSecurity.find('a').attributes('href')).toBe('test/path');
});
@@ -203,9 +272,8 @@ describe('redesigned App component', () => {
it('should show latest pipeline info on the compliance tab with correct link when latestPipelinePath is defined', () => {
const latestPipelineInfoCompliance = findByTestId('latest-pipeline-info-compliance');
- expect(latestPipelineInfoCompliance.exists()).toBe(true);
expect(latestPipelineInfoCompliance.text()).toMatchInterpolatedText(
- i18n.securityTestingDescription,
+ i18n.latestPipelineDescription,
);
expect(latestPipelineInfoCompliance.find('a').attributes('href')).toBe('test/path');
});
@@ -217,7 +285,7 @@ describe('redesigned App component', () => {
augmentedSecurityFeatures: securityFeaturesMock,
augmentedComplianceFeatures: complianceFeaturesMock,
gitlabCiPresent: true,
- gitlabCiHistoryPath: 'test/historyPath',
+ gitlabCiHistoryPath,
});
});
diff --git a/spec/frontend/security_configuration/utils_spec.js b/spec/frontend/security_configuration/utils_spec.js
index 6ad167cadda..eaed4532baa 100644
--- a/spec/frontend/security_configuration/utils_spec.js
+++ b/spec/frontend/security_configuration/utils_spec.js
@@ -35,7 +35,15 @@ const mockValidCustomFeature = [
{
name: 'SAST',
type: 'SAST',
- customfield: 'customvalue',
+ customField: 'customvalue',
+ },
+];
+
+const mockValidCustomFeatureSnakeCase = [
+ {
+ name: 'SAST',
+ type: 'SAST',
+ custom_field: 'customvalue',
},
];
@@ -79,3 +87,15 @@ describe('returns an object with augmentedSecurityFeatures and augmentedComplian
).toEqual(expectedOutputCustomFeature);
});
});
+
+describe('returns an object with camelcased keys', () => {
+ it('given a customfeature in snakecase', () => {
+ expect(
+ augmentFeatures(
+ mockSecurityFeatures,
+ mockComplianceFeatures,
+ mockValidCustomFeatureSnakeCase,
+ ),
+ ).toEqual(expectedOutputCustomFeature);
+ });
+});
diff --git a/spec/frontend/sentry/index_spec.js b/spec/frontend/sentry/index_spec.js
index 13b9b9e909c..d1f098112e8 100644
--- a/spec/frontend/sentry/index_spec.js
+++ b/spec/frontend/sentry/index_spec.js
@@ -7,6 +7,8 @@ describe('SentryConfig options', () => {
const gitlabUrl = 'gitlabUrl';
const environment = 'test';
const revision = 'revision';
+ const featureCategory = 'my_feature_category';
+
let indexReturnValue;
beforeEach(() => {
@@ -16,6 +18,7 @@ describe('SentryConfig options', () => {
current_user_id: currentUserId,
gitlab_url: gitlabUrl,
revision,
+ feature_category: featureCategory,
};
process.env.HEAD_COMMIT_SHA = revision;
@@ -34,6 +37,7 @@ describe('SentryConfig options', () => {
release: revision,
tags: {
revision,
+ feature_category: featureCategory,
},
});
});
diff --git a/spec/frontend/sentry/sentry_config_spec.js b/spec/frontend/sentry/sentry_config_spec.js
index 1f5097ef2a8..9f67b681b8d 100644
--- a/spec/frontend/sentry/sentry_config_spec.js
+++ b/spec/frontend/sentry/sentry_config_spec.js
@@ -72,11 +72,13 @@ describe('SentryConfig', () => {
release: 'revision',
tags: {
revision: 'revision',
+ feature_category: 'my_feature_category',
},
};
beforeEach(() => {
jest.spyOn(Sentry, 'init').mockImplementation();
+ jest.spyOn(Sentry, 'setTags').mockImplementation();
sentryConfig.options = options;
sentryConfig.IGNORE_ERRORS = 'ignore_errors';
@@ -89,7 +91,6 @@ describe('SentryConfig', () => {
expect(Sentry.init).toHaveBeenCalledWith({
dsn: options.dsn,
release: options.release,
- tags: options.tags,
sampleRate: 0.95,
whitelistUrls: options.whitelistUrls,
environment: 'test',
@@ -98,6 +99,10 @@ describe('SentryConfig', () => {
});
});
+ it('should call Sentry.setTags', () => {
+ expect(Sentry.setTags).toHaveBeenCalledWith(options.tags);
+ });
+
it('should set environment from options', () => {
sentryConfig.options.environment = 'development';
@@ -106,7 +111,6 @@ describe('SentryConfig', () => {
expect(Sentry.init).toHaveBeenCalledWith({
dsn: options.dsn,
release: options.release,
- tags: options.tags,
sampleRate: 0.95,
whitelistUrls: options.whitelistUrls,
environment: 'development',
diff --git a/spec/frontend/serverless/components/__snapshots__/empty_state_spec.js.snap b/spec/frontend/serverless/components/__snapshots__/empty_state_spec.js.snap
index 36f6746b754..53bef449c2f 100644
--- a/spec/frontend/serverless/components/__snapshots__/empty_state_spec.js.snap
+++ b/spec/frontend/serverless/components/__snapshots__/empty_state_spec.js.snap
@@ -3,7 +3,7 @@
exports[`EmptyStateComponent should render content 1`] = `
"<section class=\\"row empty-state text-center\\">
<div class=\\"col-12\\">
- <div class=\\"svg-250 svg-content\\"><img src=\\"/image.svg\\" alt=\\"\\" class=\\"gl-max-w-full\\"></div>
+ <div class=\\"svg-250 svg-content\\"><img src=\\"/image.svg\\" alt=\\"\\" role=\\"img\\" class=\\"gl-max-w-full\\"></div>
</div>
<div class=\\"col-12\\">
<div class=\\"text-content gl-mx-auto gl-my-0 gl-p-5\\">
diff --git a/spec/frontend/sidebar/components/assignees/collapsed_assignee_spec.js b/spec/frontend/sidebar/components/assignees/collapsed_assignee_spec.js
index b49e6255923..2d5a3653631 100644
--- a/spec/frontend/sidebar/components/assignees/collapsed_assignee_spec.js
+++ b/spec/frontend/sidebar/components/assignees/collapsed_assignee_spec.js
@@ -1,7 +1,6 @@
-import { shallowMount } from '@vue/test-utils';
+import { mount } from '@vue/test-utils';
import AssigneeAvatar from '~/sidebar/components/assignees/assignee_avatar.vue';
import CollapsedAssignee from '~/sidebar/components/assignees/collapsed_assignee.vue';
-import UserNameWithStatus from '~/sidebar/components/assignees/user_name_with_status.vue';
import userDataMock from '../../user_data_mock';
const TEST_USER = userDataMock();
@@ -17,11 +16,8 @@ describe('CollapsedAssignee assignee component', () => {
...props,
};
- wrapper = shallowMount(CollapsedAssignee, {
+ wrapper = mount(CollapsedAssignee, {
propsData,
- stubs: {
- UserNameWithStatus,
- },
});
}
diff --git a/spec/frontend/sidebar/components/assignees/sidebar_assignees_widget_spec.js b/spec/frontend/sidebar/components/assignees/sidebar_assignees_widget_spec.js
index 0e052abffeb..8504684d23a 100644
--- a/spec/frontend/sidebar/components/assignees/sidebar_assignees_widget_spec.js
+++ b/spec/frontend/sidebar/components/assignees/sidebar_assignees_widget_spec.js
@@ -176,7 +176,7 @@ describe('Sidebar assignees widget', () => {
).toBe(true);
});
- it('emits an event with assignees list on successful mutation', async () => {
+ it('emits an event with assignees list and issuable id on successful mutation', async () => {
createComponent();
await waitForPromises();
@@ -193,18 +193,21 @@ describe('Sidebar assignees widget', () => {
expect(wrapper.emitted('assignees-updated')).toEqual([
[
- [
- {
- __typename: 'User',
- avatarUrl:
- 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
- id: 'gid://gitlab/User/1',
- name: 'Administrator',
- username: 'root',
- webUrl: '/root',
- status: null,
- },
- ],
+ {
+ assignees: [
+ {
+ __typename: 'User',
+ avatarUrl:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ id: 'gid://gitlab/User/1',
+ name: 'Administrator',
+ username: 'root',
+ webUrl: '/root',
+ status: null,
+ },
+ ],
+ id: 1,
+ },
],
]);
});
@@ -285,6 +288,21 @@ describe('Sidebar assignees widget', () => {
expect(updateIssueAssigneesMutationSuccess).not.toHaveBeenCalled();
expect(findUserSelect().isVisible()).toBe(true);
});
+
+ it('calls the mutation old issuable id if `iid` prop was changed', async () => {
+ findUserSelect().vm.$emit('input', [{ username: 'francina.skiles' }]);
+ wrapper.setProps({
+ iid: '2',
+ });
+ await nextTick();
+ findEditableItem().vm.$emit('close');
+
+ expect(updateIssueAssigneesMutationSuccess).toHaveBeenCalledWith({
+ assigneeUsernames: ['francina.skiles'],
+ fullPath: '/mygroup/myProject',
+ iid: '1',
+ });
+ });
});
it('shows an error if update assignees mutation is rejected', async () => {
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 9483c6624c5..4dbf3d426bb 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
@@ -1,25 +1,21 @@
-import { GlSprintf } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
+import { mount } from '@vue/test-utils';
import { AVAILABILITY_STATUS } from '~/set_status_modal/utils';
import UserNameWithStatus from '~/sidebar/components/assignees/user_name_with_status.vue';
-const name = 'Goku';
+const name = 'Administrator';
const containerClasses = 'gl-cool-class gl-over-9000';
describe('UserNameWithStatus', () => {
let wrapper;
function createComponent(props = {}) {
- return shallowMount(UserNameWithStatus, {
+ wrapper = mount(UserNameWithStatus, {
propsData: { name, containerClasses, ...props },
- stubs: {
- GlSprintf,
- },
});
}
beforeEach(() => {
- wrapper = createComponent();
+ createComponent();
});
afterEach(() => {
@@ -41,11 +37,39 @@ describe('UserNameWithStatus', () => {
describe(`with availability="${AVAILABILITY_STATUS.BUSY}"`, () => {
beforeEach(() => {
- wrapper = createComponent({ availability: AVAILABILITY_STATUS.BUSY });
+ createComponent({ availability: AVAILABILITY_STATUS.BUSY });
});
it('will render "Busy"', () => {
- expect(wrapper.html()).toContain('Goku (Busy)');
+ expect(wrapper.text()).toContain('(Busy)');
+ });
+ });
+
+ describe('when user has pronouns set', () => {
+ const pronouns = 'they/them';
+
+ beforeEach(() => {
+ createComponent({ pronouns });
+ });
+
+ it("renders user's name with pronouns", () => {
+ expect(wrapper.text()).toMatchInterpolatedText(`${name} (${pronouns})`);
+ });
+ });
+
+ describe('when user does not have pronouns set', () => {
+ describe.each`
+ pronouns
+ ${undefined}
+ ${null}
+ ${''}
+ ${' '}
+ `('when `pronouns` prop is $pronouns', ({ pronouns }) => {
+ it("renders only the user's name", () => {
+ createComponent({ pronouns });
+
+ expect(wrapper.text()).toMatchInterpolatedText(name);
+ });
});
});
});
diff --git a/spec/frontend/sidebar/components/sidebar_dropdown_widget_spec.js b/spec/frontend/sidebar/components/sidebar_dropdown_widget_spec.js
index 8d58854b013..f5e5ab4a984 100644
--- a/spec/frontend/sidebar/components/sidebar_dropdown_widget_spec.js
+++ b/spec/frontend/sidebar/components/sidebar_dropdown_widget_spec.js
@@ -451,8 +451,9 @@ describe('SidebarDropdownWidget', () => {
expect(projectMilestonesSpy).toHaveBeenNthCalledWith(1, {
fullPath: mockIssue.projectPath,
- title: '',
+ sort: null,
state: 'active',
+ title: '',
});
});
@@ -477,8 +478,9 @@ describe('SidebarDropdownWidget', () => {
expect(projectMilestonesSpy).toHaveBeenNthCalledWith(2, {
fullPath: mockIssue.projectPath,
- title: mockSearchTerm,
+ sort: null,
state: 'active',
+ title: mockSearchTerm,
});
});
});
diff --git a/spec/frontend/sidebar/components/todo_toggle/sidebar_todo_widget_spec.js b/spec/frontend/sidebar/components/todo_toggle/sidebar_todo_widget_spec.js
new file mode 100644
index 00000000000..23f1753c4bf
--- /dev/null
+++ b/spec/frontend/sidebar/components/todo_toggle/sidebar_todo_widget_spec.js
@@ -0,0 +1,126 @@
+import { GlIcon } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import createFlash from '~/flash';
+import SidebarTodoWidget from '~/sidebar/components/todo_toggle/sidebar_todo_widget.vue';
+import epicTodoQuery from '~/sidebar/queries/epic_todo.query.graphql';
+import TodoButton from '~/vue_shared/components/sidebar/todo_toggle/todo_button.vue';
+import { todosResponse, noTodosResponse } from '../../mock_data';
+
+jest.mock('~/flash');
+
+Vue.use(VueApollo);
+
+describe('Sidebar Todo Widget', () => {
+ let wrapper;
+ let fakeApollo;
+
+ const findTodoButton = () => wrapper.findComponent(TodoButton);
+
+ const createComponent = ({
+ todosQueryHandler = jest.fn().mockResolvedValue(noTodosResponse),
+ } = {}) => {
+ fakeApollo = createMockApollo([[epicTodoQuery, todosQueryHandler]]);
+
+ wrapper = shallowMount(SidebarTodoWidget, {
+ apolloProvider: fakeApollo,
+ provide: {
+ canUpdate: true,
+ isClassicSidebar: true,
+ },
+ propsData: {
+ fullPath: 'group',
+ issuableIid: '1',
+ issuableId: 'gid://gitlab/Epic/4',
+ issuableType: 'epic',
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ fakeApollo = null;
+ });
+
+ describe('when user does not have a todo for the issuable', () => {
+ beforeEach(() => {
+ createComponent();
+ return waitForPromises();
+ });
+
+ it('passes false isTodo prop to Todo button component', () => {
+ expect(findTodoButton().props('isTodo')).toBe(false);
+ });
+
+ it('emits `todoUpdated` event with a `false` payload', () => {
+ expect(wrapper.emitted('todoUpdated')).toEqual([[false]]);
+ });
+ });
+
+ describe('when user has a todo for the issuable', () => {
+ beforeEach(() => {
+ createComponent({
+ todosQueryHandler: jest.fn().mockResolvedValue(todosResponse),
+ });
+ return waitForPromises();
+ });
+
+ it('passes true isTodo prop to Todo button component', () => {
+ expect(findTodoButton().props('isTodo')).toBe(true);
+ });
+
+ it('emits `todoUpdated` event with a `true` payload', () => {
+ expect(wrapper.emitted('todoUpdated')).toEqual([[true]]);
+ });
+ });
+
+ it('displays a flash message when query is rejected', async () => {
+ createComponent({
+ todosQueryHandler: jest.fn().mockRejectedValue('Houston, we have a problem'),
+ });
+ await waitForPromises();
+
+ expect(createFlash).toHaveBeenCalled();
+ });
+
+ describe('collapsed', () => {
+ const event = { stopPropagation: jest.fn(), preventDefault: jest.fn() };
+
+ beforeEach(() => {
+ createComponent({
+ todosQueryHandler: jest.fn().mockResolvedValue(noTodosResponse),
+ });
+ return waitForPromises();
+ });
+
+ it('shows add todo icon', () => {
+ expect(wrapper.find(GlIcon).exists()).toBe(true);
+
+ expect(wrapper.find(GlIcon).props('name')).toBe('todo-add');
+ });
+
+ it('sets default tooltip title', () => {
+ expect(wrapper.find(GlIcon).attributes('title')).toBe('Add a to do');
+ });
+
+ it('when user has a to do', async () => {
+ createComponent({
+ todosQueryHandler: jest.fn().mockResolvedValue(todosResponse),
+ });
+
+ await waitForPromises();
+ expect(wrapper.find(GlIcon).props('name')).toBe('todo-done');
+ expect(wrapper.find(GlIcon).attributes('title')).toBe('Mark as done');
+ });
+
+ it('emits `todoUpdated` event on click on icon', async () => {
+ wrapper.find(GlIcon).vm.$emit('click', event);
+
+ await wrapper.vm.$nextTick();
+ expect(wrapper.emitted('todoUpdated')).toEqual([[false]]);
+ });
+ });
+});
diff --git a/spec/frontend/sidebar/lock/edit_form_buttons_spec.js b/spec/frontend/sidebar/lock/edit_form_buttons_spec.js
index 49283ea99cf..1673425947e 100644
--- a/spec/frontend/sidebar/lock/edit_form_buttons_spec.js
+++ b/spec/frontend/sidebar/lock/edit_form_buttons_spec.js
@@ -1,5 +1,5 @@
import { mount } from '@vue/test-utils';
-import { deprecatedCreateFlash as flash } from '~/flash';
+import createFlash from '~/flash';
import { createStore as createMrStore } from '~/mr_notes/stores';
import createStore from '~/notes/stores';
import EditFormButtons from '~/sidebar/components/lock/edit_form_buttons.vue';
@@ -130,7 +130,7 @@ describe('EditFormButtons', () => {
});
it('does not flash an error message', () => {
- expect(flash).not.toHaveBeenCalled();
+ expect(createFlash).not.toHaveBeenCalled();
});
});
@@ -165,9 +165,9 @@ describe('EditFormButtons', () => {
});
it('calls flash with the correct message', () => {
- expect(flash).toHaveBeenCalledWith(
- `Something went wrong trying to change the locked state of this ${issuableDisplayName}`,
- );
+ expect(createFlash).toHaveBeenCalledWith({
+ message: `Something went wrong trying to change the locked state of this ${issuableDisplayName}`,
+ });
});
});
});
diff --git a/spec/frontend/sidebar/mock_data.js b/spec/frontend/sidebar/mock_data.js
index d6287b93fb9..9fab24d7518 100644
--- a/spec/frontend/sidebar/mock_data.js
+++ b/spec/frontend/sidebar/mock_data.js
@@ -530,6 +530,7 @@ export const mockMilestone1 = {
title: 'Foobar Milestone',
webUrl: 'http://gdk.test:3000/groups/gitlab-org/-/milestones/1',
state: 'active',
+ expired: false,
};
export const mockMilestone2 = {
@@ -538,6 +539,7 @@ export const mockMilestone2 = {
title: 'Awesome Milestone',
webUrl: 'http://gdk.test:3000/groups/gitlab-org/-/milestones/2',
state: 'active',
+ expired: false,
};
export const mockProjectMilestonesResponse = {
@@ -571,6 +573,7 @@ export const mockMilestoneMutationResponse = {
id: 'gid://gitlab/Milestone/2',
title: 'Awesome Milestone',
state: 'active',
+ expired: false,
__typename: 'Milestone',
},
__typename: 'Issue',
@@ -609,4 +612,38 @@ export const issuableTimeTrackingResponse = {
},
};
+export const todosResponse = {
+ data: {
+ workspace: {
+ __typename: 'Group',
+ issuable: {
+ __typename: 'Epic',
+ id: 'gid://gitlab/Epic/4',
+ currentUserTodos: {
+ nodes: [
+ {
+ id: 'gid://gitlab/Todo/433',
+ },
+ ],
+ },
+ },
+ },
+ },
+};
+
+export const noTodosResponse = {
+ data: {
+ workspace: {
+ __typename: 'Group',
+ issuable: {
+ __typename: 'Epic',
+ id: 'gid://gitlab/Epic/4',
+ currentUserTodos: {
+ nodes: [],
+ },
+ },
+ },
+ },
+};
+
export default mockData;
diff --git a/spec/frontend/sidebar/sidebar_move_issue_spec.js b/spec/frontend/sidebar/sidebar_move_issue_spec.js
index 6a7758ace40..d9972ae75c3 100644
--- a/spec/frontend/sidebar/sidebar_move_issue_spec.js
+++ b/spec/frontend/sidebar/sidebar_move_issue_spec.js
@@ -1,5 +1,6 @@
import MockAdapter from 'axios-mock-adapter';
import $ from 'jquery';
+import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import SidebarMoveIssue from '~/sidebar/lib/sidebar_move_issue';
import SidebarService from '~/sidebar/services/sidebar_service';
@@ -7,6 +8,8 @@ import SidebarMediator from '~/sidebar/sidebar_mediator';
import SidebarStore from '~/sidebar/stores/sidebar_store';
import Mock from './mock_data';
+jest.mock('~/flash');
+
describe('SidebarMoveIssue', () => {
let mock;
const test = {};
@@ -99,7 +102,6 @@ describe('SidebarMoveIssue', () => {
});
it('should remove loading state from confirm button on failure', (done) => {
- jest.spyOn(window, 'Flash').mockImplementation(() => {});
jest.spyOn(test.mediator, 'moveIssue').mockReturnValue(Promise.reject());
test.mediator.setMoveToProjectId(7);
@@ -108,7 +110,7 @@ describe('SidebarMoveIssue', () => {
expect(test.mediator.moveIssue).toHaveBeenCalled();
// Wait for the move issue request to fail
setImmediate(() => {
- expect(window.Flash).toHaveBeenCalled();
+ expect(createFlash).toHaveBeenCalled();
expect(test.$confirmButton.prop('disabled')).toBeFalsy();
expect(test.$confirmButton.hasClass('is-loading')).toBe(false);
done();
diff --git a/spec/frontend/snippets/components/__snapshots__/snippet_blob_edit_spec.js.snap b/spec/frontend/snippets/components/__snapshots__/snippet_blob_edit_spec.js.snap
index b0c253bca65..e12255fe825 100644
--- a/spec/frontend/snippets/components/__snapshots__/snippet_blob_edit_spec.js.snap
+++ b/spec/frontend/snippets/components/__snapshots__/snippet_blob_edit_spec.js.snap
@@ -13,7 +13,7 @@ exports[`Snippet Blob Edit component with loaded blob matches snapshot 1`] = `
value="foo/bar/test.md"
/>
- <editor-lite-stub
+ <source-editor-stub
editoroptions="[object Object]"
fileglobalid="blob_local_7"
filename="foo/bar/test.md"
diff --git a/spec/frontend/snippets/components/edit_spec.js b/spec/frontend/snippets/components/edit_spec.js
index efdb52cfcd9..4e88ab9504e 100644
--- a/spec/frontend/snippets/components/edit_spec.js
+++ b/spec/frontend/snippets/components/edit_spec.js
@@ -7,8 +7,7 @@ import { useFakeDate } from 'helpers/fake_date';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import GetSnippetQuery from 'shared_queries/snippet/snippet.query.graphql';
-import UnsolvedCaptchaError from '~/captcha/unsolved_captcha_error';
-import { deprecatedCreateFlash as Flash } from '~/flash';
+import createFlash from '~/flash';
import * as urlUtils from '~/lib/utils/url_utility';
import SnippetEditApp from '~/snippets/components/edit.vue';
import SnippetBlobActionsEdit from '~/snippets/components/snippet_blob_actions_edit.vue';
@@ -29,7 +28,6 @@ jest.mock('~/flash');
const TEST_UPLOADED_FILES = ['foo/bar.txt', 'alpha/beta.js'];
const TEST_API_ERROR = new Error('TEST_API_ERROR');
-const TEST_CAPTCHA_ERROR = new UnsolvedCaptchaError();
const TEST_MUTATION_ERROR = 'Test mutation error';
const TEST_ACTIONS = {
NO_CONTENT: merge({}, testEntries.created.diff, { content: '' }),
@@ -319,14 +317,16 @@ describe('Snippet Edit app', () => {
});
expect(urlUtils.redirectTo).not.toHaveBeenCalled();
- expect(Flash).toHaveBeenCalledWith(expectMessage);
+ expect(createFlash).toHaveBeenCalledWith({
+ message: expectMessage,
+ });
},
);
- describe.each([TEST_API_ERROR, TEST_CAPTCHA_ERROR])('with apollo network error', (error) => {
+ describe('with apollo network error', () => {
beforeEach(async () => {
jest.spyOn(console, 'error').mockImplementation();
- mutateSpy.mockRejectedValue(error);
+ mutateSpy.mockRejectedValue(TEST_API_ERROR);
await createComponentAndSubmit();
});
@@ -337,9 +337,9 @@ describe('Snippet Edit app', () => {
it('should flash', () => {
// Apollo automatically wraps the resolver's error in a NetworkError
- expect(Flash).toHaveBeenCalledWith(
- `Can't update snippet: Network error: ${error.message}`,
- );
+ expect(createFlash).toHaveBeenCalledWith({
+ message: `Can't update snippet: Network error: ${TEST_API_ERROR.message}`,
+ });
});
it('should console error', () => {
@@ -348,7 +348,7 @@ describe('Snippet Edit app', () => {
// eslint-disable-next-line no-console
expect(console.error).toHaveBeenCalledWith(
'[gitlab] unexpected error while updating snippet',
- expect.objectContaining({ message: `Network error: ${error.message}` }),
+ expect.objectContaining({ message: `Network error: ${TEST_API_ERROR.message}` }),
);
});
});
diff --git a/spec/frontend/snippets/components/snippet_blob_edit_spec.js b/spec/frontend/snippets/components/snippet_blob_edit_spec.js
index 4b3b21c5507..7ea27864519 100644
--- a/spec/frontend/snippets/components/snippet_blob_edit_spec.js
+++ b/spec/frontend/snippets/components/snippet_blob_edit_spec.js
@@ -8,7 +8,7 @@ import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { joinPaths } from '~/lib/utils/url_utility';
import SnippetBlobEdit from '~/snippets/components/snippet_blob_edit.vue';
-import EditorLite from '~/vue_shared/components/editor_lite.vue';
+import SourceEditor from '~/vue_shared/components/source_editor.vue';
jest.mock('~/flash');
@@ -48,7 +48,7 @@ describe('Snippet Blob Edit component', () => {
const findLoadingIcon = () => wrapper.find(GlLoadingIcon);
const findHeader = () => wrapper.find(BlobHeaderEdit);
- const findContent = () => wrapper.find(EditorLite);
+ const findContent = () => wrapper.find(SourceEditor);
const getLastUpdatedArgs = () => {
const event = wrapper.emitted()['blob-updated'];
diff --git a/spec/frontend/static_site_editor/services/submit_content_changes_spec.js b/spec/frontend/static_site_editor/services/submit_content_changes_spec.js
index d9bceb76a37..757611166d7 100644
--- a/spec/frontend/static_site_editor/services/submit_content_changes_spec.js
+++ b/spec/frontend/static_site_editor/services/submit_content_changes_spec.js
@@ -8,8 +8,8 @@ import {
SUBMIT_CHANGES_MERGE_REQUEST_ERROR,
TRACKING_ACTION_CREATE_COMMIT,
TRACKING_ACTION_CREATE_MERGE_REQUEST,
- USAGE_PING_TRACKING_ACTION_CREATE_COMMIT,
- USAGE_PING_TRACKING_ACTION_CREATE_MERGE_REQUEST,
+ SERVICE_PING_TRACKING_ACTION_CREATE_COMMIT,
+ SERVICE_PING_TRACKING_ACTION_CREATE_MERGE_REQUEST,
DEFAULT_FORMATTING_CHANGES_COMMIT_MESSAGE,
DEFAULT_FORMATTING_CHANGES_COMMIT_DESCRIPTION,
} from '~/static_site_editor/constants';
@@ -237,7 +237,7 @@ describe('submitContentChanges', () => {
});
});
- describe('sends the correct Usage Ping tracking event', () => {
+ describe('sends the correct Service Ping tracking event', () => {
beforeEach(() => {
jest.spyOn(Api, 'trackRedisCounterEvent').mockResolvedValue({ data: '' });
});
@@ -245,7 +245,7 @@ describe('submitContentChanges', () => {
it('for commiting changes', () => {
return submitContentChanges(buildPayload()).then(() => {
expect(Api.trackRedisCounterEvent).toHaveBeenCalledWith(
- USAGE_PING_TRACKING_ACTION_CREATE_COMMIT,
+ SERVICE_PING_TRACKING_ACTION_CREATE_COMMIT,
);
});
});
@@ -253,7 +253,7 @@ describe('submitContentChanges', () => {
it('for creating a merge request', () => {
return submitContentChanges(buildPayload()).then(() => {
expect(Api.trackRedisCounterEvent).toHaveBeenCalledWith(
- USAGE_PING_TRACKING_ACTION_CREATE_MERGE_REQUEST,
+ SERVICE_PING_TRACKING_ACTION_CREATE_MERGE_REQUEST,
);
});
});
diff --git a/spec/frontend/terraform/components/terraform_list_spec.js b/spec/frontend/terraform/components/terraform_list_spec.js
index 882b7b55b3e..c622f86072d 100644
--- a/spec/frontend/terraform/components/terraform_list_spec.js
+++ b/spec/frontend/terraform/components/terraform_list_spec.js
@@ -47,6 +47,9 @@ describe('TerraformList', () => {
localVue,
apolloProvider,
propsData,
+ stubs: {
+ GlTab,
+ },
});
};
diff --git a/spec/frontend/token_access/mock_data.js b/spec/frontend/token_access/mock_data.js
new file mode 100644
index 00000000000..14d7b00cb6d
--- /dev/null
+++ b/spec/frontend/token_access/mock_data.js
@@ -0,0 +1,84 @@
+export const enabledJobTokenScope = {
+ data: {
+ project: {
+ ciCdSettings: {
+ jobTokenScopeEnabled: true,
+ __typename: 'ProjectCiCdSetting',
+ },
+ __typename: 'Project',
+ },
+ },
+};
+
+export const disabledJobTokenScope = {
+ data: {
+ project: {
+ ciCdSettings: {
+ jobTokenScopeEnabled: false,
+ __typename: 'ProjectCiCdSetting',
+ },
+ __typename: 'Project',
+ },
+ },
+};
+
+export const updateJobTokenScope = {
+ data: {
+ ciCdSettingsUpdate: {
+ ciCdSettings: {
+ jobTokenScopeEnabled: true,
+ __typename: 'ProjectCiCdSetting',
+ },
+ errors: [],
+ __typename: 'CiCdSettingsUpdatePayload',
+ },
+ },
+};
+
+export const projectsWithScope = {
+ data: {
+ project: {
+ __typename: 'Project',
+ ciJobTokenScope: {
+ __typename: 'CiJobTokenScopeType',
+ projects: {
+ __typename: 'ProjectConnection',
+ nodes: [
+ {
+ fullPath: 'root/332268-test',
+ name: 'root/332268-test',
+ },
+ ],
+ },
+ },
+ },
+ },
+};
+
+export const addProjectSuccess = {
+ data: {
+ ciJobTokenScopeAddProject: {
+ errors: [],
+ __typename: 'CiJobTokenScopeAddProjectPayload',
+ },
+ },
+};
+
+export const removeProjectSuccess = {
+ data: {
+ ciJobTokenScopeRemoveProject: {
+ errors: [],
+ __typename: 'CiJobTokenScopeRemoveProjectPayload',
+ },
+ },
+};
+
+export const mockProjects = [
+ {
+ name: 'merge-train-stuff',
+ fullPath: 'root/merge-train-stuff',
+ isLocked: false,
+ __typename: 'Project',
+ },
+ { name: 'ci-project', fullPath: 'root/ci-project', isLocked: true, __typename: 'Project' },
+];
diff --git a/spec/frontend/token_access/token_access_spec.js b/spec/frontend/token_access/token_access_spec.js
new file mode 100644
index 00000000000..c7323eb19fe
--- /dev/null
+++ b/spec/frontend/token_access/token_access_spec.js
@@ -0,0 +1,218 @@
+import { GlToggle, GlLoadingIcon } from '@gitlab/ui';
+import { createLocalVue, shallowMount, mount } from '@vue/test-utils';
+import VueApollo from 'vue-apollo';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import createFlash from '~/flash';
+import TokenAccess from '~/token_access/components/token_access.vue';
+import addProjectCIJobTokenScopeMutation from '~/token_access/graphql/mutations/add_project_ci_job_token_scope.mutation.graphql';
+import removeProjectCIJobTokenScopeMutation from '~/token_access/graphql/mutations/remove_project_ci_job_token_scope.mutation.graphql';
+import updateCIJobTokenScopeMutation from '~/token_access/graphql/mutations/update_ci_job_token_scope.mutation.graphql';
+import getCIJobTokenScopeQuery from '~/token_access/graphql/queries/get_ci_job_token_scope.query.graphql';
+import getProjectsWithCIJobTokenScopeQuery from '~/token_access/graphql/queries/get_projects_with_ci_job_token_scope.query.graphql';
+import {
+ enabledJobTokenScope,
+ disabledJobTokenScope,
+ updateJobTokenScope,
+ projectsWithScope,
+ addProjectSuccess,
+ removeProjectSuccess,
+} from './mock_data';
+
+const projectPath = 'root/my-repo';
+const error = new Error('Error');
+const localVue = createLocalVue();
+
+localVue.use(VueApollo);
+
+jest.mock('~/flash');
+
+describe('TokenAccess component', () => {
+ let wrapper;
+
+ const enabledJobTokenScopeHandler = jest.fn().mockResolvedValue(enabledJobTokenScope);
+ const disabledJobTokenScopeHandler = jest.fn().mockResolvedValue(disabledJobTokenScope);
+ const updateJobTokenScopeHandler = jest.fn().mockResolvedValue(updateJobTokenScope);
+ const getProjectsWithScope = jest.fn().mockResolvedValue(projectsWithScope);
+ const addProjectSuccessHandler = jest.fn().mockResolvedValue(addProjectSuccess);
+ const addProjectFailureHandler = jest.fn().mockRejectedValue(error);
+ const removeProjectSuccessHandler = jest.fn().mockResolvedValue(removeProjectSuccess);
+ const removeProjectFailureHandler = jest.fn().mockRejectedValue(error);
+
+ const findToggle = () => wrapper.findComponent(GlToggle);
+ const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
+ const findAddProjectBtn = () => wrapper.find('[data-testid="add-project-button"]');
+ const findRemoveProjectBtn = () => wrapper.find('[data-testid="remove-project-button"]');
+ const findTokenSection = () => wrapper.find('[data-testid="token-section"]');
+
+ const createMockApolloProvider = (requestHandlers) => {
+ return createMockApollo(requestHandlers);
+ };
+
+ const createComponent = (requestHandlers, mountFn = shallowMount) => {
+ wrapper = mountFn(TokenAccess, {
+ localVue,
+ provide: {
+ fullPath: projectPath,
+ },
+ apolloProvider: createMockApolloProvider(requestHandlers),
+ data() {
+ return {
+ targetProjectPath: 'root/test',
+ };
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('loading state', () => {
+ it('shows loading state while waiting on query to resolve', async () => {
+ createComponent([
+ [getCIJobTokenScopeQuery, enabledJobTokenScopeHandler],
+ [getProjectsWithCIJobTokenScopeQuery, getProjectsWithScope],
+ ]);
+
+ expect(findLoadingIcon().exists()).toBe(true);
+
+ await waitForPromises();
+
+ expect(findLoadingIcon().exists()).toBe(false);
+ });
+ });
+
+ describe('toggle', () => {
+ it('the toggle should be enabled and the token section should show', async () => {
+ createComponent([
+ [getCIJobTokenScopeQuery, enabledJobTokenScopeHandler],
+ [getProjectsWithCIJobTokenScopeQuery, getProjectsWithScope],
+ ]);
+
+ await waitForPromises();
+
+ expect(findToggle().props('value')).toBe(true);
+ expect(findTokenSection().exists()).toBe(true);
+ });
+
+ it('the toggle should be disabled and the token section should not show', async () => {
+ createComponent([
+ [getCIJobTokenScopeQuery, disabledJobTokenScopeHandler],
+ [getProjectsWithCIJobTokenScopeQuery, getProjectsWithScope],
+ ]);
+
+ await waitForPromises();
+
+ expect(findToggle().props('value')).toBe(false);
+ expect(findTokenSection().exists()).toBe(false);
+ });
+
+ it('switching the toggle calls the mutation and fetches the projects again', async () => {
+ createComponent([
+ [getCIJobTokenScopeQuery, disabledJobTokenScopeHandler],
+ [updateCIJobTokenScopeMutation, updateJobTokenScopeHandler],
+ [getProjectsWithCIJobTokenScopeQuery, getProjectsWithScope],
+ ]);
+
+ await waitForPromises();
+
+ expect(getProjectsWithScope).toHaveBeenCalledTimes(1);
+
+ findToggle().vm.$emit('change', true);
+
+ await waitForPromises();
+
+ expect(updateJobTokenScopeHandler).toHaveBeenCalledWith({
+ input: { fullPath: projectPath, jobTokenScopeEnabled: true },
+ });
+ expect(getProjectsWithScope).toHaveBeenCalledTimes(2);
+ });
+ });
+
+ describe('add project', () => {
+ it('calls add project mutation', async () => {
+ createComponent(
+ [
+ [getCIJobTokenScopeQuery, enabledJobTokenScopeHandler],
+ [getProjectsWithCIJobTokenScopeQuery, getProjectsWithScope],
+ [addProjectCIJobTokenScopeMutation, addProjectSuccessHandler],
+ ],
+ mount,
+ );
+
+ await waitForPromises();
+
+ findAddProjectBtn().trigger('click');
+
+ expect(addProjectSuccessHandler).toHaveBeenCalledWith({
+ input: {
+ projectPath,
+ targetProjectPath: 'root/test',
+ },
+ });
+ });
+
+ it('add project handles error correctly', async () => {
+ createComponent(
+ [
+ [getCIJobTokenScopeQuery, enabledJobTokenScopeHandler],
+ [getProjectsWithCIJobTokenScopeQuery, getProjectsWithScope],
+ [addProjectCIJobTokenScopeMutation, addProjectFailureHandler],
+ ],
+ mount,
+ );
+
+ await waitForPromises();
+
+ findAddProjectBtn().trigger('click');
+
+ await waitForPromises();
+
+ expect(createFlash).toHaveBeenCalled();
+ });
+ });
+
+ describe('remove project', () => {
+ it('calls remove project mutation', async () => {
+ createComponent(
+ [
+ [getCIJobTokenScopeQuery, enabledJobTokenScopeHandler],
+ [getProjectsWithCIJobTokenScopeQuery, getProjectsWithScope],
+ [removeProjectCIJobTokenScopeMutation, removeProjectSuccessHandler],
+ ],
+ mount,
+ );
+
+ await waitForPromises();
+
+ findRemoveProjectBtn().trigger('click');
+
+ expect(removeProjectSuccessHandler).toHaveBeenCalledWith({
+ input: {
+ projectPath,
+ targetProjectPath: 'root/332268-test',
+ },
+ });
+ });
+
+ it('remove project handles error correctly', async () => {
+ createComponent(
+ [
+ [getCIJobTokenScopeQuery, enabledJobTokenScopeHandler],
+ [getProjectsWithCIJobTokenScopeQuery, getProjectsWithScope],
+ [removeProjectCIJobTokenScopeMutation, removeProjectFailureHandler],
+ ],
+ mount,
+ );
+
+ await waitForPromises();
+
+ findRemoveProjectBtn().trigger('click');
+
+ await waitForPromises();
+
+ expect(createFlash).toHaveBeenCalled();
+ });
+ });
+});
diff --git a/spec/frontend/token_access/token_projects_table_spec.js b/spec/frontend/token_access/token_projects_table_spec.js
new file mode 100644
index 00000000000..3bda0d0b530
--- /dev/null
+++ b/spec/frontend/token_access/token_projects_table_spec.js
@@ -0,0 +1,51 @@
+import { GlTable, GlButton } from '@gitlab/ui';
+import { mount } from '@vue/test-utils';
+import TokenProjectsTable from '~/token_access/components/token_projects_table.vue';
+import { mockProjects } from './mock_data';
+
+describe('Token projects table', () => {
+ let wrapper;
+
+ const createComponent = () => {
+ wrapper = mount(TokenProjectsTable, {
+ provide: {
+ fullPath: 'root/ci-project',
+ },
+ propsData: {
+ projects: mockProjects,
+ },
+ });
+ };
+
+ const findTable = () => wrapper.findComponent(GlTable);
+ const findAllTableRows = () => wrapper.findAll('[data-testid="projects-token-table-row"]');
+ const findDeleteProjectBtn = () => wrapper.findComponent(GlButton);
+ const findAllDeleteProjectBtn = () => wrapper.findAllComponents(GlButton);
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('displays a table', () => {
+ expect(findTable().exists()).toBe(true);
+ });
+
+ it('displays the correct amount of table rows', () => {
+ expect(findAllTableRows()).toHaveLength(mockProjects.length);
+ });
+
+ it('delete project button emits event with correct project to delete', async () => {
+ await findDeleteProjectBtn().trigger('click');
+
+ expect(wrapper.emitted('removeProject')).toEqual([[mockProjects[0].fullPath]]);
+ });
+
+ it('does not show the remove icon if the project is locked', () => {
+ // currently two mock projects with one being a locked project
+ expect(findAllDeleteProjectBtn()).toHaveLength(1);
+ });
+});
diff --git a/spec/frontend/tracking_spec.js b/spec/frontend/tracking_spec.js
index d8dae2b2dc0..13498cfb823 100644
--- a/spec/frontend/tracking_spec.js
+++ b/spec/frontend/tracking_spec.js
@@ -197,6 +197,52 @@ describe('Tracking', () => {
expectedError,
);
});
+
+ it('does not add empty form whitelist rules', () => {
+ Tracking.enableFormTracking({ fields: { allow: ['input-class1'] } });
+
+ expect(snowplowSpy).toHaveBeenCalledWith(
+ 'enableFormTracking',
+ { fields: { whitelist: ['input-class1'] } },
+ [],
+ );
+ });
+
+ describe('when `document.readyState` does not equal `complete`', () => {
+ const originalReadyState = document.readyState;
+ const setReadyState = (value) => {
+ Object.defineProperty(document, 'readyState', {
+ value,
+ configurable: true,
+ });
+ };
+ const fireReadyStateChangeEvent = () => {
+ document.dispatchEvent(new Event('readystatechange'));
+ };
+
+ beforeEach(() => {
+ setReadyState('interactive');
+ });
+
+ afterEach(() => {
+ setReadyState(originalReadyState);
+ });
+
+ it('does not call `window.snowplow` until `readystatechange` is fired and `document.readyState` equals `complete`', () => {
+ Tracking.enableFormTracking({ fields: { allow: ['input-class1'] } });
+
+ expect(snowplowSpy).not.toHaveBeenCalled();
+
+ fireReadyStateChangeEvent();
+
+ expect(snowplowSpy).not.toHaveBeenCalled();
+
+ setReadyState('complete');
+ fireReadyStateChangeEvent();
+
+ expect(snowplowSpy).toHaveBeenCalled();
+ });
+ });
});
describe('.flushPendingEvents', () => {
diff --git a/spec/frontend/vue_alerts_spec.js b/spec/frontend/vue_alerts_spec.js
index 05b73415544..30be606292f 100644
--- a/spec/frontend/vue_alerts_spec.js
+++ b/spec/frontend/vue_alerts_spec.js
@@ -28,8 +28,8 @@ describe('VueAlerts', () => {
alerts
.map(
(x) => `
- <div class="js-vue-alert"
- data-dismissible="${x.dismissible}"
+ <div class="js-vue-alert"
+ data-dismissible="${x.dismissible}"
data-title="${x.title}"
data-primary-button-text="${x.primaryButtonText}"
data-primary-button-link="${x.primaryButtonLink}"
diff --git a/spec/frontend/vue_mr_widget/components/mr_widget_header_spec.js b/spec/frontend/vue_mr_widget/components/mr_widget_header_spec.js
index 115f21d8b35..f44f0b98207 100644
--- a/spec/frontend/vue_mr_widget/components/mr_widget_header_spec.js
+++ b/spec/frontend/vue_mr_widget/components/mr_widget_header_spec.js
@@ -1,4 +1,4 @@
-import { shallowMount } from '@vue/test-utils';
+import { shallowMount, mount } from '@vue/test-utils';
import { nextTick } from 'vue';
import Header from '~/vue_merge_request_widget/components/mr_widget_header.vue';
@@ -26,6 +26,15 @@ describe('MRWidgetHeader', () => {
expect(downloadPlainDiffEl.attributes('href')).toBe('/mr/plainDiffPath');
};
+ const commonMrProps = {
+ divergedCommitsCount: 1,
+ sourceBranch: 'mr-widget-refactor',
+ sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">Link</a>',
+ targetBranch: 'main',
+ targetBranchPath: '/foo/bar/main',
+ statusPath: 'abc',
+ };
+
describe('computed', () => {
describe('shouldShowCommitsBehindText', () => {
it('return true when there are divergedCommitsCount', () => {
@@ -59,36 +68,28 @@ describe('MRWidgetHeader', () => {
describe('commitsBehindText', () => {
it('returns singular when there is one commit', () => {
- createComponent({
- mr: {
- divergedCommitsCount: 1,
- sourceBranch: 'mr-widget-refactor',
- sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">Link</a>',
- targetBranch: 'main',
- targetBranchPath: '/foo/bar/main',
- statusPath: 'abc',
+ wrapper = mount(Header, {
+ propsData: {
+ mr: commonMrProps,
},
});
- expect(wrapper.vm.commitsBehindText).toBe(
- 'The source branch is <a href="/foo/bar/main">1 commit behind</a> the target branch',
+ expect(wrapper.find('.diverged-commits-count').element.innerHTML).toBe(
+ 'The source branch is <a href="/foo/bar/main" class="gl-link">1 commit behind</a> the target branch',
);
});
it('returns plural when there is more than one commit', () => {
- createComponent({
- mr: {
- divergedCommitsCount: 2,
- sourceBranch: 'mr-widget-refactor',
- sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">Link</a>',
- targetBranch: 'main',
- targetBranchPath: '/foo/bar/main',
- statusPath: 'abc',
+ wrapper = mount(Header, {
+ propsData: {
+ mr: {
+ ...commonMrProps,
+ divergedCommitsCount: 2,
+ },
},
});
-
- expect(wrapper.vm.commitsBehindText).toBe(
- 'The source branch is <a href="/foo/bar/main">2 commits behind</a> the target branch',
+ expect(wrapper.find('.diverged-commits-count').element.innerHTML).toBe(
+ 'The source branch is <a href="/foo/bar/main" class="gl-link">2 commits behind</a> the target branch',
);
});
});
@@ -273,19 +274,18 @@ describe('MRWidgetHeader', () => {
describe('with diverged commits', () => {
beforeEach(() => {
- createComponent({
- mr: {
- divergedCommitsCount: 12,
- sourceBranch: 'mr-widget-refactor',
- sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">mr-widget-refactor</a>',
- sourceBranchRemoved: false,
- targetBranchPath: 'foo/bar/commits-path',
- targetBranchTreePath: 'foo/bar/tree/path',
- targetBranch: 'main',
- isOpen: true,
- emailPatchesPath: '/mr/email-patches',
- plainDiffPath: '/mr/plainDiffPath',
- statusPath: 'abc',
+ wrapper = mount(Header, {
+ propsData: {
+ mr: {
+ ...commonMrProps,
+ divergedCommitsCount: 12,
+ sourceBranchRemoved: false,
+ targetBranchPath: 'foo/bar/commits-path',
+ targetBranchTreePath: 'foo/bar/tree/path',
+ isOpen: true,
+ emailPatchesPath: '/mr/email-patches',
+ plainDiffPath: '/mr/plainDiffPath',
+ },
},
});
});
diff --git a/spec/frontend/vue_mr_widget/components/mr_widget_rebase_spec.js b/spec/frontend/vue_mr_widget/components/mr_widget_rebase_spec.js
index 5081e1e5906..d3221cc2fc7 100644
--- a/spec/frontend/vue_mr_widget/components/mr_widget_rebase_spec.js
+++ b/spec/frontend/vue_mr_widget/components/mr_widget_rebase_spec.js
@@ -70,9 +70,9 @@ describe('Merge request widget rebase component', () => {
const text = findRebaseMessageElText();
- expect(text).toContain('Fast-forward merge is not possible.');
+ expect(text).toContain('Merge blocked');
expect(text.replace(/\s\s+/g, ' ')).toContain(
- 'Rebase the source branch onto the target branch.',
+ 'the source branch must be rebased onto the target branch',
);
});
@@ -111,12 +111,10 @@ describe('Merge request widget rebase component', () => {
const text = findRebaseMessageElText();
- expect(text).toContain('Fast-forward merge is not possible.');
- expect(text).toContain('Rebase the source branch onto');
- expect(text).toContain('foo');
- expect(text.replace(/\s\s+/g, ' ')).toContain(
- 'to allow this merge request to be merged.',
+ expect(text).toContain(
+ 'Merge blocked: the source branch must be rebased onto the target branch.',
);
+ expect(text).toContain('the source branch must be rebased');
});
it('should render the correct target branch name', () => {
@@ -136,7 +134,7 @@ describe('Merge request widget rebase component', () => {
const elem = findRebaseMessageEl();
expect(elem.text()).toContain(
- `Fast-forward merge is not possible. Rebase the source branch onto ${targetBranch} to allow this merge request to be merged.`,
+ `Merge blocked: the source branch must be rebased onto the target branch.`,
);
});
});
diff --git a/spec/frontend/vue_mr_widget/components/states/commit_edit_spec.js b/spec/frontend/vue_mr_widget/components/states/commit_edit_spec.js
index 5d09af50420..8214cedc4a1 100644
--- a/spec/frontend/vue_mr_widget/components/states/commit_edit_spec.js
+++ b/spec/frontend/vue_mr_widget/components/states/commit_edit_spec.js
@@ -63,7 +63,7 @@ describe('Commits edit component', () => {
beforeEach(() => {
createComponent({
header: `<div class="test-header">${testCommitMessage}</div>`,
- checkbox: `<label slot="checkbox" class="test-checkbox">${testLabel}</label >`,
+ checkbox: `<label class="test-checkbox">${testLabel}</label >`,
});
});
diff --git a/spec/frontend/vue_mr_widget/components/states/mr_widget_conflicts_spec.js b/spec/frontend/vue_mr_widget/components/states/mr_widget_conflicts_spec.js
index fee78d3af94..e1bce7f0474 100644
--- a/spec/frontend/vue_mr_widget/components/states/mr_widget_conflicts_spec.js
+++ b/spec/frontend/vue_mr_widget/components/states/mr_widget_conflicts_spec.js
@@ -199,7 +199,7 @@ describe('MRWidgetConflicts', () => {
});
expect(removeBreakLine(wrapper.text()).trim()).toContain(
- 'Fast-forward merge is not possible. To merge this request, first rebase locally.',
+ 'Merge blocked: fast-forward merge is not possible. To merge this request, first rebase locally.',
);
});
});
diff --git a/spec/frontend/vue_mr_widget/components/states/mr_widget_merged_spec.js b/spec/frontend/vue_mr_widget/components/states/mr_widget_merged_spec.js
index 6bb87893c31..9c3a6d581e8 100644
--- a/spec/frontend/vue_mr_widget/components/states/mr_widget_merged_spec.js
+++ b/spec/frontend/vue_mr_widget/components/states/mr_widget_merged_spec.js
@@ -217,7 +217,6 @@ describe('MRWidgetMerged', () => {
vm.mr.sourceBranchRemoved = false;
Vue.nextTick(() => {
- expect(vm.$el.innerText).toContain('You can delete the source branch now');
expect(vm.$el.innerText).not.toContain('The source branch has been deleted');
done();
});
@@ -229,7 +228,6 @@ describe('MRWidgetMerged', () => {
Vue.nextTick(() => {
expect(vm.$el.innerText).toContain('The source branch is being deleted');
- expect(vm.$el.innerText).not.toContain('You can delete the source branch now');
expect(vm.$el.innerText).not.toContain('The source branch has been deleted');
done();
});
diff --git a/spec/frontend/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js b/spec/frontend/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js
index 2d00cd8e8d4..cd77d442cbf 100644
--- a/spec/frontend/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js
+++ b/spec/frontend/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js
@@ -70,6 +70,9 @@ const createComponent = (customConfig = {}, mergeRequestWidgetGraphql = false) =
mergeRequestWidgetGraphql,
},
},
+ stubs: {
+ CommitEdit,
+ },
});
};
diff --git a/spec/frontend/vue_mr_widget/mock_data.js b/spec/frontend/vue_mr_widget/mock_data.js
index 8e36a9225d6..e6f1e15d718 100644
--- a/spec/frontend/vue_mr_widget/mock_data.js
+++ b/spec/frontend/vue_mr_widget/mock_data.js
@@ -273,9 +273,9 @@ export default {
'http://localhost:3000/root/acets-app/commit/53027d060246c8f47e4a9310fb332aa52f221775',
mr_troubleshooting_docs_path: 'help',
ci_troubleshooting_docs_path: 'help2',
- merge_request_pipelines_docs_path: '/help/ci/merge_request_pipelines/index.md',
+ merge_request_pipelines_docs_path: '/help/ci/pipelines/merge_request_pipelines.md',
merge_train_when_pipeline_succeeds_docs_path:
- '/help/ci/merge_request_pipelines/pipelines_for_merged_results/merge_trains/#startadd-to-merge-train-when-pipeline-succeeds',
+ '/help/ci/pipelines/merge_trains.md#startadd-to-merge-train-when-pipeline-succeeds',
squash: true,
visual_review_app_available: true,
merge_trains_enabled: true,
diff --git a/spec/frontend/vue_shared/components/__snapshots__/awards_list_spec.js.snap b/spec/frontend/vue_shared/components/__snapshots__/awards_list_spec.js.snap
index 3f91591f5cd..c14cf0db370 100644
--- a/spec/frontend/vue_shared/components/__snapshots__/awards_list_spec.js.snap
+++ b/spec/frontend/vue_shared/components/__snapshots__/awards_list_spec.js.snap
@@ -7,7 +7,7 @@ exports[`vue_shared/components/awards_list default matches snapshot 1`] = `
<button
class="btn gl-mr-3 gl-my-2 btn-default btn-md gl-button"
data-testid="award-button"
- title="Ada, Leonardo, and Marie"
+ title="Ada, Leonardo, and Marie reacted with :thumbsup:"
type="button"
>
<!---->
@@ -37,7 +37,7 @@ exports[`vue_shared/components/awards_list default matches snapshot 1`] = `
<button
class="btn gl-mr-3 gl-my-2 btn-default btn-md gl-button selected"
data-testid="award-button"
- title="You, Ada, and Marie"
+ title="You, Ada, and Marie reacted with :thumbsdown:"
type="button"
>
<!---->
@@ -67,7 +67,7 @@ exports[`vue_shared/components/awards_list default matches snapshot 1`] = `
<button
class="btn gl-mr-3 gl-my-2 btn-default btn-md gl-button"
data-testid="award-button"
- title="Ada and Jane"
+ title="Ada and Jane reacted with :smile:"
type="button"
>
<!---->
@@ -97,7 +97,7 @@ exports[`vue_shared/components/awards_list default matches snapshot 1`] = `
<button
class="btn gl-mr-3 gl-my-2 btn-default btn-md gl-button selected"
data-testid="award-button"
- title="You, Ada, Jane, and Leonardo"
+ title="You, Ada, Jane, and Leonardo reacted with :ok_hand:"
type="button"
>
<!---->
@@ -127,7 +127,7 @@ exports[`vue_shared/components/awards_list default matches snapshot 1`] = `
<button
class="btn gl-mr-3 gl-my-2 btn-default btn-md gl-button selected"
data-testid="award-button"
- title="You"
+ title="You reacted with :cactus:"
type="button"
>
<!---->
@@ -157,7 +157,7 @@ exports[`vue_shared/components/awards_list default matches snapshot 1`] = `
<button
class="btn gl-mr-3 gl-my-2 btn-default btn-md gl-button"
data-testid="award-button"
- title="Marie"
+ title="Marie reacted with :a:"
type="button"
>
<!---->
@@ -187,7 +187,7 @@ exports[`vue_shared/components/awards_list default matches snapshot 1`] = `
<button
class="btn gl-mr-3 gl-my-2 btn-default btn-md gl-button selected"
data-testid="award-button"
- title="You"
+ title="You reacted with :b:"
type="button"
>
<!---->
diff --git a/spec/frontend/vue_shared/components/__snapshots__/editor_lite_spec.js.snap b/spec/frontend/vue_shared/components/__snapshots__/source_editor_spec.js.snap
index 26785855369..7ce155f6a5d 100644
--- a/spec/frontend/vue_shared/components/__snapshots__/editor_lite_spec.js.snap
+++ b/spec/frontend/vue_shared/components/__snapshots__/source_editor_spec.js.snap
@@ -1,9 +1,9 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`Editor Lite component rendering matches the snapshot 1`] = `
+exports[`Source Editor component rendering matches the snapshot 1`] = `
<div
data-editor-loading=""
- id="editor-lite-snippet_777"
+ id="source-editor-snippet_777"
>
<pre
class="editor-loading-content"
diff --git a/spec/frontend/vue_shared/components/awards_list_spec.js b/spec/frontend/vue_shared/components/awards_list_spec.js
index 55f9eedc169..95e9760c181 100644
--- a/spec/frontend/vue_shared/components/awards_list_spec.js
+++ b/spec/frontend/vue_shared/components/awards_list_spec.js
@@ -98,43 +98,43 @@ describe('vue_shared/components/awards_list', () => {
classes: REACTION_CONTROL_CLASSES,
count: 3,
html: matchingEmojiTag(EMOJI_THUMBSUP),
- title: 'Ada, Leonardo, and Marie',
+ title: `Ada, Leonardo, and Marie reacted with :${EMOJI_THUMBSUP}:`,
},
{
classes: [...REACTION_CONTROL_CLASSES, 'selected'],
count: 3,
html: matchingEmojiTag(EMOJI_THUMBSDOWN),
- title: 'You, Ada, and Marie',
+ title: `You, Ada, and Marie reacted with :${EMOJI_THUMBSDOWN}:`,
},
{
classes: REACTION_CONTROL_CLASSES,
count: 2,
html: matchingEmojiTag(EMOJI_SMILE),
- title: 'Ada and Jane',
+ title: `Ada and Jane reacted with :${EMOJI_SMILE}:`,
},
{
classes: [...REACTION_CONTROL_CLASSES, 'selected'],
count: 4,
html: matchingEmojiTag(EMOJI_OK),
- title: 'You, Ada, Jane, and Leonardo',
+ title: `You, Ada, Jane, and Leonardo reacted with :${EMOJI_OK}:`,
},
{
classes: [...REACTION_CONTROL_CLASSES, 'selected'],
count: 1,
html: matchingEmojiTag(EMOJI_CACTUS),
- title: 'You',
+ title: `You reacted with :${EMOJI_CACTUS}:`,
},
{
classes: REACTION_CONTROL_CLASSES,
count: 1,
html: matchingEmojiTag(EMOJI_A),
- title: 'Marie',
+ title: `Marie reacted with :${EMOJI_A}:`,
},
{
classes: [...REACTION_CONTROL_CLASSES, 'selected'],
count: 1,
html: matchingEmojiTag(EMOJI_B),
- title: 'You',
+ title: `You reacted with :${EMOJI_B}:`,
},
]);
});
@@ -246,13 +246,13 @@ describe('vue_shared/components/awards_list', () => {
classes: REACTION_CONTROL_CLASSES,
count: 1,
html: matchingEmojiTag(EMOJI_100),
- title: 'Marie',
+ title: `Marie reacted with :${EMOJI_100}:`,
},
{
classes: REACTION_CONTROL_CLASSES,
count: 1,
html: matchingEmojiTag(EMOJI_SMILE),
- title: 'Marie',
+ title: `Marie reacted with :${EMOJI_SMILE}:`,
},
]);
});
diff --git a/spec/frontend/vue_shared/components/blob_viewers/rich_viewer_spec.js b/spec/frontend/vue_shared/components/blob_viewers/rich_viewer_spec.js
index f592db935ec..d14f3e5559f 100644
--- a/spec/frontend/vue_shared/components/blob_viewers/rich_viewer_spec.js
+++ b/spec/frontend/vue_shared/components/blob_viewers/rich_viewer_spec.js
@@ -10,9 +10,10 @@ describe('Blob Rich Viewer component', () => {
const content = '<h1 id="markdown">Foo Bar</h1>';
const defaultType = 'markdown';
- function createComponent(type = defaultType) {
+ function createComponent(type = defaultType, richViewer) {
wrapper = shallowMount(RichViewer, {
propsData: {
+ richViewer,
content,
type,
},
@@ -31,6 +32,12 @@ describe('Blob Rich Viewer component', () => {
expect(wrapper.html()).toContain(content);
});
+ it('renders the richViewer if one is present', () => {
+ const richViewer = '<div class="js-pdf-viewer"></div>';
+ createComponent('pdf', richViewer);
+ expect(wrapper.html()).toContain(richViewer);
+ });
+
it('queries for advanced viewer', () => {
expect(handleBlobRichViewer).toHaveBeenCalledWith(expect.anything(), defaultType);
});
diff --git a/spec/frontend/vue_shared/components/blob_viewers/simple_viewer_spec.js b/spec/frontend/vue_shared/components/blob_viewers/simple_viewer_spec.js
index 46d4edad891..c6c351a7f3f 100644
--- a/spec/frontend/vue_shared/components/blob_viewers/simple_viewer_spec.js
+++ b/spec/frontend/vue_shared/components/blob_viewers/simple_viewer_spec.js
@@ -2,7 +2,7 @@ import { shallowMount } from '@vue/test-utils';
import waitForPromises from 'helpers/wait_for_promises';
import { HIGHLIGHT_CLASS_NAME } from '~/vue_shared/components/blob_viewers/constants';
import SimpleViewer from '~/vue_shared/components/blob_viewers/simple_viewer.vue';
-import EditorLite from '~/vue_shared/components/editor_lite.vue';
+import SourceEditor from '~/vue_shared/components/source_editor.vue';
describe('Blob Simple Viewer component', () => {
let wrapper;
@@ -96,7 +96,7 @@ describe('Blob Simple Viewer component', () => {
});
describe('Vue refactoring to use Source Editor', () => {
- const findEditorLite = () => wrapper.find(EditorLite);
+ const findSourceEditor = () => wrapper.find(SourceEditor);
it.each`
doesRender | condition | isRawContent | isRefactorFlagEnabled
@@ -105,19 +105,19 @@ describe('Blob Simple Viewer component', () => {
${'Does not'} | ${'both, the FF and rawContent are not specified'} | ${false} | ${false}
${'Does'} | ${'both, the FF and rawContent are specified'} | ${true} | ${true}
`(
- '$doesRender render Editor Lite component in readonly mode when $condition',
+ '$doesRender render Source Editor component in readonly mode when $condition',
async ({ isRawContent, isRefactorFlagEnabled } = {}) => {
createComponent('raw content', isRawContent, isRefactorFlagEnabled);
await waitForPromises();
if (isRawContent && isRefactorFlagEnabled) {
- expect(findEditorLite().exists()).toBe(true);
+ expect(findSourceEditor().exists()).toBe(true);
- expect(findEditorLite().props('value')).toBe('raw content');
- expect(findEditorLite().props('fileName')).toBe('test.js');
- expect(findEditorLite().props('editorOptions')).toEqual({ readOnly: true });
+ expect(findSourceEditor().props('value')).toBe('raw content');
+ expect(findSourceEditor().props('fileName')).toBe('test.js');
+ expect(findSourceEditor().props('editorOptions')).toEqual({ readOnly: true });
} else {
- expect(findEditorLite().exists()).toBe(false);
+ expect(findSourceEditor().exists()).toBe(false);
}
},
);
diff --git a/spec/frontend/vue_shared/components/diff_viewer/viewers/image_diff_viewer_spec.js b/spec/frontend/vue_shared/components/diff_viewer/viewers/image_diff_viewer_spec.js
index eacc41ccdad..8deb466b33c 100644
--- a/spec/frontend/vue_shared/components/diff_viewer/viewers/image_diff_viewer_spec.js
+++ b/spec/frontend/vue_shared/components/diff_viewer/viewers/image_diff_viewer_spec.js
@@ -109,9 +109,11 @@ describe('ImageDiffViewer', () => {
components: {
imageDiffViewer,
},
- data: {
- ...allProps,
- diffMode: 'renamed',
+ data() {
+ return {
+ ...allProps,
+ diffMode: 'renamed',
+ };
},
...compileToFunctions(`
<image-diff-viewer
@@ -121,7 +123,9 @@ describe('ImageDiffViewer', () => {
:new-size="newSize"
:old-size="oldSize"
>
- <span slot="image-overlay" class="overlay">test</span>
+ <template #image-overlay>
+ <span class="overlay">test</span>
+ </template>
</image-diff-viewer>
`),
}).$mount();
diff --git a/spec/frontend/vue_shared/components/dismissible_alert_spec.js b/spec/frontend/vue_shared/components/dismissible_alert_spec.js
index cfa6d1064e5..fcd004d35a7 100644
--- a/spec/frontend/vue_shared/components/dismissible_alert_spec.js
+++ b/spec/frontend/vue_shared/components/dismissible_alert_spec.js
@@ -5,18 +5,12 @@ import DismissibleAlert from '~/vue_shared/components/dismissible_alert.vue';
const TEST_HTML = 'Hello World! <strong>Foo</strong>';
describe('vue_shared/components/dismissible_alert', () => {
- const testAlertProps = {
- primaryButtonText: 'Lorem ipsum',
- primaryButtonLink: '/lorem/ipsum',
- };
-
let wrapper;
const createComponent = (props = {}) => {
wrapper = shallowMount(DismissibleAlert, {
propsData: {
html: TEST_HTML,
- ...testAlertProps,
...props,
},
});
@@ -28,16 +22,13 @@ describe('vue_shared/components/dismissible_alert', () => {
const findAlert = () => wrapper.find(GlAlert);
- describe('with default', () => {
+ describe('default', () => {
beforeEach(() => {
createComponent();
});
it('shows alert', () => {
- const alert = findAlert();
-
- expect(alert.exists()).toBe(true);
- expect(alert.props()).toEqual(expect.objectContaining(testAlertProps));
+ expect(findAlert().exists()).toBe(true);
});
it('shows given HTML', () => {
@@ -54,4 +45,32 @@ describe('vue_shared/components/dismissible_alert', () => {
});
});
});
+
+ describe('with additional props', () => {
+ const testAlertProps = {
+ dismissible: true,
+ title: 'Mock Title',
+ primaryButtonText: 'Lorem ipsum',
+ primaryButtonLink: '/lorem/ipsum',
+ variant: 'warning',
+ };
+
+ beforeEach(() => {
+ createComponent(testAlertProps);
+ });
+
+ it('passes other props', () => {
+ expect(findAlert().props()).toEqual(expect.objectContaining(testAlertProps));
+ });
+ });
+
+ describe('with unsafe HTML', () => {
+ beforeEach(() => {
+ createComponent({ html: '<a onclick="alert("XSS")">Link</a>' });
+ });
+
+ it('removes unsafe HTML', () => {
+ expect(findAlert().html()).toContain('<a>Link</a>');
+ });
+ });
});
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 d757b7fac72..181fc4017a3 100644
--- a/spec/frontend/vue_shared/components/file_finder/index_spec.js
+++ b/spec/frontend/vue_shared/components/file_finder/index_spec.js
@@ -154,6 +154,16 @@ describe('File finder item spec', () => {
});
});
+ describe('DOM Performance', () => {
+ it('renders less DOM nodes if not visible by utilizing v-if', async () => {
+ vm.visible = false;
+
+ await waitForPromises();
+
+ expect(vm.$el).toBeInstanceOf(Comment);
+ });
+ });
+
describe('watches', () => {
describe('searchText', () => {
it('resets focusedIndex when updated', (done) => {
@@ -169,7 +179,7 @@ describe('File finder item spec', () => {
});
describe('visible', () => {
- it('returns searchText when false', (done) => {
+ it('resets searchText when changed to false', (done) => {
vm.searchText = 'test';
vm.visible = true;
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_utils_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_utils_spec.js
index 93cddff8421..1b97011bf7f 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_utils_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_utils_spec.js
@@ -11,7 +11,7 @@ import {
processFilters,
filterToQueryObject,
urlQueryToFilter,
- getRecentlyUsedTokenValues,
+ getRecentlyUsedSuggestions,
setTokenValueToRecentlyUsed,
} from '~/vue_shared/components/filtered_search_bar/filtered_search_utils';
@@ -328,32 +328,32 @@ describe('urlQueryToFilter', () => {
);
});
-describe('getRecentlyUsedTokenValues', () => {
+describe('getRecentlyUsedSuggestions', () => {
useLocalStorageSpy();
beforeEach(() => {
localStorage.removeItem(mockStorageKey);
});
- it('returns array containing recently used token values from provided recentTokenValuesStorageKey', () => {
+ it('returns array containing recently used token values from provided recentSuggestionsStorageKey', () => {
setLocalStorageAvailability(true);
const mockExpectedArray = [{ foo: 'bar' }];
localStorage.setItem(mockStorageKey, JSON.stringify(mockExpectedArray));
- expect(getRecentlyUsedTokenValues(mockStorageKey)).toEqual(mockExpectedArray);
+ expect(getRecentlyUsedSuggestions(mockStorageKey)).toEqual(mockExpectedArray);
});
- it('returns empty array when provided recentTokenValuesStorageKey does not have anything in localStorage', () => {
+ it('returns empty array when provided recentSuggestionsStorageKey does not have anything in localStorage', () => {
setLocalStorageAvailability(true);
- expect(getRecentlyUsedTokenValues(mockStorageKey)).toEqual([]);
+ expect(getRecentlyUsedSuggestions(mockStorageKey)).toEqual([]);
});
it('returns empty array when when access to localStorage is not available', () => {
setLocalStorageAvailability(false);
- expect(getRecentlyUsedTokenValues(mockStorageKey)).toEqual([]);
+ expect(getRecentlyUsedSuggestions(mockStorageKey)).toEqual([]);
});
});
@@ -366,7 +366,7 @@ describe('setTokenValueToRecentlyUsed', () => {
localStorage.removeItem(mockStorageKey);
});
- it('adds provided tokenValue to localStorage for recentTokenValuesStorageKey', () => {
+ it('adds provided tokenValue to localStorage for recentSuggestionsStorageKey', () => {
setLocalStorageAvailability(true);
setTokenValueToRecentlyUsed(mockStorageKey, mockTokenValue1);
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/author_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/author_token_spec.js
index 951b050495c..74f579e77ed 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/author_token_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/author_token_spec.js
@@ -94,7 +94,7 @@ describe('AuthorToken', () => {
it('calls `config.fetchAuthors` with provided searchTerm param', () => {
jest.spyOn(wrapper.vm.config, 'fetchAuthors');
- getBaseToken().vm.$emit('fetch-token-values', mockAuthors[0].username);
+ getBaseToken().vm.$emit('fetch-suggestions', mockAuthors[0].username);
expect(wrapper.vm.config.fetchAuthors).toHaveBeenCalledWith(
mockAuthorToken.fetchPath,
@@ -105,17 +105,17 @@ describe('AuthorToken', () => {
it('sets response to `authors` when request is succesful', () => {
jest.spyOn(wrapper.vm.config, 'fetchAuthors').mockResolvedValue(mockAuthors);
- getBaseToken().vm.$emit('fetch-token-values', 'root');
+ getBaseToken().vm.$emit('fetch-suggestions', 'root');
return waitForPromises().then(() => {
- expect(getBaseToken().props('tokenValues')).toEqual(mockAuthors);
+ expect(getBaseToken().props('suggestions')).toEqual(mockAuthors);
});
});
it('calls `createFlash` with flash error message when request fails', () => {
jest.spyOn(wrapper.vm.config, 'fetchAuthors').mockRejectedValue({});
- getBaseToken().vm.$emit('fetch-token-values', 'root');
+ getBaseToken().vm.$emit('fetch-suggestions', 'root');
return waitForPromises().then(() => {
expect(createFlash).toHaveBeenCalledWith({
@@ -127,17 +127,17 @@ describe('AuthorToken', () => {
it('sets `loading` to false when request completes', async () => {
jest.spyOn(wrapper.vm.config, 'fetchAuthors').mockRejectedValue({});
- getBaseToken().vm.$emit('fetch-token-values', 'root');
+ getBaseToken().vm.$emit('fetch-suggestions', 'root');
await waitForPromises();
- expect(getBaseToken().props('tokensListLoading')).toBe(false);
+ expect(getBaseToken().props('suggestionsLoading')).toBe(false);
});
});
});
describe('template', () => {
- const activateTokenValuesList = async () => {
+ const activateSuggestionsList = async () => {
const tokenSegments = wrapper.findAllComponents(GlFilteredSearchTokenSegment);
const suggestionsSegment = tokenSegments.at(2);
suggestionsSegment.vm.$emit('activate');
@@ -154,7 +154,7 @@ describe('AuthorToken', () => {
expect(baseTokenEl.exists()).toBe(true);
expect(baseTokenEl.props()).toMatchObject({
- tokenValues: mockAuthors,
+ suggestions: mockAuthors,
fnActiveTokenValue: wrapper.vm.getActiveAuthor,
});
});
@@ -221,7 +221,7 @@ describe('AuthorToken', () => {
stubs: { Portal: true },
});
- await activateTokenValuesList();
+ await activateSuggestionsList();
const suggestions = wrapper.findAll(GlFilteredSearchSuggestion);
@@ -252,7 +252,7 @@ describe('AuthorToken', () => {
stubs: { Portal: true },
});
- await activateTokenValuesList();
+ await activateSuggestionsList();
const suggestions = wrapper.findAll(GlFilteredSearchSuggestion);
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 89c5cedc9b8..cd6ffd679d0 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
@@ -7,7 +7,7 @@ import {
import { DEFAULT_LABELS } from '~/vue_shared/components/filtered_search_bar/constants';
import {
- getRecentlyUsedTokenValues,
+ getRecentlyUsedSuggestions,
setTokenValueToRecentlyUsed,
} from '~/vue_shared/components/filtered_search_bar/filtered_search_utils';
import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_token.vue';
@@ -49,10 +49,10 @@ const mockProps = {
config: mockLabelToken,
value: { data: '' },
active: false,
- tokenValues: [],
- tokensListLoading: false,
- defaultTokenValues: DEFAULT_LABELS,
- recentTokenValuesStorageKey: mockStorageKey,
+ suggestions: [],
+ suggestionsLoading: false,
+ defaultSuggestions: DEFAULT_LABELS,
+ recentSuggestionsStorageKey: mockStorageKey,
fnCurrentTokenValue: jest.fn(),
};
@@ -83,7 +83,7 @@ describe('BaseToken', () => {
props: {
...mockProps,
value: { data: `"${mockRegularLabel.title}"` },
- tokenValues: mockLabels,
+ suggestions: mockLabels,
},
});
});
@@ -93,8 +93,8 @@ describe('BaseToken', () => {
});
describe('data', () => {
- it('calls `getRecentlyUsedTokenValues` to populate `recentTokenValues` when `recentTokenValuesStorageKey` is defined', () => {
- expect(getRecentlyUsedTokenValues).toHaveBeenCalledWith(mockStorageKey);
+ it('calls `getRecentlyUsedSuggestions` to populate `recentSuggestions` when `recentSuggestionsStorageKey` is defined', () => {
+ expect(getRecentlyUsedSuggestions).toHaveBeenCalledWith(mockStorageKey);
});
});
@@ -147,15 +147,15 @@ describe('BaseToken', () => {
wrapperWithTokenActive.destroy();
});
- it('emits `fetch-token-values` event on the component when value of this prop is changed to false and `tokenValues` array is empty', async () => {
+ it('emits `fetch-suggestions` event on the component when value of this prop is changed to false and `suggestions` array is empty', async () => {
wrapperWithTokenActive.setProps({
active: false,
});
await wrapperWithTokenActive.vm.$nextTick();
- expect(wrapperWithTokenActive.emitted('fetch-token-values')).toBeTruthy();
- expect(wrapperWithTokenActive.emitted('fetch-token-values')).toEqual([
+ expect(wrapperWithTokenActive.emitted('fetch-suggestions')).toBeTruthy();
+ expect(wrapperWithTokenActive.emitted('fetch-suggestions')).toEqual([
[`"${mockRegularLabel.title}"`],
]);
});
@@ -164,7 +164,7 @@ describe('BaseToken', () => {
describe('methods', () => {
describe('handleTokenValueSelected', () => {
- it('calls `setTokenValueToRecentlyUsed` when `recentTokenValuesStorageKey` is defined', () => {
+ it('calls `setTokenValueToRecentlyUsed` when `recentSuggestionsStorageKey` is defined', () => {
const mockTokenValue = {
id: 1,
title: 'Foo',
@@ -175,14 +175,14 @@ describe('BaseToken', () => {
expect(setTokenValueToRecentlyUsed).toHaveBeenCalledWith(mockStorageKey, mockTokenValue);
});
- it('does not add token from preloadedTokenValues', async () => {
+ it('does not add token from preloadedSuggestions', async () => {
const mockTokenValue = {
id: 1,
title: 'Foo',
};
wrapper.setProps({
- preloadedTokenValues: [mockTokenValue],
+ preloadedSuggestions: [mockTokenValue],
});
await wrapper.vm.$nextTick();
@@ -228,7 +228,7 @@ describe('BaseToken', () => {
wrapperWithNoStubs.destroy();
});
- it('emits `fetch-token-values` event on component after a delay when component emits `input` event', async () => {
+ it('emits `fetch-suggestions` event on component after a delay when component emits `input` event', async () => {
jest.useFakeTimers();
wrapperWithNoStubs.find(GlFilteredSearchToken).vm.$emit('input', { data: 'foo' });
@@ -236,8 +236,8 @@ describe('BaseToken', () => {
jest.runAllTimers();
- expect(wrapperWithNoStubs.emitted('fetch-token-values')).toBeTruthy();
- expect(wrapperWithNoStubs.emitted('fetch-token-values')[2]).toEqual(['foo']);
+ expect(wrapperWithNoStubs.emitted('fetch-suggestions')).toBeTruthy();
+ expect(wrapperWithNoStubs.emitted('fetch-suggestions')[2]).toEqual(['foo']);
});
});
});
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/iteration_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/iteration_token_spec.js
index ca5dc984ae0..bd654c5a9cb 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/iteration_token_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/iteration_token_spec.js
@@ -7,7 +7,7 @@ import { mockIterationToken } from '../mock_data';
jest.mock('~/flash');
describe('IterationToken', () => {
- const title = 'gitlab-org: #1';
+ const id = 123;
let wrapper;
const createComponent = ({ config = mockIterationToken, value = { data: '' } } = {}) =>
@@ -28,14 +28,14 @@ describe('IterationToken', () => {
});
it('renders iteration value', async () => {
- wrapper = createComponent({ value: { data: title } });
+ wrapper = createComponent({ value: { data: id } });
await wrapper.vm.$nextTick();
const tokenSegments = wrapper.findAllComponents(GlFilteredSearchTokenSegment);
expect(tokenSegments).toHaveLength(3); // `Iteration` `=` `gitlab-org: #1`
- expect(tokenSegments.at(2).text()).toBe(title);
+ expect(tokenSegments.at(2).text()).toBe(id.toString());
});
it('fetches initial values', () => {
@@ -43,10 +43,10 @@ describe('IterationToken', () => {
wrapper = createComponent({
config: { ...mockIterationToken, fetchIterations: fetchIterationsSpy },
- value: { data: title },
+ value: { data: id },
});
- expect(fetchIterationsSpy).toHaveBeenCalledWith(title);
+ expect(fetchIterationsSpy).toHaveBeenCalledWith(id);
});
it('fetches iterations on user input', () => {
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 cc40ff96b65..ec9458f64d2 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
@@ -159,7 +159,7 @@ describe('LabelToken', () => {
expect(baseTokenEl.exists()).toBe(true);
expect(baseTokenEl.props()).toMatchObject({
- tokenValues: mockLabels,
+ suggestions: mockLabels,
fnActiveTokenValue: wrapper.vm.getActiveLabel,
});
});
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 9f550ac9afc..74ceb03bb96 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
@@ -9,6 +9,7 @@ import MockAdapter from 'axios-mock-adapter';
import waitForPromises from 'helpers/wait_for_promises';
import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
+import { sortMilestonesByDueDate } from '~/milestones/milestone_utils';
import { DEFAULT_MILESTONES } from '~/vue_shared/components/filtered_search_bar/constants';
import MilestoneToken from '~/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue';
@@ -21,6 +22,7 @@ import {
} from '../mock_data';
jest.mock('~/flash');
+jest.mock('~/milestones/milestone_utils');
const defaultStubs = {
Portal: true,
@@ -112,6 +114,7 @@ describe('MilestoneToken', () => {
return waitForPromises().then(() => {
expect(wrapper.vm.milestones).toEqual(mockMilestones);
+ expect(sortMilestonesByDueDate).toHaveBeenCalled();
});
});
diff --git a/spec/frontend/vue_shared/components/notes/__snapshots__/placeholder_note_spec.js.snap b/spec/frontend/vue_shared/components/notes/__snapshots__/placeholder_note_spec.js.snap
index f3ce03796f9..5e956d66b6a 100644
--- a/spec/frontend/vue_shared/components/notes/__snapshots__/placeholder_note_spec.js.snap
+++ b/spec/frontend/vue_shared/components/notes/__snapshots__/placeholder_note_spec.js.snap
@@ -55,6 +55,8 @@ exports[`Issue placeholder note component matches snapshot 1`] = `
<p>
Foo
</p>
+
+
</div>
</div>
</div>
diff --git a/spec/frontend/vue_shared/components/paginated_list_spec.js b/spec/frontend/vue_shared/components/paginated_list_spec.js
index c0ee49f194f..9f819cc4e94 100644
--- a/spec/frontend/vue_shared/components/paginated_list_spec.js
+++ b/spec/frontend/vue_shared/components/paginated_list_spec.js
@@ -7,9 +7,11 @@ describe('Pagination links component', () => {
let glPaginatedList;
const template = `
- <div class="slot" slot-scope="{ listItem }">
- <span class="item">Item Name: {{listItem.id}}</span>
- </div>
+ <template #default="{ listItem }">
+ <div class="slot">
+ <span class="item">Item Name: {{ listItem.id }}</span>
+ </div>
+ </template>
`;
const props = {
diff --git a/spec/frontend/vue_shared/components/project_avatar/default_spec.js b/spec/frontend/vue_shared/components/project_avatar/default_spec.js
index 0daadeebc20..84dad2374cb 100644
--- a/spec/frontend/vue_shared/components/project_avatar/default_spec.js
+++ b/spec/frontend/vue_shared/components/project_avatar/default_spec.js
@@ -3,7 +3,7 @@ import mountComponent from 'helpers/vue_mount_component_helper';
import { projectData } from 'jest/ide/mock_data';
import { TEST_HOST } from 'spec/test_constants';
import { getFirstCharacterCapitalized } from '~/lib/utils/text_utility';
-import ProjectAvatarDefault from '~/vue_shared/components/project_avatar/default.vue';
+import ProjectAvatarDefault from '~/vue_shared/components/deprecated_project_avatar/default.vue';
describe('ProjectAvatarDefault component', () => {
const Component = Vue.extend(ProjectAvatarDefault);
diff --git a/spec/frontend/vue_shared/components/project_avatar_spec.js b/spec/frontend/vue_shared/components/project_avatar_spec.js
new file mode 100644
index 00000000000..d55f3127a74
--- /dev/null
+++ b/spec/frontend/vue_shared/components/project_avatar_spec.js
@@ -0,0 +1,67 @@
+import { GlAvatar } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import ProjectAvatar from '~/vue_shared/components/project_avatar.vue';
+
+const defaultProps = {
+ projectName: 'GitLab',
+};
+
+describe('ProjectAvatar', () => {
+ let wrapper;
+
+ const findGlAvatar = () => wrapper.findComponent(GlAvatar);
+
+ const createComponent = ({ props, attrs } = {}) => {
+ wrapper = shallowMount(ProjectAvatar, { propsData: { ...defaultProps, ...props }, attrs });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('renders GlAvatar with correct props', () => {
+ createComponent();
+
+ const avatar = findGlAvatar();
+ expect(avatar.exists()).toBe(true);
+ expect(avatar.props()).toMatchObject({
+ alt: defaultProps.projectName,
+ entityName: defaultProps.projectName,
+ size: 32,
+ src: '',
+ });
+ });
+
+ describe('with `size` prop', () => {
+ it('renders GlAvatar with specified `size` prop', () => {
+ const mockSize = 48;
+ createComponent({ props: { size: mockSize } });
+
+ const avatar = findGlAvatar();
+ expect(avatar.props('size')).toBe(mockSize);
+ });
+ });
+
+ describe('with `projectAvatarUrl` prop', () => {
+ it('renders GlAvatar with specified `src` prop', () => {
+ const mockProjectAvatarUrl = 'https://gitlab.com';
+ createComponent({ props: { projectAvatarUrl: mockProjectAvatarUrl } });
+
+ const avatar = findGlAvatar();
+ expect(avatar.props('src')).toBe(mockProjectAvatarUrl);
+ });
+ });
+
+ describe.each`
+ alt
+ ${''}
+ ${'custom-alt'}
+ `('when `alt` prop is "$alt"', ({ alt }) => {
+ it('renders GlAvatar with specified `alt` attribute', () => {
+ createComponent({ props: { alt } });
+
+ const avatar = findGlAvatar();
+ expect(avatar.props('alt')).toBe(alt);
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/project_selector/project_list_item_spec.js b/spec/frontend/vue_shared/components/project_selector/project_list_item_spec.js
index 649eb2643f1..ab028ea52b7 100644
--- a/spec/frontend/vue_shared/components/project_selector/project_list_item_spec.js
+++ b/spec/frontend/vue_shared/components/project_selector/project_list_item_spec.js
@@ -1,5 +1,6 @@
import { shallowMount, createLocalVue } from '@vue/test-utils';
import { trimText } from 'helpers/text_helper';
+import ProjectAvatar from '~/vue_shared/components/deprecated_project_avatar/default.vue';
import ProjectListItem from '~/vue_shared/components/project_selector/project_list_item.vue';
const localVue = createLocalVue();
@@ -53,7 +54,7 @@ describe('ProjectListItem component', () => {
it(`renders the project avatar`, () => {
wrapper = shallowMount(Component, options);
- expect(wrapper.find('.js-project-avatar').exists()).toBe(true);
+ expect(wrapper.findComponent(ProjectAvatar).exists()).toBe(true);
});
it(`renders a simple namespace name with a trailing slash`, () => {
diff --git a/spec/frontend/vue_shared/components/resizable_chart/__snapshots__/resizable_chart_container_spec.js.snap b/spec/frontend/vue_shared/components/resizable_chart/__snapshots__/resizable_chart_container_spec.js.snap
index add0c36a120..cdfe311acd9 100644
--- a/spec/frontend/vue_shared/components/resizable_chart/__snapshots__/resizable_chart_container_spec.js.snap
+++ b/spec/frontend/vue_shared/components/resizable_chart/__snapshots__/resizable_chart_container_spec.js.snap
@@ -2,20 +2,22 @@
exports[`Resizable Chart Container renders the component 1`] = `
<div>
- <div
- class="slot"
- >
- <span
- class="width"
+ <template>
+ <div
+ class="slot"
>
- 0
- </span>
-
- <span
- class="height"
- >
- 0
- </span>
- </div>
+ <span
+ class="width"
+ >
+ 0
+ </span>
+
+ <span
+ class="height"
+ >
+ 0
+ </span>
+ </div>
+ </template>
</div>
`;
diff --git a/spec/frontend/vue_shared/components/resizable_chart/resizable_chart_container_spec.js b/spec/frontend/vue_shared/components/resizable_chart/resizable_chart_container_spec.js
index 1fce3c5d0b0..40f0c0f29f2 100644
--- a/spec/frontend/vue_shared/components/resizable_chart/resizable_chart_container_spec.js
+++ b/spec/frontend/vue_shared/components/resizable_chart/resizable_chart_container_spec.js
@@ -16,10 +16,12 @@ describe('Resizable Chart Container', () => {
wrapper = mount(ResizableChartContainer, {
scopedSlots: {
default: `
- <div class="slot" slot-scope="{ width, height }">
- <span class="width">{{width}}</span>
- <span class="height">{{height}}</span>
- </div>
+ <template #default="{ width, height }">
+ <div class="slot">
+ <span class="width">{{width}}</span>
+ <span class="height">{{height}}</span>
+ </div>
+ </template>
`,
},
});
diff --git a/spec/frontend/vue_shared/components/security_reports/artifact_downloads/merge_request_artifact_download_spec.js b/spec/frontend/vue_shared/components/security_reports/artifact_downloads/merge_request_artifact_download_spec.js
index d58c87d66cb..395c74dcba6 100644
--- a/spec/frontend/vue_shared/components/security_reports/artifact_downloads/merge_request_artifact_download_spec.js
+++ b/spec/frontend/vue_shared/components/security_reports/artifact_downloads/merge_request_artifact_download_spec.js
@@ -3,7 +3,7 @@ import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import {
- expectedDownloadDropdownProps,
+ expectedDownloadDropdownPropsWithTitle,
securityReportMergeRequestDownloadPathsQueryResponse,
} from 'jest/vue_shared/security_reports/mock_data';
import createFlash from '~/flash';
@@ -80,7 +80,7 @@ describe('Merge request artifact Download', () => {
});
it('renders the download dropdown', () => {
- expect(findDownloadDropdown().props()).toEqual(expectedDownloadDropdownProps);
+ expect(findDownloadDropdown().props()).toEqual(expectedDownloadDropdownPropsWithTitle);
});
});
diff --git a/spec/frontend/vue_shared/components/sidebar/copyable_field_spec.js b/spec/frontend/vue_shared/components/sidebar/copyable_field_spec.js
index b99b1a66b79..3980033862e 100644
--- a/spec/frontend/vue_shared/components/sidebar/copyable_field_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/copyable_field_spec.js
@@ -1,4 +1,4 @@
-import { GlLoadingIcon } from '@gitlab/ui';
+import { GlLoadingIcon, GlSprintf } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import CopyableField from '~/vue_shared/components/sidebar/copyable_field.vue';
@@ -14,6 +14,9 @@ describe('SidebarCopyableField', () => {
const createComponent = (propsData = defaultProps) => {
wrapper = shallowMount(CopyableField, {
propsData,
+ stubs: {
+ GlSprintf,
+ },
});
};
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view_spec.js
index 60903933505..06ea88c09a0 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view_spec.js
@@ -54,7 +54,6 @@ describe('DropdownContentsLabelsView', () => {
afterEach(() => {
wrapper.destroy();
- wrapper = null;
});
const findDropdownContent = () => wrapper.find('[data-testid="dropdown-content"]');
@@ -381,6 +380,15 @@ describe('DropdownContentsLabelsView', () => {
expect(findDropdownFooter().exists()).toBe(false);
});
+ it('does not render footer list items when `allowLabelCreate` is false and `labelsManagePath` is null', () => {
+ createComponent({
+ ...mockConfig,
+ allowLabelCreate: false,
+ labelsManagePath: null,
+ });
+ expect(findDropdownFooter().exists()).toBe(false);
+ });
+
it('renders footer list items when `state.variant` is "embedded"', () => {
expect(findDropdownFooter().exists()).toBe(true);
});
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/actions_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/actions_spec.js
index 3f11095cb04..46ade5d5857 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/actions_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/actions_spec.js
@@ -1,11 +1,14 @@
import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
+import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import * as actions from '~/vue_shared/components/sidebar/labels_select_vue/store/actions';
import * as types from '~/vue_shared/components/sidebar/labels_select_vue/store/mutation_types';
import defaultState from '~/vue_shared/components/sidebar/labels_select_vue/store/state';
+jest.mock('~/flash');
+
describe('LabelsSelect Actions', () => {
let state;
const mockInitialState = {
@@ -91,10 +94,6 @@ describe('LabelsSelect Actions', () => {
});
describe('receiveLabelsFailure', () => {
- beforeEach(() => {
- setFixtures('<div class="flash-container"></div>');
- });
-
it('sets value `state.labelsFetchInProgress` to `false`', (done) => {
testAction(
actions.receiveLabelsFailure,
@@ -109,9 +108,7 @@ describe('LabelsSelect Actions', () => {
it('shows flash error', () => {
actions.receiveLabelsFailure({ commit: () => {} });
- expect(document.querySelector('.flash-container .flash-text').innerText.trim()).toBe(
- 'Error fetching labels.',
- );
+ expect(createFlash).toHaveBeenCalledWith({ message: 'Error fetching labels.' });
});
});
@@ -186,10 +183,6 @@ describe('LabelsSelect Actions', () => {
});
describe('receiveCreateLabelFailure', () => {
- beforeEach(() => {
- setFixtures('<div class="flash-container"></div>');
- });
-
it('sets value `state.labelCreateInProgress` to `false`', (done) => {
testAction(
actions.receiveCreateLabelFailure,
@@ -204,9 +197,7 @@ describe('LabelsSelect Actions', () => {
it('shows flash error', () => {
actions.receiveCreateLabelFailure({ commit: () => {} });
- expect(document.querySelector('.flash-container .flash-text').innerText.trim()).toBe(
- 'Error creating label.',
- );
+ expect(createFlash).toHaveBeenCalledWith({ message: 'Error creating label.' });
});
});
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/mutations_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/mutations_spec.js
index ab266ac8aed..1d2a9c34599 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/mutations_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/mutations_spec.js
@@ -153,7 +153,16 @@ describe('LabelsSelect Mutations', () => {
});
describe(`${types.UPDATE_SELECTED_LABELS}`, () => {
- const labels = [{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }];
+ let labels;
+
+ beforeEach(() => {
+ labels = [
+ { id: 1, title: 'scoped::test', set: true },
+ { id: 2, set: false, title: 'scoped::one' },
+ { id: 3, title: '' },
+ { id: 4, title: '' },
+ ];
+ });
it('updates `state.labels` to include `touched` and `set` props based on provided `labels` param', () => {
const updatedLabelIds = [2];
@@ -169,5 +178,23 @@ describe('LabelsSelect Mutations', () => {
}
});
});
+
+ describe('when label is scoped', () => {
+ it('unsets the currently selected scoped label and sets the current label', () => {
+ const state = {
+ labels,
+ };
+ mutations[types.UPDATE_SELECTED_LABELS](state, {
+ labels: [{ id: 2, title: 'scoped::one' }],
+ });
+
+ expect(state.labels).toEqual([
+ { id: 1, title: 'scoped::test', set: false },
+ { id: 2, set: true, title: 'scoped::one', touched: true },
+ { id: 3, title: '' },
+ { id: 4, title: '' },
+ ]);
+ });
+ });
});
});
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_value_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_value_spec.js
index 59f3268c000..b3ffee2d020 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_value_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_value_spec.js
@@ -1,88 +1,97 @@
import { GlLabel } from '@gitlab/ui';
-import { shallowMount, createLocalVue } from '@vue/test-utils';
-import Vuex from 'vuex';
+import { shallowMount } from '@vue/test-utils';
import DropdownValue from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_value.vue';
-import labelsSelectModule from '~/vue_shared/components/sidebar/labels_select_widget/store';
-
-import { mockConfig, mockRegularLabel, mockScopedLabel } from './mock_data';
-
-const localVue = createLocalVue();
-localVue.use(Vuex);
+import { mockRegularLabel, mockScopedLabel } from './mock_data';
describe('DropdownValue', () => {
let wrapper;
- const createComponent = (initialState = {}, slots = {}) => {
- const store = new Vuex.Store(labelsSelectModule());
-
- store.dispatch('setInitialState', { ...mockConfig, ...initialState });
+ const findAllLabels = () => wrapper.findAllComponents(GlLabel);
+ const findRegularLabel = () => findAllLabels().at(0);
+ const findScopedLabel = () => findAllLabels().at(1);
+ const findWrapper = () => wrapper.find('[data-testid="value-wrapper"]');
+ const findEmptyPlaceholder = () => wrapper.find('[data-testid="empty-placeholder"]');
+ const createComponent = (props = {}, slots = {}) => {
wrapper = shallowMount(DropdownValue, {
- localVue,
- store,
slots,
+ propsData: {
+ selectedLabels: [mockRegularLabel, mockScopedLabel],
+ allowLabelRemove: true,
+ allowScopedLabels: true,
+ labelsFilterBasePath: '/gitlab-org/my-project/issues',
+ labelsFilterParam: 'label_name',
+ ...props,
+ },
});
};
afterEach(() => {
wrapper.destroy();
- wrapper = null;
});
- describe('methods', () => {
- describe('labelFilterUrl', () => {
- it('returns a label filter URL based on provided label param', () => {
- createComponent();
-
- expect(wrapper.vm.labelFilterUrl(mockRegularLabel)).toBe(
- '/gitlab-org/my-project/issues?label_name[]=Foo%20Label',
- );
- });
+ describe('when there are no labels', () => {
+ beforeEach(() => {
+ createComponent(
+ {
+ selectedLabels: [],
+ },
+ {
+ default: 'None',
+ },
+ );
});
- describe('scopedLabel', () => {
- beforeEach(() => {
- createComponent();
- });
+ it('does not apply `has-labels` class to the wrapping container', () => {
+ expect(findWrapper().classes()).not.toContain('has-labels');
+ });
- it('returns `true` when provided label param is a scoped label', () => {
- expect(wrapper.vm.scopedLabel(mockScopedLabel)).toBe(true);
- });
+ it('renders an empty placeholder', () => {
+ expect(findEmptyPlaceholder().exists()).toBe(true);
+ expect(findEmptyPlaceholder().text()).toBe('None');
+ });
- it('returns `false` when provided label param is a regular label', () => {
- expect(wrapper.vm.scopedLabel(mockRegularLabel)).toBe(false);
- });
+ it('does not render any labels', () => {
+ expect(findAllLabels().length).toBe(0);
});
});
- describe('template', () => {
- it('renders class `has-labels` on component container element when `selectedLabels` is not empty', () => {
+ describe('when there are labels', () => {
+ beforeEach(() => {
createComponent();
+ });
- expect(wrapper.attributes('class')).toContain('has-labels');
+ it('applies `has-labels` class to the wrapping container', () => {
+ expect(findWrapper().classes()).toContain('has-labels');
});
- it('renders element containing `None` when `selectedLabels` is empty', () => {
- createComponent(
- {
- selectedLabels: [],
- },
- {
- default: 'None',
- },
- );
- const noneEl = wrapper.find('span.text-secondary');
+ it('does not render an empty placeholder', () => {
+ expect(findEmptyPlaceholder().exists()).toBe(false);
+ });
- expect(noneEl.exists()).toBe(true);
- expect(noneEl.text()).toBe('None');
+ it('renders a list of two labels', () => {
+ expect(findAllLabels().length).toBe(2);
});
- it('renders labels when `selectedLabels` is not empty', () => {
- createComponent();
+ it('passes correct props to the regular label', () => {
+ expect(findRegularLabel().props('target')).toBe(
+ '/gitlab-org/my-project/issues?label_name[]=Foo%20Label',
+ );
+ expect(findRegularLabel().props('scoped')).toBe(false);
+ });
+
+ it('passes correct props to the scoped label', () => {
+ expect(findScopedLabel().props('target')).toBe(
+ '/gitlab-org/my-project/issues?label_name[]=Foo%3A%3ABar',
+ );
+ expect(findScopedLabel().props('scoped')).toBe(true);
+ });
- expect(wrapper.findAll(GlLabel).length).toBe(2);
+ it('emits `onLabelRemove` event with the correct ID', () => {
+ findRegularLabel().vm.$emit('close');
+ expect(wrapper.emitted('onLabelRemove')).toEqual([[mockRegularLabel.id]]);
});
});
});
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/labels_select_root_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/labels_select_root_spec.js
index ee1346c362f..66971446f47 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/labels_select_root_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/labels_select_root_spec.js
@@ -34,6 +34,10 @@ describe('LabelsSelectRoot', () => {
stubs: {
'dropdown-contents': DropdownContents,
},
+ provide: {
+ iid: '1',
+ projectPath: 'test',
+ },
});
};
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/store/actions_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/store/actions_spec.js
index 7ef4b769b6b..27de7de2411 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/store/actions_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/store/actions_spec.js
@@ -1,11 +1,14 @@
import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
+import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import * as actions from '~/vue_shared/components/sidebar/labels_select_widget/store/actions';
import * as types from '~/vue_shared/components/sidebar/labels_select_widget/store/mutation_types';
import defaultState from '~/vue_shared/components/sidebar/labels_select_widget/store/state';
+jest.mock('~/flash');
+
describe('LabelsSelect Actions', () => {
let state;
const mockInitialState = {
@@ -91,10 +94,6 @@ describe('LabelsSelect Actions', () => {
});
describe('receiveLabelsFailure', () => {
- beforeEach(() => {
- setFixtures('<div class="flash-container"></div>');
- });
-
it('sets value `state.labelsFetchInProgress` to `false`', (done) => {
testAction(
actions.receiveLabelsFailure,
@@ -109,9 +108,7 @@ describe('LabelsSelect Actions', () => {
it('shows flash error', () => {
actions.receiveLabelsFailure({ commit: () => {} });
- expect(document.querySelector('.flash-container .flash-text').innerText.trim()).toBe(
- 'Error fetching labels.',
- );
+ expect(createFlash).toHaveBeenCalledWith({ message: 'Error fetching labels.' });
});
});
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/store/mutations_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/store/mutations_spec.js
index acb275b5d90..9e965cb33e8 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/store/mutations_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/store/mutations_spec.js
@@ -120,7 +120,16 @@ describe('LabelsSelect Mutations', () => {
});
describe(`${types.UPDATE_SELECTED_LABELS}`, () => {
- const labels = [{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }];
+ let labels;
+
+ beforeEach(() => {
+ labels = [
+ { id: 1, title: 'scoped::test', set: true },
+ { id: 2, set: false, title: 'scoped::one' },
+ { id: 3, title: '' },
+ { id: 4, title: '' },
+ ];
+ });
it('updates `state.labels` to include `touched` and `set` props based on provided `labels` param', () => {
const updatedLabelIds = [2];
@@ -136,5 +145,23 @@ describe('LabelsSelect Mutations', () => {
}
});
});
+
+ describe('when label is scoped', () => {
+ it('unsets the currently selected scoped label and sets the current label', () => {
+ const state = {
+ labels,
+ };
+ mutations[types.UPDATE_SELECTED_LABELS](state, {
+ labels: [{ id: 2, title: 'scoped::one' }],
+ });
+
+ expect(state.labels).toEqual([
+ { id: 1, title: 'scoped::test', set: false },
+ { id: 2, set: true, title: 'scoped::one', touched: true },
+ { id: 3, title: '' },
+ { id: 4, title: '' },
+ ]);
+ });
+ });
});
});
diff --git a/spec/frontend/vue_shared/components/todo_button_spec.js b/spec/frontend/vue_shared/components/sidebar/todo_button_spec.js
index 8043bb7785b..de3e1ccfb03 100644
--- a/spec/frontend/vue_shared/components/todo_button_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/todo_button_spec.js
@@ -1,9 +1,10 @@
import { GlButton } from '@gitlab/ui';
import { shallowMount, mount } from '@vue/test-utils';
-import TodoButton from '~/vue_shared/components/todo_button.vue';
+import TodoButton from '~/vue_shared/components/sidebar/todo_toggle/todo_button.vue';
describe('Todo Button', () => {
let wrapper;
+ let dispatchEventSpy;
const createComponent = (props = {}, mountFn = shallowMount) => {
wrapper = mountFn(TodoButton, {
@@ -13,8 +14,17 @@ describe('Todo Button', () => {
});
};
+ beforeEach(() => {
+ dispatchEventSpy = jest.spyOn(document, 'dispatchEvent');
+ jest.spyOn(document, 'querySelector').mockReturnValue({
+ innerText: 2,
+ });
+ });
+
afterEach(() => {
wrapper.destroy();
+ dispatchEventSpy = null;
+ jest.clearAllMocks();
});
it('renders GlButton', () => {
@@ -30,6 +40,16 @@ describe('Todo Button', () => {
expect(wrapper.emitted().click).toBeTruthy();
});
+ it('calls dispatchDocumentEvent to update global To-Do counter correctly', () => {
+ createComponent({}, mount);
+ wrapper.find(GlButton).trigger('click');
+ const dispatchedEvent = dispatchEventSpy.mock.calls[0][0];
+
+ expect(dispatchEventSpy).toHaveBeenCalledTimes(1);
+ expect(dispatchedEvent.detail).toEqual({ count: 1 });
+ expect(dispatchedEvent.type).toBe('todo:toggle');
+ });
+
it.each`
label | isTodo
${'Mark as done'} | ${true}
diff --git a/spec/frontend/vue_shared/components/editor_lite_spec.js b/spec/frontend/vue_shared/components/source_editor_spec.js
index badd5aed0e3..dca4d60e23c 100644
--- a/spec/frontend/vue_shared/components/editor_lite_spec.js
+++ b/spec/frontend/vue_shared/components/source_editor_spec.js
@@ -1,12 +1,12 @@
import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
import { EDITOR_READY_EVENT } from '~/editor/constants';
-import Editor from '~/editor/editor_lite';
-import EditorLite from '~/vue_shared/components/editor_lite.vue';
+import Editor from '~/editor/source_editor';
+import SourceEditor from '~/vue_shared/components/source_editor.vue';
-jest.mock('~/editor/editor_lite');
+jest.mock('~/editor/source_editor');
-describe('Editor Lite component', () => {
+describe('Source Editor component', () => {
let wrapper;
let mockInstance;
@@ -30,7 +30,7 @@ describe('Editor Lite component', () => {
};
});
function createComponent(props = {}) {
- wrapper = shallowMount(EditorLite, {
+ wrapper = shallowMount(SourceEditor, {
propsData: {
value,
fileName,
@@ -73,10 +73,10 @@ describe('Editor Lite component', () => {
createComponent({ value: undefined });
expect(spy).not.toHaveBeenCalled();
- expect(wrapper.find('[id^="editor-lite-"]').exists()).toBe(true);
+ expect(wrapper.find('[id^="source-editor-"]').exists()).toBe(true);
});
- it('initialises Editor Lite instance', () => {
+ it('initialises Source Editor instance', () => {
const el = wrapper.find({ ref: 'editor' }).element;
expect(createInstanceMock).toHaveBeenCalledWith({
el,
@@ -111,7 +111,7 @@ describe('Editor Lite component', () => {
expect(wrapper.emitted().input).toEqual([[value]]);
});
- it('emits EDITOR_READY_EVENT event when the Editor Lite is ready', async () => {
+ it('emits EDITOR_READY_EVENT event when the Source Editor is ready', async () => {
const el = wrapper.find({ ref: 'editor' }).element;
expect(wrapper.emitted()[EDITOR_READY_EVENT]).toBeUndefined();
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 87fe8619f28..538e67ef354 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,5 @@
-import { GlSkeletonLoader, GlSprintf, GlIcon } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
+import { GlSkeletonLoader, GlIcon } from '@gitlab/ui';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
import { AVAILABILITY_STATUS } from '~/set_status_modal/utils';
import UserNameWithStatus from '~/sidebar/components/assignees/user_name_with_status.vue';
import UserPopover from '~/vue_shared/components/user_popover/user_popover.vue';
@@ -13,6 +13,7 @@ const DEFAULT_PROPS = {
bio: null,
workInformation: null,
status: null,
+ pronouns: 'they/them',
loaded: true,
},
};
@@ -30,23 +31,18 @@ describe('User Popover Component', () => {
wrapper.destroy();
});
- const findByTestId = (testid) => wrapper.find(`[data-testid="${testid}"]`);
const findUserStatus = () => wrapper.find('.js-user-status');
const findTarget = () => document.querySelector('.js-user-link');
const findUserName = () => wrapper.find(UserNameWithStatus);
- const findSecurityBotDocsLink = () => findByTestId('user-popover-bot-docs-link');
+ const findSecurityBotDocsLink = () => wrapper.findByTestId('user-popover-bot-docs-link');
const createWrapper = (props = {}, options = {}) => {
- wrapper = shallowMount(UserPopover, {
+ wrapper = mountExtended(UserPopover, {
propsData: {
...DEFAULT_PROPS,
target: findTarget(),
...props,
},
- stubs: {
- GlSprintf,
- UserNameWithStatus,
- },
...options,
});
};
@@ -232,6 +228,12 @@ describe('User Popover Component', () => {
expect(wrapper.text()).not.toContain('(Busy)');
});
+
+ it('passes `pronouns` prop to `UserNameWithStatus` component', () => {
+ createWrapper();
+
+ expect(findUserName().props('pronouns')).toBe('they/them');
+ });
});
describe('bot user', () => {
diff --git a/spec/frontend/vue_shared/components/user_select_spec.js b/spec/frontend/vue_shared/components/user_select_spec.js
index 0fabc6525ea..b777ac0a0a4 100644
--- a/spec/frontend/vue_shared/components/user_select_spec.js
+++ b/spec/frontend/vue_shared/components/user_select_spec.js
@@ -275,48 +275,4 @@ describe('User select dropdown', () => {
expect(findEmptySearchResults().exists()).toBe(true);
});
});
-
- // TODO Remove this test after the following issue is resolved in the backend
- // https://gitlab.com/gitlab-org/gitlab/-/issues/329750
- describe('temporary error suppression', () => {
- beforeEach(() => {
- jest.spyOn(console, 'error').mockImplementation();
- });
-
- const nullError = { message: 'Cannot return null for non-nullable field GroupMember.user' };
-
- it.each`
- mockErrors
- ${[nullError]}
- ${[nullError, nullError]}
- `('does not emit errors', async ({ mockErrors }) => {
- createComponent({
- searchQueryHandler: jest.fn().mockResolvedValue({
- errors: mockErrors,
- }),
- });
- await waitForSearch();
-
- expect(wrapper.emitted()).toEqual({});
- // eslint-disable-next-line no-console
- expect(console.error).toHaveBeenCalled();
- });
-
- it.each`
- mockErrors
- ${[{ message: 'serious error' }]}
- ${[nullError, { message: 'serious error' }]}
- `('emits error when non-null related errors are included', async ({ mockErrors }) => {
- createComponent({
- searchQueryHandler: jest.fn().mockResolvedValue({
- errors: mockErrors,
- }),
- });
- await waitForSearch();
-
- expect(wrapper.emitted('error')).toEqual([[]]);
- // eslint-disable-next-line no-console
- expect(console.error).not.toHaveBeenCalled();
- });
- });
});
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 5a6c91bda9f..0fd4d0dab87 100644
--- a/spec/frontend/vue_shared/components/web_ide_link_spec.js
+++ b/spec/frontend/vue_shared/components/web_ide_link_spec.js
@@ -15,8 +15,8 @@ const ACTION_EDIT = {
tooltip: '',
attrs: {
'data-qa-selector': 'edit_button',
- 'data-track-event': 'click_edit',
- 'data-track-label': 'Edit',
+ 'data-track-action': 'click_consolidated_edit',
+ 'data-track-label': 'edit',
},
};
const ACTION_EDIT_CONFIRM_FORK = {
@@ -32,8 +32,8 @@ const ACTION_WEB_IDE = {
text: 'Web IDE',
attrs: {
'data-qa-selector': 'web_ide_button',
- 'data-track-event': 'click_edit_ide',
- 'data-track-label': 'Web IDE',
+ 'data-track-action': 'click_consolidated_edit_ide',
+ 'data-track-label': 'web_ide',
},
};
const ACTION_WEB_IDE_CONFIRM_FORK = {
diff --git a/spec/frontend/vue_shared/new_namespace/components/welcome_spec.js b/spec/frontend/vue_shared/new_namespace/components/welcome_spec.js
index 602213fca83..2d51f6dbeeb 100644
--- a/spec/frontend/vue_shared/new_namespace/components/welcome_spec.js
+++ b/spec/frontend/vue_shared/new_namespace/components/welcome_spec.js
@@ -1,12 +1,8 @@
import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
import { mockTracking } from 'helpers/tracking_helper';
-import { TRACKING_CONTEXT_SCHEMA } from '~/experimentation/constants';
-import { getExperimentData } from '~/experimentation/utils';
import WelcomePage from '~/vue_shared/new_namespace/components/welcome.vue';
-jest.mock('~/experimentation/utils', () => ({ getExperimentData: jest.fn() }));
-
describe('Welcome page', () => {
let wrapper;
let trackingSpy;
@@ -28,7 +24,6 @@ describe('Welcome page', () => {
beforeEach(() => {
trackingSpy = mockTracking('_category_', document, jest.spyOn);
trackingSpy.mockImplementation(() => {});
- getExperimentData.mockReturnValue(undefined);
});
afterEach(() => {
@@ -38,7 +33,7 @@ describe('Welcome page', () => {
});
it('tracks link clicks', async () => {
- createComponent({ propsData: { experiment: 'foo', panels: [{ name: 'test', href: '#' }] } });
+ createComponent({ propsData: { panels: [{ name: 'test', href: '#' }] } });
const link = wrapper.find('a');
link.trigger('click');
await nextTick();
@@ -47,25 +42,6 @@ describe('Welcome page', () => {
});
});
- it('adds experiment data if in experiment', async () => {
- const mockExperimentData = 'data';
- getExperimentData.mockReturnValue(mockExperimentData);
-
- createComponent({ propsData: { experiment: 'foo', panels: [{ name: 'test', href: '#' }] } });
- const link = wrapper.find('a');
- link.trigger('click');
- await nextTick();
- return wrapper.vm.$nextTick().then(() => {
- expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_tab', {
- label: 'test',
- context: {
- data: mockExperimentData,
- schema: TRACKING_CONTEXT_SCHEMA,
- },
- });
- });
- });
-
it('renders footer slot if provided', () => {
const DUMMY = 'Test message';
createComponent({
diff --git a/spec/frontend/vue_shared/new_namespace/new_namespace_page_spec.js b/spec/frontend/vue_shared/new_namespace/new_namespace_page_spec.js
index 30937921900..6115dc6e61b 100644
--- a/spec/frontend/vue_shared/new_namespace/new_namespace_page_spec.js
+++ b/spec/frontend/vue_shared/new_namespace/new_namespace_page_spec.js
@@ -37,13 +37,6 @@ describe('Experimental new project creation app', () => {
window.location.hash = '';
});
- it('passes experiment to welcome component if provided', () => {
- const EXPERIMENT = 'foo';
- createComponent({ propsData: { experiment: EXPERIMENT } });
-
- expect(findWelcomePage().props().experiment).toBe(EXPERIMENT);
- });
-
describe('with empty hash', () => {
beforeEach(() => {
createComponent();
diff --git a/spec/frontend/vue_shared/oncall_schedules_list_spec.js b/spec/frontend/vue_shared/oncall_schedules_list_spec.js
index 5c30809c09b..f83a5187b8b 100644
--- a/spec/frontend/vue_shared/oncall_schedules_list_spec.js
+++ b/spec/frontend/vue_shared/oncall_schedules_list_spec.js
@@ -18,7 +18,7 @@ const mockSchedules = [
},
];
-const userName = 'User 1';
+const userName = "O'User";
describe('On-call schedules list', () => {
let wrapper;
diff --git a/spec/frontend/vue_shared/plugins/global_toast_spec.js b/spec/frontend/vue_shared/plugins/global_toast_spec.js
index 89f43a5e556..322586a772c 100644
--- a/spec/frontend/vue_shared/plugins/global_toast_spec.js
+++ b/spec/frontend/vue_shared/plugins/global_toast_spec.js
@@ -1,11 +1,10 @@
-import Vue from 'vue';
-import toast from '~/vue_shared/plugins/global_toast';
+import toast, { instance } from '~/vue_shared/plugins/global_toast';
describe('Global toast', () => {
let spyFunc;
beforeEach(() => {
- spyFunc = jest.spyOn(Vue.prototype.$toast, 'show').mockImplementation(() => {});
+ spyFunc = jest.spyOn(instance.$toast, 'show').mockImplementation(() => {});
});
afterEach(() => {
@@ -18,7 +17,7 @@ describe('Global toast', () => {
toast(arg1, arg2);
- expect(Vue.prototype.$toast.show).toHaveBeenCalledTimes(1);
- expect(Vue.prototype.$toast.show).toHaveBeenCalledWith(arg1, arg2);
+ expect(instance.$toast.show).toHaveBeenCalledTimes(1);
+ expect(instance.$toast.show).toHaveBeenCalledWith(arg1, arg2);
});
});
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 517eee6a729..facbd51168c 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
@@ -9,6 +9,7 @@ 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 { REPORT_TYPE_SAST } from '~/vue_shared/security_reports/constants';
import { buildConfigureSecurityFeatureMockFactory } from './apollo_mocks';
jest.mock('~/lib/utils/url_utility');
@@ -169,6 +170,29 @@ describe('ManageViaMr component', () => {
},
);
+ describe('canRender static method', () => {
+ it.each`
+ context | type | available | configured | canEnableByMergeRequest | expectedValue
+ ${'an unconfigured feature'} | ${REPORT_TYPE_SAST} | ${true} | ${false} | ${true} | ${true}
+ ${'a configured feature'} | ${REPORT_TYPE_SAST} | ${true} | ${true} | ${true} | ${false}
+ ${'an unavailable feature'} | ${REPORT_TYPE_SAST} | ${false} | ${false} | ${true} | ${false}
+ ${'a feature which cannot be enabled via MR'} | ${REPORT_TYPE_SAST} | ${true} | ${false} | ${false} | ${false}
+ ${'an unknown feature'} | ${'foo'} | ${true} | ${false} | ${true} | ${false}
+ `(
+ 'given $context returns $expectedValue',
+ ({ type, available, configured, canEnableByMergeRequest, expectedValue }) => {
+ expect(
+ ManageViaMr.canRender({
+ type,
+ available,
+ configured,
+ canEnableByMergeRequest,
+ }),
+ ).toBe(expectedValue);
+ },
+ );
+ });
+
describe('button props', () => {
it('passes the variant and category props to the GlButton', () => {
const variant = 'danger';
diff --git a/spec/frontend/vue_shared/security_reports/components/security_report_download_dropdown_spec.js b/spec/frontend/vue_shared/security_reports/components/security_report_download_dropdown_spec.js
index 9138d2d3f4c..4b75da0b126 100644
--- a/spec/frontend/vue_shared/security_reports/components/security_report_download_dropdown_spec.js
+++ b/spec/frontend/vue_shared/security_reports/components/security_report_download_dropdown_spec.js
@@ -40,14 +40,13 @@ describe('SecurityReportDownloadDropdown component', () => {
expect(findDropdown().props('loading')).toBe(false);
});
- it('renders a dropdown items for each artifact', () => {
+ it('renders a dropdown item for each artifact', () => {
artifacts.forEach((artifact, i) => {
const item = findDropdownItems().at(i);
expect(item.text()).toContain(artifact.name);
- expect(item.attributes()).toMatchObject({
- href: artifact.path,
- download: expect.any(String),
- });
+
+ expect(item.element.getAttribute('href')).toBe(artifact.path);
+ expect(item.element.getAttribute('download')).toBeDefined();
});
});
});
@@ -61,4 +60,32 @@ describe('SecurityReportDownloadDropdown component', () => {
expect(findDropdown().props('loading')).toBe(true);
});
});
+
+ describe('given title props', () => {
+ beforeEach(() => {
+ createComponent({ artifacts: [], loading: true, title: 'test title' });
+ });
+
+ it('should render title', () => {
+ expect(findDropdown().attributes('title')).toBe('test title');
+ });
+
+ it('should not render text', () => {
+ expect(findDropdown().text().trim()).toBe('');
+ });
+ });
+
+ describe('given text props', () => {
+ beforeEach(() => {
+ createComponent({ artifacts: [], loading: true, text: 'test text' });
+ });
+
+ it('should not render title', () => {
+ expect(findDropdown().props().title).not.toBeDefined();
+ });
+
+ it('should render text', () => {
+ expect(findDropdown().props().text).toContain('test text');
+ });
+ });
});
diff --git a/spec/frontend/vue_shared/security_reports/mock_data.js b/spec/frontend/vue_shared/security_reports/mock_data.js
index bd9ce3b7314..06631710509 100644
--- a/spec/frontend/vue_shared/security_reports/mock_data.js
+++ b/spec/frontend/vue_shared/security_reports/mock_data.js
@@ -581,9 +581,18 @@ export const secretDetectionArtifacts = [
},
];
-export const expectedDownloadDropdownProps = {
+export const expectedDownloadDropdownPropsWithTitle = {
loading: false,
artifacts: [...secretDetectionArtifacts, ...sastArtifacts],
+ text: '',
+ title: 'Download results',
+};
+
+export const expectedDownloadDropdownPropsWithText = {
+ loading: false,
+ artifacts: [...secretDetectionArtifacts, ...sastArtifacts],
+ title: '',
+ text: 'Download results',
};
/**
diff --git a/spec/frontend/vue_shared/security_reports/security_reports_app_spec.js b/spec/frontend/vue_shared/security_reports/security_reports_app_spec.js
index 038d7754776..bef538e1ff1 100644
--- a/spec/frontend/vue_shared/security_reports/security_reports_app_spec.js
+++ b/spec/frontend/vue_shared/security_reports/security_reports_app_spec.js
@@ -8,7 +8,7 @@ import createMockApollo from 'helpers/mock_apollo_helper';
import { trimText } from 'helpers/text_helper';
import waitForPromises from 'helpers/wait_for_promises';
import {
- expectedDownloadDropdownProps,
+ expectedDownloadDropdownPropsWithText,
securityReportMergeRequestDownloadPathsQueryNoArtifactsResponse,
securityReportMergeRequestDownloadPathsQueryResponse,
sastDiffSuccessMock,
@@ -99,7 +99,7 @@ describe('Security reports app', () => {
});
it('renders the download dropdown', () => {
- expect(findDownloadDropdown().props()).toEqual(expectedDownloadDropdownProps);
+ expect(findDownloadDropdown().props()).toEqual(expectedDownloadDropdownPropsWithText);
});
it('renders the expected message', () => {
@@ -203,7 +203,7 @@ describe('Security reports app', () => {
});
it('renders the download dropdown', () => {
- expect(findDownloadDropdown().props()).toEqual(expectedDownloadDropdownProps);
+ expect(findDownloadDropdown().props()).toEqual(expectedDownloadDropdownPropsWithText);
});
});
@@ -225,7 +225,7 @@ describe('Security reports app', () => {
});
it('renders the download dropdown', () => {
- expect(findDownloadDropdown().props()).toEqual(expectedDownloadDropdownProps);
+ expect(findDownloadDropdown().props()).toEqual(expectedDownloadDropdownPropsWithText);
});
});
@@ -247,7 +247,7 @@ describe('Security reports app', () => {
});
it('renders the download dropdown', () => {
- expect(findDownloadDropdown().props()).toEqual(expectedDownloadDropdownProps);
+ expect(findDownloadDropdown().props()).toEqual(expectedDownloadDropdownPropsWithText);
});
});
diff --git a/spec/frontend/vuex_shared/bindings_spec.js b/spec/frontend/vuex_shared/bindings_spec.js
index 0f91a09018f..4e210143c8c 100644
--- a/spec/frontend/vuex_shared/bindings_spec.js
+++ b/spec/frontend/vuex_shared/bindings_spec.js
@@ -3,7 +3,7 @@ import { mapComputed } from '~/vuex_shared/bindings';
describe('Binding utils', () => {
describe('mapComputed', () => {
- const defaultArgs = [['baz'], 'bar', 'foo'];
+ const defaultArgs = [['baz'], 'bar', 'foo', 'qux'];
const createDummy = (mapComputedArgs = defaultArgs) => ({
computed: {
@@ -29,12 +29,18 @@ describe('Binding utils', () => {
},
};
- it('returns an object with keys equal to the first fn parameter ', () => {
+ it('returns an object with keys equal to the first fn parameter', () => {
const keyList = ['foo1', 'foo2'];
const result = mapComputed(keyList, 'foo', 'bar');
expect(Object.keys(result)).toEqual(keyList);
});
+ it('returns an object with keys equal to the first fn parameter when the root is a function', () => {
+ const keyList = ['foo1', 'foo2'];
+ const result = mapComputed(keyList, 'foo', (state) => state.bar);
+ expect(Object.keys(result)).toEqual(keyList);
+ });
+
it('returned object has set and get function', () => {
const result = mapComputed(['baz'], 'foo', 'bar');
expect(result.baz.set).toBeDefined();