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

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'spec/frontend')
-rw-r--r--spec/frontend/__helpers__/matchers.js68
-rw-r--r--spec/frontend/__helpers__/matchers/index.js3
-rw-r--r--spec/frontend/__helpers__/matchers/to_have_sprite_icon.js36
-rw-r--r--spec/frontend/__helpers__/matchers/to_have_tracking_attributes.js35
-rw-r--r--spec/frontend/__helpers__/matchers/to_have_tracking_attributes_spec.js65
-rw-r--r--spec/frontend/__helpers__/matchers/to_match_interpolated_text.js30
-rw-r--r--spec/frontend/__helpers__/matchers/to_match_interpolated_text_spec.js46
-rw-r--r--spec/frontend/__helpers__/matchers_spec.js48
-rw-r--r--spec/frontend/__helpers__/shared_test_setup.js2
-rw-r--r--spec/frontend/__helpers__/wait_using_real_timer.js7
-rw-r--r--spec/frontend/alerts_settings/components/alerts_settings_form_spec.js10
-rw-r--r--spec/frontend/analytics/shared/components/projects_dropdown_filter_spec.js4
-rw-r--r--spec/frontend/api/packages_api_spec.js11
-rw-r--r--spec/frontend/behaviors/copy_to_clipboard_spec.js187
-rw-r--r--spec/frontend/blob/components/__snapshots__/blob_header_filepath_spec.js.snap14
-rw-r--r--spec/frontend/blob/components/__snapshots__/blob_header_spec.js.snap2
-rw-r--r--spec/frontend/blob/components/blob_edit_header_spec.js2
-rw-r--r--spec/frontend/blob/components/blob_header_filepath_spec.js8
-rw-r--r--spec/frontend/blob/line_highlighter_spec.js (renamed from spec/frontend/line_highlighter_spec.js)2
-rw-r--r--spec/frontend/blob/viewer/index_spec.js1
-rw-r--r--spec/frontend/boards/components/board_card_spec.js4
-rw-r--r--spec/frontend/boards/components/board_content_sidebar_spec.js20
-rw-r--r--spec/frontend/boards/components/board_filtered_search_spec.js22
-rw-r--r--spec/frontend/boards/components/board_list_header_spec.js8
-rw-r--r--spec/frontend/boards/components/boards_selector_spec.js12
-rw-r--r--spec/frontend/boards/stores/actions_spec.js32
-rw-r--r--spec/frontend/branches/branches_delete_modal_spec.js40
-rw-r--r--spec/frontend/ci_lint/components/ci_lint_spec.js2
-rw-r--r--spec/frontend/clusters/agents/components/show_spec.js8
-rw-r--r--spec/frontend/clusters/forms/components/integration_form_spec.js4
-rw-r--r--spec/frontend/clusters_list/components/agent_options_spec.js211
-rw-r--r--spec/frontend/clusters_list/components/agent_table_spec.js26
-rw-r--r--spec/frontend/clusters_list/components/clusters_spec.js2
-rw-r--r--spec/frontend/clusters_list/mocks/apollo.js12
-rw-r--r--spec/frontend/commit/pipelines/pipelines_table_spec.js2
-rw-r--r--spec/frontend/content_editor/components/wrappers/frontmatter_spec.js5
-rw-r--r--spec/frontend/content_editor/extensions/code_block_highlight_spec.js6
-rw-r--r--spec/frontend/content_editor/extensions/code_spec.js8
-rw-r--r--spec/frontend/content_editor/extensions/frontmatter_spec.js25
-rw-r--r--spec/frontend/content_editor/extensions/image_spec.js41
-rw-r--r--spec/frontend/content_editor/extensions/link_spec.js2
-rw-r--r--spec/frontend/content_editor/services/markdown_serializer_spec.js15
-rw-r--r--spec/frontend/create_cluster/components/cluster_form_dropdown_spec.js2
-rw-r--r--spec/frontend/create_cluster/eks_cluster/components/service_credentials_form_spec.js6
-rw-r--r--spec/frontend/create_cluster/gke_cluster/components/gke_machine_type_dropdown_spec.js2
-rw-r--r--spec/frontend/create_cluster/gke_cluster/components/gke_project_id_dropdown_spec.js6
-rw-r--r--spec/frontend/create_cluster/gke_cluster/components/gke_zone_dropdown_spec.js2
-rw-r--r--spec/frontend/crm/contact_form_spec.js4
-rw-r--r--spec/frontend/crm/form_spec.js278
-rw-r--r--spec/frontend/crm/mock_data.js8
-rw-r--r--spec/frontend/crm/new_organization_form_spec.js2
-rw-r--r--spec/frontend/cycle_analytics/stage_table_spec.js57
-rw-r--r--spec/frontend/cycle_analytics/value_stream_metrics_spec.js2
-rw-r--r--spec/frontend/deploy_freeze/components/timezone_dropdown_spec.js2
-rw-r--r--spec/frontend/design_management/components/design_notes/__snapshots__/design_note_signed_out_spec.js.snap41
-rw-r--r--spec/frontend/design_management/components/design_notes/design_discussion_spec.js50
-rw-r--r--spec/frontend/design_management/components/design_notes/design_note_signed_out_spec.js36
-rw-r--r--spec/frontend/design_management/components/design_overlay_spec.js10
-rw-r--r--spec/frontend/design_management/components/design_presentation_spec.js27
-rw-r--r--spec/frontend/design_management/components/design_sidebar_spec.js51
-rw-r--r--spec/frontend/design_management/components/image_spec.js2
-rw-r--r--spec/frontend/design_management/components/toolbar/design_navigation_spec.js4
-rw-r--r--spec/frontend/design_management/components/toolbar/index_spec.js2
-rw-r--r--spec/frontend/design_management/components/upload/design_version_dropdown_spec.js2
-rw-r--r--spec/frontend/design_management/pages/design/__snapshots__/index_spec.js.snap27
-rw-r--r--spec/frontend/design_management/pages/design/index_spec.js2
-rw-r--r--spec/frontend/design_management/pages/index_spec.js14
-rw-r--r--spec/frontend/diffs/components/image_diff_overlay_spec.js19
-rw-r--r--spec/frontend/editor/source_editor_spec.js15
-rw-r--r--spec/frontend/emoji/components/category_spec.js2
-rw-r--r--spec/frontend/emoji/components/emoji_list_spec.js2
-rw-r--r--spec/frontend/environments/confirm_rollback_modal_spec.js8
-rw-r--r--spec/frontend/environments/deployment_spec.js29
-rw-r--r--spec/frontend/environments/deployment_status_badge_spec.js42
-rw-r--r--spec/frontend/environments/environment_actions_spec.js35
-rw-r--r--spec/frontend/environments/environment_stop_spec.js72
-rw-r--r--spec/frontend/environments/graphql/mock_data.js136
-rw-r--r--spec/frontend/environments/graphql/resolvers_spec.js34
-rw-r--r--spec/frontend/environments/new_environment_folder_spec.js34
-rw-r--r--spec/frontend/environments/new_environment_item_spec.js341
-rw-r--r--spec/frontend/environments/new_environments_app_spec.js37
-rw-r--r--spec/frontend/error_tracking/components/error_details_spec.js28
-rw-r--r--spec/frontend/error_tracking/components/error_tracking_list_spec.js2
-rw-r--r--spec/frontend/fixtures/blob.rb1
-rw-r--r--spec/frontend/fixtures/runner.rb117
-rw-r--r--spec/frontend/fixtures/static/project_select_combo_button.html2
-rw-r--r--spec/frontend/flash_spec.js255
-rw-r--r--spec/frontend/google_cloud/components/app_spec.js2
-rw-r--r--spec/frontend/google_cloud/components/deployments_service_table_spec.js40
-rw-r--r--spec/frontend/google_cloud/components/home_spec.js4
-rw-r--r--spec/frontend/google_tag_manager/index_spec.js259
-rw-r--r--spec/frontend/groups/components/group_item_spec.js1
-rw-r--r--spec/frontend/groups/components/item_stats_spec.js1
-rw-r--r--spec/frontend/groups/landing_spec.js (renamed from spec/frontend/landing_spec.js)2
-rw-r--r--spec/frontend/groups/transfer_edit_spec.js (renamed from spec/frontend/transfer_edit_spec.js)2
-rw-r--r--spec/frontend/ide/components/jobs/__snapshots__/stage_spec.js.snap9
-rw-r--r--spec/frontend/ide/components/preview/clientside_spec.js20
-rw-r--r--spec/frontend/ide/components/repo_editor_spec.js11
-rw-r--r--spec/frontend/ide/components/terminal/terminal_spec.js2
-rw-r--r--spec/frontend/ide/stores/modules/pipelines/actions_spec.js18
-rw-r--r--spec/frontend/integrations/edit/components/dynamic_field_spec.js13
-rw-r--r--spec/frontend/integrations/edit/components/integration_form_spec.js413
-rw-r--r--spec/frontend/integrations/edit/store/actions_spec.js37
-rw-r--r--spec/frontend/integrations/edit/store/mutations_spec.js24
-rw-r--r--spec/frontend/integrations/edit/store/state_spec.js2
-rw-r--r--spec/frontend/integrations/overrides/components/integration_overrides_spec.js16
-rw-r--r--spec/frontend/integrations/overrides/components/integration_tabs_spec.js64
-rw-r--r--spec/frontend/invite_members/components/invite_members_modal_spec.js12
-rw-r--r--spec/frontend/invite_members/mock_data/api_responses.js2
-rw-r--r--spec/frontend/issuable/components/related_issuable_item_spec.js2
-rw-r--r--spec/frontend/issues/create_merge_request_dropdown_spec.js (renamed from spec/frontend/create_merge_request_dropdown_spec.js)2
-rw-r--r--spec/frontend/issues/list/components/issue_card_time_info_spec.js (renamed from spec/frontend/issues_list/components/issue_card_time_info_spec.js)2
-rw-r--r--spec/frontend/issues/list/components/issues_list_app_spec.js (renamed from spec/frontend/issues_list/components/issues_list_app_spec.js)16
-rw-r--r--spec/frontend/issues/list/components/jira_issues_import_status_app_spec.js (renamed from spec/frontend/issues_list/components/jira_issues_import_status_app_spec.js)2
-rw-r--r--spec/frontend/issues/list/components/new_issue_dropdown_spec.js (renamed from spec/frontend/issues_list/components/new_issue_dropdown_spec.js)4
-rw-r--r--spec/frontend/issues/list/mock_data.js (renamed from spec/frontend/issues_list/mock_data.js)0
-rw-r--r--spec/frontend/issues/list/utils_spec.js (renamed from spec/frontend/issues_list/utils_spec.js)6
-rw-r--r--spec/frontend/issues/new/components/title_suggestions_spec.js14
-rw-r--r--spec/frontend/issues/show/components/fields/type_spec.js18
-rw-r--r--spec/frontend/issues/show/components/header_actions_spec.js19
-rw-r--r--spec/frontend/issues/show/components/sentry_error_stack_trace_spec.js (renamed from spec/frontend/issues/sentry_error_stack_trace/components/sentry_error_stack_trace_spec.js)11
-rw-r--r--spec/frontend/issues/show/issue_spec.js6
-rw-r--r--spec/frontend/issues_list/components/__snapshots__/issuables_list_app_spec.js.snap14
-rw-r--r--spec/frontend/issues_list/components/issuable_spec.js508
-rw-r--r--spec/frontend/issues_list/components/issuables_list_app_spec.js653
-rw-r--r--spec/frontend/issues_list/issuable_list_test_data.js77
-rw-r--r--spec/frontend/issues_list/service_desk_helper_spec.js28
-rw-r--r--spec/frontend/jira_import/utils/jira_import_utils_spec.js2
-rw-r--r--spec/frontend/jobs/bridge/app_spec.js123
-rw-r--r--spec/frontend/jobs/bridge/components/empty_state_spec.js7
-rw-r--r--spec/frontend/jobs/bridge/components/sidebar_spec.js97
-rw-r--r--spec/frontend/jobs/bridge/mock_data.js101
-rw-r--r--spec/frontend/jobs/components/table/job_table_app_spec.js2
-rw-r--r--spec/frontend/labels/delete_label_modal_spec.js33
-rw-r--r--spec/frontend/lib/utils/resize_observer_spec.js68
-rw-r--r--spec/frontend/monitoring/components/__snapshots__/dashboard_template_spec.js.snap2
-rw-r--r--spec/frontend/monitoring/components/charts/time_series_spec.js2
-rw-r--r--spec/frontend/monitoring/components/dashboard_spec.js2
-rw-r--r--spec/frontend/monitoring/components/dashboards_dropdown_spec.js2
-rw-r--r--spec/frontend/mr_popover/mr_popover_spec.js4
-rw-r--r--spec/frontend/notes/components/comment_form_spec.js4
-rw-r--r--spec/frontend/notes/components/note_form_spec.js2
-rw-r--r--spec/frontend/notifications/components/custom_notifications_modal_spec.js6
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/details_header_spec.js76
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/empty_state_spec.js54
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/tags_list_row_spec.js21
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/tags_list_spec.js100
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/pages/details_spec.js19
-rw-r--r--spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/__snapshots__/file_sha_spec.js.snap5
-rw-r--r--spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/file_sha_spec.js2
-rw-r--r--spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/__snapshots__/packages_list_app_spec.js.snap8
-rw-r--r--spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/packages_list_spec.js8
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/conan_installation_spec.js.snap14
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/file_sha_spec.js.snap5
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/maven_installation_spec.js.snap42
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/npm_installation_spec.js.snap14
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/nuget_installation_spec.js.snap14
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/pypi_installation_spec.js.snap24
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/composer_installation_spec.js8
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/conan_installation_spec.js23
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/file_sha_spec.js2
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/maven_installation_spec.js30
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/npm_installation_spec.js28
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/nuget_installation_spec.js18
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/package_files_spec.js2
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/pypi_installation_spec.js22
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/list/__snapshots__/package_list_row_spec.js.snap10
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/list/package_list_row_spec.js23
-rw-r--r--spec/frontend/packages_and_registries/package_registry/mock_data.js11
-rw-r--r--spec/frontend/packages_and_registries/package_registry/pages/__snapshots__/list_spec.js.snap8
-rw-r--r--spec/frontend/packages_and_registries/package_registry/pages/details_spec.js (renamed from spec/frontend/packages_and_registries/package_registry/components/details/app_spec.js)38
-rw-r--r--spec/frontend/packages_and_registries/shared/components/__snapshots__/publish_method_spec.js.snap (renamed from spec/frontend/packages_and_registries/shared/__snapshots__/publish_method_spec.js.snap)0
-rw-r--r--spec/frontend/packages_and_registries/shared/components/__snapshots__/registry_breadcrumb_spec.js.snap (renamed from spec/frontend/packages_and_registries/container_registry/explorer/components/__snapshots__/registry_breadcrumb_spec.js.snap)38
-rw-r--r--spec/frontend/packages_and_registries/shared/components/package_icon_and_name_spec.js (renamed from spec/frontend/packages_and_registries/shared/package_icon_and_name_spec.js)0
-rw-r--r--spec/frontend/packages_and_registries/shared/components/package_path_spec.js (renamed from spec/frontend/packages_and_registries/shared/package_path_spec.js)0
-rw-r--r--spec/frontend/packages_and_registries/shared/components/package_tags_spec.js (renamed from spec/frontend/packages_and_registries/shared/package_tags_spec.js)0
-rw-r--r--spec/frontend/packages_and_registries/shared/components/packages_list_loader_spec.js (renamed from spec/frontend/packages_and_registries/shared/packages_list_loader_spec.js)0
-rw-r--r--spec/frontend/packages_and_registries/shared/components/persisted_search_spec.js145
-rw-r--r--spec/frontend/packages_and_registries/shared/components/publish_method_spec.js (renamed from spec/frontend/packages_and_registries/shared/publish_method_spec.js)0
-rw-r--r--spec/frontend/packages_and_registries/shared/components/registry_breadcrumb_spec.js (renamed from spec/frontend/packages_and_registries/container_registry/explorer/components/registry_breadcrumb_spec.js)2
-rw-r--r--spec/frontend/pages/dashboard/todos/index/todos_spec.js4
-rw-r--r--spec/frontend/pages/projects/pipeline_schedules/shared/components/timezone_dropdown_spec.js6
-rw-r--r--spec/frontend/pages/projects/shared/permissions/components/settings_panel_spec.js42
-rw-r--r--spec/frontend/pages/shared/nav/sidebar_tracking_spec.js36
-rw-r--r--spec/frontend/pages/shared/wikis/components/wiki_form_spec.js211
-rw-r--r--spec/frontend/pipeline_editor/components/editor/text_editor_spec.js40
-rw-r--r--spec/frontend/pipeline_editor/components/header/validation_segment_spec.js64
-rw-r--r--spec/frontend/pipeline_editor/components/ui/editor_tab_spec.js89
-rw-r--r--spec/frontend/pipeline_editor/mock_data.js1
-rw-r--r--spec/frontend/pipeline_editor/pipeline_editor_app_spec.js131
-rw-r--r--spec/frontend/pipelines/__snapshots__/utils_spec.js.snap29
-rw-r--r--spec/frontend/pipelines/graph/mock_data.js125
-rw-r--r--spec/frontend/profile/account/components/update_username_spec.js2
-rw-r--r--spec/frontend/profile/add_ssh_key_validation_spec.js36
-rw-r--r--spec/frontend/project_select_combo_button_spec.js4
-rw-r--r--spec/frontend/projects/commits/components/author_select_spec.js10
-rw-r--r--spec/frontend/projects/compare/components/revision_dropdown_legacy_spec.js2
-rw-r--r--spec/frontend/projects/project_find_file_spec.js (renamed from spec/frontend/project_find_file_spec.js)2
-rw-r--r--spec/frontend/repository/components/blob_button_group_spec.js47
-rw-r--r--spec/frontend/repository/components/blob_content_viewer_spec.js28
-rw-r--r--spec/frontend/repository/components/blob_controls_spec.js88
-rw-r--r--spec/frontend/repository/components/breadcrumbs_spec.js8
-rw-r--r--spec/frontend/repository/components/last_commit_spec.js2
-rw-r--r--spec/frontend/repository/components/preview/index_spec.js6
-rw-r--r--spec/frontend/repository/components/table/index_spec.js2
-rw-r--r--spec/frontend/repository/components/table/row_spec.js2
-rw-r--r--spec/frontend/repository/components/tree_content_spec.js8
-rw-r--r--spec/frontend/repository/components/upload_blob_modal_spec.js4
-rw-r--r--spec/frontend/repository/mock_data.js23
-rw-r--r--spec/frontend/runner/admin_runner_edit/admin_runner_edit_app_spec.js (renamed from spec/frontend/runner/runner_detail/runner_details_app_spec.js)31
-rw-r--r--spec/frontend/runner/admin_runners/admin_runners_app_spec.js116
-rw-r--r--spec/frontend/runner/components/cells/runner_actions_cell_spec.js66
-rw-r--r--spec/frontend/runner/components/registration/registration_token_reset_dropdown_item_spec.js40
-rw-r--r--spec/frontend/runner/components/runner_header_spec.js93
-rw-r--r--spec/frontend/runner/components/runner_list_spec.js4
-rw-r--r--spec/frontend/runner/components/runner_status_badge_spec.js20
-rw-r--r--spec/frontend/runner/components/runner_type_alert_spec.js61
-rw-r--r--spec/frontend/runner/components/runner_update_form_spec.js12
-rw-r--r--spec/frontend/runner/components/search_tokens/tag_token_spec.js6
-rw-r--r--spec/frontend/runner/components/stat/runner_online_stat_spec.js34
-rw-r--r--spec/frontend/runner/components/stat/runner_stats_spec.js46
-rw-r--r--spec/frontend/runner/components/stat/runner_status_stat_spec.js67
-rw-r--r--spec/frontend/runner/group_runners/group_runners_app_spec.js50
-rw-r--r--spec/frontend/runner/mock_data.js4
-rw-r--r--spec/frontend/runner/runner_search_utils_spec.js18
-rw-r--r--spec/frontend/runner/runner_update_form_utils_spec.js (renamed from spec/frontend/runner/runner_detail/runner_update_form_utils_spec.js)7
-rw-r--r--spec/frontend/search/topbar/components/searchable_dropdown_spec.js4
-rw-r--r--spec/frontend/security_configuration/components/app_spec.js38
-rw-r--r--spec/frontend/security_configuration/components/training_provider_list_spec.js181
-rw-r--r--spec/frontend/security_configuration/mock_data.js25
-rw-r--r--spec/frontend/serverless/components/__snapshots__/empty_state_spec.js.snap8
-rw-r--r--spec/frontend/set_status_modal/set_status_modal_wrapper_spec.js11
-rw-r--r--spec/frontend/sidebar/components/reviewers/uncollapsed_reviewer_list_spec.js8
-rw-r--r--spec/frontend/sidebar/participants_spec.js8
-rw-r--r--spec/frontend/snippets/components/__snapshots__/snippet_description_edit_spec.js.snap2
-rw-r--r--spec/frontend/snippets/components/snippet_blob_view_spec.js2
-rw-r--r--spec/frontend/snippets/components/snippet_header_spec.js2
-rw-r--r--spec/frontend/static_site_editor/components/edit_area_spec.js4
-rw-r--r--spec/frontend/static_site_editor/rich_content_editor/modals/add_image/add_image_modal_spec.js4
-rw-r--r--spec/frontend/static_site_editor/rich_content_editor/services/renderers/render_identifier_paragraph_spec.js2
-rw-r--r--spec/frontend/terraform/components/states_table_actions_spec.js2
-rw-r--r--spec/frontend/tracking/tracking_initialization_spec.js5
-rw-r--r--spec/frontend/version_check_image_spec.js42
-rw-r--r--spec/frontend/vue_mr_widget/components/approvals/approvals_spec.js2
-rw-r--r--spec/frontend/vue_mr_widget/components/mr_widget_header_spec.js4
-rw-r--r--spec/frontend/vue_mr_widget/components/mr_widget_rebase_spec.js209
-rw-r--r--spec/frontend/vue_mr_widget/components/states/merge_checks_failed_spec.js29
-rw-r--r--spec/frontend/vue_mr_widget/components/states/mr_widget_auto_merge_enabled_spec.js4
-rw-r--r--spec/frontend/vue_mr_widget/components/states/mr_widget_commits_header_spec.js4
-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_missing_branch_spec.js2
-rw-r--r--spec/frontend/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js8
-rw-r--r--spec/frontend/vue_mr_widget/components/terraform/mock_data.js6
-rw-r--r--spec/frontend/vue_mr_widget/components/terraform/mr_widget_terraform_container_spec.js2
-rw-r--r--spec/frontend/vue_mr_widget/extentions/terraform/index_spec.js178
-rw-r--r--spec/frontend/vue_mr_widget/mock_data.js2
-rw-r--r--spec/frontend/vue_mr_widget/mr_widget_options_spec.js69
-rw-r--r--spec/frontend/vue_mr_widget/stores/mr_widget_store_spec.js2
-rw-r--r--spec/frontend/vue_mr_widget/test_extensions.js10
-rw-r--r--spec/frontend/vue_shared/alert_details/alert_details_spec.js2
-rw-r--r--spec/frontend/vue_shared/alert_details/sidebar/alert_sidebar_assignees_spec.js6
-rw-r--r--spec/frontend/vue_shared/components/chronic_duration_input_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/clipboard_button_spec.js72
-rw-r--r--spec/frontend/vue_shared/components/confirm_danger/confirm_danger_spec.js6
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_bar_root_spec.js22
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/author_token_spec.js14
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/branch_token_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/emoji_token_spec.js2
-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.js2
-rw-r--r--spec/frontend/vue_shared/components/gitlab_version_check_spec.js77
-rw-r--r--spec/frontend/vue_shared/components/line_numbers_spec.js36
-rw-r--r--spec/frontend/vue_shared/components/markdown/field_spec.js36
-rw-r--r--spec/frontend/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/registry/__snapshots__/code_instruction_spec.js.snap11
-rw-r--r--spec/frontend/vue_shared/components/registry/code_instruction_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/sidebar/issuable_move_dropdown_spec.js24
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_create_view_spec.js8
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view_spec.js24
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view_spec.js26
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view_spec.js13
-rw-r--r--spec/frontend/vue_shared/components/source_viewer_spec.js47
-rw-r--r--spec/frontend/vue_shared/components/web_ide_link_spec.js155
-rw-r--r--spec/frontend/vue_shared/directives/track_event_spec.js2
-rw-r--r--spec/frontend/vue_shared/issuable/list/components/issuable_list_root_spec.js6
-rw-r--r--spec/frontend/vue_shared/issuable/list/components/issuable_tabs_spec.js5
-rw-r--r--spec/frontend/vue_shared/issuable/list/mock_data.js2
-rw-r--r--spec/frontend/vue_shared/issuable/show/components/issuable_title_spec.js2
-rw-r--r--spec/frontend/work_items/pages/work_item_root_spec.js31
289 files changed, 6837 insertions, 2959 deletions
diff --git a/spec/frontend/__helpers__/matchers.js b/spec/frontend/__helpers__/matchers.js
deleted file mode 100644
index 945abdafe9a..00000000000
--- a/spec/frontend/__helpers__/matchers.js
+++ /dev/null
@@ -1,68 +0,0 @@
-export default {
- toHaveSpriteIcon: (element, iconName) => {
- if (!iconName) {
- throw new Error('toHaveSpriteIcon is missing iconName argument!');
- }
-
- if (!(element instanceof HTMLElement)) {
- throw new Error(`${element} is not a DOM element!`);
- }
-
- const iconReferences = [].slice.apply(element.querySelectorAll('svg use'));
- const matchingIcon = iconReferences.find(
- (reference) => reference.parentNode.getAttribute('data-testid') === `${iconName}-icon`,
- );
-
- const pass = Boolean(matchingIcon);
-
- let message;
- if (pass) {
- message = `${element.outerHTML} contains the sprite icon "${iconName}"!`;
- } else {
- message = `${element.outerHTML} does not contain the sprite icon "${iconName}"!`;
-
- const existingIcons = iconReferences.map((reference) => {
- const iconUrl = reference.getAttribute('href');
- return `"${iconUrl.replace(/^.+#/, '')}"`;
- });
- if (existingIcons.length > 0) {
- message += ` (only found ${existingIcons.join(',')})`;
- }
- }
-
- return {
- pass,
- message: () => message,
- };
- },
- toMatchInterpolatedText(received, match) {
- let clearReceived;
- let clearMatch;
-
- try {
- clearReceived = received.replace(/\s\s+/gm, ' ').replace(/\s\./gm, '.').trim();
- } catch (e) {
- return { actual: received, message: 'The received value is not a string', pass: false };
- }
- try {
- clearMatch = match.replace(/%{\w+}/gm, '').trim();
- } catch (e) {
- return { message: 'The comparator value is not a string', pass: false };
- }
- const pass = clearReceived === clearMatch;
- const message = pass
- ? () => `
- \n\n
- Expected: ${this.utils.printExpected(clearReceived)}
- To not equal: ${this.utils.printReceived(clearMatch)}
- `
- : () =>
- `
- \n\n
- Expected: ${this.utils.printExpected(clearReceived)}
- To equal: ${this.utils.printReceived(clearMatch)}
- `;
-
- return { actual: received, message, pass };
- },
-};
diff --git a/spec/frontend/__helpers__/matchers/index.js b/spec/frontend/__helpers__/matchers/index.js
new file mode 100644
index 00000000000..76571bafb06
--- /dev/null
+++ b/spec/frontend/__helpers__/matchers/index.js
@@ -0,0 +1,3 @@
+export * from './to_have_sprite_icon';
+export * from './to_have_tracking_attributes';
+export * from './to_match_interpolated_text';
diff --git a/spec/frontend/__helpers__/matchers/to_have_sprite_icon.js b/spec/frontend/__helpers__/matchers/to_have_sprite_icon.js
new file mode 100644
index 00000000000..bce9d93bea8
--- /dev/null
+++ b/spec/frontend/__helpers__/matchers/to_have_sprite_icon.js
@@ -0,0 +1,36 @@
+export const toHaveSpriteIcon = (element, iconName) => {
+ if (!iconName) {
+ throw new Error('toHaveSpriteIcon is missing iconName argument!');
+ }
+
+ if (!(element instanceof HTMLElement)) {
+ throw new Error(`${element} is not a DOM element!`);
+ }
+
+ const iconReferences = [].slice.apply(element.querySelectorAll('svg use'));
+ const matchingIcon = iconReferences.find(
+ (reference) => reference.parentNode.getAttribute('data-testid') === `${iconName}-icon`,
+ );
+
+ const pass = Boolean(matchingIcon);
+
+ let message;
+ if (pass) {
+ message = `${element.outerHTML} contains the sprite icon "${iconName}"!`;
+ } else {
+ message = `${element.outerHTML} does not contain the sprite icon "${iconName}"!`;
+
+ const existingIcons = iconReferences.map((reference) => {
+ const iconUrl = reference.getAttribute('href');
+ return `"${iconUrl.replace(/^.+#/, '')}"`;
+ });
+ if (existingIcons.length > 0) {
+ message += ` (only found ${existingIcons.join(',')})`;
+ }
+ }
+
+ return {
+ pass,
+ message: () => message,
+ };
+};
diff --git a/spec/frontend/__helpers__/matchers/to_have_tracking_attributes.js b/spec/frontend/__helpers__/matchers/to_have_tracking_attributes.js
new file mode 100644
index 00000000000..fd3f3aa042f
--- /dev/null
+++ b/spec/frontend/__helpers__/matchers/to_have_tracking_attributes.js
@@ -0,0 +1,35 @@
+import { diff } from 'jest-diff';
+import { isObject, mapValues, isEqual } from 'lodash';
+
+export const toHaveTrackingAttributes = (actual, obj) => {
+ if (!(actual instanceof Element)) {
+ return { actual, message: () => 'The received value must be an Element.', pass: false };
+ }
+
+ if (!isObject(obj)) {
+ return {
+ message: () => `The matching object must be an object. Found ${obj}.`,
+ pass: false,
+ };
+ }
+
+ const actualAttributes = mapValues(obj, (val, key) => actual.getAttribute(`data-track-${key}`));
+
+ const matcherPass = isEqual(actualAttributes, obj);
+
+ const failMessage = () => {
+ // We can match, but still fail because we're in a `expect...not.` context
+ if (matcherPass) {
+ return `Expected the element's tracking attributes not to match. Found that they matched ${JSON.stringify(
+ obj,
+ )}.`;
+ }
+
+ const objDiff = diff(actualAttributes, obj);
+ return `Expected the element's tracking attributes to match the given object. Diff:
+${objDiff}
+`;
+ };
+
+ return { actual, message: failMessage, pass: matcherPass };
+};
diff --git a/spec/frontend/__helpers__/matchers/to_have_tracking_attributes_spec.js b/spec/frontend/__helpers__/matchers/to_have_tracking_attributes_spec.js
new file mode 100644
index 00000000000..74073ed4063
--- /dev/null
+++ b/spec/frontend/__helpers__/matchers/to_have_tracking_attributes_spec.js
@@ -0,0 +1,65 @@
+import { diff } from 'jest-diff';
+
+describe('custom matcher toHaveTrackingAttributes', () => {
+ const createElementWithAttrs = (attributes) => {
+ const el = document.createElement('div');
+
+ Object.entries(attributes).forEach(([key, value]) => {
+ el.setAttribute(key, value);
+ });
+
+ return el;
+ };
+
+ it('blows up if actual is not an element', () => {
+ expect(() => {
+ expect({}).toHaveTrackingAttributes({});
+ }).toThrow('The received value must be an Element.');
+ });
+
+ it('blows up if expected is not an object', () => {
+ expect(() => {
+ expect(createElementWithAttrs({})).toHaveTrackingAttributes('foo');
+ }).toThrow('The matching object must be an object.');
+ });
+
+ it('prints diff when fails', () => {
+ const expectedDiff = diff({ label: 'foo' }, { label: 'a' });
+ expect(() => {
+ expect(createElementWithAttrs({ 'data-track-label': 'foo' })).toHaveTrackingAttributes({
+ label: 'a',
+ });
+ }).toThrow(
+ `Expected the element's tracking attributes to match the given object. Diff:\n${expectedDiff}\n`,
+ );
+ });
+
+ describe('positive assertions', () => {
+ it.each`
+ attrs | expected
+ ${{ 'data-track-label': 'foo' }} | ${{ label: 'foo' }}
+ ${{ 'data-track-label': 'foo' }} | ${{}}
+ ${{ 'data-track-label': 'foo', label: 'bar' }} | ${{ label: 'foo' }}
+ ${{ 'data-track-label': 'foo', 'data-track-extra': '123' }} | ${{ label: 'foo', extra: '123' }}
+ ${{ 'data-track-label': 'foo', 'data-track-extra': '123' }} | ${{ extra: '123' }}
+ ${{ label: 'foo', extra: '123', id: '7' }} | ${{}}
+ `('$expected matches element with attrs $attrs', ({ attrs, expected }) => {
+ expect(createElementWithAttrs(attrs)).toHaveTrackingAttributes(expected);
+ });
+ });
+
+ describe('negative assertions', () => {
+ it.each`
+ attrs | expected
+ ${{}} | ${{ label: 'foo' }}
+ ${{ label: 'foo' }} | ${{ label: 'foo' }}
+ ${{ 'data-track-label': 'bar', label: 'foo' }} | ${{ label: 'foo' }}
+ ${{ 'data-track-label': 'foo' }} | ${{ extra: '123' }}
+ ${{ 'data-track-label': 'foo', 'data-track-extra': '123' }} | ${{ label: 'foo', extra: '456' }}
+ ${{ 'data-track-label': 'foo', 'data-track-extra': '123' }} | ${{ label: 'foo', extra: '123', action: 'click' }}
+ ${{ label: 'foo', extra: '123', id: '7' }} | ${{ id: '7' }}
+ `('$expected does not match element with attrs $attrs', ({ attrs, expected }) => {
+ expect(createElementWithAttrs(attrs)).not.toHaveTrackingAttributes(expected);
+ });
+ });
+});
diff --git a/spec/frontend/__helpers__/matchers/to_match_interpolated_text.js b/spec/frontend/__helpers__/matchers/to_match_interpolated_text.js
new file mode 100644
index 00000000000..4ce814a01b4
--- /dev/null
+++ b/spec/frontend/__helpers__/matchers/to_match_interpolated_text.js
@@ -0,0 +1,30 @@
+export const toMatchInterpolatedText = (received, match) => {
+ let clearReceived;
+ let clearMatch;
+
+ try {
+ clearReceived = received.replace(/\s\s+/gm, ' ').replace(/\s\./gm, '.').trim();
+ } catch (e) {
+ return { actual: received, message: 'The received value is not a string', pass: false };
+ }
+ try {
+ clearMatch = match.replace(/%{\w+}/gm, '').trim();
+ } catch (e) {
+ return { message: 'The comparator value is not a string', pass: false };
+ }
+ const pass = clearReceived === clearMatch;
+ const message = pass
+ ? () => `
+ \n\n
+ Expected: ${this.utils.printExpected(clearReceived)}
+ To not equal: ${this.utils.printReceived(clearMatch)}
+ `
+ : () =>
+ `
+ \n\n
+ Expected: ${this.utils.printExpected(clearReceived)}
+ To equal: ${this.utils.printReceived(clearMatch)}
+ `;
+
+ return { actual: received, message, pass };
+};
diff --git a/spec/frontend/__helpers__/matchers/to_match_interpolated_text_spec.js b/spec/frontend/__helpers__/matchers/to_match_interpolated_text_spec.js
new file mode 100644
index 00000000000..f6fd00011fe
--- /dev/null
+++ b/spec/frontend/__helpers__/matchers/to_match_interpolated_text_spec.js
@@ -0,0 +1,46 @@
+describe('custom matcher toMatchInterpolatedText', () => {
+ describe('malformed input', () => {
+ it.each([null, 1, Symbol, Array, Object])(
+ 'fails graciously if the expected value is %s',
+ (expected) => {
+ expect(expected).not.toMatchInterpolatedText('null');
+ },
+ );
+ });
+ describe('malformed matcher', () => {
+ it.each([null, 1, Symbol, Array, Object])(
+ 'fails graciously if the matcher is %s',
+ (matcher) => {
+ expect('null').not.toMatchInterpolatedText(matcher);
+ },
+ );
+ });
+
+ describe('positive assertion', () => {
+ it.each`
+ htmlString | templateString
+ ${'foo'} | ${'foo'}
+ ${'foo'} | ${'foo%{foo}'}
+ ${'foo '} | ${'foo'}
+ ${'foo '} | ${'foo%{foo}'}
+ ${'foo . '} | ${'foo%{foo}.'}
+ ${'foo bar . '} | ${'foo%{foo} bar.'}
+ ${'foo\n\nbar . '} | ${'foo%{foo} bar.'}
+ ${'foo bar . .'} | ${'foo%{fooStart} bar.%{fooEnd}.'}
+ `('$htmlString equals $templateString', ({ htmlString, templateString }) => {
+ expect(htmlString).toMatchInterpolatedText(templateString);
+ });
+ });
+
+ describe('negative assertion', () => {
+ it.each`
+ htmlString | templateString
+ ${'foo'} | ${'bar'}
+ ${'foo'} | ${'bar%{foo}'}
+ ${'foo'} | ${'@{lol}foo%{foo}'}
+ ${' fo o '} | ${'foo'}
+ `('$htmlString does not equal $templateString', ({ htmlString, templateString }) => {
+ expect(htmlString).not.toMatchInterpolatedText(templateString);
+ });
+ });
+});
diff --git a/spec/frontend/__helpers__/matchers_spec.js b/spec/frontend/__helpers__/matchers_spec.js
deleted file mode 100644
index dfd6f754c72..00000000000
--- a/spec/frontend/__helpers__/matchers_spec.js
+++ /dev/null
@@ -1,48 +0,0 @@
-describe('Custom jest matchers', () => {
- describe('toMatchInterpolatedText', () => {
- describe('malformed input', () => {
- it.each([null, 1, Symbol, Array, Object])(
- 'fails graciously if the expected value is %s',
- (expected) => {
- expect(expected).not.toMatchInterpolatedText('null');
- },
- );
- });
- describe('malformed matcher', () => {
- it.each([null, 1, Symbol, Array, Object])(
- 'fails graciously if the matcher is %s',
- (matcher) => {
- expect('null').not.toMatchInterpolatedText(matcher);
- },
- );
- });
-
- describe('positive assertion', () => {
- it.each`
- htmlString | templateString
- ${'foo'} | ${'foo'}
- ${'foo'} | ${'foo%{foo}'}
- ${'foo '} | ${'foo'}
- ${'foo '} | ${'foo%{foo}'}
- ${'foo . '} | ${'foo%{foo}.'}
- ${'foo bar . '} | ${'foo%{foo} bar.'}
- ${'foo\n\nbar . '} | ${'foo%{foo} bar.'}
- ${'foo bar . .'} | ${'foo%{fooStart} bar.%{fooEnd}.'}
- `('$htmlString equals $templateString', ({ htmlString, templateString }) => {
- expect(htmlString).toMatchInterpolatedText(templateString);
- });
- });
-
- describe('negative assertion', () => {
- it.each`
- htmlString | templateString
- ${'foo'} | ${'bar'}
- ${'foo'} | ${'bar%{foo}'}
- ${'foo'} | ${'@{lol}foo%{foo}'}
- ${' fo o '} | ${'foo'}
- `('$htmlString does not equal $templateString', ({ htmlString, templateString }) => {
- expect(htmlString).not.toMatchInterpolatedText(templateString);
- });
- });
- });
-});
diff --git a/spec/frontend/__helpers__/shared_test_setup.js b/spec/frontend/__helpers__/shared_test_setup.js
index 03389e16b65..7b5df18ee0f 100644
--- a/spec/frontend/__helpers__/shared_test_setup.js
+++ b/spec/frontend/__helpers__/shared_test_setup.js
@@ -8,7 +8,7 @@ import setWindowLocation from './set_window_location_helper';
import { setGlobalDateToFakeDate } from './fake_date';
import { loadHTMLFixture, setHTMLFixture } from './fixtures';
import { TEST_HOST } from './test_constants';
-import customMatchers from './matchers';
+import * as customMatchers from './matchers';
import './dom_shims';
import './jquery';
diff --git a/spec/frontend/__helpers__/wait_using_real_timer.js b/spec/frontend/__helpers__/wait_using_real_timer.js
deleted file mode 100644
index 110d5f46c08..00000000000
--- a/spec/frontend/__helpers__/wait_using_real_timer.js
+++ /dev/null
@@ -1,7 +0,0 @@
-/* useful for timing promises when jest fakeTimers are not reliable enough */
-export default (timeout) =>
- new Promise((resolve) => {
- jest.useRealTimers();
- setTimeout(resolve, timeout);
- jest.useFakeTimers();
- });
diff --git a/spec/frontend/alerts_settings/components/alerts_settings_form_spec.js b/spec/frontend/alerts_settings/components/alerts_settings_form_spec.js
index bdc1dde7d48..018303fcae7 100644
--- a/spec/frontend/alerts_settings/components/alerts_settings_form_spec.js
+++ b/spec/frontend/alerts_settings/components/alerts_settings_form_spec.js
@@ -319,6 +319,8 @@ describe('AlertsSettingsForm', () => {
const validPayloadMsg = payload === emptySamplePayload ? 'not valid' : 'valid';
it(`textarea should be ${enabledState} when payload reset ${payloadResetMsg} and payload is ${validPayloadMsg}`, async () => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
currentIntegration: { payloadExample: payload },
resetPayloadAndMappingConfirmed,
@@ -345,6 +347,8 @@ describe('AlertsSettingsForm', () => {
: 'was not confirmed';
it(`shows ${caption} button when sample payload ${samplePayloadMsg} and payload reset ${payloadResetMsg}`, async () => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
currentIntegration: {
payloadExample,
@@ -359,6 +363,8 @@ describe('AlertsSettingsForm', () => {
describe('Parsing payload', () => {
beforeEach(() => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
resetPayloadAndMappingConfirmed: true,
});
@@ -456,6 +462,8 @@ describe('AlertsSettingsForm', () => {
});
it('should be able to submit when form is dirty', async () => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
currentIntegration: { type: typeSet.http, name: 'Existing integration' },
});
@@ -466,6 +474,8 @@ describe('AlertsSettingsForm', () => {
});
it('should not be able to submit when form is pristine', async () => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
currentIntegration: { type: typeSet.http, name: 'Existing integration' },
});
diff --git a/spec/frontend/analytics/shared/components/projects_dropdown_filter_spec.js b/spec/frontend/analytics/shared/components/projects_dropdown_filter_spec.js
index 5d681c7da4f..28d7ebe28df 100644
--- a/spec/frontend/analytics/shared/components/projects_dropdown_filter_spec.js
+++ b/spec/frontend/analytics/shared/components/projects_dropdown_filter_spec.js
@@ -126,6 +126,8 @@ describe('ProjectsDropdownFilter component', () => {
});
it('applies the correct queryParams when making an api call', async () => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({ searchTerm: 'gitlab' });
expect(spyQuery).toHaveBeenCalledTimes(1);
@@ -204,6 +206,8 @@ describe('ProjectsDropdownFilter component', () => {
await createWithMockDropdown({ multiSelect: true });
selectDropdownItemAtIndex(0);
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({ searchTerm: 'this is a very long search string' });
});
diff --git a/spec/frontend/api/packages_api_spec.js b/spec/frontend/api/packages_api_spec.js
index 3286dccb1b2..d55d2036dcf 100644
--- a/spec/frontend/api/packages_api_spec.js
+++ b/spec/frontend/api/packages_api_spec.js
@@ -38,12 +38,17 @@ describe('Api', () => {
mock.onPut(expectedUrl).replyOnce(httpStatus.OK, apiResponse);
return publishPackage(
- { projectPath, name, version: 0, fileName: name, files: [{}] },
+ {
+ projectPath,
+ name,
+ version: 0,
+ fileName: name,
+ files: [new File(['zip contents'], 'bar.zip')],
+ },
{ status: 'hidden', select: 'package_file' },
).then(({ data }) => {
expect(data).toEqual(apiResponse);
- expect(axios.put).toHaveBeenCalledWith(expectedUrl, expect.any(FormData), {
- headers: { 'Content-Type': 'multipart/form-data' },
+ expect(axios.put).toHaveBeenCalledWith(expectedUrl, expect.any(File), {
params: { select: 'package_file', status: 'hidden' },
});
});
diff --git a/spec/frontend/behaviors/copy_to_clipboard_spec.js b/spec/frontend/behaviors/copy_to_clipboard_spec.js
new file mode 100644
index 00000000000..c5beaa0ba5d
--- /dev/null
+++ b/spec/frontend/behaviors/copy_to_clipboard_spec.js
@@ -0,0 +1,187 @@
+import initCopyToClipboard, {
+ CLIPBOARD_SUCCESS_EVENT,
+ CLIPBOARD_ERROR_EVENT,
+ I18N_ERROR_MESSAGE,
+} from '~/behaviors/copy_to_clipboard';
+import { show, hide, fixTitle, once } from '~/tooltips';
+
+let onceCallback = () => {};
+jest.mock('~/tooltips', () => ({
+ show: jest.fn(),
+ hide: jest.fn(),
+ fixTitle: jest.fn(),
+ once: jest.fn((event, callback) => {
+ onceCallback = callback;
+ }),
+}));
+
+describe('initCopyToClipboard', () => {
+ let clearSelection;
+ let focusSpy;
+ let dispatchEventSpy;
+ let button;
+ let clipboardInstance;
+
+ afterEach(() => {
+ document.body.innerHTML = '';
+ clipboardInstance = null;
+ });
+
+ const title = 'Copy this value';
+ const defaultButtonAttributes = {
+ 'data-clipboard-text': 'foo bar',
+ title,
+ 'data-title': title,
+ };
+ const createButton = (attributes = {}) => {
+ const combinedAttributes = { ...defaultButtonAttributes, ...attributes };
+ button = document.createElement('button');
+ Object.keys(combinedAttributes).forEach((attributeName) => {
+ button.setAttribute(attributeName, combinedAttributes[attributeName]);
+ });
+ document.body.appendChild(button);
+ };
+
+ const init = () => {
+ clipboardInstance = initCopyToClipboard();
+ };
+
+ const setupSpies = () => {
+ clearSelection = jest.fn();
+ focusSpy = jest.spyOn(button, 'focus');
+ dispatchEventSpy = jest.spyOn(button, 'dispatchEvent');
+ };
+
+ const emitSuccessEvent = () => {
+ clipboardInstance.emit('success', {
+ action: 'copy',
+ text: 'foo bar',
+ trigger: button,
+ clearSelection,
+ });
+ };
+
+ const emitErrorEvent = () => {
+ clipboardInstance.emit('error', {
+ action: 'copy',
+ text: 'foo bar',
+ trigger: button,
+ clearSelection,
+ });
+ };
+
+ const itHandlesTooltip = (expectedTooltip) => {
+ it('handles tooltip', () => {
+ expect(button.getAttribute('title')).toBe(expectedTooltip);
+ expect(button.getAttribute('aria-label')).toBe(expectedTooltip);
+ expect(fixTitle).toHaveBeenCalledWith(button);
+ expect(show).toHaveBeenCalledWith(button);
+ expect(once).toHaveBeenCalledWith('hidden', expect.any(Function));
+
+ expect(hide).not.toHaveBeenCalled();
+ jest.runAllTimers();
+ expect(hide).toHaveBeenCalled();
+
+ onceCallback({ target: button });
+ expect(button.getAttribute('title')).toBe(title);
+ expect(button.getAttribute('aria-label')).toBe(title);
+ expect(fixTitle).toHaveBeenCalledWith(button);
+ });
+ };
+
+ describe('when value is successfully copied', () => {
+ it(`calls clearSelection, focuses the button, and dispatches ${CLIPBOARD_SUCCESS_EVENT} event`, () => {
+ createButton();
+ init();
+ setupSpies();
+ emitSuccessEvent();
+
+ expect(clearSelection).toHaveBeenCalled();
+ expect(focusSpy).toHaveBeenCalled();
+ expect(dispatchEventSpy).toHaveBeenCalledWith(new Event(CLIPBOARD_SUCCESS_EVENT));
+ });
+
+ describe('when `data-clipboard-handle-tooltip` is set to `false`', () => {
+ beforeEach(() => {
+ createButton({
+ 'data-clipboard-handle-tooltip': 'false',
+ });
+ init();
+ emitSuccessEvent();
+ });
+
+ it('does not handle success tooltip', () => {
+ expect(show).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('when `data-clipboard-handle-tooltip` is set to `true`', () => {
+ beforeEach(() => {
+ createButton({
+ 'data-clipboard-handle-tooltip': 'true',
+ });
+ init();
+ emitSuccessEvent();
+ });
+
+ itHandlesTooltip('Copied');
+ });
+
+ describe('when `data-clipboard-handle-tooltip` is not set', () => {
+ beforeEach(() => {
+ createButton();
+ init();
+ emitSuccessEvent();
+ });
+
+ itHandlesTooltip('Copied');
+ });
+ });
+
+ describe('when there is an error copying the value', () => {
+ it(`dispatches ${CLIPBOARD_ERROR_EVENT} event`, () => {
+ createButton();
+ init();
+ setupSpies();
+ emitErrorEvent();
+
+ expect(dispatchEventSpy).toHaveBeenCalledWith(new Event(CLIPBOARD_ERROR_EVENT));
+ });
+
+ describe('when `data-clipboard-handle-tooltip` is set to `false`', () => {
+ beforeEach(() => {
+ createButton({
+ 'data-clipboard-handle-tooltip': 'false',
+ });
+ init();
+ emitErrorEvent();
+ });
+
+ it('does not handle error tooltip', () => {
+ expect(show).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('when `data-clipboard-handle-tooltip` is set to `true`', () => {
+ beforeEach(() => {
+ createButton({
+ 'data-clipboard-handle-tooltip': 'true',
+ });
+ init();
+ emitErrorEvent();
+ });
+
+ itHandlesTooltip(I18N_ERROR_MESSAGE);
+ });
+
+ describe('when `data-clipboard-handle-tooltip` is not set', () => {
+ beforeEach(() => {
+ createButton();
+ init();
+ emitErrorEvent();
+ });
+
+ itHandlesTooltip(I18N_ERROR_MESSAGE);
+ });
+ });
+});
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 46a5631b028..d698ee72ea4 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
@@ -20,12 +20,6 @@ exports[`Blob Header Filepath rendering matches the snapshot 1`] = `
foo/bar/dummy.md
</strong>
- <small
- class="mr-2"
- >
- a lot
- </small>
-
<clipboard-button-stub
category="tertiary"
cssclass="btn-clipboard btn-transparent lh-100 position-static"
@@ -36,5 +30,13 @@ exports[`Blob Header Filepath rendering matches the snapshot 1`] = `
tooltipplacement="top"
variant="default"
/>
+
+ <small
+ class="mr-2"
+ >
+ a lot
+ </small>
+
+ <!---->
</div>
`;
diff --git a/spec/frontend/blob/components/__snapshots__/blob_header_spec.js.snap b/spec/frontend/blob/components/__snapshots__/blob_header_spec.js.snap
index db9684239a1..22bec77276b 100644
--- a/spec/frontend/blob/components/__snapshots__/blob_header_spec.js.snap
+++ b/spec/frontend/blob/components/__snapshots__/blob_header_spec.js.snap
@@ -17,7 +17,7 @@ exports[`Blob Header Default Actions rendering matches the snapshot 1`] = `
</div>
<div
- class="gl-display-none gl-sm-display-flex"
+ class="gl-sm-display-flex file-actions"
>
<viewer-switcher-stub
value="simple"
diff --git a/spec/frontend/blob/components/blob_edit_header_spec.js b/spec/frontend/blob/components/blob_edit_header_spec.js
index ac3080c65a5..910fc5c946d 100644
--- a/spec/frontend/blob/components/blob_edit_header_spec.js
+++ b/spec/frontend/blob/components/blob_edit_header_spec.js
@@ -44,6 +44,8 @@ describe('Blob Header Editing', () => {
const inputComponent = wrapper.find(GlFormInput);
const newValue = 'bar.txt';
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
name: newValue,
});
diff --git a/spec/frontend/blob/components/blob_header_filepath_spec.js b/spec/frontend/blob/components/blob_header_filepath_spec.js
index d935f73c0d1..8220b598ff6 100644
--- a/spec/frontend/blob/components/blob_header_filepath_spec.js
+++ b/spec/frontend/blob/components/blob_header_filepath_spec.js
@@ -1,3 +1,4 @@
+import { GlBadge } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import BlobHeaderFilepath from '~/blob/components/blob_header_filepath.vue';
import { numberToHumanSize } from '~/lib/utils/number_utils';
@@ -24,6 +25,8 @@ describe('Blob Header Filepath', () => {
wrapper.destroy();
});
+ const findBadge = () => wrapper.find(GlBadge);
+
describe('rendering', () => {
it('matches the snapshot', () => {
createComponent();
@@ -54,6 +57,11 @@ describe('Blob Header Filepath', () => {
expect(wrapper.vm.blobSize).toBe('a lot');
});
+ it('renders LFS badge if LFS if enabled', () => {
+ createComponent({ storedExternally: true, externalStorage: 'lfs' });
+ expect(findBadge().text()).toBe('LFS');
+ });
+
it('renders a slot and prepends its contents to the existing one', () => {
const slotContent = 'Foo Bar';
createComponent(
diff --git a/spec/frontend/line_highlighter_spec.js b/spec/frontend/blob/line_highlighter_spec.js
index 97ae6c0e3b7..330f1f3137e 100644
--- a/spec/frontend/line_highlighter_spec.js
+++ b/spec/frontend/blob/line_highlighter_spec.js
@@ -1,8 +1,8 @@
/* eslint-disable no-return-assign, no-new, no-underscore-dangle */
import $ from 'jquery';
+import LineHighlighter from '~/blob/line_highlighter';
import * as utils from '~/lib/utils/common_utils';
-import LineHighlighter from '~/line_highlighter';
describe('LineHighlighter', () => {
const testContext = {};
diff --git a/spec/frontend/blob/viewer/index_spec.js b/spec/frontend/blob/viewer/index_spec.js
index 061ac7ad167..9e9f866d40c 100644
--- a/spec/frontend/blob/viewer/index_spec.js
+++ b/spec/frontend/blob/viewer/index_spec.js
@@ -21,6 +21,7 @@ describe('Blob viewer', () => {
setTestTimeout(2000);
beforeEach(() => {
+ window.gon.features = { refactorBlobViewer: false }; // This file is based on the old (non-refactored) blob viewer
jest.spyOn(window, 'requestIdleCallback').mockImplementation(execImmediately);
$.fn.extend(jQueryMock);
mock = new MockAdapter(axios);
diff --git a/spec/frontend/boards/components/board_card_spec.js b/spec/frontend/boards/components/board_card_spec.js
index 5742dfdc5d2..3af173aa18c 100644
--- a/spec/frontend/boards/components/board_card_spec.js
+++ b/spec/frontend/boards/components/board_card_spec.js
@@ -167,7 +167,7 @@ describe('Board card', () => {
mountComponent({ item: { ...mockIssue, isLoading: true } });
expect(wrapper.classes()).toContain('is-disabled');
- expect(wrapper.classes()).not.toContain('user-can-drag');
+ expect(wrapper.classes()).not.toContain('gl-cursor-grab');
});
});
@@ -177,7 +177,7 @@ describe('Board card', () => {
mountComponent();
expect(wrapper.classes()).not.toContain('is-disabled');
- expect(wrapper.classes()).toContain('user-can-drag');
+ expect(wrapper.classes()).toContain('gl-cursor-grab');
});
});
});
diff --git a/spec/frontend/boards/components/board_content_sidebar_spec.js b/spec/frontend/boards/components/board_content_sidebar_spec.js
index 7b176cea2a3..368c7d561f8 100644
--- a/spec/frontend/boards/components/board_content_sidebar_spec.js
+++ b/spec/frontend/boards/components/board_content_sidebar_spec.js
@@ -9,6 +9,7 @@ import BoardContentSidebar from '~/boards/components/board_content_sidebar.vue';
import BoardSidebarTitle from '~/boards/components/sidebar/board_sidebar_title.vue';
import { ISSUABLE } from '~/boards/constants';
import SidebarDateWidget from '~/sidebar/components/date/sidebar_date_widget.vue';
+import SidebarSeverity from '~/sidebar/components/severity/sidebar_severity.vue';
import SidebarSubscriptionsWidget from '~/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue';
import SidebarTodoWidget from '~/sidebar/components/todo_toggle/sidebar_todo_widget.vue';
import SidebarLabelsWidget from '~/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue';
@@ -96,7 +97,7 @@ describe('BoardContentSidebar', () => {
});
it('confirms we render MountingPortal', () => {
- expect(wrapper.find(MountingPortal).props()).toMatchObject({
+ expect(wrapper.findComponent(MountingPortal).props()).toMatchObject({
mountTo: '#js-right-sidebar-portal',
append: true,
name: 'board-content-sidebar',
@@ -141,6 +142,10 @@ describe('BoardContentSidebar', () => {
);
});
+ it('does not render SidebarSeverity', () => {
+ expect(wrapper.findComponent(SidebarSeverity).exists()).toBe(false);
+ });
+
describe('when we emit close', () => {
let toggleBoardItem;
@@ -160,4 +165,17 @@ describe('BoardContentSidebar', () => {
});
});
});
+
+ describe('incident sidebar', () => {
+ beforeEach(() => {
+ createStore({
+ mockGetters: { activeBoardItem: () => ({ ...mockIssue, epic: null, type: 'INCIDENT' }) },
+ });
+ createComponent();
+ });
+
+ it('renders SidebarSeverity', () => {
+ expect(wrapper.findComponent(SidebarSeverity).exists()).toBe(true);
+ });
+ });
});
diff --git a/spec/frontend/boards/components/board_filtered_search_spec.js b/spec/frontend/boards/components/board_filtered_search_spec.js
index ea551e94f2f..a8398a138ba 100644
--- a/spec/frontend/boards/components/board_filtered_search_spec.js
+++ b/spec/frontend/boards/components/board_filtered_search_spec.js
@@ -118,6 +118,7 @@ describe('BoardFilteredSearch', () => {
it('sets the url params to the correct results', async () => {
const mockFilters = [
{ type: 'author', value: { data: 'root', operator: '=' } },
+ { type: 'assignee', value: { data: 'root', operator: '=' } },
{ type: 'label', value: { data: 'label', operator: '=' } },
{ type: 'label', value: { data: 'label2', operator: '=' } },
{ type: 'milestone', value: { data: 'New Milestone', operator: '=' } },
@@ -133,7 +134,26 @@ describe('BoardFilteredSearch', () => {
title: '',
replace: true,
url:
- 'http://test.host/?author_username=root&label_name[]=label&label_name[]=label2&milestone_title=New+Milestone&iteration_id=3341&types=INCIDENT&weight=2&release_tag=v1.0.0',
+ 'http://test.host/?author_username=root&label_name[]=label&label_name[]=label2&assignee_username=root&milestone_title=New+Milestone&iteration_id=3341&types=INCIDENT&weight=2&release_tag=v1.0.0',
+ });
+ });
+
+ describe('when assignee is passed a wildcard value', () => {
+ const url = (arg) => `http://test.host/?assignee_id=${arg}`;
+
+ it.each([
+ ['None', url('None')],
+ ['Any', url('Any')],
+ ])('sets the url param %s', (assigneeParam, expected) => {
+ const mockFilters = [{ type: 'assignee', value: { data: assigneeParam, operator: '=' } }];
+ jest.spyOn(urlUtility, 'updateHistory');
+ findFilteredSearch().vm.$emit('onFilter', mockFilters);
+
+ expect(urlUtility.updateHistory).toHaveBeenCalledWith({
+ title: '',
+ replace: true,
+ url: expected,
+ });
});
});
});
diff --git a/spec/frontend/boards/components/board_list_header_spec.js b/spec/frontend/boards/components/board_list_header_spec.js
index 148d0c5684d..8cc0ad5f30c 100644
--- a/spec/frontend/boards/components/board_list_header_spec.js
+++ b/spec/frontend/boards/components/board_list_header_spec.js
@@ -180,18 +180,18 @@ describe('Board List Header Component', () => {
const canDragList = [ListType.label, ListType.milestone, ListType.iteration, ListType.assignee];
it.each(cannotDragList)(
- 'does not have user-can-drag-class so user cannot drag list',
+ 'does not have gl-cursor-grab class so user cannot drag list',
(listType) => {
createComponent({ listType });
- expect(findTitle().classes()).not.toContain('user-can-drag');
+ expect(findTitle().classes()).not.toContain('gl-cursor-grab');
},
);
- it.each(canDragList)('has user-can-drag-class so user can drag list', (listType) => {
+ it.each(canDragList)('has gl-cursor-grab class so user can drag list', (listType) => {
createComponent({ listType });
- expect(findTitle().classes()).toContain('user-can-drag');
+ expect(findTitle().classes()).toContain('gl-cursor-grab');
});
});
});
diff --git a/spec/frontend/boards/components/boards_selector_spec.js b/spec/frontend/boards/components/boards_selector_spec.js
index c841c17a029..9cf7c5774bf 100644
--- a/spec/frontend/boards/components/boards_selector_spec.js
+++ b/spec/frontend/boards/components/boards_selector_spec.js
@@ -96,6 +96,8 @@ describe('BoardsSelector', () => {
});
wrapper.vm.$apollo.addSmartQuery = jest.fn((_, options) => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
[options.loadingKey]: true,
});
@@ -161,6 +163,8 @@ describe('BoardsSelector', () => {
// Emits gl-dropdown show event to simulate the dropdown is opened at initialization time
findDropdown().vm.$emit('show');
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
await wrapper.setData({
loadingBoards: false,
loadingRecentBoards: false,
@@ -176,6 +180,8 @@ describe('BoardsSelector', () => {
describe('filtering', () => {
beforeEach(async () => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
boards,
});
@@ -208,6 +214,8 @@ describe('BoardsSelector', () => {
describe('recent boards section', () => {
it('shows only when boards are greater than 10', async () => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
boards,
});
@@ -217,6 +225,8 @@ describe('BoardsSelector', () => {
});
it('does not show when boards are less than 10', async () => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
boards: boards.slice(0, 5),
});
@@ -226,6 +236,8 @@ describe('BoardsSelector', () => {
});
it('does not show when recentBoards api returns empty array', async () => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
recentBoards: [],
});
diff --git a/spec/frontend/boards/stores/actions_spec.js b/spec/frontend/boards/stores/actions_spec.js
index 51340a3ea4f..7c842d71688 100644
--- a/spec/frontend/boards/stores/actions_spec.js
+++ b/spec/frontend/boards/stores/actions_spec.js
@@ -29,6 +29,8 @@ import * as types from '~/boards/stores/mutation_types';
import mutations from '~/boards/stores/mutations';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import projectBoardMilestones from '~/boards/graphql/project_board_milestones.query.graphql';
+import groupBoardMilestones from '~/boards/graphql/group_board_milestones.query.graphql';
import {
mockLists,
mockListsById,
@@ -308,6 +310,36 @@ describe('fetchMilestones', () => {
expect(() => actions.fetchMilestones(store)).toThrow(new Error('Unknown board type'));
});
+ it.each([
+ [
+ 'project',
+ {
+ query: projectBoardMilestones,
+ variables: { fullPath: 'gitlab-org/gitlab', state: 'active' },
+ },
+ ],
+ [
+ 'group',
+ {
+ query: groupBoardMilestones,
+ variables: { fullPath: 'gitlab-org/gitlab', state: 'active' },
+ },
+ ],
+ ])(
+ 'when boardType is %s it calls fetchMilestones with the correct query and variables',
+ (boardType, variables) => {
+ jest.spyOn(gqlClient, 'query').mockResolvedValue(queryResponse);
+
+ const store = createStore();
+
+ store.state.boardType = boardType;
+
+ actions.fetchMilestones(store);
+
+ expect(gqlClient.query).toHaveBeenCalledWith(variables);
+ },
+ );
+
it('sets milestonesLoading to true', async () => {
jest.spyOn(gqlClient, 'query').mockResolvedValue(queryResponse);
diff --git a/spec/frontend/branches/branches_delete_modal_spec.js b/spec/frontend/branches/branches_delete_modal_spec.js
deleted file mode 100644
index 8b10cca7a11..00000000000
--- a/spec/frontend/branches/branches_delete_modal_spec.js
+++ /dev/null
@@ -1,40 +0,0 @@
-import $ from 'jquery';
-import DeleteModal from '~/branches/branches_delete_modal';
-
-describe('branches delete modal', () => {
- describe('setDisableDeleteButton', () => {
- let submitSpy;
- let $deleteButton;
-
- beforeEach(() => {
- setFixtures(`
- <div id="modal-delete-branch">
- <form>
- <button type="submit" class="js-delete-branch">Delete</button>
- </form>
- </div>
- `);
- $deleteButton = $('.js-delete-branch');
- submitSpy = jest.fn((event) => event.preventDefault());
- $('#modal-delete-branch form').on('submit', submitSpy);
- // eslint-disable-next-line no-new
- new DeleteModal();
- });
-
- it('does not submit if button is disabled', () => {
- $deleteButton.attr('disabled', true);
-
- $deleteButton.click();
-
- expect(submitSpy).not.toHaveBeenCalled();
- });
-
- it('submits if button is not disabled', () => {
- $deleteButton.attr('disabled', false);
-
- $deleteButton.click();
-
- expect(submitSpy).toHaveBeenCalled();
- });
- });
-});
diff --git a/spec/frontend/ci_lint/components/ci_lint_spec.js b/spec/frontend/ci_lint/components/ci_lint_spec.js
index 70d116c12d3..c4b2927764e 100644
--- a/spec/frontend/ci_lint/components/ci_lint_spec.js
+++ b/spec/frontend/ci_lint/components/ci_lint_spec.js
@@ -66,6 +66,8 @@ describe('CI Lint', () => {
it('validate action calls mutation with dry run', async () => {
const dryRunEnabled = true;
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
await wrapper.setData({ dryRun: dryRunEnabled });
findValidateBtn().vm.$emit('click');
diff --git a/spec/frontend/clusters/agents/components/show_spec.js b/spec/frontend/clusters/agents/components/show_spec.js
index d5a8117f48c..2a3c11f4b47 100644
--- a/spec/frontend/clusters/agents/components/show_spec.js
+++ b/spec/frontend/clusters/agents/components/show_spec.js
@@ -19,7 +19,7 @@ describe('ClusterAgentShow', () => {
let wrapper;
useFakeDate([2021, 2, 15]);
- const propsData = {
+ const provide = {
agentName: 'cluster-agent',
projectPath: 'path/to/project',
};
@@ -49,7 +49,7 @@ describe('ClusterAgentShow', () => {
shallowMount(ClusterAgentShow, {
localVue,
apolloProvider,
- propsData,
+ provide,
stubs: { GlSprintf, TimeAgoTooltip, GlTab },
}),
);
@@ -60,7 +60,7 @@ describe('ClusterAgentShow', () => {
wrapper = extendedWrapper(
shallowMount(ClusterAgentShow, {
- propsData,
+ provide,
mocks: { $apollo, clusterAgent },
slots,
stubs: { GlTab },
@@ -85,7 +85,7 @@ describe('ClusterAgentShow', () => {
});
it('displays the agent name', () => {
- expect(wrapper.text()).toContain(propsData.agentName);
+ expect(wrapper.text()).toContain(provide.agentName);
});
it('displays agent create information', () => {
diff --git a/spec/frontend/clusters/forms/components/integration_form_spec.js b/spec/frontend/clusters/forms/components/integration_form_spec.js
index b129baa2d83..d041cd1e164 100644
--- a/spec/frontend/clusters/forms/components/integration_form_spec.js
+++ b/spec/frontend/clusters/forms/components/integration_form_spec.js
@@ -82,6 +82,8 @@ describe('ClusterIntegrationForm', () => {
.then(() => {
// setData is a bad approach because it changes the internal implementation which we should not touch
// but our GlFormInput lacks the ability to set a new value.
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({ toggleEnabled: !defaultStoreValues.enabled });
})
.then(() => {
@@ -93,6 +95,8 @@ describe('ClusterIntegrationForm', () => {
return wrapper.vm
.$nextTick()
.then(() => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({ envScope: `${defaultStoreValues.environmentScope}1` });
})
.then(() => {
diff --git a/spec/frontend/clusters_list/components/agent_options_spec.js b/spec/frontend/clusters_list/components/agent_options_spec.js
new file mode 100644
index 00000000000..05bab247816
--- /dev/null
+++ b/spec/frontend/clusters_list/components/agent_options_spec.js
@@ -0,0 +1,211 @@
+import { GlDropdown, GlDropdownItem, GlModal, GlFormInput } from '@gitlab/ui';
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { ENTER_KEY } from '~/lib/utils/keys';
+import getAgentsQuery from '~/clusters_list/graphql/queries/get_agents.query.graphql';
+import deleteAgentMutation from '~/clusters_list/graphql/mutations/delete_agent.mutation.graphql';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import AgentOptions from '~/clusters_list/components/agent_options.vue';
+import { MAX_LIST_COUNT } from '~/clusters_list/constants';
+import { getAgentResponse, mockDeleteResponse, mockErrorDeleteResponse } from '../mocks/apollo';
+
+Vue.use(VueApollo);
+
+const projectPath = 'path/to/project';
+const defaultBranchName = 'default';
+const maxAgents = MAX_LIST_COUNT;
+const agent = {
+ id: 'agent-id',
+ name: 'agent-name',
+ webPath: 'agent-webPath',
+};
+
+describe('AgentOptions', () => {
+ let wrapper;
+ let toast;
+ let apolloProvider;
+ let deleteResponse;
+
+ const findModal = () => wrapper.findComponent(GlModal);
+ const findDropdown = () => wrapper.findComponent(GlDropdown);
+ const findDeleteBtn = () => wrapper.findComponent(GlDropdownItem);
+ const findInput = () => wrapper.findComponent(GlFormInput);
+ const findPrimaryAction = () => findModal().props('actionPrimary');
+ const findPrimaryActionAttributes = (attr) => findPrimaryAction().attributes[0][attr];
+
+ const createMockApolloProvider = ({ mutationResponse }) => {
+ deleteResponse = jest.fn().mockResolvedValue(mutationResponse);
+
+ return createMockApollo([[deleteAgentMutation, deleteResponse]]);
+ };
+
+ const writeQuery = () => {
+ apolloProvider.clients.defaultClient.cache.writeQuery({
+ query: getAgentsQuery,
+ variables: {
+ projectPath,
+ defaultBranchName,
+ first: maxAgents,
+ last: null,
+ },
+ data: getAgentResponse.data,
+ });
+ };
+
+ const createWrapper = ({ mutationResponse = mockDeleteResponse } = {}) => {
+ apolloProvider = createMockApolloProvider({ mutationResponse });
+ const provide = {
+ projectPath,
+ };
+ const propsData = {
+ defaultBranchName,
+ maxAgents,
+ agent,
+ };
+
+ toast = jest.fn();
+
+ wrapper = shallowMountExtended(AgentOptions, {
+ apolloProvider,
+ provide,
+ propsData,
+ mocks: { $toast: { show: toast } },
+ stubs: { GlModal },
+ });
+ wrapper.vm.$refs.modal.hide = jest.fn();
+
+ writeQuery();
+ return wrapper.vm.$nextTick();
+ };
+
+ const submitAgentToDelete = async () => {
+ findDeleteBtn().vm.$emit('click');
+ findInput().vm.$emit('input', agent.name);
+ await findModal().vm.$emit('primary');
+ };
+
+ beforeEach(() => {
+ return createWrapper({});
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ apolloProvider = null;
+ deleteResponse = null;
+ toast = null;
+ });
+
+ describe('delete agent action', () => {
+ it('displays a delete button', () => {
+ expect(findDeleteBtn().text()).toBe('Delete agent');
+ });
+
+ describe('when clicking the delete button', () => {
+ beforeEach(() => {
+ findDeleteBtn().vm.$emit('click');
+ });
+
+ it('displays a delete confirmation modal', () => {
+ expect(findModal().isVisible()).toBe(true);
+ });
+ });
+
+ describe.each`
+ condition | agentName | isDisabled | mutationCalled
+ ${'the input with agent name is missing'} | ${''} | ${true} | ${false}
+ ${'the input with agent name is incorrect'} | ${'wrong-name'} | ${true} | ${false}
+ ${'the input with agent name is correct'} | ${agent.name} | ${false} | ${true}
+ `('when $condition', ({ agentName, isDisabled, mutationCalled }) => {
+ beforeEach(() => {
+ findDeleteBtn().vm.$emit('click');
+ findInput().vm.$emit('input', agentName);
+ });
+
+ it(`${isDisabled ? 'disables' : 'enables'} the modal primary button`, () => {
+ expect(findPrimaryActionAttributes('disabled')).toBe(isDisabled);
+ });
+
+ describe('when user clicks the modal primary button', () => {
+ beforeEach(async () => {
+ await findModal().vm.$emit('primary');
+ });
+
+ if (mutationCalled) {
+ it('calls the delete mutation', () => {
+ expect(deleteResponse).toHaveBeenCalledWith({ input: { id: agent.id } });
+ });
+ } else {
+ it("doesn't call the delete mutation", () => {
+ expect(deleteResponse).not.toHaveBeenCalled();
+ });
+ }
+ });
+
+ describe('when user presses the enter button', () => {
+ beforeEach(async () => {
+ await findInput().vm.$emit('keydown', new KeyboardEvent({ key: ENTER_KEY }));
+ });
+
+ if (mutationCalled) {
+ it('calls the delete mutation', () => {
+ expect(deleteResponse).toHaveBeenCalledWith({ input: { id: agent.id } });
+ });
+ } else {
+ it("doesn't call the delete mutation", () => {
+ expect(deleteResponse).not.toHaveBeenCalled();
+ });
+ }
+ });
+ });
+
+ describe('when agent was deleted successfully', () => {
+ beforeEach(async () => {
+ await submitAgentToDelete();
+ });
+
+ it('calls the toast action', () => {
+ expect(toast).toHaveBeenCalledWith(`${agent.name} successfully deleted`);
+ });
+ });
+ });
+
+ describe('when getting an error deleting agent', () => {
+ beforeEach(async () => {
+ await createWrapper({ mutationResponse: mockErrorDeleteResponse });
+
+ submitAgentToDelete();
+ });
+
+ it('displays the error message', () => {
+ expect(toast).toHaveBeenCalledWith('could not delete agent');
+ });
+ });
+
+ describe('when the delete modal was closed', () => {
+ beforeEach(async () => {
+ const loadingResponse = new Promise(() => {});
+ await createWrapper({ mutationResponse: loadingResponse });
+
+ submitAgentToDelete();
+ });
+
+ it('reenables the options dropdown', async () => {
+ expect(findPrimaryActionAttributes('loading')).toBe(true);
+ expect(findDropdown().attributes('disabled')).toBe('true');
+
+ await findModal().vm.$emit('hide');
+
+ expect(findPrimaryActionAttributes('loading')).toBe(false);
+ expect(findDropdown().attributes('disabled')).toBeUndefined();
+ });
+
+ it('clears the agent name input', async () => {
+ expect(findInput().attributes('value')).toBe(agent.name);
+
+ await findModal().vm.$emit('hide');
+
+ expect(findInput().attributes('value')).toBeUndefined();
+ });
+ });
+});
diff --git a/spec/frontend/clusters_list/components/agent_table_spec.js b/spec/frontend/clusters_list/components/agent_table_spec.js
index a6d76b069cf..887c17bb4ad 100644
--- a/spec/frontend/clusters_list/components/agent_table_spec.js
+++ b/spec/frontend/clusters_list/components/agent_table_spec.js
@@ -1,16 +1,22 @@
import { GlLink, GlIcon } from '@gitlab/ui';
import AgentTable from '~/clusters_list/components/agent_table.vue';
+import AgentOptions from '~/clusters_list/components/agent_options.vue';
import { ACTIVE_CONNECTION_TIME } from '~/clusters_list/constants';
import { mountExtended } from 'helpers/vue_test_utils_helper';
+import { stubComponent } from 'helpers/stub_component';
import timeagoMixin from '~/vue_shared/mixins/timeago';
const connectedTimeNow = new Date();
const connectedTimeInactive = new Date(connectedTimeNow.getTime() - ACTIVE_CONNECTION_TIME);
+const provideData = {
+ projectPath: 'path/to/project',
+};
const propsData = {
agents: [
{
name: 'agent-1',
+ id: 'agent-1-id',
configFolder: {
webPath: '/agent/full/path',
},
@@ -21,6 +27,7 @@ const propsData = {
},
{
name: 'agent-2',
+ id: 'agent-2-id',
webPath: '/agent-2',
status: 'active',
lastContact: connectedTimeNow.getTime(),
@@ -34,6 +41,7 @@ const propsData = {
},
{
name: 'agent-3',
+ id: 'agent-3-id',
webPath: '/agent-3',
status: 'inactive',
lastContact: connectedTimeInactive.getTime(),
@@ -48,6 +56,10 @@ const propsData = {
],
};
+const AgentOptionsStub = stubComponent(AgentOptions, {
+ template: `<div></div>`,
+});
+
describe('AgentTable', () => {
let wrapper;
@@ -57,15 +69,21 @@ describe('AgentTable', () => {
const findLastContactText = (at) => wrapper.findAllByTestId('cluster-agent-last-contact').at(at);
const findConfiguration = (at) =>
wrapper.findAllByTestId('cluster-agent-configuration-link').at(at);
+ const findAgentOptions = () => wrapper.findAllComponents(AgentOptions);
beforeEach(() => {
- wrapper = mountExtended(AgentTable, { propsData });
+ wrapper = mountExtended(AgentTable, {
+ propsData,
+ provide: provideData,
+ stubs: {
+ AgentOptions: AgentOptionsStub,
+ },
+ });
});
afterEach(() => {
if (wrapper) {
wrapper.destroy();
- wrapper = null;
}
});
@@ -108,5 +126,9 @@ describe('AgentTable', () => {
expect(findLink.exists()).toBe(hasLink);
expect(findConfiguration(lineNumber).text()).toBe(agentPath);
});
+
+ it('displays actions menu for each agent', () => {
+ expect(findAgentOptions()).toHaveLength(3);
+ });
});
});
diff --git a/spec/frontend/clusters_list/components/clusters_spec.js b/spec/frontend/clusters_list/components/clusters_spec.js
index a34202c789d..9af25a534d8 100644
--- a/spec/frontend/clusters_list/components/clusters_spec.js
+++ b/spec/frontend/clusters_list/components/clusters_spec.js
@@ -272,6 +272,8 @@ describe('Clusters', () => {
describe('when updating currentPage', () => {
beforeEach(() => {
mockPollingApi(200, apiData, paginationHeader(totalSecondPage, perPage, 2));
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({ currentPage: 2 });
return axios.waitForAll();
});
diff --git a/spec/frontend/clusters_list/mocks/apollo.js b/spec/frontend/clusters_list/mocks/apollo.js
index 804f9834506..c4a31ed4394 100644
--- a/spec/frontend/clusters_list/mocks/apollo.js
+++ b/spec/frontend/clusters_list/mocks/apollo.js
@@ -75,3 +75,15 @@ export const getAgentResponse = {
},
},
};
+
+export const mockDeleteResponse = {
+ data: { clusterAgentDelete: { errors: [] } },
+};
+
+export const mockErrorDeleteResponse = {
+ data: {
+ clusterAgentDelete: {
+ errors: ['could not delete agent'],
+ },
+ },
+};
diff --git a/spec/frontend/commit/pipelines/pipelines_table_spec.js b/spec/frontend/commit/pipelines/pipelines_table_spec.js
index c376b58cc72..e209f628aa2 100644
--- a/spec/frontend/commit/pipelines/pipelines_table_spec.js
+++ b/spec/frontend/commit/pipelines/pipelines_table_spec.js
@@ -92,6 +92,8 @@ describe('Pipelines table in Commits and Merge requests', () => {
it('should make an API request when using pagination', async () => {
jest.spyOn(wrapper.vm, 'updateContent').mockImplementation(() => {});
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
await wrapper.setData({
store: {
state: {
diff --git a/spec/frontend/content_editor/components/wrappers/frontmatter_spec.js b/spec/frontend/content_editor/components/wrappers/frontmatter_spec.js
index de8f8efd260..415f1314a36 100644
--- a/spec/frontend/content_editor/components/wrappers/frontmatter_spec.js
+++ b/spec/frontend/content_editor/components/wrappers/frontmatter_spec.js
@@ -26,6 +26,11 @@ describe('content/components/wrappers/frontmatter', () => {
expect(wrapper.findComponent(NodeViewWrapper).classes()).toContain('gl-relative');
});
+ it('adds content-editor-code-block class to the pre element', () => {
+ createWrapper();
+ expect(wrapper.findComponent(NodeViewWrapper).classes()).toContain('content-editor-code-block');
+ });
+
it('renders a node-view-content as a code element', () => {
createWrapper();
diff --git a/spec/frontend/content_editor/extensions/code_block_highlight_spec.js b/spec/frontend/content_editor/extensions/code_block_highlight_spec.js
index 6a0a0c76825..05fa0f79ef0 100644
--- a/spec/frontend/content_editor/extensions/code_block_highlight_spec.js
+++ b/spec/frontend/content_editor/extensions/code_block_highlight_spec.js
@@ -36,4 +36,10 @@ describe('content_editor/extensions/code_block_highlight', () => {
expect(editorHtmlOutput.classList.toString()).toContain('code highlight js-syntax-highlight');
});
+
+ it('adds content-editor-code-block class to the pre element', () => {
+ const editorHtmlOutput = parseHTML(tiptapEditor.getHTML()).querySelector('pre');
+
+ expect(editorHtmlOutput.classList.toString()).toContain('content-editor-code-block');
+ });
});
diff --git a/spec/frontend/content_editor/extensions/code_spec.js b/spec/frontend/content_editor/extensions/code_spec.js
new file mode 100644
index 00000000000..0a54ac6a96b
--- /dev/null
+++ b/spec/frontend/content_editor/extensions/code_spec.js
@@ -0,0 +1,8 @@
+import Code from '~/content_editor/extensions/code';
+import { EXTENSION_PRIORITY_LOWER } from '~/content_editor/constants';
+
+describe('content_editor/extensions/code', () => {
+ it('has a lower loading priority', () => {
+ expect(Code.config.priority).toBe(EXTENSION_PRIORITY_LOWER);
+ });
+});
diff --git a/spec/frontend/content_editor/extensions/frontmatter_spec.js b/spec/frontend/content_editor/extensions/frontmatter_spec.js
index 517f6947b9a..a8cbad6ef81 100644
--- a/spec/frontend/content_editor/extensions/frontmatter_spec.js
+++ b/spec/frontend/content_editor/extensions/frontmatter_spec.js
@@ -1,30 +1,47 @@
import Frontmatter from '~/content_editor/extensions/frontmatter';
+import CodeBlockHighlight from '~/content_editor/extensions/code_block_highlight';
import { createTestEditor, createDocBuilder, triggerNodeInputRule } from '../test_utils';
describe('content_editor/extensions/frontmatter', () => {
let tiptapEditor;
let doc;
- let p;
+ let frontmatter;
+ let codeBlock;
beforeEach(() => {
- tiptapEditor = createTestEditor({ extensions: [Frontmatter] });
+ tiptapEditor = createTestEditor({ extensions: [Frontmatter, CodeBlockHighlight] });
({
- builders: { doc, p },
+ builders: { doc, codeBlock, frontmatter },
} = createDocBuilder({
tiptapEditor,
names: {
frontmatter: { nodeType: Frontmatter.name },
+ codeBlock: { nodeType: CodeBlockHighlight.name },
},
}));
});
it('does not insert a frontmatter block when executing code block input rule', () => {
- const expectedDoc = doc(p(''));
+ const expectedDoc = doc(codeBlock(''));
const inputRuleText = '``` ';
triggerNodeInputRule({ tiptapEditor, inputRuleText });
expect(tiptapEditor.getJSON()).toEqual(expectedDoc.toJSON());
});
+
+ it.each`
+ command | result | resultDesc
+ ${'toggleCodeBlock'} | ${() => doc(codeBlock(''))} | ${'code block element'}
+ ${'setCodeBlock'} | ${() => doc(codeBlock(''))} | ${'code block element'}
+ ${'setFrontmatter'} | ${() => doc(frontmatter(''))} | ${'frontmatter element'}
+ ${'toggleFrontmatter'} | ${() => doc(frontmatter(''))} | ${'frontmatter element'}
+ `('executing $command should generate a document with a $resultDesc', ({ command, result }) => {
+ const expectedDoc = result();
+
+ tiptapEditor.commands[command]();
+
+ expect(tiptapEditor.getJSON()).toEqual(expectedDoc.toJSON());
+ });
});
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..256f7bad309
--- /dev/null
+++ b/spec/frontend/content_editor/extensions/image_spec.js
@@ -0,0 +1,41 @@
+import Image from '~/content_editor/extensions/image';
+import { createTestEditor, createDocBuilder } from '../test_utils';
+
+describe('content_editor/extensions/image', () => {
+ let tiptapEditor;
+ let doc;
+ let p;
+ let image;
+
+ beforeEach(() => {
+ tiptapEditor = createTestEditor({ extensions: [Image] });
+
+ ({
+ builders: { doc, p, image },
+ } = createDocBuilder({
+ tiptapEditor,
+ names: {
+ image: { nodeType: Image.name },
+ },
+ }));
+ });
+
+ it('adds data-canonical-src attribute when rendering to HTML', () => {
+ const initialDoc = doc(
+ p(
+ image({
+ canonicalSrc: 'uploads/image.jpg',
+ src: '/-/wikis/uploads/image.jpg',
+ alt: 'image',
+ title: 'this is an image',
+ }),
+ ),
+ );
+
+ tiptapEditor.commands.setContent(initialDoc.toJSON());
+
+ expect(tiptapEditor.getHTML()).toEqual(
+ '<p><img src="/-/wikis/uploads/image.jpg" alt="image" title="this is an image" data-canonical-src="uploads/image.jpg"></p>',
+ );
+ });
+});
diff --git a/spec/frontend/content_editor/extensions/link_spec.js b/spec/frontend/content_editor/extensions/link_spec.js
index ead898554d1..bb841357d37 100644
--- a/spec/frontend/content_editor/extensions/link_spec.js
+++ b/spec/frontend/content_editor/extensions/link_spec.js
@@ -33,7 +33,7 @@ describe('content_editor/extensions/link', () => {
${'documentation](readme.md'} | ${() => p('documentation](readme.md')}
${'http://example.com '} | ${() => p(link({ href: 'http://example.com' }, 'http://example.com'))}
${'https://example.com '} | ${() => p(link({ href: 'https://example.com' }, 'https://example.com'))}
- ${'www.example.com '} | ${() => p(link({ href: 'www.example.com' }, 'www.example.com'))}
+ ${'www.example.com '} | ${() => p(link({ href: 'http://www.example.com' }, 'www.example.com'))}
${'example.com/ab.html '} | ${() => p('example.com/ab.html')}
${'https://www.google.com '} | ${() => p(link({ href: 'https://www.google.com' }, 'https://www.google.com'))}
`('with input=$input, then should insert a $insertedNode', ({ input, insertedNode }) => {
diff --git a/spec/frontend/content_editor/services/markdown_serializer_spec.js b/spec/frontend/content_editor/services/markdown_serializer_spec.js
index 97f6d8f6334..01d4c994e88 100644
--- a/spec/frontend/content_editor/services/markdown_serializer_spec.js
+++ b/spec/frontend/content_editor/services/markdown_serializer_spec.js
@@ -164,6 +164,17 @@ describe('markdownSerializer', () => {
expect(serialize(paragraph(italic('italics')))).toBe('_italics_');
});
+ it('correctly serializes code blocks wrapped by italics and bold marks', () => {
+ const text = 'code block';
+
+ expect(serialize(paragraph(italic(code(text))))).toBe(`_\`${text}\`_`);
+ expect(serialize(paragraph(code(italic(text))))).toBe(`_\`${text}\`_`);
+ expect(serialize(paragraph(bold(code(text))))).toBe(`**\`${text}\`**`);
+ expect(serialize(paragraph(code(bold(text))))).toBe(`**\`${text}\`**`);
+ expect(serialize(paragraph(strike(code(text))))).toBe(`~~\`${text}\`~~`);
+ expect(serialize(paragraph(code(strike(text))))).toBe(`~~\`${text}\`~~`);
+ });
+
it('correctly serializes inline diff', () => {
expect(
serialize(
@@ -341,6 +352,10 @@ this is not really json but just trying out whether this case works or not
);
});
+ it('does not serialize an image when src and canonicalSrc are empty', () => {
+ expect(serialize(paragraph(image({})))).toBe('');
+ });
+
it('correctly serializes an image with a title', () => {
expect(serialize(paragraph(image({ src: 'img.jpg', title: 'baz', alt: 'foo bar' })))).toBe(
'![foo bar](img.jpg "baz")',
diff --git a/spec/frontend/create_cluster/components/cluster_form_dropdown_spec.js b/spec/frontend/create_cluster/components/cluster_form_dropdown_spec.js
index 0c6095e601f..4e92fa1df16 100644
--- a/spec/frontend/create_cluster/components/cluster_form_dropdown_spec.js
+++ b/spec/frontend/create_cluster/components/cluster_form_dropdown_spec.js
@@ -206,6 +206,8 @@ describe('ClusterFormDropdown', () => {
const searchQuery = secondItem.name;
wrapper.setProps({ items });
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({ searchQuery });
return wrapper.vm.$nextTick().then(() => {
diff --git a/spec/frontend/create_cluster/eks_cluster/components/service_credentials_form_spec.js b/spec/frontend/create_cluster/eks_cluster/components/service_credentials_form_spec.js
index d866ffd4efb..a0510d46794 100644
--- a/spec/frontend/create_cluster/eks_cluster/components/service_credentials_form_spec.js
+++ b/spec/frontend/create_cluster/eks_cluster/components/service_credentials_form_spec.js
@@ -67,6 +67,8 @@ describe('ServiceCredentialsForm', () => {
});
it('enables submit button when role ARN is not provided', () => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
vm.setData({ roleArn: '123' });
return vm.vm.$nextTick().then(() => {
@@ -75,6 +77,8 @@ describe('ServiceCredentialsForm', () => {
});
it('dispatches createRole action when submit button is clicked', () => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
vm.setData({ roleArn: '123' }); // set role ARN to enable button
findSubmitButton().vm.$emit('click', new Event('click'));
@@ -84,6 +88,8 @@ describe('ServiceCredentialsForm', () => {
describe('when is creating role', () => {
beforeEach(() => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
vm.setData({ roleArn: '123' }); // set role ARN to enable button
state.isCreatingRole = true;
diff --git a/spec/frontend/create_cluster/gke_cluster/components/gke_machine_type_dropdown_spec.js b/spec/frontend/create_cluster/gke_cluster/components/gke_machine_type_dropdown_spec.js
index 8f4903dd91b..2b6f2134553 100644
--- a/spec/frontend/create_cluster/gke_cluster/components/gke_machine_type_dropdown_spec.js
+++ b/spec/frontend/create_cluster/gke_cluster/components/gke_machine_type_dropdown_spec.js
@@ -79,6 +79,8 @@ describe('GkeMachineTypeDropdown', () => {
store = createStore();
wrapper = createComponent(store);
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({ isLoading: true });
return wrapper.vm.$nextTick().then(() => {
diff --git a/spec/frontend/create_cluster/gke_cluster/components/gke_project_id_dropdown_spec.js b/spec/frontend/create_cluster/gke_cluster/components/gke_project_id_dropdown_spec.js
index b191b107609..2b0acc8cf5d 100644
--- a/spec/frontend/create_cluster/gke_cluster/components/gke_project_id_dropdown_spec.js
+++ b/spec/frontend/create_cluster/gke_cluster/components/gke_project_id_dropdown_spec.js
@@ -83,6 +83,8 @@ describe('GkeProjectIdDropdown', () => {
it('returns default toggle text', () => {
bootstrap();
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({ isLoading: false });
return wrapper.vm.$nextTick().then(() => {
@@ -99,6 +101,8 @@ describe('GkeProjectIdDropdown', () => {
hasProject: () => true,
},
);
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({ isLoading: false });
return wrapper.vm.$nextTick().then(() => {
@@ -110,6 +114,8 @@ describe('GkeProjectIdDropdown', () => {
bootstrap({
projects: null,
});
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({ isLoading: false });
return wrapper.vm.$nextTick().then(() => {
diff --git a/spec/frontend/create_cluster/gke_cluster/components/gke_zone_dropdown_spec.js b/spec/frontend/create_cluster/gke_cluster/components/gke_zone_dropdown_spec.js
index 4054b768e34..22fc681f863 100644
--- a/spec/frontend/create_cluster/gke_cluster/components/gke_zone_dropdown_spec.js
+++ b/spec/frontend/create_cluster/gke_cluster/components/gke_zone_dropdown_spec.js
@@ -47,6 +47,8 @@ describe('GkeZoneDropdown', () => {
describe('isLoading', () => {
beforeEach(() => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({ isLoading: true });
return wrapper.vm.$nextTick();
});
diff --git a/spec/frontend/crm/contact_form_spec.js b/spec/frontend/crm/contact_form_spec.js
index b2753ad8cf5..0edab4f5ec5 100644
--- a/spec/frontend/crm/contact_form_spec.js
+++ b/spec/frontend/crm/contact_form_spec.js
@@ -112,7 +112,7 @@ describe('Customer relations contact form component', () => {
await waitForPromises();
expect(findError().exists()).toBe(true);
- expect(findError().text()).toBe('Phone is invalid.');
+ expect(findError().text()).toBe('create contact is invalid.');
});
});
@@ -151,7 +151,7 @@ describe('Customer relations contact form component', () => {
await waitForPromises();
expect(findError().exists()).toBe(true);
- expect(findError().text()).toBe('Email is invalid.');
+ expect(findError().text()).toBe('update contact is invalid.');
});
});
});
diff --git a/spec/frontend/crm/form_spec.js b/spec/frontend/crm/form_spec.js
new file mode 100644
index 00000000000..0e3abc05c37
--- /dev/null
+++ b/spec/frontend/crm/form_spec.js
@@ -0,0 +1,278 @@
+import { GlAlert } from '@gitlab/ui';
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import VueRouter from 'vue-router';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import Form from '~/crm/components/form.vue';
+import routes from '~/crm/routes';
+import createContactMutation from '~/crm/components/queries/create_contact.mutation.graphql';
+import updateContactMutation from '~/crm/components/queries/update_contact.mutation.graphql';
+import getGroupContactsQuery from '~/crm/components/queries/get_group_contacts.query.graphql';
+import createOrganizationMutation from '~/crm/components/queries/create_organization.mutation.graphql';
+import getGroupOrganizationsQuery from '~/crm/components/queries/get_group_organizations.query.graphql';
+import {
+ createContactMutationErrorResponse,
+ createContactMutationResponse,
+ getGroupContactsQueryResponse,
+ updateContactMutationErrorResponse,
+ updateContactMutationResponse,
+ createOrganizationMutationErrorResponse,
+ createOrganizationMutationResponse,
+ getGroupOrganizationsQueryResponse,
+} from './mock_data';
+
+const FORM_CREATE_CONTACT = 'create contact';
+const FORM_UPDATE_CONTACT = 'update contact';
+const FORM_CREATE_ORG = 'create organization';
+
+describe('Reusable form component', () => {
+ Vue.use(VueApollo);
+ Vue.use(VueRouter);
+
+ const DEFAULT_RESPONSES = {
+ createContact: Promise.resolve(createContactMutationResponse),
+ updateContact: Promise.resolve(updateContactMutationResponse),
+ createOrg: Promise.resolve(createOrganizationMutationResponse),
+ };
+
+ let wrapper;
+ let handler;
+ let fakeApollo;
+ let router;
+
+ beforeEach(() => {
+ router = new VueRouter({
+ base: '',
+ mode: 'history',
+ routes,
+ });
+ router.push('/test');
+
+ handler = jest.fn().mockImplementation((key) => DEFAULT_RESPONSES[key]);
+
+ const hanlderWithKey = (key) => (...args) => handler(key, ...args);
+
+ fakeApollo = createMockApollo([
+ [createContactMutation, hanlderWithKey('createContact')],
+ [updateContactMutation, hanlderWithKey('updateContact')],
+ [createOrganizationMutation, hanlderWithKey('createOrg')],
+ ]);
+
+ fakeApollo.clients.defaultClient.cache.writeQuery({
+ query: getGroupContactsQuery,
+ variables: { groupFullPath: 'flightjs' },
+ data: getGroupContactsQueryResponse.data,
+ });
+
+ fakeApollo.clients.defaultClient.cache.writeQuery({
+ query: getGroupOrganizationsQuery,
+ variables: { groupFullPath: 'flightjs' },
+ data: getGroupOrganizationsQueryResponse.data,
+ });
+ });
+
+ const mockToastShow = jest.fn();
+
+ const findSaveButton = () => wrapper.findByTestId('save-button');
+ const findForm = () => wrapper.find('form');
+ const findError = () => wrapper.findComponent(GlAlert);
+
+ const mountComponent = (propsData) => {
+ wrapper = shallowMountExtended(Form, {
+ router,
+ apolloProvider: fakeApollo,
+ propsData: { drawerOpen: true, ...propsData },
+ mocks: {
+ $toast: {
+ show: mockToastShow,
+ },
+ },
+ });
+ };
+
+ const mountContact = ({ propsData } = {}) => {
+ mountComponent({
+ fields: [
+ { name: 'firstName', label: 'First name', required: true },
+ { name: 'lastName', label: 'Last name', required: true },
+ { name: 'email', label: 'Email', required: true },
+ { name: 'phone', label: 'Phone' },
+ { name: 'description', label: 'Description' },
+ ],
+ ...propsData,
+ });
+ };
+
+ const mountContactCreate = () => {
+ const propsData = {
+ title: 'New contact',
+ successMessage: 'Contact has been added',
+ buttonLabel: 'Create contact',
+ getQuery: {
+ query: getGroupContactsQuery,
+ variables: { groupFullPath: 'flightjs' },
+ },
+ getQueryNodePath: 'group.contacts',
+ mutation: createContactMutation,
+ additionalCreateParams: { groupId: 'gid://gitlab/Group/26' },
+ };
+ mountContact({ propsData });
+ };
+
+ const mountContactUpdate = () => {
+ const propsData = {
+ title: 'Edit contact',
+ successMessage: 'Contact has been updated',
+ mutation: updateContactMutation,
+ existingModel: {
+ id: 'gid://gitlab/CustomerRelations::Contact/12',
+ firstName: 'First',
+ lastName: 'Last',
+ email: 'email@example.com',
+ },
+ };
+ mountContact({ propsData });
+ };
+
+ const mountOrganization = ({ propsData } = {}) => {
+ mountComponent({
+ fields: [
+ { name: 'name', label: 'Name', required: true },
+ { name: 'defaultRate', label: 'Default rate', input: { type: 'number', step: '0.01' } },
+ { name: 'description', label: 'Description' },
+ ],
+ ...propsData,
+ });
+ };
+
+ const mountOrganizationCreate = () => {
+ const propsData = {
+ title: 'New organization',
+ successMessage: 'Organization has been added',
+ buttonLabel: 'Create organization',
+ getQuery: {
+ query: getGroupOrganizationsQuery,
+ variables: { groupFullPath: 'flightjs' },
+ },
+ getQueryNodePath: 'group.organizations',
+ mutation: createOrganizationMutation,
+ additionalCreateParams: { groupId: 'gid://gitlab/Group/26' },
+ };
+ mountOrganization({ propsData });
+ };
+
+ const forms = {
+ [FORM_CREATE_CONTACT]: {
+ mountFunction: mountContactCreate,
+ mutationErrorResponse: createContactMutationErrorResponse,
+ toastMessage: 'Contact has been added',
+ },
+ [FORM_UPDATE_CONTACT]: {
+ mountFunction: mountContactUpdate,
+ mutationErrorResponse: updateContactMutationErrorResponse,
+ toastMessage: 'Contact has been updated',
+ },
+ [FORM_CREATE_ORG]: {
+ mountFunction: mountOrganizationCreate,
+ mutationErrorResponse: createOrganizationMutationErrorResponse,
+ toastMessage: 'Organization has been added',
+ },
+ };
+ const asTestParams = (...keys) => keys.map((name) => [name, forms[name]]);
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe.each(asTestParams(FORM_CREATE_CONTACT, FORM_UPDATE_CONTACT))(
+ '%s form save button',
+ (name, { mountFunction }) => {
+ beforeEach(() => {
+ mountFunction();
+ });
+
+ it('should be disabled when required fields are empty', async () => {
+ wrapper.find('#firstName').vm.$emit('input', '');
+ await waitForPromises();
+
+ expect(findSaveButton().props('disabled')).toBe(true);
+ });
+
+ it('should not be disabled when required fields have values', async () => {
+ wrapper.find('#firstName').vm.$emit('input', 'A');
+ wrapper.find('#lastName').vm.$emit('input', 'B');
+ wrapper.find('#email').vm.$emit('input', 'C');
+ await waitForPromises();
+
+ expect(findSaveButton().props('disabled')).toBe(false);
+ });
+ },
+ );
+
+ describe.each(asTestParams(FORM_CREATE_ORG))('%s form save button', (name, { mountFunction }) => {
+ beforeEach(() => {
+ mountFunction();
+ });
+
+ it('should be disabled when required field is empty', async () => {
+ wrapper.find('#name').vm.$emit('input', '');
+ await waitForPromises();
+
+ expect(findSaveButton().props('disabled')).toBe(true);
+ });
+
+ it('should not be disabled when required field has a value', async () => {
+ wrapper.find('#name').vm.$emit('input', 'A');
+ await waitForPromises();
+
+ expect(findSaveButton().props('disabled')).toBe(false);
+ });
+ });
+
+ describe.each(asTestParams(FORM_CREATE_CONTACT, FORM_UPDATE_CONTACT, FORM_CREATE_ORG))(
+ 'when %s mutation is successful',
+ (name, { mountFunction, toastMessage }) => {
+ it('form should display correct toast message', async () => {
+ mountFunction();
+
+ findForm().trigger('submit');
+ await waitForPromises();
+
+ expect(mockToastShow).toHaveBeenCalledWith(toastMessage);
+ });
+ },
+ );
+
+ describe.each(asTestParams(FORM_CREATE_CONTACT, FORM_UPDATE_CONTACT, FORM_CREATE_ORG))(
+ 'when %s mutation fails',
+ (formName, { mutationErrorResponse, mountFunction }) => {
+ beforeEach(() => {
+ jest.spyOn(console, 'error').mockImplementation();
+ });
+
+ it('should show error on reject', async () => {
+ handler.mockRejectedValue('ERROR');
+
+ mountFunction();
+
+ findForm().trigger('submit');
+ await waitForPromises();
+
+ expect(findError().text()).toBe('Something went wrong. Please try again.');
+ });
+
+ it('should show error on error response', async () => {
+ handler.mockResolvedValue(mutationErrorResponse);
+
+ mountFunction();
+
+ findForm().trigger('submit');
+ await waitForPromises();
+
+ expect(findError().text()).toBe(`${formName} is invalid.`);
+ });
+ },
+ );
+});
diff --git a/spec/frontend/crm/mock_data.js b/spec/frontend/crm/mock_data.js
index f7af2ccdb72..e351e101b29 100644
--- a/spec/frontend/crm/mock_data.js
+++ b/spec/frontend/crm/mock_data.js
@@ -82,7 +82,6 @@ export const getGroupOrganizationsQueryResponse = {
export const createContactMutationResponse = {
data: {
customerRelationsContactCreate: {
- __typeName: 'CustomerRelationsContactCreatePayload',
contact: {
__typename: 'CustomerRelationsContact',
id: 'gid://gitlab/CustomerRelations::Contact/1',
@@ -102,7 +101,7 @@ export const createContactMutationErrorResponse = {
data: {
customerRelationsContactCreate: {
contact: null,
- errors: ['Phone is invalid.'],
+ errors: ['create contact is invalid.'],
},
},
};
@@ -130,7 +129,7 @@ export const updateContactMutationErrorResponse = {
data: {
customerRelationsContactUpdate: {
contact: null,
- errors: ['Email is invalid.'],
+ errors: ['update contact is invalid.'],
},
},
};
@@ -138,7 +137,6 @@ export const updateContactMutationErrorResponse = {
export const createOrganizationMutationResponse = {
data: {
customerRelationsOrganizationCreate: {
- __typeName: 'CustomerRelationsOrganizationCreatePayload',
organization: {
__typename: 'CustomerRelationsOrganization',
id: 'gid://gitlab/CustomerRelations::Organization/2',
@@ -155,7 +153,7 @@ export const createOrganizationMutationErrorResponse = {
data: {
customerRelationsOrganizationCreate: {
organization: null,
- errors: ['Name cannot be blank.'],
+ errors: ['create organization is invalid.'],
},
},
};
diff --git a/spec/frontend/crm/new_organization_form_spec.js b/spec/frontend/crm/new_organization_form_spec.js
index 976b626f35f..0a7909774c9 100644
--- a/spec/frontend/crm/new_organization_form_spec.js
+++ b/spec/frontend/crm/new_organization_form_spec.js
@@ -103,7 +103,7 @@ describe('Customer relations organizations root app', () => {
await waitForPromises();
expect(findError().exists()).toBe(true);
- expect(findError().text()).toBe('Name cannot be blank.');
+ expect(findError().text()).toBe('create organization is invalid.');
});
});
});
diff --git a/spec/frontend/cycle_analytics/stage_table_spec.js b/spec/frontend/cycle_analytics/stage_table_spec.js
index 3158446c37d..9605dce2668 100644
--- a/spec/frontend/cycle_analytics/stage_table_spec.js
+++ b/spec/frontend/cycle_analytics/stage_table_spec.js
@@ -24,6 +24,7 @@ const findTable = () => wrapper.findComponent(GlTable);
const findTableHead = () => wrapper.find('thead');
const findTableHeadColumns = () => findTableHead().findAll('th');
const findStageEventTitle = (ev) => extendedWrapper(ev).findByTestId('vsa-stage-event-title');
+const findStageEventLink = (ev) => extendedWrapper(ev).findByTestId('vsa-stage-event-link');
const findStageTime = () => wrapper.findByTestId('vsa-stage-event-time');
const findIcon = (name) => wrapper.findByTestId(`${name}-icon`);
@@ -86,6 +87,15 @@ describe('StageTable', () => {
expect(titles[index]).toBe(ev.title);
});
});
+
+ it('will not display the project name in the record link', () => {
+ const evs = findStageEvents();
+
+ const links = evs.wrappers.map((ev) => findStageEventLink(ev).text());
+ issueEventItems.forEach((ev, index) => {
+ expect(links[index]).toBe(`#${ev.iid}`);
+ });
+ });
});
describe('default event', () => {
@@ -187,6 +197,53 @@ describe('StageTable', () => {
});
});
+ describe('includeProjectName set', () => {
+ const fakenamespace = 'some/fake/path';
+ beforeEach(() => {
+ wrapper = createComponent({ includeProjectName: true });
+ });
+
+ it('will display the project name in the record link', () => {
+ const evs = findStageEvents();
+
+ const links = evs.wrappers.map((ev) => findStageEventLink(ev).text());
+ issueEventItems.forEach((ev, index) => {
+ expect(links[index]).toBe(`${ev.projectPath}#${ev.iid}`);
+ });
+ });
+
+ describe.each`
+ namespaceFullPath | hasFullPath
+ ${'fake'} | ${false}
+ ${fakenamespace} | ${true}
+ `('with a namespace', ({ namespaceFullPath, hasFullPath }) => {
+ let evs = null;
+ let links = null;
+
+ beforeEach(() => {
+ wrapper = createComponent({
+ includeProjectName: true,
+ stageEvents: issueEventItems.map((ie) => ({ ...ie, namespaceFullPath })),
+ });
+
+ evs = findStageEvents();
+ links = evs.wrappers.map((ev) => findStageEventLink(ev).text());
+ });
+
+ it(`with namespaceFullPath='${namespaceFullPath}' ${
+ hasFullPath ? 'will' : 'does not'
+ } include the namespace`, () => {
+ issueEventItems.forEach((ev, index) => {
+ if (hasFullPath) {
+ expect(links[index]).toBe(`${namespaceFullPath}/${ev.projectPath}#${ev.iid}`);
+ } else {
+ expect(links[index]).toBe(`${ev.projectPath}#${ev.iid}`);
+ }
+ });
+ });
+ });
+ });
+
describe('Pagination', () => {
beforeEach(() => {
wrapper = createComponent();
diff --git a/spec/frontend/cycle_analytics/value_stream_metrics_spec.js b/spec/frontend/cycle_analytics/value_stream_metrics_spec.js
index c97e4845bc2..082db2cc312 100644
--- a/spec/frontend/cycle_analytics/value_stream_metrics_spec.js
+++ b/spec/frontend/cycle_analytics/value_stream_metrics_spec.js
@@ -63,6 +63,8 @@ describe('ValueStreamMetrics', () => {
it('renders hidden GlSingleStat components for each metric', async () => {
await waitForPromises();
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({ isLoading: true });
await wrapper.vm.$nextTick();
diff --git a/spec/frontend/deploy_freeze/components/timezone_dropdown_spec.js b/spec/frontend/deploy_freeze/components/timezone_dropdown_spec.js
index 4dd5c29a917..5f4d4071f29 100644
--- a/spec/frontend/deploy_freeze/components/timezone_dropdown_spec.js
+++ b/spec/frontend/deploy_freeze/components/timezone_dropdown_spec.js
@@ -26,6 +26,8 @@ describe('Deploy freeze timezone dropdown', () => {
},
});
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({ searchTerm });
};
diff --git a/spec/frontend/design_management/components/design_notes/__snapshots__/design_note_signed_out_spec.js.snap b/spec/frontend/design_management/components/design_notes/__snapshots__/design_note_signed_out_spec.js.snap
new file mode 100644
index 00000000000..ab37cb90bd3
--- /dev/null
+++ b/spec/frontend/design_management/components/design_notes/__snapshots__/design_note_signed_out_spec.js.snap
@@ -0,0 +1,41 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`DesignNoteSignedOut renders message containing register and sign-in links while user wants to reply to a discussion 1`] = `
+<div
+ class="disabled-comment text-center"
+>
+ Please
+ <gl-link-stub
+ href="/users/sign_up?redirect_to_referer=yes"
+ >
+ register
+ </gl-link-stub>
+ or
+ <gl-link-stub
+ href="/users/sign_in?redirect_to_referer=yes"
+ >
+ sign in
+ </gl-link-stub>
+ to reply.
+</div>
+`;
+
+exports[`DesignNoteSignedOut renders message containing register and sign-in links while user wants to start a new discussion 1`] = `
+<div
+ class="disabled-comment text-center"
+>
+ Please
+ <gl-link-stub
+ href="/users/sign_up?redirect_to_referer=yes"
+ >
+ register
+ </gl-link-stub>
+ or
+ <gl-link-stub
+ href="/users/sign_in?redirect_to_referer=yes"
+ >
+ sign in
+ </gl-link-stub>
+ to start a new discussion.
+</div>
+`;
diff --git a/spec/frontend/design_management/components/design_notes/design_discussion_spec.js b/spec/frontend/design_management/components/design_notes/design_discussion_spec.js
index 9335d800a16..e816a05ba53 100644
--- a/spec/frontend/design_management/components/design_notes/design_discussion_spec.js
+++ b/spec/frontend/design_management/components/design_notes/design_discussion_spec.js
@@ -1,7 +1,9 @@
import { GlLoadingIcon } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
+import { ApolloMutation } from 'vue-apollo';
import DesignDiscussion from '~/design_management/components/design_notes/design_discussion.vue';
import DesignNote from '~/design_management/components/design_notes/design_note.vue';
+import DesignNoteSignedOut from '~/design_management/components/design_notes/design_note_signed_out.vue';
import DesignReplyForm from '~/design_management/components/design_notes/design_reply_form.vue';
import ToggleRepliesWidget from '~/design_management/components/design_notes/toggle_replies_widget.vue';
import createNoteMutation from '~/design_management/graphql/mutations/create_note.mutation.graphql';
@@ -20,6 +22,7 @@ const defaultMockDiscussion = {
const DEFAULT_TODO_COUNT = 2;
describe('Design discussions component', () => {
+ const originalGon = window.gon;
let wrapper;
const findDesignNotes = () => wrapper.findAll(DesignNote);
@@ -31,6 +34,7 @@ describe('Design discussions component', () => {
const findResolvedMessage = () => wrapper.find('[data-testid="resolved-message"]');
const findResolveLoadingIcon = () => wrapper.find(GlLoadingIcon);
const findResolveCheckbox = () => wrapper.find('[data-testid="resolve-checkbox"]');
+ const findApolloMutation = () => wrapper.findComponent(ApolloMutation);
const mutationVariables = {
mutation: createNoteMutation,
@@ -42,6 +46,8 @@ describe('Design discussions component', () => {
},
},
};
+ const registerPath = '/users/sign_up?redirect_to_referer=yes';
+ const signInPath = '/users/sign_in?redirect_to_referer=yes';
const mutate = jest.fn().mockResolvedValue({ data: { createNote: { errors: [] } } });
const readQuery = jest.fn().mockReturnValue({
project: {
@@ -62,6 +68,8 @@ describe('Design discussions component', () => {
designId: 'design-id',
discussionIndex: 1,
discussionWithOpenForm: '',
+ registerPath,
+ signInPath,
...props,
},
data() {
@@ -88,8 +96,13 @@ describe('Design discussions component', () => {
});
}
+ beforeEach(() => {
+ window.gon = { current_user_id: 1 };
+ });
+
afterEach(() => {
wrapper.destroy();
+ window.gon = originalGon;
});
describe('when discussion is not resolvable', () => {
@@ -349,4 +362,41 @@ describe('Design discussions component', () => {
expect(wrapper.emitted('open-form')).toBeTruthy();
});
+
+ describe('when user is not logged in', () => {
+ const findDesignNoteSignedOut = () => wrapper.findComponent(DesignNoteSignedOut);
+
+ beforeEach(() => {
+ window.gon = { current_user_id: null };
+ createComponent(
+ {
+ discussion: {
+ ...defaultMockDiscussion,
+ },
+ discussionWithOpenForm: defaultMockDiscussion.id,
+ },
+ { discussionComment: 'test', isFormRendered: true },
+ );
+ });
+
+ it('does not render resolve discussion button', () => {
+ expect(findResolveButton().exists()).toBe(false);
+ });
+
+ it('does not render replace-placeholder component', () => {
+ expect(findReplyPlaceholder().exists()).toBe(false);
+ });
+
+ it('does not render apollo-mutation component', () => {
+ expect(findApolloMutation().exists()).toBe(false);
+ });
+
+ it('renders design-note-signed-out component', () => {
+ expect(findDesignNoteSignedOut().exists()).toBe(true);
+ expect(findDesignNoteSignedOut().props()).toMatchObject({
+ registerPath,
+ signInPath,
+ });
+ });
+ });
});
diff --git a/spec/frontend/design_management/components/design_notes/design_note_signed_out_spec.js b/spec/frontend/design_management/components/design_notes/design_note_signed_out_spec.js
new file mode 100644
index 00000000000..e71bb5ab520
--- /dev/null
+++ b/spec/frontend/design_management/components/design_notes/design_note_signed_out_spec.js
@@ -0,0 +1,36 @@
+import { GlSprintf } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import DesignNoteSignedOut from '~/design_management/components/design_notes/design_note_signed_out.vue';
+
+function createComponent(isAddDiscussion = false) {
+ return shallowMount(DesignNoteSignedOut, {
+ propsData: {
+ registerPath: '/users/sign_up?redirect_to_referer=yes',
+ signInPath: '/users/sign_in?redirect_to_referer=yes',
+ isAddDiscussion,
+ },
+ stubs: {
+ GlSprintf,
+ },
+ });
+}
+
+describe('DesignNoteSignedOut', () => {
+ let wrapper;
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('renders message containing register and sign-in links while user wants to reply to a discussion', () => {
+ wrapper = createComponent();
+
+ expect(wrapper.element).toMatchSnapshot();
+ });
+
+ it('renders message containing register and sign-in links while user wants to start a new discussion', () => {
+ wrapper = createComponent(true);
+
+ expect(wrapper.element).toMatchSnapshot();
+ });
+});
diff --git a/spec/frontend/design_management/components/design_overlay_spec.js b/spec/frontend/design_management/components/design_overlay_spec.js
index d3119be7159..4bda5054090 100644
--- a/spec/frontend/design_management/components/design_overlay_spec.js
+++ b/spec/frontend/design_management/components/design_overlay_spec.js
@@ -117,6 +117,8 @@ describe('Design overlay component', () => {
it.each([notes[0].discussion.notes.nodes[1], notes[0].discussion.notes.nodes[0]])(
'should not apply inactive class to the pin for the active discussion',
(note) => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
activeDiscussion: {
id: note.id,
@@ -131,6 +133,8 @@ describe('Design overlay component', () => {
);
it('should apply inactive class to all pins besides the active one', () => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
activeDiscussion: {
id: notes[0].id,
@@ -212,6 +216,8 @@ describe('Design overlay component', () => {
const { position } = note;
const newCoordinates = { x: 20, y: 20 };
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
movingNoteNewPosition: {
...position,
@@ -345,6 +351,8 @@ describe('Design overlay component', () => {
});
const newCoordinates = { x: 20, y: 20 };
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
movingNoteStartPosition: {
...notes[0].position,
@@ -368,6 +376,8 @@ describe('Design overlay component', () => {
it('should calculate delta correctly from state', () => {
createComponent();
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
movingNoteStartPosition: {
clientX: 10,
diff --git a/spec/frontend/design_management/components/design_presentation_spec.js b/spec/frontend/design_management/components/design_presentation_spec.js
index edf8b965153..adec9ef469d 100644
--- a/spec/frontend/design_management/components/design_presentation_spec.js
+++ b/spec/frontend/design_management/components/design_presentation_spec.js
@@ -15,6 +15,7 @@ const mockOverlayData = {
};
describe('Design management design presentation component', () => {
+ const originalGon = window.gon;
let wrapper;
function createComponent(
@@ -39,6 +40,8 @@ describe('Design management design presentation component', () => {
stubs,
});
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData(data);
wrapper.element.scrollTo = jest.fn();
}
@@ -113,8 +116,13 @@ describe('Design management design presentation component', () => {
});
}
+ beforeEach(() => {
+ window.gon = { current_user_id: 1 };
+ });
+
afterEach(() => {
wrapper.destroy();
+ window.gon = originalGon;
});
it('renders image and overlay when image provided', () => {
@@ -550,4 +558,23 @@ describe('Design management design presentation component', () => {
});
});
});
+
+ describe('when user is not logged in', () => {
+ beforeEach(() => {
+ window.gon = { current_user_id: null };
+ createComponent(
+ {
+ image: 'test.jpg',
+ imageName: 'test',
+ },
+ mockOverlayData,
+ );
+ });
+
+ it('disables commenting from design overlay', () => {
+ expect(wrapper.findComponent(DesignOverlay).props()).toMatchObject({
+ disableCommenting: true,
+ });
+ });
+ });
});
diff --git a/spec/frontend/design_management/components/design_sidebar_spec.js b/spec/frontend/design_management/components/design_sidebar_spec.js
index 8eb993ec7b5..4cd71bdb7f3 100644
--- a/spec/frontend/design_management/components/design_sidebar_spec.js
+++ b/spec/frontend/design_management/components/design_sidebar_spec.js
@@ -2,6 +2,7 @@ import { GlCollapse, GlPopover } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import Cookies from 'js-cookie';
import DesignDiscussion from '~/design_management/components/design_notes/design_discussion.vue';
+import DesignNoteSignedOut from '~/design_management/components/design_notes/design_note_signed_out.vue';
import DesignSidebar from '~/design_management/components/design_sidebar.vue';
import DesignTodoButton from '~/design_management/components/design_todo_button.vue';
import updateActiveDiscussionMutation from '~/design_management/graphql/mutations/update_active_discussion.mutation.graphql';
@@ -30,6 +31,7 @@ const cookieKey = 'hide_design_resolved_comments_popover';
const mutate = jest.fn().mockResolvedValue();
describe('Design management design sidebar component', () => {
+ const originalGon = window.gon;
let wrapper;
const findDiscussions = () => wrapper.findAll(DesignDiscussion);
@@ -58,11 +60,20 @@ describe('Design management design sidebar component', () => {
},
},
stubs: { GlPopover },
+ provide: {
+ registerPath: '/users/sign_up?redirect_to_referer=yes',
+ signInPath: '/users/sign_in?redirect_to_referer=yes',
+ },
});
}
+ beforeEach(() => {
+ window.gon = { current_user_id: 1 };
+ });
+
afterEach(() => {
wrapper.destroy();
+ window.gon = originalGon;
});
it('renders participants', () => {
@@ -248,4 +259,44 @@ describe('Design management design sidebar component', () => {
expect(Cookies.set).toHaveBeenCalledWith(cookieKey, 'true', { expires: 365 * 10 });
});
});
+
+ describe('when user is not logged in', () => {
+ const findDesignNoteSignedOut = () => wrapper.findComponent(DesignNoteSignedOut);
+
+ beforeEach(() => {
+ window.gon = { current_user_id: null };
+ });
+
+ describe('design has no discussions', () => {
+ beforeEach(() => {
+ createComponent({
+ design: {
+ ...design,
+ discussions: {
+ nodes: [],
+ },
+ },
+ });
+ });
+
+ it('does not render a message about possibility to create a new discussion', () => {
+ expect(findNewDiscussionDisclaimer().exists()).toBe(false);
+ });
+
+ it('renders design-note-signed-out component', () => {
+ expect(findDesignNoteSignedOut().exists()).toBe(true);
+ });
+ });
+
+ describe('design has discussions', () => {
+ beforeEach(() => {
+ Cookies.set(cookieKey, true);
+ createComponent();
+ });
+
+ it('renders design-note-signed-out component', () => {
+ expect(findDesignNoteSignedOut().exists()).toBe(true);
+ });
+ });
+ });
});
diff --git a/spec/frontend/design_management/components/image_spec.js b/spec/frontend/design_management/components/image_spec.js
index 765d902f9a6..ac3afc73c86 100644
--- a/spec/frontend/design_management/components/image_spec.js
+++ b/spec/frontend/design_management/components/image_spec.js
@@ -9,6 +9,8 @@ describe('Design management large image component', () => {
wrapper = shallowMount(DesignImage, {
propsData,
});
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData(data);
}
diff --git a/spec/frontend/design_management/components/toolbar/design_navigation_spec.js b/spec/frontend/design_management/components/toolbar/design_navigation_spec.js
index 1d9b9c002f9..6e0592984a2 100644
--- a/spec/frontend/design_management/components/toolbar/design_navigation_spec.js
+++ b/spec/frontend/design_management/components/toolbar/design_navigation_spec.js
@@ -42,6 +42,8 @@ describe('Design management pagination component', () => {
});
it('renders navigation buttons', () => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
designCollection: { designs: [{ id: '1' }, { id: '2' }] },
});
@@ -53,6 +55,8 @@ describe('Design management pagination component', () => {
describe('keyboard buttons navigation', () => {
beforeEach(() => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
designCollection: { designs: [{ filename: '1' }, { filename: '2' }, { filename: '3' }] },
});
diff --git a/spec/frontend/design_management/components/toolbar/index_spec.js b/spec/frontend/design_management/components/toolbar/index_spec.js
index 009ffe57744..cf872046f53 100644
--- a/spec/frontend/design_management/components/toolbar/index_spec.js
+++ b/spec/frontend/design_management/components/toolbar/index_spec.js
@@ -48,6 +48,8 @@ describe('Design management toolbar component', () => {
},
});
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
permissions: {
createDesign,
diff --git a/spec/frontend/design_management/components/upload/design_version_dropdown_spec.js b/spec/frontend/design_management/components/upload/design_version_dropdown_spec.js
index ebfe27eaa71..a4fb671ae13 100644
--- a/spec/frontend/design_management/components/upload/design_version_dropdown_spec.js
+++ b/spec/frontend/design_management/components/upload/design_version_dropdown_spec.js
@@ -34,6 +34,8 @@ describe('Design management design version dropdown component', () => {
stubs: { GlSprintf },
});
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
allVersions: maxVersions > -1 ? mockAllVersions.slice(0, maxVersions) : mockAllVersions,
});
diff --git a/spec/frontend/design_management/pages/design/__snapshots__/index_spec.js.snap b/spec/frontend/design_management/pages/design/__snapshots__/index_spec.js.snap
index 3d04840b1f8..31b3117cb6c 100644
--- a/spec/frontend/design_management/pages/design/__snapshots__/index_spec.js.snap
+++ b/spec/frontend/design_management/pages/design/__snapshots__/index_spec.js.snap
@@ -70,6 +70,13 @@ exports[`Design management design index page renders design index 1`] = `
<!---->
+ <design-note-signed-out-stub
+ class="gl-mb-4"
+ isadddiscussion="true"
+ registerpath=""
+ signinpath=""
+ />
+
<design-discussion-stub
data-testid="unresolved-discussion"
designid="gid::/gitlab/Design/1"
@@ -77,6 +84,8 @@ exports[`Design management design index page renders design index 1`] = `
discussionwithopenform=""
markdownpreviewpath="/project-path/preview_markdown?target_type=Issue"
noteableid="gid::/gitlab/Design/1"
+ registerpath=""
+ signinpath=""
/>
<gl-button-stub
@@ -126,6 +135,8 @@ exports[`Design management design index page renders design index 1`] = `
discussionwithopenform=""
markdownpreviewpath="/project-path/preview_markdown?target_type=Issue"
noteableid="gid::/gitlab/Design/1"
+ registerpath=""
+ signinpath=""
/>
</gl-collapse-stub>
@@ -231,14 +242,14 @@ exports[`Design management design index page with error GlAlert is rendered in c
participants="[object Object]"
/>
- <h2
- class="new-discussion-disclaimer gl-font-base gl-m-0 gl-mb-4"
- data-testid="new-discussion-disclaimer"
- >
-
- Click the image where you'd like to start a new discussion
-
- </h2>
+ <!---->
+
+ <design-note-signed-out-stub
+ class="gl-mb-4"
+ isadddiscussion="true"
+ registerpath=""
+ signinpath=""
+ />
<!---->
diff --git a/spec/frontend/design_management/pages/design/index_spec.js b/spec/frontend/design_management/pages/design/index_spec.js
index 6ce384b4869..98e2313e9f2 100644
--- a/spec/frontend/design_management/pages/design/index_spec.js
+++ b/spec/frontend/design_management/pages/design/index_spec.js
@@ -317,6 +317,8 @@ describe('Design management design index page', () => {
describe('when no design exists for given version', () => {
it('redirects to /designs', () => {
createComponent({ loading: true });
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
allVersions: mockAllVersions,
});
diff --git a/spec/frontend/design_management/pages/index_spec.js b/spec/frontend/design_management/pages/index_spec.js
index 427161a391b..dd0f7972553 100644
--- a/spec/frontend/design_management/pages/index_spec.js
+++ b/spec/frontend/design_management/pages/index_spec.js
@@ -91,6 +91,8 @@ const designToMove = {
};
describe('Design management index page', () => {
+ const registerPath = '/users/sign_up?redirect_to_referer=yes';
+ const signInPath = '/users/sign_in?redirect_to_referer=yes';
let mutate;
let wrapper;
let fakeApollo;
@@ -164,6 +166,8 @@ describe('Design management index page', () => {
provide: {
projectPath: 'project-path',
issueIid: '1',
+ registerPath,
+ signInPath,
},
});
}
@@ -186,6 +190,10 @@ describe('Design management index page', () => {
apolloProvider: fakeApollo,
router,
stubs: { VueDraggable },
+ provide: {
+ registerPath,
+ signInPath,
+ },
});
}
@@ -204,6 +212,8 @@ describe('Design management index page', () => {
it('renders error', async () => {
createComponent();
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({ error: true });
await nextTick();
@@ -381,6 +391,8 @@ describe('Design management index page', () => {
it('updates state appropriately after upload complete', async () => {
createComponent({ stubs: { GlEmptyState } });
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({ filesToBeSaved: [{ name: 'test' }] });
wrapper.vm.onUploadDesignDone(designUploadMutationCreatedResponse);
@@ -393,6 +405,8 @@ describe('Design management index page', () => {
it('updates state appropriately after upload error', async () => {
createComponent({ stubs: { GlEmptyState } });
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({ filesToBeSaved: [{ name: 'test' }] });
wrapper.vm.onUploadDesignError();
diff --git a/spec/frontend/diffs/components/image_diff_overlay_spec.js b/spec/frontend/diffs/components/image_diff_overlay_spec.js
index 47b144b2387..8c1a8041f6c 100644
--- a/spec/frontend/diffs/components/image_diff_overlay_spec.js
+++ b/spec/frontend/diffs/components/image_diff_overlay_spec.js
@@ -6,8 +6,8 @@ import { imageDiffDiscussions } from '../mock_data/diff_discussions';
describe('Diffs image diff overlay component', () => {
const dimensions = {
- width: 100,
- height: 200,
+ width: 99.9,
+ height: 199.5,
};
let wrapper;
let dispatch;
@@ -38,7 +38,6 @@ describe('Diffs image diff overlay component', () => {
afterEach(() => {
wrapper.destroy();
- wrapper = null;
});
it('renders comment badges', () => {
@@ -81,17 +80,21 @@ describe('Diffs image diff overlay component', () => {
it('dispatches openDiffFileCommentForm when clicking overlay', () => {
createComponent({ canComment: true });
- wrapper.find('.js-add-image-diff-note-button').trigger('click', { offsetX: 0, offsetY: 0 });
+ wrapper.find('.js-add-image-diff-note-button').trigger('click', { offsetX: 1.2, offsetY: 3.8 });
expect(dispatch).toHaveBeenCalledWith('diffs/openDiffFileCommentForm', {
fileHash: 'ABC',
- x: 0,
- y: 0,
+ x: 1,
+ y: 4,
width: 100,
height: 200,
- xPercent: 0,
- yPercent: 0,
+ xPercent: expect.any(Number),
+ yPercent: expect.any(Number),
});
+
+ const { xPercent, yPercent } = dispatch.mock.calls[0][1];
+ expect(xPercent).toBeCloseTo(0.6);
+ expect(yPercent).toBeCloseTo(1.9);
});
describe('toggle discussion', () => {
diff --git a/spec/frontend/editor/source_editor_spec.js b/spec/frontend/editor/source_editor_spec.js
index bc53202c919..049cab3a83b 100644
--- a/spec/frontend/editor/source_editor_spec.js
+++ b/spec/frontend/editor/source_editor_spec.js
@@ -342,27 +342,30 @@ describe('Base editor', () => {
describe('implementation', () => {
let instance;
- beforeEach(() => {
- instance = editor.createInstance({ el: editorEl, blobPath, blobContent });
- });
it('correctly proxies value from the model', () => {
+ instance = editor.createInstance({ el: editorEl, blobPath, blobContent });
expect(instance.getValue()).toBe(blobContent);
});
- it('emits the EDITOR_READY_EVENT event after setting up the instance', () => {
+ it('emits the EDITOR_READY_EVENT event passing the instance after setting it up', () => {
jest.spyOn(monacoEditor, 'create').mockImplementation(() => {
return {
setModel: jest.fn(),
onDidDispose: jest.fn(),
layout: jest.fn(),
+ dispose: jest.fn(),
};
});
- const eventSpy = jest.fn();
+ let passedInstance;
+ const eventSpy = jest.fn().mockImplementation((ev) => {
+ passedInstance = ev.detail.instance;
+ });
editorEl.addEventListener(EDITOR_READY_EVENT, eventSpy);
expect(eventSpy).not.toHaveBeenCalled();
- editor.createInstance({ el: editorEl });
+ instance = editor.createInstance({ el: editorEl });
expect(eventSpy).toHaveBeenCalled();
+ expect(passedInstance).toBe(instance);
});
});
diff --git a/spec/frontend/emoji/components/category_spec.js b/spec/frontend/emoji/components/category_spec.js
index afd36a1eb88..82dc0cdc250 100644
--- a/spec/frontend/emoji/components/category_spec.js
+++ b/spec/frontend/emoji/components/category_spec.js
@@ -26,6 +26,8 @@ describe('Emoji category component', () => {
});
it('renders group', async () => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
await wrapper.setData({ renderGroup: true });
expect(wrapper.find(EmojiGroup).attributes('rendergroup')).toBe('true');
diff --git a/spec/frontend/emoji/components/emoji_list_spec.js b/spec/frontend/emoji/components/emoji_list_spec.js
index 9dc73ef191e..a72ba614d9f 100644
--- a/spec/frontend/emoji/components/emoji_list_spec.js
+++ b/spec/frontend/emoji/components/emoji_list_spec.js
@@ -28,6 +28,8 @@ async function factory(render, propsData = { searchValue: '' }) {
await nextTick();
if (render) {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({ render: true });
// Wait for component to render
diff --git a/spec/frontend/environments/confirm_rollback_modal_spec.js b/spec/frontend/environments/confirm_rollback_modal_spec.js
index b699f953945..b8dcb7c0d08 100644
--- a/spec/frontend/environments/confirm_rollback_modal_spec.js
+++ b/spec/frontend/environments/confirm_rollback_modal_spec.js
@@ -26,7 +26,7 @@ describe('Confirm Rollback Modal Component', () => {
commit: {
shortId: 'abc0123',
},
- 'last?': true,
+ isLast: true,
},
modalId: 'test',
};
@@ -145,7 +145,7 @@ describe('Confirm Rollback Modal Component', () => {
...environment,
lastDeployment: {
...environment.lastDeployment,
- 'last?': false,
+ isLast: false,
},
},
hasMultipleCommits,
@@ -167,7 +167,7 @@ describe('Confirm Rollback Modal Component', () => {
...environment,
lastDeployment: {
...environment.lastDeployment,
- 'last?': false,
+ isLast: false,
},
},
hasMultipleCommits,
@@ -191,7 +191,7 @@ describe('Confirm Rollback Modal Component', () => {
...environment,
lastDeployment: {
...environment.lastDeployment,
- 'last?': true,
+ isLast: true,
},
},
hasMultipleCommits,
diff --git a/spec/frontend/environments/deployment_spec.js b/spec/frontend/environments/deployment_spec.js
new file mode 100644
index 00000000000..37209bdc86c
--- /dev/null
+++ b/spec/frontend/environments/deployment_spec.js
@@ -0,0 +1,29 @@
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import Deployment from '~/environments/components/deployment.vue';
+import DeploymentStatusBadge from '~/environments/components/deployment_status_badge.vue';
+import { resolvedEnvironment } from './graphql/mock_data';
+
+describe('~/environments/components/deployment.vue', () => {
+ let wrapper;
+
+ const createWrapper = ({ propsData = {} } = {}) =>
+ mountExtended(Deployment, {
+ propsData: {
+ deployment: resolvedEnvironment.lastDeployment,
+ ...propsData,
+ },
+ });
+
+ afterEach(() => {
+ wrapper?.destroy();
+ });
+
+ describe('status', () => {
+ it('should pass the deployable status to the badge', () => {
+ wrapper = createWrapper();
+ expect(wrapper.findComponent(DeploymentStatusBadge).props('status')).toBe(
+ resolvedEnvironment.lastDeployment.status,
+ );
+ });
+ });
+});
diff --git a/spec/frontend/environments/deployment_status_badge_spec.js b/spec/frontend/environments/deployment_status_badge_spec.js
new file mode 100644
index 00000000000..02aae57396a
--- /dev/null
+++ b/spec/frontend/environments/deployment_status_badge_spec.js
@@ -0,0 +1,42 @@
+import { GlBadge } from '@gitlab/ui';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import { s__ } from '~/locale';
+import DeploymentStatusBadge from '~/environments/components/deployment_status_badge.vue';
+
+describe('~/environments/components/deployment_status_badge.vue', () => {
+ let wrapper;
+
+ const createWrapper = ({ propsData = {} } = {}) =>
+ mountExtended(DeploymentStatusBadge, {
+ propsData,
+ });
+
+ describe.each`
+ status | text | variant | icon
+ ${'created'} | ${s__('Deployment|Created')} | ${'neutral'} | ${'status_created'}
+ ${'running'} | ${s__('Deployment|Running')} | ${'info'} | ${'status_running'}
+ ${'success'} | ${s__('Deployment|Success')} | ${'success'} | ${'status_success'}
+ ${'failed'} | ${s__('Deployment|Failed')} | ${'danger'} | ${'status_failed'}
+ ${'canceled'} | ${s__('Deployment|Cancelled')} | ${'neutral'} | ${'status_canceled'}
+ ${'skipped'} | ${s__('Deployment|Skipped')} | ${'neutral'} | ${'status_skipped'}
+ ${'blocked'} | ${s__('Deployment|Waiting')} | ${'neutral'} | ${'status_manual'}
+ `('$status', ({ status, text, variant, icon }) => {
+ let badge;
+
+ beforeEach(() => {
+ wrapper = createWrapper({ propsData: { status } });
+ badge = wrapper.findComponent(GlBadge);
+ });
+
+ it(`sets the text to ${text}`, () => {
+ expect(wrapper.text()).toBe(text);
+ });
+
+ it(`sets the variant to ${variant}`, () => {
+ expect(badge.props('variant')).toBe(variant);
+ });
+ it(`sets the icon to ${icon}`, () => {
+ expect(badge.props('icon')).toBe(icon);
+ });
+ });
+});
diff --git a/spec/frontend/environments/environment_actions_spec.js b/spec/frontend/environments/environment_actions_spec.js
index db78a6b0cdd..1b68a692db8 100644
--- a/spec/frontend/environments/environment_actions_spec.js
+++ b/spec/frontend/environments/environment_actions_spec.js
@@ -1,9 +1,13 @@
import { GlDropdown, GlDropdownItem, GlLoadingIcon, GlIcon } from '@gitlab/ui';
import { shallowMount, mount } from '@vue/test-utils';
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
import { TEST_HOST } from 'helpers/test_constants';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import EnvironmentActions from '~/environments/components/environment_actions.vue';
import eventHub from '~/environments/event_hub';
+import actionMutation from '~/environments/graphql/mutations/action.mutation.graphql';
+import createMockApollo from 'helpers/mock_apollo_helper';
const scheduledJobAction = {
name: 'scheduled action',
@@ -25,12 +29,13 @@ describe('EnvironmentActions Component', () => {
const findEnvironmentActionsButton = () =>
wrapper.find('[data-testid="environment-actions-button"]');
- function createComponent(props, { mountFn = shallowMount } = {}) {
+ function createComponent(props, { mountFn = shallowMount, options = {} } = {}) {
wrapper = mountFn(EnvironmentActions, {
propsData: { actions: [], ...props },
directives: {
GlTooltip: createMockDirective(),
},
+ ...options,
});
}
@@ -150,4 +155,32 @@ describe('EnvironmentActions Component', () => {
expect(findDropdownItem(expiredJobAction).text()).toContain('00:00:00');
});
});
+
+ describe('graphql', () => {
+ Vue.use(VueApollo);
+
+ const action = {
+ name: 'bar',
+ play_path: 'https://gitlab.com/play',
+ };
+
+ let mockApollo;
+
+ beforeEach(() => {
+ mockApollo = createMockApollo();
+ createComponent(
+ { actions: [action], graphql: true },
+ { options: { apolloProvider: mockApollo } },
+ );
+ });
+
+ it('should trigger a graphql mutation on click', () => {
+ jest.spyOn(mockApollo.defaultClient, 'mutate');
+ findDropdownItem(action).vm.$emit('click');
+ expect(mockApollo.defaultClient.mutate).toHaveBeenCalledWith({
+ mutation: actionMutation,
+ variables: { action },
+ });
+ });
+ });
});
diff --git a/spec/frontend/environments/environment_stop_spec.js b/spec/frontend/environments/environment_stop_spec.js
index dff444b79f3..358abca2f77 100644
--- a/spec/frontend/environments/environment_stop_spec.js
+++ b/spec/frontend/environments/environment_stop_spec.js
@@ -1,38 +1,80 @@
import { GlButton } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import $ from 'jquery';
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import setEnvironmentToStopMutation from '~/environments/graphql/mutations/set_environment_to_stop.mutation.graphql';
+import isEnvironmentStoppingQuery from '~/environments/graphql/queries/is_environment_stopping.query.graphql';
import StopComponent from '~/environments/components/environment_stop.vue';
import eventHub from '~/environments/event_hub';
-
-$.fn.tooltip = () => {};
+import createMockApollo from 'helpers/mock_apollo_helper';
+import { resolvedEnvironment } from './graphql/mock_data';
describe('Stop Component', () => {
let wrapper;
- const createWrapper = () => {
+ const createWrapper = (props = {}, options = {}) => {
wrapper = shallowMount(StopComponent, {
propsData: {
environment: {},
+ ...props,
},
+ ...options,
});
};
const findButton = () => wrapper.find(GlButton);
- beforeEach(() => {
- jest.spyOn(window, 'confirm');
+ describe('eventHub', () => {
+ beforeEach(() => {
+ createWrapper();
+ });
- createWrapper();
- });
+ it('should render a button to stop the environment', () => {
+ expect(findButton().exists()).toBe(true);
+ expect(wrapper.attributes('title')).toEqual('Stop environment');
+ });
- it('should render a button to stop the environment', () => {
- expect(findButton().exists()).toBe(true);
- expect(wrapper.attributes('title')).toEqual('Stop environment');
+ it('emits requestStopEnvironment in the event hub when button is clicked', () => {
+ jest.spyOn(eventHub, '$emit');
+ findButton().vm.$emit('click');
+ expect(eventHub.$emit).toHaveBeenCalledWith('requestStopEnvironment', wrapper.vm.environment);
+ });
});
- it('emits requestStopEnvironment in the event hub when button is clicked', () => {
- jest.spyOn(eventHub, '$emit');
- findButton().vm.$emit('click');
- expect(eventHub.$emit).toHaveBeenCalledWith('requestStopEnvironment', wrapper.vm.environment);
+ describe('graphql', () => {
+ Vue.use(VueApollo);
+ let mockApollo;
+
+ beforeEach(() => {
+ mockApollo = createMockApollo();
+ mockApollo.clients.defaultClient.writeQuery({
+ query: isEnvironmentStoppingQuery,
+ variables: { environment: resolvedEnvironment },
+ data: { isEnvironmentStopping: true },
+ });
+
+ createWrapper(
+ { graphql: true, environment: resolvedEnvironment },
+ { apolloProvider: mockApollo },
+ );
+ });
+
+ it('should render a button to stop the environment', () => {
+ expect(findButton().exists()).toBe(true);
+ expect(wrapper.attributes('title')).toEqual('Stop environment');
+ });
+
+ it('sets the environment to stop on click', () => {
+ jest.spyOn(mockApollo.defaultClient, 'mutate');
+ findButton().vm.$emit('click');
+ expect(mockApollo.defaultClient.mutate).toHaveBeenCalledWith({
+ mutation: setEnvironmentToStopMutation,
+ variables: { environment: resolvedEnvironment },
+ });
+ });
+
+ it('should show a loading icon if the environment is currently stopping', async () => {
+ expect(findButton().props('loading')).toBe(true);
+ });
});
});
diff --git a/spec/frontend/environments/graphql/mock_data.js b/spec/frontend/environments/graphql/mock_data.js
index e75d3ac0321..fce30973547 100644
--- a/spec/frontend/environments/graphql/mock_data.js
+++ b/spec/frontend/environments/graphql/mock_data.js
@@ -477,7 +477,141 @@ export const resolvedEnvironment = {
externalUrl: 'https://example.org',
environmentType: 'review',
nameWithoutType: 'hello',
- lastDeployment: null,
+ lastDeployment: {
+ id: 78,
+ iid: 24,
+ sha: 'f3ba6dd84f8f891373e9b869135622b954852db1',
+ ref: { name: 'main', refPath: '/h5bp/html5-boilerplate/-/tree/main' },
+ status: 'success',
+ createdAt: '2022-01-07T15:47:27.415Z',
+ deployedAt: '2022-01-07T15:47:32.450Z',
+ tag: false,
+ isLast: true,
+ user: {
+ id: 1,
+ username: 'root',
+ name: 'Administrator',
+ state: 'active',
+ avatarUrl:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ webUrl: 'http://gck.test:3000/root',
+ showStatus: false,
+ path: '/root',
+ },
+ deployable: {
+ id: 1014,
+ name: 'deploy-prod',
+ started: '2022-01-07T15:47:31.037Z',
+ complete: true,
+ archived: false,
+ buildPath: '/h5bp/html5-boilerplate/-/jobs/1014',
+ retryPath: '/h5bp/html5-boilerplate/-/jobs/1014/retry',
+ playable: false,
+ scheduled: false,
+ createdAt: '2022-01-07T15:47:27.404Z',
+ updatedAt: '2022-01-07T15:47:32.341Z',
+ status: {
+ icon: 'status_success',
+ text: 'passed',
+ label: 'passed',
+ group: 'success',
+ tooltip: 'passed',
+ hasDetails: true,
+ detailsPath: '/h5bp/html5-boilerplate/-/jobs/1014',
+ illustration: {
+ image:
+ '/assets/illustrations/skipped-job_empty-29a8a37d8a61d1b6f68cf3484f9024e53cd6eb95e28eae3554f8011a1146bf27.svg',
+ size: 'svg-430',
+ title: 'This job does not have a trace.',
+ },
+ favicon:
+ '/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png',
+ action: {
+ icon: 'retry',
+ title: 'Retry',
+ path: '/h5bp/html5-boilerplate/-/jobs/1014/retry',
+ method: 'post',
+ buttonTitle: 'Retry this job',
+ },
+ },
+ },
+ commit: {
+ id: 'f3ba6dd84f8f891373e9b869135622b954852db1',
+ shortId: 'f3ba6dd8',
+ createdAt: '2022-01-07T15:47:26.000+00:00',
+ parentIds: ['3213b6ac17afab99be37d5d38f38c6c8407387cc'],
+ title: 'Update .gitlab-ci.yml file',
+ message: 'Update .gitlab-ci.yml file',
+ authorName: 'Administrator',
+ authorEmail: 'admin@example.com',
+ authoredDate: '2022-01-07T15:47:26.000+00:00',
+ committerName: 'Administrator',
+ committerEmail: 'admin@example.com',
+ committedDate: '2022-01-07T15:47:26.000+00:00',
+ trailers: {},
+ webUrl:
+ 'http://gck.test:3000/h5bp/html5-boilerplate/-/commit/f3ba6dd84f8f891373e9b869135622b954852db1',
+ author: {
+ id: 1,
+ username: 'root',
+ name: 'Administrator',
+ state: 'active',
+ avatarUrl:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ webUrl: 'http://gck.test:3000/root',
+ showStatus: false,
+ path: '/root',
+ },
+ authorGravatarUrl:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ commitUrl:
+ 'http://gck.test:3000/h5bp/html5-boilerplate/-/commit/f3ba6dd84f8f891373e9b869135622b954852db1',
+ commitPath: '/h5bp/html5-boilerplate/-/commit/f3ba6dd84f8f891373e9b869135622b954852db1',
+ },
+ manualActions: [
+ {
+ id: 1015,
+ name: 'deploy-staging',
+ started: null,
+ complete: false,
+ archived: false,
+ buildPath: '/h5bp/html5-boilerplate/-/jobs/1015',
+ playPath: '/h5bp/html5-boilerplate/-/jobs/1015/play',
+ playable: true,
+ scheduled: false,
+ createdAt: '2022-01-07T15:47:27.422Z',
+ updatedAt: '2022-01-07T15:47:28.557Z',
+ status: {
+ icon: 'status_manual',
+ text: 'manual',
+ label: 'manual play action',
+ group: 'manual',
+ tooltip: 'manual action',
+ hasDetails: true,
+ detailsPath: '/h5bp/html5-boilerplate/-/jobs/1015',
+ illustration: {
+ image:
+ '/assets/illustrations/manual_action-c55aee2c5f9ebe9f72751480af8bb307be1a6f35552f344cc6d1bf979d3422f6.svg',
+ size: 'svg-394',
+ title: 'This job requires a manual action',
+ content:
+ 'This job requires manual intervention to start. Before starting this job, you can add variables below for last-minute configuration changes.',
+ },
+ favicon:
+ '/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png',
+ action: {
+ icon: 'play',
+ title: 'Play',
+ path: '/h5bp/html5-boilerplate/-/jobs/1015/play',
+ method: 'post',
+ buttonTitle: 'Trigger this manual action',
+ },
+ },
+ },
+ ],
+ scheduledActions: [],
+ cluster: null,
+ },
hasStopAction: false,
rolloutStatus: null,
environmentPath: '/h5bp/html5-boilerplate/-/environments/41',
diff --git a/spec/frontend/environments/graphql/resolvers_spec.js b/spec/frontend/environments/graphql/resolvers_spec.js
index d8d26b74504..6b53dc24f0f 100644
--- a/spec/frontend/environments/graphql/resolvers_spec.js
+++ b/spec/frontend/environments/graphql/resolvers_spec.js
@@ -1,8 +1,10 @@
import MockAdapter from 'axios-mock-adapter';
+import { s__ } from '~/locale';
import axios from '~/lib/utils/axios_utils';
import { resolvers } from '~/environments/graphql/resolvers';
import environmentToRollback from '~/environments/graphql/queries/environment_to_rollback.query.graphql';
import environmentToDelete from '~/environments/graphql/queries/environment_to_delete.query.graphql';
+import environmentToStopQuery from '~/environments/graphql/queries/environment_to_stop.query.graphql';
import createMockApollo from 'helpers/mock_apollo_helper';
import pollIntervalQuery from '~/environments/graphql/queries/poll_interval.query.graphql';
import pageInfoQuery from '~/environments/graphql/queries/page_info.query.graphql';
@@ -210,4 +212,36 @@ describe('~/frontend/environments/graphql/resolvers', () => {
});
});
});
+ describe('setEnvironmentToStop', () => {
+ it('should write the given environment to the cache', () => {
+ localState.client.writeQuery = jest.fn();
+ mockResolvers.Mutation.setEnvironmentToStop(
+ null,
+ { environment: resolvedEnvironment },
+ localState,
+ );
+
+ expect(localState.client.writeQuery).toHaveBeenCalledWith({
+ query: environmentToStopQuery,
+ data: { environmentToStop: resolvedEnvironment },
+ });
+ });
+ });
+ describe('action', () => {
+ it('should POST to the given path', async () => {
+ mock.onPost(ENDPOINT).reply(200);
+ const errors = await mockResolvers.Mutation.action(null, { action: { playPath: ENDPOINT } });
+
+ expect(errors).toEqual({ __typename: 'LocalEnvironmentErrors', errors: [] });
+ });
+ it('should return a nice error message on fail', async () => {
+ mock.onPost(ENDPOINT).reply(500);
+ const errors = await mockResolvers.Mutation.action(null, { action: { playPath: ENDPOINT } });
+
+ expect(errors).toEqual({
+ __typename: 'LocalEnvironmentErrors',
+ errors: [s__('Environments|An error occurred while making the request.')],
+ });
+ });
+ });
});
diff --git a/spec/frontend/environments/new_environment_folder_spec.js b/spec/frontend/environments/new_environment_folder_spec.js
index 27d27d5869a..6823c88a5a1 100644
--- a/spec/frontend/environments/new_environment_folder_spec.js
+++ b/spec/frontend/environments/new_environment_folder_spec.js
@@ -1,10 +1,13 @@
import VueApollo from 'vue-apollo';
-import Vue from 'vue';
+import Vue, { nextTick } from 'vue';
import { GlCollapse, GlIcon } from '@gitlab/ui';
+import waitForPromises from 'helpers/wait_for_promises';
import createMockApollo from 'helpers/mock_apollo_helper';
import { mountExtended } from 'helpers/vue_test_utils_helper';
+import { stubTransition } from 'helpers/stub_transition';
import { __, s__ } from '~/locale';
import EnvironmentsFolder from '~/environments/components/new_environment_folder.vue';
+import EnvironmentItem from '~/environments/components/new_environment_item.vue';
import { resolvedEnvironmentsApp, resolvedFolder } from './graphql/mock_data';
Vue.use(VueApollo);
@@ -25,13 +28,20 @@ describe('~/environments/components/new_environments_folder.vue', () => {
};
const createWrapper = (propsData, apolloProvider) =>
- mountExtended(EnvironmentsFolder, { apolloProvider, propsData });
+ mountExtended(EnvironmentsFolder, {
+ apolloProvider,
+ propsData,
+ stubs: { transition: stubTransition() },
+ });
- beforeEach(() => {
+ beforeEach(async () => {
environmentFolderMock = jest.fn();
[nestedEnvironment] = resolvedEnvironmentsApp.environments;
environmentFolderMock.mockReturnValue(resolvedFolder);
wrapper = createWrapper({ nestedEnvironment }, createApolloProvider());
+
+ await nextTick();
+ await waitForPromises();
folderName = wrapper.findByText(nestedEnvironment.name);
button = wrapper.findByRole('button', { name: __('Expand') });
});
@@ -57,7 +67,8 @@ describe('~/environments/components/new_environments_folder.vue', () => {
const link = findLink();
expect(collapse.attributes('visible')).toBeUndefined();
- expect(icons.wrappers.map((i) => i.props('name'))).toEqual(['angle-right', 'folder-o']);
+ const iconNames = icons.wrappers.map((i) => i.props('name')).slice(0, 2);
+ expect(iconNames).toEqual(['angle-right', 'folder-o']);
expect(folderName.classes('gl-font-weight-bold')).toBe(false);
expect(link.exists()).toBe(false);
});
@@ -68,10 +79,21 @@ describe('~/environments/components/new_environments_folder.vue', () => {
const link = findLink();
expect(button.attributes('aria-label')).toBe(__('Collapse'));
- expect(collapse.attributes('visible')).toBe('true');
- expect(icons.wrappers.map((i) => i.props('name'))).toEqual(['angle-down', 'folder-open']);
+ expect(collapse.attributes('visible')).toBe('visible');
+ const iconNames = icons.wrappers.map((i) => i.props('name')).slice(0, 2);
+ expect(iconNames).toEqual(['angle-down', 'folder-open']);
expect(folderName.classes('gl-font-weight-bold')).toBe(true);
expect(link.attributes('href')).toBe(nestedEnvironment.latest.folderPath);
});
+
+ it('displays all environments when opened', async () => {
+ await button.trigger('click');
+
+ const names = resolvedFolder.environments.map((e) =>
+ expect.stringMatching(e.nameWithoutType),
+ );
+ const environments = wrapper.findAllComponents(EnvironmentItem).wrappers.map((w) => w.text());
+ expect(environments).toEqual(expect.arrayContaining(names));
+ });
});
});
diff --git a/spec/frontend/environments/new_environment_item_spec.js b/spec/frontend/environments/new_environment_item_spec.js
new file mode 100644
index 00000000000..244aef5c43b
--- /dev/null
+++ b/spec/frontend/environments/new_environment_item_spec.js
@@ -0,0 +1,341 @@
+import VueApollo from 'vue-apollo';
+import Vue from 'vue';
+import { GlCollapse, GlIcon } from '@gitlab/ui';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import { stubTransition } from 'helpers/stub_transition';
+import { __, s__ } from '~/locale';
+import EnvironmentItem from '~/environments/components/new_environment_item.vue';
+import Deployment from '~/environments/components/deployment.vue';
+import { resolvedEnvironment } from './graphql/mock_data';
+
+Vue.use(VueApollo);
+
+describe('~/environments/components/new_environment_item.vue', () => {
+ let wrapper;
+
+ const createApolloProvider = () => {
+ return createMockApollo();
+ };
+
+ const createWrapper = ({ propsData = {}, apolloProvider } = {}) =>
+ mountExtended(EnvironmentItem, {
+ apolloProvider,
+ propsData: { environment: resolvedEnvironment, ...propsData },
+ stubs: { transition: stubTransition() },
+ });
+
+ const findDeployment = () => wrapper.findComponent(Deployment);
+
+ afterEach(() => {
+ wrapper?.destroy();
+ });
+
+ it('displays the name when not in a folder', () => {
+ wrapper = createWrapper({ apolloProvider: createApolloProvider() });
+
+ const name = wrapper.findByRole('link', { name: resolvedEnvironment.name });
+ expect(name.exists()).toBe(true);
+ });
+
+ it('displays the name minus the folder prefix when in a folder', () => {
+ wrapper = createWrapper({
+ propsData: { inFolder: true },
+ apolloProvider: createApolloProvider(),
+ });
+
+ const name = wrapper.findByRole('link', { name: resolvedEnvironment.nameWithoutType });
+ expect(name.exists()).toBe(true);
+ });
+
+ it('truncates the name if it is very long', () => {
+ const environment = {
+ ...resolvedEnvironment,
+ name:
+ 'this is a really long name that should be truncated because otherwise it would look strange in the UI',
+ };
+ wrapper = createWrapper({ propsData: { environment }, apolloProvider: createApolloProvider() });
+
+ const name = wrapper.findByRole('link', {
+ name: (text) => environment.name.startsWith(text.slice(0, -1)),
+ });
+ expect(name.exists()).toBe(true);
+ expect(name.text()).toHaveLength(80);
+ });
+
+ describe('url', () => {
+ it('shows a link for the url if one is present', () => {
+ wrapper = createWrapper({ apolloProvider: createApolloProvider() });
+
+ const url = wrapper.findByRole('link', { name: s__('Environments|Open live environment') });
+
+ expect(url.attributes('href')).toEqual(resolvedEnvironment.externalUrl);
+ });
+
+ it('does not show a link for the url if one is missing', () => {
+ wrapper = createWrapper({
+ propsData: { environment: { ...resolvedEnvironment, externalUrl: '' } },
+ apolloProvider: createApolloProvider(),
+ });
+
+ const url = wrapper.findByRole('link', { name: s__('Environments|Open live environment') });
+
+ expect(url.exists()).toBe(false);
+ });
+ });
+
+ describe('actions', () => {
+ it('shows a dropdown if there are actions to perform', () => {
+ wrapper = createWrapper({ apolloProvider: createApolloProvider() });
+
+ const actions = wrapper.findByRole('button', { name: __('Deploy to...') });
+
+ expect(actions.exists()).toBe(true);
+ });
+
+ it('does not show a dropdown if there are no actions to perform', () => {
+ wrapper = createWrapper({
+ propsData: {
+ environment: {
+ ...resolvedEnvironment,
+ lastDeployment: null,
+ },
+ apolloProvider: createApolloProvider(),
+ },
+ });
+
+ const actions = wrapper.findByRole('button', { name: __('Deploy to...') });
+
+ expect(actions.exists()).toBe(false);
+ });
+
+ it('passes all the actions down to the action component', () => {
+ wrapper = createWrapper({ apolloProvider: createApolloProvider() });
+
+ const action = wrapper.findByRole('menuitem', { name: 'deploy-staging' });
+
+ expect(action.exists()).toBe(true);
+ });
+ });
+
+ describe('stop', () => {
+ it('shows a buton to stop the environment if the environment is available', () => {
+ wrapper = createWrapper({ apolloProvider: createApolloProvider() });
+
+ const stop = wrapper.findByRole('button', { name: s__('Environments|Stop environment') });
+
+ expect(stop.exists()).toBe(true);
+ });
+
+ it('does not show a buton to stop the environment if the environment is stopped', () => {
+ wrapper = createWrapper({
+ propsData: { environment: { ...resolvedEnvironment, canStop: false } },
+ apolloProvider: createApolloProvider(),
+ });
+
+ const stop = wrapper.findByRole('button', { name: s__('Environments|Stop environment') });
+
+ expect(stop.exists()).toBe(false);
+ });
+ });
+
+ describe('rollback', () => {
+ it('shows the option to rollback/re-deploy if available', () => {
+ wrapper = createWrapper({ apolloProvider: createApolloProvider() });
+
+ const rollback = wrapper.findByRole('menuitem', {
+ name: s__('Environments|Re-deploy to environment'),
+ });
+
+ expect(rollback.exists()).toBe(true);
+ });
+
+ it('does not show the option to rollback/re-deploy if not available', () => {
+ wrapper = createWrapper({
+ propsData: { environment: { ...resolvedEnvironment, lastDeployment: null } },
+ apolloProvider: createApolloProvider(),
+ });
+
+ const rollback = wrapper.findByRole('menuitem', {
+ name: s__('Environments|Re-deploy to environment'),
+ });
+
+ expect(rollback.exists()).toBe(false);
+ });
+ });
+
+ describe('pin', () => {
+ it('shows the option to pin the environment if there is an autostop date', () => {
+ wrapper = createWrapper({
+ propsData: {
+ environment: { ...resolvedEnvironment, autoStopAt: new Date(Date.now() + 100000) },
+ },
+ apolloProvider: createApolloProvider(),
+ });
+
+ const rollback = wrapper.findByRole('menuitem', { name: __('Prevent auto-stopping') });
+
+ expect(rollback.exists()).toBe(true);
+ });
+
+ it('does not show the option to pin the environment if there is no autostop date', () => {
+ wrapper = createWrapper({ apolloProvider: createApolloProvider() });
+
+ const rollback = wrapper.findByRole('menuitem', { name: __('Prevent auto-stopping') });
+
+ expect(rollback.exists()).toBe(false);
+ });
+ });
+
+ describe('monitoring', () => {
+ it('shows the link to monitoring if metrics are set up', () => {
+ wrapper = createWrapper({
+ propsData: { environment: { ...resolvedEnvironment, metricsPath: '/metrics' } },
+ apolloProvider: createApolloProvider(),
+ });
+
+ const rollback = wrapper.findByRole('menuitem', { name: __('Monitoring') });
+
+ expect(rollback.exists()).toBe(true);
+ });
+
+ it('does not show the link to monitoring if metrics are not set up', () => {
+ wrapper = createWrapper({ apolloProvider: createApolloProvider() });
+
+ const rollback = wrapper.findByRole('menuitem', { name: __('Monitoring') });
+
+ expect(rollback.exists()).toBe(false);
+ });
+ });
+ describe('terminal', () => {
+ it('shows the link to the terminal if set up', () => {
+ wrapper = createWrapper({
+ propsData: { environment: { ...resolvedEnvironment, terminalPath: '/terminal' } },
+ apolloProvider: createApolloProvider(),
+ });
+
+ const rollback = wrapper.findByRole('menuitem', { name: __('Terminal') });
+
+ expect(rollback.exists()).toBe(true);
+ });
+
+ it('does not show the link to the terminal if not set up', () => {
+ wrapper = createWrapper({ apolloProvider: createApolloProvider() });
+
+ const rollback = wrapper.findByRole('menuitem', { name: __('Terminal') });
+
+ expect(rollback.exists()).toBe(false);
+ });
+ });
+
+ describe('delete', () => {
+ it('shows the button to delete the environment if possible', () => {
+ wrapper = createWrapper({
+ propsData: {
+ environment: { ...resolvedEnvironment, canDelete: true, deletePath: '/terminal' },
+ },
+ apolloProvider: createApolloProvider(),
+ });
+
+ const rollback = wrapper.findByRole('menuitem', {
+ name: s__('Environments|Delete environment'),
+ });
+
+ expect(rollback.exists()).toBe(true);
+ });
+
+ it('does not show the button to delete the environment if not possible', () => {
+ wrapper = createWrapper({ apolloProvider: createApolloProvider() });
+
+ const rollback = wrapper.findByRole('menuitem', {
+ name: s__('Environments|Delete environment'),
+ });
+
+ expect(rollback.exists()).toBe(false);
+ });
+ });
+
+ describe('collapse', () => {
+ let icon;
+ let collapse;
+ let button;
+ let environmentName;
+
+ beforeEach(() => {
+ wrapper = createWrapper({ apolloProvider: createApolloProvider() });
+ collapse = wrapper.findComponent(GlCollapse);
+ icon = wrapper.findComponent(GlIcon);
+ button = wrapper.findByRole('button', { name: __('Expand') });
+ environmentName = wrapper.findByText(resolvedEnvironment.name);
+ });
+
+ it('is collapsed by default', () => {
+ expect(collapse.attributes('visible')).toBeUndefined();
+ expect(icon.props('name')).toEqual('angle-right');
+ expect(environmentName.classes('gl-font-weight-bold')).toBe(false);
+ });
+
+ it('opens on click', async () => {
+ expect(findDeployment().isVisible()).toBe(false);
+
+ await button.trigger('click');
+
+ expect(button.attributes('aria-label')).toBe(__('Collapse'));
+ expect(collapse.attributes('visible')).toBe('visible');
+ expect(icon.props('name')).toEqual('angle-down');
+ expect(environmentName.classes('gl-font-weight-bold')).toBe(true);
+ expect(findDeployment().isVisible()).toBe(true);
+ });
+ });
+ describe('last deployment', () => {
+ it('should pass the last deployment to the deployment component when it exists', () => {
+ wrapper = createWrapper({ apolloProvider: createApolloProvider() });
+
+ const deployment = findDeployment();
+ expect(deployment.props('deployment')).toEqual(resolvedEnvironment.lastDeployment);
+ });
+ it('should not show the last deployment when it is missing', () => {
+ const environment = {
+ ...resolvedEnvironment,
+ lastDeployment: null,
+ };
+
+ wrapper = createWrapper({
+ propsData: { environment },
+ apolloProvider: createApolloProvider(),
+ });
+
+ const deployment = findDeployment();
+ expect(deployment.exists()).toBe(false);
+ });
+ });
+
+ describe('upcoming deployment', () => {
+ it('should pass the upcoming deployment to the deployment component when it exists', () => {
+ const upcomingDeployment = resolvedEnvironment.lastDeployment;
+ const environment = { ...resolvedEnvironment, lastDeployment: null, upcomingDeployment };
+ wrapper = createWrapper({
+ propsData: { environment },
+ apolloProvider: createApolloProvider(),
+ });
+
+ const deployment = findDeployment();
+ expect(deployment.props('deployment')).toEqual(upcomingDeployment);
+ });
+ it('should not show the upcoming deployment when it is missing', () => {
+ const environment = {
+ ...resolvedEnvironment,
+ lastDeployment: null,
+ upcomingDeployment: null,
+ };
+
+ wrapper = createWrapper({
+ propsData: { environment },
+ apolloProvider: createApolloProvider(),
+ });
+
+ const deployment = findDeployment();
+ expect(deployment.exists()).toBe(false);
+ });
+ });
+});
diff --git a/spec/frontend/environments/new_environments_app_spec.js b/spec/frontend/environments/new_environments_app_spec.js
index 1e9bd4d64c9..c9eccc26694 100644
--- a/spec/frontend/environments/new_environments_app_spec.js
+++ b/spec/frontend/environments/new_environments_app_spec.js
@@ -8,7 +8,9 @@ import setWindowLocation from 'helpers/set_window_location_helper';
import { sprintf, __, s__ } from '~/locale';
import EnvironmentsApp from '~/environments/components/new_environments_app.vue';
import EnvironmentsFolder from '~/environments/components/new_environment_folder.vue';
-import { resolvedEnvironmentsApp, resolvedFolder } from './graphql/mock_data';
+import EnvironmentsItem from '~/environments/components/new_environment_item.vue';
+import StopEnvironmentModal from '~/environments/components/stop_environment_modal.vue';
+import { resolvedEnvironmentsApp, resolvedFolder, resolvedEnvironment } from './graphql/mock_data';
Vue.use(VueApollo);
@@ -17,6 +19,7 @@ describe('~/environments/components/new_environments_app.vue', () => {
let environmentAppMock;
let environmentFolderMock;
let paginationMock;
+ let environmentToStopMock;
const createApolloProvider = () => {
const mockResolvers = {
@@ -24,6 +27,7 @@ describe('~/environments/components/new_environments_app.vue', () => {
environmentApp: environmentAppMock,
folder: environmentFolderMock,
pageInfo: paginationMock,
+ environmentToStop: environmentToStopMock,
},
};
@@ -45,6 +49,7 @@ describe('~/environments/components/new_environments_app.vue', () => {
provide = {},
environmentsApp,
folder,
+ environmentToStop = {},
pageInfo = {
total: 20,
perPage: 5,
@@ -58,6 +63,7 @@ describe('~/environments/components/new_environments_app.vue', () => {
environmentAppMock.mockReturnValue(environmentsApp);
environmentFolderMock.mockReturnValue(folder);
paginationMock.mockReturnValue(pageInfo);
+ environmentToStopMock.mockReturnValue(environmentToStop);
const apolloProvider = createApolloProvider();
wrapper = createWrapper({ apolloProvider, provide });
@@ -68,6 +74,7 @@ describe('~/environments/components/new_environments_app.vue', () => {
beforeEach(() => {
environmentAppMock = jest.fn();
environmentFolderMock = jest.fn();
+ environmentToStopMock = jest.fn();
paginationMock = jest.fn();
});
@@ -87,6 +94,18 @@ describe('~/environments/components/new_environments_app.vue', () => {
expect(text).not.toContainEqual(expect.stringMatching('production'));
});
+ it('should show all the environments that are fetched', async () => {
+ await createWrapperWithMocked({
+ environmentsApp: resolvedEnvironmentsApp,
+ folder: resolvedFolder,
+ });
+
+ const text = wrapper.findAllComponents(EnvironmentsItem).wrappers.map((w) => w.text());
+
+ expect(text).not.toContainEqual(expect.stringMatching('review'));
+ expect(text).toContainEqual(expect.stringMatching('production'));
+ });
+
it('should show a button to create a new environment', async () => {
await createWrapperWithMocked({
environmentsApp: resolvedEnvironmentsApp,
@@ -168,13 +187,27 @@ describe('~/environments/components/new_environments_app.vue', () => {
expect(environmentAppMock).toHaveBeenCalledWith(
expect.anything(),
- expect.objectContaining({ scope: 'stopped' }),
+ expect.objectContaining({ scope: 'stopped', page: 1 }),
expect.anything(),
expect.anything(),
);
});
});
+ describe('modals', () => {
+ it('should pass the environment to stop to the stop environment modal', async () => {
+ await createWrapperWithMocked({
+ environmentsApp: resolvedEnvironmentsApp,
+ folder: resolvedFolder,
+ environmentToStop: resolvedEnvironment,
+ });
+
+ const modal = wrapper.findComponent(StopEnvironmentModal);
+
+ expect(modal.props('environment')).toMatchObject(resolvedEnvironment);
+ });
+ });
+
describe('pagination', () => {
it('should sync page from query params on load', async () => {
await createWrapperWithMocked({
diff --git a/spec/frontend/error_tracking/components/error_details_spec.js b/spec/frontend/error_tracking/components/error_details_spec.js
index 4e459d800e8..77f51193258 100644
--- a/spec/frontend/error_tracking/components/error_details_spec.js
+++ b/spec/frontend/error_tracking/components/error_details_spec.js
@@ -173,6 +173,8 @@ describe('ErrorDetails', () => {
beforeEach(() => {
mocks.$apollo.queries.error.loading = false;
mountComponent();
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
error: {
id: 'gid://gitlab/Gitlab::ErrorTracking::DetailedError/129381',
@@ -203,6 +205,8 @@ describe('ErrorDetails', () => {
const culprit = '<script>console.log("surprise!")</script>';
beforeEach(() => {
store.state.details.loadingStacktrace = false;
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
error: {
culprit,
@@ -222,6 +226,8 @@ describe('ErrorDetails', () => {
describe('Badges', () => {
it('should show language and error level badges', () => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
error: {
tags: { level: 'error', logger: 'ruby' },
@@ -233,6 +239,8 @@ describe('ErrorDetails', () => {
});
it('should NOT show the badge if the tag is not present', () => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
error: {
tags: { level: 'error' },
@@ -246,6 +254,8 @@ describe('ErrorDetails', () => {
it.each(Object.keys(severityLevel))(
'should set correct severity level variant for %s badge',
(level) => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
error: {
tags: { level: severityLevel[level] },
@@ -260,6 +270,8 @@ describe('ErrorDetails', () => {
);
it('should fallback for ERROR severityLevelVariant when severityLevel is unknown', () => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
error: {
tags: { level: 'someNewErrorLevel' },
@@ -408,6 +420,8 @@ describe('ErrorDetails', () => {
it('should show alert with closed issueId', () => {
const closedIssueId = 123;
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
isAlertVisible: true,
closedIssueId,
@@ -429,6 +443,8 @@ describe('ErrorDetails', () => {
describe('is present', () => {
beforeEach(() => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
error: {
gitlabIssuePath,
@@ -451,6 +467,8 @@ describe('ErrorDetails', () => {
describe('is not present', () => {
beforeEach(() => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
error: {
gitlabIssuePath: null,
@@ -480,6 +498,8 @@ describe('ErrorDetails', () => {
it('should display a link', () => {
mocks.$apollo.queries.error.loading = false;
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
error: {
gitlabCommit,
@@ -493,6 +513,8 @@ describe('ErrorDetails', () => {
it('should not display a link', () => {
mocks.$apollo.queries.error.loading = false;
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
error: {
gitlabCommit: null,
@@ -519,6 +541,8 @@ describe('ErrorDetails', () => {
it('should display links to Sentry', async () => {
mocks.$apollo.queries.error.loading = false;
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
await wrapper.setData({
error: {
firstReleaseVersion,
@@ -535,6 +559,8 @@ describe('ErrorDetails', () => {
it('should display links to GitLab when integrated', async () => {
mocks.$apollo.queries.error.loading = false;
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
await wrapper.setData({
error: {
firstReleaseVersion,
@@ -557,6 +583,8 @@ describe('ErrorDetails', () => {
jest.spyOn(Tracking, 'event');
mocks.$apollo.queries.error.loading = false;
mountComponent();
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
error: { externalUrl },
});
diff --git a/spec/frontend/error_tracking/components/error_tracking_list_spec.js b/spec/frontend/error_tracking/components/error_tracking_list_spec.js
index c0c542ae587..74d5731bbea 100644
--- a/spec/frontend/error_tracking/components/error_tracking_list_spec.js
+++ b/spec/frontend/error_tracking/components/error_tracking_list_spec.js
@@ -396,6 +396,8 @@ describe('ErrorTrackingList', () => {
GlPagination: false,
},
});
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({ pageValue: 2 });
return wrapper.vm.$nextTick();
});
diff --git a/spec/frontend/fixtures/blob.rb b/spec/frontend/fixtures/blob.rb
index bfdeee0881b..35a7ff4eb07 100644
--- a/spec/frontend/fixtures/blob.rb
+++ b/spec/frontend/fixtures/blob.rb
@@ -12,6 +12,7 @@ RSpec.describe Projects::BlobController, '(JavaScript fixtures)', type: :control
render_views
before do
+ stub_feature_flags(refactor_blob_viewer: false) # This fixture is only used by the legacy (non-refactored) blob viewer
sign_in(user)
allow(SecureRandom).to receive(:hex).and_return('securerandomhex:thereisnospoon')
end
diff --git a/spec/frontend/fixtures/runner.rb b/spec/frontend/fixtures/runner.rb
index fa150fbf57c..36e6cf72750 100644
--- a/spec/frontend/fixtures/runner.rb
+++ b/spec/frontend/fixtures/runner.rb
@@ -24,80 +24,109 @@ RSpec.describe 'Runner (JavaScript fixtures)' do
remove_repository(project)
end
- describe GraphQL::Query, type: :request do
- get_runners_query_name = 'get_runners.query.graphql'
-
+ describe do
before do
sign_in(admin)
enable_admin_mode!(admin)
end
- let_it_be(:query) do
- get_graphql_query_as_string("#{query_path}#{get_runners_query_name}")
- end
+ describe GraphQL::Query, type: :request do
+ get_runners_query_name = 'get_runners.query.graphql'
- it "#{fixtures_path}#{get_runners_query_name}.json" do
- post_graphql(query, current_user: admin, variables: {})
+ let_it_be(:query) do
+ get_graphql_query_as_string("#{query_path}#{get_runners_query_name}")
+ end
- expect_graphql_errors_to_be_empty
- end
+ it "#{fixtures_path}#{get_runners_query_name}.json" do
+ post_graphql(query, current_user: admin, variables: {})
- it "#{fixtures_path}#{get_runners_query_name}.paginated.json" do
- post_graphql(query, current_user: admin, variables: { first: 2 })
+ expect_graphql_errors_to_be_empty
+ end
- expect_graphql_errors_to_be_empty
+ it "#{fixtures_path}#{get_runners_query_name}.paginated.json" do
+ post_graphql(query, current_user: admin, variables: { first: 2 })
+
+ expect_graphql_errors_to_be_empty
+ end
end
- end
- describe GraphQL::Query, type: :request do
- get_runner_query_name = 'get_runner.query.graphql'
+ describe GraphQL::Query, type: :request do
+ get_runners_count_query_name = 'get_runners_count.query.graphql'
- before do
- sign_in(admin)
- enable_admin_mode!(admin)
- end
+ let_it_be(:query) do
+ get_graphql_query_as_string("#{query_path}#{get_runners_count_query_name}")
+ end
+
+ it "#{fixtures_path}#{get_runners_count_query_name}.json" do
+ post_graphql(query, current_user: admin, variables: {})
- let_it_be(:query) do
- get_graphql_query_as_string("#{query_path}#{get_runner_query_name}")
+ expect_graphql_errors_to_be_empty
+ end
end
- it "#{fixtures_path}#{get_runner_query_name}.json" do
- post_graphql(query, current_user: admin, variables: {
- id: instance_runner.to_global_id.to_s
- })
+ describe GraphQL::Query, type: :request do
+ get_runner_query_name = 'get_runner.query.graphql'
- expect_graphql_errors_to_be_empty
+ let_it_be(:query) do
+ get_graphql_query_as_string("#{query_path}#{get_runner_query_name}")
+ end
+
+ it "#{fixtures_path}#{get_runner_query_name}.json" do
+ post_graphql(query, current_user: admin, variables: {
+ id: instance_runner.to_global_id.to_s
+ })
+
+ expect_graphql_errors_to_be_empty
+ end
end
end
- describe GraphQL::Query, type: :request do
- get_group_runners_query_name = 'get_group_runners.query.graphql'
-
+ describe do
let_it_be(:group_owner) { create(:user) }
before do
group.add_owner(group_owner)
end
- let_it_be(:query) do
- get_graphql_query_as_string("#{query_path}#{get_group_runners_query_name}")
- end
+ describe GraphQL::Query, type: :request do
+ get_group_runners_query_name = 'get_group_runners.query.graphql'
+
+ let_it_be(:query) do
+ get_graphql_query_as_string("#{query_path}#{get_group_runners_query_name}")
+ end
- it "#{fixtures_path}#{get_group_runners_query_name}.json" do
- post_graphql(query, current_user: group_owner, variables: {
- groupFullPath: group.full_path
- })
+ it "#{fixtures_path}#{get_group_runners_query_name}.json" do
+ post_graphql(query, current_user: group_owner, variables: {
+ groupFullPath: group.full_path
+ })
- expect_graphql_errors_to_be_empty
+ expect_graphql_errors_to_be_empty
+ end
+
+ it "#{fixtures_path}#{get_group_runners_query_name}.paginated.json" do
+ post_graphql(query, current_user: group_owner, variables: {
+ groupFullPath: group.full_path,
+ first: 1
+ })
+
+ expect_graphql_errors_to_be_empty
+ end
end
- it "#{fixtures_path}#{get_group_runners_query_name}.paginated.json" do
- post_graphql(query, current_user: group_owner, variables: {
- groupFullPath: group.full_path,
- first: 1
- })
+ describe GraphQL::Query, type: :request do
+ get_group_runners_count_query_name = 'get_group_runners_count.query.graphql'
+
+ let_it_be(:query) do
+ get_graphql_query_as_string("#{query_path}#{get_group_runners_count_query_name}")
+ end
+
+ it "#{fixtures_path}#{get_group_runners_count_query_name}.json" do
+ post_graphql(query, current_user: group_owner, variables: {
+ groupFullPath: group.full_path
+ })
- expect_graphql_errors_to_be_empty
+ expect_graphql_errors_to_be_empty
+ end
end
end
end
diff --git a/spec/frontend/fixtures/static/project_select_combo_button.html b/spec/frontend/fixtures/static/project_select_combo_button.html
index 444e0bc84a2..3776610ed4c 100644
--- a/spec/frontend/fixtures/static/project_select_combo_button.html
+++ b/spec/frontend/fixtures/static/project_select_combo_button.html
@@ -1,6 +1,6 @@
<div class="project-item-select-holder">
<input class="project-item-select" data-group-id="12345" data-relative-path="issues/new" />
- <a class="new-project-item-link" data-label="New issue" data-type="issues" href="">
+ <a class="js-new-project-item-link" data-label="issue" data-type="issues" href="">
<span class="gl-spinner"></span>
</a>
<a class="new-project-item-select-button">
diff --git a/spec/frontend/flash_spec.js b/spec/frontend/flash_spec.js
index fc736f2d155..d5451ec2064 100644
--- a/spec/frontend/flash_spec.js
+++ b/spec/frontend/flash_spec.js
@@ -1,9 +1,12 @@
import * as Sentry from '@sentry/browser';
+import { setHTMLFixture } from 'helpers/fixtures';
import createFlash, {
hideFlash,
addDismissFlashClickListener,
FLASH_TYPES,
FLASH_CLOSED_EVENT,
+ createAlert,
+ VARIANT_WARNING,
} from '~/flash';
jest.mock('@sentry/browser');
@@ -68,6 +71,236 @@ describe('Flash', () => {
});
});
+ describe('createAlert', () => {
+ const mockMessage = 'a message';
+ let alert;
+
+ describe('no flash-container', () => {
+ it('does not add to the DOM', () => {
+ alert = createAlert({ message: mockMessage });
+
+ expect(alert).toBeNull();
+ expect(document.querySelector('.gl-alert')).toBeNull();
+ });
+ });
+
+ describe('with flash-container', () => {
+ beforeEach(() => {
+ setHTMLFixture('<div class="flash-container"></div>');
+ });
+
+ afterEach(() => {
+ if (alert) {
+ alert.$destroy();
+ }
+ document.querySelector('.flash-container')?.remove();
+ });
+
+ it('adds alert element into the document by default', () => {
+ alert = createAlert({ message: mockMessage });
+
+ expect(document.querySelector('.flash-container').textContent.trim()).toBe(mockMessage);
+ expect(document.querySelector('.flash-container .gl-alert')).not.toBeNull();
+ });
+
+ it('adds flash of a warning type', () => {
+ alert = createAlert({ message: mockMessage, variant: VARIANT_WARNING });
+
+ expect(
+ document.querySelector('.flash-container .gl-alert.gl-alert-warning'),
+ ).not.toBeNull();
+ });
+
+ it('escapes text', () => {
+ alert = createAlert({ message: '<script>alert("a");</script>' });
+
+ const html = document.querySelector('.flash-container').innerHTML;
+
+ expect(html).toContain('&lt;script&gt;alert("a");&lt;/script&gt;');
+ expect(html).not.toContain('<script>alert("a");</script>');
+ });
+
+ it('adds alert into specified container', () => {
+ setHTMLFixture(`
+ <div class="my-alert-container"></div>
+ <div class="my-other-container"></div>
+ `);
+
+ alert = createAlert({ message: mockMessage, containerSelector: '.my-alert-container' });
+
+ expect(document.querySelector('.my-alert-container .gl-alert')).not.toBeNull();
+ expect(document.querySelector('.my-alert-container').innerText.trim()).toBe(mockMessage);
+
+ expect(document.querySelector('.my-other-container .gl-alert')).toBeNull();
+ expect(document.querySelector('.my-other-container').innerText.trim()).toBe('');
+ });
+
+ it('adds alert into specified parent', () => {
+ setHTMLFixture(`
+ <div id="my-parent">
+ <div class="flash-container"></div>
+ </div>
+ <div id="my-other-parent">
+ <div class="flash-container"></div>
+ </div>
+ `);
+
+ alert = createAlert({ message: mockMessage, parent: document.getElementById('my-parent') });
+
+ expect(document.querySelector('#my-parent .flash-container .gl-alert')).not.toBeNull();
+ expect(document.querySelector('#my-parent .flash-container').innerText.trim()).toBe(
+ mockMessage,
+ );
+
+ expect(document.querySelector('#my-other-parent .flash-container .gl-alert')).toBeNull();
+ expect(document.querySelector('#my-other-parent .flash-container').innerText.trim()).toBe(
+ '',
+ );
+ });
+
+ it('removes element after clicking', () => {
+ alert = createAlert({ message: mockMessage });
+
+ expect(document.querySelector('.flash-container .gl-alert')).not.toBeNull();
+
+ document.querySelector('.gl-dismiss-btn').click();
+
+ expect(document.querySelector('.flash-container .gl-alert')).toBeNull();
+ });
+
+ it('does not capture error using Sentry', () => {
+ alert = createAlert({
+ message: mockMessage,
+ captureError: false,
+ error: new Error('Error!'),
+ });
+
+ expect(Sentry.captureException).not.toHaveBeenCalled();
+ });
+
+ it('captures error using Sentry', () => {
+ alert = createAlert({
+ message: mockMessage,
+ captureError: true,
+ error: new Error('Error!'),
+ });
+
+ expect(Sentry.captureException).toHaveBeenCalledWith(expect.any(Error));
+ expect(Sentry.captureException).toHaveBeenCalledWith(
+ expect.objectContaining({
+ message: 'Error!',
+ }),
+ );
+ });
+
+ describe('with buttons', () => {
+ const findAlertAction = () => document.querySelector('.flash-container .gl-alert-action');
+
+ it('adds primary button', () => {
+ alert = createAlert({
+ message: mockMessage,
+ primaryButton: {
+ text: 'Ok',
+ },
+ });
+
+ expect(findAlertAction().textContent.trim()).toBe('Ok');
+ });
+
+ it('creates link with href', () => {
+ alert = createAlert({
+ message: mockMessage,
+ primaryButton: {
+ link: '/url',
+ text: 'Ok',
+ },
+ });
+
+ const action = findAlertAction();
+
+ expect(action.textContent.trim()).toBe('Ok');
+ expect(action.nodeName).toBe('A');
+ expect(action.getAttribute('href')).toBe('/url');
+ });
+
+ it('create button as href when no href is present', () => {
+ alert = createAlert({
+ message: mockMessage,
+ primaryButton: {
+ text: 'Ok',
+ },
+ });
+
+ const action = findAlertAction();
+
+ expect(action.nodeName).toBe('BUTTON');
+ expect(action.getAttribute('href')).toBe(null);
+ });
+
+ it('escapes the title text', () => {
+ alert = createAlert({
+ message: mockMessage,
+ primaryButton: {
+ text: '<script>alert("a")</script>',
+ },
+ });
+
+ const html = findAlertAction().innerHTML;
+
+ expect(html).toContain('&lt;script&gt;alert("a")&lt;/script&gt;');
+ expect(html).not.toContain('<script>alert("a")</script>');
+ });
+
+ it('calls actionConfig clickHandler on click', () => {
+ const clickHandler = jest.fn();
+
+ alert = createAlert({
+ message: mockMessage,
+ primaryButton: {
+ text: 'Ok',
+ clickHandler,
+ },
+ });
+
+ expect(clickHandler).toHaveBeenCalledTimes(0);
+
+ findAlertAction().click();
+
+ expect(clickHandler).toHaveBeenCalledTimes(1);
+ expect(clickHandler).toHaveBeenCalledWith(expect.any(MouseEvent));
+ });
+ });
+
+ describe('Alert API', () => {
+ describe('dismiss', () => {
+ it('dismiss programmatically with .dismiss()', () => {
+ expect(document.querySelector('.gl-alert')).toBeNull();
+
+ alert = createAlert({ message: mockMessage });
+
+ expect(document.querySelector('.gl-alert')).not.toBeNull();
+
+ alert.dismiss();
+
+ expect(document.querySelector('.gl-alert')).toBeNull();
+ });
+
+ it('calls onDismiss when dismissed', () => {
+ const dismissHandler = jest.fn();
+
+ alert = createAlert({ message: mockMessage, onDismiss: dismissHandler });
+
+ expect(dismissHandler).toHaveBeenCalledTimes(0);
+
+ alert.dismiss();
+
+ expect(dismissHandler).toHaveBeenCalledTimes(1);
+ });
+ });
+ });
+ });
+ });
+
describe('createFlash', () => {
const message = 'test';
const fadeTransition = false;
@@ -91,7 +324,7 @@ describe('Flash', () => {
describe('with flash-container', () => {
beforeEach(() => {
- setFixtures(
+ setHTMLFixture(
'<div class="content-wrapper js-content-wrapper"><div class="flash-container"></div></div>',
);
});
@@ -115,11 +348,12 @@ describe('Flash', () => {
});
it('escapes text', () => {
- createFlash({ ...defaultParams, message: '<script>alert("a");</script>' });
+ createFlash({ ...defaultParams, message: '<script>alert("a")</script>' });
- expect(document.querySelector('.flash-text').textContent.trim()).toBe(
- '<script>alert("a");</script>',
- );
+ const html = document.querySelector('.flash-text').innerHTML;
+
+ expect(html).toContain('&lt;script&gt;alert("a")&lt;/script&gt;');
+ expect(html).not.toContain('<script>alert("a")</script>');
});
it('adds flash into specified parent', () => {
@@ -193,8 +427,10 @@ describe('Flash', () => {
},
});
- expect(findFlashAction().href).toBe(`${window.location}testing`);
- expect(findFlashAction().textContent.trim()).toBe('test');
+ const action = findFlashAction();
+
+ expect(action.href).toBe(`${window.location}testing`);
+ expect(action.textContent.trim()).toBe('test');
});
it('uses hash as href when no href is present', () => {
@@ -227,7 +463,10 @@ describe('Flash', () => {
},
});
- expect(findFlashAction().textContent.trim()).toBe('<script>alert("a")</script>');
+ const html = findFlashAction().innerHTML;
+
+ expect(html).toContain('&lt;script&gt;alert("a")&lt;/script&gt;');
+ expect(html).not.toContain('<script>alert("a")</script>');
});
it('calls actionConfig clickHandler on click', () => {
diff --git a/spec/frontend/google_cloud/components/app_spec.js b/spec/frontend/google_cloud/components/app_spec.js
index 570ac1e6ed1..92bc7596f7d 100644
--- a/spec/frontend/google_cloud/components/app_spec.js
+++ b/spec/frontend/google_cloud/components/app_spec.js
@@ -24,6 +24,8 @@ const HOME_PROPS = {
serviceAccounts: [{}, {}],
createServiceAccountUrl: '#url-create-service-account',
emptyIllustrationUrl: '#url-empty-illustration',
+ deploymentsCloudRunUrl: '#url-deployments-cloud-run',
+ deploymentsCloudStorageUrl: '#deploymentsCloudStorageUrl',
};
describe('google_cloud App component', () => {
diff --git a/spec/frontend/google_cloud/components/deployments_service_table_spec.js b/spec/frontend/google_cloud/components/deployments_service_table_spec.js
new file mode 100644
index 00000000000..76c3bfd00a8
--- /dev/null
+++ b/spec/frontend/google_cloud/components/deployments_service_table_spec.js
@@ -0,0 +1,40 @@
+import { mount } from '@vue/test-utils';
+import { GlButton, GlTable } from '@gitlab/ui';
+import DeploymentsServiceTable from '~/google_cloud/components/deployments_service_table.vue';
+
+describe('google_cloud DeploymentsServiceTable component', () => {
+ let wrapper;
+
+ const findTable = () => wrapper.findComponent(GlTable);
+ const findButtons = () => findTable().findAllComponents(GlButton);
+ const findCloudRunButton = () => findButtons().at(0);
+ const findCloudStorageButton = () => findButtons().at(1);
+
+ beforeEach(() => {
+ const propsData = {
+ cloudRunUrl: '#url-deployments-cloud-run',
+ cloudStorageUrl: '#url-deployments-cloud-storage',
+ };
+ wrapper = mount(DeploymentsServiceTable, { propsData });
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('should contain a table', () => {
+ expect(findTable().exists()).toBe(true);
+ });
+
+ it('should contain configure cloud run button', () => {
+ const cloudRunButton = findCloudRunButton();
+ expect(cloudRunButton.exists()).toBe(true);
+ expect(cloudRunButton.props().disabled).toBe(true);
+ });
+
+ it('should contain configure cloud storage button', () => {
+ const cloudStorageButton = findCloudStorageButton();
+ expect(cloudStorageButton.exists()).toBe(true);
+ expect(cloudStorageButton.props().disabled).toBe(true);
+ });
+});
diff --git a/spec/frontend/google_cloud/components/home_spec.js b/spec/frontend/google_cloud/components/home_spec.js
index 9b4c3a79f11..3a009fc88ce 100644
--- a/spec/frontend/google_cloud/components/home_spec.js
+++ b/spec/frontend/google_cloud/components/home_spec.js
@@ -20,6 +20,8 @@ describe('google_cloud Home component', () => {
serviceAccounts: [{}, {}],
createServiceAccountUrl: '#url-create-service-account',
emptyIllustrationUrl: '#url-empty-illustration',
+ deploymentsCloudRunUrl: '#url-deployments-cloud-run',
+ deploymentsCloudStorageUrl: '#deploymentsCloudStorageUrl',
};
beforeEach(() => {
@@ -42,7 +44,7 @@ describe('google_cloud Home component', () => {
it('should contain three tab items', () => {
expect(findTabItemsModel()).toEqual([
{ title: 'Configuration', disabled: undefined },
- { title: 'Deployments', disabled: '' },
+ { title: 'Deployments', disabled: undefined },
{ title: 'Services', disabled: '' },
]);
});
diff --git a/spec/frontend/google_tag_manager/index_spec.js b/spec/frontend/google_tag_manager/index_spec.js
new file mode 100644
index 00000000000..ff38de28da6
--- /dev/null
+++ b/spec/frontend/google_tag_manager/index_spec.js
@@ -0,0 +1,259 @@
+import { merge } from 'lodash';
+import {
+ trackFreeTrialAccountSubmissions,
+ trackNewRegistrations,
+ trackSaasTrialSubmit,
+ trackSaasTrialSkip,
+ trackSaasTrialGroup,
+ trackSaasTrialProject,
+ trackSaasTrialProjectImport,
+ trackSaasTrialGetStarted,
+} from '~/google_tag_manager';
+import { setHTMLFixture } from 'helpers/fixtures';
+import { logError } from '~/lib/logger';
+
+jest.mock('~/lib/logger');
+
+describe('~/google_tag_manager/index', () => {
+ let spy;
+
+ beforeEach(() => {
+ spy = jest.fn();
+
+ window.dataLayer = {
+ push: spy,
+ };
+ window.gon.features = {
+ gitlabGtmDatalayer: true,
+ };
+ });
+
+ const createHTML = ({ links = [], forms = [] } = {}) => {
+ // .foo elements are used to test elements which shouldn't do anything
+ const allLinks = links.concat({ cls: 'foo' });
+ const allForms = forms.concat({ cls: 'foo' });
+
+ const el = document.createElement('div');
+
+ allLinks.forEach(({ cls = '', id = '', href = '#', text = 'Hello', attributes = {} }) => {
+ const a = document.createElement('a');
+ a.id = id;
+ a.href = href || '#';
+ a.className = cls;
+ a.textContent = text;
+
+ Object.entries(attributes).forEach(([key, value]) => {
+ a.setAttribute(key, value);
+ });
+
+ el.append(a);
+ });
+
+ allForms.forEach(({ cls = '', id = '' }) => {
+ const form = document.createElement('form');
+ form.id = id;
+ form.className = cls;
+
+ el.append(form);
+ });
+
+ return el.innerHTML;
+ };
+
+ const triggerEvent = (selector, eventType) => {
+ const el = document.querySelector(selector);
+
+ el.dispatchEvent(new Event(eventType));
+ };
+
+ const getSelector = ({ id, cls }) => (id ? `#${id}` : `.${cls}`);
+
+ const createTestCase = (subject, { forms = [], links = [] }) => {
+ const expectedFormEvents = forms.map(({ expectation, ...form }) => ({
+ selector: getSelector(form),
+ trigger: 'submit',
+ expectation,
+ }));
+
+ const expectedLinkEvents = links.map(({ expectation, ...link }) => ({
+ selector: getSelector(link),
+ trigger: 'click',
+ expectation,
+ }));
+
+ return [
+ subject,
+ {
+ forms,
+ links,
+ expectedEvents: [...expectedFormEvents, ...expectedLinkEvents],
+ },
+ ];
+ };
+
+ const createOmniAuthTestCase = (subject, accountType) =>
+ createTestCase(subject, {
+ forms: [
+ {
+ id: 'new_new_user',
+ expectation: {
+ event: 'accountSubmit',
+ accountMethod: 'form',
+ accountType,
+ },
+ },
+ ],
+ links: [
+ {
+ // id is needed so that the test selects the right element to trigger
+ id: 'test-0',
+ cls: 'js-oauth-login',
+ attributes: {
+ 'data-provider': 'myspace',
+ },
+ expectation: {
+ event: 'accountSubmit',
+ accountMethod: 'myspace',
+ accountType,
+ },
+ },
+ {
+ id: 'test-1',
+ cls: 'js-oauth-login',
+ attributes: {
+ 'data-provider': 'gitlab',
+ },
+ expectation: {
+ event: 'accountSubmit',
+ accountMethod: 'gitlab',
+ accountType,
+ },
+ },
+ ],
+ });
+
+ describe.each([
+ createOmniAuthTestCase(trackFreeTrialAccountSubmissions, 'freeThirtyDayTrial'),
+ createOmniAuthTestCase(trackNewRegistrations, 'standardSignUp'),
+ createTestCase(trackSaasTrialSkip, {
+ links: [{ cls: 'js-skip-trial', expectation: { event: 'saasTrialSkip' } }],
+ }),
+ createTestCase(trackSaasTrialGroup, {
+ forms: [{ cls: 'js-saas-trial-group', expectation: { event: 'saasTrialGroup' } }],
+ }),
+ createTestCase(trackSaasTrialProject, {
+ forms: [{ id: 'new_project', expectation: { event: 'saasTrialProject' } }],
+ }),
+ createTestCase(trackSaasTrialProjectImport, {
+ links: [
+ {
+ id: 'js-test-btn-0',
+ cls: 'js-import-project-btn',
+ attributes: { 'data-platform': 'bitbucket' },
+ expectation: { event: 'saasTrialProjectImport', saasProjectImport: 'bitbucket' },
+ },
+ {
+ // id is neeeded so we trigger the right element in the test
+ id: 'js-test-btn-1',
+ cls: 'js-import-project-btn',
+ attributes: { 'data-platform': 'github' },
+ expectation: { event: 'saasTrialProjectImport', saasProjectImport: 'github' },
+ },
+ ],
+ }),
+ createTestCase(trackSaasTrialGetStarted, {
+ links: [
+ {
+ cls: 'js-get-started-btn',
+ expectation: { event: 'saasTrialGetStarted' },
+ },
+ ],
+ }),
+ ])('%p', (subject, { links = [], forms = [], expectedEvents }) => {
+ beforeEach(() => {
+ setHTMLFixture(createHTML({ links, forms }));
+
+ subject();
+ });
+
+ it.each(expectedEvents)('when %p', ({ selector, trigger, expectation }) => {
+ expect(spy).not.toHaveBeenCalled();
+
+ triggerEvent(selector, trigger);
+
+ expect(spy).toHaveBeenCalledTimes(1);
+ expect(spy).toHaveBeenCalledWith(expectation);
+ expect(logError).not.toHaveBeenCalled();
+ });
+
+ it('when random link is clicked, does nothing', () => {
+ triggerEvent('a.foo', 'click');
+
+ expect(spy).not.toHaveBeenCalled();
+ });
+
+ it('when random form is submitted, does nothing', () => {
+ triggerEvent('form.foo', 'submit');
+
+ expect(spy).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('No listener events', () => {
+ it('when trackSaasTrialSubmit is invoked', () => {
+ expect(spy).not.toHaveBeenCalled();
+
+ trackSaasTrialSubmit();
+
+ expect(spy).toHaveBeenCalledTimes(1);
+ expect(spy).toHaveBeenCalledWith({ event: 'saasTrialSubmit' });
+ expect(logError).not.toHaveBeenCalled();
+ });
+ });
+
+ describe.each([
+ { dataLayer: null },
+ { gon: { features: null } },
+ { gon: { features: { gitlabGtmDatalayer: false } } },
+ ])('when window %o', (windowAttrs) => {
+ beforeEach(() => {
+ merge(window, windowAttrs);
+ });
+
+ it('no ops', () => {
+ setHTMLFixture(createHTML({ forms: [{ id: 'new_project' }] }));
+
+ trackSaasTrialProject();
+
+ triggerEvent('#new_project', 'submit');
+
+ expect(spy).not.toHaveBeenCalled();
+ expect(logError).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('when window.dataLayer throws error', () => {
+ const pushError = new Error('test');
+
+ beforeEach(() => {
+ window.dataLayer = {
+ push() {
+ throw pushError;
+ },
+ };
+ });
+
+ it('logs error', () => {
+ setHTMLFixture(createHTML({ forms: [{ id: 'new_project' }] }));
+
+ trackSaasTrialProject();
+
+ triggerEvent('#new_project', 'submit');
+
+ expect(logError).toHaveBeenCalledWith(
+ 'Unexpected error while pushing to dataLayer',
+ pushError,
+ );
+ });
+ });
+});
diff --git a/spec/frontend/groups/components/group_item_spec.js b/spec/frontend/groups/components/group_item_spec.js
index 60d47895a95..8ea7e54aef4 100644
--- a/spec/frontend/groups/components/group_item_spec.js
+++ b/spec/frontend/groups/components/group_item_spec.js
@@ -100,6 +100,7 @@ describe('GroupItemComponent', () => {
wrapper.destroy();
group.type = 'project';
+ group.lastActivityAt = '2017-04-09T18:40:39.101Z';
wrapper = createComponent({ group });
expect(wrapper.vm.isGroup).toBe(false);
diff --git a/spec/frontend/groups/components/item_stats_spec.js b/spec/frontend/groups/components/item_stats_spec.js
index 49f3f5da43c..fdc267bc14a 100644
--- a/spec/frontend/groups/components/item_stats_spec.js
+++ b/spec/frontend/groups/components/item_stats_spec.js
@@ -38,6 +38,7 @@ describe('ItemStats', () => {
...mockParentGroupItem,
type: ITEM_TYPE.PROJECT,
starCount: 4,
+ lastActivityAt: '2017-04-09T18:40:39.101Z',
};
createComponent({ item });
diff --git a/spec/frontend/landing_spec.js b/spec/frontend/groups/landing_spec.js
index 448d8ee2e81..f90f541eb96 100644
--- a/spec/frontend/landing_spec.js
+++ b/spec/frontend/groups/landing_spec.js
@@ -1,5 +1,5 @@
import Cookies from 'js-cookie';
-import Landing from '~/landing';
+import Landing from '~/groups/landing';
describe('Landing', () => {
const test = {};
diff --git a/spec/frontend/transfer_edit_spec.js b/spec/frontend/groups/transfer_edit_spec.js
index 4091d753fe5..bc070920d02 100644
--- a/spec/frontend/transfer_edit_spec.js
+++ b/spec/frontend/groups/transfer_edit_spec.js
@@ -1,7 +1,7 @@
import $ from 'jquery';
import { loadHTMLFixture } from 'helpers/fixtures';
-import setupTransferEdit from '~/transfer_edit';
+import setupTransferEdit from '~/groups/transfer_edit';
describe('setupTransferEdit', () => {
const formSelector = '.js-group-transfer-form';
diff --git a/spec/frontend/ide/components/jobs/__snapshots__/stage_spec.js.snap b/spec/frontend/ide/components/jobs/__snapshots__/stage_spec.js.snap
index faa70982fac..d1cf9f2e248 100644
--- a/spec/frontend/ide/components/jobs/__snapshots__/stage_spec.js.snap
+++ b/spec/frontend/ide/components/jobs/__snapshots__/stage_spec.js.snap
@@ -25,11 +25,12 @@ exports[`IDE pipeline stage renders stage details & icon 1`] = `
<div
class="gl-mr-3 gl-ml-2"
>
- <span
- class="badge badge-pill"
+ <gl-badge-stub
+ size="md"
+ variant="muted"
>
- 4
- </span>
+ 4
+ </gl-badge-stub>
</div>
<gl-icon-stub
diff --git a/spec/frontend/ide/components/preview/clientside_spec.js b/spec/frontend/ide/components/preview/clientside_spec.js
index 1768f01f3b8..b168eec0f16 100644
--- a/spec/frontend/ide/components/preview/clientside_spec.js
+++ b/spec/frontend/ide/components/preview/clientside_spec.js
@@ -73,6 +73,8 @@ describe('IDE clientside preview', () => {
const createInitializedComponent = () => {
createComponent();
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
sandpackReady: true,
manager: {
@@ -202,6 +204,8 @@ describe('IDE clientside preview', () => {
it('returns false if loading and mainEntry exists', () => {
createComponent({ getters: { packageJson: dummyPackageJson } });
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({ loading: true });
expect(wrapper.vm.showPreview).toBe(false);
@@ -209,6 +213,8 @@ describe('IDE clientside preview', () => {
it('returns true if not loading and mainEntry exists', () => {
createComponent({ getters: { packageJson: dummyPackageJson } });
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({ loading: false });
expect(wrapper.vm.showPreview).toBe(true);
@@ -218,12 +224,16 @@ describe('IDE clientside preview', () => {
describe('showEmptyState', () => {
it('returns true if no mainEntry exists', () => {
createComponent();
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({ loading: false });
expect(wrapper.vm.showEmptyState).toBe(true);
});
it('returns false if loading', () => {
createComponent();
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({ loading: true });
expect(wrapper.vm.showEmptyState).toBe(false);
@@ -231,6 +241,8 @@ describe('IDE clientside preview', () => {
it('returns false if not loading and mainEntry exists', () => {
createComponent({ getters: { packageJson: dummyPackageJson } });
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({ loading: false });
expect(wrapper.vm.showEmptyState).toBe(false);
@@ -307,6 +319,8 @@ describe('IDE clientside preview', () => {
describe('update', () => {
it('initializes manager if manager is empty', () => {
createComponent({ getters: { packageJson: dummyPackageJson } });
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({ sandpackReady: true });
wrapper.vm.update();
@@ -340,6 +354,8 @@ describe('IDE clientside preview', () => {
describe('template', () => {
it('renders ide-preview element when showPreview is true', () => {
createComponent({ getters: { packageJson: dummyPackageJson } });
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({ loading: false });
return wrapper.vm.$nextTick(() => {
@@ -349,6 +365,8 @@ describe('IDE clientside preview', () => {
it('renders empty state', () => {
createComponent();
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({ loading: false });
return wrapper.vm.$nextTick(() => {
@@ -360,6 +378,8 @@ describe('IDE clientside preview', () => {
it('renders loading icon', () => {
createComponent();
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({ loading: true });
return wrapper.vm.$nextTick(() => {
diff --git a/spec/frontend/ide/components/repo_editor_spec.js b/spec/frontend/ide/components/repo_editor_spec.js
index c957c64aa10..15af2d03704 100644
--- a/spec/frontend/ide/components/repo_editor_spec.js
+++ b/spec/frontend/ide/components/repo_editor_spec.js
@@ -5,7 +5,6 @@ import Vue from 'vue';
import Vuex from 'vuex';
import '~/behaviors/markdown/render_gfm';
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 { EditorMarkdownExtension } from '~/editor/extensions/source_editor_markdown_ext';
@@ -540,7 +539,6 @@ describe('RepoEditor', () => {
},
});
await vm.$nextTick();
- await vm.$nextTick();
expect(vm.initEditor).toHaveBeenCalled();
});
@@ -567,8 +565,8 @@ describe('RepoEditor', () => {
// switching from edit to diff mode usually triggers editor initialization
vm.$store.state.viewer = viewerTypes.diff;
- // we delay returning the file to make sure editor doesn't initialize before we fetch file content
- await waitUsingRealTimer(30);
+ jest.runOnlyPendingTimers();
+
return 'rawFileData123\n';
});
@@ -598,8 +596,9 @@ describe('RepoEditor', () => {
return aContent;
})
.mockImplementationOnce(async () => {
- // we delay returning fileB content to make sure the editor doesn't initialize prematurely
- await waitUsingRealTimer(30);
+ // we delay returning fileB content
+ // to make sure the editor doesn't initialize prematurely
+ jest.advanceTimersByTime(30);
return bContent;
});
diff --git a/spec/frontend/ide/components/terminal/terminal_spec.js b/spec/frontend/ide/components/terminal/terminal_spec.js
index c4b186c004a..afc49e22c83 100644
--- a/spec/frontend/ide/components/terminal/terminal_spec.js
+++ b/spec/frontend/ide/components/terminal/terminal_spec.js
@@ -128,6 +128,8 @@ describe('IDE Terminal', () => {
canScrollDown: false,
});
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({ canScrollUp: true, canScrollDown: true });
return nextTick().then(() => {
diff --git a/spec/frontend/ide/stores/modules/pipelines/actions_spec.js b/spec/frontend/ide/stores/modules/pipelines/actions_spec.js
index 9aa31136c89..3ede37e2eed 100644
--- a/spec/frontend/ide/stores/modules/pipelines/actions_spec.js
+++ b/spec/frontend/ide/stores/modules/pipelines/actions_spec.js
@@ -188,6 +188,24 @@ describe('IDE pipelines actions', () => {
.catch(done.fail);
});
});
+
+ it('sets latest pipeline to `null` and stops polling on empty project', (done) => {
+ mockedState = {
+ ...mockedState,
+ rootGetters: {
+ lastCommit: null,
+ },
+ };
+
+ testAction(
+ fetchLatestPipeline,
+ {},
+ mockedState,
+ [{ type: types.RECEIVE_LASTEST_PIPELINE_SUCCESS, payload: null }],
+ [{ type: 'stopPipelinePolling' }],
+ done,
+ );
+ });
});
describe('requestJobs', () => {
diff --git a/spec/frontend/integrations/edit/components/dynamic_field_spec.js b/spec/frontend/integrations/edit/components/dynamic_field_spec.js
index bf044e388ea..b0fb94d2b29 100644
--- a/spec/frontend/integrations/edit/components/dynamic_field_spec.js
+++ b/spec/frontend/integrations/edit/components/dynamic_field_spec.js
@@ -61,7 +61,7 @@ describe('DynamicField', () => {
});
it(`renders GlFormCheckbox with correct text content when checkboxLabel is ${checkboxLabel}`, () => {
- expect(findGlFormCheckbox().text()).toBe(checkboxLabel ?? defaultProps.title);
+ expect(findGlFormCheckbox().text()).toContain(checkboxLabel ?? defaultProps.title);
});
it('does not render other types of input', () => {
@@ -182,6 +182,17 @@ describe('DynamicField', () => {
expect(findGlFormGroup().find('small').text()).toBe(defaultProps.help);
});
+ describe('when type is checkbox', () => {
+ it('renders description with help text', () => {
+ createComponent({
+ type: 'checkbox',
+ });
+
+ expect(findGlFormGroup().find('small').exists()).toBe(false);
+ expect(findGlFormCheckbox().text()).toContain(defaultProps.help);
+ });
+ });
+
it('renders description with help text as HTML', () => {
const helpHTML = 'The <strong>URL</strong> of the project';
diff --git a/spec/frontend/integrations/edit/components/integration_form_spec.js b/spec/frontend/integrations/edit/components/integration_form_spec.js
index 4c1394f3a87..8cf8a403e5d 100644
--- a/spec/frontend/integrations/edit/components/integration_form_spec.js
+++ b/spec/frontend/integrations/edit/components/integration_form_spec.js
@@ -1,9 +1,10 @@
+import { GlForm } from '@gitlab/ui';
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import * as Sentry from '@sentry/browser';
import { setHTMLFixture } from 'helpers/fixtures';
-import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import { mockIntegrationProps } from 'jest/integrations/edit/mock_data';
+import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import waitForPromises from 'helpers/wait_for_promises';
import ActiveCheckbox from '~/integrations/edit/components/active_checkbox.vue';
import ConfirmationModal from '~/integrations/edit/components/confirmation_modal.vue';
import DynamicField from '~/integrations/edit/components/dynamic_field.vue';
@@ -13,7 +14,6 @@ import JiraTriggerFields from '~/integrations/edit/components/jira_trigger_field
import OverrideDropdown from '~/integrations/edit/components/override_dropdown.vue';
import ResetConfirmationModal from '~/integrations/edit/components/reset_confirmation_modal.vue';
import TriggerFields from '~/integrations/edit/components/trigger_fields.vue';
-import waitForPromises from 'helpers/wait_for_promises';
import {
integrationLevels,
I18N_SUCCESSFUL_CONNECTION_MESSAGE,
@@ -23,9 +23,12 @@ import {
import { createStore } from '~/integrations/edit/store';
import eventHub from '~/integrations/edit/event_hub';
import httpStatus from '~/lib/utils/http_status';
+import { refreshCurrentPage } from '~/lib/utils/url_utility';
+import { mockIntegrationProps } from '../mock_data';
jest.mock('~/integrations/edit/event_hub');
jest.mock('@sentry/browser');
+jest.mock('~/lib/utils/url_utility');
describe('IntegrationForm', () => {
const mockToastShow = jest.fn();
@@ -34,12 +37,18 @@ describe('IntegrationForm', () => {
let dispatch;
let mockAxios;
let mockForm;
+ let vueIntegrationFormFeatureFlag;
+
+ const createForm = () => {
+ mockForm = document.createElement('form');
+ jest.spyOn(document, 'querySelector').mockReturnValue(mockForm);
+ };
const createComponent = ({
customStateProps = {},
- featureFlags = {},
initialState = {},
props = {},
+ mountFn = shallowMountExtended,
} = {}) => {
const store = createStore({
customState: { ...mockIntegrationProps, ...customStateProps },
@@ -47,11 +56,12 @@ describe('IntegrationForm', () => {
});
dispatch = jest.spyOn(store, 'dispatch').mockImplementation();
- wrapper = shallowMountExtended(IntegrationForm, {
- propsData: { ...props, formSelector: '.test' },
- provide: {
- glFeatures: featureFlags,
- },
+ if (!vueIntegrationFormFeatureFlag) {
+ createForm();
+ }
+
+ wrapper = mountFn(IntegrationForm, {
+ propsData: { ...props },
store,
stubs: {
OverrideDropdown,
@@ -65,26 +75,33 @@ describe('IntegrationForm', () => {
show: mockToastShow,
},
},
+ provide: {
+ glFeatures: {
+ vueIntegrationForm: vueIntegrationFormFeatureFlag,
+ },
+ },
});
};
- const createForm = ({ isValid = true } = {}) => {
- mockForm = document.createElement('form');
- jest.spyOn(document, 'querySelector').mockReturnValue(mockForm);
- jest.spyOn(mockForm, 'checkValidity').mockReturnValue(isValid);
- jest.spyOn(mockForm, 'submit');
- };
-
const findOverrideDropdown = () => wrapper.findComponent(OverrideDropdown);
const findActiveCheckbox = () => wrapper.findComponent(ActiveCheckbox);
const findConfirmationModal = () => wrapper.findComponent(ConfirmationModal);
const findResetConfirmationModal = () => wrapper.findComponent(ResetConfirmationModal);
const findResetButton = () => wrapper.findByTestId('reset-button');
- const findSaveButton = () => wrapper.findByTestId('save-button');
+ const findProjectSaveButton = () => wrapper.findByTestId('save-button');
+ const findInstanceOrGroupSaveButton = () => wrapper.findByTestId('save-button-instance-group');
const findTestButton = () => wrapper.findByTestId('test-button');
const findJiraTriggerFields = () => wrapper.findComponent(JiraTriggerFields);
const findJiraIssuesFields = () => wrapper.findComponent(JiraIssuesFields);
const findTriggerFields = () => wrapper.findComponent(TriggerFields);
+ const findGlForm = () => wrapper.findComponent(GlForm);
+ const findRedirectToField = () => wrapper.findByTestId('redirect-to-field');
+ const findFormElement = () => (vueIntegrationFormFeatureFlag ? findGlForm().element : mockForm);
+
+ const mockFormFunctions = ({ checkValidityReturn }) => {
+ jest.spyOn(findFormElement(), 'checkValidity').mockReturnValue(checkValidityReturn);
+ jest.spyOn(findFormElement(), 'submit');
+ };
beforeEach(() => {
mockAxios = new MockAdapter(axios);
@@ -220,6 +237,7 @@ describe('IntegrationForm', () => {
createComponent({
customStateProps: { type: 'jira', testPath: '/test' },
+ mountFn: mountExtended,
});
});
@@ -338,6 +356,19 @@ describe('IntegrationForm', () => {
});
});
});
+
+ describe('when `vueIntegrationForm` feature flag is $vueIntegrationFormEnabled', () => {
+ it('renders hidden fields', () => {
+ vueIntegrationFormFeatureFlag = true;
+ createComponent({
+ customStateProps: {
+ redirectTo: '/services',
+ },
+ });
+
+ expect(findRedirectToField().attributes('value')).toBe('/services');
+ });
+ });
});
describe('ActiveCheckbox', () => {
@@ -358,190 +389,292 @@ describe('IntegrationForm', () => {
});
describe.each`
- formActive | novalidate
- ${true} | ${null}
- ${false} | ${'true'}
+ formActive | vueIntegrationFormEnabled | novalidate
+ ${true} | ${true} | ${null}
+ ${false} | ${true} | ${'novalidate'}
+ ${true} | ${false} | ${null}
+ ${false} | ${false} | ${'true'}
`(
- 'when `toggle-integration-active` is emitted with $formActive',
- ({ formActive, novalidate }) => {
+ 'when `vueIntegrationForm` feature flag is $vueIntegrationFormEnabled and `toggle-integration-active` is emitted with $formActive',
+ ({ formActive, vueIntegrationFormEnabled, novalidate }) => {
beforeEach(async () => {
- createForm();
+ vueIntegrationFormFeatureFlag = vueIntegrationFormEnabled;
+
createComponent({
customStateProps: {
showActive: true,
initialActivated: false,
},
+ mountFn: mountExtended,
});
+ mockFormFunctions({ checkValidityReturn: false });
await findActiveCheckbox().vm.$emit('toggle-integration-active', formActive);
});
it(`sets noValidate to ${novalidate}`, () => {
- expect(mockForm.getAttribute('novalidate')).toBe(novalidate);
+ expect(findFormElement().getAttribute('novalidate')).toBe(novalidate);
});
},
);
});
- describe('when `save` button is clicked', () => {
- describe('buttons', () => {
- beforeEach(async () => {
- createForm();
- createComponent({
- customStateProps: {
- showActive: true,
- canTest: true,
- initialActivated: true,
- },
+ describe.each`
+ vueIntegrationFormEnabled
+ ${true}
+ ${false}
+ `(
+ 'when `vueIntegrationForm` feature flag is $vueIntegrationFormEnabled',
+ ({ vueIntegrationFormEnabled }) => {
+ beforeEach(() => {
+ vueIntegrationFormFeatureFlag = vueIntegrationFormEnabled;
+ });
+
+ describe('when `save` button is clicked', () => {
+ describe('buttons', () => {
+ beforeEach(async () => {
+ createComponent({
+ customStateProps: {
+ showActive: true,
+ canTest: true,
+ initialActivated: true,
+ },
+ mountFn: mountExtended,
+ });
+
+ await findProjectSaveButton().vm.$emit('click', new Event('click'));
+ });
+
+ it('sets save button `loading` prop to `true`', () => {
+ expect(findProjectSaveButton().props('loading')).toBe(true);
+ });
+
+ it('sets test button `disabled` prop to `true`', () => {
+ expect(findTestButton().props('disabled')).toBe(true);
+ });
});
- await findSaveButton().vm.$emit('click', new Event('click'));
- });
+ describe.each`
+ checkValidityReturn | integrationActive
+ ${true} | ${false}
+ ${true} | ${true}
+ ${false} | ${false}
+ `(
+ 'when form is valid (checkValidity returns $checkValidityReturn and integrationActive is $integrationActive)',
+ ({ integrationActive, checkValidityReturn }) => {
+ beforeEach(async () => {
+ createComponent({
+ customStateProps: {
+ showActive: true,
+ canTest: true,
+ initialActivated: integrationActive,
+ },
+ mountFn: mountExtended,
+ });
+
+ mockFormFunctions({ checkValidityReturn });
+
+ await findProjectSaveButton().vm.$emit('click', new Event('click'));
+ });
+
+ it('submits form', () => {
+ expect(findFormElement().submit).toHaveBeenCalledTimes(1);
+ });
+ },
+ );
+
+ describe('when form is invalid (checkValidity returns false and integrationActive is true)', () => {
+ beforeEach(async () => {
+ createComponent({
+ customStateProps: {
+ showActive: true,
+ canTest: true,
+ initialActivated: true,
+ },
+ mountFn: mountExtended,
+ });
+ mockFormFunctions({ checkValidityReturn: false });
+
+ await findProjectSaveButton().vm.$emit('click', new Event('click'));
+ });
- it('sets save button `loading` prop to `true`', () => {
- expect(findSaveButton().props('loading')).toBe(true);
- });
+ it('does not submit form', () => {
+ expect(findFormElement().submit).not.toHaveBeenCalled();
+ });
- it('sets test button `disabled` prop to `true`', () => {
- expect(findTestButton().props('disabled')).toBe(true);
- });
- });
+ it('sets save button `loading` prop to `false`', () => {
+ expect(findProjectSaveButton().props('loading')).toBe(false);
+ });
- describe.each`
- checkValidityReturn | integrationActive
- ${true} | ${false}
- ${true} | ${true}
- ${false} | ${false}
- `(
- 'when form is valid (checkValidity returns $checkValidityReturn and integrationActive is $integrationActive)',
- ({ integrationActive, checkValidityReturn }) => {
- beforeEach(async () => {
- createForm({ isValid: checkValidityReturn });
- createComponent({
- customStateProps: {
- showActive: true,
- canTest: true,
- initialActivated: integrationActive,
- },
+ it('sets test button `disabled` prop to `false`', () => {
+ expect(findTestButton().props('disabled')).toBe(false);
});
- await findSaveButton().vm.$emit('click', new Event('click'));
+ it('emits `VALIDATE_INTEGRATION_FORM_EVENT`', () => {
+ expect(eventHub.$emit).toHaveBeenCalledWith(VALIDATE_INTEGRATION_FORM_EVENT);
+ });
});
+ });
- it('submit form', () => {
- expect(mockForm.submit).toHaveBeenCalledTimes(1);
- });
- },
- );
+ describe('when `test` button is clicked', () => {
+ describe('when form is invalid', () => {
+ it('emits `VALIDATE_INTEGRATION_FORM_EVENT` event to the event hub', () => {
+ createComponent({
+ customStateProps: {
+ showActive: true,
+ canTest: true,
+ },
+ mountFn: mountExtended,
+ });
+ mockFormFunctions({ checkValidityReturn: false });
- describe('when form is invalid (checkValidity returns false and integrationActive is true)', () => {
- beforeEach(async () => {
- createForm({ isValid: false });
- createComponent({
- customStateProps: {
- showActive: true,
- canTest: true,
- initialActivated: true,
- },
+ findTestButton().vm.$emit('click', new Event('click'));
+
+ expect(eventHub.$emit).toHaveBeenCalledWith(VALIDATE_INTEGRATION_FORM_EVENT);
+ });
});
- await findSaveButton().vm.$emit('click', new Event('click'));
- });
+ describe('when form is valid', () => {
+ const mockTestPath = '/test';
- it('does not submit form', () => {
- expect(mockForm.submit).not.toHaveBeenCalled();
- });
+ beforeEach(() => {
+ createComponent({
+ customStateProps: {
+ showActive: true,
+ canTest: true,
+ testPath: mockTestPath,
+ },
+ mountFn: mountExtended,
+ });
+ mockFormFunctions({ checkValidityReturn: true });
+ });
- it('sets save button `loading` prop to `false`', () => {
- expect(findSaveButton().props('loading')).toBe(false);
- });
+ describe('buttons', () => {
+ beforeEach(async () => {
+ await findTestButton().vm.$emit('click', new Event('click'));
+ });
- it('sets test button `disabled` prop to `false`', () => {
- expect(findTestButton().props('disabled')).toBe(false);
- });
+ it('sets test button `loading` prop to `true`', () => {
+ expect(findTestButton().props('loading')).toBe(true);
+ });
- it('emits `VALIDATE_INTEGRATION_FORM_EVENT`', () => {
- expect(eventHub.$emit).toHaveBeenCalledWith(VALIDATE_INTEGRATION_FORM_EVENT);
+ it('sets save button `disabled` prop to `true`', () => {
+ expect(findProjectSaveButton().props('disabled')).toBe(true);
+ });
+ });
+
+ describe.each`
+ scenario | replyStatus | errorMessage | expectToast | expectSentry
+ ${'when "test settings" request fails'} | ${httpStatus.INTERNAL_SERVER_ERROR} | ${undefined} | ${I18N_DEFAULT_ERROR_MESSAGE} | ${true}
+ ${'when "test settings" returns an error'} | ${httpStatus.OK} | ${'an error'} | ${'an error'} | ${false}
+ ${'when "test settings" succeeds'} | ${httpStatus.OK} | ${undefined} | ${I18N_SUCCESSFUL_CONNECTION_MESSAGE} | ${false}
+ `('$scenario', ({ replyStatus, errorMessage, expectToast, expectSentry }) => {
+ beforeEach(async () => {
+ mockAxios.onPut(mockTestPath).replyOnce(replyStatus, {
+ error: Boolean(errorMessage),
+ message: errorMessage,
+ });
+
+ await findTestButton().vm.$emit('click', new Event('click'));
+ await waitForPromises();
+ });
+
+ it(`calls toast with '${expectToast}'`, () => {
+ expect(mockToastShow).toHaveBeenCalledWith(expectToast);
+ });
+
+ it('sets `loading` prop of test button to `false`', () => {
+ expect(findTestButton().props('loading')).toBe(false);
+ });
+
+ it('sets save button `disabled` prop to `false`', () => {
+ expect(findProjectSaveButton().props('disabled')).toBe(false);
+ });
+
+ it(`${expectSentry ? 'does' : 'does not'} capture exception in Sentry`, () => {
+ expect(Sentry.captureException).toHaveBeenCalledTimes(expectSentry ? 1 : 0);
+ });
+ });
+ });
});
- });
- });
+ },
+ );
+
+ describe('when `reset-confirmation-modal` emits `reset` event', () => {
+ const mockResetPath = '/reset';
- describe('when `test` button is clicked', () => {
- describe('when form is invalid', () => {
- it('emits `VALIDATE_INTEGRATION_FORM_EVENT` event to the event hub', () => {
- createForm({ isValid: false });
+ describe('buttons', () => {
+ beforeEach(async () => {
createComponent({
customStateProps: {
- showActive: true,
+ integrationLevel: integrationLevels.GROUP,
canTest: true,
+ resetPath: mockResetPath,
},
});
- findTestButton().vm.$emit('click', new Event('click'));
+ await findResetConfirmationModal().vm.$emit('reset');
+ });
- expect(eventHub.$emit).toHaveBeenCalledWith(VALIDATE_INTEGRATION_FORM_EVENT);
+ it('sets reset button `loading` prop to `true`', () => {
+ expect(findResetButton().props('loading')).toBe(true);
});
- });
- describe('when form is valid', () => {
- const mockTestPath = '/test';
+ it('sets other button `disabled` props to `true`', () => {
+ expect(findInstanceOrGroupSaveButton().props('disabled')).toBe(true);
+ expect(findTestButton().props('disabled')).toBe(true);
+ });
+ });
- beforeEach(() => {
- createForm({ isValid: true });
+ describe('when "reset settings" request fails', () => {
+ beforeEach(async () => {
+ mockAxios.onPost(mockResetPath).replyOnce(httpStatus.INTERNAL_SERVER_ERROR);
createComponent({
customStateProps: {
- showActive: true,
+ integrationLevel: integrationLevels.GROUP,
canTest: true,
- testPath: mockTestPath,
+ resetPath: mockResetPath,
},
});
- });
-
- describe('buttons', () => {
- beforeEach(async () => {
- await findTestButton().vm.$emit('click', new Event('click'));
- });
- it('sets test button `loading` prop to `true`', () => {
- expect(findTestButton().props('loading')).toBe(true);
- });
+ await findResetConfirmationModal().vm.$emit('reset');
+ await waitForPromises();
+ });
- it('sets save button `disabled` prop to `true`', () => {
- expect(findSaveButton().props('disabled')).toBe(true);
- });
+ it('displays a toast', () => {
+ expect(mockToastShow).toHaveBeenCalledWith(I18N_DEFAULT_ERROR_MESSAGE);
});
- describe.each`
- scenario | replyStatus | errorMessage | expectToast | expectSentry
- ${'when "test settings" request fails'} | ${httpStatus.INTERNAL_SERVER_ERROR} | ${undefined} | ${I18N_DEFAULT_ERROR_MESSAGE} | ${true}
- ${'when "test settings" returns an error'} | ${httpStatus.OK} | ${'an error'} | ${'an error'} | ${false}
- ${'when "test settings" succeeds'} | ${httpStatus.OK} | ${undefined} | ${I18N_SUCCESSFUL_CONNECTION_MESSAGE} | ${false}
- `('$scenario', ({ replyStatus, errorMessage, expectToast, expectSentry }) => {
- beforeEach(async () => {
- mockAxios.onPut(mockTestPath).replyOnce(replyStatus, {
- error: Boolean(errorMessage),
- message: errorMessage,
- });
+ it('captures exception in Sentry', () => {
+ expect(Sentry.captureException).toHaveBeenCalledTimes(1);
+ });
- await findTestButton().vm.$emit('click', new Event('click'));
- await waitForPromises();
- });
+ it('sets reset button `loading` prop to `false`', () => {
+ expect(findResetButton().props('loading')).toBe(false);
+ });
- it(`calls toast with '${expectToast}'`, () => {
- expect(mockToastShow).toHaveBeenCalledWith(expectToast);
- });
+ it('sets button `disabled` props to `false`', () => {
+ expect(findInstanceOrGroupSaveButton().props('disabled')).toBe(false);
+ expect(findTestButton().props('disabled')).toBe(false);
+ });
+ });
- it('sets `loading` prop of test button to `false`', () => {
- expect(findTestButton().props('loading')).toBe(false);
+ describe('when "reset settings" succeeds', () => {
+ beforeEach(async () => {
+ mockAxios.onPost(mockResetPath).replyOnce(httpStatus.OK);
+ createComponent({
+ customStateProps: {
+ integrationLevel: integrationLevels.GROUP,
+ resetPath: mockResetPath,
+ },
});
- it('sets save button `disabled` prop to `false`', () => {
- expect(findSaveButton().props('disabled')).toBe(false);
- });
+ await findResetConfirmationModal().vm.$emit('reset');
+ await waitForPromises();
+ });
- it(`${expectSentry ? 'does' : 'does not'} capture exception in Sentry`, () => {
- expect(Sentry.captureException).toHaveBeenCalledTimes(expectSentry ? 1 : 0);
- });
+ it('calls `refreshCurrentPage`', () => {
+ expect(refreshCurrentPage).toHaveBeenCalledTimes(1);
});
});
});
diff --git a/spec/frontend/integrations/edit/store/actions_spec.js b/spec/frontend/integrations/edit/store/actions_spec.js
index b413de2b286..a5627d8b669 100644
--- a/spec/frontend/integrations/edit/store/actions_spec.js
+++ b/spec/frontend/integrations/edit/store/actions_spec.js
@@ -4,17 +4,12 @@ import testAction from 'helpers/vuex_action_helper';
import { I18N_FETCH_TEST_SETTINGS_DEFAULT_ERROR_MESSAGE } from '~/integrations/constants';
import {
setOverride,
- setIsResetting,
- requestResetIntegration,
- receiveResetIntegrationSuccess,
- receiveResetIntegrationError,
requestJiraIssueTypes,
receiveJiraIssueTypesSuccess,
receiveJiraIssueTypesError,
} from '~/integrations/edit/store/actions';
import * as types from '~/integrations/edit/store/mutation_types';
import createState from '~/integrations/edit/store/state';
-import { refreshCurrentPage } from '~/lib/utils/url_utility';
import { mockJiraIssueTypes } from '../mock_data';
jest.mock('~/lib/utils/url_utility');
@@ -38,38 +33,6 @@ describe('Integration form store actions', () => {
});
});
- describe('setIsResetting', () => {
- it('should commit isResetting mutation', () => {
- return testAction(setIsResetting, true, state, [
- { type: types.SET_IS_RESETTING, payload: true },
- ]);
- });
- });
-
- describe('requestResetIntegration', () => {
- it('should commit REQUEST_RESET_INTEGRATION mutation', () => {
- return testAction(requestResetIntegration, null, state, [
- { type: types.REQUEST_RESET_INTEGRATION },
- ]);
- });
- });
-
- describe('receiveResetIntegrationSuccess', () => {
- it('should call refreshCurrentPage()', () => {
- return testAction(receiveResetIntegrationSuccess, null, state, [], [], () => {
- expect(refreshCurrentPage).toHaveBeenCalled();
- });
- });
- });
-
- describe('receiveResetIntegrationError', () => {
- it('should commit RECEIVE_RESET_INTEGRATION_ERROR mutation', () => {
- return testAction(receiveResetIntegrationError, null, state, [
- { type: types.RECEIVE_RESET_INTEGRATION_ERROR },
- ]);
- });
- });
-
describe('requestJiraIssueTypes', () => {
describe.each`
scenario | responseCode | response | action
diff --git a/spec/frontend/integrations/edit/store/mutations_spec.js b/spec/frontend/integrations/edit/store/mutations_spec.js
index 641547550d1..ecac9d88982 100644
--- a/spec/frontend/integrations/edit/store/mutations_spec.js
+++ b/spec/frontend/integrations/edit/store/mutations_spec.js
@@ -17,30 +17,6 @@ describe('Integration form store mutations', () => {
});
});
- describe(`${types.SET_IS_RESETTING}`, () => {
- it('sets isResetting', () => {
- mutations[types.SET_IS_RESETTING](state, true);
-
- expect(state.isResetting).toBe(true);
- });
- });
-
- describe(`${types.REQUEST_RESET_INTEGRATION}`, () => {
- it('sets isResetting', () => {
- mutations[types.REQUEST_RESET_INTEGRATION](state);
-
- expect(state.isResetting).toBe(true);
- });
- });
-
- describe(`${types.RECEIVE_RESET_INTEGRATION_ERROR}`, () => {
- it('sets isResetting', () => {
- mutations[types.RECEIVE_RESET_INTEGRATION_ERROR](state);
-
- expect(state.isResetting).toBe(false);
- });
- });
-
describe(`${types.SET_JIRA_ISSUE_TYPES}`, () => {
it('sets jiraIssueTypes', () => {
const jiraIssueTypes = ['issue', 'epic'];
diff --git a/spec/frontend/integrations/edit/store/state_spec.js b/spec/frontend/integrations/edit/store/state_spec.js
index 5582be7fd3c..0b4ca8fb65c 100644
--- a/spec/frontend/integrations/edit/store/state_spec.js
+++ b/spec/frontend/integrations/edit/store/state_spec.js
@@ -5,8 +5,6 @@ describe('Integration form state factory', () => {
expect(createState()).toEqual({
defaultState: null,
customState: {},
- isSaving: false,
- isResetting: false,
override: false,
isLoadingJiraIssueTypes: false,
jiraIssueTypes: [],
diff --git a/spec/frontend/integrations/overrides/components/integration_overrides_spec.js b/spec/frontend/integrations/overrides/components/integration_overrides_spec.js
index 8abd83887f7..6aa3e661677 100644
--- a/spec/frontend/integrations/overrides/components/integration_overrides_spec.js
+++ b/spec/frontend/integrations/overrides/components/integration_overrides_spec.js
@@ -5,6 +5,8 @@ import MockAdapter from 'axios-mock-adapter';
import waitForPromises from 'helpers/wait_for_promises';
import { DEFAULT_PER_PAGE } from '~/api';
import IntegrationOverrides from '~/integrations/overrides/components/integration_overrides.vue';
+import IntegrationTabs from '~/integrations/overrides/components/integration_tabs.vue';
+
import axios from '~/lib/utils/axios_utils';
import httpStatus from '~/lib/utils/http_status';
import ProjectAvatar from '~/vue_shared/components/project_avatar.vue';
@@ -49,6 +51,7 @@ describe('IntegrationOverrides', () => {
const findGlTable = () => wrapper.findComponent(GlTable);
const findPagination = () => wrapper.findComponent(GlPagination);
+ const findIntegrationTabs = () => wrapper.findComponent(IntegrationTabs);
const findRowsAsModel = () =>
findGlTable()
.findAllComponents(GlLink)
@@ -72,6 +75,12 @@ describe('IntegrationOverrides', () => {
expect(table.exists()).toBe(true);
expect(table.attributes('busy')).toBe('true');
});
+
+ it('renders IntegrationTabs with count as `null`', () => {
+ createComponent();
+
+ expect(findIntegrationTabs().props('projectOverridesCount')).toBe(null);
+ });
});
describe('when initial request is successful', () => {
@@ -84,6 +93,13 @@ describe('IntegrationOverrides', () => {
expect(table.attributes('busy')).toBeFalsy();
});
+ it('renders IntegrationTabs with count', async () => {
+ createComponent();
+ await waitForPromises();
+
+ expect(findIntegrationTabs().props('projectOverridesCount')).toBe(mockOverrides.length);
+ });
+
describe('table template', () => {
beforeEach(async () => {
createComponent({ mountFn: mount });
diff --git a/spec/frontend/integrations/overrides/components/integration_tabs_spec.js b/spec/frontend/integrations/overrides/components/integration_tabs_spec.js
new file mode 100644
index 00000000000..a728b4d391f
--- /dev/null
+++ b/spec/frontend/integrations/overrides/components/integration_tabs_spec.js
@@ -0,0 +1,64 @@
+import { mount, shallowMount } from '@vue/test-utils';
+import { GlBadge, GlTab } from '@gitlab/ui';
+
+import IntegrationTabs from '~/integrations/overrides/components/integration_tabs.vue';
+import { settingsTabTitle, overridesTabTitle } from '~/integrations/constants';
+
+describe('IntegrationTabs', () => {
+ let wrapper;
+
+ const editPath = 'mock/edit';
+
+ const createComponent = ({ mountFn = shallowMount, props = {} } = {}) => {
+ wrapper = mountFn(IntegrationTabs, {
+ propsData: props,
+ provide: {
+ editPath,
+ },
+ stubs: {
+ GlTab,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ const findGlBadge = () => wrapper.findComponent(GlBadge);
+ const findGlTab = () => wrapper.findComponent(GlTab);
+ const findSettingsLink = () => wrapper.find('a');
+
+ describe('template', () => {
+ it('renders "Settings" tab as a link', () => {
+ createComponent({ mountFn: mount });
+
+ expect(findSettingsLink().text()).toMatchInterpolatedText(settingsTabTitle);
+ expect(findSettingsLink().attributes('href')).toBe(editPath);
+ });
+
+ it('renders "Projects using custom settings" tab as active', () => {
+ const projectOverridesCount = '1';
+
+ createComponent({
+ props: { projectOverridesCount },
+ });
+
+ expect(findGlTab().exists()).toBe(true);
+ expect(findGlTab().text()).toMatchInterpolatedText(
+ `${overridesTabTitle} ${projectOverridesCount}`,
+ );
+ expect(findGlBadge().text()).toBe(projectOverridesCount);
+ });
+
+ describe('when count is `null', () => {
+ it('renders "Projects using custom settings" tab without count', () => {
+ createComponent();
+
+ expect(findGlTab().exists()).toBe(true);
+ expect(findGlTab().text()).toMatchInterpolatedText(overridesTabTitle);
+ expect(findGlBadge().exists()).toBe(false);
+ });
+ });
+ });
+});
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 e190ddf243e..3ab89b3dff2 100644
--- a/spec/frontend/invite_members/components/invite_members_modal_spec.js
+++ b/spec/frontend/invite_members/components/invite_members_modal_spec.js
@@ -474,6 +474,8 @@ describe('InviteMembersModal', () => {
beforeEach(() => {
createInviteMembersToGroupWrapper();
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({ newUsersToInvite: [user1] });
});
@@ -644,6 +646,8 @@ describe('InviteMembersModal', () => {
beforeEach(() => {
createInviteMembersToGroupWrapper();
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({ newUsersToInvite: [user3] });
});
@@ -712,6 +716,8 @@ describe('InviteMembersModal', () => {
it('displays the invalid syntax error if one of the emails is invalid', async () => {
createInviteMembersToGroupWrapper();
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({ newUsersToInvite: [user3, user4] });
mockInvitationsApi(httpStatus.CREATED, invitationsApiResponse.ERROR_EMAIL_INVALID);
@@ -787,6 +793,8 @@ describe('InviteMembersModal', () => {
beforeEach(() => {
createInviteMembersToGroupWrapper();
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({ newUsersToInvite: [user1, user3] });
mockInvitationsApi(httpStatus.BAD_REQUEST, invitationsApiResponse.EMAIL_INVALID);
@@ -815,6 +823,8 @@ describe('InviteMembersModal', () => {
beforeEach(() => {
createComponent({ groupToBeSharedWith: sharedGroup });
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({ inviteeType: 'group' });
wrapper.vm.$toast = { show: jest.fn() };
jest.spyOn(Api, 'groupShareWithGroup').mockResolvedValue({ data: groupPostData });
@@ -837,6 +847,8 @@ describe('InviteMembersModal', () => {
beforeEach(() => {
createInviteGroupToGroupWrapper();
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({ groupToBeSharedWith: sharedGroup });
wrapper.vm.$toast = { show: jest.fn() };
diff --git a/spec/frontend/invite_members/mock_data/api_responses.js b/spec/frontend/invite_members/mock_data/api_responses.js
index dd84b4fd78f..a3e426376d8 100644
--- a/spec/frontend/invite_members/mock_data/api_responses.js
+++ b/spec/frontend/invite_members/mock_data/api_responses.js
@@ -26,7 +26,7 @@ const INVITATIONS_API_MULTIPLE_EMAIL_RESTRICTED = {
const INVITATIONS_API_EMAIL_TAKEN = {
message: {
- 'email@example2.com': 'Invite email has already been taken',
+ 'email@example.org': 'Invite email has already been taken',
},
status: 'error',
};
diff --git a/spec/frontend/issuable/components/related_issuable_item_spec.js b/spec/frontend/issuable/components/related_issuable_item_spec.js
index 6ac4c9e8546..6a896ccd21a 100644
--- a/spec/frontend/issuable/components/related_issuable_item_spec.js
+++ b/spec/frontend/issuable/components/related_issuable_item_spec.js
@@ -169,6 +169,8 @@ describe('RelatedIssuableItem', () => {
});
it('renders disabled button when removeDisabled', async () => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({ removeDisabled: true });
await wrapper.vm.$nextTick();
diff --git a/spec/frontend/create_merge_request_dropdown_spec.js b/spec/frontend/issues/create_merge_request_dropdown_spec.js
index 9f07eea433a..fdc0bd7d72e 100644
--- a/spec/frontend/create_merge_request_dropdown_spec.js
+++ b/spec/frontend/issues/create_merge_request_dropdown_spec.js
@@ -1,7 +1,7 @@
import MockAdapter from 'axios-mock-adapter';
import { TEST_HOST } from 'helpers/test_constants';
import confidentialState from '~/confidential_merge_request/state';
-import CreateMergeRequestDropdown from '~/create_merge_request_dropdown';
+import CreateMergeRequestDropdown from '~/issues/create_merge_request_dropdown';
import axios from '~/lib/utils/axios_utils';
describe('CreateMergeRequestDropdown', () => {
diff --git a/spec/frontend/issues_list/components/issue_card_time_info_spec.js b/spec/frontend/issues/list/components/issue_card_time_info_spec.js
index 7c5faeb8dc1..e9c48b60da4 100644
--- a/spec/frontend/issues_list/components/issue_card_time_info_spec.js
+++ b/spec/frontend/issues/list/components/issue_card_time_info_spec.js
@@ -1,7 +1,7 @@
import { GlIcon, GlLink } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { useFakeDate } from 'helpers/fake_date';
-import IssueCardTimeInfo from '~/issues_list/components/issue_card_time_info.vue';
+import IssueCardTimeInfo from '~/issues/list/components/issue_card_time_info.vue';
describe('CE IssueCardTimeInfo component', () => {
useFakeDate(2020, 11, 11);
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 f24c090fa92..66428ee0492 100644
--- a/spec/frontend/issues_list/components/issues_list_app_spec.js
+++ b/spec/frontend/issues/list/components/issues_list_app_spec.js
@@ -5,8 +5,8 @@ import AxiosMockAdapter from 'axios-mock-adapter';
import { cloneDeep } from 'lodash';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
-import getIssuesQuery from 'ee_else_ce/issues_list/queries/get_issues.query.graphql';
-import getIssuesCountsQuery from 'ee_else_ce/issues_list/queries/get_issues_counts.query.graphql';
+import getIssuesQuery from 'ee_else_ce/issues/list/queries/get_issues.query.graphql';
+import getIssuesCountsQuery from 'ee_else_ce/issues/list/queries/get_issues_counts.query.graphql';
import createMockApollo from 'helpers/mock_apollo_helper';
import setWindowLocation from 'helpers/set_window_location_helper';
import { TEST_HOST } from 'helpers/test_constants';
@@ -17,15 +17,15 @@ import {
filteredTokens,
locationSearch,
urlParams,
-} from 'jest/issues_list/mock_data';
+} from 'jest/issues/list/mock_data';
import createFlash, { FLASH_TYPES } from '~/flash';
import { convertToGraphQLId, getIdFromGraphQLId } from '~/graphql_shared/utils';
import CsvImportExportButtons from '~/issuable/components/csv_import_export_buttons.vue';
import IssuableByEmail from '~/issuable/components/issuable_by_email.vue';
import IssuableList from '~/vue_shared/issuable/list/components/issuable_list_root.vue';
import { IssuableListTabs, IssuableStates } from '~/vue_shared/issuable/list/constants';
-import IssuesListApp from '~/issues_list/components/issues_list_app.vue';
-import NewIssueDropdown from '~/issues_list/components/new_issue_dropdown.vue';
+import IssuesListApp from '~/issues/list/components/issues_list_app.vue';
+import NewIssueDropdown from '~/issues/list/components/new_issue_dropdown.vue';
import {
CREATED_DESC,
DUE_DATE_OVERDUE,
@@ -41,9 +41,9 @@ import {
TOKEN_TYPE_RELEASE,
TOKEN_TYPE_TYPE,
urlSortParams,
-} from '~/issues_list/constants';
-import eventHub from '~/issues_list/eventhub';
-import { getSortOptions } from '~/issues_list/utils';
+} from '~/issues/list/constants';
+import eventHub from '~/issues/list/eventhub';
+import { getSortOptions } from '~/issues/list/utils';
import axios from '~/lib/utils/axios_utils';
import { scrollUp } from '~/lib/utils/scroll_utils';
import { joinPaths } from '~/lib/utils/url_utility';
diff --git a/spec/frontend/issues_list/components/jira_issues_import_status_app_spec.js b/spec/frontend/issues/list/components/jira_issues_import_status_app_spec.js
index 633799816d8..d6d6bb14e9d 100644
--- a/spec/frontend/issues_list/components/jira_issues_import_status_app_spec.js
+++ b/spec/frontend/issues/list/components/jira_issues_import_status_app_spec.js
@@ -1,7 +1,7 @@
import { GlAlert, GlLabel } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
-import JiraIssuesImportStatus from '~/issues_list/components/jira_issues_import_status_app.vue';
+import JiraIssuesImportStatus from '~/issues/list/components/jira_issues_import_status_app.vue';
describe('JiraIssuesImportStatus', () => {
const issuesPath = 'gitlab-org/gitlab-test/-/issues';
diff --git a/spec/frontend/issues_list/components/new_issue_dropdown_spec.js b/spec/frontend/issues/list/components/new_issue_dropdown_spec.js
index 1c9a87e8af2..0c52e66ff14 100644
--- a/spec/frontend/issues_list/components/new_issue_dropdown_spec.js
+++ b/spec/frontend/issues/list/components/new_issue_dropdown_spec.js
@@ -2,8 +2,8 @@ import { GlDropdown, GlDropdownItem, GlSearchBoxByType } from '@gitlab/ui';
import { createLocalVue, mount, shallowMount } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
-import NewIssueDropdown from '~/issues_list/components/new_issue_dropdown.vue';
-import searchProjectsQuery from '~/issues_list/queries/search_projects.query.graphql';
+import NewIssueDropdown from '~/issues/list/components/new_issue_dropdown.vue';
+import searchProjectsQuery from '~/issues/list/queries/search_projects.query.graphql';
import { DASH_SCOPE, joinPaths } from '~/lib/utils/url_utility';
import {
emptySearchProjectsQueryResponse,
diff --git a/spec/frontend/issues_list/mock_data.js b/spec/frontend/issues/list/mock_data.js
index 948699876ce..948699876ce 100644
--- a/spec/frontend/issues_list/mock_data.js
+++ b/spec/frontend/issues/list/mock_data.js
diff --git a/spec/frontend/issues_list/utils_spec.js b/spec/frontend/issues/list/utils_spec.js
index 8e1d70db92d..0e4979fd7b4 100644
--- a/spec/frontend/issues_list/utils_spec.js
+++ b/spec/frontend/issues/list/utils_spec.js
@@ -7,14 +7,14 @@ import {
locationSearchWithSpecialValues,
urlParams,
urlParamsWithSpecialValues,
-} from 'jest/issues_list/mock_data';
+} from 'jest/issues/list/mock_data';
import {
defaultPageSizeParams,
DUE_DATE_VALUES,
largePageSizeParams,
RELATIVE_POSITION_ASC,
urlSortParams,
-} from '~/issues_list/constants';
+} from '~/issues/list/constants';
import {
convertToApiParams,
convertToSearchQuery,
@@ -24,7 +24,7 @@ import {
getInitialPageParams,
getSortKey,
getSortOptions,
-} from '~/issues_list/utils';
+} from '~/issues/list/utils';
describe('getInitialPageParams', () => {
it.each(Object.keys(urlSortParams))(
diff --git a/spec/frontend/issues/new/components/title_suggestions_spec.js b/spec/frontend/issues/new/components/title_suggestions_spec.js
index 984d0c9d25b..f6b93cc5a62 100644
--- a/spec/frontend/issues/new/components/title_suggestions_spec.js
+++ b/spec/frontend/issues/new/components/title_suggestions_spec.js
@@ -38,6 +38,8 @@ describe('Issue title suggestions component', () => {
});
it('renders component', () => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData(data);
return wrapper.vm.$nextTick(() => {
@@ -47,6 +49,8 @@ describe('Issue title suggestions component', () => {
it('does not render with empty search', () => {
wrapper.setProps({ search: '' });
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData(data);
return wrapper.vm.$nextTick(() => {
@@ -55,6 +59,8 @@ describe('Issue title suggestions component', () => {
});
it('does not render when loading', () => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
...data,
loading: 1,
@@ -66,6 +72,8 @@ describe('Issue title suggestions component', () => {
});
it('does not render with empty issues data', () => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({ issues: [] });
return wrapper.vm.$nextTick(() => {
@@ -74,6 +82,8 @@ describe('Issue title suggestions component', () => {
});
it('renders list of issues', () => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData(data);
return wrapper.vm.$nextTick(() => {
@@ -82,6 +92,8 @@ describe('Issue title suggestions component', () => {
});
it('adds margin class to first item', () => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData(data);
return wrapper.vm.$nextTick(() => {
@@ -90,6 +102,8 @@ describe('Issue title suggestions component', () => {
});
it('does not add margin class to last item', () => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData(data);
return wrapper.vm.$nextTick(() => {
diff --git a/spec/frontend/issues/show/components/fields/type_spec.js b/spec/frontend/issues/show/components/fields/type_spec.js
index 3ece10e70db..7f7b16583e6 100644
--- a/spec/frontend/issues/show/components/fields/type_spec.js
+++ b/spec/frontend/issues/show/components/fields/type_spec.js
@@ -4,7 +4,7 @@ import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import IssueTypeField, { i18n } from '~/issues/show/components/fields/type.vue';
-import { IssuableTypes } from '~/issues/show/constants';
+import { issuableTypes } from '~/issues/show/constants';
import {
getIssueStateQueryResponse,
updateIssueStateQueryResponse,
@@ -69,8 +69,8 @@ describe('Issue type field component', () => {
it.each`
at | text | icon
- ${0} | ${IssuableTypes[0].text} | ${IssuableTypes[0].icon}
- ${1} | ${IssuableTypes[1].text} | ${IssuableTypes[1].icon}
+ ${0} | ${issuableTypes[0].text} | ${issuableTypes[0].icon}
+ ${1} | ${issuableTypes[1].text} | ${issuableTypes[1].icon}
`(`renders the issue type $text with an icon in the dropdown`, ({ at, text, icon }) => {
expect(findTypeFromDropDownItemIconAt(at).attributes('name')).toBe(icon);
expect(findTypeFromDropDownItemAt(at).text()).toBe(text);
@@ -81,20 +81,20 @@ describe('Issue type field component', () => {
});
it('renders a form select with the `issue_type` value', () => {
- expect(findTypeFromDropDown().attributes('value')).toBe(IssuableTypes.issue);
+ expect(findTypeFromDropDown().attributes('value')).toBe(issuableTypes.issue);
});
describe('with Apollo cache mock', () => {
it('renders the selected issueType', async () => {
mockIssueStateData.mockResolvedValue(getIssueStateQueryResponse);
await waitForPromises();
- expect(findTypeFromDropDown().attributes('value')).toBe(IssuableTypes.issue);
+ expect(findTypeFromDropDown().attributes('value')).toBe(issuableTypes.issue);
});
it('updates the `issue_type` in the apollo cache when the value is changed', async () => {
- findTypeFromDropDownItems().at(1).vm.$emit('click', IssuableTypes.incident);
+ findTypeFromDropDownItems().at(1).vm.$emit('click', issuableTypes.incident);
await wrapper.vm.$nextTick();
- expect(findTypeFromDropDown().attributes('value')).toBe(IssuableTypes.incident);
+ expect(findTypeFromDropDown().attributes('value')).toBe(issuableTypes.incident);
});
describe('when user is a guest', () => {
@@ -104,7 +104,7 @@ describe('Issue type field component', () => {
expect(findTypeFromDropDownItemAt(0).isVisible()).toBe(true);
expect(findTypeFromDropDownItemAt(1).isVisible()).toBe(false);
- expect(findTypeFromDropDown().attributes('value')).toBe(IssuableTypes.issue);
+ expect(findTypeFromDropDown().attributes('value')).toBe(issuableTypes.issue);
});
it('and incident is selected, includes incident in the dropdown', async () => {
@@ -113,7 +113,7 @@ describe('Issue type field component', () => {
expect(findTypeFromDropDownItemAt(0).isVisible()).toBe(true);
expect(findTypeFromDropDownItemAt(1).isVisible()).toBe(true);
- expect(findTypeFromDropDown().attributes('value')).toBe(IssuableTypes.incident);
+ expect(findTypeFromDropDown().attributes('value')).toBe(issuableTypes.incident);
});
});
});
diff --git a/spec/frontend/issues/show/components/header_actions_spec.js b/spec/frontend/issues/show/components/header_actions_spec.js
index 2a16c699c4d..d09bf6faa13 100644
--- a/spec/frontend/issues/show/components/header_actions_spec.js
+++ b/spec/frontend/issues/show/components/header_actions_spec.js
@@ -4,11 +4,10 @@ import { shallowMount } from '@vue/test-utils';
import Vuex from 'vuex';
import { mockTracking } from 'helpers/tracking_helper';
import createFlash, { FLASH_TYPES } from '~/flash';
-import { IssuableType } from '~/vue_shared/issuable/show/constants';
+import { IssuableStatus, IssueType } from '~/issues/constants';
import DeleteIssueModal from '~/issues/show/components/delete_issue_modal.vue';
import HeaderActions from '~/issues/show/components/header_actions.vue';
-import { IssuableStatus } from '~/issues/constants';
-import { IssueStateEvent } from '~/issues/show/constants';
+import { ISSUE_STATE_EVENT_CLOSE, ISSUE_STATE_EVENT_REOPEN } from '~/issues/show/constants';
import promoteToEpicMutation from '~/issues/show/queries/promote_to_epic.mutation.graphql';
import * as urlUtility from '~/lib/utils/url_utility';
import eventHub from '~/notes/event_hub';
@@ -36,7 +35,7 @@ describe('HeaderActions component', () => {
iid: '32',
isIssueAuthor: true,
issuePath: 'gitlab-org/gitlab-test/-/issues/1',
- issueType: IssuableType.Issue,
+ issueType: IssueType.Issue,
newIssuePath: 'gitlab-org/gitlab-test/-/issues/new',
projectPath: 'gitlab-org/gitlab-test',
reportAbusePath:
@@ -112,14 +111,14 @@ describe('HeaderActions component', () => {
describe.each`
issueType
- ${IssuableType.Issue}
- ${IssuableType.Incident}
+ ${IssueType.Issue}
+ ${IssueType.Incident}
`('when issue type is $issueType', ({ issueType }) => {
describe('close/reopen button', () => {
describe.each`
description | issueState | buttonText | newIssueState
- ${`when the ${issueType} is open`} | ${IssuableStatus.Open} | ${`Close ${issueType}`} | ${IssueStateEvent.Close}
- ${`when the ${issueType} is closed`} | ${IssuableStatus.Closed} | ${`Reopen ${issueType}`} | ${IssueStateEvent.Reopen}
+ ${`when the ${issueType} is open`} | ${IssuableStatus.Open} | ${`Close ${issueType}`} | ${ISSUE_STATE_EVENT_CLOSE}
+ ${`when the ${issueType} is closed`} | ${IssuableStatus.Closed} | ${`Reopen ${issueType}`} | ${ISSUE_STATE_EVENT_REOPEN}
`('$description', ({ issueState, buttonText, newIssueState }) => {
beforeEach(() => {
dispatchEventSpy = jest.spyOn(document, 'dispatchEvent');
@@ -306,7 +305,7 @@ describe('HeaderActions component', () => {
input: {
iid: defaultProps.iid,
projectPath: defaultProps.projectPath,
- stateEvent: IssueStateEvent.Close,
+ stateEvent: ISSUE_STATE_EVENT_CLOSE,
},
},
}),
@@ -345,7 +344,7 @@ describe('HeaderActions component', () => {
input: {
iid: defaultProps.iid.toString(),
projectPath: defaultProps.projectPath,
- stateEvent: IssueStateEvent.Close,
+ stateEvent: ISSUE_STATE_EVENT_CLOSE,
},
},
}),
diff --git a/spec/frontend/issues/sentry_error_stack_trace/components/sentry_error_stack_trace_spec.js b/spec/frontend/issues/show/components/sentry_error_stack_trace_spec.js
index 5a51ae3cfe0..b38d2b60057 100644
--- a/spec/frontend/issues/sentry_error_stack_trace/components/sentry_error_stack_trace_spec.js
+++ b/spec/frontend/issues/show/components/sentry_error_stack_trace_spec.js
@@ -1,11 +1,9 @@
+import Vue from 'vue';
import { GlLoadingIcon } from '@gitlab/ui';
-import { createLocalVue, shallowMount } from '@vue/test-utils';
+import { shallowMount } from '@vue/test-utils';
import Vuex from 'vuex';
import Stacktrace from '~/error_tracking/components/stacktrace.vue';
-import SentryErrorStackTrace from '~/issues/sentry_error_stack_trace/components/sentry_error_stack_trace.vue';
-
-const localVue = createLocalVue();
-localVue.use(Vuex);
+import SentryErrorStackTrace from '~/issues/show/components/sentry_error_stack_trace.vue';
describe('Sentry Error Stack Trace', () => {
let actions;
@@ -13,13 +11,14 @@ describe('Sentry Error Stack Trace', () => {
let store;
let wrapper;
+ Vue.use(Vuex);
+
function mountComponent({
stubs = {
stacktrace: Stacktrace,
},
} = {}) {
wrapper = shallowMount(SentryErrorStackTrace, {
- localVue,
stubs,
store,
propsData: {
diff --git a/spec/frontend/issues/show/issue_spec.js b/spec/frontend/issues/show/issue_spec.js
index 6d7a31a6c8c..68c2e3768c7 100644
--- a/spec/frontend/issues/show/issue_spec.js
+++ b/spec/frontend/issues/show/issue_spec.js
@@ -1,6 +1,6 @@
import MockAdapter from 'axios-mock-adapter';
import waitForPromises from 'helpers/wait_for_promises';
-import { initIssuableApp } from '~/issues/show/issue';
+import { initIssueApp } from '~/issues/show';
import * as parseData from '~/issues/show/utils/parse_data';
import axios from '~/lib/utils/axios_utils';
import createStore from '~/notes/stores';
@@ -17,7 +17,7 @@ const setupHTML = (initialData) => {
};
describe('Issue show index', () => {
- describe('initIssuableApp', () => {
+ describe('initIssueApp', () => {
it('should initialize app with no potential XSS attack', async () => {
const alertSpy = jest.spyOn(window, 'alert').mockImplementation(() => {});
const parseDataSpy = jest.spyOn(parseData, 'parseIssuableData');
@@ -29,7 +29,7 @@ describe('Issue show index', () => {
const initialDataEl = document.getElementById('js-issuable-app');
const issuableData = parseData.parseIssuableData(initialDataEl);
- initIssuableApp(issuableData, createStore());
+ initIssueApp(issuableData, createStore());
await waitForPromises();
diff --git a/spec/frontend/issues_list/components/__snapshots__/issuables_list_app_spec.js.snap b/spec/frontend/issues_list/components/__snapshots__/issuables_list_app_spec.js.snap
deleted file mode 100644
index c327b7de827..00000000000
--- a/spec/frontend/issues_list/components/__snapshots__/issuables_list_app_spec.js.snap
+++ /dev/null
@@ -1,14 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`Issuables list component with empty issues response with all state should display a catch-all if there are no issues to show 1`] = `
-<gl-empty-state-stub
- svgpath="/emptySvg"
- title="There are no issues to show"
-/>
-`;
-
-exports[`Issuables list component with empty issues response with closed state should display a message "There are no closed issues" if there are no closed issues 1`] = `"There are no closed issues"`;
-
-exports[`Issuables list component with empty issues response with empty query should display the message "There are no open issues" 1`] = `"There are no open issues"`;
-
-exports[`Issuables list component with empty issues response with query in window location should display "Sorry, your filter produced no results" if filters are too specific 1`] = `"Sorry, your filter produced no results"`;
diff --git a/spec/frontend/issues_list/components/issuable_spec.js b/spec/frontend/issues_list/components/issuable_spec.js
deleted file mode 100644
index f3c2ae1f9dc..00000000000
--- a/spec/frontend/issues_list/components/issuable_spec.js
+++ /dev/null
@@ -1,508 +0,0 @@
-import { GlSprintf, GlLabel, GlIcon, GlLink } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
-import { TEST_HOST } from 'helpers/test_constants';
-import { trimText } from 'helpers/text_helper';
-import Issuable from '~/issues_list/components/issuable.vue';
-import { isScopedLabel } from '~/lib/utils/common_utils';
-import { formatDate } from '~/lib/utils/datetime_utility';
-import { mergeUrlParams } from '~/lib/utils/url_utility';
-import initUserPopovers from '~/user_popovers';
-import IssueAssignees from '~/issuable/components/issue_assignees.vue';
-import { simpleIssue, testAssignees, testLabels } from '../issuable_list_test_data';
-
-jest.mock('~/user_popovers');
-
-const TODAY = new Date();
-
-const createTestDateFromDelta = (timeDelta) =>
- formatDate(new Date(TODAY.getTime() + timeDelta), 'yyyy-mm-dd');
-
-// TODO: Encapsulate date helpers https://gitlab.com/gitlab-org/gitlab/-/issues/320883
-const MONTHS_IN_MS = 1000 * 60 * 60 * 24 * 31;
-const TEST_MONTH_AGO = createTestDateFromDelta(-MONTHS_IN_MS);
-const TEST_MONTH_LATER = createTestDateFromDelta(MONTHS_IN_MS);
-const DATE_FORMAT = 'mmm d, yyyy';
-const TEST_USER_NAME = 'Tyler Durden';
-const TEST_BASE_URL = `${TEST_HOST}/issues`;
-const TEST_TASK_STATUS = '50 of 100 tasks completed';
-const TEST_MILESTONE = {
- title: 'Milestone title',
- web_url: `${TEST_HOST}/milestone/1`,
-};
-const TEXT_CLOSED = 'CLOSED';
-const TEST_META_COUNT = 100;
-const MOCK_GITLAB_URL = 'http://0.0.0.0:3000';
-
-describe('Issuable component', () => {
- let issuable;
- let wrapper;
-
- const factory = (props = {}, scopedLabelsAvailable = false) => {
- wrapper = shallowMount(Issuable, {
- propsData: {
- issuable: simpleIssue,
- baseUrl: TEST_BASE_URL,
- ...props,
- },
- provide: {
- scopedLabelsAvailable,
- },
- stubs: {
- 'gl-sprintf': GlSprintf,
- },
- });
- };
-
- beforeEach(() => {
- issuable = { ...simpleIssue };
- gon.gitlab_url = MOCK_GITLAB_URL;
- });
-
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
- const checkExists = (findFn) => () => findFn().exists();
- const hasIcon = (iconName, iconWrapper = wrapper) =>
- iconWrapper.findAll(GlIcon).wrappers.some((icon) => icon.props('name') === iconName);
- const hasConfidentialIcon = () => hasIcon('eye-slash');
- const findTaskStatus = () => wrapper.find('.task-status');
- const findOpenedAgoContainer = () => wrapper.find('[data-testid="openedByMessage"]');
- const findAuthor = () => wrapper.find({ ref: 'openedAgoByContainer' });
- const findMilestone = () => wrapper.find('.js-milestone');
- const findMilestoneTooltip = () => findMilestone().attributes('title');
- const findDueDate = () => wrapper.find('.js-due-date');
- const findLabels = () => wrapper.findAll(GlLabel);
- const findWeight = () => wrapper.find('[data-testid="weight"]');
- const findAssignees = () => wrapper.find(IssueAssignees);
- const findBlockingIssuesCount = () => wrapper.find('[data-testid="blocking-issues"]');
- const findMergeRequestsCount = () => wrapper.find('[data-testid="merge-requests"]');
- const findUpvotes = () => wrapper.find('[data-testid="upvotes"]');
- const findDownvotes = () => wrapper.find('[data-testid="downvotes"]');
- const findNotes = () => wrapper.find('[data-testid="notes-count"]');
- const findBulkCheckbox = () => wrapper.find('input.selected-issuable');
- const findScopedLabels = () => findLabels().filter((w) => isScopedLabel({ title: w.text() }));
- const findUnscopedLabels = () => findLabels().filter((w) => !isScopedLabel({ title: w.text() }));
- const findIssuableTitle = () => wrapper.find('[data-testid="issuable-title"]');
- const findIssuableStatus = () => wrapper.find('[data-testid="issuable-status"]');
- const containsJiraLogo = () => wrapper.find('[data-testid="jira-logo"]').exists();
- const findHealthStatus = () => wrapper.find('.health-status');
-
- describe('when mounted', () => {
- it('initializes user popovers', () => {
- expect(initUserPopovers).not.toHaveBeenCalled();
-
- factory();
-
- expect(initUserPopovers).toHaveBeenCalledWith([wrapper.vm.$refs.openedAgoByContainer.$el]);
- });
- });
-
- describe('when scopedLabels feature is available', () => {
- beforeEach(() => {
- issuable.labels = [...testLabels];
-
- factory({ issuable }, true);
- });
-
- describe('when label is scoped', () => {
- it('returns label with correct props', () => {
- const scopedLabel = findScopedLabels().at(0);
-
- expect(scopedLabel.props('scoped')).toBe(true);
- });
- });
-
- describe('when label is not scoped', () => {
- it('returns label with correct props', () => {
- const notScopedLabel = findUnscopedLabels().at(0);
-
- expect(notScopedLabel.props('scoped')).toBe(false);
- });
- });
- });
-
- describe('when scopedLabels feature is not available', () => {
- beforeEach(() => {
- issuable.labels = [...testLabels];
-
- factory({ issuable });
- });
-
- describe('when label is scoped', () => {
- it('label scoped props is false', () => {
- const scopedLabel = findScopedLabels().at(0);
-
- expect(scopedLabel.props('scoped')).toBe(false);
- });
- });
-
- describe('when label is not scoped', () => {
- it('label scoped props is false', () => {
- const notScopedLabel = findUnscopedLabels().at(0);
-
- expect(notScopedLabel.props('scoped')).toBe(false);
- });
- });
- });
-
- describe('with simple issuable', () => {
- beforeEach(() => {
- Object.assign(issuable, {
- has_tasks: false,
- task_status: TEST_TASK_STATUS,
- created_at: TEST_MONTH_AGO,
- author: {
- ...issuable.author,
- name: TEST_USER_NAME,
- },
- labels: [],
- });
-
- factory({ issuable });
- });
-
- it.each`
- desc | check
- ${'bulk editing checkbox'} | ${checkExists(findBulkCheckbox)}
- ${'confidential icon'} | ${hasConfidentialIcon}
- ${'task status'} | ${checkExists(findTaskStatus)}
- ${'milestone'} | ${checkExists(findMilestone)}
- ${'due date'} | ${checkExists(findDueDate)}
- ${'labels'} | ${checkExists(findLabels)}
- ${'weight'} | ${checkExists(findWeight)}
- ${'blocking issues count'} | ${checkExists(findBlockingIssuesCount)}
- ${'merge request count'} | ${checkExists(findMergeRequestsCount)}
- ${'upvotes'} | ${checkExists(findUpvotes)}
- ${'downvotes'} | ${checkExists(findDownvotes)}
- `('does not render $desc', ({ check }) => {
- expect(check()).toBe(false);
- });
-
- it('show relative reference path', () => {
- expect(wrapper.find('.js-ref-path').text()).toBe(issuable.references.relative);
- });
-
- it('does not have closed text', () => {
- expect(wrapper.text()).not.toContain(TEXT_CLOSED);
- });
-
- it('does not have closed class', () => {
- expect(wrapper.classes('closed')).toBe(false);
- });
-
- it('renders fuzzy created date and author', () => {
- expect(trimText(findOpenedAgoContainer().text())).toContain(
- `created 1 month ago by ${TEST_USER_NAME}`,
- );
- });
-
- it('renders no comments', () => {
- expect(findNotes().classes('no-comments')).toBe(true);
- });
-
- it.each`
- gitlabWebUrl | webUrl | expectedHref | expectedTarget | isExternal
- ${undefined} | ${`${MOCK_GITLAB_URL}/issue`} | ${`${MOCK_GITLAB_URL}/issue`} | ${undefined} | ${false}
- ${undefined} | ${'https://jira.com/issue'} | ${'https://jira.com/issue'} | ${'_blank'} | ${true}
- ${'/gitlab-org/issue'} | ${'https://jira.com/issue'} | ${'/gitlab-org/issue'} | ${undefined} | ${false}
- `(
- 'renders issuable title correctly when `gitlabWebUrl` is `$gitlabWebUrl` and webUrl is `$webUrl`',
- async ({ webUrl, gitlabWebUrl, expectedHref, expectedTarget, isExternal }) => {
- factory({
- issuable: {
- ...issuable,
- web_url: webUrl,
- gitlab_web_url: gitlabWebUrl,
- },
- });
-
- const titleEl = findIssuableTitle();
-
- expect(titleEl.exists()).toBe(true);
- expect(titleEl.find(GlLink).attributes('href')).toBe(expectedHref);
- expect(titleEl.find(GlLink).attributes('target')).toBe(expectedTarget);
- expect(titleEl.find(GlLink).text()).toBe(issuable.title);
-
- expect(titleEl.find(GlIcon).exists()).toBe(isExternal);
- },
- );
- });
-
- describe('with confidential issuable', () => {
- beforeEach(() => {
- issuable.confidential = true;
-
- factory({ issuable });
- });
-
- it('renders the confidential icon', () => {
- expect(hasConfidentialIcon()).toBe(true);
- });
- });
-
- describe('with Jira issuable', () => {
- beforeEach(() => {
- issuable.external_tracker = 'jira';
-
- factory({ issuable });
- });
-
- it('renders the Jira icon', () => {
- expect(containsJiraLogo()).toBe(true);
- });
-
- it('opens issuable in a new tab', () => {
- expect(findIssuableTitle().props('target')).toBe('_blank');
- });
-
- it('opens author in a new tab', () => {
- expect(findAuthor().props('target')).toBe('_blank');
- });
-
- describe('with Jira status', () => {
- const expectedStatus = 'In Progress';
-
- beforeEach(() => {
- issuable.status = expectedStatus;
-
- factory({ issuable });
- });
-
- it('renders the Jira status', () => {
- expect(findIssuableStatus().text()).toBe(expectedStatus);
- });
- });
- });
-
- describe('with task status', () => {
- beforeEach(() => {
- Object.assign(issuable, {
- has_tasks: true,
- task_status: TEST_TASK_STATUS,
- });
-
- factory({ issuable });
- });
-
- it('renders task status', () => {
- expect(findTaskStatus().exists()).toBe(true);
- expect(findTaskStatus().text()).toBe(TEST_TASK_STATUS);
- });
- });
-
- describe.each`
- desc | dueDate | expectedTooltipPart
- ${'past due'} | ${TEST_MONTH_AGO} | ${'Past due'}
- ${'future due'} | ${TEST_MONTH_LATER} | ${'1 month remaining'}
- `('with milestone with $desc', ({ dueDate, expectedTooltipPart }) => {
- beforeEach(() => {
- issuable.milestone = { ...TEST_MILESTONE, due_date: dueDate };
-
- factory({ issuable });
- });
-
- it('renders milestone', () => {
- expect(findMilestone().exists()).toBe(true);
- expect(hasIcon('clock', findMilestone())).toBe(true);
- expect(findMilestone().text()).toEqual(TEST_MILESTONE.title);
- });
-
- it('renders tooltip', () => {
- expect(findMilestoneTooltip()).toBe(
- `${formatDate(dueDate, DATE_FORMAT)} (${expectedTooltipPart})`,
- );
- });
-
- it('renders milestone with the correct href', () => {
- const { title } = issuable.milestone;
- const expected = mergeUrlParams({ milestone_title: title }, TEST_BASE_URL);
-
- expect(findMilestone().attributes('href')).toBe(expected);
- });
- });
-
- describe.each`
- dueDate | hasClass | desc
- ${TEST_MONTH_LATER} | ${false} | ${'with future due date'}
- ${TEST_MONTH_AGO} | ${true} | ${'with past due date'}
- `('$desc', ({ dueDate, hasClass }) => {
- beforeEach(() => {
- issuable.due_date = dueDate;
-
- factory({ issuable });
- });
-
- it('renders due date', () => {
- expect(findDueDate().exists()).toBe(true);
- expect(findDueDate().text()).toBe(formatDate(dueDate, DATE_FORMAT));
- });
-
- it(hasClass ? 'has cred class' : 'does not have cred class', () => {
- expect(findDueDate().classes('cred')).toEqual(hasClass);
- });
- });
-
- describe('with labels', () => {
- beforeEach(() => {
- issuable.labels = [...testLabels];
-
- factory({ issuable });
- });
-
- it('renders labels', () => {
- factory({ issuable });
-
- const labels = findLabels().wrappers.map((label) => ({
- href: label.props('target'),
- text: label.text(),
- tooltip: label.attributes('description'),
- }));
-
- const expected = testLabels.map((label) => ({
- href: mergeUrlParams({ 'label_name[]': label.name }, TEST_BASE_URL),
- text: label.name,
- tooltip: label.description,
- }));
-
- expect(labels).toEqual(expected);
- });
- });
-
- describe('with labels for Jira issuable', () => {
- beforeEach(() => {
- issuable.labels = [...testLabels];
- issuable.external_tracker = 'jira';
-
- factory({ issuable });
- });
-
- it('renders labels', () => {
- factory({ issuable });
-
- const labels = findLabels().wrappers.map((label) => ({
- href: label.props('target'),
- text: label.text(),
- tooltip: label.attributes('description'),
- }));
-
- const expected = testLabels.map((label) => ({
- href: mergeUrlParams({ 'labels[]': label.name }, TEST_BASE_URL),
- text: label.name,
- tooltip: label.description,
- }));
-
- expect(labels).toEqual(expected);
- });
- });
-
- describe.each`
- weight
- ${0}
- ${10}
- ${12345}
- `('with weight $weight', ({ weight }) => {
- beforeEach(() => {
- issuable.weight = weight;
-
- factory({ issuable });
- });
-
- it('renders weight', () => {
- expect(findWeight().exists()).toBe(true);
- expect(findWeight().text()).toEqual(weight.toString());
- });
- });
-
- describe('with closed state', () => {
- beforeEach(() => {
- issuable.state = 'closed';
-
- factory({ issuable });
- });
-
- it('renders closed text', () => {
- expect(wrapper.text()).toContain(TEXT_CLOSED);
- });
-
- it('has closed class', () => {
- expect(wrapper.classes('closed')).toBe(true);
- });
- });
-
- describe('with assignees', () => {
- beforeEach(() => {
- issuable.assignees = testAssignees;
-
- factory({ issuable });
- });
-
- it('renders assignees', () => {
- expect(findAssignees().exists()).toBe(true);
- expect(findAssignees().props('assignees')).toEqual(testAssignees);
- });
- });
-
- describe.each`
- desc | key | finder
- ${'with blocking issues count'} | ${'blocking_issues_count'} | ${findBlockingIssuesCount}
- ${'with merge requests count'} | ${'merge_requests_count'} | ${findMergeRequestsCount}
- ${'with upvote count'} | ${'upvotes'} | ${findUpvotes}
- ${'with downvote count'} | ${'downvotes'} | ${findDownvotes}
- ${'with notes count'} | ${'user_notes_count'} | ${findNotes}
- `('$desc', ({ key, finder }) => {
- beforeEach(() => {
- issuable[key] = TEST_META_COUNT;
-
- factory({ issuable });
- });
-
- it('renders correct count', () => {
- expect(finder().exists()).toBe(true);
- expect(finder().text()).toBe(TEST_META_COUNT.toString());
- expect(finder().classes('no-comments')).toBe(false);
- });
- });
-
- describe('with bulk editing', () => {
- describe.each`
- selected | desc
- ${true} | ${'when selected'}
- ${false} | ${'when unselected'}
- `('$desc', ({ selected }) => {
- beforeEach(() => {
- factory({ isBulkEditing: true, selected });
- });
-
- it(`renders checked is ${selected}`, () => {
- expect(findBulkCheckbox().element.checked).toBe(selected);
- });
-
- it('emits select when clicked', () => {
- expect(wrapper.emitted().select).toBeUndefined();
-
- findBulkCheckbox().trigger('click');
-
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.emitted().select).toEqual([[{ issuable, selected: !selected }]]);
- });
- });
- });
- });
-
- if (IS_EE) {
- describe('with health status', () => {
- it('renders health status tag', () => {
- factory({ issuable });
- expect(findHealthStatus().exists()).toBe(true);
- });
-
- it('does not render when health status is absent', () => {
- issuable.health_status = null;
- factory({ issuable });
- expect(findHealthStatus().exists()).toBe(false);
- });
- });
- }
-});
diff --git a/spec/frontend/issues_list/components/issuables_list_app_spec.js b/spec/frontend/issues_list/components/issuables_list_app_spec.js
deleted file mode 100644
index 11854db534e..00000000000
--- a/spec/frontend/issues_list/components/issuables_list_app_spec.js
+++ /dev/null
@@ -1,653 +0,0 @@
-import {
- GlEmptyState,
- GlPagination,
- GlDeprecatedSkeletonLoading as GlSkeletonLoading,
-} from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
-import axios from 'axios';
-import MockAdapter from 'axios-mock-adapter';
-import setWindowLocation from 'helpers/set_window_location_helper';
-import { TEST_HOST } from 'helpers/test_constants';
-import waitForPromises from 'helpers/wait_for_promises';
-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';
-import issuablesEventBus from '~/issues_list/eventhub';
-import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
-
-jest.mock('~/flash');
-jest.mock('~/issues_list/eventhub');
-jest.mock('~/lib/utils/common_utils', () => ({
- ...jest.requireActual('~/lib/utils/common_utils'),
- scrollToElement: () => {},
-}));
-
-const TEST_LOCATION = `${TEST_HOST}/issues`;
-const TEST_ENDPOINT = '/issues';
-const TEST_CREATE_ISSUES_PATH = '/createIssue';
-const TEST_SVG_PATH = '/emptySvg';
-
-const MOCK_ISSUES = Array(PAGE_SIZE_MANUAL)
- .fill(0)
- .map((_, i) => ({
- id: i,
- web_url: `url${i}`,
- }));
-
-describe('Issuables list component', () => {
- let mockAxios;
- let wrapper;
- let apiSpy;
-
- const setupApiMock = (cb) => {
- apiSpy = jest.fn(cb);
-
- mockAxios.onGet(TEST_ENDPOINT).reply((cfg) => apiSpy(cfg));
- };
-
- const factory = (props = { sortKey: 'priority' }) => {
- const emptyStateMeta = {
- createIssuePath: TEST_CREATE_ISSUES_PATH,
- svgPath: TEST_SVG_PATH,
- };
-
- wrapper = shallowMount(IssuablesListApp, {
- propsData: {
- endpoint: TEST_ENDPOINT,
- emptyStateMeta,
- ...props,
- },
- });
- };
-
- const findLoading = () => wrapper.find(GlSkeletonLoading);
- const findIssuables = () => wrapper.findAll(Issuable);
- const findFilteredSearchBar = () => wrapper.find(FilteredSearchBar);
- const findFirstIssuable = () => findIssuables().wrappers[0];
- const findEmptyState = () => wrapper.find(GlEmptyState);
-
- beforeEach(() => {
- mockAxios = new MockAdapter(axios);
-
- setWindowLocation(TEST_LOCATION);
- });
-
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- mockAxios.restore();
- });
-
- describe('with failed issues response', () => {
- beforeEach(() => {
- setupApiMock(() => [500]);
-
- factory();
-
- return waitForPromises();
- });
-
- it('does not show loading', () => {
- expect(wrapper.vm.loading).toBe(false);
- });
-
- it('flashes an error', () => {
- expect(createFlash).toHaveBeenCalledTimes(1);
- });
- });
-
- describe('with successful issues response', () => {
- beforeEach(() => {
- setupApiMock(() => [
- 200,
- MOCK_ISSUES.slice(0, PAGE_SIZE),
- {
- 'x-total': 100,
- 'x-page': 2,
- },
- ]);
- });
-
- it('has default props and data', () => {
- factory();
- expect(wrapper.vm).toMatchObject({
- // Props
- canBulkEdit: false,
- emptyStateMeta: {
- createIssuePath: TEST_CREATE_ISSUES_PATH,
- svgPath: TEST_SVG_PATH,
- },
- // Data
- filters: {
- state: 'opened',
- },
- isBulkEditing: false,
- issuables: [],
- loading: true,
- page: 1,
- selection: {},
- totalItems: 0,
- });
- });
-
- it('does not call API until mounted', () => {
- factory();
- expect(apiSpy).not.toHaveBeenCalled();
- });
-
- describe('when mounted', () => {
- beforeEach(() => {
- factory();
- });
-
- it('calls API', () => {
- expect(apiSpy).toHaveBeenCalled();
- });
-
- it('shows loading', () => {
- expect(findLoading().exists()).toBe(true);
- expect(findIssuables().length).toBe(0);
- expect(findEmptyState().exists()).toBe(false);
- });
- });
-
- describe('when finished loading', () => {
- beforeEach(() => {
- factory();
-
- return waitForPromises();
- });
-
- it('does not display empty state', () => {
- expect(wrapper.vm.issuables.length).toBeGreaterThan(0);
- expect(wrapper.vm.emptyState).toEqual({});
- expect(wrapper.find(GlEmptyState).exists()).toBe(false);
- });
-
- it('sets the proper page and total items', () => {
- expect(wrapper.vm.totalItems).toBe(100);
- expect(wrapper.vm.page).toBe(2);
- });
-
- it('renders one page of issuables and pagination', () => {
- expect(findIssuables().length).toBe(PAGE_SIZE);
- expect(wrapper.find(GlPagination).exists()).toBe(true);
- });
- });
-
- it('does not render FilteredSearchBar', () => {
- factory();
-
- expect(findFilteredSearchBar().exists()).toBe(false);
- });
- });
-
- describe('with bulk editing enabled', () => {
- beforeEach(() => {
- issuablesEventBus.$on.mockReset();
- issuablesEventBus.$emit.mockReset();
-
- setupApiMock(() => [200, MOCK_ISSUES.slice(0)]);
- factory({ canBulkEdit: true });
-
- return waitForPromises();
- });
-
- it('is not enabled by default', () => {
- expect(wrapper.vm.isBulkEditing).toBe(false);
- });
-
- it('does not select issues by default', () => {
- expect(wrapper.vm.selection).toEqual({});
- });
-
- it('"Select All" checkbox toggles all visible issuables"', () => {
- wrapper.vm.onSelectAll();
- expect(wrapper.vm.selection).toEqual(
- wrapper.vm.issuables.reduce((acc, i) => ({ ...acc, [i.id]: true }), {}),
- );
-
- wrapper.vm.onSelectAll();
- expect(wrapper.vm.selection).toEqual({});
- });
-
- it('"Select All checkbox" selects all issuables if only some are selected"', () => {
- wrapper.vm.selection = { [wrapper.vm.issuables[0].id]: true };
- wrapper.vm.onSelectAll();
- expect(wrapper.vm.selection).toEqual(
- wrapper.vm.issuables.reduce((acc, i) => ({ ...acc, [i.id]: true }), {}),
- );
- });
-
- it('selects and deselects issuables', () => {
- const [i0, i1, i2] = wrapper.vm.issuables;
-
- expect(wrapper.vm.selection).toEqual({});
- wrapper.vm.onSelectIssuable({ issuable: i0, selected: false });
- expect(wrapper.vm.selection).toEqual({});
- wrapper.vm.onSelectIssuable({ issuable: i1, selected: true });
- expect(wrapper.vm.selection).toEqual({ 1: true });
- wrapper.vm.onSelectIssuable({ issuable: i0, selected: true });
- expect(wrapper.vm.selection).toEqual({ 1: true, 0: true });
- wrapper.vm.onSelectIssuable({ issuable: i2, selected: true });
- expect(wrapper.vm.selection).toEqual({ 1: true, 0: true, 2: true });
- wrapper.vm.onSelectIssuable({ issuable: i2, selected: true });
- expect(wrapper.vm.selection).toEqual({ 1: true, 0: true, 2: true });
- wrapper.vm.onSelectIssuable({ issuable: i0, selected: false });
- expect(wrapper.vm.selection).toEqual({ 1: true, 2: true });
- });
-
- it('broadcasts a message to the bulk edit sidebar when a value is added to selection', () => {
- issuablesEventBus.$emit.mockReset();
- const i1 = wrapper.vm.issuables[1];
-
- wrapper.vm.onSelectIssuable({ issuable: i1, selected: true });
-
- return wrapper.vm.$nextTick().then(() => {
- expect(issuablesEventBus.$emit).toHaveBeenCalledTimes(1);
- expect(issuablesEventBus.$emit).toHaveBeenCalledWith('issuables:updateBulkEdit');
- });
- });
-
- it('does not broadcast a message to the bulk edit sidebar when a value is not added to selection', () => {
- issuablesEventBus.$emit.mockReset();
-
- return wrapper.vm
- .$nextTick()
- .then(waitForPromises)
- .then(() => {
- const i1 = wrapper.vm.issuables[1];
-
- wrapper.vm.onSelectIssuable({ issuable: i1, selected: false });
- })
- .then(wrapper.vm.$nextTick)
- .then(() => {
- expect(issuablesEventBus.$emit).toHaveBeenCalledTimes(0);
- });
- });
-
- it('listens to a message to toggle bulk editing', () => {
- expect(wrapper.vm.isBulkEditing).toBe(false);
- expect(issuablesEventBus.$on.mock.calls[0][0]).toBe('issuables:toggleBulkEdit');
- issuablesEventBus.$on.mock.calls[0][1](true); // Call the message handler
-
- return waitForPromises()
- .then(() => {
- expect(wrapper.vm.isBulkEditing).toBe(true);
- issuablesEventBus.$on.mock.calls[0][1](false);
- })
- .then(() => {
- expect(wrapper.vm.isBulkEditing).toBe(false);
- });
- });
- });
-
- describe('with query params in window.location', () => {
- const expectedFilters = {
- assignee_username: 'root',
- author_username: 'root',
- confidential: 'yes',
- my_reaction_emoji: 'airplane',
- scope: 'all',
- state: 'opened',
- weight: '0',
- milestone: 'v3.0',
- labels: 'Aquapod,Astro',
- order_by: 'milestone_due',
- sort: 'desc',
- };
-
- describe('when page is not present in params', () => {
- const query =
- '?assignee_username=root&author_username=root&confidential=yes&label_name%5B%5D=Aquapod&label_name%5B%5D=Astro&milestone_title=v3.0&my_reaction_emoji=airplane&scope=all&sort=priority&state=opened&weight=0&not[label_name][]=Afterpod&not[milestone_title][]=13';
-
- beforeEach(() => {
- setWindowLocation(query);
-
- setupApiMock(() => [200, MOCK_ISSUES.slice(0)]);
- factory({ sortKey: 'milestone_due_desc' });
-
- return waitForPromises();
- });
-
- afterEach(() => {
- apiSpy.mockClear();
- });
-
- it('applies filters and sorts', () => {
- expect(wrapper.vm.hasFilters).toBe(true);
- expect(wrapper.vm.filters).toEqual({
- ...expectedFilters,
- 'not[milestone]': ['13'],
- 'not[labels]': ['Afterpod'],
- });
-
- expect(apiSpy).toHaveBeenCalledWith(
- expect.objectContaining({
- params: {
- ...expectedFilters,
- with_labels_details: true,
- page: 1,
- per_page: PAGE_SIZE,
- 'not[milestone]': ['13'],
- 'not[labels]': ['Afterpod'],
- },
- }),
- );
- });
-
- it('passes the base url to issuable', () => {
- expect(findFirstIssuable().props('baseUrl')).toBe(TEST_LOCATION);
- });
- });
-
- describe('when page is present in the param', () => {
- const query =
- '?assignee_username=root&author_username=root&confidential=yes&label_name%5B%5D=Aquapod&label_name%5B%5D=Astro&milestone_title=v3.0&my_reaction_emoji=airplane&scope=all&sort=priority&state=opened&weight=0&page=3';
-
- beforeEach(() => {
- setWindowLocation(query);
-
- setupApiMock(() => [200, MOCK_ISSUES.slice(0)]);
- factory({ sortKey: 'milestone_due_desc' });
-
- return waitForPromises();
- });
-
- afterEach(() => {
- apiSpy.mockClear();
- });
-
- it('applies filters and sorts', () => {
- expect(apiSpy).toHaveBeenCalledWith(
- expect.objectContaining({
- params: {
- ...expectedFilters,
- with_labels_details: true,
- page: 3,
- per_page: PAGE_SIZE,
- },
- }),
- );
- });
- });
- });
-
- describe('with hash in window.location', () => {
- beforeEach(() => {
- setWindowLocation(`${TEST_LOCATION}#stuff`);
- setupApiMock(() => [200, MOCK_ISSUES.slice(0)]);
- factory();
- return waitForPromises();
- });
-
- it('passes the base url to issuable', () => {
- expect(findFirstIssuable().props('baseUrl')).toBe(TEST_LOCATION);
- });
- });
-
- describe('with manual sort', () => {
- beforeEach(() => {
- setupApiMock(() => [200, MOCK_ISSUES.slice(0)]);
- factory({ sortKey: RELATIVE_POSITION });
- });
-
- it('uses manual page size', () => {
- expect(apiSpy).toHaveBeenCalledWith(
- expect.objectContaining({
- params: expect.objectContaining({
- per_page: PAGE_SIZE_MANUAL,
- }),
- }),
- );
- });
- });
-
- describe('with empty issues response', () => {
- beforeEach(() => {
- setupApiMock(() => [200, []]);
- });
-
- describe('with query in window location', () => {
- beforeEach(() => {
- setWindowLocation('?weight=Any');
-
- factory();
-
- return waitForPromises().then(() => wrapper.vm.$nextTick());
- });
-
- it('should display "Sorry, your filter produced no results" if filters are too specific', () => {
- expect(findEmptyState().props('title')).toMatchSnapshot();
- });
- });
-
- describe('with closed state', () => {
- beforeEach(() => {
- setWindowLocation('?state=closed');
-
- factory();
-
- return waitForPromises().then(() => wrapper.vm.$nextTick());
- });
-
- it('should display a message "There are no closed issues" if there are no closed issues', () => {
- expect(findEmptyState().props('title')).toMatchSnapshot();
- });
- });
-
- describe('with all state', () => {
- beforeEach(() => {
- setWindowLocation('?state=all');
-
- factory();
-
- return waitForPromises().then(() => wrapper.vm.$nextTick());
- });
-
- it('should display a catch-all if there are no issues to show', () => {
- expect(findEmptyState().element).toMatchSnapshot();
- });
- });
-
- describe('with empty query', () => {
- beforeEach(() => {
- factory();
-
- return wrapper.vm.$nextTick().then(waitForPromises);
- });
-
- it('should display the message "There are no open issues"', () => {
- expect(findEmptyState().props('title')).toMatchSnapshot();
- });
- });
- });
-
- describe('when paginates', () => {
- const newPage = 3;
-
- describe('when total-items is defined in response headers', () => {
- beforeEach(() => {
- window.history.pushState = jest.fn();
- setupApiMock(() => [
- 200,
- MOCK_ISSUES.slice(0, PAGE_SIZE),
- {
- 'x-total': 100,
- 'x-page': 2,
- },
- ]);
-
- factory();
-
- return waitForPromises();
- });
-
- afterEach(() => {
- // reset to original value
- window.history.pushState.mockRestore();
- });
-
- it('calls window.history.pushState one time', () => {
- // Trigger pagination
- wrapper.find(GlPagination).vm.$emit('input', newPage);
-
- expect(window.history.pushState).toHaveBeenCalledTimes(1);
- });
-
- it('sets params in the url', () => {
- // Trigger pagination
- wrapper.find(GlPagination).vm.$emit('input', newPage);
-
- expect(window.history.pushState).toHaveBeenCalledWith(
- {},
- '',
- `${TEST_LOCATION}?state=opened&order_by=priority&sort=asc&page=${newPage}`,
- );
- });
- });
-
- describe('when total-items is not defined in the headers', () => {
- const page = 2;
- const prevPage = page - 1;
- const nextPage = page + 1;
-
- beforeEach(() => {
- setupApiMock(() => [
- 200,
- MOCK_ISSUES.slice(0, PAGE_SIZE),
- {
- 'x-page': page,
- },
- ]);
-
- factory();
-
- return waitForPromises();
- });
-
- it('finds the correct props applied to GlPagination', () => {
- expect(wrapper.find(GlPagination).props()).toMatchObject({
- nextPage,
- prevPage,
- value: page,
- });
- });
- });
- });
-
- describe('when type is "jira"', () => {
- it('renders FilteredSearchBar', () => {
- factory({ type: 'jira' });
-
- expect(findFilteredSearchBar().exists()).toBe(true);
- });
-
- describe('initialSortBy', () => {
- const query = '?sort=updated_asc';
-
- it('sets default value', () => {
- factory({ type: 'jira' });
-
- expect(findFilteredSearchBar().props('initialSortBy')).toBe('created_desc');
- });
-
- it('sets value according to query', () => {
- setWindowLocation(query);
-
- factory({ type: 'jira' });
-
- expect(findFilteredSearchBar().props('initialSortBy')).toBe('updated_asc');
- });
- });
-
- describe('initialFilterValue', () => {
- it('does not set value when no query', () => {
- factory({ type: 'jira' });
-
- expect(findFilteredSearchBar().props('initialFilterValue')).toEqual([]);
- });
-
- it('sets value according to query', () => {
- const query = '?search=free+text';
-
- setWindowLocation(query);
-
- factory({ type: 'jira' });
-
- expect(findFilteredSearchBar().props('initialFilterValue')).toEqual(['free text']);
- });
- });
-
- describe('on filter search', () => {
- beforeEach(() => {
- factory({ type: 'jira' });
-
- window.history.pushState = jest.fn();
- });
-
- afterEach(() => {
- window.history.pushState.mockRestore();
- });
-
- const emitOnFilter = (filter) => findFilteredSearchBar().vm.$emit('onFilter', filter);
-
- describe('empty filter', () => {
- const mockFilter = [];
-
- it('updates URL with correct params', () => {
- emitOnFilter(mockFilter);
-
- expect(window.history.pushState).toHaveBeenCalledWith(
- {},
- '',
- `${TEST_LOCATION}?state=opened`,
- );
- });
- });
-
- describe('filter with search term', () => {
- const mockFilter = [
- {
- type: 'filtered-search-term',
- value: { data: 'free' },
- },
- ];
-
- it('updates URL with correct params', () => {
- emitOnFilter(mockFilter);
-
- expect(window.history.pushState).toHaveBeenCalledWith(
- {},
- '',
- `${TEST_LOCATION}?state=opened&search=free`,
- );
- });
- });
-
- describe('filter with multiple search terms', () => {
- const mockFilter = [
- {
- type: 'filtered-search-term',
- value: { data: 'free' },
- },
- {
- type: 'filtered-search-term',
- value: { data: 'text' },
- },
- ];
-
- it('updates URL with correct params', () => {
- emitOnFilter(mockFilter);
-
- expect(window.history.pushState).toHaveBeenCalledWith(
- {},
- '',
- `${TEST_LOCATION}?state=opened&search=free+text`,
- );
- });
- });
- });
- });
-});
diff --git a/spec/frontend/issues_list/issuable_list_test_data.js b/spec/frontend/issues_list/issuable_list_test_data.js
deleted file mode 100644
index 313aa15bd31..00000000000
--- a/spec/frontend/issues_list/issuable_list_test_data.js
+++ /dev/null
@@ -1,77 +0,0 @@
-export const simpleIssue = {
- id: 442,
- iid: 31,
- title: 'Dismiss Cipher with no integrity',
- state: 'opened',
- created_at: '2019-08-26T19:06:32.667Z',
- updated_at: '2019-08-28T19:53:58.314Z',
- labels: [],
- milestone: null,
- assignees: [],
- author: {
- id: 3,
- name: 'Elnora Bernhard',
- username: 'treva.lesch',
- state: 'active',
- avatar_url: 'https://www.gravatar.com/avatar/a8c0d9c2882406cf2a9b71494625a796?s=80&d=identicon',
- web_url: 'http://localhost:3001/treva.lesch',
- },
- assignee: null,
- user_notes_count: 0,
- blocking_issues_count: 0,
- merge_requests_count: 0,
- upvotes: 0,
- downvotes: 0,
- due_date: null,
- confidential: false,
- web_url: 'http://localhost:3001/h5bp/html5-boilerplate/issues/31',
- has_tasks: false,
- weight: null,
- references: {
- relative: 'html-boilerplate#45',
- },
- health_status: 'on_track',
-};
-
-export const testLabels = [
- {
- id: 1,
- name: 'Tanuki',
- description: 'A cute animal',
- color: '#ff0000',
- text_color: '#ffffff',
- },
- {
- id: 2,
- name: 'Octocat',
- description: 'A grotesque mish-mash of whiskers and tentacles',
- color: '#333333',
- text_color: '#000000',
- },
- {
- id: 3,
- name: 'scoped::label',
- description: 'A scoped label',
- color: '#00ff00',
- text_color: '#ffffff',
- },
-];
-
-export const testAssignees = [
- {
- id: 1,
- name: 'Administrator',
- username: 'root',
- state: 'active',
- avatar_url: 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
- web_url: 'http://localhost:3001/root',
- },
- {
- id: 22,
- name: 'User 0',
- username: 'user0',
- state: 'active',
- avatar_url: 'https://www.gravatar.com/avatar/52e4ce24a915fb7e51e1ad3b57f4b00a?s=80&d=identicon',
- web_url: 'http://localhost:3001/user0',
- },
-];
diff --git a/spec/frontend/issues_list/service_desk_helper_spec.js b/spec/frontend/issues_list/service_desk_helper_spec.js
deleted file mode 100644
index 16aee853341..00000000000
--- a/spec/frontend/issues_list/service_desk_helper_spec.js
+++ /dev/null
@@ -1,28 +0,0 @@
-import { emptyStateHelper, generateMessages } from '~/issues_list/service_desk_helper';
-
-describe('service desk helper', () => {
- const emptyStateMessages = generateMessages({});
-
- // Note: isServiceDeskEnabled must not be true when isServiceDeskSupported is false (it's an invalid case).
- describe.each`
- isServiceDeskSupported | isServiceDeskEnabled | canEditProjectSettings | expectedMessage
- ${true} | ${true} | ${true} | ${'serviceDeskEnabledAndCanEditProjectSettings'}
- ${true} | ${true} | ${false} | ${'serviceDeskEnabledAndCannotEditProjectSettings'}
- ${true} | ${false} | ${true} | ${'serviceDeskDisabledAndCanEditProjectSettings'}
- ${true} | ${false} | ${false} | ${'serviceDeskDisabledAndCannotEditProjectSettings'}
- ${false} | ${false} | ${true} | ${'serviceDeskIsNotSupported'}
- ${false} | ${false} | ${false} | ${'serviceDeskIsNotEnabled'}
- `(
- 'isServiceDeskSupported = $isServiceDeskSupported, isServiceDeskEnabled = $isServiceDeskEnabled, canEditProjectSettings = $canEditProjectSettings',
- ({ isServiceDeskSupported, isServiceDeskEnabled, canEditProjectSettings, expectedMessage }) => {
- it(`displays ${expectedMessage} message`, () => {
- const emptyStateMeta = {
- isServiceDeskEnabled,
- isServiceDeskSupported,
- canEditProjectSettings,
- };
- expect(emptyStateHelper(emptyStateMeta)).toEqual(emptyStateMessages[expectedMessage]);
- });
- },
- );
-});
diff --git a/spec/frontend/jira_import/utils/jira_import_utils_spec.js b/spec/frontend/jira_import/utils/jira_import_utils_spec.js
index 9696d95f8c4..4207038f50c 100644
--- a/spec/frontend/jira_import/utils/jira_import_utils_spec.js
+++ b/spec/frontend/jira_import/utils/jira_import_utils_spec.js
@@ -1,5 +1,5 @@
import { useLocalStorageSpy } from 'helpers/local_storage_helper';
-import { JIRA_IMPORT_SUCCESS_ALERT_HIDE_MAP_KEY } from '~/issues_list/constants';
+import { JIRA_IMPORT_SUCCESS_ALERT_HIDE_MAP_KEY } from '~/jira_import/utils/constants';
import {
calculateJiraImportLabel,
extractJiraProjectsOptions,
diff --git a/spec/frontend/jobs/bridge/app_spec.js b/spec/frontend/jobs/bridge/app_spec.js
index 0e232ab240d..c0faab90552 100644
--- a/spec/frontend/jobs/bridge/app_spec.js
+++ b/spec/frontend/jobs/bridge/app_spec.js
@@ -1,27 +1,104 @@
-import { shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
+import { shallowMount, createLocalVue } from '@vue/test-utils';
+import { GlBreakpointInstance } from '@gitlab/ui/dist/utils';
+import { GlLoadingIcon } from '@gitlab/ui';
+import VueApollo from 'vue-apollo';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import getPipelineQuery from '~/jobs/bridge/graphql/queries/pipeline.query.graphql';
+import waitForPromises from 'helpers/wait_for_promises';
import BridgeApp from '~/jobs/bridge/app.vue';
import BridgeEmptyState from '~/jobs/bridge/components/empty_state.vue';
import BridgeSidebar from '~/jobs/bridge/components/sidebar.vue';
+import CiHeader from '~/vue_shared/components/header_ci_component.vue';
+import {
+ MOCK_BUILD_ID,
+ MOCK_PIPELINE_IID,
+ MOCK_PROJECT_FULL_PATH,
+ mockPipelineQueryResponse,
+} from './mock_data';
+
+const localVue = createLocalVue();
+localVue.use(VueApollo);
describe('Bridge Show Page', () => {
let wrapper;
+ let mockApollo;
+ let mockPipelineQuery;
+
+ const createComponent = (options) => {
+ wrapper = shallowMount(BridgeApp, {
+ provide: {
+ buildId: MOCK_BUILD_ID,
+ projectFullPath: MOCK_PROJECT_FULL_PATH,
+ pipelineIid: MOCK_PIPELINE_IID,
+ },
+ mocks: {
+ $apollo: {
+ queries: {
+ pipeline: {
+ loading: true,
+ },
+ },
+ },
+ },
+ ...options,
+ });
+ };
- const createComponent = () => {
- wrapper = shallowMount(BridgeApp, {});
+ const createComponentWithApollo = () => {
+ const handlers = [[getPipelineQuery, mockPipelineQuery]];
+ mockApollo = createMockApollo(handlers);
+
+ createComponent({
+ localVue,
+ apolloProvider: mockApollo,
+ mocks: {},
+ });
};
+ const findCiHeader = () => wrapper.findComponent(CiHeader);
const findEmptyState = () => wrapper.findComponent(BridgeEmptyState);
+ const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findSidebar = () => wrapper.findComponent(BridgeSidebar);
+ beforeEach(() => {
+ mockPipelineQuery = jest.fn();
+ });
+
afterEach(() => {
+ mockPipelineQuery.mockReset();
wrapper.destroy();
});
- describe('template', () => {
+ describe('while pipeline query is loading', () => {
beforeEach(() => {
createComponent();
});
+ it('renders loading icon', () => {
+ expect(findLoadingIcon().exists()).toBe(true);
+ });
+ });
+
+ describe('after pipeline query is loaded', () => {
+ beforeEach(() => {
+ mockPipelineQuery.mockResolvedValue(mockPipelineQueryResponse);
+ createComponentWithApollo();
+ waitForPromises();
+ });
+
+ it('query is called with correct variables', async () => {
+ expect(mockPipelineQuery).toHaveBeenCalledTimes(1);
+ expect(mockPipelineQuery).toHaveBeenCalledWith({
+ fullPath: MOCK_PROJECT_FULL_PATH,
+ iid: MOCK_PIPELINE_IID,
+ });
+ });
+
+ it('renders CI header state', () => {
+ expect(findCiHeader().exists()).toBe(true);
+ });
+
it('renders empty state', () => {
expect(findEmptyState().exists()).toBe(true);
});
@@ -30,4 +107,42 @@ describe('Bridge Show Page', () => {
expect(findSidebar().exists()).toBe(true);
});
});
+
+ describe('sidebar expansion', () => {
+ beforeEach(() => {
+ mockPipelineQuery.mockResolvedValue(mockPipelineQueryResponse);
+ createComponentWithApollo();
+ waitForPromises();
+ });
+
+ describe('on resize', () => {
+ it.each`
+ breakpoint | isSidebarExpanded
+ ${'xs'} | ${false}
+ ${'sm'} | ${false}
+ ${'md'} | ${true}
+ ${'lg'} | ${true}
+ ${'xl'} | ${true}
+ `(
+ 'sets isSidebarExpanded to `$isSidebarExpanded` when the breakpoint is "$breakpoint"',
+ async ({ breakpoint, isSidebarExpanded }) => {
+ jest.spyOn(GlBreakpointInstance, 'getBreakpointSize').mockReturnValue(breakpoint);
+
+ window.dispatchEvent(new Event('resize'));
+ await nextTick();
+
+ expect(findSidebar().exists()).toBe(isSidebarExpanded);
+ },
+ );
+ });
+
+ it('toggles expansion on button click', async () => {
+ expect(findSidebar().exists()).toBe(true);
+
+ wrapper.vm.toggleSidebar();
+ await nextTick();
+
+ expect(findSidebar().exists()).toBe(false);
+ });
+ });
});
diff --git a/spec/frontend/jobs/bridge/components/empty_state_spec.js b/spec/frontend/jobs/bridge/components/empty_state_spec.js
index 83642450118..38c55b296f0 100644
--- a/spec/frontend/jobs/bridge/components/empty_state_spec.js
+++ b/spec/frontend/jobs/bridge/components/empty_state_spec.js
@@ -6,14 +6,13 @@ import { MOCK_EMPTY_ILLUSTRATION_PATH, MOCK_PATH_TO_DOWNSTREAM } from '../mock_d
describe('Bridge Empty State', () => {
let wrapper;
- const createComponent = (props) => {
+ const createComponent = ({ downstreamPipelinePath }) => {
wrapper = shallowMount(BridgeEmptyState, {
provide: {
emptyStateIllustrationPath: MOCK_EMPTY_ILLUSTRATION_PATH,
},
propsData: {
- downstreamPipelinePath: MOCK_PATH_TO_DOWNSTREAM,
- ...props,
+ downstreamPipelinePath,
},
});
};
@@ -28,7 +27,7 @@ describe('Bridge Empty State', () => {
describe('template', () => {
beforeEach(() => {
- createComponent();
+ createComponent({ downstreamPipelinePath: MOCK_PATH_TO_DOWNSTREAM });
});
it('renders illustration', () => {
diff --git a/spec/frontend/jobs/bridge/components/sidebar_spec.js b/spec/frontend/jobs/bridge/components/sidebar_spec.js
index ba4018753af..5006d4f08a6 100644
--- a/spec/frontend/jobs/bridge/components/sidebar_spec.js
+++ b/spec/frontend/jobs/bridge/components/sidebar_spec.js
@@ -1,24 +1,38 @@
import { GlButton, GlDropdown } from '@gitlab/ui';
-import { GlBreakpointInstance } from '@gitlab/ui/dist/utils';
-import { nextTick } from 'vue';
import { shallowMount } from '@vue/test-utils';
import BridgeSidebar from '~/jobs/bridge/components/sidebar.vue';
-import { BUILD_NAME } from '../mock_data';
+import CommitBlock from '~/jobs/components/commit_block.vue';
+import { mockCommit, mockJob } from '../mock_data';
describe('Bridge Sidebar', () => {
let wrapper;
- const createComponent = () => {
+ const MockHeaderEl = {
+ getBoundingClientRect() {
+ return {
+ bottom: '40',
+ };
+ },
+ };
+
+ const createComponent = ({ featureFlag } = {}) => {
wrapper = shallowMount(BridgeSidebar, {
provide: {
- buildName: BUILD_NAME,
+ glFeatures: {
+ triggerJobRetryAction: featureFlag,
+ },
+ },
+ propsData: {
+ bridgeJob: mockJob,
+ commit: mockCommit,
},
});
};
- const findSidebar = () => wrapper.find('aside');
+ const findJobTitle = () => wrapper.find('h4');
+ const findCommitBlock = () => wrapper.findComponent(CommitBlock);
const findRetryDropdown = () => wrapper.find(GlDropdown);
- const findToggle = () => wrapper.find(GlButton);
+ const findToggleBtn = () => wrapper.findComponent(GlButton);
afterEach(() => {
wrapper.destroy();
@@ -29,8 +43,23 @@ describe('Bridge Sidebar', () => {
createComponent();
});
- it('renders retry dropdown', () => {
- expect(findRetryDropdown().exists()).toBe(true);
+ it('renders job name', () => {
+ expect(findJobTitle().text()).toBe(mockJob.name);
+ });
+
+ it('renders commit information', () => {
+ expect(findCommitBlock().exists()).toBe(true);
+ });
+ });
+
+ describe('styles', () => {
+ beforeEach(async () => {
+ jest.spyOn(document, 'querySelector').mockReturnValue(MockHeaderEl);
+ createComponent();
+ });
+
+ it('calculates root styles correctly', () => {
+ expect(wrapper.attributes('style')).toBe('width: 290px; top: 40px;');
});
});
@@ -39,38 +68,32 @@ describe('Bridge Sidebar', () => {
createComponent();
});
- it('toggles expansion on button click', async () => {
- expect(findSidebar().classes()).not.toContain('gl-display-none');
+ it('emits toggle sidebar event on button click', async () => {
+ expect(wrapper.emitted('toggleSidebar')).toBe(undefined);
- findToggle().vm.$emit('click');
- await nextTick();
+ findToggleBtn().vm.$emit('click');
- expect(findSidebar().classes()).toContain('gl-display-none');
+ expect(wrapper.emitted('toggleSidebar')).toHaveLength(1);
});
+ });
- describe('on resize', () => {
- it.each`
- breakpoint | isSidebarExpanded
- ${'xs'} | ${false}
- ${'sm'} | ${false}
- ${'md'} | ${true}
- ${'lg'} | ${true}
- ${'xl'} | ${true}
- `(
- 'sets isSidebarExpanded to `$isSidebarExpanded` when the breakpoint is "$breakpoint"',
- async ({ breakpoint, isSidebarExpanded }) => {
- jest.spyOn(GlBreakpointInstance, 'getBreakpointSize').mockReturnValue(breakpoint);
-
- window.dispatchEvent(new Event('resize'));
- await nextTick();
-
- if (isSidebarExpanded) {
- expect(findSidebar().classes()).not.toContain('gl-display-none');
- } else {
- expect(findSidebar().classes()).toContain('gl-display-none');
- }
- },
- );
+ describe('retry action', () => {
+ describe('when feature flag is ON', () => {
+ beforeEach(() => {
+ createComponent({ featureFlag: true });
+ });
+
+ it('renders retry dropdown', () => {
+ expect(findRetryDropdown().exists()).toBe(true);
+ });
+ });
+
+ describe('when feature flag is OFF', () => {
+ it('does not render retry dropdown', () => {
+ createComponent({ featureFlag: false });
+
+ expect(findRetryDropdown().exists()).toBe(false);
+ });
});
});
});
diff --git a/spec/frontend/jobs/bridge/mock_data.js b/spec/frontend/jobs/bridge/mock_data.js
index 146d1a062ac..4084bb54163 100644
--- a/spec/frontend/jobs/bridge/mock_data.js
+++ b/spec/frontend/jobs/bridge/mock_data.js
@@ -1,3 +1,102 @@
export const MOCK_EMPTY_ILLUSTRATION_PATH = '/path/to/svg';
export const MOCK_PATH_TO_DOWNSTREAM = '/path/to/downstream/pipeline';
-export const BUILD_NAME = 'Child Pipeline Trigger';
+export const MOCK_BUILD_ID = '1331';
+export const MOCK_PIPELINE_IID = '174';
+export const MOCK_PROJECT_FULL_PATH = '/root/project/';
+export const MOCK_SHA = '38f3d89147765427a7ce58be28cd76d14efa682a';
+
+export const mockCommit = {
+ id: `gid://gitlab/CommitPresenter/${MOCK_SHA}`,
+ shortId: '38f3d891',
+ title: 'Update .gitlab-ci.yml file',
+ webPath: `/root/project/-/commit/${MOCK_SHA}`,
+ __typename: 'Commit',
+};
+
+export const mockJob = {
+ createdAt: '2021-12-10T09:05:45Z',
+ id: 'gid://gitlab/Ci::Build/1331',
+ name: 'triggerJobName',
+ scheduledAt: null,
+ startedAt: '2021-12-10T09:13:43Z',
+ status: 'SUCCESS',
+ triggered: null,
+ detailedStatus: {
+ id: '1',
+ detailsPath: '/root/project/-/jobs/1331',
+ icon: 'status_success',
+ group: 'success',
+ text: 'passed',
+ tooltip: 'passed',
+ __typename: 'DetailedStatus',
+ },
+ downstreamPipeline: {
+ id: '1',
+ path: '/root/project/-/pipelines/175',
+ },
+ stage: {
+ id: '1',
+ name: 'build',
+ __typename: 'CiStage',
+ },
+ __typename: 'CiJob',
+};
+
+export const mockUser = {
+ id: 'gid://gitlab/User/1',
+ avatarUrl: 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ name: 'Administrator',
+ username: 'root',
+ webPath: '/root',
+ webUrl: 'http://gdk.test:3000/root',
+ status: {
+ message: 'making great things',
+ __typename: 'UserStatus',
+ },
+ __typename: 'UserCore',
+};
+
+export const mockStage = {
+ id: '1',
+ name: 'build',
+ jobs: {
+ nodes: [mockJob],
+ __typename: 'CiJobConnection',
+ },
+ __typename: 'CiStage',
+};
+
+export const mockPipelineQueryResponse = {
+ data: {
+ project: {
+ id: '1',
+ pipeline: {
+ commit: mockCommit,
+ id: 'gid://gitlab/Ci::Pipeline/174',
+ iid: '88',
+ path: '/root/project/-/pipelines/174',
+ sha: MOCK_SHA,
+ ref: 'main',
+ refPath: 'path/to/ref',
+ user: mockUser,
+ detailedStatus: {
+ id: '1',
+ icon: 'status_failed',
+ group: 'failed',
+ __typename: 'DetailedStatus',
+ },
+ stages: {
+ edges: [
+ {
+ node: mockStage,
+ __typename: 'CiStageEdge',
+ },
+ ],
+ __typename: 'CiStageConnection',
+ },
+ __typename: 'Pipeline',
+ },
+ __typename: 'Project',
+ },
+ },
+};
diff --git a/spec/frontend/jobs/components/table/job_table_app_spec.js b/spec/frontend/jobs/components/table/job_table_app_spec.js
index 482d0df4e9a..05988eecb10 100644
--- a/spec/frontend/jobs/components/table/job_table_app_spec.js
+++ b/spec/frontend/jobs/components/table/job_table_app_spec.js
@@ -114,6 +114,8 @@ describe('Job table app', () => {
await wrapper.vm.$nextTick();
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
jobs: {
pageInfo: {
diff --git a/spec/frontend/labels/delete_label_modal_spec.js b/spec/frontend/labels/delete_label_modal_spec.js
index c1e6ce87990..98049538948 100644
--- a/spec/frontend/labels/delete_label_modal_spec.js
+++ b/spec/frontend/labels/delete_label_modal_spec.js
@@ -13,6 +13,10 @@ describe('DeleteLabelModal', () => {
subjectName: 'GitLab Org',
destroyPath: `${TEST_HOST}/2`,
},
+ {
+ labelName: 'admin label',
+ destroyPath: `${TEST_HOST}/3`,
+ },
];
beforeEach(() => {
@@ -22,8 +26,12 @@ describe('DeleteLabelModal', () => {
const button = document.createElement('button');
button.setAttribute('class', 'js-delete-label-modal-button');
button.setAttribute('data-label-name', x.labelName);
- button.setAttribute('data-subject-name', x.subjectName);
button.setAttribute('data-destroy-path', x.destroyPath);
+
+ if (x.subjectName) {
+ button.setAttribute('data-subject-name', x.subjectName);
+ }
+
button.innerHTML = 'Action';
buttonContainer.appendChild(button);
});
@@ -62,6 +70,7 @@ describe('DeleteLabelModal', () => {
index
${0}
${1}
+ ${2}
`(`when multiple buttons exist`, ({ index }) => {
beforeEach(() => {
initDeleteLabelModal();
@@ -69,14 +78,22 @@ describe('DeleteLabelModal', () => {
});
it('correct props are passed to gl-modal', () => {
- expect(findModal().querySelector('.modal-title').innerHTML).toContain(
- buttons[index].labelName,
- );
- expect(findModal().querySelector('.modal-body').innerHTML).toContain(
- buttons[index].subjectName,
- );
+ const button = buttons[index];
+
+ expect(findModal().querySelector('.modal-title').innerHTML).toContain(button.labelName);
+
+ if (button.subjectName) {
+ expect(findModal().querySelector('.modal-body').textContent).toContain(
+ `${button.labelName} will be permanently deleted from ${button.subjectName}. This cannot be undone.`,
+ );
+ } else {
+ expect(findModal().querySelector('.modal-body').textContent).toContain(
+ `${button.labelName} will be permanently deleted. This cannot be undone.`,
+ );
+ }
+
expect(findModal().querySelector('.modal-footer .btn-danger').href).toContain(
- buttons[index].destroyPath,
+ button.destroyPath,
);
});
});
diff --git a/spec/frontend/lib/utils/resize_observer_spec.js b/spec/frontend/lib/utils/resize_observer_spec.js
new file mode 100644
index 00000000000..419aff28935
--- /dev/null
+++ b/spec/frontend/lib/utils/resize_observer_spec.js
@@ -0,0 +1,68 @@
+import { contentTop } from '~/lib/utils/common_utils';
+import { scrollToTargetOnResize } from '~/lib/utils/resize_observer';
+
+jest.mock('~/lib/utils/common_utils');
+
+function mockStickyHeaderSize(val) {
+ contentTop.mockReturnValue(val);
+}
+
+describe('ResizeObserver Utility', () => {
+ let observer;
+ const triggerResize = () => {
+ const entry = document.querySelector('#content-body');
+ entry.dispatchEvent(new CustomEvent(`ResizeUpdate`, { detail: { entry } }));
+ };
+
+ beforeEach(() => {
+ mockStickyHeaderSize(90);
+
+ jest.spyOn(document.documentElement, 'scrollTo');
+
+ setFixtures(`<div id="content-body"><div class="target">element to scroll to</div></div>`);
+
+ const target = document.querySelector('.target');
+
+ jest.spyOn(target, 'getBoundingClientRect').mockReturnValue({ top: 200 });
+
+ observer = scrollToTargetOnResize({
+ target: '.target',
+ container: '#content-body',
+ });
+ });
+
+ afterEach(() => {
+ contentTop.mockReset();
+ });
+
+ describe('Observer behavior', () => {
+ it('returns null for empty target', () => {
+ observer = scrollToTargetOnResize({
+ target: '',
+ container: '#content-body',
+ });
+
+ expect(observer).toBe(null);
+ });
+
+ it('returns ResizeObserver instance', () => {
+ expect(observer).toBeInstanceOf(ResizeObserver);
+ });
+
+ it('scrolls body so anchor is just below sticky header (contentTop)', () => {
+ triggerResize();
+
+ expect(document.documentElement.scrollTo).toHaveBeenCalledWith({ top: 110 });
+ });
+
+ const interactionEvents = ['mousedown', 'touchstart', 'keydown', 'wheel'];
+ it.each(interactionEvents)('does not hijack scroll after user input from %s', (eventType) => {
+ const event = new Event(eventType);
+ document.dispatchEvent(event);
+
+ triggerResize();
+
+ expect(document.documentElement.scrollTo).not.toHaveBeenCalledWith();
+ });
+ });
+});
diff --git a/spec/frontend/monitoring/components/__snapshots__/dashboard_template_spec.js.snap b/spec/frontend/monitoring/components/__snapshots__/dashboard_template_spec.js.snap
index aaa0a91ffe0..681fb05a6c4 100644
--- a/spec/frontend/monitoring/components/__snapshots__/dashboard_template_spec.js.snap
+++ b/spec/frontend/monitoring/components/__snapshots__/dashboard_template_spec.js.snap
@@ -128,7 +128,7 @@ exports[`Dashboard template matches the default snapshot 1`] = `
emptynodatasvgpath="/images/illustrations/monitoring/no_data.svg"
emptyunabletoconnectsvgpath="/images/illustrations/monitoring/unable_to_connect.svg"
selectedstate="gettingStarted"
- settingspath="/monitoring/monitor-project/-/services/prometheus/edit"
+ settingspath="/monitoring/monitor-project/-/integrations/prometheus/edit"
/>
</div>
`;
diff --git a/spec/frontend/monitoring/components/charts/time_series_spec.js b/spec/frontend/monitoring/components/charts/time_series_spec.js
index 27f7489aa49..ff6f0b9b0c7 100644
--- a/spec/frontend/monitoring/components/charts/time_series_spec.js
+++ b/spec/frontend/monitoring/components/charts/time_series_spec.js
@@ -661,6 +661,8 @@ describe('Time series component', () => {
const commitUrl = `${mockProjectDir}/-/commit/${mockSha}`;
beforeEach(() => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
tooltip: {
type: 'deployments',
diff --git a/spec/frontend/monitoring/components/dashboard_spec.js b/spec/frontend/monitoring/components/dashboard_spec.js
index 9331048bce3..7730e7f347f 100644
--- a/spec/frontend/monitoring/components/dashboard_spec.js
+++ b/spec/frontend/monitoring/components/dashboard_spec.js
@@ -792,6 +792,8 @@ describe('Dashboard', () => {
});
createShallowWrapper({ hasMetrics: true });
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({ hoveredPanel: panelRef });
return wrapper.vm.$nextTick();
diff --git a/spec/frontend/monitoring/components/dashboards_dropdown_spec.js b/spec/frontend/monitoring/components/dashboards_dropdown_spec.js
index 589354e7849..f6d30384847 100644
--- a/spec/frontend/monitoring/components/dashboards_dropdown_spec.js
+++ b/spec/frontend/monitoring/components/dashboards_dropdown_spec.js
@@ -38,6 +38,8 @@ describe('DashboardsDropdown', () => {
const findSearchInput = () => wrapper.find({ ref: 'monitorDashboardsDropdownSearch' });
const findNoItemsMsg = () => wrapper.find({ ref: 'monitorDashboardsDropdownMsg' });
const findStarredListDivider = () => wrapper.find({ ref: 'starredListDivider' });
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
const setSearchTerm = (searchTerm) => wrapper.setData({ searchTerm });
beforeEach(() => {
diff --git a/spec/frontend/mr_popover/mr_popover_spec.js b/spec/frontend/mr_popover/mr_popover_spec.js
index 0c6e4211b10..36ad82e93a5 100644
--- a/spec/frontend/mr_popover/mr_popover_spec.js
+++ b/spec/frontend/mr_popover/mr_popover_spec.js
@@ -35,6 +35,8 @@ describe('MR Popover', () => {
describe('loaded state', () => {
it('matches the snapshot', () => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
mergeRequest: {
title: 'Updated Title',
@@ -55,6 +57,8 @@ describe('MR Popover', () => {
});
it('does not show CI Icon if there is no pipeline data', () => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
mergeRequest: {
state: 'opened',
diff --git a/spec/frontend/notes/components/comment_form_spec.js b/spec/frontend/notes/components/comment_form_spec.js
index c3a51c51de0..16dbf60cef4 100644
--- a/spec/frontend/notes/components/comment_form_spec.js
+++ b/spec/frontend/notes/components/comment_form_spec.js
@@ -263,6 +263,8 @@ describe('issue_comment_form component', () => {
jest.spyOn(wrapper.vm, 'stopPolling');
jest.spyOn(wrapper.vm, 'saveNote').mockResolvedValue();
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
await wrapper.setData({ note: 'hello world' });
await findCommentButton().trigger('click');
@@ -388,6 +390,8 @@ describe('issue_comment_form component', () => {
it('should enable comment button if it has note', async () => {
mountComponent();
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
await wrapper.setData({ note: 'Foo' });
expect(findCommentTypeDropdown().props('disabled')).toBe(false);
diff --git a/spec/frontend/notes/components/note_form_spec.js b/spec/frontend/notes/components/note_form_spec.js
index 48bfd6eac5a..d3b5ab02f24 100644
--- a/spec/frontend/notes/components/note_form_spec.js
+++ b/spec/frontend/notes/components/note_form_spec.js
@@ -257,6 +257,8 @@ describe('issue_note_form component', () => {
props = { ...props, ...options };
wrapper = createComponentWrapper();
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({ isSubmittingWithKeydown: true });
const textarea = wrapper.find('textarea');
diff --git a/spec/frontend/notifications/components/custom_notifications_modal_spec.js b/spec/frontend/notifications/components/custom_notifications_modal_spec.js
index 0782ec7cdd5..7a036d25559 100644
--- a/spec/frontend/notifications/components/custom_notifications_modal_spec.js
+++ b/spec/frontend/notifications/components/custom_notifications_modal_spec.js
@@ -88,6 +88,8 @@ describe('CustomNotificationsModal', () => {
beforeEach(async () => {
wrapper = createComponent();
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
events: [
{ id: 'new_release', enabled: true, name: 'New release', loading: false },
@@ -211,6 +213,8 @@ describe('CustomNotificationsModal', () => {
wrapper = createComponent({ injectedProperties });
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
events: [
{ id: 'new_release', enabled: true, name: 'New release', loading: false },
@@ -239,6 +243,8 @@ describe('CustomNotificationsModal', () => {
mockAxios.onPut('/api/v4/notification_settings').reply(httpStatus.NOT_FOUND, {});
wrapper = createComponent();
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
events: [
{ id: 'new_release', enabled: true, name: 'New release', loading: false },
diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/details_header_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/details_header_spec.js
index f06300efa29..5278e730ec9 100644
--- a/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/details_header_spec.js
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/details_header_spec.js
@@ -1,7 +1,6 @@
-import { GlDropdownItem, GlIcon } from '@gitlab/ui';
+import { GlDropdownItem, GlIcon, GlDropdown } from '@gitlab/ui';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
-import { GlDropdown } from 'jest/packages_and_registries/container_registry/explorer/stubs';
import { useFakeDate } from 'helpers/fake_date';
import createMockApollo from 'helpers/mock_apollo_helper';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
@@ -51,6 +50,7 @@ describe('Details Header', () => {
const findCleanup = () => findByTestId('cleanup');
const findDeleteButton = () => wrapper.findComponent(GlDropdownItem);
const findInfoIcon = () => wrapper.findComponent(GlIcon);
+ const findMenu = () => wrapper.findComponent(GlDropdown);
const waitForMetadataItems = async () => {
// Metadata items are printed by a loop in the title-area and it takes two ticks for them to be available
@@ -139,51 +139,53 @@ describe('Details Header', () => {
});
});
- describe('delete button', () => {
- it('exists', () => {
- mountComponent();
+ describe('menu', () => {
+ it.each`
+ canDelete | disabled | isVisible
+ ${true} | ${false} | ${true}
+ ${true} | ${true} | ${false}
+ ${false} | ${false} | ${false}
+ ${false} | ${true} | ${false}
+ `(
+ 'when canDelete is $canDelete and disabled is $disabled is $isVisible that the menu is visible',
+ ({ canDelete, disabled, isVisible }) => {
+ mountComponent({ propsData: { image: { ...defaultImage, canDelete }, disabled } });
- expect(findDeleteButton().exists()).toBe(true);
- });
+ expect(findMenu().exists()).toBe(isVisible);
+ },
+ );
- it('has the correct text', () => {
- mountComponent();
+ describe('delete button', () => {
+ it('exists', () => {
+ mountComponent();
- expect(findDeleteButton().text()).toBe('Delete image repository');
- });
+ expect(findDeleteButton().exists()).toBe(true);
+ });
- it('has the correct props', () => {
- mountComponent();
+ it('has the correct text', () => {
+ mountComponent();
- expect(findDeleteButton().attributes()).toMatchObject(
- expect.objectContaining({
- variant: 'danger',
- }),
- );
- });
+ expect(findDeleteButton().text()).toBe('Delete image repository');
+ });
- it('emits the correct event', () => {
- mountComponent();
+ it('has the correct props', () => {
+ mountComponent();
- findDeleteButton().vm.$emit('click');
+ expect(findDeleteButton().attributes()).toMatchObject(
+ expect.objectContaining({
+ variant: 'danger',
+ }),
+ );
+ });
- expect(wrapper.emitted('delete')).toEqual([[]]);
- });
+ it('emits the correct event', () => {
+ mountComponent();
- it.each`
- canDelete | disabled | isDisabled
- ${true} | ${false} | ${undefined}
- ${true} | ${true} | ${'true'}
- ${false} | ${false} | ${'true'}
- ${false} | ${true} | ${'true'}
- `(
- 'when canDelete is $canDelete and disabled is $disabled is $isDisabled that the button is disabled',
- ({ canDelete, disabled, isDisabled }) => {
- mountComponent({ propsData: { image: { ...defaultImage, canDelete }, disabled } });
+ findDeleteButton().vm.$emit('click');
- expect(findDeleteButton().attributes('disabled')).toBe(isDisabled);
- },
- );
+ expect(wrapper.emitted('delete')).toEqual([[]]);
+ });
+ });
});
describe('metadata items', () => {
diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/empty_state_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/empty_state_spec.js
deleted file mode 100644
index f14284e9efe..00000000000
--- a/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/empty_state_spec.js
+++ /dev/null
@@ -1,54 +0,0 @@
-import { GlEmptyState } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
-import component from '~/packages_and_registries/container_registry/explorer/components/details_page/empty_state.vue';
-import {
- NO_TAGS_TITLE,
- NO_TAGS_MESSAGE,
- MISSING_OR_DELETED_IMAGE_TITLE,
- MISSING_OR_DELETED_IMAGE_MESSAGE,
-} from '~/packages_and_registries/container_registry/explorer/constants';
-
-describe('EmptyTagsState component', () => {
- let wrapper;
-
- const findEmptyState = () => wrapper.find(GlEmptyState);
-
- const mountComponent = (propsData) => {
- wrapper = shallowMount(component, {
- stubs: {
- GlEmptyState,
- },
- propsData,
- });
- };
-
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
- it('contains gl-empty-state', () => {
- mountComponent();
- expect(findEmptyState().exists()).toBe(true);
- });
-
- it.each`
- isEmptyImage | title | description
- ${false} | ${NO_TAGS_TITLE} | ${NO_TAGS_MESSAGE}
- ${true} | ${MISSING_OR_DELETED_IMAGE_TITLE} | ${MISSING_OR_DELETED_IMAGE_MESSAGE}
- `(
- 'when isEmptyImage is $isEmptyImage has the correct props',
- ({ isEmptyImage, title, description }) => {
- mountComponent({
- noContainersImage: 'foo',
- isEmptyImage,
- });
-
- expect(findEmptyState().props()).toMatchObject({
- title,
- description,
- svgPath: 'foo',
- });
- },
- );
-});
diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/tags_list_row_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/tags_list_row_spec.js
index 00b1d03b7c2..057312828ff 100644
--- a/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/tags_list_row_spec.js
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/tags_list_row_spec.js
@@ -75,16 +75,19 @@ describe('tags list row', () => {
});
it.each`
- digest | disabled
- ${'foo'} | ${true}
- ${null} | ${false}
- ${null} | ${true}
- ${'foo'} | ${true}
- `('is disabled when the digest $digest and disabled is $disabled', ({ digest, disabled }) => {
- mountComponent({ tag: { ...tag, digest }, disabled });
+ digest | disabled | isDisabled
+ ${'foo'} | ${true} | ${'true'}
+ ${null} | ${true} | ${'true'}
+ ${null} | ${false} | ${undefined}
+ ${'foo'} | ${false} | ${undefined}
+ `(
+ 'disabled attribute is set to $isDisabled when the digest $digest and disabled is $disabled',
+ ({ digest, disabled, isDisabled }) => {
+ mountComponent({ tag: { ...tag, digest }, disabled });
- expect(findCheckbox().attributes('disabled')).toBe('true');
- });
+ expect(findCheckbox().attributes('disabled')).toBe(isDisabled);
+ },
+ );
it('is wired to the selected prop', () => {
mountComponent({ ...defaultProps, selected: true });
diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/tags_list_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/tags_list_spec.js
index 56f12e2f0bb..0dcf988c814 100644
--- a/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/tags_list_spec.js
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/tags_list_spec.js
@@ -1,16 +1,25 @@
import { shallowMount, createLocalVue } from '@vue/test-utils';
import { nextTick } from 'vue';
+import { GlEmptyState } from '@gitlab/ui';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { stripTypenames } from 'helpers/graphql_helpers';
-import EmptyTagsState from '~/packages_and_registries/container_registry/explorer/components/details_page/empty_state.vue';
+
import component from '~/packages_and_registries/container_registry/explorer/components/details_page/tags_list.vue';
import TagsListRow from '~/packages_and_registries/container_registry/explorer/components/details_page/tags_list_row.vue';
import TagsLoader from '~/packages_and_registries/container_registry/explorer/components/details_page/tags_loader.vue';
import RegistryList from '~/packages_and_registries/shared/components/registry_list.vue';
+import PersistedSearch from '~/packages_and_registries/shared/components/persisted_search.vue';
import getContainerRepositoryTagsQuery from '~/packages_and_registries/container_registry/explorer/graphql/queries/get_container_repository_tags.query.graphql';
-import { GRAPHQL_PAGE_SIZE } from '~/packages_and_registries/container_registry/explorer/constants/index';
+import {
+ GRAPHQL_PAGE_SIZE,
+ NO_TAGS_TITLE,
+ NO_TAGS_MESSAGE,
+ NO_TAGS_MATCHING_FILTERS_TITLE,
+ NO_TAGS_MATCHING_FILTERS_DESCRIPTION,
+} from '~/packages_and_registries/container_registry/explorer/constants/index';
+import { FILTERED_SEARCH_TERM } from '~/packages_and_registries/shared/constants';
import { tagsMock, imageTagsMock, tagsPageInfo } from '../../mock_data';
const localVue = createLocalVue();
@@ -21,11 +30,20 @@ describe('Tags List', () => {
let resolver;
const tags = [...tagsMock];
+ const defaultConfig = {
+ noContainersImage: 'noContainersImage',
+ };
+
+ const findPersistedSearch = () => wrapper.findComponent(PersistedSearch);
const findTagsListRow = () => wrapper.findAllComponents(TagsListRow);
const findRegistryList = () => wrapper.findComponent(RegistryList);
- const findEmptyState = () => wrapper.findComponent(EmptyTagsState);
+ const findEmptyState = () => wrapper.findComponent(GlEmptyState);
const findTagsLoader = () => wrapper.findComponent(TagsLoader);
+ const fireFirstSortUpdate = () => {
+ findPersistedSearch().vm.$emit('update', { sort: 'NAME_ASC', filters: [] });
+ };
+
const waitForApolloRequestRender = async () => {
await waitForPromises();
await nextTick();
@@ -44,7 +62,7 @@ describe('Tags List', () => {
stubs: { RegistryList },
provide() {
return {
- config: {},
+ config: defaultConfig,
};
},
});
@@ -61,10 +79,23 @@ describe('Tags List', () => {
describe('registry list', () => {
beforeEach(() => {
mountComponent();
-
+ fireFirstSortUpdate();
return waitForApolloRequestRender();
});
+ it('has a persisted search', () => {
+ expect(findPersistedSearch().props()).toMatchObject({
+ defaultOrder: 'NAME',
+ defaultSort: 'asc',
+ sortableFields: [
+ {
+ label: 'Name',
+ orderBy: 'NAME',
+ },
+ ],
+ });
+ });
+
it('binds the correct props', () => {
expect(findRegistryList().props()).toMatchObject({
title: '2 tags',
@@ -75,11 +106,13 @@ describe('Tags List', () => {
});
describe('events', () => {
- it('prev-page fetch the previous page', () => {
+ it('prev-page fetch the previous page', async () => {
findRegistryList().vm.$emit('prev-page');
expect(resolver).toHaveBeenCalledWith({
first: null,
+ name: '',
+ sort: 'NAME_ASC',
before: tagsPageInfo.startCursor,
last: GRAPHQL_PAGE_SIZE,
id: '1',
@@ -92,6 +125,8 @@ describe('Tags List', () => {
expect(resolver).toHaveBeenCalledWith({
after: tagsPageInfo.endCursor,
first: GRAPHQL_PAGE_SIZE,
+ name: '',
+ sort: 'NAME_ASC',
id: '1',
});
});
@@ -108,6 +143,7 @@ describe('Tags List', () => {
describe('list rows', () => {
it('one row exist for each tag', async () => {
mountComponent();
+ fireFirstSortUpdate();
await waitForApolloRequestRender();
@@ -116,6 +152,7 @@ describe('Tags List', () => {
it('the correct props are bound to it', async () => {
mountComponent({ propsData: { disabled: true, id: 1 } });
+ fireFirstSortUpdate();
await waitForApolloRequestRender();
@@ -130,7 +167,7 @@ describe('Tags List', () => {
describe('events', () => {
it('select event update the selected items', async () => {
mountComponent();
-
+ fireFirstSortUpdate();
await waitForApolloRequestRender();
findTagsListRow().at(0).vm.$emit('select');
@@ -142,7 +179,7 @@ describe('Tags List', () => {
it('delete event emit a delete event', async () => {
mountComponent();
-
+ fireFirstSortUpdate();
await waitForApolloRequestRender();
findTagsListRow().at(0).vm.$emit('delete');
@@ -154,32 +191,45 @@ describe('Tags List', () => {
describe('when the list of tags is empty', () => {
beforeEach(() => {
resolver = jest.fn().mockResolvedValue(imageTagsMock([]));
- });
-
- it('has the empty state', async () => {
mountComponent();
-
- await waitForApolloRequestRender();
-
- expect(findEmptyState().exists()).toBe(true);
+ fireFirstSortUpdate();
+ return waitForApolloRequestRender();
});
- it('does not show the loader', async () => {
- mountComponent();
-
- await waitForApolloRequestRender();
-
+ it('does not show the loader', () => {
expect(findTagsLoader().exists()).toBe(false);
});
- it('does not show the list', async () => {
- mountComponent();
+ it('does not show the list', () => {
+ expect(findRegistryList().exists()).toBe(false);
+ });
- await waitForApolloRequestRender();
+ describe('empty state', () => {
+ it('default empty state', () => {
+ expect(findEmptyState().props()).toMatchObject({
+ svgPath: defaultConfig.noContainersImage,
+ title: NO_TAGS_TITLE,
+ description: NO_TAGS_MESSAGE,
+ });
+ });
- expect(findRegistryList().exists()).toBe(false);
+ it('when filtered shows a filtered message', async () => {
+ findPersistedSearch().vm.$emit('update', {
+ sort: 'NAME_ASC',
+ filters: [{ type: FILTERED_SEARCH_TERM, value: { data: 'foo' } }],
+ });
+
+ await waitForApolloRequestRender();
+
+ expect(findEmptyState().props()).toMatchObject({
+ svgPath: defaultConfig.noContainersImage,
+ title: NO_TAGS_MATCHING_FILTERS_TITLE,
+ description: NO_TAGS_MATCHING_FILTERS_DESCRIPTION,
+ });
+ });
});
});
+
describe('loading state', () => {
it.each`
isImageLoading | queryExecuting | loadingVisible
@@ -191,7 +241,7 @@ describe('Tags List', () => {
'when the isImageLoading is $isImageLoading, and is $queryExecuting that the query is still executing is $loadingVisible that the loader is shown',
async ({ isImageLoading, queryExecuting, loadingVisible }) => {
mountComponent({ propsData: { isImageLoading, isMobile: false, id: 1 } });
-
+ fireFirstSortUpdate();
if (!queryExecuting) {
await waitForApolloRequestRender();
}
diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/pages/details_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/pages/details_spec.js
index 9b821ba8ef3..7992bead60a 100644
--- a/spec/frontend/packages_and_registries/container_registry/explorer/pages/details_spec.js
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/pages/details_spec.js
@@ -1,4 +1,4 @@
-import { GlKeysetPagination } from '@gitlab/ui';
+import { GlKeysetPagination, GlEmptyState } from '@gitlab/ui';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
import { nextTick } from 'vue';
@@ -8,7 +8,6 @@ import axios from '~/lib/utils/axios_utils';
import DeleteImage from '~/packages_and_registries/container_registry/explorer/components/delete_image.vue';
import DeleteAlert from '~/packages_and_registries/container_registry/explorer/components/details_page/delete_alert.vue';
import DetailsHeader from '~/packages_and_registries/container_registry/explorer/components/details_page/details_header.vue';
-import EmptyTagsState from '~/packages_and_registries/container_registry/explorer/components/details_page/empty_state.vue';
import PartialCleanupAlert from '~/packages_and_registries/container_registry/explorer/components/details_page/partial_cleanup_alert.vue';
import StatusAlert from '~/packages_and_registries/container_registry/explorer/components/details_page/status_alert.vue';
import TagsList from '~/packages_and_registries/container_registry/explorer/components/details_page/tags_list.vue';
@@ -20,6 +19,8 @@ import {
ALERT_DANGER_IMAGE,
MISSING_OR_DELETED_IMAGE_BREADCRUMB,
ROOT_IMAGE_TEXT,
+ MISSING_OR_DELETED_IMAGE_TITLE,
+ MISSING_OR_DELETED_IMAGE_MESSAGE,
} from '~/packages_and_registries/container_registry/explorer/constants';
import deleteContainerRepositoryTagsMutation from '~/packages_and_registries/container_registry/explorer/graphql/mutations/delete_container_repository_tags.mutation.graphql';
import getContainerRepositoryDetailsQuery from '~/packages_and_registries/container_registry/explorer/graphql/queries/get_container_repository_details.query.graphql';
@@ -50,7 +51,7 @@ describe('Details Page', () => {
const findTagsList = () => wrapper.find(TagsList);
const findDeleteAlert = () => wrapper.find(DeleteAlert);
const findDetailsHeader = () => wrapper.find(DetailsHeader);
- const findEmptyState = () => wrapper.find(EmptyTagsState);
+ const findEmptyState = () => wrapper.find(GlEmptyState);
const findPartialCleanupAlert = () => wrapper.find(PartialCleanupAlert);
const findStatusAlert = () => wrapper.find(StatusAlert);
const findDeleteImage = () => wrapper.find(DeleteImage);
@@ -61,6 +62,10 @@ describe('Details Page', () => {
updateName: jest.fn(),
};
+ const defaultConfig = {
+ noContainersImage: 'noContainersImage',
+ };
+
const cleanTags = tagsMock.map((t) => {
const result = { ...t };
// eslint-disable-next-line no-underscore-dangle
@@ -78,7 +83,7 @@ describe('Details Page', () => {
mutationResolver = jest.fn().mockResolvedValue(graphQLDeleteImageRepositoryTagsMock),
tagsResolver = jest.fn().mockResolvedValue(graphQLImageDetailsMock(imageTagsMock)),
options,
- config = {},
+ config = defaultConfig,
} = {}) => {
localVue.use(VueApollo);
@@ -154,7 +159,11 @@ describe('Details Page', () => {
await waitForApolloRequestRender();
- expect(findEmptyState().exists()).toBe(true);
+ expect(findEmptyState().props()).toMatchObject({
+ description: MISSING_OR_DELETED_IMAGE_MESSAGE,
+ svgPath: defaultConfig.noContainersImage,
+ title: MISSING_OR_DELETED_IMAGE_TITLE,
+ });
});
});
diff --git a/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/__snapshots__/file_sha_spec.js.snap b/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/__snapshots__/file_sha_spec.js.snap
index 881d441e116..f95564e3fad 100644
--- a/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/__snapshots__/file_sha_spec.js.snap
+++ b/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/__snapshots__/file_sha_spec.js.snap
@@ -15,11 +15,14 @@ exports[`FileSha renders 1`] = `
foo
<gl-button-stub
- aria-label="Copy this value"
+ aria-label="Copy SHA"
+ aria-live="polite"
buttontextclasses=""
category="tertiary"
+ data-clipboard-handle-tooltip="false"
data-clipboard-text="foo"
icon="copy-to-clipboard"
+ id="clipboard-button-1"
size="small"
title="Copy SHA"
variant="default"
diff --git a/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/file_sha_spec.js b/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/file_sha_spec.js
index 9ce590bfb51..d7caa8ca2d8 100644
--- a/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/file_sha_spec.js
+++ b/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/file_sha_spec.js
@@ -4,6 +4,8 @@ import FileSha from '~/packages_and_registries/infrastructure_registry/details/c
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import DetailsRow from '~/vue_shared/components/registry/details_row.vue';
+jest.mock('lodash/uniqueId', () => (prefix) => (prefix ? `${prefix}1` : 1));
+
describe('FileSha', () => {
let wrapper;
diff --git a/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/__snapshots__/packages_list_app_spec.js.snap b/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/__snapshots__/packages_list_app_spec.js.snap
index 99a7b8e427a..7cdf21dde46 100644
--- a/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/__snapshots__/packages_list_app_spec.js.snap
+++ b/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/__snapshots__/packages_list_app_spec.js.snap
@@ -10,10 +10,10 @@ exports[`packages_list_app renders 1`] = `
<div>
<section
- class="row empty-state text-center"
+ class="gl-display-flex empty-state gl-text-center gl-flex-direction-column"
>
<div
- class="col-12"
+ class="gl-max-w-full"
>
<div
class="svg-250 svg-content"
@@ -28,10 +28,10 @@ exports[`packages_list_app renders 1`] = `
</div>
<div
- class="col-12"
+ class="gl-max-w-full gl-m-auto"
>
<div
- class="text-content gl-mx-auto gl-my-0 gl-p-5"
+ class="gl-mx-auto gl-my-0 gl-p-5"
>
<h1
class="gl-font-size-h-display gl-line-height-36 h4"
diff --git a/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/packages_list_spec.js b/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/packages_list_spec.js
index 2fb76b98925..26569f20e94 100644
--- a/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/packages_list_spec.js
+++ b/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/packages_list_spec.js
@@ -134,6 +134,8 @@ describe('packages_list', () => {
});
it('deleteItemConfirmation resets itemToBeDeleted', () => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({ itemToBeDeleted: 1 });
wrapper.vm.deleteItemConfirmation();
expect(wrapper.vm.itemToBeDeleted).toEqual(null);
@@ -141,6 +143,8 @@ describe('packages_list', () => {
it('deleteItemConfirmation emit package:delete', () => {
const itemToBeDeleted = { id: 2 };
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({ itemToBeDeleted });
wrapper.vm.deleteItemConfirmation();
return wrapper.vm.$nextTick(() => {
@@ -149,6 +153,8 @@ describe('packages_list', () => {
});
it('deleteItemCanceled resets itemToBeDeleted', () => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({ itemToBeDeleted: 1 });
wrapper.vm.deleteItemCanceled();
expect(wrapper.vm.itemToBeDeleted).toEqual(null);
@@ -194,6 +200,8 @@ describe('packages_list', () => {
beforeEach(() => {
mountComponent();
eventSpy = jest.spyOn(Tracking, 'event');
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({ itemToBeDeleted: { package_type: 'conan' } });
});
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/conan_installation_spec.js.snap b/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/conan_installation_spec.js.snap
index e9f80d5f512..b3d0d88be4d 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/conan_installation_spec.js.snap
+++ b/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/conan_installation_spec.js.snap
@@ -23,14 +23,18 @@ exports[`ConanInstallation renders all the messages 1`] = `
<code-instruction-stub
copytext="Copy Conan Setup Command"
- instruction="conan remote add gitlab conanPath"
+ instruction="conan remote add gitlab http://gdk.test:3000/api/v4/projects/1/packages/conan"
label="Add Conan Remote"
trackingaction="copy_conan_setup_command"
trackinglabel="code_instruction"
/>
-
- <gl-sprintf-stub
- message="For more information on the Conan registry, %{linkStart}see the documentation%{linkEnd}."
- />
+ For more information on the Conan registry,
+ <gl-link-stub
+ href="/help/user/packages/conan_repository/index"
+ target="_blank"
+ >
+ see the documentation
+ </gl-link-stub>
+ .
</div>
`;
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/file_sha_spec.js.snap b/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/file_sha_spec.js.snap
index 881d441e116..f95564e3fad 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/file_sha_spec.js.snap
+++ b/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/file_sha_spec.js.snap
@@ -15,11 +15,14 @@ exports[`FileSha renders 1`] = `
foo
<gl-button-stub
- aria-label="Copy this value"
+ aria-label="Copy SHA"
+ aria-live="polite"
buttontextclasses=""
category="tertiary"
+ data-clipboard-handle-tooltip="false"
data-clipboard-text="foo"
icon="copy-to-clipboard"
+ id="clipboard-button-1"
size="small"
title="Copy SHA"
variant="default"
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/maven_installation_spec.js.snap b/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/maven_installation_spec.js.snap
index 4865b8205ab..67f1906f6fd 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/maven_installation_spec.js.snap
+++ b/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/maven_installation_spec.js.snap
@@ -19,7 +19,7 @@ exports[`MavenInstallation groovy renders all the messages 1`] = `
<code-instruction-stub
copytext="Copy add Gradle Groovy DSL repository command"
instruction="maven {
- url 'mavenPath'
+ url 'http://gdk.test:3000/api/v4/projects/1/packages/maven'
}"
label="Add Gradle Groovy DSL repository command"
multiline="true"
@@ -47,7 +47,7 @@ exports[`MavenInstallation kotlin renders all the messages 1`] = `
<code-instruction-stub
copytext="Copy add Gradle Kotlin DSL repository command"
- instruction="maven(\\"mavenPath\\")"
+ instruction="maven(\\"http://gdk.test:3000/api/v4/projects/1/packages/maven\\")"
label="Add Gradle Kotlin DSL repository command"
multiline="true"
trackingaction="copy_kotlin_add_to_source_command"
@@ -64,9 +64,15 @@ exports[`MavenInstallation maven renders all the messages 1`] = `
/>
<p>
- <gl-sprintf-stub
- message="Copy and paste this inside your %{codeStart}pom.xml%{codeEnd} %{codeStart}dependencies%{codeEnd} block."
- />
+ Copy and paste this inside your
+ <code>
+ pom.xml
+ </code>
+
+ <code>
+ dependencies
+ </code>
+ block.
</p>
<code-instruction-stub
@@ -97,9 +103,11 @@ exports[`MavenInstallation maven renders all the messages 1`] = `
</h3>
<p>
- <gl-sprintf-stub
- message="If you haven't already done so, you will need to add the below to your %{codeStart}pom.xml%{codeEnd} file."
- />
+ If you haven't already done so, you will need to add the below to your
+ <code>
+ pom.xml
+ </code>
+ file.
</p>
<code-instruction-stub
@@ -107,19 +115,19 @@ exports[`MavenInstallation maven renders all the messages 1`] = `
instruction="<repositories>
<repository>
<id>gitlab-maven</id>
- <url>mavenPath</url>
+ <url>http://gdk.test:3000/api/v4/projects/1/packages/maven</url>
</repository>
</repositories>
<distributionManagement>
<repository>
<id>gitlab-maven</id>
- <url>mavenPath</url>
+ <url>http://gdk.test:3000/api/v4/projects/1/packages/maven</url>
</repository>
<snapshotRepository>
<id>gitlab-maven</id>
- <url>mavenPath</url>
+ <url>http://gdk.test:3000/api/v4/projects/1/packages/maven</url>
</snapshotRepository>
</distributionManagement>"
label=""
@@ -127,9 +135,13 @@ exports[`MavenInstallation maven renders all the messages 1`] = `
trackingaction="copy_maven_setup_xml"
trackinglabel="code_instruction"
/>
-
- <gl-sprintf-stub
- message="For more information on the Maven registry, %{linkStart}see the documentation%{linkEnd}."
- />
+ For more information on the Maven registry,
+ <gl-link-stub
+ href="/help/user/packages/maven_repository/index"
+ target="_blank"
+ >
+ see the documentation
+ </gl-link-stub>
+ .
</div>
`;
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/npm_installation_spec.js.snap b/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/npm_installation_spec.js.snap
index d5649e39561..4520ae9c328 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/npm_installation_spec.js.snap
+++ b/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/npm_installation_spec.js.snap
@@ -32,14 +32,18 @@ exports[`NpmInstallation renders all the messages 1`] = `
<code-instruction-stub
copytext="Copy npm setup command"
- instruction="echo @gitlab-org:registry=npmPath/ >> .npmrc"
+ instruction="echo @gitlab-org:registry=npmInstanceUrl/ >> .npmrc"
label=""
trackingaction="copy_npm_setup_command"
trackinglabel="code_instruction"
/>
-
- <gl-sprintf-stub
- message="You may also need to setup authentication using an auth token. %{linkStart}See the documentation%{linkEnd} to find out more."
- />
+ You may also need to setup authentication using an auth token.
+ <gl-link-stub
+ href="/help/user/packages/npm_registry/index"
+ target="_blank"
+ >
+ See the documentation
+ </gl-link-stub>
+ to find out more.
</div>
`;
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/nuget_installation_spec.js.snap b/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/nuget_installation_spec.js.snap
index 29ddd7b77ed..92930a6309a 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/nuget_installation_spec.js.snap
+++ b/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/nuget_installation_spec.js.snap
@@ -23,14 +23,18 @@ exports[`NugetInstallation renders all the messages 1`] = `
<code-instruction-stub
copytext="Copy NuGet Setup Command"
- instruction="nuget source Add -Name \\"GitLab\\" -Source \\"nugetPath\\" -UserName <your_username> -Password <your_token>"
+ instruction="nuget source Add -Name \\"GitLab\\" -Source \\"http://gdk.test:3000/api/v4/projects/1/packages/nuget/index.json\\" -UserName <your_username> -Password <your_token>"
label="Add NuGet Source"
trackingaction="copy_nuget_setup_command"
trackinglabel="code_instruction"
/>
-
- <gl-sprintf-stub
- message="For more information on the NuGet registry, %{linkStart}see the documentation%{linkEnd}."
- />
+ For more information on the NuGet registry,
+ <gl-link-stub
+ href="/help/user/packages/nuget_repository/index"
+ target="_blank"
+ >
+ see the documentation
+ </gl-link-stub>
+ .
</div>
`;
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/pypi_installation_spec.js.snap b/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/pypi_installation_spec.js.snap
index 158bbbc3463..06ae8645101 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/pypi_installation_spec.js.snap
+++ b/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/pypi_installation_spec.js.snap
@@ -10,7 +10,7 @@ exports[`PypiInstallation renders all the messages 1`] = `
<code-instruction-stub
copytext="Copy Pip command"
data-testid="pip-command"
- instruction="pip install @gitlab-org/package-15 --extra-index-url pypiPath"
+ instruction="pip install @gitlab-org/package-15 --extra-index-url http://__token__:<your_personal_token>@gdk.test:3000/api/v4/projects/1/packages/pypi/simple"
label="Pip Command"
trackingaction="copy_pip_install_command"
trackinglabel="code_instruction"
@@ -23,16 +23,18 @@ exports[`PypiInstallation renders all the messages 1`] = `
</h3>
<p>
- <gl-sprintf-stub
- message="If you haven't already done so, you will need to add the below to your %{codeStart}.pypirc%{codeEnd} file."
- />
+ If you haven't already done so, you will need to add the below to your
+ <code>
+ .pypirc
+ </code>
+ file.
</p>
<code-instruction-stub
copytext="Copy .pypirc content"
data-testid="pypi-setup-content"
instruction="[gitlab]
-repository = pypiSetupPath
+repository = http://gdk.test:3000/api/v4/projects/1/packages/pypi
username = __token__
password = <your personal access token>"
label=""
@@ -40,9 +42,13 @@ password = <your personal access token>"
trackingaction="copy_pypi_setup_command"
trackinglabel="code_instruction"
/>
-
- <gl-sprintf-stub
- message="For more information on the PyPi registry, %{linkStart}see the documentation%{linkEnd}."
- />
+ For more information on the PyPi registry,
+ <gl-link-stub
+ href="/help/user/packages/pypi_repository/index"
+ target="_blank"
+ >
+ see the documentation
+ </gl-link-stub>
+ .
</div>
`;
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/composer_installation_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/composer_installation_spec.js
index aedf20e873a..0aba8f7efc7 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/details/composer_installation_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/details/composer_installation_spec.js
@@ -7,6 +7,7 @@ import {
TRACKING_ACTION_COPY_COMPOSER_REGISTRY_INCLUDE_COMMAND,
TRACKING_ACTION_COPY_COMPOSER_PACKAGE_INCLUDE_COMMAND,
PACKAGE_TYPE_COMPOSER,
+ COMPOSER_HELP_PATH,
} from '~/packages_and_registries/package_registry/constants';
const packageEntity = { ...packageData(), packageType: PACKAGE_TYPE_COMPOSER };
@@ -24,9 +25,6 @@ describe('ComposerInstallation', () => {
function createComponent(groupListUrl = 'groupListUrl') {
wrapper = shallowMountExtended(ComposerInstallation, {
provide: {
- composerHelpPath: 'composerHelpPath',
- composerConfigRepositoryName: 'composerConfigRepositoryName',
- composerPath: 'composerPath',
groupListUrl,
},
propsData: { packageEntity },
@@ -61,7 +59,7 @@ describe('ComposerInstallation', () => {
const registryIncludeCommand = findRegistryInclude();
expect(registryIncludeCommand.exists()).toBe(true);
expect(registryIncludeCommand.props()).toMatchObject({
- instruction: `composer config repositories.composerConfigRepositoryName '{"type": "composer", "url": "composerPath"}'`,
+ instruction: `composer config repositories.${packageEntity.composerConfigRepositoryUrl} '{"type": "composer", "url": "${packageEntity.composerUrl}"}'`,
copyText: 'Copy registry include',
trackingAction: TRACKING_ACTION_COPY_COMPOSER_REGISTRY_INCLUDE_COMMAND,
});
@@ -96,7 +94,7 @@ describe('ComposerInstallation', () => {
'For more information on Composer packages in GitLab, see the documentation.',
);
expect(findHelpLink().attributes()).toMatchObject({
- href: 'composerHelpPath',
+ href: COMPOSER_HELP_PATH,
target: '_blank',
});
});
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/conan_installation_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/conan_installation_spec.js
index 6b642cc21b7..bf9425def9a 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/details/conan_installation_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/details/conan_installation_spec.js
@@ -1,8 +1,12 @@
+import { GlLink, GlSprintf } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { packageData } from 'jest/packages_and_registries/package_registry/mock_data';
import ConanInstallation from '~/packages_and_registries/package_registry/components/details/conan_installation.vue';
import InstallationTitle from '~/packages_and_registries/package_registry/components/details/installation_title.vue';
-import { PACKAGE_TYPE_CONAN } from '~/packages_and_registries/package_registry/constants';
+import {
+ PACKAGE_TYPE_CONAN,
+ CONAN_HELP_PATH,
+} from '~/packages_and_registries/package_registry/constants';
import CodeInstructions from '~/vue_shared/components/registry/code_instruction.vue';
const packageEntity = { ...packageData(), packageType: PACKAGE_TYPE_CONAN };
@@ -12,16 +16,16 @@ describe('ConanInstallation', () => {
const findCodeInstructions = () => wrapper.findAllComponents(CodeInstructions);
const findInstallationTitle = () => wrapper.findComponent(InstallationTitle);
+ const findSetupDocsLink = () => wrapper.findComponent(GlLink);
function createComponent() {
wrapper = shallowMountExtended(ConanInstallation, {
- provide: {
- conanHelpPath: 'conanHelpPath',
- conanPath: 'conanPath',
- },
propsData: {
packageEntity,
},
+ stubs: {
+ GlSprintf,
+ },
});
}
@@ -58,8 +62,15 @@ describe('ConanInstallation', () => {
describe('setup commands', () => {
it('renders the correct command', () => {
expect(findCodeInstructions().at(1).props('instruction')).toBe(
- 'conan remote add gitlab conanPath',
+ `conan remote add gitlab ${packageEntity.conanUrl}`,
);
});
+
+ it('has a link to the docs', () => {
+ expect(findSetupDocsLink().attributes()).toMatchObject({
+ href: CONAN_HELP_PATH,
+ target: '_blank',
+ });
+ });
});
});
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/file_sha_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/file_sha_spec.js
index ebfbbe5b864..feed7a7c46c 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/details/file_sha_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/details/file_sha_spec.js
@@ -4,6 +4,8 @@ import FileSha from '~/packages_and_registries/package_registry/components/detai
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import DetailsRow from '~/vue_shared/components/registry/details_row.vue';
+jest.mock('lodash/uniqueId', () => (prefix) => (prefix ? `${prefix}1` : 1));
+
describe('FileSha', () => {
let wrapper;
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/maven_installation_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/maven_installation_spec.js
index eed7e903833..fc60039db30 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/details/maven_installation_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/details/maven_installation_spec.js
@@ -1,3 +1,4 @@
+import { GlLink, GlSprintf } from '@gitlab/ui';
import { nextTick } from 'vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
@@ -16,6 +17,7 @@ import {
TRACKING_ACTION_COPY_KOTLIN_INSTALL_COMMAND,
TRACKING_ACTION_COPY_KOTLIN_ADD_TO_SOURCE_COMMAND,
PACKAGE_TYPE_MAVEN,
+ MAVEN_HELP_PATH,
} from '~/packages_and_registries/package_registry/constants';
import CodeInstructions from '~/vue_shared/components/registry/code_instruction.vue';
@@ -28,9 +30,6 @@ describe('MavenInstallation', () => {
metadata: mavenMetadata(),
};
- const mavenHelpPath = 'mavenHelpPath';
- const mavenPath = 'mavenPath';
-
const xmlCodeBlock = `<dependency>
<groupId>appGroup</groupId>
<artifactId>appName</artifactId>
@@ -40,43 +39,43 @@ describe('MavenInstallation', () => {
const mavenSetupXml = `<repositories>
<repository>
<id>gitlab-maven</id>
- <url>${mavenPath}</url>
+ <url>${packageEntity.mavenUrl}</url>
</repository>
</repositories>
<distributionManagement>
<repository>
<id>gitlab-maven</id>
- <url>${mavenPath}</url>
+ <url>${packageEntity.mavenUrl}</url>
</repository>
<snapshotRepository>
<id>gitlab-maven</id>
- <url>${mavenPath}</url>
+ <url>${packageEntity.mavenUrl}</url>
</snapshotRepository>
</distributionManagement>`;
const gradleGroovyInstallCommandText = `implementation 'appGroup:appName:appVersion'`;
const gradleGroovyAddSourceCommandText = `maven {
- url '${mavenPath}'
+ url '${packageEntity.mavenUrl}'
}`;
const gradleKotlinInstallCommandText = `implementation("appGroup:appName:appVersion")`;
- const gradleKotlinAddSourceCommandText = `maven("${mavenPath}")`;
+ const gradleKotlinAddSourceCommandText = `maven("${packageEntity.mavenUrl}")`;
const findCodeInstructions = () => wrapper.findAllComponents(CodeInstructions);
const findInstallationTitle = () => wrapper.findComponent(InstallationTitle);
+ const findSetupDocsLink = () => wrapper.findComponent(GlLink);
function createComponent({ data = {} } = {}) {
wrapper = shallowMountExtended(MavenInstallation, {
- provide: {
- mavenHelpPath,
- mavenPath,
- },
propsData: {
packageEntity,
},
data() {
return data;
},
+ stubs: {
+ GlSprintf,
+ },
});
}
@@ -148,6 +147,13 @@ describe('MavenInstallation', () => {
trackingAction: TRACKING_ACTION_COPY_MAVEN_SETUP,
});
});
+
+ it('has a setup link', () => {
+ expect(findSetupDocsLink().attributes()).toMatchObject({
+ href: MAVEN_HELP_PATH,
+ target: '_blank',
+ });
+ });
});
});
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/npm_installation_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/npm_installation_spec.js
index b89410ede13..8c0e2d948ca 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/details/npm_installation_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/details/npm_installation_spec.js
@@ -1,4 +1,4 @@
-import { GlFormRadioGroup } from '@gitlab/ui';
+import { GlLink, GlSprintf, GlFormRadioGroup } from '@gitlab/ui';
import { nextTick } from 'vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
@@ -15,6 +15,7 @@ import {
YARN_PACKAGE_MANAGER,
PROJECT_PACKAGE_ENDPOINT_TYPE,
INSTANCE_PACKAGE_ENDPOINT_TYPE,
+ NPM_HELP_PATH,
} from '~/packages_and_registries/package_registry/constants';
import CodeInstructions from '~/vue_shared/components/registry/code_instruction.vue';
@@ -29,13 +30,12 @@ describe('NpmInstallation', () => {
const findCodeInstructions = () => wrapper.findAllComponents(CodeInstructions);
const findInstallationTitle = () => wrapper.findComponent(InstallationTitle);
const findEndPointTypeSector = () => wrapper.findComponent(GlFormRadioGroup);
+ const findSetupDocsLink = () => wrapper.findComponent(GlLink);
function createComponent({ data = {} } = {}) {
wrapper = shallowMountExtended(NpmInstallation, {
provide: {
- npmHelpPath: 'npmHelpPath',
- npmPath: 'npmPath',
- npmProjectPath: 'npmProjectPath',
+ npmInstanceUrl: 'npmInstanceUrl',
},
propsData: {
packageEntity,
@@ -43,6 +43,7 @@ describe('NpmInstallation', () => {
data() {
return data;
},
+ stubs: { GlSprintf },
});
}
@@ -58,6 +59,13 @@ describe('NpmInstallation', () => {
expect(wrapper.element).toMatchSnapshot();
});
+ it('has a setup link', () => {
+ expect(findSetupDocsLink().attributes()).toMatchObject({
+ href: NPM_HELP_PATH,
+ target: '_blank',
+ });
+ });
+
describe('endpoint type selector', () => {
it('has the endpoint type selector', () => {
expect(findEndPointTypeSector().exists()).toBe(true);
@@ -109,7 +117,7 @@ describe('NpmInstallation', () => {
it('renders the correct setup command', () => {
expect(findCodeInstructions().at(1).props()).toMatchObject({
- instruction: 'echo @gitlab-org:registry=npmPath/ >> .npmrc',
+ instruction: 'echo @gitlab-org:registry=npmInstanceUrl/ >> .npmrc',
multiline: false,
trackingAction: TRACKING_ACTION_COPY_NPM_SETUP_COMMAND,
});
@@ -121,7 +129,7 @@ describe('NpmInstallation', () => {
await nextTick();
expect(findCodeInstructions().at(1).props()).toMatchObject({
- instruction: `echo @gitlab-org:registry=npmProjectPath/ >> .npmrc`,
+ instruction: `echo @gitlab-org:registry=${packageEntity.npmUrl}/ >> .npmrc`,
multiline: false,
trackingAction: TRACKING_ACTION_COPY_NPM_SETUP_COMMAND,
});
@@ -131,7 +139,7 @@ describe('NpmInstallation', () => {
await nextTick();
expect(findCodeInstructions().at(1).props()).toMatchObject({
- instruction: `echo @gitlab-org:registry=npmPath/ >> .npmrc`,
+ instruction: `echo @gitlab-org:registry=npmInstanceUrl/ >> .npmrc`,
multiline: false,
trackingAction: TRACKING_ACTION_COPY_NPM_SETUP_COMMAND,
});
@@ -153,7 +161,7 @@ describe('NpmInstallation', () => {
it('renders the correct registry command', () => {
expect(findCodeInstructions().at(1).props()).toMatchObject({
- instruction: 'echo \\"@gitlab-org:registry\\" \\"npmPath/\\" >> .yarnrc',
+ instruction: 'echo \\"@gitlab-org:registry\\" \\"npmInstanceUrl/\\" >> .yarnrc',
multiline: false,
trackingAction: TRACKING_ACTION_COPY_YARN_SETUP_COMMAND,
});
@@ -165,7 +173,7 @@ describe('NpmInstallation', () => {
await nextTick();
expect(findCodeInstructions().at(1).props()).toMatchObject({
- instruction: `echo \\"@gitlab-org:registry\\" \\"npmProjectPath/\\" >> .yarnrc`,
+ instruction: `echo \\"@gitlab-org:registry\\" \\"${packageEntity.npmUrl}/\\" >> .yarnrc`,
multiline: false,
trackingAction: TRACKING_ACTION_COPY_YARN_SETUP_COMMAND,
});
@@ -175,7 +183,7 @@ describe('NpmInstallation', () => {
await nextTick();
expect(findCodeInstructions().at(1).props()).toMatchObject({
- instruction: 'echo \\"@gitlab-org:registry\\" \\"npmPath/\\" >> .yarnrc',
+ instruction: 'echo \\"@gitlab-org:registry\\" \\"npmInstanceUrl/\\" >> .yarnrc',
multiline: false,
trackingAction: TRACKING_ACTION_COPY_YARN_SETUP_COMMAND,
});
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/nuget_installation_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/nuget_installation_spec.js
index c48a3f07299..d324d43258c 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/details/nuget_installation_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/details/nuget_installation_spec.js
@@ -1,3 +1,4 @@
+import { GlLink, GlSprintf } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { packageData } from 'jest/packages_and_registries/package_registry/mock_data';
import InstallationTitle from '~/packages_and_registries/package_registry/components/details/installation_title.vue';
@@ -6,6 +7,7 @@ import {
TRACKING_ACTION_COPY_NUGET_INSTALL_COMMAND,
TRACKING_ACTION_COPY_NUGET_SETUP_COMMAND,
PACKAGE_TYPE_NUGET,
+ NUGET_HELP_PATH,
} from '~/packages_and_registries/package_registry/constants';
import CodeInstructions from '~/vue_shared/components/registry/code_instruction.vue';
@@ -15,21 +17,18 @@ describe('NugetInstallation', () => {
let wrapper;
const nugetInstallationCommandStr = 'nuget install @gitlab-org/package-15 -Source "GitLab"';
- const nugetSetupCommandStr =
- 'nuget source Add -Name "GitLab" -Source "nugetPath" -UserName <your_username> -Password <your_token>';
+ const nugetSetupCommandStr = `nuget source Add -Name "GitLab" -Source "${packageEntity.nugetUrl}" -UserName <your_username> -Password <your_token>`;
const findCodeInstructions = () => wrapper.findAllComponents(CodeInstructions);
const findInstallationTitle = () => wrapper.findComponent(InstallationTitle);
+ const findSetupDocsLink = () => wrapper.findComponent(GlLink);
function createComponent() {
wrapper = shallowMountExtended(NugetInstallation, {
- provide: {
- nugetHelpPath: 'nugetHelpPath',
- nugetPath: 'nugetPath',
- },
propsData: {
packageEntity,
},
+ stubs: { GlSprintf },
});
}
@@ -71,5 +70,12 @@ describe('NugetInstallation', () => {
trackingAction: TRACKING_ACTION_COPY_NUGET_SETUP_COMMAND,
});
});
+
+ it('it has docs link', () => {
+ expect(findSetupDocsLink().attributes()).toMatchObject({
+ href: NUGET_HELP_PATH,
+ target: '_blank',
+ });
+ });
});
});
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/package_files_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/package_files_spec.js
index 042b2026199..f8a4ba8f3bc 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/details/package_files_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/details/package_files_spec.js
@@ -28,8 +28,8 @@ describe('Package Files', () => {
const createComponent = ({ packageFiles = [file], canDelete = true } = {}) => {
wrapper = mountExtended(PackageFiles, {
- provide: { canDelete },
propsData: {
+ canDelete,
packageFiles,
},
stubs: {
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/pypi_installation_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/pypi_installation_spec.js
index 410c1b65348..f2fef6436a6 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/details/pypi_installation_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/details/pypi_installation_spec.js
@@ -1,3 +1,4 @@
+import { GlLink, GlSprintf } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { packageData } from 'jest/packages_and_registries/package_registry/mock_data';
import InstallationTitle from '~/packages_and_registries/package_registry/components/details/installation_title.vue';
@@ -6,6 +7,7 @@ import {
PACKAGE_TYPE_PYPI,
TRACKING_ACTION_COPY_PIP_INSTALL_COMMAND,
TRACKING_ACTION_COPY_PYPI_SETUP_COMMAND,
+ PYPI_HELP_PATH,
} from '~/packages_and_registries/package_registry/constants';
const packageEntity = { ...packageData(), packageType: PACKAGE_TYPE_PYPI };
@@ -13,9 +15,9 @@ const packageEntity = { ...packageData(), packageType: PACKAGE_TYPE_PYPI };
describe('PypiInstallation', () => {
let wrapper;
- const pipCommandStr = 'pip install @gitlab-org/package-15 --extra-index-url pypiPath';
+ const pipCommandStr = `pip install @gitlab-org/package-15 --extra-index-url ${packageEntity.pypiUrl}`;
const pypiSetupStr = `[gitlab]
-repository = pypiSetupPath
+repository = ${packageEntity.pypiSetupUrl}
username = __token__
password = <your personal access token>`;
@@ -23,17 +25,16 @@ password = <your personal access token>`;
const setupInstruction = () => wrapper.findByTestId('pypi-setup-content');
const findInstallationTitle = () => wrapper.findComponent(InstallationTitle);
+ const findSetupDocsLink = () => wrapper.findComponent(GlLink);
function createComponent() {
wrapper = shallowMountExtended(PypiInstallation, {
- provide: {
- pypiHelpPath: 'pypiHelpPath',
- pypiPath: 'pypiPath',
- pypiSetupPath: 'pypiSetupPath',
- },
propsData: {
packageEntity,
},
+ stubs: {
+ GlSprintf,
+ },
});
}
@@ -76,5 +77,12 @@ password = <your personal access token>`;
trackingAction: TRACKING_ACTION_COPY_PYPI_SETUP_COMMAND,
});
});
+
+ it('has a link to the docs', () => {
+ expect(findSetupDocsLink().attributes()).toMatchObject({
+ href: PYPI_HELP_PATH,
+ target: '_blank',
+ });
+ });
});
});
diff --git a/spec/frontend/packages_and_registries/package_registry/components/list/__snapshots__/package_list_row_spec.js.snap b/spec/frontend/packages_and_registries/package_registry/components/list/__snapshots__/package_list_row_spec.js.snap
index 165ee962417..18a99f70756 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/list/__snapshots__/package_list_row_spec.js.snap
+++ b/spec/frontend/packages_and_registries/package_registry/components/list/__snapshots__/package_list_row_spec.js.snap
@@ -22,16 +22,20 @@ exports[`packages_list_row renders 1`] = `
<div
class="gl-display-flex gl-align-items-center gl-mr-3 gl-min-w-0"
>
- <gl-link-stub
+ <router-link-stub
+ ariacurrentvalue="page"
class="gl-text-body gl-min-w-0"
data-qa-selector="package_link"
- href="http://gdk.test:3000/gitlab-org/gitlab-test/-/packages/111"
+ data-testid="details-link"
+ event="click"
+ tag="a"
+ to="[object Object]"
>
<gl-truncate-stub
position="end"
text="@gitlab-org/package-15"
/>
- </gl-link-stub>
+ </router-link-stub>
<!---->
diff --git a/spec/frontend/packages_and_registries/package_registry/components/list/package_list_row_spec.js b/spec/frontend/packages_and_registries/package_registry/components/list/package_list_row_spec.js
index 292667ec47c..9467a613b2a 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/list/package_list_row_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/list/package_list_row_spec.js
@@ -1,7 +1,11 @@
-import { GlLink, GlSprintf } from '@gitlab/ui';
+import { GlSprintf } from '@gitlab/ui';
+import { createLocalVue } from '@vue/test-utils';
+import VueRouter from 'vue-router';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+
import PackagesListRow from '~/packages_and_registries/package_registry/components/list/package_list_row.vue';
import PackagePath from '~/packages_and_registries/shared/components/package_path.vue';
import PackageTags from '~/packages_and_registries/shared/components/package_tags.vue';
@@ -13,6 +17,9 @@ import { PACKAGE_ERROR_STATUS } from '~/packages_and_registries/package_registry
import ListItem from '~/vue_shared/components/registry/list_item.vue';
import { packageData, packagePipelines, packageProject, packageTags } from '../../mock_data';
+const localVue = createLocalVue();
+localVue.use(VueRouter);
+
describe('packages_list_row', () => {
let wrapper;
@@ -28,7 +35,7 @@ describe('packages_list_row', () => {
const findDeleteButton = () => wrapper.findByTestId('action-delete');
const findPackageIconAndName = () => wrapper.find(PackageIconAndName);
const findListItem = () => wrapper.findComponent(ListItem);
- const findPackageLink = () => wrapper.findComponent(GlLink);
+ const findPackageLink = () => wrapper.findByTestId('details-link');
const findWarningIcon = () => wrapper.findByTestId('warning-icon');
const findLeftSecondaryInfos = () => wrapper.findByTestId('left-secondary-infos');
const findPublishMethod = () => wrapper.findComponent(PublishMethod);
@@ -40,6 +47,7 @@ describe('packages_list_row', () => {
provide = defaultProvide,
} = {}) => {
wrapper = shallowMountExtended(PackagesListRow, {
+ localVue,
provide,
stubs: {
ListItem,
@@ -63,6 +71,15 @@ describe('packages_list_row', () => {
expect(wrapper.element).toMatchSnapshot();
});
+ it('has a link to navigate to the details page', () => {
+ mountComponent();
+
+ expect(findPackageLink().props()).toMatchObject({
+ event: 'click',
+ to: { name: 'details', params: { id: getIdFromGraphQLId(packageWithoutTags.id) } },
+ });
+ });
+
describe('tags', () => {
it('renders package tags when a package has tags', () => {
mountComponent({ packageEntity: packageWithTags });
@@ -120,7 +137,7 @@ describe('packages_list_row', () => {
});
it('details link is disabled', () => {
- expect(findPackageLink().attributes('disabled')).toBe('true');
+ expect(findPackageLink().props('event')).toBe('');
});
it('has a warning icon', () => {
diff --git a/spec/frontend/packages_and_registries/package_registry/mock_data.js b/spec/frontend/packages_and_registries/package_registry/mock_data.js
index 4c23b52b8a2..c6a59f20998 100644
--- a/spec/frontend/packages_and_registries/package_registry/mock_data.js
+++ b/spec/frontend/packages_and_registries/package_registry/mock_data.js
@@ -120,12 +120,22 @@ export const packageVersions = () => [
export const packageData = (extend) => ({
id: 'gid://gitlab/Packages::Package/111',
+ canDestroy: true,
name: '@gitlab-org/package-15',
packageType: 'NPM',
version: '1.0.0',
createdAt: '2020-08-17T14:23:32Z',
updatedAt: '2020-08-17T14:23:32Z',
status: 'DEFAULT',
+ mavenUrl: 'http://gdk.test:3000/api/v4/projects/1/packages/maven',
+ npmUrl: 'http://gdk.test:3000/api/v4/projects/1/packages/npm',
+ nugetUrl: 'http://gdk.test:3000/api/v4/projects/1/packages/nuget/index.json',
+ composerConfigRepositoryUrl: 'gdk.test/22',
+ composerUrl: 'http://gdk.test:3000/api/v4/group/22/-/packages/composer/packages.json',
+ conanUrl: 'http://gdk.test:3000/api/v4/projects/1/packages/conan',
+ pypiUrl:
+ 'http://__token__:<your_personal_token>@gdk.test:3000/api/v4/projects/1/packages/pypi/simple',
+ pypiSetupUrl: 'http://gdk.test:3000/api/v4/projects/1/packages/pypi',
...extend,
});
@@ -185,6 +195,7 @@ export const packageDetailsQuery = (extendPackage) => ({
project: {
id: '1',
path: 'projectPath',
+ name: 'gitlab-test',
},
tags: {
nodes: packageTags(),
diff --git a/spec/frontend/packages_and_registries/package_registry/pages/__snapshots__/list_spec.js.snap b/spec/frontend/packages_and_registries/package_registry/pages/__snapshots__/list_spec.js.snap
index dbe3c70c3cb..ed96abe24b1 100644
--- a/spec/frontend/packages_and_registries/package_registry/pages/__snapshots__/list_spec.js.snap
+++ b/spec/frontend/packages_and_registries/package_registry/pages/__snapshots__/list_spec.js.snap
@@ -11,10 +11,10 @@ exports[`PackagesListApp renders 1`] = `
<div>
<section
- class="row empty-state text-center"
+ class="gl-display-flex empty-state gl-text-center gl-flex-direction-column"
>
<div
- class="col-12"
+ class="gl-max-w-full"
>
<div
class="svg-250 svg-content"
@@ -29,10 +29,10 @@ exports[`PackagesListApp renders 1`] = `
</div>
<div
- class="col-12"
+ class="gl-max-w-full gl-m-auto"
>
<div
- class="text-content gl-mx-auto gl-my-0 gl-p-5"
+ class="gl-mx-auto gl-my-0 gl-p-5"
>
<h1
class="gl-font-size-h-display gl-line-height-36 h4"
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/app_spec.js b/spec/frontend/packages_and_registries/package_registry/pages/details_spec.js
index 0bea84693f6..637e2edf3be 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/details/app_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/pages/details_spec.js
@@ -9,7 +9,7 @@ import waitForPromises from 'helpers/wait_for_promises';
import createFlash from '~/flash';
import AdditionalMetadata from '~/packages_and_registries/package_registry/components/details/additional_metadata.vue';
-import PackagesApp from '~/packages_and_registries/package_registry/components/details/app.vue';
+import PackagesApp from '~/packages_and_registries/package_registry/pages/details.vue';
import DependencyRow from '~/packages_and_registries/package_registry/components/details/dependency_row.vue';
import InstallationCommands from '~/packages_and_registries/package_registry/components/details/installation_commands.vue';
import PackageFiles from '~/packages_and_registries/package_registry/components/details/package_files.vue';
@@ -36,7 +36,7 @@ import {
packageFiles,
packageDestroyFileMutation,
packageDestroyFileMutationError,
-} from '../../mock_data';
+} from '../mock_data';
jest.mock('~/flash');
useMockLocationHelper();
@@ -47,21 +47,22 @@ describe('PackagesApp', () => {
let wrapper;
let apolloProvider;
+ const breadCrumbState = {
+ updateName: jest.fn(),
+ };
+
const provide = {
packageId: '111',
- titleComponent: 'PackageTitle',
- projectName: 'projectName',
- canDelete: 'canDelete',
- svgPath: 'svgPath',
- npmPath: 'npmPath',
- npmHelpPath: 'npmHelpPath',
+ emptyListIllustration: 'svgPath',
projectListUrl: 'projectListUrl',
groupListUrl: 'groupListUrl',
+ breadCrumbState,
};
function createComponent({
resolver = jest.fn().mockResolvedValue(packageDetailsQuery()),
fileDeleteMutationResolver = jest.fn().mockResolvedValue(packageDestroyFileMutation()),
+ routeId = '1',
} = {}) {
localVue.use(VueApollo);
@@ -87,6 +88,13 @@ describe('PackagesApp', () => {
GlTabs,
GlTab,
},
+ mocks: {
+ $route: {
+ params: {
+ id: routeId,
+ },
+ },
+ },
});
}
@@ -149,7 +157,7 @@ describe('PackagesApp', () => {
expect(findPackageHistory().exists()).toBe(true);
expect(findPackageHistory().props()).toMatchObject({
packageEntity: expect.objectContaining(packageData()),
- projectName: provide.projectName,
+ projectName: packageDetailsQuery().data.package.project.name,
});
});
@@ -175,9 +183,18 @@ describe('PackagesApp', () => {
});
});
+ it('calls the appropriate function to set the breadcrumbState', async () => {
+ const { name, version } = packageData();
+ createComponent();
+
+ await waitForPromises();
+
+ expect(breadCrumbState.updateName).toHaveBeenCalledWith(`${name} v ${version}`);
+ });
+
describe('delete package', () => {
const originalReferrer = document.referrer;
- const setReferrer = (value = provide.projectName) => {
+ const setReferrer = (value = packageDetailsQuery().data.package.project.name) => {
Object.defineProperty(document, 'referrer', {
value,
configurable: true,
@@ -244,6 +261,7 @@ describe('PackagesApp', () => {
expect(findPackageFiles().exists()).toBe(true);
expect(findPackageFiles().props('packageFiles')[0]).toMatchObject(expectedFile);
+ expect(findPackageFiles().props('canDelete')).toBe(packageData().canDestroy);
});
it('does not render the package files table when the package is composer', async () => {
diff --git a/spec/frontend/packages_and_registries/shared/__snapshots__/publish_method_spec.js.snap b/spec/frontend/packages_and_registries/shared/components/__snapshots__/publish_method_spec.js.snap
index 5f243799bae..5f243799bae 100644
--- a/spec/frontend/packages_and_registries/shared/__snapshots__/publish_method_spec.js.snap
+++ b/spec/frontend/packages_and_registries/shared/components/__snapshots__/publish_method_spec.js.snap
diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/components/__snapshots__/registry_breadcrumb_spec.js.snap b/spec/frontend/packages_and_registries/shared/components/__snapshots__/registry_breadcrumb_spec.js.snap
index 7044c1285d8..ceae8eebaef 100644
--- a/spec/frontend/packages_and_registries/container_registry/explorer/components/__snapshots__/registry_breadcrumb_spec.js.snap
+++ b/spec/frontend/packages_and_registries/shared/components/__snapshots__/registry_breadcrumb_spec.js.snap
@@ -1,7 +1,8 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Registry Breadcrumb when is not rootRoute renders 1`] = `
-<div
+<nav
+ aria-label="Breadcrumb"
class="gl-breadcrumbs"
>
@@ -24,19 +25,25 @@ exports[`Registry Breadcrumb when is not rootRoute renders 1`] = `
class="gl-breadcrumb-separator"
data-testid="separator"
>
- <svg
- aria-hidden="true"
- class="gl-icon s8"
- data-testid="angle-right-icon"
- role="img"
+ <span
+ class="gl-mx-n5"
>
- <use
- href="#angle-right"
- />
- </svg>
+ <svg
+ aria-hidden="true"
+ class="gl-icon s8"
+ data-testid="angle-right-icon"
+ role="img"
+ >
+ <use
+ href="#angle-right"
+ />
+ </svg>
+ </span>
</span>
</a>
</li>
+
+ <!---->
<li
class="breadcrumb-item gl-breadcrumb-item"
>
@@ -52,12 +59,15 @@ exports[`Registry Breadcrumb when is not rootRoute renders 1`] = `
<!---->
</a>
</li>
+
+ <!---->
</ol>
-</div>
+</nav>
`;
exports[`Registry Breadcrumb when is rootRoute renders 1`] = `
-<div
+<nav
+ aria-label="Breadcrumb"
class="gl-breadcrumbs"
>
@@ -79,6 +89,8 @@ exports[`Registry Breadcrumb when is rootRoute renders 1`] = `
<!---->
</a>
</li>
+
+ <!---->
</ol>
-</div>
+</nav>
`;
diff --git a/spec/frontend/packages_and_registries/shared/package_icon_and_name_spec.js b/spec/frontend/packages_and_registries/shared/components/package_icon_and_name_spec.js
index d6d1970cb12..d6d1970cb12 100644
--- a/spec/frontend/packages_and_registries/shared/package_icon_and_name_spec.js
+++ b/spec/frontend/packages_and_registries/shared/components/package_icon_and_name_spec.js
diff --git a/spec/frontend/packages_and_registries/shared/package_path_spec.js b/spec/frontend/packages_and_registries/shared/components/package_path_spec.js
index 93425d4f399..93425d4f399 100644
--- a/spec/frontend/packages_and_registries/shared/package_path_spec.js
+++ b/spec/frontend/packages_and_registries/shared/components/package_path_spec.js
diff --git a/spec/frontend/packages_and_registries/shared/package_tags_spec.js b/spec/frontend/packages_and_registries/shared/components/package_tags_spec.js
index 33e96c0775e..33e96c0775e 100644
--- a/spec/frontend/packages_and_registries/shared/package_tags_spec.js
+++ b/spec/frontend/packages_and_registries/shared/components/package_tags_spec.js
diff --git a/spec/frontend/packages_and_registries/shared/packages_list_loader_spec.js b/spec/frontend/packages_and_registries/shared/components/packages_list_loader_spec.js
index 0005162e0bb..0005162e0bb 100644
--- a/spec/frontend/packages_and_registries/shared/packages_list_loader_spec.js
+++ b/spec/frontend/packages_and_registries/shared/components/packages_list_loader_spec.js
diff --git a/spec/frontend/packages_and_registries/shared/components/persisted_search_spec.js b/spec/frontend/packages_and_registries/shared/components/persisted_search_spec.js
new file mode 100644
index 00000000000..bd492a5ae8f
--- /dev/null
+++ b/spec/frontend/packages_and_registries/shared/components/persisted_search_spec.js
@@ -0,0 +1,145 @@
+import { nextTick } from 'vue';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import RegistrySearch from '~/vue_shared/components/registry/registry_search.vue';
+import component from '~/packages_and_registries/shared/components/persisted_search.vue';
+import UrlSync from '~/vue_shared/components/url_sync.vue';
+import { useMockLocationHelper } from 'helpers/mock_window_location_helper';
+import { getQueryParams, extractFilterAndSorting } from '~/packages_and_registries/shared/utils';
+
+jest.mock('~/packages_and_registries/shared/utils');
+
+useMockLocationHelper();
+
+describe('Persisted Search', () => {
+ let wrapper;
+
+ const defaultQueryParamsMock = {
+ filters: ['foo'],
+ sorting: { sort: 'desc', orderBy: 'test' },
+ };
+
+ const defaultProps = {
+ sortableFields: [
+ { orderBy: 'test', label: 'test' },
+ { orderBy: 'foo', label: 'foo' },
+ ],
+ defaultOrder: 'test',
+ defaultSort: 'asc',
+ };
+
+ const findRegistrySearch = () => wrapper.findComponent(RegistrySearch);
+ const findUrlSync = () => wrapper.findComponent(UrlSync);
+
+ const mountComponent = (propsData = defaultProps) => {
+ wrapper = shallowMountExtended(component, {
+ propsData,
+ stubs: {
+ UrlSync,
+ },
+ });
+ };
+
+ beforeEach(() => {
+ extractFilterAndSorting.mockReturnValue(defaultQueryParamsMock);
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('has a registry search component', async () => {
+ mountComponent();
+
+ await nextTick();
+
+ expect(findRegistrySearch().exists()).toBe(true);
+ });
+
+ it('registry search is mounted after mount', async () => {
+ mountComponent();
+
+ expect(findRegistrySearch().exists()).toBe(false);
+ });
+
+ it('has a UrlSync component', () => {
+ mountComponent();
+
+ expect(findUrlSync().exists()).toBe(true);
+ });
+
+ it('on sorting:changed emits update event and update internal sort', async () => {
+ const payload = { sort: 'desc', orderBy: 'test' };
+
+ mountComponent();
+
+ await nextTick();
+
+ findRegistrySearch().vm.$emit('sorting:changed', payload);
+
+ await nextTick();
+
+ expect(findRegistrySearch().props('sorting')).toMatchObject(payload);
+
+ // there is always a first call on mounted that emits up default values
+ expect(wrapper.emitted('update')[1]).toEqual([
+ {
+ filters: ['foo'],
+ sort: 'TEST_DESC',
+ },
+ ]);
+ });
+
+ it('on filter:changed updates the filters', async () => {
+ const payload = ['foo'];
+
+ mountComponent();
+
+ await nextTick();
+
+ findRegistrySearch().vm.$emit('filter:changed', payload);
+
+ await nextTick();
+
+ expect(findRegistrySearch().props('filter')).toEqual(['foo']);
+ });
+
+ it('on filter:submit emits update event', async () => {
+ mountComponent();
+
+ await nextTick();
+
+ findRegistrySearch().vm.$emit('filter:submit');
+
+ expect(wrapper.emitted('update')[1]).toEqual([
+ {
+ filters: ['foo'],
+ sort: 'TEST_DESC',
+ },
+ ]);
+ });
+
+ it('on query:changed calls updateQuery from UrlSync', async () => {
+ jest.spyOn(UrlSync.methods, 'updateQuery').mockImplementation(() => {});
+
+ mountComponent();
+
+ await nextTick();
+
+ findRegistrySearch().vm.$emit('query:changed');
+
+ expect(UrlSync.methods.updateQuery).toHaveBeenCalled();
+ });
+
+ it('sets the component sorting and filtering based on the querystring', async () => {
+ mountComponent();
+
+ await nextTick();
+
+ expect(getQueryParams).toHaveBeenCalled();
+
+ expect(findRegistrySearch().props()).toMatchObject({
+ filter: defaultQueryParamsMock.filters,
+ sorting: defaultQueryParamsMock.sorting,
+ });
+ });
+});
diff --git a/spec/frontend/packages_and_registries/shared/publish_method_spec.js b/spec/frontend/packages_and_registries/shared/components/publish_method_spec.js
index fa8f8f7641a..fa8f8f7641a 100644
--- a/spec/frontend/packages_and_registries/shared/publish_method_spec.js
+++ b/spec/frontend/packages_and_registries/shared/components/publish_method_spec.js
diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/components/registry_breadcrumb_spec.js b/spec/frontend/packages_and_registries/shared/components/registry_breadcrumb_spec.js
index e5a8438f23f..6dfe116c285 100644
--- a/spec/frontend/packages_and_registries/container_registry/explorer/components/registry_breadcrumb_spec.js
+++ b/spec/frontend/packages_and_registries/shared/components/registry_breadcrumb_spec.js
@@ -1,6 +1,6 @@
import { mount } from '@vue/test-utils';
-import component from '~/packages_and_registries/container_registry/explorer/components/registry_breadcrumb.vue';
+import component from '~/packages_and_registries/shared/components/registry_breadcrumb.vue';
describe('Registry Breadcrumb', () => {
let wrapper;
diff --git a/spec/frontend/pages/dashboard/todos/index/todos_spec.js b/spec/frontend/pages/dashboard/todos/index/todos_spec.js
index 5bba98bdf96..6a7ce80ec5a 100644
--- a/spec/frontend/pages/dashboard/todos/index/todos_spec.js
+++ b/spec/frontend/pages/dashboard/todos/index/todos_spec.js
@@ -94,13 +94,13 @@ describe('Todos', () => {
});
it('updates pending text', () => {
- expect(document.querySelector('.js-todos-pending .badge').innerHTML).toEqual(
+ expect(document.querySelector('.js-todos-pending .js-todos-badge').innerHTML).toEqual(
addDelimiter(TEST_COUNT_BIG),
);
});
it('updates done text', () => {
- expect(document.querySelector('.js-todos-done .badge').innerHTML).toEqual(
+ expect(document.querySelector('.js-todos-done .js-todos-badge').innerHTML).toEqual(
addDelimiter(TEST_DONE_COUNT_BIG),
);
});
diff --git a/spec/frontend/pages/projects/pipeline_schedules/shared/components/timezone_dropdown_spec.js b/spec/frontend/pages/projects/pipeline_schedules/shared/components/timezone_dropdown_spec.js
index 53c1733eab9..b700c255e8c 100644
--- a/spec/frontend/pages/projects/pipeline_schedules/shared/components/timezone_dropdown_spec.js
+++ b/spec/frontend/pages/projects/pipeline_schedules/shared/components/timezone_dropdown_spec.js
@@ -38,14 +38,14 @@ describe('Timezone Dropdown', () => {
const tzStr = '[UTC + 5.5] Sri Jayawardenepura';
const tzValue = 'Asia/Colombo';
- expect($inputEl.val()).toBe('UTC');
+ expect($inputEl.val()).toBe('Etc/UTC');
$(`${tzListSel}:contains('${tzStr}')`, $wrapper).trigger('click');
const val = $inputEl.val();
expect(val).toBe(tzValue);
- expect(val).not.toBe('UTC');
+ expect(val).not.toBe('Etc/UTC');
});
it('will format data array of timezones into a list of offsets', () => {
@@ -67,7 +67,7 @@ describe('Timezone Dropdown', () => {
it('will default the timezone to UTC', () => {
const tz = $inputEl.val();
- expect(tz).toBe('UTC');
+ expect(tz).toBe('Etc/UTC');
});
});
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 0020269e4e7..8a9bb025d55 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
@@ -7,6 +7,7 @@ import {
visibilityLevelDescriptions,
visibilityOptions,
} from '~/pages/projects/shared/permissions/constants';
+import ConfirmDanger from '~/vue_shared/components/confirm_danger/confirm_danger.vue';
const defaultProps = {
currentSettings: {
@@ -47,6 +48,8 @@ const defaultProps = {
packagesAvailable: false,
packagesHelpPath: '/help/user/packages/index',
requestCveAvailable: true,
+ confirmationPhrase: 'my-fake-project',
+ showVisibilityConfirmModal: false,
};
describe('Settings Panel', () => {
@@ -104,6 +107,7 @@ describe('Settings Panel', () => {
);
const findMetricsVisibilitySettings = () => wrapper.find({ ref: 'metrics-visibility-settings' });
const findOperationsSettings = () => wrapper.find({ ref: 'operations-settings' });
+ const findConfirmDangerButton = () => wrapper.findComponent(ConfirmDanger);
afterEach(() => {
wrapper.destroy();
@@ -177,6 +181,44 @@ describe('Settings Panel', () => {
expect(findRequestAccessEnabledInput().exists()).toBe(false);
});
+
+ it('does not require confirmation if the visibility is reduced', async () => {
+ wrapper = mountComponent({
+ currentSettings: { visibilityLevel: visibilityOptions.INTERNAL },
+ });
+
+ expect(findConfirmDangerButton().exists()).toBe(false);
+
+ await findProjectVisibilityLevelInput().setValue(visibilityOptions.PRIVATE);
+
+ expect(findConfirmDangerButton().exists()).toBe(false);
+ });
+
+ describe('showVisibilityConfirmModal=true', () => {
+ beforeEach(() => {
+ wrapper = mountComponent({
+ currentSettings: { visibilityLevel: visibilityOptions.INTERNAL },
+ showVisibilityConfirmModal: true,
+ });
+ });
+
+ it('will render the confirmation dialog if the visibility is reduced', async () => {
+ expect(findConfirmDangerButton().exists()).toBe(false);
+
+ await findProjectVisibilityLevelInput().setValue(visibilityOptions.PRIVATE);
+
+ expect(findConfirmDangerButton().exists()).toBe(true);
+ });
+
+ it('emits the `confirm` event when the reduce visibility warning is confirmed', async () => {
+ expect(wrapper.emitted('confirm')).toBeUndefined();
+
+ await findProjectVisibilityLevelInput().setValue(visibilityOptions.PRIVATE);
+ await findConfirmDangerButton().vm.$emit('confirm');
+
+ expect(wrapper.emitted('confirm')).toHaveLength(1);
+ });
+ });
});
describe('Issues settings', () => {
diff --git a/spec/frontend/pages/shared/nav/sidebar_tracking_spec.js b/spec/frontend/pages/shared/nav/sidebar_tracking_spec.js
index 2c8eb8e459f..04f53e048ed 100644
--- a/spec/frontend/pages/shared/nav/sidebar_tracking_spec.js
+++ b/spec/frontend/pages/shared/nav/sidebar_tracking_spec.js
@@ -57,9 +57,9 @@ describe('~/pages/shared/nav/sidebar_tracking.js', () => {
menu.classList.add('is-over', 'is-showing-fly-out');
menuLink.click();
- expect(menu.dataset).toMatchObject({
- trackAction: 'click_menu',
- trackExtra: JSON.stringify({
+ expect(menu).toHaveTrackingAttributes({
+ action: 'click_menu',
+ extra: JSON.stringify({
sidebar_display: 'Expanded',
menu_display: 'Fly out',
}),
@@ -74,9 +74,9 @@ describe('~/pages/shared/nav/sidebar_tracking.js', () => {
submenuList.classList.add('fly-out-list');
menuLink.click();
- expect(menu.dataset).toMatchObject({
- trackAction: 'click_menu_item',
- trackExtra: JSON.stringify({
+ expect(menu).toHaveTrackingAttributes({
+ action: 'click_menu_item',
+ extra: JSON.stringify({
sidebar_display: 'Expanded',
menu_display: 'Fly out',
}),
@@ -92,9 +92,9 @@ describe('~/pages/shared/nav/sidebar_tracking.js', () => {
menu.classList.add('active');
menuLink.click();
- expect(menu.dataset).toMatchObject({
- trackAction: 'click_menu',
- trackExtra: JSON.stringify({
+ expect(menu).toHaveTrackingAttributes({
+ action: 'click_menu',
+ extra: JSON.stringify({
sidebar_display: 'Expanded',
menu_display: 'Expanded',
}),
@@ -108,9 +108,9 @@ describe('~/pages/shared/nav/sidebar_tracking.js', () => {
menu.classList.add('active');
menuLink.click();
- expect(menu.dataset).toMatchObject({
- trackAction: 'click_menu_item',
- trackExtra: JSON.stringify({
+ expect(menu).toHaveTrackingAttributes({
+ action: 'click_menu_item',
+ extra: JSON.stringify({
sidebar_display: 'Expanded',
menu_display: 'Expanded',
}),
@@ -131,9 +131,9 @@ describe('~/pages/shared/nav/sidebar_tracking.js', () => {
menu.classList.add('is-over', 'is-showing-fly-out');
menuLink.click();
- expect(menu.dataset).toMatchObject({
- trackAction: 'click_menu',
- trackExtra: JSON.stringify({
+ expect(menu).toHaveTrackingAttributes({
+ action: 'click_menu',
+ extra: JSON.stringify({
sidebar_display: 'Collapsed',
menu_display: 'Fly out',
}),
@@ -148,9 +148,9 @@ describe('~/pages/shared/nav/sidebar_tracking.js', () => {
submenuList.classList.add('fly-out-list');
menuLink.click();
- expect(menu.dataset).toMatchObject({
- trackAction: 'click_menu_item',
- trackExtra: JSON.stringify({
+ expect(menu).toHaveTrackingAttributes({
+ action: 'click_menu_item',
+ extra: JSON.stringify({
sidebar_display: 'Collapsed',
menu_display: 'Fly out',
}),
diff --git a/spec/frontend/pages/shared/wikis/components/wiki_form_spec.js b/spec/frontend/pages/shared/wikis/components/wiki_form_spec.js
index f4236146d33..fd581eebd1e 100644
--- a/spec/frontend/pages/shared/wikis/components/wiki_form_spec.js
+++ b/spec/frontend/pages/shared/wikis/components/wiki_form_spec.js
@@ -1,5 +1,5 @@
import { nextTick } from 'vue';
-import { GlLoadingIcon, GlModal } from '@gitlab/ui';
+import { GlLoadingIcon, GlModal, GlAlert, GlButton } from '@gitlab/ui';
import { mount, shallowMount } from '@vue/test-utils';
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
@@ -31,25 +31,28 @@ describe('WikiForm', () => {
const findContent = () => wrapper.find('#wiki_content');
const findMessage = () => wrapper.find('#wiki_message');
const findSubmitButton = () => wrapper.findByTestId('wiki-submit-button');
- const findCancelButton = () => wrapper.findByRole('link', { name: 'Cancel' });
- const findUseNewEditorButton = () => wrapper.findByRole('button', { name: 'Use the new editor' });
+ const findCancelButton = () => wrapper.findByTestId('wiki-cancel-button');
+ const findUseNewEditorButton = () => wrapper.findByText('Use the new editor');
const findToggleEditingModeButton = () => wrapper.findByTestId('toggle-editing-mode-button');
- const findDismissContentEditorAlertButton = () =>
- wrapper.findByRole('button', { name: 'Try this later' });
+ const findDismissContentEditorAlertButton = () => wrapper.findByText('Try this later');
const findSwitchToOldEditorButton = () =>
wrapper.findByRole('button', { name: 'Switch me back to the classic editor.' });
- const findTitleHelpLink = () => wrapper.findByRole('link', { name: 'Learn more.' });
+ const findTitleHelpLink = () => wrapper.findByText('Learn more.');
const findMarkdownHelpLink = () => wrapper.findByTestId('wiki-markdown-help-link');
const findContentEditor = () => wrapper.findComponent(ContentEditor);
const findClassicEditor = () => wrapper.findComponent(MarkdownField);
const setFormat = (value) => {
const format = findFormat();
- format.find(`option[value=${value}]`).setSelected();
- format.element.dispatchEvent(new Event('change'));
+
+ return format.find(`option[value=${value}]`).setSelected();
};
- const triggerFormSubmit = () => findForm().element.dispatchEvent(new Event('submit'));
+ const triggerFormSubmit = () => {
+ findForm().element.dispatchEvent(new Event('submit'));
+
+ return nextTick();
+ };
const dispatchBeforeUnload = () => {
const e = new Event('beforeunload');
@@ -84,34 +87,14 @@ describe('WikiForm', () => {
Org: 'org',
};
- function createWrapper(
- persisted = false,
- { pageInfo, glFeatures = { wikiSwitchBetweenContentEditorRawMarkdown: false } } = {},
- ) {
- wrapper = extendedWrapper(
- mount(
- WikiForm,
- {
- provide: {
- formatOptions,
- glFeatures,
- pageInfo: {
- ...(persisted ? pageInfoPersisted : pageInfoNew),
- ...pageInfo,
- },
- },
- },
- { attachToDocument: true },
- ),
- );
- }
-
- const createShallowWrapper = (
+ function createWrapper({
+ mountFn = shallowMount,
persisted = false,
- { pageInfo, glFeatures = { wikiSwitchBetweenContentEditorRawMarkdown: false } } = {},
- ) => {
+ pageInfo,
+ glFeatures = { wikiSwitchBetweenContentEditorRawMarkdown: false },
+ } = {}) {
wrapper = extendedWrapper(
- shallowMount(WikiForm, {
+ mountFn(WikiForm, {
provide: {
formatOptions,
glFeatures,
@@ -122,10 +105,12 @@ describe('WikiForm', () => {
},
stubs: {
MarkdownField,
+ GlAlert,
+ GlButton,
},
}),
);
- };
+ }
beforeEach(() => {
trackingSpy = mockTracking(undefined, null, jest.spyOn);
@@ -147,26 +132,24 @@ describe('WikiForm', () => {
`(
'updates the commit message to $message when title is $title and persisted=$persisted',
async ({ title, message, persisted }) => {
- createWrapper(persisted);
-
- findTitle().setValue(title);
+ createWrapper({ persisted });
- await wrapper.vm.$nextTick();
+ await findTitle().setValue(title);
expect(findMessage().element.value).toBe(message);
},
);
it('sets the commit message to "Update My page" when the page first loads when persisted', async () => {
- createWrapper(true);
+ createWrapper({ persisted: true });
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(findMessage().element.value).toBe('Update My page');
});
it('does not trim page content by default', () => {
- createWrapper(true);
+ createWrapper({ persisted: true });
expect(findContent().element.value).toBe(' My page content ');
});
@@ -178,20 +161,16 @@ describe('WikiForm', () => {
${'asciidoc'} | ${'link:page-slug[Link title]'}
${'org'} | ${'[[page-slug]]'}
`('updates the link help message when format=$value is selected', async ({ value, text }) => {
- createWrapper();
+ createWrapper({ mountFn: mount });
- setFormat(value);
-
- await wrapper.vm.$nextTick();
+ await setFormat(value);
expect(wrapper.text()).toContain(text);
});
- it('starts with no unload warning', async () => {
+ it('starts with no unload warning', () => {
createWrapper();
- await wrapper.vm.$nextTick();
-
const e = dispatchBeforeUnload();
expect(typeof e.returnValue).not.toBe('string');
expect(e.preventDefault).not.toHaveBeenCalled();
@@ -203,20 +182,16 @@ describe('WikiForm', () => {
${false} | ${'You can specify the full path for the new file. We will automatically create any missing directories.'} | ${'/help/user/project/wiki/index#create-a-new-wiki-page'}
`(
'shows appropriate title help text and help link for when persisted=$persisted',
- async ({ persisted, titleHelpLink, titleHelpText }) => {
- createWrapper(persisted);
-
- await wrapper.vm.$nextTick();
+ ({ persisted, titleHelpLink, titleHelpText }) => {
+ createWrapper({ persisted });
expect(wrapper.text()).toContain(titleHelpText);
expect(findTitleHelpLink().attributes().href).toBe(titleHelpLink);
},
);
- it('shows correct link for wiki specific markdown docs', async () => {
- createWrapper();
-
- await wrapper.vm.$nextTick();
+ it('shows correct link for wiki specific markdown docs', () => {
+ createWrapper({ mountFn: mount });
expect(findMarkdownHelpLink().attributes().href).toBe(
'/help/user/markdown#wiki-specific-markdown',
@@ -225,12 +200,11 @@ describe('WikiForm', () => {
describe('when wiki content is updated', () => {
beforeEach(async () => {
- createWrapper(true);
+ createWrapper({ mountFn: mount, persisted: true });
const input = findContent();
- input.setValue(' Lorem ipsum dolar sit! ');
- await input.trigger('input');
+ await input.setValue(' Lorem ipsum dolar sit! ');
});
it('sets before unload warning', () => {
@@ -241,17 +215,15 @@ describe('WikiForm', () => {
describe('form submit', () => {
beforeEach(async () => {
- triggerFormSubmit();
-
- await wrapper.vm.$nextTick();
+ await triggerFormSubmit();
});
- it('when form submitted, unsets before unload warning', async () => {
+ it('when form submitted, unsets before unload warning', () => {
const e = dispatchBeforeUnload();
expect(e.preventDefault).not.toHaveBeenCalled();
});
- it('triggers wiki format tracking event', async () => {
+ it('triggers wiki format tracking event', () => {
expect(trackingSpy).toHaveBeenCalledTimes(1);
});
@@ -264,22 +236,20 @@ describe('WikiForm', () => {
describe('submit button state', () => {
it.each`
title | content | buttonState | disabledAttr
- ${'something'} | ${'something'} | ${'enabled'} | ${undefined}
- ${''} | ${'something'} | ${'disabled'} | ${'disabled'}
- ${'something'} | ${''} | ${'disabled'} | ${'disabled'}
- ${''} | ${''} | ${'disabled'} | ${'disabled'}
- ${' '} | ${' '} | ${'disabled'} | ${'disabled'}
+ ${'something'} | ${'something'} | ${'enabled'} | ${false}
+ ${''} | ${'something'} | ${'disabled'} | ${true}
+ ${'something'} | ${''} | ${'disabled'} | ${true}
+ ${''} | ${''} | ${'disabled'} | ${true}
+ ${' '} | ${' '} | ${'disabled'} | ${true}
`(
"when title='$title', content='$content', then the button is $buttonState'",
async ({ title, content, disabledAttr }) => {
createWrapper();
- findTitle().setValue(title);
- findContent().setValue(content);
+ await findTitle().setValue(title);
+ await findContent().setValue(content);
- await wrapper.vm.$nextTick();
-
- expect(findSubmitButton().attributes().disabled).toBe(disabledAttr);
+ expect(findSubmitButton().props().disabled).toBe(disabledAttr);
},
);
@@ -288,7 +258,7 @@ describe('WikiForm', () => {
${true} | ${'Save changes'}
${false} | ${'Create page'}
`('when persisted=$persisted, label is set to $buttonLabel', ({ persisted, buttonLabel }) => {
- createWrapper(persisted);
+ createWrapper({ persisted });
expect(findSubmitButton().text()).toBe(buttonLabel);
});
@@ -302,7 +272,7 @@ describe('WikiForm', () => {
`(
'when persisted=$persisted, redirects the user to appropriate path',
({ persisted, redirectLink }) => {
- createWrapper(persisted);
+ createWrapper({ persisted });
expect(findCancelButton().attributes().href).toBe(redirectLink);
},
@@ -311,7 +281,7 @@ describe('WikiForm', () => {
describe('when wikiSwitchBetweenContentEditorRawMarkdown feature flag is not enabled', () => {
beforeEach(() => {
- createShallowWrapper(true, {
+ createWrapper({
glFeatures: { wikiSwitchBetweenContentEditorRawMarkdown: false },
});
});
@@ -323,7 +293,7 @@ describe('WikiForm', () => {
describe('when wikiSwitchBetweenContentEditorRawMarkdown feature flag is enabled', () => {
beforeEach(() => {
- createShallowWrapper(true, {
+ createWrapper({
glFeatures: { wikiSwitchBetweenContentEditorRawMarkdown: true },
});
});
@@ -404,10 +374,6 @@ describe('WikiForm', () => {
});
describe('wiki content editor', () => {
- beforeEach(() => {
- createWrapper(true);
- });
-
it.each`
format | buttonExists
${'markdown'} | ${true}
@@ -415,15 +381,17 @@ describe('WikiForm', () => {
`(
'gl-alert containing "use new editor" button exists: $buttonExists if format is $format',
async ({ format, buttonExists }) => {
- setFormat(format);
+ createWrapper();
- await wrapper.vm.$nextTick();
+ await setFormat(format);
expect(findUseNewEditorButton().exists()).toBe(buttonExists);
},
);
it('gl-alert containing "use new editor" button is dismissed on clicking dismiss button', async () => {
+ createWrapper();
+
await findDismissContentEditorAlertButton().trigger('click');
expect(findUseNewEditorButton().exists()).toBe(false);
@@ -442,22 +410,24 @@ describe('WikiForm', () => {
);
};
- it('shows classic editor by default', assertOldEditorIsVisible);
+ it('shows classic editor by default', () => {
+ createWrapper({ persisted: true });
+
+ assertOldEditorIsVisible();
+ });
describe('switch format to rdoc', () => {
beforeEach(async () => {
- setFormat('rdoc');
+ createWrapper({ persisted: true });
- await wrapper.vm.$nextTick();
+ await setFormat('rdoc');
});
it('continues to show the classic editor', assertOldEditorIsVisible);
describe('switch format back to markdown', () => {
beforeEach(async () => {
- setFormat('rdoc');
-
- await wrapper.vm.$nextTick();
+ await setFormat('markdown');
});
it(
@@ -469,6 +439,7 @@ describe('WikiForm', () => {
describe('clicking "use new editor": editor fails to load', () => {
beforeEach(async () => {
+ createWrapper({ mountFn: mount });
mock.onPost(/preview-markdown/).reply(400);
await findUseNewEditorButton().trigger('click');
@@ -494,10 +465,12 @@ describe('WikiForm', () => {
});
describe('clicking "use new editor": editor loads successfully', () => {
- beforeEach(() => {
+ beforeEach(async () => {
+ createWrapper({ persisted: true, mountFn: mount });
+
mock.onPost(/preview-markdown/).reply(200, { body: '<p>hello <strong>world</strong></p>' });
- findUseNewEditorButton().trigger('click');
+ await findUseNewEditorButton().trigger('click');
});
it('shows a tip to send feedback', () => {
@@ -542,46 +515,40 @@ describe('WikiForm', () => {
});
it('unsets before unload warning on form submit', async () => {
- triggerFormSubmit();
-
- await nextTick();
+ await triggerFormSubmit();
const e = dispatchBeforeUnload();
expect(e.preventDefault).not.toHaveBeenCalled();
});
- });
- it('triggers tracking events on form submit', async () => {
- triggerFormSubmit();
+ it('triggers tracking events on form submit', async () => {
+ await triggerFormSubmit();
- await wrapper.vm.$nextTick();
-
- expect(trackingSpy).toHaveBeenCalledWith(undefined, SAVED_USING_CONTENT_EDITOR_ACTION, {
- label: WIKI_CONTENT_EDITOR_TRACKING_LABEL,
- });
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, SAVED_USING_CONTENT_EDITOR_ACTION, {
+ label: WIKI_CONTENT_EDITOR_TRACKING_LABEL,
+ });
- expect(trackingSpy).toHaveBeenCalledWith(undefined, WIKI_FORMAT_UPDATED_ACTION, {
- label: WIKI_FORMAT_LABEL,
- extra: {
- value: findFormat().element.value,
- old_format: pageInfoPersisted.format,
- project_path: pageInfoPersisted.path,
- },
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, WIKI_FORMAT_UPDATED_ACTION, {
+ label: WIKI_FORMAT_LABEL,
+ extra: {
+ value: findFormat().element.value,
+ old_format: pageInfoPersisted.format,
+ project_path: pageInfoPersisted.path,
+ },
+ });
});
- });
-
- it('updates content from content editor on form submit', async () => {
- // old value
- expect(findContent().element.value).toBe(' My page content ');
- // wait for content editor to load
- await waitForPromises();
+ it('updates content from content editor on form submit', async () => {
+ // old value
+ expect(findContent().element.value).toBe(' My page content ');
- triggerFormSubmit();
+ // wait for content editor to load
+ await waitForPromises();
- await wrapper.vm.$nextTick();
+ await triggerFormSubmit();
- expect(findContent().element.value).toBe('hello **world**');
+ expect(findContent().element.value).toBe('hello **world**');
+ });
});
describe('clicking "switch to classic editor"', () => {
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 cab4810cbf1..f15d5f334d6 100644
--- a/spec/frontend/pipeline_editor/components/editor/text_editor_spec.js
+++ b/spec/frontend/pipeline_editor/components/editor/text_editor_spec.js
@@ -17,19 +17,12 @@ describe('Pipeline Editor | Text editor component', () => {
let editorReadyListener;
let mockUse;
let mockRegisterCiSchema;
+ let mockEditorInstance;
+ let editorInstanceDetail;
const MockSourceEditor = {
template: '<div/>',
props: ['value', 'fileName'],
- mounted() {
- this.$emit(EDITOR_READY_EVENT);
- },
- methods: {
- getEditor: () => ({
- use: mockUse,
- registerCiSchema: mockRegisterCiSchema,
- }),
- },
};
const createComponent = (glFeatures = {}, mountFn = shallowMount) => {
@@ -58,6 +51,21 @@ describe('Pipeline Editor | Text editor component', () => {
const findEditor = () => wrapper.findComponent(MockSourceEditor);
+ beforeEach(() => {
+ editorReadyListener = jest.fn();
+ mockUse = jest.fn();
+ mockRegisterCiSchema = jest.fn();
+ mockEditorInstance = {
+ use: mockUse,
+ registerCiSchema: mockRegisterCiSchema,
+ };
+ editorInstanceDetail = {
+ detail: {
+ instance: mockEditorInstance,
+ },
+ };
+ });
+
afterEach(() => {
wrapper.destroy();
@@ -67,10 +75,6 @@ describe('Pipeline Editor | Text editor component', () => {
describe('template', () => {
beforeEach(() => {
- editorReadyListener = jest.fn();
- mockUse = jest.fn();
- mockRegisterCiSchema = jest.fn();
-
createComponent();
});
@@ -87,7 +91,7 @@ describe('Pipeline Editor | Text editor component', () => {
});
it('bubbles up events', () => {
- findEditor().vm.$emit(EDITOR_READY_EVENT);
+ findEditor().vm.$emit(EDITOR_READY_EVENT, editorInstanceDetail);
expect(editorReadyListener).toHaveBeenCalled();
});
@@ -97,11 +101,7 @@ describe('Pipeline Editor | Text editor component', () => {
describe('when `schema_linting` feature flag is on', () => {
beforeEach(() => {
createComponent({ schemaLinting: true });
- // Since the editor will have already mounted, the event will have fired.
- // To ensure we properly test this, we clear the mock and re-remit the event.
- mockRegisterCiSchema.mockClear();
- mockUse.mockClear();
- findEditor().vm.$emit(EDITOR_READY_EVENT);
+ findEditor().vm.$emit(EDITOR_READY_EVENT, editorInstanceDetail);
});
it('configures editor with syntax highlight', () => {
@@ -113,7 +113,7 @@ describe('Pipeline Editor | Text editor component', () => {
describe('when `schema_linting` feature flag is off', () => {
beforeEach(() => {
createComponent();
- findEditor().vm.$emit(EDITOR_READY_EVENT);
+ findEditor().vm.$emit(EDITOR_READY_EVENT, editorInstanceDetail);
});
it('does not call the register CI schema function', () => {
diff --git a/spec/frontend/pipeline_editor/components/header/validation_segment_spec.js b/spec/frontend/pipeline_editor/components/header/validation_segment_spec.js
index fd8a100bb2c..570323826d1 100644
--- a/spec/frontend/pipeline_editor/components/header/validation_segment_spec.js
+++ b/spec/frontend/pipeline_editor/components/header/validation_segment_spec.js
@@ -1,40 +1,61 @@
+import VueApollo from 'vue-apollo';
import { GlIcon } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
+import { shallowMount, createLocalVue } from '@vue/test-utils';
import { escape } from 'lodash';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import createMockApollo from 'helpers/mock_apollo_helper';
import { sprintf } from '~/locale';
import ValidationSegment, {
i18n,
} from '~/pipeline_editor/components/header/validation_segment.vue';
+import getAppStatus from '~/pipeline_editor/graphql/queries/client/app_status.query.graphql';
import {
CI_CONFIG_STATUS_INVALID,
EDITOR_APP_STATUS_EMPTY,
EDITOR_APP_STATUS_INVALID,
EDITOR_APP_STATUS_LOADING,
+ EDITOR_APP_STATUS_LINT_UNAVAILABLE,
EDITOR_APP_STATUS_VALID,
} from '~/pipeline_editor/constants';
-import { mockYmlHelpPagePath, mergeUnwrappedCiConfig, mockCiYml } from '../../mock_data';
+import {
+ mergeUnwrappedCiConfig,
+ mockCiYml,
+ mockLintUnavailableHelpPagePath,
+ mockYmlHelpPagePath,
+} from '../../mock_data';
+
+const localVue = createLocalVue();
+localVue.use(VueApollo);
describe('Validation segment component', () => {
let wrapper;
- const createComponent = ({ props = {}, appStatus }) => {
+ const mockApollo = createMockApollo();
+
+ const createComponent = ({ props = {}, appStatus = EDITOR_APP_STATUS_INVALID }) => {
+ mockApollo.clients.defaultClient.cache.writeQuery({
+ query: getAppStatus,
+ data: {
+ app: {
+ __typename: 'PipelineEditorApp',
+ status: appStatus,
+ },
+ },
+ });
+
wrapper = extendedWrapper(
shallowMount(ValidationSegment, {
+ localVue,
+ apolloProvider: mockApollo,
provide: {
ymlHelpPagePath: mockYmlHelpPagePath,
+ lintUnavailableHelpPagePath: mockLintUnavailableHelpPagePath,
},
propsData: {
ciConfig: mergeUnwrappedCiConfig(),
ciFileContent: mockCiYml,
...props,
},
- // Simulate graphQL client query result
- data() {
- return {
- appStatus,
- };
- },
}),
);
};
@@ -92,6 +113,7 @@ describe('Validation segment component', () => {
appStatus: EDITOR_APP_STATUS_INVALID,
});
});
+
it('has warning icon', () => {
expect(findIcon().props('name')).toBe('warning-solid');
});
@@ -149,4 +171,28 @@ describe('Validation segment component', () => {
});
});
});
+
+ describe('when the lint service is unavailable', () => {
+ beforeEach(() => {
+ createComponent({
+ appStatus: EDITOR_APP_STATUS_LINT_UNAVAILABLE,
+ props: {
+ ciConfig: {},
+ },
+ });
+ });
+
+ it('show a message that the service is unavailable', () => {
+ expect(findValidationMsg().text()).toBe(i18n.unavailableValidation);
+ });
+
+ it('shows the time-out icon', () => {
+ expect(findIcon().props('name')).toBe('time-out');
+ });
+
+ it('shows the learn more link', () => {
+ expect(findLearnMoreLink().attributes('href')).toBe(mockLintUnavailableHelpPagePath);
+ expect(findLearnMoreLink().text()).toBe(i18n.learnMore);
+ });
+ });
});
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 3becf82ed6e..6206a0f6aed 100644
--- a/spec/frontend/pipeline_editor/components/ui/editor_tab_spec.js
+++ b/spec/frontend/pipeline_editor/components/ui/editor_tab_spec.js
@@ -75,34 +75,83 @@ describe('~/pipeline_editor/components/ui/editor_tab.vue', () => {
expect(mockChildMounted).toHaveBeenCalledWith(mockContent1);
});
- describe('showing the tab content depending on `isEmpty` and `isInvalid`', () => {
+ describe('alerts', () => {
+ describe('unavailable state', () => {
+ beforeEach(() => {
+ createWrapper({ props: { isUnavailable: true } });
+ });
+
+ it('shows the invalid alert when the status is invalid', () => {
+ const alert = findAlert();
+
+ expect(alert.exists()).toBe(true);
+ expect(alert.text()).toContain(wrapper.vm.$options.i18n.unavailable);
+ });
+ });
+
+ describe('invalid state', () => {
+ beforeEach(() => {
+ createWrapper({ props: { isInvalid: true } });
+ });
+
+ it('shows the invalid alert when the status is invalid', () => {
+ const alert = findAlert();
+
+ expect(alert.exists()).toBe(true);
+ expect(alert.text()).toBe(wrapper.vm.$options.i18n.invalid);
+ });
+ });
+
+ describe('empty state', () => {
+ const text = 'my custom alert message';
+
+ beforeEach(() => {
+ createWrapper({
+ props: { isEmpty: true, emptyMessage: text },
+ });
+ });
+
+ it('displays an empty message', () => {
+ createWrapper({
+ props: { isEmpty: true },
+ });
+
+ const alert = findAlert();
+
+ expect(alert.exists()).toBe(true);
+ expect(alert.text()).toBe(
+ 'This tab will be usable when the CI/CD configuration file is populated with valid syntax.',
+ );
+ });
+
+ it('can have a custom empty message', () => {
+ const alert = findAlert();
+
+ expect(alert.exists()).toBe(true);
+ expect(alert.text()).toBe(text);
+ });
+ });
+ });
+
+ describe('showing the tab content depending on `isEmpty`, `isUnavailable` and `isInvalid`', () => {
it.each`
- isEmpty | isInvalid | showSlotComponent | text
- ${undefined} | ${undefined} | ${true} | ${'renders'}
- ${false} | ${false} | ${true} | ${'renders'}
- ${undefined} | ${true} | ${false} | ${'hides'}
- ${true} | ${false} | ${false} | ${'hides'}
- ${false} | ${true} | ${false} | ${'hides'}
+ isEmpty | isUnavailable | isInvalid | showSlotComponent | text
+ ${undefined} | ${undefined} | ${undefined} | ${true} | ${'renders'}
+ ${false} | ${false} | ${false} | ${true} | ${'renders'}
+ ${undefined} | ${true} | ${true} | ${false} | ${'hides'}
+ ${true} | ${false} | ${false} | ${false} | ${'hides'}
+ ${false} | ${true} | ${false} | ${false} | ${'hides'}
+ ${false} | ${false} | ${true} | ${false} | ${'hides'}
`(
- '$text the slot component when isEmpty:$isEmpty and isInvalid:$isInvalid',
- ({ isEmpty, isInvalid, showSlotComponent }) => {
+ '$text the slot component when isEmpty:$isEmpty, isUnavailable:$isUnavailable and isInvalid:$isInvalid',
+ ({ isEmpty, isUnavailable, isInvalid, showSlotComponent }) => {
createWrapper({
- props: { isEmpty, isInvalid },
+ props: { isEmpty, isUnavailable, isInvalid },
});
expect(findSlotComponent().exists()).toBe(showSlotComponent);
expect(findAlert().exists()).toBe(!showSlotComponent);
},
);
-
- it('can have a custom empty message', () => {
- const text = 'my custom alert message';
- createWrapper({ props: { isEmpty: true, emptyMessage: text } });
-
- const alert = findAlert();
-
- expect(alert.exists()).toBe(true);
- expect(alert.text()).toBe(text);
- });
});
describe('user interaction', () => {
diff --git a/spec/frontend/pipeline_editor/mock_data.js b/spec/frontend/pipeline_editor/mock_data.js
index fc2cbdeda0a..f02f6870653 100644
--- a/spec/frontend/pipeline_editor/mock_data.js
+++ b/spec/frontend/pipeline_editor/mock_data.js
@@ -10,6 +10,7 @@ export const mockNewMergeRequestPath = '/-/merge_requests/new';
export const mockCommitSha = 'aabbccdd';
export const mockCommitNextSha = 'eeffgghh';
export const mockLintHelpPagePath = '/-/lint-help';
+export const mockLintUnavailableHelpPagePath = '/-/pipeline-editor/troubleshoot';
export const mockYmlHelpPagePath = '/-/yml-help';
export const mockCommitMessage = 'My commit message';
diff --git a/spec/frontend/pipeline_editor/pipeline_editor_app_spec.js b/spec/frontend/pipeline_editor/pipeline_editor_app_spec.js
index 09d7d4f7ca6..63eca253c48 100644
--- a/spec/frontend/pipeline_editor/pipeline_editor_app_spec.js
+++ b/spec/frontend/pipeline_editor/pipeline_editor_app_spec.js
@@ -5,10 +5,15 @@ import createMockApollo from 'helpers/mock_apollo_helper';
import setWindowLocation from 'helpers/set_window_location_helper';
import waitForPromises from 'helpers/wait_for_promises';
+import { resolvers } from '~/pipeline_editor/graphql/resolvers';
import PipelineEditorTabs from '~/pipeline_editor/components/pipeline_editor_tabs.vue';
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, LOAD_FAILURE_UNKNOWN } from '~/pipeline_editor/constants';
+import PipelineEditorHeader from '~/pipeline_editor/components/header/pipeline_editor_header.vue';
+import ValidationSegment, {
+ i18n as validationSegmenti18n,
+} from '~/pipeline_editor/components/header/validation_segment.vue';
+import { COMMIT_SUCCESS, COMMIT_FAILURE } from '~/pipeline_editor/constants';
import getBlobContent from '~/pipeline_editor/graphql/queries/blob_content.query.graphql';
import getCiConfigData from '~/pipeline_editor/graphql/queries/ci_config.query.graphql';
import getTemplate from '~/pipeline_editor/graphql/queries/get_starter_template.query.graphql';
@@ -61,11 +66,6 @@ describe('Pipeline editor app component', () => {
wrapper = shallowMount(PipelineEditorApp, {
provide: { ...mockProvide, ...provide },
stubs,
- data() {
- return {
- commitSha: '',
- };
- },
mocks: {
$apollo: {
queries: {
@@ -90,17 +90,11 @@ describe('Pipeline editor app component', () => {
[getLatestCommitShaQuery, mockLatestCommitShaQuery],
[getPipelineQuery, mockPipelineQuery],
];
- mockApollo = createMockApollo(handlers);
+
+ mockApollo = createMockApollo(handlers, resolvers);
const options = {
localVue,
- data() {
- return {
- currentBranch: mockDefaultBranch,
- lastCommitBranch: '',
- appStatus: '',
- };
- },
mocks: {},
apolloProvider: mockApollo,
};
@@ -116,6 +110,7 @@ describe('Pipeline editor app component', () => {
const findEmptyState = () => wrapper.findComponent(PipelineEditorEmptyState);
const findEmptyStateButton = () =>
wrapper.findComponent(PipelineEditorEmptyState).findComponent(GlButton);
+ const findValidationSegment = () => wrapper.findComponent(ValidationSegment);
beforeEach(() => {
mockBlobContentData = jest.fn();
@@ -240,6 +235,26 @@ describe('Pipeline editor app component', () => {
});
});
+ describe('when the lint query returns a 500 error', () => {
+ beforeEach(async () => {
+ mockCiConfigData.mockRejectedValueOnce(new Error(500));
+ await createComponentWithApollo({
+ stubs: { PipelineEditorHome, PipelineEditorHeader, ValidationSegment },
+ });
+ });
+
+ it('shows that the lint service is down', () => {
+ expect(findValidationSegment().text()).toContain(
+ validationSegmenti18n.unavailableValidation,
+ );
+ });
+
+ it('does not report an error or scroll to the top', () => {
+ expect(findAlert().exists()).toBe(false);
+ expect(window.scrollTo).not.toHaveBeenCalled();
+ });
+ });
+
describe('when the user commits', () => {
const updateFailureMessage = 'The GitLab CI configuration could not be updated.';
const updateSuccessMessage = 'Your changes have been successfully committed.';
@@ -411,94 +426,6 @@ describe('Pipeline editor app component', () => {
});
});
- describe('when multiple errors occurs in a row', () => {
- const updateFailureMessage = 'The GitLab CI configuration could not be updated.';
- const unknownFailureMessage = 'The CI configuration was not loaded, please try again.';
- const unknownReasons = ['Commit failed'];
- const alertErrorMessage = `${updateFailureMessage} ${unknownReasons[0]}`;
-
- const emitError = (type = COMMIT_FAILURE, reasons = unknownReasons) =>
- findEditorHome().vm.$emit('showError', {
- type,
- reasons,
- });
-
- beforeEach(async () => {
- mockBlobContentData.mockResolvedValue(mockBlobContentQueryResponse);
- mockCiConfigData.mockResolvedValue(mockCiConfigQueryResponse);
- mockLatestCommitShaQuery.mockResolvedValue(mockCommitShaResults);
-
- window.scrollTo = jest.fn();
-
- await createComponentWithApollo({ stubs: { PipelineEditorMessages } });
- await emitError();
- });
-
- it('shows an error message for the first error', () => {
- expect(findAlert().text()).toMatchInterpolatedText(alertErrorMessage);
- });
-
- it('scrolls to the top of the page to bring attention to the error message', () => {
- expect(window.scrollTo).toHaveBeenCalledWith({ top: 0, behavior: 'smooth' });
- expect(window.scrollTo).toHaveBeenCalledTimes(1);
- });
-
- it('does not scroll to the top of the page if the same error occur multiple times in a row', async () => {
- await emitError();
-
- expect(window.scrollTo).toHaveBeenCalledTimes(1);
- expect(findAlert().text()).toMatchInterpolatedText(alertErrorMessage);
- });
-
- it('scrolls to the top if the error is different', async () => {
- await emitError(LOAD_FAILURE_UNKNOWN, []);
-
- expect(findAlert().text()).toMatchInterpolatedText(unknownFailureMessage);
- expect(window.scrollTo).toHaveBeenCalledTimes(2);
- });
-
- describe('when a user dismiss the alert', () => {
- beforeEach(async () => {
- await findAlert().vm.$emit('dismiss');
- });
-
- it('shows an error if the type is the same, but the reason is different', async () => {
- const newReason = 'Something broke';
-
- await emitError(COMMIT_FAILURE, [newReason]);
-
- expect(window.scrollTo).toHaveBeenCalledTimes(2);
- expect(findAlert().text()).toMatchInterpolatedText(`${updateFailureMessage} ${newReason}`);
- });
-
- it('does not show an error or scroll if a new error with the same type occurs', async () => {
- await emitError();
-
- expect(window.scrollTo).toHaveBeenCalledTimes(1);
- expect(findAlert().exists()).toBe(false);
- });
-
- it('it shows an error and scroll when a new type is emitted', async () => {
- await emitError(LOAD_FAILURE_UNKNOWN, []);
-
- expect(window.scrollTo).toHaveBeenCalledTimes(2);
- expect(findAlert().text()).toMatchInterpolatedText(unknownFailureMessage);
- });
-
- it('it shows an error and scroll if a previously shown type happen again', async () => {
- await emitError(LOAD_FAILURE_UNKNOWN, []);
-
- expect(window.scrollTo).toHaveBeenCalledTimes(2);
- expect(findAlert().text()).toMatchInterpolatedText(unknownFailureMessage);
-
- await emitError();
-
- expect(window.scrollTo).toHaveBeenCalledTimes(3);
- expect(findAlert().text()).toMatchInterpolatedText(alertErrorMessage);
- });
- });
- });
-
describe('when add_new_config_file query param is present', () => {
const originalLocation = window.location.href;
diff --git a/spec/frontend/pipelines/__snapshots__/utils_spec.js.snap b/spec/frontend/pipelines/__snapshots__/utils_spec.js.snap
index 99de0d2a3ef..52461885342 100644
--- a/spec/frontend/pipelines/__snapshots__/utils_spec.js.snap
+++ b/spec/frontend/pipelines/__snapshots__/utils_spec.js.snap
@@ -13,6 +13,7 @@ Array [
"id": "6",
"name": "build_a_nlfjkdnlvskfnksvjknlfdjvlvnjdkjdf_nvjkenjkrlngjeknjkl",
"needs": Array [],
+ "previousStageJobsOrNeeds": Array [],
"scheduledAt": null,
"status": Object {
"__typename": "DetailedStatus",
@@ -53,6 +54,7 @@ Array [
"id": "11",
"name": "build_b",
"needs": Array [],
+ "previousStageJobsOrNeeds": Array [],
"scheduledAt": null,
"status": Object {
"__typename": "DetailedStatus",
@@ -93,6 +95,7 @@ Array [
"id": "16",
"name": "build_c",
"needs": Array [],
+ "previousStageJobsOrNeeds": Array [],
"scheduledAt": null,
"status": Object {
"__typename": "DetailedStatus",
@@ -133,6 +136,7 @@ Array [
"id": "21",
"name": "build_d 1/3",
"needs": Array [],
+ "previousStageJobsOrNeeds": Array [],
"scheduledAt": null,
"status": Object {
"__typename": "DetailedStatus",
@@ -157,6 +161,7 @@ Array [
"id": "24",
"name": "build_d 2/3",
"needs": Array [],
+ "previousStageJobsOrNeeds": Array [],
"scheduledAt": null,
"status": Object {
"__typename": "DetailedStatus",
@@ -181,6 +186,7 @@ Array [
"id": "27",
"name": "build_d 3/3",
"needs": Array [],
+ "previousStageJobsOrNeeds": Array [],
"scheduledAt": null,
"status": Object {
"__typename": "DetailedStatus",
@@ -221,6 +227,7 @@ Array [
"id": "59",
"name": "test_c",
"needs": Array [],
+ "previousStageJobsOrNeeds": Array [],
"scheduledAt": null,
"status": Object {
"__typename": "DetailedStatus",
@@ -267,6 +274,11 @@ Array [
"build_b",
"build_a_nlfjkdnlvskfnksvjknlfdjvlvnjdkjdf_nvjkenjkrlngjeknjkl",
],
+ "previousStageJobsOrNeeds": Array [
+ "build_c",
+ "build_b",
+ "build_a_nlfjkdnlvskfnksvjknlfdjvlvnjdkjdf_nvjkenjkrlngjeknjkl",
+ ],
"scheduledAt": null,
"status": Object {
"__typename": "DetailedStatus",
@@ -313,6 +325,13 @@ Array [
"build_b",
"build_a_nlfjkdnlvskfnksvjknlfdjvlvnjdkjdf_nvjkenjkrlngjeknjkl",
],
+ "previousStageJobsOrNeeds": Array [
+ "build_d 3/3",
+ "build_d 2/3",
+ "build_d 1/3",
+ "build_b",
+ "build_a_nlfjkdnlvskfnksvjknlfdjvlvnjdkjdf_nvjkenjkrlngjeknjkl",
+ ],
"scheduledAt": null,
"status": Object {
"__typename": "DetailedStatus",
@@ -343,6 +362,13 @@ Array [
"build_b",
"build_a_nlfjkdnlvskfnksvjknlfdjvlvnjdkjdf_nvjkenjkrlngjeknjkl",
],
+ "previousStageJobsOrNeeds": Array [
+ "build_d 3/3",
+ "build_d 2/3",
+ "build_d 1/3",
+ "build_b",
+ "build_a_nlfjkdnlvskfnksvjknlfdjvlvnjdkjdf_nvjkenjkrlngjeknjkl",
+ ],
"scheduledAt": null,
"status": Object {
"__typename": "DetailedStatus",
@@ -385,6 +411,9 @@ Array [
"needs": Array [
"build_b",
],
+ "previousStageJobsOrNeeds": Array [
+ "build_b",
+ ],
"scheduledAt": null,
"status": Object {
"__typename": "DetailedStatus",
diff --git a/spec/frontend/pipelines/graph/mock_data.js b/spec/frontend/pipelines/graph/mock_data.js
index dcbbde7bf36..41823bfdb9f 100644
--- a/spec/frontend/pipelines/graph/mock_data.js
+++ b/spec/frontend/pipelines/graph/mock_data.js
@@ -73,6 +73,10 @@ export const mockPipelineResponse = {
__typename: 'CiBuildNeedConnection',
nodes: [],
},
+ previousStageJobsOrNeeds: {
+ __typename: 'CiJobConnection',
+ nodes: [],
+ },
},
],
},
@@ -118,6 +122,10 @@ export const mockPipelineResponse = {
__typename: 'CiBuildNeedConnection',
nodes: [],
},
+ previousStageJobsOrNeeds: {
+ __typename: 'CiJobConnection',
+ nodes: [],
+ },
},
],
},
@@ -163,6 +171,10 @@ export const mockPipelineResponse = {
__typename: 'CiBuildNeedConnection',
nodes: [],
},
+ previousStageJobsOrNeeds: {
+ __typename: 'CiJobConnection',
+ nodes: [],
+ },
},
],
},
@@ -208,6 +220,10 @@ export const mockPipelineResponse = {
__typename: 'CiBuildNeedConnection',
nodes: [],
},
+ previousStageJobsOrNeeds: {
+ __typename: 'CiJobConnection',
+ nodes: [],
+ },
},
{
__typename: 'CiJob',
@@ -235,6 +251,10 @@ export const mockPipelineResponse = {
__typename: 'CiBuildNeedConnection',
nodes: [],
},
+ previousStageJobsOrNeeds: {
+ __typename: 'CiJobConnection',
+ nodes: [],
+ },
},
{
__typename: 'CiJob',
@@ -262,6 +282,10 @@ export const mockPipelineResponse = {
__typename: 'CiBuildNeedConnection',
nodes: [],
},
+ previousStageJobsOrNeeds: {
+ __typename: 'CiJobConnection',
+ nodes: [],
+ },
},
],
},
@@ -339,6 +363,27 @@ export const mockPipelineResponse = {
},
],
},
+ previousStageJobsOrNeeds: {
+ __typename: 'CiJobConnection',
+ nodes: [
+ {
+ __typename: 'CiBuildNeed',
+ id: '37',
+ name: 'build_c',
+ },
+ {
+ __typename: 'CiBuildNeed',
+ id: '38',
+ name: 'build_b',
+ },
+ {
+ __typename: 'CiBuildNeed',
+ id: '39',
+ name:
+ 'build_a_nlfjkdnlvskfnksvjknlfdjvlvnjdkjdf_nvjkenjkrlngjeknjkl',
+ },
+ ],
+ },
},
],
},
@@ -411,6 +456,37 @@ export const mockPipelineResponse = {
},
],
},
+ previousStageJobsOrNeeds: {
+ __typename: 'CiJobConnection',
+ nodes: [
+ {
+ __typename: 'CiBuildNeed',
+ id: '45',
+ name: 'build_d 3/3',
+ },
+ {
+ __typename: 'CiBuildNeed',
+ id: '46',
+ name: 'build_d 2/3',
+ },
+ {
+ __typename: 'CiBuildNeed',
+ id: '47',
+ name: 'build_d 1/3',
+ },
+ {
+ __typename: 'CiBuildNeed',
+ id: '48',
+ name: 'build_b',
+ },
+ {
+ __typename: 'CiBuildNeed',
+ id: '49',
+ name:
+ 'build_a_nlfjkdnlvskfnksvjknlfdjvlvnjdkjdf_nvjkenjkrlngjeknjkl',
+ },
+ ],
+ },
},
{
__typename: 'CiJob',
@@ -465,6 +541,37 @@ export const mockPipelineResponse = {
},
],
},
+ previousStageJobsOrNeeds: {
+ __typename: 'CiJobConnection',
+ nodes: [
+ {
+ __typename: 'CiBuildNeed',
+ id: '52',
+ name: 'build_d 3/3',
+ },
+ {
+ __typename: 'CiBuildNeed',
+ id: '53',
+ name: 'build_d 2/3',
+ },
+ {
+ __typename: 'CiBuildNeed',
+ id: '54',
+ name: 'build_d 1/3',
+ },
+ {
+ __typename: 'CiBuildNeed',
+ id: '55',
+ name: 'build_b',
+ },
+ {
+ __typename: 'CiBuildNeed',
+ id: '56',
+ name:
+ 'build_a_nlfjkdnlvskfnksvjknlfdjvlvnjdkjdf_nvjkenjkrlngjeknjkl',
+ },
+ ],
+ },
},
],
},
@@ -503,6 +610,10 @@ export const mockPipelineResponse = {
__typename: 'CiBuildNeedConnection',
nodes: [],
},
+ previousStageJobsOrNeeds: {
+ __typename: 'CiJobConnection',
+ nodes: [],
+ },
},
],
},
@@ -547,6 +658,16 @@ export const mockPipelineResponse = {
},
],
},
+ previousStageJobsOrNeeds: {
+ __typename: 'CiJobConnection',
+ nodes: [
+ {
+ __typename: 'CiBuildNeed',
+ id: '65',
+ name: 'build_b',
+ },
+ ],
+ },
},
],
},
@@ -720,6 +841,10 @@ export const wrappedPipelineReturn = {
__typename: 'CiBuildNeedConnection',
nodes: [],
},
+ previousStageJobsOrNeeds: {
+ __typename: 'CiJobConnection',
+ nodes: [],
+ },
status: {
__typename: 'DetailedStatus',
id: '84',
diff --git a/spec/frontend/profile/account/components/update_username_spec.js b/spec/frontend/profile/account/components/update_username_spec.js
index 42adefcd0bb..bda07af4feb 100644
--- a/spec/frontend/profile/account/components/update_username_spec.js
+++ b/spec/frontend/profile/account/components/update_username_spec.js
@@ -79,6 +79,8 @@ describe('UpdateUsername component', () => {
beforeEach(async () => {
createComponent();
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({ newUsername });
await wrapper.vm.$nextTick();
diff --git a/spec/frontend/profile/add_ssh_key_validation_spec.js b/spec/frontend/profile/add_ssh_key_validation_spec.js
index 1fec864599c..a6bcca0ccb3 100644
--- a/spec/frontend/profile/add_ssh_key_validation_spec.js
+++ b/spec/frontend/profile/add_ssh_key_validation_spec.js
@@ -3,18 +3,18 @@ import AddSshKeyValidation from '../../../app/assets/javascripts/profile/add_ssh
describe('AddSshKeyValidation', () => {
describe('submit', () => {
it('returns true if isValid is true', () => {
- const addSshKeyValidation = new AddSshKeyValidation({});
- jest.spyOn(AddSshKeyValidation, 'isPublicKey').mockReturnValue(true);
+ const addSshKeyValidation = new AddSshKeyValidation([], {});
+ jest.spyOn(addSshKeyValidation, 'isPublicKey').mockReturnValue(true);
- expect(addSshKeyValidation.submit()).toBeTruthy();
+ expect(addSshKeyValidation.submit()).toBe(true);
});
it('calls preventDefault and toggleWarning if isValid is false', () => {
- const addSshKeyValidation = new AddSshKeyValidation({});
+ const addSshKeyValidation = new AddSshKeyValidation([], {});
const event = {
preventDefault: jest.fn(),
};
- jest.spyOn(AddSshKeyValidation, 'isPublicKey').mockReturnValue(false);
+ jest.spyOn(addSshKeyValidation, 'isPublicKey').mockReturnValue(false);
jest.spyOn(addSshKeyValidation, 'toggleWarning').mockImplementation(() => {});
addSshKeyValidation.submit(event);
@@ -31,14 +31,15 @@ describe('AddSshKeyValidation', () => {
warningElement.classList.add('hide');
const addSshKeyValidation = new AddSshKeyValidation(
+ [],
{},
warningElement,
originalSubmitElement,
);
addSshKeyValidation.toggleWarning(true);
- expect(warningElement.classList.contains('hide')).toBeFalsy();
- expect(originalSubmitElement.classList.contains('hide')).toBeTruthy();
+ expect(warningElement.classList.contains('hide')).toBe(false);
+ expect(originalSubmitElement.classList.contains('hide')).toBe(true);
});
it('hides warningElement and shows originalSubmitElement if isVisible is false', () => {
@@ -47,25 +48,32 @@ describe('AddSshKeyValidation', () => {
originalSubmitElement.classList.add('hide');
const addSshKeyValidation = new AddSshKeyValidation(
+ [],
{},
warningElement,
originalSubmitElement,
);
addSshKeyValidation.toggleWarning(false);
- expect(warningElement.classList.contains('hide')).toBeTruthy();
- expect(originalSubmitElement.classList.contains('hide')).toBeFalsy();
+ expect(warningElement.classList.contains('hide')).toBe(true);
+ expect(originalSubmitElement.classList.contains('hide')).toBe(false);
});
});
describe('isPublicKey', () => {
- it('returns false if probably invalid public ssh key', () => {
- expect(AddSshKeyValidation.isPublicKey('nope')).toBeFalsy();
+ it('returns false if value begins with an algorithm name that is unsupported', () => {
+ const addSshKeyValidation = new AddSshKeyValidation(['ssh-rsa', 'ssh-algorithm'], {});
+
+ expect(addSshKeyValidation.isPublicKey('nope key')).toBe(false);
+ expect(addSshKeyValidation.isPublicKey('ssh- key')).toBe(false);
+ expect(addSshKeyValidation.isPublicKey('unsupported-ssh-rsa key')).toBe(false);
});
- it('returns true if probably valid public ssh key', () => {
- expect(AddSshKeyValidation.isPublicKey('ssh-')).toBeTruthy();
- expect(AddSshKeyValidation.isPublicKey('ecdsa-sha2-')).toBeTruthy();
+ it('returns true if value begins with an algorithm name that is supported', () => {
+ const addSshKeyValidation = new AddSshKeyValidation(['ssh-rsa', 'ssh-algorithm'], {});
+
+ expect(addSshKeyValidation.isPublicKey('ssh-rsa key')).toBe(true);
+ expect(addSshKeyValidation.isPublicKey('ssh-algorithm key')).toBe(true);
});
});
});
diff --git a/spec/frontend/project_select_combo_button_spec.js b/spec/frontend/project_select_combo_button_spec.js
index 5cdc3d174a1..40e7d27edc8 100644
--- a/spec/frontend/project_select_combo_button_spec.js
+++ b/spec/frontend/project_select_combo_button_spec.js
@@ -28,7 +28,7 @@ describe('Project Select Combo Button', () => {
loadFixtures(fixturePath);
- testContext.newItemBtn = document.querySelector('.new-project-item-link');
+ testContext.newItemBtn = document.querySelector('.js-new-project-item-link');
testContext.projectSelectInput = document.querySelector('.project-item-select');
});
@@ -120,7 +120,6 @@ describe('Project Select Combo Button', () => {
const returnedVariants = testContext.method();
expect(returnedVariants.localStorageItemType).toBe('new-merge-request');
- expect(returnedVariants.defaultTextPrefix).toBe('New merge request');
expect(returnedVariants.presetTextSuffix).toBe('merge request');
});
@@ -131,7 +130,6 @@ describe('Project Select Combo Button', () => {
const returnedVariants = testContext.method();
expect(returnedVariants.localStorageItemType).toBe('new-issue');
- expect(returnedVariants.defaultTextPrefix).toBe('New issue');
expect(returnedVariants.presetTextSuffix).toBe('issue');
});
});
diff --git a/spec/frontend/projects/commits/components/author_select_spec.js b/spec/frontend/projects/commits/components/author_select_spec.js
index 60d36597fda..23b4cccd92c 100644
--- a/spec/frontend/projects/commits/components/author_select_spec.js
+++ b/spec/frontend/projects/commits/components/author_select_spec.js
@@ -65,6 +65,8 @@ describe('Author Select', () => {
describe('user is searching via "filter by commit message"', () => {
it('disables dropdown container', () => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({ hasSearchParam: true });
return wrapper.vm.$nextTick().then(() => {
@@ -73,6 +75,8 @@ describe('Author Select', () => {
});
it('has correct tooltip message', () => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({ hasSearchParam: true });
return wrapper.vm.$nextTick().then(() => {
@@ -83,6 +87,8 @@ describe('Author Select', () => {
});
it('disables dropdown', () => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({ hasSearchParam: false });
return wrapper.vm.$nextTick().then(() => {
@@ -103,6 +109,8 @@ describe('Author Select', () => {
});
it('displays the current selected author', () => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({ currentAuthor });
return wrapper.vm.$nextTick().then(() => {
@@ -156,6 +164,8 @@ describe('Author Select', () => {
isChecked: true,
};
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({ currentAuthor });
return wrapper.vm.$nextTick().then(() => {
diff --git a/spec/frontend/projects/compare/components/revision_dropdown_legacy_spec.js b/spec/frontend/projects/compare/components/revision_dropdown_legacy_spec.js
index 38e13dc5462..eb80d57fb3c 100644
--- a/spec/frontend/projects/compare/components/revision_dropdown_legacy_spec.js
+++ b/spec/frontend/projects/compare/components/revision_dropdown_legacy_spec.js
@@ -101,6 +101,8 @@ describe('RevisionDropdown component', () => {
const findGlDropdownItems = () => wrapper.findAll(GlDropdownItem);
const findFirstGlDropdownItem = () => findGlDropdownItems().at(0);
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({ branches: ['some-branch'] });
await wrapper.vm.$nextTick();
diff --git a/spec/frontend/project_find_file_spec.js b/spec/frontend/projects/project_find_file_spec.js
index 106b41bcc02..9c1000039b1 100644
--- a/spec/frontend/project_find_file_spec.js
+++ b/spec/frontend/projects/project_find_file_spec.js
@@ -3,7 +3,7 @@ import $ from 'jquery';
import { TEST_HOST } from 'helpers/test_constants';
import { sanitize } from '~/lib/dompurify';
import axios from '~/lib/utils/axios_utils';
-import ProjectFindFile from '~/project_find_file';
+import ProjectFindFile from '~/projects/project_find_file';
jest.mock('~/lib/dompurify', () => ({
addHook: jest.fn(),
diff --git a/spec/frontend/repository/components/blob_button_group_spec.js b/spec/frontend/repository/components/blob_button_group_spec.js
index 9f9d574a8ed..d5b882bd715 100644
--- a/spec/frontend/repository/components/blob_button_group_spec.js
+++ b/spec/frontend/repository/components/blob_button_group_spec.js
@@ -1,6 +1,5 @@
import { GlButton } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
-import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
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';
@@ -16,6 +15,7 @@ const DEFAULT_PROPS = {
projectPath: 'some/project/path',
isLocked: false,
canLock: true,
+ showForkSuggestion: false,
};
const DEFAULT_INJECT = {
@@ -27,7 +27,7 @@ describe('BlobButtonGroup component', () => {
let wrapper;
const createComponent = (props = {}) => {
- wrapper = shallowMount(BlobButtonGroup, {
+ wrapper = mountExtended(BlobButtonGroup, {
propsData: {
...DEFAULT_PROPS,
...props,
@@ -35,9 +35,6 @@ describe('BlobButtonGroup component', () => {
provide: {
...DEFAULT_INJECT,
},
- directives: {
- GlModal: createMockDirective(),
- },
});
};
@@ -47,7 +44,8 @@ describe('BlobButtonGroup component', () => {
const findDeleteBlobModal = () => wrapper.findComponent(DeleteBlobModal);
const findUploadBlobModal = () => wrapper.findComponent(UploadBlobModal);
- const findReplaceButton = () => wrapper.find('[data-testid="replace"]');
+ const findDeleteButton = () => wrapper.findByTestId('delete');
+ const findReplaceButton = () => wrapper.findByTestId('replace');
it('renders component', () => {
createComponent();
@@ -63,6 +61,8 @@ describe('BlobButtonGroup component', () => {
describe('buttons', () => {
beforeEach(() => {
createComponent();
+ jest.spyOn(findUploadBlobModal().vm, 'show');
+ jest.spyOn(findDeleteBlobModal().vm, 'show');
});
it('renders both the replace and delete button', () => {
@@ -75,10 +75,37 @@ describe('BlobButtonGroup component', () => {
});
it('triggers the UploadBlobModal from the replace button', () => {
- const { value } = getBinding(findReplaceButton().element, 'gl-modal');
- const modalId = findUploadBlobModal().props('modalId');
+ findReplaceButton().trigger('click');
+
+ expect(findUploadBlobModal().vm.show).toHaveBeenCalled();
+ });
+
+ it('triggers the DeleteBlobModal from the delete button', () => {
+ findDeleteButton().trigger('click');
+
+ expect(findDeleteBlobModal().vm.show).toHaveBeenCalled();
+ });
+
+ describe('showForkSuggestion set to true', () => {
+ beforeEach(() => {
+ createComponent({ showForkSuggestion: true });
+ jest.spyOn(findUploadBlobModal().vm, 'show');
+ jest.spyOn(findDeleteBlobModal().vm, 'show');
+ });
+
+ it('does not trigger the UploadBlobModal from the replace button', () => {
+ findReplaceButton().trigger('click');
+
+ expect(findUploadBlobModal().vm.show).not.toHaveBeenCalled();
+ expect(wrapper.emitted().fork).toBeTruthy();
+ });
+
+ it('does not trigger the DeleteBlobModal from the delete button', () => {
+ findDeleteButton().trigger('click');
- expect(modalId).toEqual(value);
+ expect(findDeleteBlobModal().vm.show).not.toHaveBeenCalled();
+ expect(wrapper.emitted().fork).toBeTruthy();
+ });
});
});
diff --git a/spec/frontend/repository/components/blob_content_viewer_spec.js b/spec/frontend/repository/components/blob_content_viewer_spec.js
index 9e00a2d0408..d3b60ec3768 100644
--- a/spec/frontend/repository/components/blob_content_viewer_spec.js
+++ b/spec/frontend/repository/components/blob_content_viewer_spec.js
@@ -83,6 +83,8 @@ const createComponent = async (mockData = {}, mountFn = shallowMount) => {
}),
);
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({ project, isBinary });
await waitForPromises();
@@ -336,35 +338,11 @@ describe('Blob content viewer component', () => {
deletePath: webPath,
canPushCode: pushCode,
canLock: true,
- isLocked: true,
+ isLocked: false,
emptyRepo: empty,
});
});
- it.each`
- canPushCode | canDownloadCode | username | canLock
- ${true} | ${true} | ${'root'} | ${true}
- ${false} | ${true} | ${'root'} | ${false}
- ${true} | ${false} | ${'root'} | ${false}
- ${true} | ${true} | ${'peter'} | ${false}
- `(
- 'passes the correct lock states',
- async ({ canPushCode, canDownloadCode, username, canLock }) => {
- gon.current_username = username;
-
- await createComponent(
- {
- pushCode: canPushCode,
- downloadCode: canDownloadCode,
- empty,
- },
- mount,
- );
-
- expect(findBlobButtonGroup().props('canLock')).toBe(canLock);
- },
- );
-
it('does not render if not logged in', async () => {
isLoggedIn.mockReturnValueOnce(false);
diff --git a/spec/frontend/repository/components/blob_controls_spec.js b/spec/frontend/repository/components/blob_controls_spec.js
new file mode 100644
index 00000000000..03e389ea5cb
--- /dev/null
+++ b/spec/frontend/repository/components/blob_controls_spec.js
@@ -0,0 +1,88 @@
+import { createLocalVue } from '@vue/test-utils';
+import VueApollo from 'vue-apollo';
+import { nextTick } from 'vue';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import BlobControls from '~/repository/components/blob_controls.vue';
+import blobControlsQuery from '~/repository/queries/blob_controls.query.graphql';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import createRouter from '~/repository/router';
+import { updateElementsVisibility } from '~/repository/utils/dom';
+import { blobControlsDataMock, refMock } from '../mock_data';
+
+jest.mock('~/repository/utils/dom');
+
+let router;
+let wrapper;
+let mockResolver;
+
+const localVue = createLocalVue();
+
+const createComponent = async () => {
+ localVue.use(VueApollo);
+
+ const project = { ...blobControlsDataMock };
+ const projectPath = 'some/project';
+
+ router = createRouter(projectPath, refMock);
+
+ router.replace({ name: 'blobPath', params: { path: '/some/file.js' } });
+
+ mockResolver = jest.fn().mockResolvedValue({ data: { project } });
+
+ wrapper = shallowMountExtended(BlobControls, {
+ localVue,
+ router,
+ apolloProvider: createMockApollo([[blobControlsQuery, mockResolver]]),
+ propsData: { projectPath },
+ mixins: [{ data: () => ({ ref: refMock }) }],
+ });
+
+ await waitForPromises();
+};
+
+describe('Blob controls component', () => {
+ const findFindButton = () => wrapper.findByTestId('find');
+ const findBlameButton = () => wrapper.findByTestId('blame');
+ const findHistoryButton = () => wrapper.findByTestId('history');
+ const findPermalinkButton = () => wrapper.findByTestId('permalink');
+
+ beforeEach(() => createComponent());
+
+ afterEach(() => wrapper.destroy());
+
+ it('renders a find button with the correct href', () => {
+ expect(findFindButton().attributes('href')).toBe('find/file.js');
+ });
+
+ it('renders a blame button with the correct href', () => {
+ expect(findBlameButton().attributes('href')).toBe('blame/file.js');
+ });
+
+ it('renders a history button with the correct href', () => {
+ expect(findHistoryButton().attributes('href')).toBe('history/file.js');
+ });
+
+ it('renders a permalink button with the correct href', () => {
+ expect(findPermalinkButton().attributes('href')).toBe('permalink/file.js');
+ });
+
+ it.each`
+ name | path
+ ${'blobPathDecoded'} | ${null}
+ ${'treePathDecoded'} | ${'myFile.js'}
+ `(
+ 'does not render any buttons if router name is $name and router path is $path',
+ async ({ name, path }) => {
+ router.replace({ name, params: { path } });
+
+ await nextTick();
+
+ expect(findFindButton().exists()).toBe(false);
+ expect(findBlameButton().exists()).toBe(false);
+ expect(findHistoryButton().exists()).toBe(false);
+ expect(findPermalinkButton().exists()).toBe(false);
+ expect(updateElementsVisibility).toHaveBeenCalledWith('.tree-controls', true);
+ },
+ );
+});
diff --git a/spec/frontend/repository/components/breadcrumbs_spec.js b/spec/frontend/repository/components/breadcrumbs_spec.js
index eb957c635ac..ad2cbd70187 100644
--- a/spec/frontend/repository/components/breadcrumbs_spec.js
+++ b/spec/frontend/repository/components/breadcrumbs_spec.js
@@ -75,6 +75,8 @@ describe('Repository breadcrumbs component', () => {
it('does not render add to tree dropdown when permissions are false', async () => {
factory('/', { canCollaborate: false });
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({ userPermissions: { forkProject: false, createMergeRequestIn: false } });
await wrapper.vm.$nextTick();
@@ -100,6 +102,8 @@ describe('Repository breadcrumbs component', () => {
it('renders add to tree dropdown when permissions are true', async () => {
factory('/', { canCollaborate: true });
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({ userPermissions: { forkProject: true, createMergeRequestIn: true } });
await wrapper.vm.$nextTick();
@@ -117,6 +121,8 @@ describe('Repository breadcrumbs component', () => {
});
it('renders the modal once loaded', async () => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({ $apollo: { queries: { userPermissions: { loading: false } } } });
await wrapper.vm.$nextTick();
@@ -139,6 +145,8 @@ describe('Repository breadcrumbs component', () => {
});
it('renders the modal once loaded', async () => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({ $apollo: { queries: { userPermissions: { loading: false } } } });
await wrapper.vm.$nextTick();
diff --git a/spec/frontend/repository/components/last_commit_spec.js b/spec/frontend/repository/components/last_commit_spec.js
index ebea7dde34a..fe05a981845 100644
--- a/spec/frontend/repository/components/last_commit_spec.js
+++ b/spec/frontend/repository/components/last_commit_spec.js
@@ -43,6 +43,8 @@ function factory(commit = createCommitData(), loading = false) {
},
},
});
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
vm.setData({ commit });
vm.vm.$apollo.queries.commit.loading = loading;
}
diff --git a/spec/frontend/repository/components/preview/index_spec.js b/spec/frontend/repository/components/preview/index_spec.js
index 466eed52739..2490258a048 100644
--- a/spec/frontend/repository/components/preview/index_spec.js
+++ b/spec/frontend/repository/components/preview/index_spec.js
@@ -34,6 +34,8 @@ describe('Repository file preview component', () => {
name: 'README.md',
});
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
vm.setData({ readme: { html: '<div class="blob">test</div>' } });
return vm.vm.$nextTick(() => {
@@ -47,6 +49,8 @@ describe('Repository file preview component', () => {
name: 'README.md',
});
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
vm.setData({ readme: { html: '<div class="blob">test</div>' } });
return vm.vm
@@ -63,6 +67,8 @@ describe('Repository file preview component', () => {
name: 'README.md',
});
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
vm.setData({ loading: 1 });
return vm.vm.$nextTick(() => {
diff --git a/spec/frontend/repository/components/table/index_spec.js b/spec/frontend/repository/components/table/index_spec.js
index c8dddefc4f2..2cd88944f81 100644
--- a/spec/frontend/repository/components/table/index_spec.js
+++ b/spec/frontend/repository/components/table/index_spec.js
@@ -89,6 +89,8 @@ describe('Repository table component', () => {
`('renders table caption for $ref in $path', ({ path, ref }) => {
factory({ path });
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
vm.setData({ ref });
return vm.vm.$nextTick(() => {
diff --git a/spec/frontend/repository/components/table/row_spec.js b/spec/frontend/repository/components/table/row_spec.js
index 7f59dbfe0d1..440baa72a3c 100644
--- a/spec/frontend/repository/components/table/row_spec.js
+++ b/spec/frontend/repository/components/table/row_spec.js
@@ -40,6 +40,8 @@ function factory(propsData = {}) {
},
});
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
vm.setData({ escapedRef: 'main' });
}
diff --git a/spec/frontend/repository/components/tree_content_spec.js b/spec/frontend/repository/components/tree_content_spec.js
index 9c5d07eede3..00ad1fc05f6 100644
--- a/spec/frontend/repository/components/tree_content_spec.js
+++ b/spec/frontend/repository/components/tree_content_spec.js
@@ -46,6 +46,8 @@ describe('Repository table component', () => {
it('renders file preview', async () => {
factory('/');
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
vm.setData({ entries: { blobs: [{ name: 'README.md' }] } });
await vm.vm.$nextTick();
@@ -134,6 +136,8 @@ describe('Repository table component', () => {
it('is not rendered if less than 1000 files', async () => {
factory('/');
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
vm.setData({ fetchCounter: 5, clickedShowMore: false });
await vm.vm.$nextTick();
@@ -153,6 +157,8 @@ describe('Repository table component', () => {
factory('/');
const blobs = new Array(totalBlobs).fill('fakeBlob');
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
vm.setData({ entries: { blobs }, pagesLoaded });
await vm.vm.$nextTick();
@@ -173,6 +179,8 @@ describe('Repository table component', () => {
${200} | ${100}
`('exponentially increases page size, to a maximum of 100', ({ fetchCounter, pageSize }) => {
factory('/');
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
vm.setData({ fetchCounter });
vm.vm.fetchFiles();
diff --git a/spec/frontend/repository/components/upload_blob_modal_spec.js b/spec/frontend/repository/components/upload_blob_modal_spec.js
index e9dfa3cd495..6b8b0752485 100644
--- a/spec/frontend/repository/components/upload_blob_modal_spec.js
+++ b/spec/frontend/repository/components/upload_blob_modal_spec.js
@@ -109,6 +109,8 @@ describe('UploadBlobModal', () => {
if (canPushCode) {
describe('when changing the branch name', () => {
it('displays the MR toggle', async () => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({ target: 'Not main' });
await wrapper.vm.$nextTick();
@@ -120,6 +122,8 @@ describe('UploadBlobModal', () => {
describe('completed form', () => {
beforeEach(() => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
file: { type: 'jpg' },
filePreviewURL: 'http://file.com?format=jpg',
diff --git a/spec/frontend/repository/mock_data.js b/spec/frontend/repository/mock_data.js
index 74d35daf578..a5ee17ba672 100644
--- a/spec/frontend/repository/mock_data.js
+++ b/spec/frontend/repository/mock_data.js
@@ -13,7 +13,9 @@ export const simpleViewerMock = {
ideForkAndEditPath: 'some_file.js/fork/ide',
canModifyBlob: true,
canCurrentUserPushToBranch: true,
+ archived: false,
storedExternally: false,
+ externalStorage: 'lfs',
rawPath: 'some_file.js',
replacePath: 'some_file.js/replace',
pipelineEditorPath: '',
@@ -50,7 +52,7 @@ export const projectMock = {
nodes: [
{
id: 'test',
- path: simpleViewerMock.path,
+ path: 'locked_file.js',
user: { id: '123', username: 'root' },
},
],
@@ -63,3 +65,22 @@ export const projectMock = {
export const propsMock = { path: 'some_file.js', projectPath: 'some/path' };
export const refMock = 'default-ref';
+
+export const blobControlsDataMock = {
+ id: '1234',
+ repository: {
+ blobs: {
+ nodes: [
+ {
+ id: '5678',
+ findFilePath: 'find/file.js',
+ blamePath: 'blame/file.js',
+ historyPath: 'history/file.js',
+ permalinkPath: 'permalink/file.js',
+ storedExternally: false,
+ externalStorage: '',
+ },
+ ],
+ },
+ },
+};
diff --git a/spec/frontend/runner/runner_detail/runner_details_app_spec.js b/spec/frontend/runner/admin_runner_edit/admin_runner_edit_app_spec.js
index 1a1428e8cb1..ad0bce5c9af 100644
--- a/spec/frontend/runner/runner_detail/runner_details_app_spec.js
+++ b/spec/frontend/runner/admin_runner_edit/admin_runner_edit_app_spec.js
@@ -2,12 +2,12 @@ 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 { createAlert } from '~/flash';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
-import RunnerTypeBadge from '~/runner/components/runner_type_badge.vue';
+import RunnerHeader from '~/runner/components/runner_header.vue';
import getRunnerQuery from '~/runner/graphql/get_runner.query.graphql';
-import RunnerDetailsApp from '~/runner/runner_details/runner_details_app.vue';
+import AdminRunnerEditApp from '~//runner/admin_runner_edit/admin_runner_edit_app.vue';
import { captureException } from '~/runner/sentry_utils';
import { runnerData } from '../mock_data';
@@ -21,14 +21,14 @@ const mockRunnerId = `${getIdFromGraphQLId(mockRunnerGraphqlId)}`;
const localVue = createLocalVue();
localVue.use(VueApollo);
-describe('RunnerDetailsApp', () => {
+describe('AdminRunnerEditApp', () => {
let wrapper;
let mockRunnerQuery;
- const findRunnerTypeBadge = () => wrapper.findComponent(RunnerTypeBadge);
+ const findRunnerHeader = () => wrapper.findComponent(RunnerHeader);
const createComponentWithApollo = ({ props = {}, mountFn = shallowMount } = {}) => {
- wrapper = mountFn(RunnerDetailsApp, {
+ wrapper = mountFn(AdminRunnerEditApp, {
localVue,
apolloProvider: createMockApollo([[getRunnerQuery, mockRunnerQuery]]),
propsData: {
@@ -40,7 +40,7 @@ describe('RunnerDetailsApp', () => {
return waitForPromises();
};
- beforeEach(async () => {
+ beforeEach(() => {
mockRunnerQuery = jest.fn().mockResolvedValue(runnerData);
});
@@ -56,15 +56,16 @@ describe('RunnerDetailsApp', () => {
});
it('displays the runner id', async () => {
- await createComponentWithApollo();
+ await createComponentWithApollo({ mountFn: mount });
- expect(wrapper.text()).toContain(`Runner #${mockRunnerId}`);
+ expect(findRunnerHeader().text()).toContain(`Runner #${mockRunnerId} created`);
});
- it('displays the runner type', async () => {
+ it('displays the runner type and status', async () => {
await createComponentWithApollo({ mountFn: mount });
- expect(findRunnerTypeBadge().text()).toBe('shared');
+ expect(findRunnerHeader().text()).toContain(`never contacted`);
+ expect(findRunnerHeader().text()).toContain(`shared`);
});
describe('When there is an error', () => {
@@ -73,15 +74,15 @@ describe('RunnerDetailsApp', () => {
await createComponentWithApollo();
});
- it('error is reported to sentry', async () => {
+ it('error is reported to sentry', () => {
expect(captureException).toHaveBeenCalledWith({
error: new Error('Network error: Error!'),
- component: 'RunnerDetailsApp',
+ component: 'AdminRunnerEditApp',
});
});
- it('error is shown to the user', async () => {
- expect(createFlash).toHaveBeenCalled();
+ it('error is shown to the user', () => {
+ expect(createAlert).toHaveBeenCalled();
});
});
});
diff --git a/spec/frontend/runner/admin_runners/admin_runners_app_spec.js b/spec/frontend/runner/admin_runners/admin_runners_app_spec.js
index 7015fe809b0..42be691ba4c 100644
--- a/spec/frontend/runner/admin_runners/admin_runners_app_spec.js
+++ b/spec/frontend/runner/admin_runners/admin_runners_app_spec.js
@@ -5,7 +5,7 @@ import createMockApollo from 'helpers/mock_apollo_helper';
import setWindowLocation from 'helpers/set_window_location_helper';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { updateHistory } from '~/lib/utils/url_utility';
@@ -13,6 +13,7 @@ import AdminRunnersApp from '~/runner/admin_runners/admin_runners_app.vue';
import RunnerTypeTabs from '~/runner/components/runner_type_tabs.vue';
import RunnerFilteredSearchBar from '~/runner/components/runner_filtered_search_bar.vue';
import RunnerList from '~/runner/components/runner_list.vue';
+import RunnerStats from '~/runner/components/stat/runner_stats.vue';
import RegistrationDropdown from '~/runner/components/registration/registration_dropdown.vue';
import RunnerPagination from '~/runner/components/runner_pagination.vue';
@@ -22,23 +23,21 @@ import {
CREATED_DESC,
DEFAULT_SORT,
INSTANCE_TYPE,
+ GROUP_TYPE,
+ PROJECT_TYPE,
PARAM_KEY_STATUS,
PARAM_KEY_TAG,
STATUS_ACTIVE,
RUNNER_PAGE_SIZE,
} from '~/runner/constants';
import getRunnersQuery from '~/runner/graphql/get_runners.query.graphql';
+import getRunnersCountQuery from '~/runner/graphql/get_runners_count.query.graphql';
import { captureException } from '~/runner/sentry_utils';
import FilteredSearch from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
-import { runnersData, runnersDataPaginated } from '../mock_data';
+import { runnersData, runnersCountData, runnersDataPaginated } from '../mock_data';
const mockRegistrationToken = 'MOCK_REGISTRATION_TOKEN';
-const mockActiveRunnersCount = '2';
-const mockAllRunnersCount = '6';
-const mockInstanceRunnersCount = '3';
-const mockGroupRunnersCount = '2';
-const mockProjectRunnersCount = '1';
jest.mock('~/flash');
jest.mock('~/runner/sentry_utils');
@@ -53,7 +52,9 @@ localVue.use(VueApollo);
describe('AdminRunnersApp', () => {
let wrapper;
let mockRunnersQuery;
+ let mockRunnersCountQuery;
+ const findRunnerStats = () => wrapper.findComponent(RunnerStats);
const findRegistrationDropdown = () => wrapper.findComponent(RegistrationDropdown);
const findRunnerTypeTabs = () => wrapper.findComponent(RunnerTypeTabs);
const findRunnerList = () => wrapper.findComponent(RunnerList);
@@ -65,27 +66,28 @@ describe('AdminRunnersApp', () => {
const findFilteredSearch = () => wrapper.findComponent(FilteredSearch);
const createComponent = ({ props = {}, mountFn = shallowMount } = {}) => {
- const handlers = [[getRunnersQuery, mockRunnersQuery]];
-
- wrapper = mountFn(AdminRunnersApp, {
- localVue,
- apolloProvider: createMockApollo(handlers),
- propsData: {
- registrationToken: mockRegistrationToken,
- activeRunnersCount: mockActiveRunnersCount,
- allRunnersCount: mockAllRunnersCount,
- instanceRunnersCount: mockInstanceRunnersCount,
- groupRunnersCount: mockGroupRunnersCount,
- projectRunnersCount: mockProjectRunnersCount,
- ...props,
- },
- });
+ const handlers = [
+ [getRunnersQuery, mockRunnersQuery],
+ [getRunnersCountQuery, mockRunnersCountQuery],
+ ];
+
+ wrapper = extendedWrapper(
+ mountFn(AdminRunnersApp, {
+ localVue,
+ apolloProvider: createMockApollo(handlers),
+ propsData: {
+ registrationToken: mockRegistrationToken,
+ ...props,
+ },
+ }),
+ );
};
beforeEach(async () => {
setWindowLocation('/admin/runners');
mockRunnersQuery = jest.fn().mockResolvedValue(runnersData);
+ mockRunnersCountQuery = jest.fn().mockResolvedValue(runnersCountData);
createComponent();
await waitForPromises();
});
@@ -95,13 +97,71 @@ describe('AdminRunnersApp', () => {
wrapper.destroy();
});
- it('shows the runner tabs with a runner count', async () => {
+ it('shows total runner counts', async () => {
createComponent({ mountFn: mount });
await waitForPromises();
+ const stats = findRunnerStats().text();
+
+ expect(stats).toMatch('Online runners 4');
+ expect(stats).toMatch('Offline runners 4');
+ expect(stats).toMatch('Stale runners 4');
+ });
+
+ it('shows the runner tabs with a runner count for each type', async () => {
+ mockRunnersCountQuery.mockImplementation(({ type }) => {
+ let count;
+ switch (type) {
+ case INSTANCE_TYPE:
+ count = 3;
+ break;
+ case GROUP_TYPE:
+ count = 2;
+ break;
+ case PROJECT_TYPE:
+ count = 1;
+ break;
+ default:
+ count = 6;
+ break;
+ }
+ return Promise.resolve({ data: { runners: { count } } });
+ });
+
+ createComponent({ mountFn: mount });
+ await waitForPromises();
+
expect(findRunnerTypeTabs().text()).toMatchInterpolatedText(
- `All ${mockAllRunnersCount} Instance ${mockInstanceRunnersCount} Group ${mockGroupRunnersCount} Project ${mockProjectRunnersCount}`,
+ `All 6 Instance 3 Group 2 Project 1`,
+ );
+ });
+
+ it('shows the runner tabs with a formatted runner count', async () => {
+ mockRunnersCountQuery.mockImplementation(({ type }) => {
+ let count;
+ switch (type) {
+ case INSTANCE_TYPE:
+ count = 3000;
+ break;
+ case GROUP_TYPE:
+ count = 2000;
+ break;
+ case PROJECT_TYPE:
+ count = 1000;
+ break;
+ default:
+ count = 6000;
+ break;
+ }
+ return Promise.resolve({ data: { runners: { count } } });
+ });
+
+ createComponent({ mountFn: mount });
+ await waitForPromises();
+
+ expect(findRunnerTypeTabs().text()).toMatchInterpolatedText(
+ `All 6,000 Instance 3,000 Group 2,000 Project 1,000`,
);
});
@@ -152,12 +212,6 @@ describe('AdminRunnersApp', () => {
]);
});
- it('shows the active runner count', () => {
- createComponent({ mountFn: mount });
-
- expect(wrapper.text()).toMatch(new RegExp(`Online Runners ${mockActiveRunnersCount}`));
- });
-
describe('when a filter is preselected', () => {
beforeEach(async () => {
setWindowLocation(`?status[]=${STATUS_ACTIVE}&runner_type[]=${INSTANCE_TYPE}&tag[]=tag1`);
@@ -241,7 +295,7 @@ describe('AdminRunnersApp', () => {
});
it('error is shown to the user', async () => {
- expect(createFlash).toHaveBeenCalledTimes(1);
+ expect(createAlert).toHaveBeenCalledTimes(1);
});
it('error is reported to sentry', async () => {
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 95c212cb0a9..4233d86c24c 100644
--- a/spec/frontend/runner/components/cells/runner_actions_cell_spec.js
+++ b/spec/frontend/runner/components/cells/runner_actions_cell_spec.js
@@ -4,7 +4,7 @@ import createMockApollo from 'helpers/mock_apollo_helper';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { captureException } from '~/runner/sentry_utils';
@@ -40,15 +40,17 @@ describe('RunnerTypeCell', () => {
const findDeleteBtn = () => wrapper.findByTestId('delete-runner');
const getTooltip = (w) => getBinding(w.element, 'gl-tooltip')?.value;
- const createComponent = ({ active = true } = {}, options) => {
+ const createComponent = (runner = {}, options) => {
wrapper = extendedWrapper(
shallowMount(RunnerActionCell, {
propsData: {
runner: {
id: mockRunner.id,
shortSha: mockRunner.shortSha,
- adminUrl: mockRunner.adminUrl,
- active,
+ editAdminUrl: mockRunner.editAdminUrl,
+ userPermissions: mockRunner.userPermissions,
+ active: mockRunner.active,
+ ...runner,
},
},
localVue,
@@ -101,7 +103,26 @@ describe('RunnerTypeCell', () => {
it('Displays the runner edit link with the correct href', () => {
createComponent();
- expect(findEditBtn().attributes('href')).toBe(mockRunner.adminUrl);
+ expect(findEditBtn().attributes('href')).toBe(mockRunner.editAdminUrl);
+ });
+
+ it('Does not render the runner edit link when user cannot update', () => {
+ createComponent({
+ userPermissions: {
+ ...mockRunner.userPermissions,
+ updateRunner: false,
+ },
+ });
+
+ expect(findEditBtn().exists()).toBe(false);
+ });
+
+ it('Does not render the runner edit link when editAdminUrl is not provided', () => {
+ createComponent({
+ editAdminUrl: null,
+ });
+
+ expect(findEditBtn().exists()).toBe(false);
});
});
@@ -179,7 +200,7 @@ describe('RunnerTypeCell', () => {
});
it('error is shown to the user', () => {
- expect(createFlash).toHaveBeenCalledTimes(1);
+ expect(createAlert).toHaveBeenCalledTimes(1);
});
});
@@ -208,11 +229,22 @@ describe('RunnerTypeCell', () => {
});
it('error is shown to the user', () => {
- expect(createFlash).toHaveBeenCalledTimes(1);
+ expect(createAlert).toHaveBeenCalledTimes(1);
});
});
});
});
+
+ it('Does not render the runner toggle active button when user cannot update', () => {
+ createComponent({
+ userPermissions: {
+ ...mockRunner.userPermissions,
+ updateRunner: false,
+ },
+ });
+
+ expect(findToggleActiveBtn().exists()).toBe(false);
+ });
});
describe('Delete action', () => {
@@ -225,6 +257,10 @@ describe('RunnerTypeCell', () => {
);
});
+ it('Renders delete button', () => {
+ expect(findDeleteBtn().exists()).toBe(true);
+ });
+
it('Delete button opens delete modal', () => {
const modalId = getBinding(findDeleteBtn().element, 'gl-modal').value;
@@ -259,6 +295,18 @@ describe('RunnerTypeCell', () => {
});
});
+ it('Does not render the runner delete button when user cannot delete', () => {
+ createComponent({
+ userPermissions: {
+ ...mockRunner.userPermissions,
+ deleteRunner: false,
+ },
+ });
+
+ expect(findDeleteBtn().exists()).toBe(false);
+ expect(findRunnerDeleteModal().exists()).toBe(false);
+ });
+
describe('When delete is clicked', () => {
beforeEach(() => {
findRunnerDeleteModal().vm.$emit('primary');
@@ -302,7 +350,7 @@ describe('RunnerTypeCell', () => {
});
it('error is shown to the user', () => {
- expect(createFlash).toHaveBeenCalledTimes(1);
+ expect(createAlert).toHaveBeenCalledTimes(1);
});
it('toast notification is not shown', () => {
@@ -334,7 +382,7 @@ describe('RunnerTypeCell', () => {
});
it('error is shown to the user', () => {
- expect(createFlash).toHaveBeenCalledTimes(1);
+ expect(createAlert).toHaveBeenCalledTimes(1);
});
});
});
diff --git a/spec/frontend/runner/components/registration/registration_token_reset_dropdown_item_spec.js b/spec/frontend/runner/components/registration/registration_token_reset_dropdown_item_spec.js
index 0d002c272b4..e75decddf70 100644
--- a/spec/frontend/runner/components/registration/registration_token_reset_dropdown_item_spec.js
+++ b/spec/frontend/runner/components/registration/registration_token_reset_dropdown_item_spec.js
@@ -1,14 +1,15 @@
-import { GlDropdownItem, GlLoadingIcon, GlToast } from '@gitlab/ui';
+import { GlDropdownItem, GlLoadingIcon, GlToast, GlModal } from '@gitlab/ui';
import { createLocalVue, shallowMount } from '@vue/test-utils';
import { nextTick } 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 { createAlert } from '~/flash';
import RegistrationTokenResetDropdownItem from '~/runner/components/registration/registration_token_reset_dropdown_item.vue';
import { INSTANCE_TYPE, GROUP_TYPE, PROJECT_TYPE } from '~/runner/constants';
import runnersRegistrationTokenResetMutation from '~/runner/graphql/runners_registration_token_reset.mutation.graphql';
import { captureException } from '~/runner/sentry_utils';
+import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
jest.mock('~/flash');
jest.mock('~/runner/sentry_utils');
@@ -18,14 +19,18 @@ localVue.use(VueApollo);
localVue.use(GlToast);
const mockNewToken = 'NEW_TOKEN';
+const modalID = 'token-reset-modal';
describe('RegistrationTokenResetDropdownItem', () => {
let wrapper;
let runnersRegistrationTokenResetMutationHandler;
let showToast;
+ const mockEvent = { preventDefault: jest.fn() };
const findDropdownItem = () => wrapper.findComponent(GlDropdownItem);
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
+ const findModal = () => wrapper.findComponent(GlModal);
+ const clickSubmit = () => findModal().vm.$emit('primary', mockEvent);
const createComponent = ({ props, provide = {} } = {}) => {
wrapper = shallowMount(RegistrationTokenResetDropdownItem, {
@@ -38,6 +43,9 @@ describe('RegistrationTokenResetDropdownItem', () => {
apolloProvider: createMockApollo([
[runnersRegistrationTokenResetMutation, runnersRegistrationTokenResetMutationHandler],
]),
+ directives: {
+ GlModal: createMockDirective(),
+ },
});
showToast = wrapper.vm.$toast ? jest.spyOn(wrapper.vm.$toast, 'show') : null;
@@ -54,8 +62,6 @@ describe('RegistrationTokenResetDropdownItem', () => {
});
createComponent();
-
- jest.spyOn(window, 'confirm');
});
afterEach(() => {
@@ -66,6 +72,18 @@ describe('RegistrationTokenResetDropdownItem', () => {
expect(findDropdownItem().exists()).toBe(true);
});
+ describe('modal directive integration', () => {
+ it('has the correct ID on the dropdown', () => {
+ const binding = getBinding(findDropdownItem().element, 'gl-modal');
+
+ expect(binding.value).toBe(modalID);
+ });
+
+ it('has the correct ID on the modal', () => {
+ expect(findModal().props('modalId')).toBe(modalID);
+ });
+ });
+
describe('On click and confirmation', () => {
const mockGroupId = '11';
const mockProjectId = '22';
@@ -82,9 +100,8 @@ describe('RegistrationTokenResetDropdownItem', () => {
props: { type },
});
- window.confirm.mockReturnValueOnce(true);
-
findDropdownItem().trigger('click');
+ clickSubmit();
await waitForPromises();
});
@@ -114,7 +131,6 @@ describe('RegistrationTokenResetDropdownItem', () => {
describe('On click without confirmation', () => {
beforeEach(async () => {
- window.confirm.mockReturnValueOnce(false);
findDropdownItem().vm.$emit('click');
await waitForPromises();
});
@@ -142,11 +158,11 @@ describe('RegistrationTokenResetDropdownItem', () => {
runnersRegistrationTokenResetMutationHandler.mockRejectedValueOnce(new Error(mockErrorMsg));
- window.confirm.mockReturnValueOnce(true);
findDropdownItem().trigger('click');
+ clickSubmit();
await waitForPromises();
- expect(createFlash).toHaveBeenLastCalledWith({
+ expect(createAlert).toHaveBeenLastCalledWith({
message: `Network error: ${mockErrorMsg}`,
});
expect(captureException).toHaveBeenCalledWith({
@@ -168,11 +184,11 @@ describe('RegistrationTokenResetDropdownItem', () => {
},
});
- window.confirm.mockReturnValueOnce(true);
findDropdownItem().trigger('click');
+ clickSubmit();
await waitForPromises();
- expect(createFlash).toHaveBeenLastCalledWith({
+ expect(createAlert).toHaveBeenLastCalledWith({
message: `${mockErrorMsg} ${mockErrorMsg2}`,
});
expect(captureException).toHaveBeenCalledWith({
@@ -184,8 +200,8 @@ describe('RegistrationTokenResetDropdownItem', () => {
describe('Immediately after click', () => {
it('shows loading state', async () => {
- window.confirm.mockReturnValue(true);
findDropdownItem().trigger('click');
+ clickSubmit();
await nextTick();
expect(findLoadingIcon().exists()).toBe(true);
diff --git a/spec/frontend/runner/components/runner_header_spec.js b/spec/frontend/runner/components/runner_header_spec.js
new file mode 100644
index 00000000000..50699df3a44
--- /dev/null
+++ b/spec/frontend/runner/components/runner_header_spec.js
@@ -0,0 +1,93 @@
+import { GlSprintf } from '@gitlab/ui';
+import { mount, shallowMount } from '@vue/test-utils';
+import { GROUP_TYPE, STATUS_ONLINE } from '~/runner/constants';
+import { TYPE_CI_RUNNER } from '~/graphql_shared/constants';
+import { convertToGraphQLId } from '~/graphql_shared/utils';
+import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
+
+import RunnerHeader from '~/runner/components/runner_header.vue';
+import RunnerTypeBadge from '~/runner/components/runner_type_badge.vue';
+import RunnerStatusBadge from '~/runner/components/runner_status_badge.vue';
+
+import { runnerData } from '../mock_data';
+
+const mockRunner = runnerData.data.runner;
+
+describe('RunnerHeader', () => {
+ let wrapper;
+
+ const findRunnerTypeBadge = () => wrapper.findComponent(RunnerTypeBadge);
+ const findRunnerStatusBadge = () => wrapper.findComponent(RunnerStatusBadge);
+ const findTimeAgo = () => wrapper.findComponent(TimeAgo);
+
+ const createComponent = ({ runner = {}, mountFn = shallowMount } = {}) => {
+ wrapper = mountFn(RunnerHeader, {
+ propsData: {
+ runner: {
+ ...mockRunner,
+ ...runner,
+ },
+ },
+ stubs: {
+ GlSprintf,
+ TimeAgo,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('displays the runner status', () => {
+ createComponent({
+ mountFn: mount,
+ runner: {
+ status: STATUS_ONLINE,
+ },
+ });
+
+ expect(findRunnerStatusBadge().text()).toContain(`online`);
+ });
+
+ it('displays the runner type', () => {
+ createComponent({
+ mountFn: mount,
+ runner: {
+ runnerType: GROUP_TYPE,
+ },
+ });
+
+ expect(findRunnerTypeBadge().text()).toContain(`group`);
+ });
+
+ it('displays the runner id', () => {
+ createComponent({
+ runner: {
+ id: convertToGraphQLId(TYPE_CI_RUNNER, 99),
+ },
+ });
+
+ expect(wrapper.text()).toContain(`Runner #99`);
+ });
+
+ it('displays the runner creation time', () => {
+ createComponent();
+
+ expect(wrapper.text()).toMatch(/created .+/);
+ expect(findTimeAgo().props('time')).toBe(mockRunner.createdAt);
+ });
+
+ it('does not display runner creation time if createdAt missing', () => {
+ createComponent({
+ runner: {
+ id: convertToGraphQLId(TYPE_CI_RUNNER, 99),
+ createdAt: null,
+ },
+ });
+
+ expect(wrapper.text()).toContain(`Runner #99`);
+ expect(wrapper.text()).not.toMatch(/created .+/);
+ expect(findTimeAgo().exists()).toBe(false);
+ });
+});
diff --git a/spec/frontend/runner/components/runner_list_spec.js b/spec/frontend/runner/components/runner_list_spec.js
index 5a14fa5a2d5..452430b7237 100644
--- a/spec/frontend/runner/components/runner_list_spec.js
+++ b/spec/frontend/runner/components/runner_list_spec.js
@@ -69,7 +69,9 @@ describe('RunnerList', () => {
const { id, description, version, ipAddress, shortSha } = mockRunners[0];
// Badges
- expect(findCell({ fieldKey: 'status' }).text()).toMatchInterpolatedText('not connected paused');
+ expect(findCell({ fieldKey: 'status' }).text()).toMatchInterpolatedText(
+ 'never contacted paused',
+ );
// Runner summary
expect(findCell({ fieldKey: 'summary' }).text()).toContain(
diff --git a/spec/frontend/runner/components/runner_status_badge_spec.js b/spec/frontend/runner/components/runner_status_badge_spec.js
index a19515d6ed2..c470c6bb989 100644
--- a/spec/frontend/runner/components/runner_status_badge_spec.js
+++ b/spec/frontend/runner/components/runner_status_badge_spec.js
@@ -6,7 +6,6 @@ import {
STATUS_ONLINE,
STATUS_OFFLINE,
STATUS_STALE,
- STATUS_NOT_CONNECTED,
STATUS_NEVER_CONTACTED,
} from '~/runner/constants';
@@ -50,20 +49,7 @@ describe('RunnerTypeBadge', () => {
expect(getTooltip().value).toBe('Runner is online; last contact was 1 minute ago');
});
- it('renders not connected state', () => {
- createComponent({
- runner: {
- contactedAt: null,
- status: STATUS_NOT_CONNECTED,
- },
- });
-
- expect(wrapper.text()).toBe('not connected');
- expect(findBadge().props('variant')).toBe('muted');
- expect(getTooltip().value).toMatch('This runner has never connected');
- });
-
- it('renders never contacted state as not connected, for backwards compatibility', () => {
+ it('renders never contacted state', () => {
createComponent({
runner: {
contactedAt: null,
@@ -71,9 +57,9 @@ describe('RunnerTypeBadge', () => {
},
});
- expect(wrapper.text()).toBe('not connected');
+ expect(wrapper.text()).toBe('never contacted');
expect(findBadge().props('variant')).toBe('muted');
- expect(getTooltip().value).toMatch('This runner has never connected');
+ expect(getTooltip().value).toMatch('This runner has never contacted');
});
it('renders offline state', () => {
diff --git a/spec/frontend/runner/components/runner_type_alert_spec.js b/spec/frontend/runner/components/runner_type_alert_spec.js
deleted file mode 100644
index 4023c75c9a8..00000000000
--- a/spec/frontend/runner/components/runner_type_alert_spec.js
+++ /dev/null
@@ -1,61 +0,0 @@
-import { GlAlert, GlLink } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
-import RunnerTypeAlert from '~/runner/components/runner_type_alert.vue';
-import { INSTANCE_TYPE, GROUP_TYPE, PROJECT_TYPE } from '~/runner/constants';
-
-describe('RunnerTypeAlert', () => {
- let wrapper;
-
- const findAlert = () => wrapper.findComponent(GlAlert);
- const findLink = () => wrapper.findComponent(GlLink);
-
- const createComponent = ({ props = {} } = {}) => {
- wrapper = shallowMount(RunnerTypeAlert, {
- propsData: {
- type: INSTANCE_TYPE,
- ...props,
- },
- });
- };
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- describe.each`
- type | exampleText | anchor
- ${INSTANCE_TYPE} | ${'This runner is available to all groups and projects'} | ${'#shared-runners'}
- ${GROUP_TYPE} | ${'This runner is available to all projects and subgroups in a group'} | ${'#group-runners'}
- ${PROJECT_TYPE} | ${'This runner is associated with one or more projects'} | ${'#specific-runners'}
- `('When it is an $type level runner', ({ type, exampleText, anchor }) => {
- beforeEach(() => {
- createComponent({ props: { type } });
- });
-
- it('Describes runner type', () => {
- expect(wrapper.text()).toMatch(exampleText);
- });
-
- it(`Shows an "info" variant`, () => {
- expect(findAlert().props('variant')).toBe('info');
- });
-
- it(`Links to anchor "${anchor}"`, () => {
- expect(findLink().attributes('href')).toBe(`/help/ci/runners/runners_scope${anchor}`);
- });
- });
-
- describe('When runner type is not correct', () => {
- it('Does not render content when type is missing', () => {
- createComponent({ props: { type: undefined } });
-
- expect(wrapper.html()).toBe('');
- });
-
- it('Validation fails for an incorrect type', () => {
- expect(() => {
- createComponent({ props: { type: 'NOT_A_TYPE' } });
- }).toThrow();
- });
- });
-});
diff --git a/spec/frontend/runner/components/runner_update_form_spec.js b/spec/frontend/runner/components/runner_update_form_spec.js
index 0e0844a785b..ebb2e67d1e2 100644
--- a/spec/frontend/runner/components/runner_update_form_spec.js
+++ b/spec/frontend/runner/components/runner_update_form_spec.js
@@ -5,7 +5,7 @@ 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, { FLASH_TYPES } from '~/flash';
+import { createAlert, VARIANT_SUCCESS } from '~/flash';
import RunnerUpdateForm from '~/runner/components/runner_update_form.vue';
import {
INSTANCE_TYPE,
@@ -79,9 +79,9 @@ describe('RunnerUpdateForm', () => {
input: expect.objectContaining(submittedRunner),
});
- expect(createFlash).toHaveBeenLastCalledWith({
+ expect(createAlert).toHaveBeenLastCalledWith({
message: expect.stringContaining('saved'),
- type: FLASH_TYPES.SUCCESS,
+ variant: VARIANT_SUCCESS,
});
expect(findSubmitDisabledAttr()).toBeUndefined();
@@ -127,7 +127,7 @@ describe('RunnerUpdateForm', () => {
await submitFormAndWait();
// Some fields are not submitted
- const { ipAddress, runnerType, ...submitted } = mockRunner;
+ const { ipAddress, runnerType, createdAt, status, ...submitted } = mockRunner;
expectToHaveSubmittedRunnerContaining(submitted);
});
@@ -238,7 +238,7 @@ describe('RunnerUpdateForm', () => {
await submitFormAndWait();
- expect(createFlash).toHaveBeenLastCalledWith({
+ expect(createAlert).toHaveBeenLastCalledWith({
message: `Network error: ${mockErrorMsg}`,
});
expect(captureException).toHaveBeenCalledWith({
@@ -262,7 +262,7 @@ describe('RunnerUpdateForm', () => {
await submitFormAndWait();
- expect(createFlash).toHaveBeenLastCalledWith({
+ expect(createAlert).toHaveBeenLastCalledWith({
message: mockErrorMsg,
});
expect(captureException).not.toHaveBeenCalled();
diff --git a/spec/frontend/runner/components/search_tokens/tag_token_spec.js b/spec/frontend/runner/components/search_tokens/tag_token_spec.js
index 89c06ba2df4..52557ff716d 100644
--- a/spec/frontend/runner/components/search_tokens/tag_token_spec.js
+++ b/spec/frontend/runner/components/search_tokens/tag_token_spec.js
@@ -3,7 +3,7 @@ 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 { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import TagToken, { TAG_SUGGESTIONS_PATH } from '~/runner/components/search_tokens/tag_token.vue';
@@ -168,8 +168,8 @@ describe('TagToken', () => {
});
it('error is shown', async () => {
- expect(createFlash).toHaveBeenCalledTimes(1);
- expect(createFlash).toHaveBeenCalledWith({ message: expect.any(String) });
+ expect(createAlert).toHaveBeenCalledTimes(1);
+ expect(createAlert).toHaveBeenCalledWith({ message: expect.any(String) });
});
});
diff --git a/spec/frontend/runner/components/stat/runner_online_stat_spec.js b/spec/frontend/runner/components/stat/runner_online_stat_spec.js
deleted file mode 100644
index 18f865aa22c..00000000000
--- a/spec/frontend/runner/components/stat/runner_online_stat_spec.js
+++ /dev/null
@@ -1,34 +0,0 @@
-import { GlSingleStat } from '@gitlab/ui/dist/charts';
-import { shallowMount, mount } from '@vue/test-utils';
-import RunnerOnlineBadge from '~/runner/components/stat/runner_online_stat.vue';
-
-describe('RunnerOnlineBadge', () => {
- let wrapper;
-
- const findSingleStat = () => wrapper.findComponent(GlSingleStat);
-
- const createComponent = ({ props = {} } = {}, mountFn = shallowMount) => {
- wrapper = mountFn(RunnerOnlineBadge, {
- propsData: {
- value: '99',
- ...props,
- },
- });
- };
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- it('Uses a success appearance', () => {
- createComponent({}, shallowMount);
-
- expect(findSingleStat().props('variant')).toBe('success');
- });
-
- it('Renders a value', () => {
- createComponent({}, mount);
-
- expect(wrapper.text()).toMatch(new RegExp(`Online Runners 99\\s+online`));
- });
-});
diff --git a/spec/frontend/runner/components/stat/runner_stats_spec.js b/spec/frontend/runner/components/stat/runner_stats_spec.js
new file mode 100644
index 00000000000..68db8621ef0
--- /dev/null
+++ b/spec/frontend/runner/components/stat/runner_stats_spec.js
@@ -0,0 +1,46 @@
+import { shallowMount, mount } from '@vue/test-utils';
+import RunnerStats from '~/runner/components/stat/runner_stats.vue';
+import RunnerStatusStat from '~/runner/components/stat/runner_status_stat.vue';
+import { STATUS_ONLINE, STATUS_OFFLINE, STATUS_STALE } from '~/runner/constants';
+
+describe('RunnerStats', () => {
+ let wrapper;
+
+ const findRunnerStatusStatAt = (i) => wrapper.findAllComponents(RunnerStatusStat).at(i);
+
+ const createComponent = ({ props = {}, mountFn = shallowMount } = {}) => {
+ wrapper = mountFn(RunnerStats, {
+ propsData: {
+ onlineRunnersCount: 3,
+ offlineRunnersCount: 2,
+ staleRunnersCount: 1,
+ ...props,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('Displays all the stats', () => {
+ createComponent({ mountFn: mount });
+
+ const stats = wrapper.text();
+
+ expect(stats).toMatch('Online runners 3');
+ expect(stats).toMatch('Offline runners 2');
+ expect(stats).toMatch('Stale runners 1');
+ });
+
+ it.each`
+ i | status
+ ${0} | ${STATUS_ONLINE}
+ ${1} | ${STATUS_OFFLINE}
+ ${2} | ${STATUS_STALE}
+ `('Displays status types at index $i', ({ i, status }) => {
+ createComponent();
+
+ expect(findRunnerStatusStatAt(i).props('status')).toBe(status);
+ });
+});
diff --git a/spec/frontend/runner/components/stat/runner_status_stat_spec.js b/spec/frontend/runner/components/stat/runner_status_stat_spec.js
new file mode 100644
index 00000000000..3218272eac7
--- /dev/null
+++ b/spec/frontend/runner/components/stat/runner_status_stat_spec.js
@@ -0,0 +1,67 @@
+import { GlSingleStat } from '@gitlab/ui/dist/charts';
+import { shallowMount, mount } from '@vue/test-utils';
+import RunnerStatusStat from '~/runner/components/stat/runner_status_stat.vue';
+import { STATUS_ONLINE, STATUS_OFFLINE, STATUS_STALE } from '~/runner/constants';
+
+describe('RunnerStatusStat', () => {
+ let wrapper;
+
+ const findSingleStat = () => wrapper.findComponent(GlSingleStat);
+
+ const createComponent = ({ props = {} } = {}, mountFn = shallowMount) => {
+ wrapper = mountFn(RunnerStatusStat, {
+ propsData: {
+ status: STATUS_ONLINE,
+ value: 99,
+ ...props,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe.each`
+ status | variant | title | badge
+ ${STATUS_ONLINE} | ${'success'} | ${'Online runners'} | ${'online'}
+ ${STATUS_OFFLINE} | ${'muted'} | ${'Offline runners'} | ${'offline'}
+ ${STATUS_STALE} | ${'warning'} | ${'Stale runners'} | ${'stale'}
+ `('Renders a stat for status "$status"', ({ status, variant, title, badge }) => {
+ beforeEach(() => {
+ createComponent({ props: { status } }, mount);
+ });
+
+ it('Renders text', () => {
+ expect(wrapper.text()).toMatch(new RegExp(`${title} 99\\s+${badge}`));
+ });
+
+ it(`Uses variant ${variant}`, () => {
+ expect(findSingleStat().props('variant')).toBe(variant);
+ });
+ });
+
+ it('Formats stat number', () => {
+ createComponent({ props: { value: 1000 } }, mount);
+
+ expect(wrapper.text()).toMatch('Online runners 1,000');
+ });
+
+ it('Shows a null result', () => {
+ createComponent({ props: { value: null } }, mount);
+
+ expect(wrapper.text()).toMatch('Online runners -');
+ });
+
+ it('Shows an undefined result', () => {
+ createComponent({ props: { value: undefined } }, mount);
+
+ expect(wrapper.text()).toMatch('Online runners -');
+ });
+
+ it('Shows result for an unknown status', () => {
+ createComponent({ props: { status: 'UNKNOWN' } }, mount);
+
+ expect(wrapper.text()).toMatch('Runners 99');
+ });
+});
diff --git a/spec/frontend/runner/group_runners/group_runners_app_spec.js b/spec/frontend/runner/group_runners/group_runners_app_spec.js
index 4451100de19..034b7848f35 100644
--- a/spec/frontend/runner/group_runners/group_runners_app_spec.js
+++ b/spec/frontend/runner/group_runners/group_runners_app_spec.js
@@ -6,12 +6,13 @@ import createMockApollo from 'helpers/mock_apollo_helper';
import setWindowLocation from 'helpers/set_window_location_helper';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { updateHistory } from '~/lib/utils/url_utility';
import RunnerFilteredSearchBar from '~/runner/components/runner_filtered_search_bar.vue';
import RunnerList from '~/runner/components/runner_list.vue';
+import RunnerStats from '~/runner/components/stat/runner_stats.vue';
import RegistrationDropdown from '~/runner/components/registration/registration_dropdown.vue';
import RunnerPagination from '~/runner/components/runner_pagination.vue';
@@ -26,10 +27,11 @@ import {
RUNNER_PAGE_SIZE,
} from '~/runner/constants';
import getGroupRunnersQuery from '~/runner/graphql/get_group_runners.query.graphql';
+import getGroupRunnersCountQuery from '~/runner/graphql/get_group_runners_count.query.graphql';
import GroupRunnersApp from '~/runner/group_runners/group_runners_app.vue';
import { captureException } from '~/runner/sentry_utils';
import FilteredSearch from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
-import { groupRunnersData, groupRunnersDataPaginated } from '../mock_data';
+import { groupRunnersData, groupRunnersDataPaginated, groupRunnersCountData } from '../mock_data';
const localVue = createLocalVue();
localVue.use(VueApollo);
@@ -48,7 +50,9 @@ jest.mock('~/lib/utils/url_utility', () => ({
describe('GroupRunnersApp', () => {
let wrapper;
let mockGroupRunnersQuery;
+ let mockGroupRunnersCountQuery;
+ const findRunnerStats = () => wrapper.findComponent(RunnerStats);
const findRegistrationDropdown = () => wrapper.findComponent(RegistrationDropdown);
const findRunnerList = () => wrapper.findComponent(RunnerList);
const findRunnerPagination = () => extendedWrapper(wrapper.findComponent(RunnerPagination));
@@ -59,7 +63,10 @@ describe('GroupRunnersApp', () => {
const findFilteredSearch = () => wrapper.findComponent(FilteredSearch);
const createComponent = ({ props = {}, mountFn = shallowMount } = {}) => {
- const handlers = [[getGroupRunnersQuery, mockGroupRunnersQuery]];
+ const handlers = [
+ [getGroupRunnersQuery, mockGroupRunnersQuery],
+ [getGroupRunnersCountQuery, mockGroupRunnersCountQuery],
+ ];
wrapper = mountFn(GroupRunnersApp, {
localVue,
@@ -77,11 +84,24 @@ describe('GroupRunnersApp', () => {
setWindowLocation(`/groups/${mockGroupFullPath}/-/runners`);
mockGroupRunnersQuery = jest.fn().mockResolvedValue(groupRunnersData);
+ mockGroupRunnersCountQuery = jest.fn().mockResolvedValue(groupRunnersCountData);
createComponent();
await waitForPromises();
});
+ it('shows total runner counts', async () => {
+ createComponent({ mountFn: mount });
+
+ await waitForPromises();
+
+ const stats = findRunnerStats().text();
+
+ expect(stats).toMatch('Online runners 2');
+ expect(stats).toMatch('Offline runners 2');
+ expect(stats).toMatch('Stale runners 2');
+ });
+
it('shows the runner setup instructions', () => {
expect(findRegistrationDropdown().props('registrationToken')).toBe(mockRegistrationToken);
expect(findRegistrationDropdown().props('type')).toBe(GROUP_TYPE);
@@ -129,28 +149,6 @@ describe('GroupRunnersApp', () => {
);
});
- describe('shows the active runner count', () => {
- const expectedOnlineCount = (count) => new RegExp(`Online Runners ${count}`);
-
- it('with a regular value', () => {
- createComponent({ mountFn: mount });
-
- expect(wrapper.text()).toMatch(expectedOnlineCount(mockGroupRunnersLimitedCount));
- });
-
- it('at the limit', () => {
- createComponent({ props: { groupRunnersLimitedCount: 1000 }, mountFn: mount });
-
- expect(wrapper.text()).toMatch(expectedOnlineCount('1,000'));
- });
-
- it('over the limit', () => {
- createComponent({ props: { groupRunnersLimitedCount: 1001 }, mountFn: mount });
-
- expect(wrapper.text()).toMatch(expectedOnlineCount('1,000\\+'));
- });
- });
-
describe('when a filter is preselected', () => {
beforeEach(async () => {
setWindowLocation(`?status[]=${STATUS_ACTIVE}&runner_type[]=${INSTANCE_TYPE}`);
@@ -236,7 +234,7 @@ describe('GroupRunnersApp', () => {
});
it('error is shown to the user', async () => {
- expect(createFlash).toHaveBeenCalledTimes(1);
+ expect(createAlert).toHaveBeenCalledTimes(1);
});
it('error is reported to sentry', async () => {
diff --git a/spec/frontend/runner/mock_data.js b/spec/frontend/runner/mock_data.js
index b8d0f1273c7..9c430e205ea 100644
--- a/spec/frontend/runner/mock_data.js
+++ b/spec/frontend/runner/mock_data.js
@@ -2,17 +2,21 @@
// Admin queries
import runnersData from 'test_fixtures/graphql/runner/get_runners.query.graphql.json';
+import runnersCountData from 'test_fixtures/graphql/runner/get_runners_count.query.graphql.json';
import runnersDataPaginated from 'test_fixtures/graphql/runner/get_runners.query.graphql.paginated.json';
import runnerData from 'test_fixtures/graphql/runner/get_runner.query.graphql.json';
// Group queries
import groupRunnersData from 'test_fixtures/graphql/runner/get_group_runners.query.graphql.json';
+import groupRunnersCountData from 'test_fixtures/graphql/runner/get_group_runners_count.query.graphql.json';
import groupRunnersDataPaginated from 'test_fixtures/graphql/runner/get_group_runners.query.graphql.paginated.json';
export {
runnerData,
+ runnersCountData,
runnersDataPaginated,
runnersData,
groupRunnersData,
+ groupRunnersCountData,
groupRunnersDataPaginated,
};
diff --git a/spec/frontend/runner/runner_search_utils_spec.js b/spec/frontend/runner/runner_search_utils_spec.js
index 0fc7917663e..aff1ec882bb 100644
--- a/spec/frontend/runner/runner_search_utils_spec.js
+++ b/spec/frontend/runner/runner_search_utils_spec.js
@@ -1,6 +1,7 @@
import { RUNNER_PAGE_SIZE } from '~/runner/constants';
import {
searchValidator,
+ updateOutdatedUrl,
fromUrlQueryToSearch,
fromSearchToUrl,
fromSearchToVariables,
@@ -190,6 +191,23 @@ describe('search_params.js', () => {
});
});
+ describe('updateOutdatedUrl', () => {
+ it('returns null for urls that do not need updating', () => {
+ expect(updateOutdatedUrl('http://test.host/')).toBe(null);
+ expect(updateOutdatedUrl('http://test.host/?a=b')).toBe(null);
+ });
+
+ it('returns updated url for updating NOT_CONNECTED to NEVER_CONTACTED', () => {
+ expect(updateOutdatedUrl('http://test.host/admin/runners?status[]=NOT_CONNECTED')).toBe(
+ 'http://test.host/admin/runners?status[]=NEVER_CONTACTED',
+ );
+
+ expect(updateOutdatedUrl('http://test.host/admin/runners?status[]=NOT_CONNECTED&a=b')).toBe(
+ 'http://test.host/admin/runners?status[]=NEVER_CONTACTED&a=b',
+ );
+ });
+ });
+
describe('fromUrlQueryToSearch', () => {
examples.forEach(({ name, urlQuery, search }) => {
it(`Converts ${name} to a search object`, () => {
diff --git a/spec/frontend/runner/runner_detail/runner_update_form_utils_spec.js b/spec/frontend/runner/runner_update_form_utils_spec.js
index 510b4e604ac..a633aee92f7 100644
--- a/spec/frontend/runner/runner_detail/runner_update_form_utils_spec.js
+++ b/spec/frontend/runner/runner_update_form_utils_spec.js
@@ -1,8 +1,5 @@
import { ACCESS_LEVEL_NOT_PROTECTED } from '~/runner/constants';
-import {
- modelToUpdateMutationVariables,
- runnerToModel,
-} from '~/runner/runner_details/runner_update_form_utils';
+import { modelToUpdateMutationVariables, runnerToModel } from '~/runner/runner_update_form_utils';
const mockId = 'gid://gitlab/Ci::Runner/1';
const mockDescription = 'Runner Desc.';
@@ -23,7 +20,7 @@ const mockModel = {
tagList: 'tag-1, tag-2',
};
-describe('~/runner/runner_details/runner_update_form_utils', () => {
+describe('~/runner/runner_update_form_utils', () => {
describe('runnerToModel', () => {
it('collects all model data', () => {
expect(runnerToModel(mockRunner)).toEqual(mockModel);
diff --git a/spec/frontend/search/topbar/components/searchable_dropdown_spec.js b/spec/frontend/search/topbar/components/searchable_dropdown_spec.js
index b21cf5c6b79..de1cefa9e9d 100644
--- a/spec/frontend/search/topbar/components/searchable_dropdown_spec.js
+++ b/spec/frontend/search/topbar/components/searchable_dropdown_spec.js
@@ -133,6 +133,8 @@ describe('Global Search Searchable Dropdown', () => {
describe(`when search is ${searchText} and frequentItems length is ${frequentItems.length}`, () => {
beforeEach(() => {
createComponent({}, { frequentItems });
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({ searchText });
});
@@ -202,6 +204,8 @@ describe('Global Search Searchable Dropdown', () => {
describe('not for the first time', () => {
beforeEach(() => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({ hasBeenOpened: true });
findGlDropdown().vm.$emit('show');
});
diff --git a/spec/frontend/security_configuration/components/app_spec.js b/spec/frontend/security_configuration/components/app_spec.js
index 0a2b18caf25..cbdf7f53913 100644
--- a/spec/frontend/security_configuration/components/app_spec.js
+++ b/spec/frontend/security_configuration/components/app_spec.js
@@ -1,4 +1,4 @@
-import { GlTab } from '@gitlab/ui';
+import { GlTab, GlTabs } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import { nextTick } from 'vue';
import { useLocalStorageSpy } from 'helpers/local_storage_helper';
@@ -77,6 +77,7 @@ describe('App component', () => {
const findMainHeading = () => wrapper.find('h1');
const findTab = () => wrapper.findComponent(GlTab);
const findTabs = () => wrapper.findAllComponents(GlTab);
+ const findGlTabs = () => wrapper.findComponent(GlTabs);
const findByTestId = (id) => wrapper.findByTestId(id);
const findFeatureCards = () => wrapper.findAllComponents(FeatureCard);
const findTrainingProviderList = () => wrapper.findComponent(TrainingProviderList);
@@ -154,6 +155,14 @@ describe('App component', () => {
expect(findTab().exists()).toBe(true);
});
+ it('passes the `sync-active-tab-with-query-params` prop', () => {
+ expect(findGlTabs().props('syncActiveTabWithQueryParams')).toBe(true);
+ });
+
+ it('lazy loads each tab', () => {
+ expect(findGlTabs().attributes('lazy')).not.toBe(undefined);
+ });
+
it('renders correct amount of tabs', () => {
expect(findTabs()).toHaveLength(expectedTabs.length);
});
@@ -161,6 +170,10 @@ describe('App component', () => {
it.each(expectedTabs)('renders the %s tab', (tabName) => {
expect(findByTestId(`${tabName}-tab`).exists()).toBe(true);
});
+
+ it.each(expectedTabs)('has the %s query-param-value', (tabName) => {
+ expect(findByTestId(`${tabName}-tab`).props('queryParamValue')).toBe(tabName);
+ });
});
it('renders right amount of feature cards for given props with correct props', () => {
@@ -182,10 +195,6 @@ describe('App component', () => {
expect(findComplianceViewHistoryLink().exists()).toBe(false);
expect(findSecurityViewHistoryLink().exists()).toBe(false);
});
-
- it('renders TrainingProviderList component', () => {
- expect(findTrainingProviderList().exists()).toBe(true);
- });
});
describe('Manage via MR Error Alert', () => {
@@ -432,6 +441,25 @@ describe('App component', () => {
});
});
+ describe('Vulnerability management', () => {
+ beforeEach(() => {
+ createComponent({
+ augmentedSecurityFeatures: securityFeaturesMock,
+ augmentedComplianceFeatures: complianceFeaturesMock,
+ });
+ });
+
+ it('renders TrainingProviderList component', () => {
+ expect(findTrainingProviderList().exists()).toBe(true);
+ });
+
+ it('renders security training description', () => {
+ const vulnerabilityManagementTab = wrapper.findByTestId('vulnerability-management-tab');
+
+ expect(vulnerabilityManagementTab.text()).toContain(i18n.securityTrainingDescription);
+ });
+ });
+
describe('when secureVulnerabilityTraining feature flag is disabled', () => {
beforeEach(() => {
createComponent({
diff --git a/spec/frontend/security_configuration/components/training_provider_list_spec.js b/spec/frontend/security_configuration/components/training_provider_list_spec.js
index 60cc36a634c..578248e696f 100644
--- a/spec/frontend/security_configuration/components/training_provider_list_spec.js
+++ b/spec/frontend/security_configuration/components/training_provider_list_spec.js
@@ -1,87 +1,192 @@
-import { GlLink, GlToggle, GlCard, GlSkeletonLoader } from '@gitlab/ui';
+import { GlAlert, GlLink, GlToggle, GlCard, GlSkeletonLoader } 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 TrainingProviderList from '~/security_configuration/components/training_provider_list.vue';
+import configureSecurityTrainingProvidersMutation from '~/security_configuration/graphql/configure_security_training_providers.mutation.graphql';
import waitForPromises from 'helpers/wait_for_promises';
-import { securityTrainingProviders, mockResolvers } from '../mock_data';
+import {
+ securityTrainingProviders,
+ createMockResolvers,
+ testProjectPath,
+ textProviderIds,
+} from '../mock_data';
Vue.use(VueApollo);
describe('TrainingProviderList component', () => {
let wrapper;
- let mockApollo;
- let mockSecurityTrainingProvidersData;
+ let apolloProvider;
- const createComponent = () => {
- mockApollo = createMockApollo([], mockResolvers);
+ const createApolloProvider = ({ resolvers } = {}) => {
+ apolloProvider = createMockApollo([], createMockResolvers({ resolvers }));
+ };
+ const createComponent = () => {
wrapper = shallowMount(TrainingProviderList, {
- apolloProvider: mockApollo,
+ provide: {
+ projectPath: testProjectPath,
+ },
+ apolloProvider,
});
};
const waitForQueryToBeLoaded = () => waitForPromises();
+ const waitForMutationToBeLoaded = waitForQueryToBeLoaded;
const findCards = () => wrapper.findAllComponents(GlCard);
const findLinks = () => wrapper.findAllComponents(GlLink);
const findToggles = () => wrapper.findAllComponents(GlToggle);
+ const findFirstToggle = () => findToggles().at(0);
const findLoader = () => wrapper.findComponent(GlSkeletonLoader);
+ const findErrorAlert = () => wrapper.findComponent(GlAlert);
- beforeEach(() => {
- mockSecurityTrainingProvidersData = jest.fn();
- mockSecurityTrainingProvidersData.mockResolvedValue(securityTrainingProviders);
-
- createComponent();
- });
+ const toggleFirstProvider = () => findFirstToggle().vm.$emit('change');
afterEach(() => {
wrapper.destroy();
- mockApollo = null;
+ apolloProvider = null;
});
- describe('when loading', () => {
- it('shows the loader', () => {
- expect(findLoader().exists()).toBe(true);
+ describe('with a successful response', () => {
+ beforeEach(() => {
+ createApolloProvider();
+ createComponent();
});
- it('does not show the cards', () => {
- expect(findCards().exists()).toBe(false);
+ describe('when loading', () => {
+ it('shows the loader', () => {
+ expect(findLoader().exists()).toBe(true);
+ });
+
+ it('does not show the cards', () => {
+ expect(findCards().exists()).toBe(false);
+ });
});
- });
- describe('basic structure', () => {
- beforeEach(async () => {
- await waitForQueryToBeLoaded();
+ describe('basic structure', () => {
+ beforeEach(async () => {
+ await waitForQueryToBeLoaded();
+ });
+
+ it('renders correct amount of cards', () => {
+ expect(findCards()).toHaveLength(securityTrainingProviders.length);
+ });
+
+ securityTrainingProviders.forEach(({ name, description, url, isEnabled }, index) => {
+ it(`shows the name for card ${index}`, () => {
+ expect(findCards().at(index).text()).toContain(name);
+ });
+
+ it(`shows the description for card ${index}`, () => {
+ expect(findCards().at(index).text()).toContain(description);
+ });
+
+ it(`shows the learn more link for card ${index}`, () => {
+ expect(findLinks().at(index).attributes()).toEqual({
+ target: '_blank',
+ href: url,
+ });
+ });
+
+ it(`shows the toggle with the correct value for card ${index}`, () => {
+ expect(findToggles().at(index).props('value')).toEqual(isEnabled);
+ });
+
+ it('does not show loader when query is populated', () => {
+ expect(findLoader().exists()).toBe(false);
+ });
+ });
});
- it('renders correct amount of cards', () => {
- expect(findCards()).toHaveLength(securityTrainingProviders.length);
+ describe('storing training provider settings', () => {
+ beforeEach(async () => {
+ jest.spyOn(apolloProvider.defaultClient, 'mutate');
+
+ await waitForMutationToBeLoaded();
+
+ toggleFirstProvider();
+ });
+
+ it.each`
+ loading | wait | desc
+ ${true} | ${false} | ${'enables loading of GlToggle when mutation is called'}
+ ${false} | ${true} | ${'disables loading of GlToggle when mutation is complete'}
+ `('$desc', async ({ loading, wait }) => {
+ if (wait) {
+ await waitForMutationToBeLoaded();
+ }
+ expect(findFirstToggle().props('isLoading')).toBe(loading);
+ });
+
+ it('calls mutation when toggle is changed', () => {
+ expect(apolloProvider.defaultClient.mutate).toHaveBeenCalledWith(
+ expect.objectContaining({
+ mutation: configureSecurityTrainingProvidersMutation,
+ variables: { input: { enabledProviders: textProviderIds, fullPath: testProjectPath } },
+ }),
+ );
+ });
});
+ });
+
+ describe('with errors', () => {
+ const expectErrorAlertToExist = () => {
+ expect(findErrorAlert().props()).toMatchObject({
+ dismissible: false,
+ variant: 'danger',
+ });
+ };
+
+ describe('when fetching training providers', () => {
+ beforeEach(async () => {
+ createApolloProvider({
+ resolvers: {
+ Query: {
+ securityTrainingProviders: jest.fn().mockReturnValue(new Error()),
+ },
+ },
+ });
+ createComponent();
- securityTrainingProviders.forEach(({ name, description, url, isEnabled }, index) => {
- it(`shows the name for card ${index}`, () => {
- expect(findCards().at(index).text()).toContain(name);
+ await waitForQueryToBeLoaded();
});
- it(`shows the description for card ${index}`, () => {
- expect(findCards().at(index).text()).toContain(description);
+ it('shows an non-dismissible error alert', () => {
+ expectErrorAlertToExist();
});
- it(`shows the learn more link for card ${index}`, () => {
- expect(findLinks().at(index).attributes()).toEqual({
- target: '_blank',
- href: url,
+ it('shows an error description', () => {
+ expect(findErrorAlert().text()).toBe(TrainingProviderList.i18n.providerQueryErrorMessage);
+ });
+ });
+
+ describe('when storing training provider configurations', () => {
+ beforeEach(async () => {
+ createApolloProvider({
+ resolvers: {
+ Mutation: {
+ configureSecurityTrainingProviders: () => ({
+ errors: ['something went wrong!'],
+ securityTrainingProviders: [],
+ }),
+ },
+ },
});
+ createComponent();
+
+ await waitForQueryToBeLoaded();
+ toggleFirstProvider();
+ await waitForMutationToBeLoaded();
});
- it(`shows the toggle with the correct value for card ${index}`, () => {
- expect(findToggles().at(index).props('value')).toEqual(isEnabled);
+ it('shows an non-dismissible error alert', () => {
+ expectErrorAlertToExist();
});
- it('does not show loader when query is populated', () => {
- expect(findLoader().exists()).toBe(false);
+ it('shows an error description', () => {
+ expect(findErrorAlert().text()).toBe(TrainingProviderList.i18n.configMutationErrorMessage);
});
});
});
diff --git a/spec/frontend/security_configuration/mock_data.js b/spec/frontend/security_configuration/mock_data.js
index cdb859c3800..37ecce3886d 100644
--- a/spec/frontend/security_configuration/mock_data.js
+++ b/spec/frontend/security_configuration/mock_data.js
@@ -1,16 +1,20 @@
+export const testProjectPath = 'foo/bar';
+
+export const textProviderIds = [101, 102];
+
export const securityTrainingProviders = [
{
- id: 101,
- name: 'Kontra',
- description: 'Interactive developer security education.',
- url: 'https://application.security/',
+ id: textProviderIds[0],
+ name: 'Vendor Name 1',
+ description: 'Interactive developer security education',
+ url: 'https://www.example.org/security/training',
isEnabled: false,
},
{
- id: 102,
- name: 'SecureCodeWarrior',
+ id: textProviderIds[1],
+ name: 'Vendor Name 2',
description: 'Security training with guide and learning pathways.',
- url: 'https://www.securecodewarrior.com/',
+ url: 'https://www.vendornametwo.com/',
isEnabled: true,
},
];
@@ -21,10 +25,15 @@ export const securityTrainingProvidersResponse = {
},
};
-export const mockResolvers = {
+const defaultMockResolvers = {
Query: {
securityTrainingProviders() {
return securityTrainingProviders;
},
},
};
+
+export const createMockResolvers = ({ resolvers: customMockResolvers = {} } = {}) => ({
+ ...defaultMockResolvers,
+ ...customMockResolvers,
+});
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 c25a8d4bb92..350055cb935 100644
--- a/spec/frontend/serverless/components/__snapshots__/empty_state_spec.js.snap
+++ b/spec/frontend/serverless/components/__snapshots__/empty_state_spec.js.snap
@@ -1,12 +1,12 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`EmptyStateComponent should render content 1`] = `
-"<section class=\\"row empty-state text-center\\">
- <div class=\\"col-12\\">
+"<section class=\\"gl-display-flex empty-state gl-text-center gl-flex-direction-column\\">
+ <div class=\\"gl-max-w-full\\">
<div class=\\"svg-250 svg-content\\"><img 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\\">
+ <div class=\\"gl-max-w-full gl-m-auto\\">
+ <div class=\\"gl-mx-auto gl-my-0 gl-p-5\\">
<h1 class=\\"gl-font-size-h-display gl-line-height-36 h4\\">
Getting started with serverless
</h1>
diff --git a/spec/frontend/set_status_modal/set_status_modal_wrapper_spec.js b/spec/frontend/set_status_modal/set_status_modal_wrapper_spec.js
index d7261784edc..0c6ed998747 100644
--- a/spec/frontend/set_status_modal/set_status_modal_wrapper_spec.js
+++ b/spec/frontend/set_status_modal/set_status_modal_wrapper_spec.js
@@ -110,14 +110,23 @@ describe('SetStatusModalWrapper', () => {
});
describe('improvedEmojiPicker is true', () => {
+ const getEmojiPicker = () => wrapper.findComponent(EmojiPicker);
+
beforeEach(async () => {
await initEmojiMock();
wrapper = createComponent({}, true);
return initModal();
});
+ it('renders emoji picker dropdown with custom positioning', () => {
+ expect(getEmojiPicker().props()).toMatchObject({
+ right: false,
+ boundary: 'viewport',
+ });
+ });
+
it('sets emojiTag when clicking in emoji picker', async () => {
- await wrapper.findComponent(EmojiPicker).vm.$emit('click', 'thumbsup');
+ await getEmojiPicker().vm.$emit('click', 'thumbsup');
expect(wrapper.vm.emojiTag).toContain('data-name="thumbsup"');
});
diff --git a/spec/frontend/sidebar/components/reviewers/uncollapsed_reviewer_list_spec.js b/spec/frontend/sidebar/components/reviewers/uncollapsed_reviewer_list_spec.js
index 13887f28d22..d0792fa7b73 100644
--- a/spec/frontend/sidebar/components/reviewers/uncollapsed_reviewer_list_spec.js
+++ b/spec/frontend/sidebar/components/reviewers/uncollapsed_reviewer_list_spec.js
@@ -48,12 +48,16 @@ describe('UncollapsedReviewerList component', () => {
});
it('renders re-request loading icon', async () => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
await wrapper.setData({ loadingStates: { 1: 'loading' } });
expect(wrapper.find('[data-testid="re-request-button"]').props('loading')).toBe(true);
});
it('renders re-request success icon', async () => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
await wrapper.setData({ loadingStates: { 1: 'success' } });
expect(wrapper.find('[data-testid="re-request-success"]').exists()).toBe(true);
@@ -98,6 +102,8 @@ describe('UncollapsedReviewerList component', () => {
});
it('renders re-request loading icon', async () => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
await wrapper.setData({ loadingStates: { 2: 'loading' } });
expect(wrapper.findAll('[data-testid="re-request-button"]').length).toBe(2);
@@ -107,6 +113,8 @@ describe('UncollapsedReviewerList component', () => {
});
it('renders re-request success icon', async () => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
await wrapper.setData({ loadingStates: { 2: 'success' } });
expect(wrapper.findAll('[data-testid="re-request-button"]').length).toBe(1);
diff --git a/spec/frontend/sidebar/participants_spec.js b/spec/frontend/sidebar/participants_spec.js
index 1210f7c9531..94cdbe7f2ef 100644
--- a/spec/frontend/sidebar/participants_spec.js
+++ b/spec/frontend/sidebar/participants_spec.js
@@ -85,6 +85,8 @@ describe('Participants', () => {
numberOfLessParticipants,
});
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
isShowingMoreParticipants: false,
});
@@ -101,6 +103,8 @@ describe('Participants', () => {
numberOfLessParticipants: 2,
});
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
isShowingMoreParticipants: true,
});
@@ -129,6 +133,8 @@ describe('Participants', () => {
numberOfLessParticipants: 2,
});
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
isShowingMoreParticipants: false,
});
@@ -145,6 +151,8 @@ describe('Participants', () => {
numberOfLessParticipants: 2,
});
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
isShowingMoreParticipants: true,
});
diff --git a/spec/frontend/snippets/components/__snapshots__/snippet_description_edit_spec.js.snap b/spec/frontend/snippets/components/__snapshots__/snippet_description_edit_spec.js.snap
index 40bc6fe6aa5..c193bb08543 100644
--- a/spec/frontend/snippets/components/__snapshots__/snippet_description_edit_spec.js.snap
+++ b/spec/frontend/snippets/components/__snapshots__/snippet_description_edit_spec.js.snap
@@ -90,6 +90,8 @@ exports[`Snippet Description Edit component rendering matches the snapshot 1`] =
/>
<!---->
+
+ <!---->
</div>
</div>
</div>
diff --git a/spec/frontend/snippets/components/snippet_blob_view_spec.js b/spec/frontend/snippets/components/snippet_blob_view_spec.js
index b92c1907980..172089f9ee6 100644
--- a/spec/frontend/snippets/components/snippet_blob_view_spec.js
+++ b/spec/frontend/snippets/components/snippet_blob_view_spec.js
@@ -157,6 +157,8 @@ describe('Blob Embeddable', () => {
});
// mimic apollo's update
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
blobContent: wrapper.vm.onContentUpdate(apolloData),
});
diff --git a/spec/frontend/snippets/components/snippet_header_spec.js b/spec/frontend/snippets/components/snippet_header_spec.js
index 2d5e0cfd615..daa9d6345b0 100644
--- a/spec/frontend/snippets/components/snippet_header_spec.js
+++ b/spec/frontend/snippets/components/snippet_header_spec.js
@@ -242,6 +242,8 @@ describe('Snippet header component', () => {
// TODO: we should avoid `wrapper.setData` since they
// are component internals. Let's use the apollo mock helpers
// in a follow-up.
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({ canCreateSnippet: true });
await wrapper.vm.$nextTick();
diff --git a/spec/frontend/static_site_editor/components/edit_area_spec.js b/spec/frontend/static_site_editor/components/edit_area_spec.js
index 1d6245e9dbb..a833fd9ff9e 100644
--- a/spec/frontend/static_site_editor/components/edit_area_spec.js
+++ b/spec/frontend/static_site_editor/components/edit_area_spec.js
@@ -132,6 +132,8 @@ describe('~/static_site_editor/components/edit_area.vue', () => {
describe('when the mode changes', () => {
const setInitialMode = (mode) => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({ editorMode: mode });
};
@@ -207,6 +209,8 @@ describe('~/static_site_editor/components/edit_area.vue', () => {
});
it('syncs matter changes to content in markdown mode', async () => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({ editorMode: EDITOR_TYPES.markdown });
const newSettings = { title: 'test' };
diff --git a/spec/frontend/static_site_editor/rich_content_editor/modals/add_image/add_image_modal_spec.js b/spec/frontend/static_site_editor/rich_content_editor/modals/add_image/add_image_modal_spec.js
index 86ae016987d..c8c9f45618d 100644
--- a/spec/frontend/static_site_editor/rich_content_editor/modals/add_image/add_image_modal_spec.js
+++ b/spec/frontend/static_site_editor/rich_content_editor/modals/add_image/add_image_modal_spec.js
@@ -48,6 +48,8 @@ describe('Add Image Modal', () => {
const file = { name: 'some_file.png' };
wrapper.vm.$refs.uploadImageTab = { validateFile: jest.fn() };
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({ file, description, tabIndex: IMAGE_TABS.UPLOAD_TAB });
findModal().vm.$emit('ok', { preventDefault });
@@ -60,6 +62,8 @@ describe('Add Image Modal', () => {
it('emits an addImage event when a valid URL is specified', () => {
const preventDefault = jest.fn();
const mockImage = { imageUrl: '/some/valid/url.png', description: 'some description' };
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({ ...mockImage, tabIndex: IMAGE_TABS.URL_TAB });
findModal().vm.$emit('ok', { preventDefault });
diff --git a/spec/frontend/static_site_editor/rich_content_editor/services/renderers/render_identifier_paragraph_spec.js b/spec/frontend/static_site_editor/rich_content_editor/services/renderers/render_identifier_paragraph_spec.js
index 6a2b89a8dcf..ddc96ed6832 100644
--- a/spec/frontend/static_site_editor/rich_content_editor/services/renderers/render_identifier_paragraph_spec.js
+++ b/spec/frontend/static_site_editor/rich_content_editor/services/renderers/render_identifier_paragraph_spec.js
@@ -13,7 +13,7 @@ const normalParagraphNode = buildMockParagraphNode(
'This is just normal paragraph. It has multiple sentences.',
);
const identifierParagraphNode = buildMockParagraphNode(
- `[another-identifier]: https://example.com "This example has a title" [identifier]: http://example1.com [this link]: http://example2.com`,
+ `[another-identifier]: https://example.com "This example has a title" [identifier]: http://example1.com [this link]: http://example.org`,
);
describe('rich_content_editor/renderers_render_identifier_paragraph', () => {
diff --git a/spec/frontend/terraform/components/states_table_actions_spec.js b/spec/frontend/terraform/components/states_table_actions_spec.js
index 9d28e8ce294..fbe55306f37 100644
--- a/spec/frontend/terraform/components/states_table_actions_spec.js
+++ b/spec/frontend/terraform/components/states_table_actions_spec.js
@@ -293,6 +293,8 @@ describe('StatesTableActions', () => {
describe('when state name is present', () => {
beforeEach(async () => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
await wrapper.setData({ removeConfirmText: defaultProps.state.name });
findRemoveModal().vm.$emit('ok');
diff --git a/spec/frontend/tracking/tracking_initialization_spec.js b/spec/frontend/tracking/tracking_initialization_spec.js
index 2b70aacc4cb..f1628ad9793 100644
--- a/spec/frontend/tracking/tracking_initialization_spec.js
+++ b/spec/frontend/tracking/tracking_initialization_spec.js
@@ -81,7 +81,8 @@ describe('Tracking', () => {
it('should activate features based on what has been enabled', () => {
initDefaultTrackers();
expect(snowplowSpy).toHaveBeenCalledWith('enableActivityTracking', 30, 30);
- expect(snowplowSpy).toHaveBeenCalledWith('trackPageView', null, [standardContext]);
+ expect(snowplowSpy).toHaveBeenCalledWith('trackPageView', 'GitLab', [standardContext]);
+ expect(snowplowSpy).toHaveBeenCalledWith('setDocumentTitle', 'GitLab');
expect(snowplowSpy).not.toHaveBeenCalledWith('enableFormTracking');
expect(snowplowSpy).not.toHaveBeenCalledWith('enableLinkClickTracking');
@@ -130,7 +131,7 @@ describe('Tracking', () => {
it('includes those contexts alongside the standard context', () => {
initDefaultTrackers();
- expect(snowplowSpy).toHaveBeenCalledWith('trackPageView', null, [
+ expect(snowplowSpy).toHaveBeenCalledWith('trackPageView', 'GitLab', [
standardContext,
...experimentContexts,
]);
diff --git a/spec/frontend/version_check_image_spec.js b/spec/frontend/version_check_image_spec.js
deleted file mode 100644
index 13bd104a91c..00000000000
--- a/spec/frontend/version_check_image_spec.js
+++ /dev/null
@@ -1,42 +0,0 @@
-import $ from 'jquery';
-import ClassSpecHelper from 'helpers/class_spec_helper';
-import VersionCheckImage from '~/version_check_image';
-
-describe('VersionCheckImage', () => {
- let testContext;
-
- beforeEach(() => {
- testContext = {};
- });
-
- describe('bindErrorEvent', () => {
- ClassSpecHelper.itShouldBeAStaticMethod(VersionCheckImage, 'bindErrorEvent');
-
- beforeEach(() => {
- testContext.imageElement = $('<div></div>');
- });
-
- it('registers an error event', () => {
- jest.spyOn($.prototype, 'on').mockImplementation(() => {});
- // eslint-disable-next-line func-names
- jest.spyOn($.prototype, 'off').mockImplementation(function () {
- return this;
- });
-
- VersionCheckImage.bindErrorEvent(testContext.imageElement);
-
- expect($.prototype.off).toHaveBeenCalledWith('error');
- expect($.prototype.on).toHaveBeenCalledWith('error', expect.any(Function));
- });
-
- it('hides the imageElement on error', () => {
- jest.spyOn($.prototype, 'hide').mockImplementation(() => {});
-
- VersionCheckImage.bindErrorEvent(testContext.imageElement);
-
- testContext.imageElement.trigger('error');
-
- expect($.prototype.hide).toHaveBeenCalled();
- });
- });
-});
diff --git a/spec/frontend/vue_mr_widget/components/approvals/approvals_spec.js b/spec/frontend/vue_mr_widget/components/approvals/approvals_spec.js
index af6624a6c43..36850e623c7 100644
--- a/spec/frontend/vue_mr_widget/components/approvals/approvals_spec.js
+++ b/spec/frontend/vue_mr_widget/components/approvals/approvals_spec.js
@@ -101,6 +101,8 @@ describe('MRWidget approvals', () => {
});
it('shows loading message', () => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({ fetchingApprovals: true });
return tick().then(() => {
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 a09269e869c..5a1f17573d4 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
@@ -153,6 +153,8 @@ describe('MRWidgetHeader', () => {
gitpodEnabled: true,
showGitpodButton: true,
gitpodUrl: 'http://gitpod.localhost',
+ userPreferencesGitpodPath: '/-/profile/preferences#user_gitpod_enabled',
+ userProfileEnableGitpodPath: '/-/profile?user%5Bgitpod_enabled%5D=true',
};
it('renders checkout branch button with modal trigger', () => {
@@ -208,6 +210,8 @@ describe('MRWidgetHeader', () => {
gitpodEnabled: true,
showGitpodButton: true,
gitpodUrl: 'http://gitpod.localhost',
+ userPreferencesGitpodPath: mrDefaultOptions.userPreferencesGitpodPath,
+ userProfileEnableGitpodPath: mrDefaultOptions.userProfileEnableGitpodPath,
webIdeUrl,
});
});
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 d3221cc2fc7..27604868b3e 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
@@ -2,10 +2,15 @@ import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
import WidgetRebase from '~/vue_merge_request_widget/components/states/mr_widget_rebase.vue';
import eventHub from '~/vue_merge_request_widget/event_hub';
+import ActionsButton from '~/vue_shared/components/actions_button.vue';
+import {
+ REBASE_BUTTON_KEY,
+ REBASE_WITHOUT_CI_BUTTON_KEY,
+} from '~/vue_merge_request_widget/constants';
let wrapper;
-function factory(propsData, mergeRequestWidgetGraphql) {
+function createWrapper(propsData, mergeRequestWidgetGraphql, rebaseWithoutCiUi) {
wrapper = shallowMount(WidgetRebase, {
propsData,
data() {
@@ -19,7 +24,7 @@ function factory(propsData, mergeRequestWidgetGraphql) {
},
};
},
- provide: { glFeatures: { mergeRequestWidgetGraphql } },
+ provide: { glFeatures: { mergeRequestWidgetGraphql, rebaseWithoutCiUi } },
mocks: {
$apollo: {
queries: {
@@ -31,8 +36,10 @@ function factory(propsData, mergeRequestWidgetGraphql) {
}
describe('Merge request widget rebase component', () => {
- const findRebaseMessageEl = () => wrapper.find('[data-testid="rebase-message"]');
- const findRebaseMessageElText = () => findRebaseMessageEl().text();
+ const findRebaseMessage = () => wrapper.find('[data-testid="rebase-message"]');
+ const findRebaseMessageText = () => findRebaseMessage().text();
+ const findRebaseButtonActions = () => wrapper.find(ActionsButton);
+ const findStandardRebaseButton = () => wrapper.find('[data-testid="standard-rebase-button"]');
afterEach(() => {
wrapper.destroy();
@@ -40,10 +47,10 @@ describe('Merge request widget rebase component', () => {
});
[true, false].forEach((mergeRequestWidgetGraphql) => {
- describe(`widget graphql is ${mergeRequestWidgetGraphql ? 'enabled' : 'dislabed'}`, () => {
- describe('While rebasing', () => {
+ describe(`widget graphql is ${mergeRequestWidgetGraphql ? 'enabled' : 'disabled'}`, () => {
+ describe('while rebasing', () => {
it('should show progress message', () => {
- factory(
+ createWrapper(
{
mr: { rebaseInProgress: true },
service: {},
@@ -51,24 +58,30 @@ describe('Merge request widget rebase component', () => {
mergeRequestWidgetGraphql,
);
- expect(findRebaseMessageElText()).toContain('Rebase in progress');
+ expect(findRebaseMessageText()).toContain('Rebase in progress');
});
});
- describe('With permissions', () => {
- it('it should render rebase button and warning message', () => {
- factory(
+ describe('with permissions', () => {
+ const rebaseMock = jest.fn().mockResolvedValue();
+ const pollMock = jest.fn().mockResolvedValue({});
+
+ it('renders the warning message', () => {
+ createWrapper(
{
mr: {
rebaseInProgress: false,
canPushToSourceBranch: true,
},
- service: {},
+ service: {
+ rebase: rebaseMock,
+ poll: pollMock,
+ },
},
mergeRequestWidgetGraphql,
);
- const text = findRebaseMessageElText();
+ const text = findRebaseMessageText();
expect(text).toContain('Merge blocked');
expect(text.replace(/\s\s+/g, ' ')).toContain(
@@ -76,73 +89,195 @@ describe('Merge request widget rebase component', () => {
);
});
- it('it should render error message when it fails', async () => {
- factory(
+ it('renders an error message when rebasing has failed', async () => {
+ createWrapper(
{
mr: {
rebaseInProgress: false,
canPushToSourceBranch: true,
},
- service: {},
+ service: {
+ rebase: rebaseMock,
+ poll: pollMock,
+ },
},
mergeRequestWidgetGraphql,
);
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({ rebasingError: 'Something went wrong!' });
await nextTick();
- expect(findRebaseMessageElText()).toContain('Something went wrong!');
+ expect(findRebaseMessageText()).toContain('Something went wrong!');
+ });
+
+ describe('Rebase button with flag rebaseWithoutCiUi', () => {
+ beforeEach(() => {
+ createWrapper(
+ {
+ mr: {
+ rebaseInProgress: false,
+ canPushToSourceBranch: true,
+ },
+ service: {
+ rebase: rebaseMock,
+ poll: pollMock,
+ },
+ },
+ mergeRequestWidgetGraphql,
+ { rebaseWithoutCiUi: true },
+ );
+ });
+
+ it('rebase button with actions is rendered', () => {
+ expect(findRebaseButtonActions().exists()).toBe(true);
+ expect(findStandardRebaseButton().exists()).toBe(false);
+ });
+
+ it('has rebase and rebase without CI actions', () => {
+ const actionNames = findRebaseButtonActions()
+ .props('actions')
+ .map((action) => action.key);
+
+ expect(actionNames).toStrictEqual([REBASE_BUTTON_KEY, REBASE_WITHOUT_CI_BUTTON_KEY]);
+ });
+
+ it('defaults to rebase action', () => {
+ expect(findRebaseButtonActions().props('selectedKey')).toStrictEqual(REBASE_BUTTON_KEY);
+ });
+
+ it('starts the rebase when clicking', async () => {
+ // ActionButtons use the actions props instead of emitting
+ // a click event, therefore simulating the behavior here:
+ findRebaseButtonActions()
+ .props('actions')
+ .find((x) => x.key === REBASE_BUTTON_KEY)
+ .handle();
+
+ await nextTick();
+
+ expect(rebaseMock).toHaveBeenCalledWith({ skipCi: false });
+ });
+
+ it('starts the CI-skipping rebase when clicking on "Rebase without CI"', async () => {
+ // ActionButtons use the actions props instead of emitting
+ // a click event, therefore simulating the behavior here:
+ findRebaseButtonActions()
+ .props('actions')
+ .find((x) => x.key === REBASE_WITHOUT_CI_BUTTON_KEY)
+ .handle();
+
+ await nextTick();
+
+ expect(rebaseMock).toHaveBeenCalledWith({ skipCi: true });
+ });
+ });
+
+ describe('Rebase button with rebaseWithoutCiUI flag disabled', () => {
+ beforeEach(() => {
+ createWrapper(
+ {
+ mr: {
+ rebaseInProgress: false,
+ canPushToSourceBranch: true,
+ },
+ service: {
+ rebase: rebaseMock,
+ poll: pollMock,
+ },
+ },
+ mergeRequestWidgetGraphql,
+ );
+ });
+
+ it('standard rebase button is rendered', () => {
+ expect(findStandardRebaseButton().exists()).toBe(true);
+ expect(findRebaseButtonActions().exists()).toBe(false);
+ });
+
+ it('calls rebase method with skip_ci false', () => {
+ findStandardRebaseButton().vm.$emit('click');
+
+ expect(rebaseMock).toHaveBeenCalledWith({ skipCi: false });
+ });
});
});
- describe('Without permissions', () => {
- it('should render a message explaining user does not have permissions', () => {
- factory(
+ describe('without permissions', () => {
+ const exampleTargetBranch = 'fake-branch-to-test-with';
+
+ describe('UI text', () => {
+ beforeEach(() => {
+ createWrapper(
+ {
+ mr: {
+ rebaseInProgress: false,
+ canPushToSourceBranch: false,
+ targetBranch: exampleTargetBranch,
+ },
+ service: {},
+ },
+ mergeRequestWidgetGraphql,
+ );
+ });
+
+ it('renders a message explaining user does not have permissions', () => {
+ const text = findRebaseMessageText();
+
+ expect(text).toContain(
+ 'Merge blocked: the source branch must be rebased onto the target branch.',
+ );
+ expect(text).toContain('the source branch must be rebased');
+ });
+
+ it('renders the correct target branch name', () => {
+ const elem = findRebaseMessage();
+
+ expect(elem.text()).toContain(
+ 'Merge blocked: the source branch must be rebased onto the target branch.',
+ );
+ });
+ });
+
+ it('does not render the rebase actions button with rebaseWithoutCiUI flag enabled', () => {
+ createWrapper(
{
mr: {
rebaseInProgress: false,
canPushToSourceBranch: false,
- targetBranch: 'foo',
+ targetBranch: exampleTargetBranch,
},
service: {},
},
mergeRequestWidgetGraphql,
+ { rebaseWithoutCiUi: true },
);
- const text = findRebaseMessageElText();
-
- expect(text).toContain(
- 'Merge blocked: the source branch must be rebased onto the target branch.',
- );
- expect(text).toContain('the source branch must be rebased');
+ expect(findRebaseButtonActions().exists()).toBe(false);
});
- it('should render the correct target branch name', () => {
- const targetBranch = 'fake-branch-to-test-with';
- factory(
+ it('does not render the standard rebase button with rebaseWithoutCiUI flag disabled', () => {
+ createWrapper(
{
mr: {
rebaseInProgress: false,
canPushToSourceBranch: false,
- targetBranch,
+ targetBranch: exampleTargetBranch,
},
service: {},
},
mergeRequestWidgetGraphql,
);
- const elem = findRebaseMessageEl();
-
- expect(elem.text()).toContain(
- `Merge blocked: the source branch must be rebased onto the target branch.`,
- );
+ expect(findStandardRebaseButton().exists()).toBe(false);
});
});
describe('methods', () => {
it('checkRebaseStatus', async () => {
jest.spyOn(eventHub, '$emit').mockImplementation(() => {});
- factory(
+ createWrapper(
{
mr: {},
service: {
diff --git a/spec/frontend/vue_mr_widget/components/states/merge_checks_failed_spec.js b/spec/frontend/vue_mr_widget/components/states/merge_checks_failed_spec.js
index bdad0bada5f..1900b53ac11 100644
--- a/spec/frontend/vue_mr_widget/components/states/merge_checks_failed_spec.js
+++ b/spec/frontend/vue_mr_widget/components/states/merge_checks_failed_spec.js
@@ -15,35 +15,12 @@ describe('Merge request widget merge checks failed state component', () => {
});
it.each`
- mrState | displayText
- ${{ isPipelineFailed: true }} | ${'pipelineFailed'}
- ${{ approvals: true, isApproved: false }} | ${'approvalNeeded'}
- ${{ hasMergeableDiscussionsState: true }} | ${'unresolvedDiscussions'}
+ mrState | displayText
+ ${{ approvals: true, isApproved: false }} | ${'approvalNeeded'}
+ ${{ blockingMergeRequests: { total_count: 1 } }} | ${'blockingMergeRequests'}
`('display $displayText text for $mrState', ({ mrState, displayText }) => {
factory({ mr: mrState });
expect(wrapper.text()).toContain(MergeChecksFailed.i18n[displayText]);
});
-
- describe('unresolved discussions', () => {
- it('renders jump to button', () => {
- factory({ mr: { hasMergeableDiscussionsState: true } });
-
- expect(wrapper.find('[data-testid="jumpToUnresolved"]').exists()).toBe(true);
- });
-
- it('renders resolve thread button', () => {
- factory({
- mr: {
- hasMergeableDiscussionsState: true,
- createIssueToResolveDiscussionsPath: 'https://gitlab.com',
- },
- });
-
- expect(wrapper.find('[data-testid="resolveIssue"]').exists()).toBe(true);
- expect(wrapper.find('[data-testid="resolveIssue"]').attributes('href')).toBe(
- 'https://gitlab.com',
- );
- });
- });
});
diff --git a/spec/frontend/vue_mr_widget/components/states/mr_widget_auto_merge_enabled_spec.js b/spec/frontend/vue_mr_widget/components/states/mr_widget_auto_merge_enabled_spec.js
index d0a6af9970e..52a56af454f 100644
--- a/spec/frontend/vue_mr_widget/components/states/mr_widget_auto_merge_enabled_spec.js
+++ b/spec/frontend/vue_mr_widget/components/states/mr_widget_auto_merge_enabled_spec.js
@@ -253,6 +253,8 @@ describe('MRWidgetAutoMergeEnabled', () => {
factory({
...defaultMrProps(),
});
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
isCancellingAutoMerge: true,
});
@@ -287,6 +289,8 @@ describe('MRWidgetAutoMergeEnabled', () => {
factory({
...defaultMrProps(),
});
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
isRemovingSourceBranch: true,
});
diff --git a/spec/frontend/vue_mr_widget/components/states/mr_widget_commits_header_spec.js b/spec/frontend/vue_mr_widget/components/states/mr_widget_commits_header_spec.js
index 5858654e518..4d05e732f48 100644
--- a/spec/frontend/vue_mr_widget/components/states/mr_widget_commits_header_spec.js
+++ b/spec/frontend/vue_mr_widget/components/states/mr_widget_commits_header_spec.js
@@ -60,6 +60,8 @@ describe('Commits header component', () => {
it('has a chevron-right icon', () => {
createComponent();
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({ expanded: false });
return wrapper.vm.$nextTick().then(() => {
@@ -111,6 +113,8 @@ describe('Commits header component', () => {
describe('when expanded', () => {
beforeEach(() => {
createComponent();
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({ expanded: true });
});
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 89de160b02f..ec222e66a97 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
@@ -41,6 +41,8 @@ describe('MRWidgetConflicts', () => {
);
if (mergeRequestWidgetGraphql) {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
userPermissions: {
canMerge: propsData.mr.canMerge,
diff --git a/spec/frontend/vue_mr_widget/components/states/mr_widget_missing_branch_spec.js b/spec/frontend/vue_mr_widget/components/states/mr_widget_missing_branch_spec.js
index 848677bf4d2..936d673768c 100644
--- a/spec/frontend/vue_mr_widget/components/states/mr_widget_missing_branch_spec.js
+++ b/spec/frontend/vue_mr_widget/components/states/mr_widget_missing_branch_spec.js
@@ -14,6 +14,8 @@ function factory(sourceBranchRemoved, mergeRequestWidgetGraphql) {
});
if (mergeRequestWidgetGraphql) {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({ state: { sourceBranchExists: !sourceBranchRemoved } });
}
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 7082a19a8e7..f4ecebbb40c 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
@@ -192,6 +192,8 @@ describe('ReadyToMerge', () => {
it('should return "Merge in progress"', async () => {
createComponent();
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({ isMergingImmediately: true });
await Vue.nextTick();
@@ -260,6 +262,8 @@ describe('ReadyToMerge', () => {
it('should return true when the vm instance is making request', async () => {
createComponent({ mr: { isMergeAllowed: true } });
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({ isMakingRequest: true });
await Vue.nextTick();
@@ -287,6 +291,8 @@ describe('ReadyToMerge', () => {
jest
.spyOn(wrapper.vm.service, 'merge')
.mockReturnValue(returnPromise('merge_when_pipeline_succeeds'));
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({ removeSourceBranch: false });
wrapper.vm.handleMergeButtonClick(true);
@@ -691,6 +697,8 @@ describe('ReadyToMerge', () => {
true,
);
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
loading: false,
state: {
diff --git a/spec/frontend/vue_mr_widget/components/terraform/mock_data.js b/spec/frontend/vue_mr_widget/components/terraform/mock_data.js
index ae280146c22..8e46af5dfd6 100644
--- a/spec/frontend/vue_mr_widget/components/terraform/mock_data.js
+++ b/spec/frontend/vue_mr_widget/components/terraform/mock_data.js
@@ -1,6 +1,6 @@
export const invalidPlanWithName = {
job_name: 'Invalid Plan',
- job_path: '/path/to/ci/logs/1',
+ job_path: '/path/to/ci/logs/3',
tf_report_error: 'api_error',
};
@@ -20,12 +20,12 @@ export const validPlanWithoutName = {
create: 10,
update: 20,
delete: 30,
- job_path: '/path/to/ci/logs/1',
+ job_path: '/path/to/ci/logs/2',
};
export const plans = {
invalid_plan_one: invalidPlanWithName,
- invalid_plan_two: invalidPlanWithName,
+ invalid_plan_two: invalidPlanWithoutName,
valid_plan_one: validPlanWithName,
valid_plan_two: validPlanWithoutName,
};
diff --git a/spec/frontend/vue_mr_widget/components/terraform/mr_widget_terraform_container_spec.js b/spec/frontend/vue_mr_widget/components/terraform/mr_widget_terraform_container_spec.js
index 364f849eb4f..9048975875a 100644
--- a/spec/frontend/vue_mr_widget/components/terraform/mr_widget_terraform_container_spec.js
+++ b/spec/frontend/vue_mr_widget/components/terraform/mr_widget_terraform_container_spec.js
@@ -43,6 +43,8 @@ describe('MrWidgetTerraformConainer', () => {
mockPollingApi(200, plans, {});
return mountWrapper().then(() => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({ loading: true });
return wrapper.vm.$nextTick();
});
diff --git a/spec/frontend/vue_mr_widget/extentions/terraform/index_spec.js b/spec/frontend/vue_mr_widget/extentions/terraform/index_spec.js
new file mode 100644
index 00000000000..f8ea6fc23a2
--- /dev/null
+++ b/spec/frontend/vue_mr_widget/extentions/terraform/index_spec.js
@@ -0,0 +1,178 @@
+import MockAdapter from 'axios-mock-adapter';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import axios from '~/lib/utils/axios_utils';
+import Poll from '~/lib/utils/poll';
+import extensionsContainer from '~/vue_merge_request_widget/components/extensions/container';
+import { registerExtension } from '~/vue_merge_request_widget/components/extensions';
+import terraformExtension from '~/vue_merge_request_widget/extensions/terraform';
+import {
+ plans,
+ validPlanWithName,
+ validPlanWithoutName,
+ invalidPlanWithName,
+ invalidPlanWithoutName,
+} from '../../components/terraform/mock_data';
+
+describe('Terraform extension', () => {
+ let wrapper;
+ let mock;
+
+ const endpoint = '/path/to/terraform/report.json';
+ const successStatusCode = 200;
+ const errorStatusCode = 500;
+
+ const findListItem = (at) => wrapper.findAllByTestId('extension-list-item').at(at);
+
+ registerExtension(terraformExtension);
+
+ const mockPollingApi = (response, body, header) => {
+ mock.onGet(endpoint).reply(response, body, header);
+ };
+
+ const createComponent = () => {
+ wrapper = mountExtended(extensionsContainer, {
+ propsData: {
+ mr: {
+ terraformReportsPath: endpoint,
+ },
+ },
+ });
+ return axios.waitForAll();
+ };
+
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ mock.restore();
+ });
+
+ describe('summary', () => {
+ describe('while loading', () => {
+ const loadingText = 'Loading Terraform reports...';
+ it('should render loading text', async () => {
+ mockPollingApi(successStatusCode, plans, {});
+ createComponent();
+
+ expect(wrapper.text()).toContain(loadingText);
+ await waitForPromises();
+ expect(wrapper.text()).not.toContain(loadingText);
+ });
+ });
+
+ describe('when the fetching fails', () => {
+ beforeEach(() => {
+ mockPollingApi(errorStatusCode, null, {});
+ return createComponent();
+ });
+
+ it('should generate one invalid plan and render correct summary text', () => {
+ expect(wrapper.text()).toContain('1 Terraform report failed to generate');
+ });
+ });
+
+ describe('when the fetching succeeds', () => {
+ describe.each`
+ responseType | response | summaryTitle | summarySubtitle
+ ${'1 invalid report'} | ${{ 0: invalidPlanWithName }} | ${'1 Terraform report failed to generate'} | ${''}
+ ${'2 valid reports'} | ${{ 0: validPlanWithName, 1: validPlanWithName }} | ${'2 Terraform reports were generated in your pipelines'} | ${''}
+ ${'1 valid and 2 invalid reports'} | ${{ 0: validPlanWithName, 1: invalidPlanWithName, 2: invalidPlanWithName }} | ${'Terraform report was generated in your pipelines'} | ${'2 Terraform reports failed to generate'}
+ `('and received $responseType', ({ response, summaryTitle, summarySubtitle }) => {
+ beforeEach(async () => {
+ mockPollingApi(successStatusCode, response, {});
+ return createComponent();
+ });
+
+ it(`should render correct summary text`, () => {
+ expect(wrapper.text()).toContain(summaryTitle);
+
+ if (summarySubtitle) {
+ expect(wrapper.text()).toContain(summarySubtitle);
+ }
+ });
+ });
+ });
+ });
+
+ describe('expanded data', () => {
+ beforeEach(async () => {
+ mockPollingApi(successStatusCode, plans, {});
+ await createComponent();
+
+ wrapper.findByTestId('toggle-button').trigger('click');
+ });
+
+ describe.each`
+ reportType | title | subtitle | logLink | lineNumber
+ ${'a valid report with name'} | ${`The job ${validPlanWithName.job_name} generated a report.`} | ${`Reported Resource Changes: ${validPlanWithName.create} to add, ${validPlanWithName.update} to change, ${validPlanWithName.delete} to delete`} | ${validPlanWithName.job_path} | ${0}
+ ${'a valid report without name'} | ${'A Terraform report was generated in your pipelines.'} | ${`Reported Resource Changes: ${validPlanWithoutName.create} to add, ${validPlanWithoutName.update} to change, ${validPlanWithoutName.delete} to delete`} | ${validPlanWithoutName.job_path} | ${1}
+ ${'an invalid report with name'} | ${`The job ${invalidPlanWithName.job_name} failed to generate a report.`} | ${'Generating the report caused an error.'} | ${invalidPlanWithName.job_path} | ${2}
+ ${'an invalid report without name'} | ${'A Terraform report failed to generate.'} | ${'Generating the report caused an error.'} | ${invalidPlanWithoutName.job_path} | ${3}
+ `('renders correct text for $reportType', ({ title, subtitle, logLink, lineNumber }) => {
+ it('renders correct text', () => {
+ expect(findListItem(lineNumber).text()).toContain(title);
+ expect(findListItem(lineNumber).text()).toContain(subtitle);
+ });
+
+ it(`${logLink ? 'renders' : "doesn't render"} the log link`, () => {
+ const logText = 'Full log';
+ if (logLink) {
+ expect(
+ findListItem(lineNumber)
+ .find('[data-testid="extension-actions-button"]')
+ .attributes('href'),
+ ).toBe(logLink);
+ } else {
+ expect(findListItem(lineNumber).text()).not.toContain(logText);
+ }
+ });
+ });
+ });
+
+ describe('polling', () => {
+ let pollRequest;
+ let pollStop;
+
+ beforeEach(() => {
+ pollRequest = jest.spyOn(Poll.prototype, 'makeRequest');
+ pollStop = jest.spyOn(Poll.prototype, 'stop');
+ });
+
+ afterEach(() => {
+ pollRequest.mockRestore();
+ pollStop.mockRestore();
+ });
+
+ describe('successful poll', () => {
+ beforeEach(() => {
+ mockPollingApi(successStatusCode, plans, {});
+
+ return createComponent();
+ });
+
+ it('does not make additional requests after poll is successful', () => {
+ expect(pollRequest).toHaveBeenCalledTimes(1);
+ expect(pollStop).toHaveBeenCalledTimes(1);
+ });
+ });
+
+ describe('polling fails', () => {
+ beforeEach(() => {
+ mockPollingApi(errorStatusCode, null, {});
+ return createComponent();
+ });
+
+ it('generates one broken plan', () => {
+ expect(wrapper.text()).toContain('1 Terraform report failed to generate');
+ });
+
+ it('does not make additional requests after poll is unsuccessful', () => {
+ expect(pollRequest).toHaveBeenCalledTimes(1);
+ expect(pollStop).toHaveBeenCalledTimes(1);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/vue_mr_widget/mock_data.js b/spec/frontend/vue_mr_widget/mock_data.js
index 4538c1320d0..20d00a116bb 100644
--- a/spec/frontend/vue_mr_widget/mock_data.js
+++ b/spec/frontend/vue_mr_widget/mock_data.js
@@ -282,6 +282,8 @@ export default {
gitpod_enabled: true,
show_gitpod_button: true,
gitpod_url: 'http://gitpod.localhost',
+ user_preferences_gitpod_path: '/-/profile/preferences#user_gitpod_enabled',
+ user_profile_enable_gitpod_path: '/-/profile?user%5Bgitpod_enabled%5D=true',
};
export const mockStore = {
diff --git a/spec/frontend/vue_mr_widget/mr_widget_options_spec.js b/spec/frontend/vue_mr_widget/mr_widget_options_spec.js
index 8d41f6620ff..56c9bae0b76 100644
--- a/spec/frontend/vue_mr_widget/mr_widget_options_spec.js
+++ b/spec/frontend/vue_mr_widget/mr_widget_options_spec.js
@@ -9,6 +9,7 @@ import waitForPromises from 'helpers/wait_for_promises';
import { securityReportMergeRequestDownloadPathsQueryResponse } from 'jest/vue_shared/security_reports/mock_data';
import api from '~/api';
import axios from '~/lib/utils/axios_utils';
+import Poll from '~/lib/utils/poll';
import { setFaviconOverlay } from '~/lib/utils/favicon';
import notify from '~/lib/utils/notify';
import SmartInterval from '~/smart_interval';
@@ -28,6 +29,8 @@ import {
workingExtension,
collapsedDataErrorExtension,
fullDataErrorExtension,
+ pollingExtension,
+ pollingErrorExtension,
} from './test_extensions';
jest.mock('~/api.js');
@@ -897,13 +900,19 @@ describe('MrWidgetOptions', () => {
});
describe('mock extension', () => {
+ let pollRequest;
+
beforeEach(() => {
+ pollRequest = jest.spyOn(Poll.prototype, 'makeRequest');
+
registerExtension(workingExtension);
createComponent();
});
afterEach(() => {
+ pollRequest.mockRestore();
+
registeredExtensions.extensions = [];
});
@@ -957,6 +966,66 @@ describe('MrWidgetOptions', () => {
expect(collapsedSection.find(GlButton).exists()).toBe(true);
expect(collapsedSection.find(GlButton).text()).toBe('Full report');
});
+
+ it('extension polling is not called if enablePolling flag is not passed', () => {
+ // called one time due to parent component polling (mount)
+ expect(pollRequest).toHaveBeenCalledTimes(1);
+ });
+ });
+
+ describe('mock polling extension', () => {
+ let pollRequest;
+ let pollStop;
+
+ beforeEach(() => {
+ pollRequest = jest.spyOn(Poll.prototype, 'makeRequest');
+ pollStop = jest.spyOn(Poll.prototype, 'stop');
+ });
+
+ afterEach(() => {
+ pollRequest.mockRestore();
+ pollStop.mockRestore();
+
+ registeredExtensions.extensions = [];
+ });
+
+ describe('success', () => {
+ beforeEach(() => {
+ registerExtension(pollingExtension);
+
+ createComponent();
+ });
+
+ it('does not make additional requests after poll is successful', () => {
+ // called two times due to parent component polling (mount) and extension polling
+ expect(pollRequest).toHaveBeenCalledTimes(2);
+ expect(pollStop).toHaveBeenCalledTimes(1);
+ });
+ });
+
+ describe('error', () => {
+ let captureException;
+
+ beforeEach(() => {
+ captureException = jest.spyOn(Sentry, 'captureException');
+
+ registerExtension(pollingErrorExtension);
+
+ createComponent();
+ });
+
+ it('does not make additional requests after poll has failed', () => {
+ // called two times due to parent component polling (mount) and extension polling
+ expect(pollRequest).toHaveBeenCalledTimes(2);
+ expect(pollStop).toHaveBeenCalledTimes(1);
+ });
+
+ it('captures sentry error and displays error when poll has failed', () => {
+ expect(captureException).toHaveBeenCalledTimes(1);
+ expect(captureException).toHaveBeenCalledWith(new Error('Fetch error'));
+ expect(wrapper.findComponent(StatusIcon).props('iconName')).toBe('error');
+ });
+ });
});
describe('mock extension errors', () => {
diff --git a/spec/frontend/vue_mr_widget/stores/mr_widget_store_spec.js b/spec/frontend/vue_mr_widget/stores/mr_widget_store_spec.js
index 6eb68a1b00d..3cdb4265ef0 100644
--- a/spec/frontend/vue_mr_widget/stores/mr_widget_store_spec.js
+++ b/spec/frontend/vue_mr_widget/stores/mr_widget_store_spec.js
@@ -15,6 +15,8 @@ describe('MergeRequestStore', () => {
gitpodEnabled: mockData.gitpod_enabled,
showGitpodButton: mockData.show_gitpod_button,
gitpodUrl: mockData.gitpod_url,
+ userPreferencesGitpodPath: mockData.user_preferences_gitpod_path,
+ userProfileEnableGitpodPath: mockData.user_profile_enable_gitpod_path,
});
});
diff --git a/spec/frontend/vue_mr_widget/test_extensions.js b/spec/frontend/vue_mr_widget/test_extensions.js
index c7ff02ab726..986c1d6545a 100644
--- a/spec/frontend/vue_mr_widget/test_extensions.js
+++ b/spec/frontend/vue_mr_widget/test_extensions.js
@@ -97,3 +97,13 @@ export const fullDataErrorExtension = {
},
},
};
+
+export const pollingExtension = {
+ ...workingExtension,
+ enablePolling: true,
+};
+
+export const pollingErrorExtension = {
+ ...collapsedDataErrorExtension,
+ enablePolling: true,
+};
diff --git a/spec/frontend/vue_shared/alert_details/alert_details_spec.js b/spec/frontend/vue_shared/alert_details/alert_details_spec.js
index 1fc655f1ebc..221beed744b 100644
--- a/spec/frontend/vue_shared/alert_details/alert_details_spec.js
+++ b/spec/frontend/vue_shared/alert_details/alert_details_spec.js
@@ -349,6 +349,8 @@ describe('AlertDetails', () => {
${1} | ${'metrics'}
${2} | ${'activity'}
`('will navigate to the correct tab via $tabId', ({ index, tabId }) => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({ currentTabIndex: index });
expect($router.replace).toHaveBeenCalledWith({ name: 'tab', params: { tabId } });
});
diff --git a/spec/frontend/vue_shared/alert_details/sidebar/alert_sidebar_assignees_spec.js b/spec/frontend/vue_shared/alert_details/sidebar/alert_sidebar_assignees_spec.js
index 9ae45071f45..29e0eee2c9a 100644
--- a/spec/frontend/vue_shared/alert_details/sidebar/alert_sidebar_assignees_spec.js
+++ b/spec/frontend/vue_shared/alert_details/sidebar/alert_sidebar_assignees_spec.js
@@ -109,6 +109,8 @@ describe('Alert Details Sidebar Assignees', () => {
});
it('renders a unassigned option', async () => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({ isDropdownSearching: false });
await wrapper.vm.$nextTick();
expect(findDropdown().text()).toBe('Unassigned');
@@ -120,6 +122,8 @@ describe('Alert Details Sidebar Assignees', () => {
it('calls `$apollo.mutate` with `AlertSetAssignees` mutation and variables containing `iid`, `assigneeUsernames`, & `projectPath`', async () => {
jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue(mockUpdatedMutationResult);
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({ isDropdownSearching: false });
await wrapper.vm.$nextTick();
@@ -136,6 +140,8 @@ describe('Alert Details Sidebar Assignees', () => {
});
it('emits an error when request contains error messages', () => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({ isDropdownSearching: false });
const errorMutationResult = {
data: {
diff --git a/spec/frontend/vue_shared/components/chronic_duration_input_spec.js b/spec/frontend/vue_shared/components/chronic_duration_input_spec.js
index 530d01402c6..083a5f60d1d 100644
--- a/spec/frontend/vue_shared/components/chronic_duration_input_spec.js
+++ b/spec/frontend/vue_shared/components/chronic_duration_input_spec.js
@@ -315,6 +315,8 @@ describe('vue_shared/components/chronic_duration_input', () => {
});
it('passes updated prop via v-model', async () => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({ value: MOCK_VALUE });
await wrapper.vm.$nextTick();
diff --git a/spec/frontend/vue_shared/components/clipboard_button_spec.js b/spec/frontend/vue_shared/components/clipboard_button_spec.js
index 33445923a49..fca5e664a96 100644
--- a/spec/frontend/vue_shared/components/clipboard_button_spec.js
+++ b/spec/frontend/vue_shared/components/clipboard_button_spec.js
@@ -1,8 +1,16 @@
import { GlButton } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
-import initCopyToClipboard from '~/behaviors/copy_to_clipboard';
+import { nextTick } from 'vue';
+
+import initCopyToClipboard, {
+ CLIPBOARD_SUCCESS_EVENT,
+ CLIPBOARD_ERROR_EVENT,
+ I18N_ERROR_MESSAGE,
+} from '~/behaviors/copy_to_clipboard';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
+jest.mock('lodash/uniqueId', () => (prefix) => (prefix ? `${prefix}1` : 1));
+
describe('clipboard button', () => {
let wrapper;
@@ -15,6 +23,42 @@ describe('clipboard button', () => {
const findButton = () => wrapper.find(GlButton);
+ const expectConfirmationTooltip = async ({ event, message }) => {
+ const title = 'Copy this value';
+
+ createWrapper({
+ text: 'copy me',
+ title,
+ });
+
+ wrapper.vm.$root.$emit = jest.fn();
+
+ const button = findButton();
+
+ expect(button.attributes()).toMatchObject({
+ title,
+ 'aria-label': title,
+ });
+
+ await button.trigger(event);
+
+ expect(wrapper.vm.$root.$emit).toHaveBeenCalledWith('bv::show::tooltip', 'clipboard-button-1');
+
+ expect(button.attributes()).toMatchObject({
+ title: message,
+ 'aria-label': message,
+ });
+
+ jest.runAllTimers();
+ await nextTick();
+
+ expect(button.attributes()).toMatchObject({
+ title,
+ 'aria-label': title,
+ });
+ expect(wrapper.vm.$root.$emit).toHaveBeenCalledWith('bv::hide::tooltip', 'clipboard-button-1');
+ };
+
afterEach(() => {
wrapper.destroy();
wrapper = null;
@@ -99,6 +143,32 @@ describe('clipboard button', () => {
expect(findButton().props('variant')).toBe(variant);
});
+ describe('confirmation tooltip', () => {
+ it('adds `id` and `data-clipboard-handle-tooltip` attributes to button', () => {
+ createWrapper({
+ text: 'copy me',
+ title: 'Copy this value',
+ });
+
+ expect(findButton().attributes()).toMatchObject({
+ id: 'clipboard-button-1',
+ 'data-clipboard-handle-tooltip': 'false',
+ 'aria-live': 'polite',
+ });
+ });
+
+ it('shows success tooltip after successful copy', () => {
+ expectConfirmationTooltip({
+ event: CLIPBOARD_SUCCESS_EVENT,
+ message: ClipboardButton.i18n.copied,
+ });
+ });
+
+ it('shows error tooltip after failed copy', () => {
+ expectConfirmationTooltip({ event: CLIPBOARD_ERROR_EVENT, message: I18N_ERROR_MESSAGE });
+ });
+ });
+
describe('integration', () => {
it('actually copies to clipboard', () => {
initCopyToClipboard();
diff --git a/spec/frontend/vue_shared/components/confirm_danger/confirm_danger_spec.js b/spec/frontend/vue_shared/components/confirm_danger/confirm_danger_spec.js
index af7f85769aa..a179afccae0 100644
--- a/spec/frontend/vue_shared/components/confirm_danger/confirm_danger_spec.js
+++ b/spec/frontend/vue_shared/components/confirm_danger/confirm_danger_spec.js
@@ -10,6 +10,7 @@ describe('Confirm Danger Modal', () => {
const phrase = 'En Taro Adun';
const buttonText = 'Click me!';
const buttonClass = 'gl-w-full';
+ const buttonVariant = 'info';
const modalId = CONFIRM_DANGER_MODAL_ID;
const findBtn = () => wrapper.findComponent(GlButton);
@@ -21,6 +22,7 @@ describe('Confirm Danger Modal', () => {
propsData: {
buttonText,
buttonClass,
+ buttonVariant,
phrase,
...props,
},
@@ -57,6 +59,10 @@ describe('Confirm Danger Modal', () => {
expect(findBtn().classes()).toContain(buttonClass);
});
+ it('passes `buttonVariant` prop to button', () => {
+ expect(findBtn().attributes('variant')).toBe(buttonVariant);
+ });
+
it('will emit `confirm` when the modal confirms', () => {
expect(wrapper.emitted('confirm')).toBeUndefined();
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_bar_root_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_bar_root_spec.js
index 64d15884333..4e9eac2dde2 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_bar_root_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_bar_root_spec.js
@@ -122,6 +122,8 @@ describe('FilteredSearchBarRoot', () => {
describe('sortDirectionIcon', () => {
it('returns string "sort-lowest" when `selectedSortDirection` is "ascending"', () => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
selectedSortDirection: SortDirection.ascending,
});
@@ -130,6 +132,8 @@ describe('FilteredSearchBarRoot', () => {
});
it('returns string "sort-highest" when `selectedSortDirection` is "descending"', () => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
selectedSortDirection: SortDirection.descending,
});
@@ -140,6 +144,8 @@ describe('FilteredSearchBarRoot', () => {
describe('sortDirectionTooltip', () => {
it('returns string "Sort direction: Ascending" when `selectedSortDirection` is "ascending"', () => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
selectedSortDirection: SortDirection.ascending,
});
@@ -148,6 +154,8 @@ describe('FilteredSearchBarRoot', () => {
});
it('returns string "Sort direction: Descending" when `selectedSortDirection` is "descending"', () => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
selectedSortDirection: SortDirection.descending,
});
@@ -158,6 +166,8 @@ describe('FilteredSearchBarRoot', () => {
describe('filteredRecentSearches', () => {
it('returns array of recent searches filtering out any string type (unsupported) items', async () => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
recentSearches: [{ foo: 'bar' }, 'foo'],
});
@@ -169,6 +179,8 @@ describe('FilteredSearchBarRoot', () => {
});
it('returns array of recent searches sanitizing any duplicate token values', async () => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
recentSearches: [
[tokenValueAuthor, tokenValueLabel, tokenValueMilestone, tokenValueLabel],
@@ -198,6 +210,8 @@ describe('FilteredSearchBarRoot', () => {
describe('filterValue', () => {
it('emits component event `onFilter` with empty array and false when filter was never selected', () => {
wrapper = createComponent({ initialFilterValue: [tokenValueEmpty] });
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
initialRender: false,
filterValue: [tokenValueEmpty],
@@ -210,6 +224,8 @@ describe('FilteredSearchBarRoot', () => {
it('emits component event `onFilter` with empty array and true when initially selected filter value was cleared', () => {
wrapper = createComponent({ initialFilterValue: [tokenValueLabel] });
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
initialRender: false,
filterValue: [tokenValueEmpty],
@@ -264,6 +280,8 @@ describe('FilteredSearchBarRoot', () => {
describe('handleSortDirectionClick', () => {
beforeEach(() => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
selectedSortOption: mockSortOptions[0],
});
@@ -312,6 +330,8 @@ describe('FilteredSearchBarRoot', () => {
const mockFilters = [tokenValueAuthor, 'foo'];
beforeEach(async () => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
filterValue: mockFilters,
});
@@ -376,6 +396,8 @@ describe('FilteredSearchBarRoot', () => {
describe('template', () => {
beforeEach(() => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
selectedSortOption: mockSortOptions[0],
selectedSortDirection: SortDirection.descending,
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 b29c394e7ae..5865c6a41b8 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
@@ -10,10 +10,7 @@ import waitForPromises from 'helpers/wait_for_promises';
import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
-import {
- DEFAULT_LABEL_ANY,
- DEFAULT_NONE_ANY,
-} from '~/vue_shared/components/filtered_search_bar/constants';
+import { DEFAULT_NONE_ANY } from '~/vue_shared/components/filtered_search_bar/constants';
import AuthorToken from '~/vue_shared/components/filtered_search_bar/tokens/author_token.vue';
import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_token.vue';
@@ -227,6 +224,8 @@ describe('AuthorToken', () => {
expect(getAvatarEl().props('src')).toBe(mockAuthors[0].avatar_url);
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
authors: [
{
@@ -274,7 +273,7 @@ describe('AuthorToken', () => {
expect(wrapper.find(GlDropdownDivider).exists()).toBe(false);
});
- it('renders `DEFAULT_LABEL_ANY` as default suggestions', async () => {
+ it('renders `DEFAULT_NONE_ANY` as default suggestions', async () => {
wrapper = createComponent({
active: true,
config: { ...mockAuthorToken, preloadedAuthors: mockPreloadedAuthors },
@@ -285,8 +284,9 @@ describe('AuthorToken', () => {
const suggestions = wrapper.findAll(GlFilteredSearchSuggestion);
- expect(suggestions).toHaveLength(1 + currentUserLength);
- expect(suggestions.at(0).text()).toBe(DEFAULT_LABEL_ANY.text);
+ expect(suggestions).toHaveLength(2 + currentUserLength);
+ expect(suggestions.at(0).text()).toBe(DEFAULT_NONE_ANY[0].text);
+ expect(suggestions.at(1).text()).toBe(DEFAULT_NONE_ANY[1].text);
});
it('emits listeners in the base-token', () => {
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/branch_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/branch_token_spec.js
index f3e8b2d0c1b..cd8be765fb5 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/branch_token_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/branch_token_spec.js
@@ -121,6 +121,8 @@ describe('BranchToken', () => {
beforeEach(async () => {
wrapper = createComponent({ value: { data: mockBranches[0].name } });
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
branches: mockBranches,
});
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/emoji_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/emoji_token_spec.js
index 36071c900df..ed9ac7c271e 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/emoji_token_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/emoji_token_spec.js
@@ -123,6 +123,8 @@ describe('EmojiToken', () => {
value: { data: `"${mockEmojis[0].name}"` },
});
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
emojis: mockEmojis,
});
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 f55fb2836e3..b9af71ad8a7 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
@@ -144,6 +144,8 @@ describe('LabelToken', () => {
beforeEach(async () => {
wrapper = createComponent({ value: { data: `"${mockRegularLabel.title}"` } });
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
labels: mockLabels,
});
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 4a098db33c5..c0d8b5fd139 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
@@ -121,6 +121,8 @@ describe('MilestoneToken', () => {
beforeEach(async () => {
wrapper = createComponent({ value: { data: `"${mockRegularMilestone.title}"` } });
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
milestones: mockMilestones,
});
diff --git a/spec/frontend/vue_shared/components/gitlab_version_check_spec.js b/spec/frontend/vue_shared/components/gitlab_version_check_spec.js
new file mode 100644
index 00000000000..b673e5407d4
--- /dev/null
+++ b/spec/frontend/vue_shared/components/gitlab_version_check_spec.js
@@ -0,0 +1,77 @@
+import { GlBadge } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import MockAdapter from 'axios-mock-adapter';
+import flushPromises from 'helpers/flush_promises';
+import axios from '~/lib/utils/axios_utils';
+import GitlabVersionCheck from '~/vue_shared/components/gitlab_version_check.vue';
+
+describe('GitlabVersionCheck', () => {
+ let wrapper;
+ let mock;
+
+ const defaultResponse = {
+ code: 200,
+ res: { severity: 'success' },
+ };
+
+ const createComponent = (mockResponse) => {
+ const response = {
+ ...defaultResponse,
+ ...mockResponse,
+ };
+
+ mock = new MockAdapter(axios);
+ mock.onGet().replyOnce(response.code, response.res);
+
+ wrapper = shallowMount(GitlabVersionCheck);
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ mock.restore();
+ });
+
+ const findGlBadge = () => wrapper.findComponent(GlBadge);
+
+ describe('template', () => {
+ describe.each`
+ description | mockResponse | renders
+ ${'successful but null'} | ${{ code: 200, res: null }} | ${false}
+ ${'successful and valid'} | ${{ code: 200, res: { severity: 'success' } }} | ${true}
+ ${'an error'} | ${{ code: 500, res: null }} | ${false}
+ `('version_check.json response', ({ description, mockResponse, renders }) => {
+ describe(`is ${description}`, () => {
+ beforeEach(async () => {
+ createComponent(mockResponse);
+ await flushPromises(); // Ensure we wrap up the axios call
+ });
+
+ it(`does${renders ? '' : ' not'} render GlBadge`, () => {
+ expect(findGlBadge().exists()).toBe(renders);
+ });
+ });
+ });
+
+ describe.each`
+ mockResponse | expectedUI
+ ${{ code: 200, res: { severity: 'success' } }} | ${{ title: 'Up to date', variant: 'success' }}
+ ${{ code: 200, res: { severity: 'warning' } }} | ${{ title: 'Update available', variant: 'warning' }}
+ ${{ code: 200, res: { severity: 'danger' } }} | ${{ title: 'Update ASAP', variant: 'danger' }}
+ `('badge ui', ({ mockResponse, expectedUI }) => {
+ describe(`when response is ${mockResponse.res.severity}`, () => {
+ beforeEach(async () => {
+ createComponent(mockResponse);
+ await flushPromises(); // Ensure we wrap up the axios call
+ });
+
+ it(`title is ${expectedUI.title}`, () => {
+ expect(findGlBadge().text()).toBe(expectedUI.title);
+ });
+
+ it(`variant is ${expectedUI.variant}`, () => {
+ expect(findGlBadge().attributes('variant')).toBe(expectedUI.variant);
+ });
+ });
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/line_numbers_spec.js b/spec/frontend/vue_shared/components/line_numbers_spec.js
index 5bedd0ccd02..38c26226863 100644
--- a/spec/frontend/vue_shared/components/line_numbers_spec.js
+++ b/spec/frontend/vue_shared/components/line_numbers_spec.js
@@ -13,7 +13,6 @@ describe('Line Numbers component', () => {
const findGlIcon = () => wrapper.findComponent(GlIcon);
const findLineNumbers = () => wrapper.findAllComponents(GlLink);
const findFirstLineNumber = () => findLineNumbers().at(0);
- const findSecondLineNumber = () => findLineNumbers().at(1);
beforeEach(() => createComponent());
@@ -24,7 +23,7 @@ describe('Line Numbers component', () => {
expect(findLineNumbers().length).toBe(lines);
expect(findFirstLineNumber().attributes()).toMatchObject({
id: 'L1',
- href: '#L1',
+ to: '#LC1',
});
});
@@ -35,37 +34,4 @@ describe('Line Numbers component', () => {
});
});
});
-
- describe('clicking a line number', () => {
- let firstLineNumber;
- let firstLineNumberElement;
-
- beforeEach(() => {
- firstLineNumber = findFirstLineNumber();
- firstLineNumberElement = firstLineNumber.element;
-
- jest.spyOn(firstLineNumberElement, 'scrollIntoView');
- jest.spyOn(firstLineNumberElement.classList, 'add');
- jest.spyOn(firstLineNumberElement.classList, 'remove');
-
- firstLineNumber.vm.$emit('click');
- });
-
- it('adds the highlight (hll) class', () => {
- expect(firstLineNumberElement.classList.add).toHaveBeenCalledWith('hll');
- });
-
- it('removes the highlight (hll) class from a previously highlighted line', () => {
- findSecondLineNumber().vm.$emit('click');
-
- expect(firstLineNumberElement.classList.remove).toHaveBeenCalledWith('hll');
- });
-
- it('scrolls the line into view', () => {
- expect(firstLineNumberElement.scrollIntoView).toHaveBeenCalledWith({
- behavior: 'smooth',
- block: 'center',
- });
- });
- });
});
diff --git a/spec/frontend/vue_shared/components/markdown/field_spec.js b/spec/frontend/vue_shared/components/markdown/field_spec.js
index 76e1a1162ad..0d90ca7f1f6 100644
--- a/spec/frontend/vue_shared/components/markdown/field_spec.js
+++ b/spec/frontend/vue_shared/components/markdown/field_spec.js
@@ -1,4 +1,5 @@
import { mount } from '@vue/test-utils';
+import { nextTick } from 'vue';
import AxiosMockAdapter from 'axios-mock-adapter';
import $ from 'jquery';
import { TEST_HOST, FIXTURES_PATH } from 'spec/test_constants';
@@ -242,6 +243,41 @@ describe('Markdown field component', () => {
expect(dropzoneSpy).toHaveBeenCalled();
});
+
+ describe('mentioning all users', () => {
+ const users = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11].map((i) => `user_${i}`);
+
+ it('shows warning on mention of all users', async () => {
+ axiosMock.onPost(markdownPreviewPath).reply(200, { references: { users } });
+
+ subject.setProps({ textareaValue: 'hello @all' });
+
+ await axios.waitFor(markdownPreviewPath).then(() => {
+ expect(subject.text()).toContain(
+ 'You are about to add 11 people to the discussion. They will all receive a notification.',
+ );
+ });
+ });
+
+ it('removes warning when all mention is removed', async () => {
+ axiosMock.onPost(markdownPreviewPath).reply(200, { references: { users } });
+
+ subject.setProps({ textareaValue: 'hello @all' });
+
+ await axios.waitFor(markdownPreviewPath);
+
+ jest.spyOn(axios, 'post');
+
+ subject.setProps({ textareaValue: 'hello @allan' });
+
+ await nextTick();
+
+ expect(axios.post).not.toHaveBeenCalled();
+ expect(subject.text()).not.toContain(
+ 'You are about to add 11 people to the discussion. They will all receive a notification.',
+ );
+ });
+ });
});
});
diff --git a/spec/frontend/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs_spec.js b/spec/frontend/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs_spec.js
index acf97713885..b330b4f5657 100644
--- a/spec/frontend/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs_spec.js
+++ b/spec/frontend/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs_spec.js
@@ -313,6 +313,8 @@ describe('AlertManagementEmptyState', () => {
it('returns correctly applied filter search values', async () => {
const searchTerm = 'foo';
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
searchTerm,
});
@@ -330,6 +332,8 @@ describe('AlertManagementEmptyState', () => {
});
it('updates props `searchTerm` and `authorUsername` with empty values when passed filters param is empty', () => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
authorUsername: 'foo',
searchTerm: 'bar',
diff --git a/spec/frontend/vue_shared/components/registry/__snapshots__/code_instruction_spec.js.snap b/spec/frontend/vue_shared/components/registry/__snapshots__/code_instruction_spec.js.snap
index 23cf6ef9785..e8d76991b90 100644
--- a/spec/frontend/vue_shared/components/registry/__snapshots__/code_instruction_spec.js.snap
+++ b/spec/frontend/vue_shared/components/registry/__snapshots__/code_instruction_spec.js.snap
@@ -3,7 +3,7 @@
exports[`Package code instruction multiline to match the snapshot 1`] = `
<div>
<label
- for="instruction-input_3"
+ for="instruction-input_1"
>
foo_label
</label>
@@ -23,7 +23,7 @@ multiline text
exports[`Package code instruction single line to match the default snapshot 1`] = `
<div>
<label
- for="instruction-input_2"
+ for="instruction-input_1"
>
foo_label
</label>
@@ -37,7 +37,7 @@ exports[`Package code instruction single line to match the default snapshot 1`]
<input
class="form-control gl-font-monospace"
data-testid="instruction-input"
- id="instruction-input_2"
+ id="instruction-input_1"
readonly="readonly"
type="text"
/>
@@ -47,9 +47,12 @@ exports[`Package code instruction single line to match the default snapshot 1`]
data-testid="instruction-button"
>
<button
- aria-label="Copy this value"
+ aria-label="Copy npm install command"
+ aria-live="polite"
class="btn input-group-text btn-default btn-md gl-button btn-default-secondary btn-icon"
+ data-clipboard-handle-tooltip="false"
data-clipboard-text="npm i @my-package"
+ id="clipboard-button-1"
title="Copy npm install command"
type="button"
>
diff --git a/spec/frontend/vue_shared/components/registry/code_instruction_spec.js b/spec/frontend/vue_shared/components/registry/code_instruction_spec.js
index 4ec608aaf07..3a2ea263a05 100644
--- a/spec/frontend/vue_shared/components/registry/code_instruction_spec.js
+++ b/spec/frontend/vue_shared/components/registry/code_instruction_spec.js
@@ -3,6 +3,8 @@ import Tracking from '~/tracking';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import CodeInstruction from '~/vue_shared/components/registry/code_instruction.vue';
+jest.mock('lodash/uniqueId', () => (prefix) => (prefix ? `${prefix}1` : 1));
+
describe('Package code instruction', () => {
let wrapper;
diff --git a/spec/frontend/vue_shared/components/sidebar/issuable_move_dropdown_spec.js b/spec/frontend/vue_shared/components/sidebar/issuable_move_dropdown_spec.js
index a5a099d803a..5336ecc614c 100644
--- a/spec/frontend/vue_shared/components/sidebar/issuable_move_dropdown_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/issuable_move_dropdown_spec.js
@@ -68,6 +68,8 @@ describe('IssuableMoveDropdown', () => {
describe('searchKey', () => {
it('calls `fetchProjects` with value of the prop', async () => {
jest.spyOn(wrapper.vm, 'fetchProjects');
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
searchKey: 'foo',
});
@@ -143,6 +145,8 @@ describe('IssuableMoveDropdown', () => {
`(
'returns $returnValue when selectedProject and provided project param $title',
async ({ project, selectedProject, returnValue }) => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
selectedProject,
});
@@ -154,6 +158,8 @@ describe('IssuableMoveDropdown', () => {
);
it('returns false when selectedProject is null', async () => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
selectedProject: null,
});
@@ -206,6 +212,8 @@ describe('IssuableMoveDropdown', () => {
});
it('renders gl-loading-icon component when projectsListLoading prop is true', async () => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
projectsListLoading: true,
});
@@ -216,6 +224,8 @@ describe('IssuableMoveDropdown', () => {
});
it('renders gl-dropdown-item components for available projects', async () => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
projects: mockProjects,
selectedProject: mockProjects[0],
@@ -234,6 +244,8 @@ describe('IssuableMoveDropdown', () => {
});
it('renders string "No matching results" when search does not yield any matches', async () => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
searchKey: 'foo',
});
@@ -241,6 +253,8 @@ describe('IssuableMoveDropdown', () => {
// Wait for `searchKey` watcher to run.
await wrapper.vm.$nextTick();
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
projects: [],
projectsListLoading: false,
@@ -254,6 +268,8 @@ describe('IssuableMoveDropdown', () => {
});
it('renders string "Failed to load projects" when loading projects list fails', async () => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
projects: [],
projectsListLoading: false,
@@ -273,6 +289,8 @@ describe('IssuableMoveDropdown', () => {
expect(moveButtonEl.text()).toBe('Move');
expect(moveButtonEl.attributes('disabled')).toBe('true');
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
selectedProject: mockProjects[0],
});
@@ -303,6 +321,8 @@ describe('IssuableMoveDropdown', () => {
});
it('gl-dropdown component prevents dropdown body from closing on `hide` event when `projectItemClick` prop is true', async () => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
projectItemClick: true,
});
@@ -326,6 +346,8 @@ describe('IssuableMoveDropdown', () => {
});
it('sets project for clicked gl-dropdown-item to selectedProject', async () => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
projects: mockProjects,
});
@@ -338,6 +360,8 @@ describe('IssuableMoveDropdown', () => {
});
it('hides dropdown and emits `move-issuable` event when move button is clicked', async () => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
selectedProject: mockProjects[0],
});
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_create_view_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_create_view_spec.js
index 1fe85637a62..0eff6a1dace 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_create_view_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_create_view_spec.js
@@ -43,6 +43,8 @@ describe('DropdownContentsCreateView', () => {
});
it('returns `true` when `labelCreateInProgress` is true', () => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
labelTitle: 'Foo',
selectedColor: '#ff0000',
@@ -55,6 +57,8 @@ describe('DropdownContentsCreateView', () => {
});
it('returns `false` when label title and color is defined and create request is not already in progress', () => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
labelTitle: 'Foo',
selectedColor: '#ff0000',
@@ -99,6 +103,8 @@ describe('DropdownContentsCreateView', () => {
describe('handleCreateClick', () => {
it('calls action `createLabel` with object containing `labelTitle` & `selectedColor`', () => {
jest.spyOn(wrapper.vm, 'createLabel').mockImplementation();
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
labelTitle: 'Foo',
selectedColor: '#ff0000',
@@ -164,6 +170,8 @@ describe('DropdownContentsCreateView', () => {
});
it('renders color input element', () => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
selectedColor: '#ff0000',
});
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 80b8edd28ba..93a0e2f75bb 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
@@ -63,6 +63,8 @@ describe('DropdownContentsLabelsView', () => {
describe('computed', () => {
describe('visibleLabels', () => {
it('returns matching labels filtered with `searchKey`', () => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
searchKey: 'bug',
});
@@ -72,6 +74,8 @@ describe('DropdownContentsLabelsView', () => {
});
it('returns matching labels with fuzzy filtering', () => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
searchKey: 'bg',
});
@@ -82,6 +86,8 @@ describe('DropdownContentsLabelsView', () => {
});
it('returns all labels when `searchKey` is empty', () => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
searchKey: '',
});
@@ -100,6 +106,8 @@ describe('DropdownContentsLabelsView', () => {
`(
'returns $returnValue when searchKey is "$searchKey" and visibleLabels is $labelsDescription',
async ({ searchKey, labels, returnValue }) => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
searchKey,
});
@@ -161,6 +169,8 @@ describe('DropdownContentsLabelsView', () => {
describe('handleKeyDown', () => {
it('decreases `currentHighlightItem` value by 1 when Up arrow key is pressed', () => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
currentHighlightItem: 1,
});
@@ -173,6 +183,8 @@ describe('DropdownContentsLabelsView', () => {
});
it('increases `currentHighlightItem` value by 1 when Down arrow key is pressed', () => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
currentHighlightItem: 1,
});
@@ -185,6 +197,8 @@ describe('DropdownContentsLabelsView', () => {
});
it('resets the search text when the Enter key is pressed', () => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
currentHighlightItem: 1,
searchKey: 'bug',
@@ -201,6 +215,8 @@ describe('DropdownContentsLabelsView', () => {
it('calls action `updateSelectedLabels` with currently highlighted label when Enter key is pressed', () => {
jest.spyOn(wrapper.vm, 'updateSelectedLabels').mockImplementation();
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
currentHighlightItem: 2,
});
@@ -220,6 +236,8 @@ describe('DropdownContentsLabelsView', () => {
it('calls action `toggleDropdownContents` when Esc key is pressed', () => {
jest.spyOn(wrapper.vm, 'toggleDropdownContents').mockImplementation();
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
currentHighlightItem: 1,
});
@@ -233,6 +251,8 @@ describe('DropdownContentsLabelsView', () => {
it('calls action `scrollIntoViewIfNeeded` in next tick when any key is pressed', () => {
jest.spyOn(wrapper.vm, 'scrollIntoViewIfNeeded').mockImplementation();
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
currentHighlightItem: 1,
});
@@ -320,6 +340,8 @@ describe('DropdownContentsLabelsView', () => {
});
it('renders label element with `highlight` set to true when value of `currentHighlightItem` is more than -1', () => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
currentHighlightItem: 0,
});
@@ -332,6 +354,8 @@ describe('DropdownContentsLabelsView', () => {
});
it('renders element containing "No matching results" when `searchKey` does not match with any label', () => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
searchKey: 'abc',
});
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view_spec.js
index d8491334b5d..3ceed670d77 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view_spec.js
@@ -1,4 +1,4 @@
-import { GlLoadingIcon, GlLink } from '@gitlab/ui';
+import { GlAlert, GlLoadingIcon, GlLink } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
@@ -9,6 +9,7 @@ import { workspaceLabelsQueries } from '~/sidebar/constants';
import DropdownContentsCreateView from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view.vue';
import createLabelMutation from '~/vue_shared/components/sidebar/labels_select_widget/graphql/create_label.mutation.graphql';
import {
+ mockRegularLabel,
mockSuggestedColors,
createLabelSuccessfulResponse,
workspaceLabelsQueryResponse,
@@ -25,8 +26,18 @@ const userRecoverableError = {
errors: ['Houston, we have a problem'],
};
+const titleTakenError = {
+ data: {
+ labelCreate: {
+ label: mockRegularLabel,
+ errors: ['Title has already been taken'],
+ },
+ },
+};
+
const createLabelSuccessHandler = jest.fn().mockResolvedValue(createLabelSuccessfulResponse);
const createLabelUserRecoverableErrorHandler = jest.fn().mockResolvedValue(userRecoverableError);
+const createLabelDuplicateErrorHandler = jest.fn().mockResolvedValue(titleTakenError);
const createLabelErrorHandler = jest.fn().mockRejectedValue('Houston, we have a problem');
describe('DropdownContentsCreateView', () => {
@@ -208,4 +219,17 @@ describe('DropdownContentsCreateView', () => {
expect(createFlash).toHaveBeenCalled();
});
+
+ it('displays error in alert if label title is already taken', async () => {
+ createComponent({ mutationHandler: createLabelDuplicateErrorHandler });
+ fillLabelAttributes();
+ await nextTick();
+
+ findCreateButton().vm.$emit('click');
+ await waitForPromises();
+
+ expect(wrapper.findComponent(GlAlert).text()).toEqual(
+ titleTakenError.data.labelCreate.errors[0],
+ );
+ });
});
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view_spec.js
index 6f5a4b7e613..7f6770e0bea 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view_spec.js
@@ -110,6 +110,19 @@ describe('DropdownContentsLabelsView', () => {
});
});
+ it('first item is highlighted when search is not empty', async () => {
+ createComponent({
+ queryHandler: jest.fn().mockResolvedValue(workspaceLabelsQueryResponse),
+ searchKey: 'Label',
+ });
+ await makeObserverAppear();
+ await waitForPromises();
+ await nextTick();
+
+ expect(findLabelsList().exists()).toBe(true);
+ expect(findFirstLabel().attributes('active')).toBe('true');
+ });
+
it('when search returns 0 results', async () => {
createComponent({
queryHandler: jest.fn().mockResolvedValue({
diff --git a/spec/frontend/vue_shared/components/source_viewer_spec.js b/spec/frontend/vue_shared/components/source_viewer_spec.js
index 758068379de..094d8d42a47 100644
--- a/spec/frontend/vue_shared/components/source_viewer_spec.js
+++ b/spec/frontend/vue_shared/components/source_viewer_spec.js
@@ -1,27 +1,35 @@
import hljs from 'highlight.js/lib/core';
+import Vue, { nextTick } from 'vue';
+import VueRouter from 'vue-router';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import SourceViewer from '~/vue_shared/components/source_viewer.vue';
import LineNumbers from '~/vue_shared/components/line_numbers.vue';
import waitForPromises from 'helpers/wait_for_promises';
jest.mock('highlight.js/lib/core');
+Vue.use(VueRouter);
+const router = new VueRouter();
describe('Source Viewer component', () => {
let wrapper;
const content = `// Some source code`;
- const highlightedContent = `<span data-testid='test-highlighted'>${content}</span>`;
+ const highlightedContent = `<span data-testid='test-highlighted' id='LC1'>${content}</span><span id='LC2'></span>`;
const language = 'javascript';
hljs.highlight.mockImplementation(() => ({ value: highlightedContent }));
hljs.highlightAuto.mockImplementation(() => ({ value: highlightedContent }));
const createComponent = async (props = {}) => {
- wrapper = shallowMountExtended(SourceViewer, { propsData: { content, language, ...props } });
+ wrapper = shallowMountExtended(SourceViewer, {
+ router,
+ propsData: { content, language, ...props },
+ });
await waitForPromises();
};
const findLineNumbers = () => wrapper.findComponent(LineNumbers);
const findHighlightedContent = () => wrapper.findByTestId('test-highlighted');
+ const findFirstLine = () => wrapper.find('#LC1');
beforeEach(() => createComponent());
@@ -56,4 +64,39 @@ describe('Source Viewer component', () => {
expect(findHighlightedContent().exists()).toBe(true);
});
});
+
+ describe('selecting a line', () => {
+ let firstLine;
+ let firstLineElement;
+
+ beforeEach(() => {
+ firstLine = findFirstLine();
+ firstLineElement = firstLine.element;
+
+ jest.spyOn(firstLineElement, 'scrollIntoView');
+ jest.spyOn(firstLineElement.classList, 'add');
+ jest.spyOn(firstLineElement.classList, 'remove');
+ });
+
+ it('adds the highlight (hll) class', async () => {
+ wrapper.vm.$router.push('#LC1');
+ await nextTick();
+
+ expect(firstLineElement.classList.add).toHaveBeenCalledWith('hll');
+ });
+
+ it('removes the highlight (hll) class from a previously highlighted line', async () => {
+ wrapper.vm.$router.push('#LC2');
+ await nextTick();
+
+ expect(firstLineElement.classList.remove).toHaveBeenCalledWith('hll');
+ });
+
+ it('scrolls the line into view', () => {
+ expect(firstLineElement.scrollIntoView).toHaveBeenCalledWith({
+ behavior: 'smooth',
+ block: 'center',
+ });
+ });
+ });
});
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 92938b2717f..659d93d6597 100644
--- a/spec/frontend/vue_shared/components/web_ide_link_spec.js
+++ b/spec/frontend/vue_shared/components/web_ide_link_spec.js
@@ -1,11 +1,18 @@
-import { shallowMount } from '@vue/test-utils';
+import { GlModal } from '@gitlab/ui';
+import { nextTick } from 'vue';
+
import ActionsButton from '~/vue_shared/components/actions_button.vue';
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
import WebIdeLink from '~/vue_shared/components/web_ide_link.vue';
+import { stubComponent } from 'helpers/stub_component';
+import { shallowMountExtended, mountExtended } from 'helpers/vue_test_utils_helper';
+
const TEST_EDIT_URL = '/gitlab-test/test/-/edit/main/';
const TEST_WEB_IDE_URL = '/-/ide/project/gitlab-test/test/edit/main/-/';
const TEST_GITPOD_URL = 'https://gitpod.test/';
+const TEST_USER_PREFERENCES_GITPOD_PATH = '/-/profile/preferences#user_gitpod_enabled';
+const TEST_USER_PROFILE_ENABLE_GITPOD_PATH = '/-/profile?user%5Bgitpod_enabled%5D=true';
const ACTION_EDIT = {
href: TEST_EDIT_URL,
@@ -54,21 +61,31 @@ const ACTION_GITPOD = {
};
const ACTION_GITPOD_ENABLE = {
...ACTION_GITPOD,
- href: '#modal-enable-gitpod',
+ href: undefined,
handle: expect.any(Function),
};
describe('Web IDE link component', () => {
let wrapper;
- function createComponent(props) {
- wrapper = shallowMount(WebIdeLink, {
+ function createComponent(props, mountFn = shallowMountExtended) {
+ wrapper = mountFn(WebIdeLink, {
propsData: {
editUrl: TEST_EDIT_URL,
webIdeUrl: TEST_WEB_IDE_URL,
gitpodUrl: TEST_GITPOD_URL,
...props,
},
+ stubs: {
+ GlModal: stubComponent(GlModal, {
+ template: `
+ <div>
+ <slot name="modal-title"></slot>
+ <slot></slot>
+ <slot name="modal-footer"></slot>
+ </div>`,
+ }),
+ },
});
}
@@ -78,6 +95,7 @@ describe('Web IDE link component', () => {
const findActionsButton = () => wrapper.find(ActionsButton);
const findLocalStorageSync = () => wrapper.find(LocalStorageSync);
+ const findModal = () => wrapper.findComponent(GlModal);
it.each([
{
@@ -97,19 +115,68 @@ describe('Web IDE link component', () => {
expectedActions: [ACTION_WEB_IDE_CONFIRM_FORK, ACTION_EDIT_CONFIRM_FORK],
},
{
- props: { showWebIdeButton: false, showGitpodButton: true, gitpodEnabled: true },
+ props: {
+ showWebIdeButton: false,
+ showGitpodButton: true,
+ userPreferencesGitpodPath: TEST_USER_PREFERENCES_GITPOD_PATH,
+ userProfileEnableGitpodPath: TEST_USER_PROFILE_ENABLE_GITPOD_PATH,
+ gitpodEnabled: true,
+ },
expectedActions: [ACTION_EDIT, ACTION_GITPOD],
},
{
- props: { showWebIdeButton: false, showGitpodButton: true, gitpodEnabled: false },
+ props: {
+ showWebIdeButton: false,
+ showGitpodButton: true,
+ userPreferencesGitpodPath: TEST_USER_PREFERENCES_GITPOD_PATH,
+ gitpodEnabled: true,
+ },
+ expectedActions: [ACTION_EDIT],
+ },
+ {
+ props: {
+ showWebIdeButton: false,
+ showGitpodButton: true,
+ userProfileEnableGitpodPath: TEST_USER_PROFILE_ENABLE_GITPOD_PATH,
+ gitpodEnabled: true,
+ },
+ expectedActions: [ACTION_EDIT],
+ },
+ {
+ props: {
+ showWebIdeButton: false,
+ showGitpodButton: true,
+ gitpodEnabled: true,
+ },
+ expectedActions: [ACTION_EDIT],
+ },
+ {
+ props: {
+ showWebIdeButton: false,
+ showGitpodButton: true,
+ userPreferencesGitpodPath: TEST_USER_PREFERENCES_GITPOD_PATH,
+ userProfileEnableGitpodPath: TEST_USER_PROFILE_ENABLE_GITPOD_PATH,
+ gitpodEnabled: false,
+ },
expectedActions: [ACTION_EDIT, ACTION_GITPOD_ENABLE],
},
{
- props: { showGitpodButton: true, gitpodEnabled: false },
+ props: {
+ showGitpodButton: true,
+ userPreferencesGitpodPath: TEST_USER_PREFERENCES_GITPOD_PATH,
+ userProfileEnableGitpodPath: TEST_USER_PROFILE_ENABLE_GITPOD_PATH,
+ gitpodEnabled: false,
+ },
expectedActions: [ACTION_WEB_IDE, ACTION_EDIT, ACTION_GITPOD_ENABLE],
},
{
- props: { showEditButton: false, showGitpodButton: true, gitpodText: 'Test Gitpod' },
+ props: {
+ showEditButton: false,
+ showGitpodButton: true,
+ userPreferencesGitpodPath: TEST_USER_PREFERENCES_GITPOD_PATH,
+ userProfileEnableGitpodPath: TEST_USER_PROFILE_ENABLE_GITPOD_PATH,
+ gitpodText: 'Test Gitpod',
+ },
expectedActions: [ACTION_WEB_IDE, { ...ACTION_GITPOD_ENABLE, text: 'Test Gitpod' }],
},
{
@@ -128,6 +195,8 @@ describe('Web IDE link component', () => {
showEditButton: false,
showWebIdeButton: true,
showGitpodButton: true,
+ userPreferencesGitpodPath: TEST_USER_PREFERENCES_GITPOD_PATH,
+ userProfileEnableGitpodPath: TEST_USER_PROFILE_ENABLE_GITPOD_PATH,
gitpodEnabled: true,
});
});
@@ -174,7 +243,7 @@ describe('Web IDE link component', () => {
])(
'emits the correct event when an action handler is called',
async ({ props, expectedEventPayload }) => {
- createComponent({ ...props, needsToFork: true });
+ createComponent({ ...props, needsToFork: true, disableForkModal: true });
findActionsButton().props('actions')[0].handle();
@@ -182,4 +251,72 @@ describe('Web IDE link component', () => {
},
);
});
+
+ describe('when Gitpod is not enabled', () => {
+ it('renders closed modal to enable Gitpod', () => {
+ createComponent({
+ showGitpodButton: true,
+ userPreferencesGitpodPath: TEST_USER_PREFERENCES_GITPOD_PATH,
+ userProfileEnableGitpodPath: TEST_USER_PROFILE_ENABLE_GITPOD_PATH,
+ gitpodEnabled: false,
+ });
+
+ const modal = findModal();
+
+ expect(modal.exists()).toBe(true);
+ expect(modal.props()).toMatchObject({
+ visible: false,
+ modalId: 'enable-gitpod-modal',
+ size: 'sm',
+ title: WebIdeLink.i18n.modal.title,
+ actionCancel: {
+ text: WebIdeLink.i18n.modal.actionCancelText,
+ },
+ actionPrimary: {
+ text: WebIdeLink.i18n.modal.actionPrimaryText,
+ attributes: {
+ variant: 'confirm',
+ category: 'primary',
+ href: TEST_USER_PROFILE_ENABLE_GITPOD_PATH,
+ 'data-method': 'put',
+ },
+ },
+ });
+ });
+
+ it('opens modal when `Gitpod` action is clicked', async () => {
+ const gitpodText = 'Open in Gitpod';
+
+ createComponent(
+ {
+ showGitpodButton: true,
+ userPreferencesGitpodPath: TEST_USER_PREFERENCES_GITPOD_PATH,
+ userProfileEnableGitpodPath: TEST_USER_PROFILE_ENABLE_GITPOD_PATH,
+ gitpodEnabled: false,
+ gitpodText,
+ },
+ mountExtended,
+ );
+
+ findLocalStorageSync().vm.$emit('input', ACTION_GITPOD.key);
+
+ await nextTick();
+ await wrapper.findByRole('button', { name: gitpodText }).trigger('click');
+
+ expect(findModal().props('visible')).toBe(true);
+ });
+ });
+
+ describe('when Gitpod is enabled', () => {
+ it('does not render modal', () => {
+ createComponent({
+ showGitpodButton: true,
+ userPreferencesGitpodPath: TEST_USER_PREFERENCES_GITPOD_PATH,
+ userProfileEnableGitpodPath: TEST_USER_PROFILE_ENABLE_GITPOD_PATH,
+ gitpodEnabled: true,
+ });
+
+ expect(findModal().exists()).toBe(false);
+ });
+ });
});
diff --git a/spec/frontend/vue_shared/directives/track_event_spec.js b/spec/frontend/vue_shared/directives/track_event_spec.js
index d7d7f4edc3f..b3f94d0242a 100644
--- a/spec/frontend/vue_shared/directives/track_event_spec.js
+++ b/spec/frontend/vue_shared/directives/track_event_spec.js
@@ -38,6 +38,8 @@ describe('Error Tracking directive', () => {
label: 'Trackable Info',
};
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({ trackingOptions });
const { category, action, label, property, value } = trackingOptions;
diff --git a/spec/frontend/vue_shared/issuable/list/components/issuable_list_root_spec.js b/spec/frontend/vue_shared/issuable/list/components/issuable_list_root_spec.js
index 5979a65e3cd..14e93108447 100644
--- a/spec/frontend/vue_shared/issuable/list/components/issuable_list_root_spec.js
+++ b/spec/frontend/vue_shared/issuable/list/components/issuable_list_root_spec.js
@@ -98,6 +98,8 @@ describe('IssuableListRoot', () => {
await wrapper.vm.$nextTick();
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
checkedIssuables,
});
@@ -111,6 +113,8 @@ describe('IssuableListRoot', () => {
describe('bulkEditIssuables', () => {
it('returns array of issuables which have `checked` set to true within checkedIssuables map', async () => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
checkedIssuables: mockCheckedIssuables,
});
@@ -180,6 +184,8 @@ describe('IssuableListRoot', () => {
describe('issuableChecked', () => {
it('returns boolean value representing checked status of issuable item', async () => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
checkedIssuables: {
[mockIssuables[0].iid]: { checked: true, issuable: mockIssuables[0] },
diff --git a/spec/frontend/vue_shared/issuable/list/components/issuable_tabs_spec.js b/spec/frontend/vue_shared/issuable/list/components/issuable_tabs_spec.js
index 8c22b67bdbe..5723e2da586 100644
--- a/spec/frontend/vue_shared/issuable/list/components/issuable_tabs_spec.js
+++ b/spec/frontend/vue_shared/issuable/list/components/issuable_tabs_spec.js
@@ -1,5 +1,6 @@
import { GlTab, GlBadge } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
+import { setLanguage } from 'helpers/locale_helper';
import IssuableTabs from '~/vue_shared/issuable/list/components/issuable_tabs.vue';
@@ -27,10 +28,12 @@ describe('IssuableTabs', () => {
let wrapper;
beforeEach(() => {
+ setLanguage('en');
wrapper = createComponent();
});
afterEach(() => {
+ setLanguage(null);
wrapper.destroy();
});
@@ -71,7 +74,7 @@ describe('IssuableTabs', () => {
// Does not render `All` badge since it has an undefined count
expect(badges).toHaveLength(2);
- expect(badges.at(0).text()).toBe(`${mockIssuableListProps.tabCounts.opened}`);
+ expect(badges.at(0).text()).toBe('5,000');
expect(badges.at(1).text()).toBe(`${mockIssuableListProps.tabCounts.closed}`);
});
diff --git a/spec/frontend/vue_shared/issuable/list/mock_data.js b/spec/frontend/vue_shared/issuable/list/mock_data.js
index e2fa99f7cc9..cfc7937b412 100644
--- a/spec/frontend/vue_shared/issuable/list/mock_data.js
+++ b/spec/frontend/vue_shared/issuable/list/mock_data.js
@@ -133,7 +133,7 @@ export const mockTabs = [
];
export const mockTabCounts = {
- opened: 5,
+ opened: 5000,
closed: 0,
all: undefined,
};
diff --git a/spec/frontend/vue_shared/issuable/show/components/issuable_title_spec.js b/spec/frontend/vue_shared/issuable/show/components/issuable_title_spec.js
index 1fcf37a0477..cb418371760 100644
--- a/spec/frontend/vue_shared/issuable/show/components/issuable_title_spec.js
+++ b/spec/frontend/vue_shared/issuable/show/components/issuable_title_spec.js
@@ -84,6 +84,8 @@ describe('IssuableTitle', () => {
});
it('renders sticky header when `stickyTitleVisible` prop is true', async () => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
wrapper.setData({
stickyTitleVisible: true,
});
diff --git a/spec/frontend/work_items/pages/work_item_root_spec.js b/spec/frontend/work_items/pages/work_item_root_spec.js
index 02795751f33..ea26b2b4fb3 100644
--- a/spec/frontend/work_items/pages/work_item_root_spec.js
+++ b/spec/frontend/work_items/pages/work_item_root_spec.js
@@ -3,6 +3,7 @@ import { 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 { mockTracking, unmockTracking } from 'helpers/tracking_helper';
import workItemQuery from '~/work_items/graphql/work_item.query.graphql';
import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
import WorkItemsRoot from '~/work_items/pages/work_item_root.vue';
@@ -15,6 +16,7 @@ Vue.use(VueApollo);
const WORK_ITEM_ID = '1';
describe('Work items root component', () => {
+ const mockUpdatedTitle = 'Updated title';
let wrapper;
let fakeApollo;
@@ -53,7 +55,6 @@ describe('Work items root component', () => {
it('updates the title when it is edited', async () => {
createComponent();
jest.spyOn(wrapper.vm.$apollo, 'mutate');
- const mockUpdatedTitle = 'Updated title';
await findTitle().vm.$emit('title-changed', mockUpdatedTitle);
@@ -91,4 +92,32 @@ describe('Work items root component', () => {
expect(findTitle().exists()).toBe(false);
});
+
+ describe('tracking', () => {
+ let trackingSpy;
+
+ beforeEach(() => {
+ trackingSpy = mockTracking('_category_', undefined, jest.spyOn);
+
+ createComponent();
+ });
+
+ afterEach(() => {
+ unmockTracking();
+ });
+
+ it('tracks item title updates', async () => {
+ await findTitle().vm.$emit('title-changed', mockUpdatedTitle);
+
+ await waitForPromises();
+
+ expect(trackingSpy).toHaveBeenCalledTimes(1);
+ expect(trackingSpy).toHaveBeenCalledWith('workItems:show', undefined, {
+ action: 'updated_title',
+ category: 'workItems:show',
+ label: 'item_title',
+ property: '[type_work_item]',
+ });
+ });
+ });
});