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

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2022-05-19 10:33:21 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2022-05-19 10:33:21 +0300
commit36a59d088eca61b834191dacea009677a96c052f (patch)
treee4f33972dab5d8ef79e3944a9f403035fceea43f /spec/frontend
parenta1761f15ec2cae7c7f7bbda39a75494add0dfd6f (diff)
Add latest changes from gitlab-org/gitlab@15-0-stable-eev15.0.0-rc42
Diffstat (limited to 'spec/frontend')
-rw-r--r--spec/frontend/.eslintrc.yml5
-rw-r--r--spec/frontend/__helpers__/dom_shims/clipboard.js5
-rw-r--r--spec/frontend/__helpers__/dom_shims/index.js1
-rw-r--r--spec/frontend/__helpers__/fixtures.js15
-rw-r--r--spec/frontend/__helpers__/flush_promises.js4
-rw-r--r--spec/frontend/__helpers__/shared_test_setup.js7
-rw-r--r--spec/frontend/__helpers__/user_mock_data_helper.js2
-rw-r--r--spec/frontend/__helpers__/vuex_action_helper.js35
-rw-r--r--spec/frontend/__helpers__/vuex_action_helper_spec.js61
-rw-r--r--spec/frontend/__helpers__/wait_for_promises.js5
-rw-r--r--spec/frontend/activities_spec.js7
-rw-r--r--spec/frontend/add_context_commits_modal/components/__snapshots__/add_context_commits_modal_spec.js.snap1
-rw-r--r--spec/frontend/admin/applications/components/delete_application_spec.js4
-rw-r--r--spec/frontend/admin/background_migrations/components/database_listbox_spec.js57
-rw-r--r--spec/frontend/admin/background_migrations/mock_data.js6
-rw-r--r--spec/frontend/admin/users/new_spec.js7
-rw-r--r--spec/frontend/alert_handler_spec.js18
-rw-r--r--spec/frontend/analytics/shared/components/metric_popover_spec.js2
-rw-r--r--spec/frontend/analytics/shared/components/projects_dropdown_filter_spec.js15
-rw-r--r--spec/frontend/api/tags_api_spec.js37
-rw-r--r--spec/frontend/api/user_api_spec.js50
-rw-r--r--spec/frontend/api_spec.js32
-rw-r--r--spec/frontend/attention_requests/components/navigation_popover_spec.js4
-rw-r--r--spec/frontend/authentication/u2f/authenticate_spec.js7
-rw-r--r--spec/frontend/authentication/u2f/register_spec.js7
-rw-r--r--spec/frontend/authentication/webauthn/authenticate_spec.js7
-rw-r--r--spec/frontend/authentication/webauthn/register_spec.js7
-rw-r--r--spec/frontend/awards_handler_spec.js7
-rw-r--r--spec/frontend/badges/components/badge_form_spec.js4
-rw-r--r--spec/frontend/badges/components/badge_list_row_spec.js4
-rw-r--r--spec/frontend/badges/components/badge_list_spec.js4
-rw-r--r--spec/frontend/badges/components/badge_spec.js7
-rw-r--r--spec/frontend/badges/store/actions_spec.js6
-rw-r--r--spec/frontend/behaviors/autosize_spec.js20
-rw-r--r--spec/frontend/behaviors/copy_as_gfm_spec.js3
-rw-r--r--spec/frontend/behaviors/date_picker_spec.js7
-rw-r--r--spec/frontend/behaviors/load_startup_css_spec.js6
-rw-r--r--spec/frontend/behaviors/markdown/highlight_current_user_spec.js7
-rw-r--r--spec/frontend/behaviors/markdown/render_sandboxed_mermaid_spec.js34
-rw-r--r--spec/frontend/behaviors/quick_submit_spec.js7
-rw-r--r--spec/frontend/behaviors/requires_input_spec.js7
-rw-r--r--spec/frontend/behaviors/shortcuts/shortcuts_issuable_spec.js57
-rw-r--r--spec/frontend/blob/blob_file_dropzone_spec.js7
-rw-r--r--spec/frontend/blob/components/__snapshots__/blob_header_spec.js.snap2
-rw-r--r--spec/frontend/blob/components/table_contents_spec.js4
-rw-r--r--spec/frontend/blob/file_template_mediator_spec.js7
-rw-r--r--spec/frontend/blob/file_template_selector_spec.js29
-rw-r--r--spec/frontend/blob/line_highlighter_spec.js8
-rw-r--r--spec/frontend/blob/openapi/index_spec.js28
-rw-r--r--spec/frontend/blob/pipeline_tour_success_modal_spec.js2
-rw-r--r--spec/frontend/blob/sketch/index_spec.js50
-rw-r--r--spec/frontend/blob/viewer/index_spec.js5
-rw-r--r--spec/frontend/blob_edit/blob_bundle_spec.js14
-rw-r--r--spec/frontend/blob_edit/edit_blob_spec.js41
-rw-r--r--spec/frontend/boards/board_card_inner_spec.js88
-rw-r--r--spec/frontend/boards/components/board_list_header_spec.js4
-rw-r--r--spec/frontend/boards/mock_data.js5
-rw-r--r--spec/frontend/boards/project_select_spec.js21
-rw-r--r--spec/frontend/boards/stores/getters_spec.js29
-rw-r--r--spec/frontend/bootstrap_jquery_spec.js13
-rw-r--r--spec/frontend/bootstrap_linked_tabs_spec.js7
-rw-r--r--spec/frontend/branches/components/delete_branch_modal_spec.js4
-rw-r--r--spec/frontend/broadcast_notification_spec.js9
-rw-r--r--spec/frontend/ci_secure_files/components/secure_files_list_spec.js56
-rw-r--r--spec/frontend/ci_variable_list/ci_variable_list/ci_variable_list_spec.js19
-rw-r--r--spec/frontend/ci_variable_list/ci_variable_list/native_form_variable_list_spec.js7
-rw-r--r--spec/frontend/ci_variable_list/components/ci_variable_modal_spec.js22
-rw-r--r--spec/frontend/clusters/agents/components/revoke_token_button_spec.js239
-rw-r--r--spec/frontend/clusters/clusters_bundle_spec.js8
-rw-r--r--spec/frontend/clusters/components/__snapshots__/new_cluster_spec.js.snap2
-rw-r--r--spec/frontend/clusters/components/new_cluster_spec.js10
-rw-r--r--spec/frontend/clusters/forms/components/integration_form_spec.js2
-rw-r--r--spec/frontend/clusters/gke_cluster_namespace/gke_cluster_namespace_spec.js (renamed from spec/frontend/create_cluster/gke_cluster_namespace/gke_cluster_namespace_spec.js)9
-rw-r--r--spec/frontend/clusters/mock_data.js12
-rw-r--r--spec/frontend/clusters_list/components/agent_table_spec.js3
-rw-r--r--spec/frontend/clusters_list/components/clusters_actions_spec.js86
-rw-r--r--spec/frontend/code_navigation/components/app_spec.js6
-rw-r--r--spec/frontend/code_navigation/store/actions_spec.js7
-rw-r--r--spec/frontend/code_navigation/utils/index_spec.js9
-rw-r--r--spec/frontend/commits_spec.js7
-rw-r--r--spec/frontend/commons/nav/user_merge_requests_spec.js7
-rw-r--r--spec/frontend/content_editor/components/__snapshots__/toolbar_button_spec.js.snap4
-rw-r--r--spec/frontend/content_editor/components/bubble_menus/code_block_spec.js (renamed from spec/frontend/content_editor/components/code_block_bubble_menu_spec.js)36
-rw-r--r--spec/frontend/content_editor/components/bubble_menus/formatting_spec.js (renamed from spec/frontend/content_editor/components/formatting_bubble_menu_spec.js)19
-rw-r--r--spec/frontend/content_editor/components/bubble_menus/link_spec.js227
-rw-r--r--spec/frontend/content_editor/components/bubble_menus/media_spec.js234
-rw-r--r--spec/frontend/content_editor/components/content_editor_spec.js2
-rw-r--r--spec/frontend/content_editor/components/toolbar_button_spec.js2
-rw-r--r--spec/frontend/content_editor/components/wrappers/code_block_spec.js (renamed from spec/frontend/content_editor/components/wrappers/frontmatter_spec.js)33
-rw-r--r--spec/frontend/content_editor/components/wrappers/media_spec.js69
-rw-r--r--spec/frontend/content_editor/extensions/attachment_spec.js32
-rw-r--r--spec/frontend/content_editor/extensions/code_block_highlight_spec.js13
-rw-r--r--spec/frontend/content_editor/extensions/diagram_spec.js16
-rw-r--r--spec/frontend/content_editor/extensions/frontmatter_spec.js12
-rw-r--r--spec/frontend/content_editor/extensions/paste_markdown_spec.js51
-rw-r--r--spec/frontend/content_editor/remark_markdown_processing_spec.js248
-rw-r--r--spec/frontend/content_editor/services/asset_resolver_spec.js23
-rw-r--r--spec/frontend/content_editor/services/code_block_language_loader_spec.js36
-rw-r--r--spec/frontend/content_editor/services/content_editor_spec.js15
-rw-r--r--spec/frontend/content_editor/services/create_content_editor_spec.js48
-rw-r--r--spec/frontend/content_editor/services/gl_api_markdown_deserializer_spec.js (renamed from spec/frontend/content_editor/services/markdown_deserializer_spec.js)16
-rw-r--r--spec/frontend/content_editor/services/markdown_serializer_spec.js44
-rw-r--r--spec/frontend/content_editor/services/markdown_sourcemap_spec.js2
-rw-r--r--spec/frontend/content_editor/test_constants.js25
-rw-r--r--spec/frontend/content_editor/test_utils.js2
-rw-r--r--spec/frontend/create_cluster/components/cluster_form_dropdown_spec.js214
-rw-r--r--spec/frontend/create_cluster/eks_cluster/components/create_eks_cluster_spec.js98
-rw-r--r--spec/frontend/create_cluster/eks_cluster/components/eks_cluster_configuration_form_spec.js562
-rw-r--r--spec/frontend/create_cluster/eks_cluster/components/service_credentials_form_spec.js124
-rw-r--r--spec/frontend/create_cluster/eks_cluster/services/aws_services_facade_spec.js178
-rw-r--r--spec/frontend/create_cluster/eks_cluster/store/actions_spec.js366
-rw-r--r--spec/frontend/create_cluster/eks_cluster/store/getters_spec.js13
-rw-r--r--spec/frontend/create_cluster/eks_cluster/store/mutations_spec.js161
-rw-r--r--spec/frontend/create_cluster/gke_cluster/components/gke_machine_type_dropdown_spec.js129
-rw-r--r--spec/frontend/create_cluster/gke_cluster/components/gke_network_dropdown_spec.js137
-rw-r--r--spec/frontend/create_cluster/gke_cluster/components/gke_project_id_dropdown_spec.js137
-rw-r--r--spec/frontend/create_cluster/gke_cluster/components/gke_submit_button_spec.js51
-rw-r--r--spec/frontend/create_cluster/gke_cluster/components/gke_subnetwork_dropdown_spec.js111
-rw-r--r--spec/frontend/create_cluster/gke_cluster/components/gke_zone_dropdown_spec.js103
-rw-r--r--spec/frontend/create_cluster/gke_cluster/gapi_loader_spec.js47
-rw-r--r--spec/frontend/create_cluster/gke_cluster/helpers.js64
-rw-r--r--spec/frontend/create_cluster/gke_cluster/mock_data.js75
-rw-r--r--spec/frontend/create_cluster/gke_cluster/stores/actions_spec.js141
-rw-r--r--spec/frontend/create_cluster/gke_cluster/stores/getters_spec.js103
-rw-r--r--spec/frontend/create_cluster/gke_cluster/stores/mutations_spec.js32
-rw-r--r--spec/frontend/create_cluster/init_create_cluster_spec.js77
-rw-r--r--spec/frontend/create_cluster/store/cluster_dropdown/actions_spec.js95
-rw-r--r--spec/frontend/create_cluster/store/cluster_dropdown/mutations_spec.js36
-rw-r--r--spec/frontend/create_item_dropdown_spec.js4
-rw-r--r--spec/frontend/crm/contact_form_wrapper_spec.js112
-rw-r--r--spec/frontend/crm/contacts_root_spec.js2
-rw-r--r--spec/frontend/crm/form_spec.js57
-rw-r--r--spec/frontend/crm/organizations_root_spec.js4
-rw-r--r--spec/frontend/cycle_analytics/base_spec.js3
-rw-r--r--spec/frontend/cycle_analytics/mock_data.js4
-rw-r--r--spec/frontend/cycle_analytics/stage_table_spec.js2
-rw-r--r--spec/frontend/cycle_analytics/value_stream_filters_spec.js54
-rw-r--r--spec/frontend/cycle_analytics/value_stream_metrics_spec.js46
-rw-r--r--spec/frontend/deprecated_jquery_dropdown_spec.js5
-rw-r--r--spec/frontend/design_management/components/design_notes/design_reply_form_spec.js4
-rw-r--r--spec/frontend/design_management/components/design_sidebar_spec.js2
-rw-r--r--spec/frontend/design_management/pages/__snapshots__/index_spec.js.snap1
-rw-r--r--spec/frontend/design_management/pages/design/__snapshots__/index_spec.js.snap1
-rw-r--r--spec/frontend/diffs/components/diff_expansion_cell_spec.js21
-rw-r--r--spec/frontend/diffs/store/actions_spec.js2
-rw-r--r--spec/frontend/diffs/store/mutations_spec.js9
-rw-r--r--spec/frontend/diffs/store/utils_spec.js24
-rw-r--r--spec/frontend/diffs/utils/diff_file_spec.js71
-rw-r--r--spec/frontend/diffs/utils/queue_events_spec.js37
-rw-r--r--spec/frontend/dropzone_input_spec.js5
-rw-r--r--spec/frontend/editor/components/helpers.js18
-rw-r--r--spec/frontend/editor/components/source_editor_toolbar_button_spec.js116
-rw-r--r--spec/frontend/editor/components/source_editor_toolbar_graphql_spec.js112
-rw-r--r--spec/frontend/editor/extensions/source_editor_toolbar_ext_spec.js156
-rw-r--r--spec/frontend/editor/schema/ci/json_tests/positive_tests/gitlab-ci.json10
-rw-r--r--spec/frontend/editor/source_editor_ci_schema_ext_spec.js5
-rw-r--r--spec/frontend/editor/source_editor_extension_base_spec.js6
-rw-r--r--spec/frontend/editor/source_editor_markdown_ext_spec.js5
-rw-r--r--spec/frontend/editor/source_editor_markdown_livepreview_ext_spec.js115
-rw-r--r--spec/frontend/editor/source_editor_spec.js8
-rw-r--r--spec/frontend/editor/source_editor_yaml_ext_spec.js9
-rw-r--r--spec/frontend/editor/utils_spec.js13
-rw-r--r--spec/frontend/emoji/components/utils_spec.js4
-rw-r--r--spec/frontend/environments/environment_folder_spec.js19
-rw-r--r--spec/frontend/filterable_list_spec.js6
-rw-r--r--spec/frontend/filtered_search/dropdown_user_spec.js7
-rw-r--r--spec/frontend/filtered_search/dropdown_utils_spec.js11
-rw-r--r--spec/frontend/filtered_search/filtered_search_dropdown_manager_spec.js7
-rw-r--r--spec/frontend/filtered_search/filtered_search_manager_spec.js8
-rw-r--r--spec/frontend/filtered_search/filtered_search_visual_tokens_spec.js9
-rw-r--r--spec/frontend/filtered_search/visual_token_value_spec.js7
-rw-r--r--spec/frontend/fixtures/api_merge_requests.rb6
-rw-r--r--spec/frontend/fixtures/runner.rb22
-rw-r--r--spec/frontend/flash_spec.js6
-rw-r--r--spec/frontend/frequent_items/components/app_spec.js12
-rw-r--r--spec/frontend/frequent_items/utils_spec.js14
-rw-r--r--spec/frontend/gfm_auto_complete_spec.js4
-rw-r--r--spec/frontend/gl_field_errors_spec.js7
-rw-r--r--spec/frontend/google_tag_manager/index_spec.js10
-rw-r--r--spec/frontend/gpg_badges_spec.js10
-rw-r--r--spec/frontend/group_settings/components/shared_runners_form_spec.js167
-rw-r--r--spec/frontend/groups/landing_spec.js2
-rw-r--r--spec/frontend/header_spec.js10
-rw-r--r--spec/frontend/helpers/startup_css_helper_spec.js9
-rw-r--r--spec/frontend/ide/components/commit_sidebar/message_field_spec.js5
-rw-r--r--spec/frontend/ide/components/new_dropdown/upload_spec.js4
-rw-r--r--spec/frontend/image_diff/image_diff_spec.js7
-rw-r--r--spec/frontend/image_diff/init_discussion_tab_spec.js7
-rw-r--r--spec/frontend/image_diff/replaced_image_diff_spec.js7
-rw-r--r--spec/frontend/import_entities/import_groups/components/import_table_spec.js31
-rw-r--r--spec/frontend/import_entities/import_groups/utils_spec.js56
-rw-r--r--spec/frontend/import_entities/import_projects/components/import_projects_table_spec.js4
-rw-r--r--spec/frontend/incidents_settings/components/__snapshots__/pagerduty_form_spec.js.snap1
-rw-r--r--spec/frontend/integrations/edit/components/integration_form_spec.js24
-rw-r--r--spec/frontend/integrations/edit/components/jira_issues_fields_spec.js49
-rw-r--r--spec/frontend/integrations/edit/mock_data.js8
-rw-r--r--spec/frontend/invite_members/components/invite_members_modal_spec.js80
-rw-r--r--spec/frontend/invite_members/components/invite_modal_base_spec.js120
-rw-r--r--spec/frontend/invite_members/components/user_limit_notification_spec.js59
-rw-r--r--spec/frontend/invite_members/mock_data/member_modal.js2
-rw-r--r--spec/frontend/invite_members/mock_data/modal_base.js3
-rw-r--r--spec/frontend/issuable/components/issuable_header_warnings_spec.js10
-rw-r--r--spec/frontend/issuable/components/status_box_spec.js76
-rw-r--r--spec/frontend/issuable/issuable_form_spec.js10
-rw-r--r--spec/frontend/issuable/related_issues/components/related_issues_root_spec.js27
-rw-r--r--spec/frontend/issues/issue_spec.js13
-rw-r--r--spec/frontend/issues/list/components/issues_list_app_spec.js175
-rw-r--r--spec/frontend/issues/list/mock_data.js14
-rw-r--r--spec/frontend/issues/list/utils_spec.js29
-rw-r--r--spec/frontend/issues/show/components/app_spec.js53
-rw-r--r--spec/frontend/issues/show/components/description_spec.js91
-rw-r--r--spec/frontend/issues/show/components/fields/description_spec.js1
-rw-r--r--spec/frontend/issues/show/components/title_spec.js7
-rw-r--r--spec/frontend/issues/show/mock_data/mock_data.js17
-rw-r--r--spec/frontend/issues/show/utils_spec.js40
-rw-r--r--spec/frontend/jira_connect/subscriptions/components/add_namespace_modal/groups_list_item_spec.js116
-rw-r--r--spec/frontend/jira_connect/subscriptions/components/app_spec.js67
-rw-r--r--spec/frontend/jira_connect/subscriptions/components/sign_in_oauth_button_spec.js35
-rw-r--r--spec/frontend/jira_connect/subscriptions/components/subscriptions_list_spec.js7
-rw-r--r--spec/frontend/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_com_spec.js (renamed from spec/frontend/jira_connect/subscriptions/pages/sign_in_spec.js)8
-rw-r--r--spec/frontend/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/index_spec.js83
-rw-r--r--spec/frontend/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/version_select_form_spec.js69
-rw-r--r--spec/frontend/jira_connect/subscriptions/pages/sign_in/sign_in_page_spec.js82
-rw-r--r--spec/frontend/jira_connect/subscriptions/pages/subscriptions_page_spec.js71
-rw-r--r--spec/frontend/jira_connect/subscriptions/pages/subscriptions_spec.js56
-rw-r--r--spec/frontend/jira_connect/subscriptions/store/actions_spec.js172
-rw-r--r--spec/frontend/jira_connect/subscriptions/store/mutations_spec.js67
-rw-r--r--spec/frontend/jobs/components/filtered_search/tokens/job_status_token_spec.js1
-rw-r--r--spec/frontend/jobs/components/job_app_spec.js4
-rw-r--r--spec/frontend/jobs/components/stuck_block_spec.js6
-rw-r--r--spec/frontend/jobs/components/table/cells/actions_cell_spec.js105
-rw-r--r--spec/frontend/jobs/mock_data.js72
-rw-r--r--spec/frontend/jobs/store/getters_spec.js20
-rw-r--r--spec/frontend/lib/dompurify_spec.js10
-rw-r--r--spec/frontend/lib/gfm/index_spec.js6
-rw-r--r--spec/frontend/lib/utils/common_utils_spec.js69
-rw-r--r--spec/frontend/lib/utils/datetime_utility_spec.js4
-rw-r--r--spec/frontend/lib/utils/dom_utils_spec.js29
-rw-r--r--spec/frontend/lib/utils/file_upload_spec.js7
-rw-r--r--spec/frontend/lib/utils/mock_data.js42
-rw-r--r--spec/frontend/lib/utils/navigation_utility_spec.js5
-rw-r--r--spec/frontend/lib/utils/resize_observer_spec.js4
-rw-r--r--spec/frontend/lib/utils/text_markdown_spec.js33
-rw-r--r--spec/frontend/lib/utils/url_utility_spec.js53
-rw-r--r--spec/frontend/lib/utils/users_cache_spec.js25
-rw-r--r--spec/frontend/listbox/index_spec.js6
-rw-r--r--spec/frontend/logs/components/tokens/token_with_loading_state_spec.js5
-rw-r--r--spec/frontend/members/components/table/role_dropdown_spec.js27
-rw-r--r--spec/frontend/merge_conflicts/store/actions_spec.js4
-rw-r--r--spec/frontend/merge_request_spec.js6
-rw-r--r--spec/frontend/merge_request_tabs_spec.js7
-rw-r--r--spec/frontend/monitoring/components/__snapshots__/dashboard_template_spec.js.snap1
-rw-r--r--spec/frontend/monitoring/components/dashboard_panel_spec.js26
-rw-r--r--spec/frontend/new_branch_spec.js7
-rw-r--r--spec/frontend/notes/components/comment_form_spec.js34
-rw-r--r--spec/frontend/notes/components/comment_type_dropdown_spec.js32
-rw-r--r--spec/frontend/notes/components/discussion_counter_spec.js42
-rw-r--r--spec/frontend/notes/components/note_body_spec.js90
-rw-r--r--spec/frontend/notes/components/note_form_spec.js21
-rw-r--r--spec/frontend/notes/components/note_header_spec.js4
-rw-r--r--spec/frontend/notes/components/notes_app_spec.js32
-rw-r--r--spec/frontend/notes/deprecated_notes_spec.js11
-rw-r--r--spec/frontend/notes/mixins/discussion_navigation_spec.js1
-rw-r--r--spec/frontend/notes/mock_data.js14
-rw-r--r--spec/frontend/notes/stores/actions_spec.js8
-rw-r--r--spec/frontend/notes/stores/getters_spec.js86
-rw-r--r--spec/frontend/oauth_remember_me_spec.js7
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/details_header_spec.js27
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/cleanup_status_spec.js38
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/registry_header_spec.js5
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/utils_spec.js21
-rw-r--r--spec/frontend/packages_and_registries/dependency_proxy/app_spec.js83
-rw-r--r--spec/frontend/packages_and_registries/dependency_proxy/components/manifest_row_spec.js66
-rw-r--r--spec/frontend/packages_and_registries/dependency_proxy/mock_data.js14
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/package_title_spec.js.snap12
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/package_title_spec.js9
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/list/__snapshots__/package_list_row_spec.js.snap39
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/list/package_list_row_spec.js29
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/list/packages_list_spec.js41
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/list/packages_title_spec.js2
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/list/tokens/package_type_token_spec.js5
-rw-r--r--spec/frontend/packages_and_registries/settings/group/components/dependency_proxy_settings_spec.js23
-rw-r--r--spec/frontend/packages_and_registries/settings/group/components/group_settings_app_spec.js12
-rw-r--r--spec/frontend/packages_and_registries/settings/project/settings/components/settings_form_spec.js19
-rw-r--r--spec/frontend/pager_spec.js31
-rw-r--r--spec/frontend/pages/admin/abuse_reports/abuse_reports_spec.js8
-rw-r--r--spec/frontend/pages/admin/application_settings/account_and_limits_spec.js7
-rw-r--r--spec/frontend/pages/admin/application_settings/metrics_and_profiling/usage_statistics_spec.js7
-rw-r--r--spec/frontend/pages/admin/projects/components/namespace_select_spec.js7
-rw-r--r--spec/frontend/pages/dashboard/todos/index/todos_spec.js7
-rw-r--r--spec/frontend/pages/import/bulk_imports/history/components/bulk_imports_history_app_spec.js17
-rw-r--r--spec/frontend/pages/projects/learn_gitlab/components/__snapshots__/learn_gitlab_spec.js.snap9
-rw-r--r--spec/frontend/pages/projects/learn_gitlab/components/learn_gitlab_section_link_spec.js47
-rw-r--r--spec/frontend/pages/projects/learn_gitlab/components/learn_gitlab_spec.js2
-rw-r--r--spec/frontend/pages/projects/learn_gitlab/components/mock_data.js9
-rw-r--r--spec/frontend/pages/projects/merge_requests/edit/check_form_state_spec.js5
-rw-r--r--spec/frontend/pages/projects/pages_domains/form_spec.js7
-rw-r--r--spec/frontend/pages/projects/pipeline_schedules/shared/components/pipeline_schedule_callout_spec.js2
-rw-r--r--spec/frontend/pages/projects/pipeline_schedules/shared/components/timezone_dropdown_spec.js9
-rw-r--r--spec/frontend/pages/search/show/refresh_counts_spec.js7
-rw-r--r--spec/frontend/pages/sessions/new/preserve_url_fragment_spec.js7
-rw-r--r--spec/frontend/pages/sessions/new/signin_tabs_memoizer_spec.js7
-rw-r--r--spec/frontend/pdf/index_spec.js18
-rw-r--r--spec/frontend/performance_bar/index_spec.js4
-rw-r--r--spec/frontend/pipeline_editor/components/commit/commit_form_spec.js4
-rw-r--r--spec/frontend/pipeline_editor/components/file-nav/pipeline_editor_file_nav_spec.js133
-rw-r--r--spec/frontend/pipeline_editor/components/file-tree/container_spec.js138
-rw-r--r--spec/frontend/pipeline_editor/components/file-tree/file_item_spec.js52
-rw-r--r--spec/frontend/pipeline_editor/components/pipeline_editor_tabs_spec.js2
-rw-r--r--spec/frontend/pipeline_editor/components/popovers/file_tree_popover_spec.js56
-rw-r--r--spec/frontend/pipeline_editor/components/popovers/walkthrough_popover_spec.js (renamed from spec/frontend/pipeline_editor/components/walkthrough_popover_spec.js)2
-rw-r--r--spec/frontend/pipeline_editor/mock_data.js35
-rw-r--r--spec/frontend/pipeline_editor/pipeline_editor_home_spec.js97
-rw-r--r--spec/frontend/pipeline_wizard/components/commit_spec.js18
-rw-r--r--spec/frontend/pipelines/__snapshots__/utils_spec.js.snap11
-rw-r--r--spec/frontend/pipelines/components/jobs/failed_jobs_app_spec.js87
-rw-r--r--spec/frontend/pipelines/components/jobs/failed_jobs_table_spec.js117
-rw-r--r--spec/frontend/pipelines/components/jobs/utils_spec.js14
-rw-r--r--spec/frontend/pipelines/components/pipeline_tabs_spec.js9
-rw-r--r--spec/frontend/pipelines/components/pipelines_list/empty_state/ci_templates_spec.js (renamed from spec/frontend/pipelines/empty_state/ci_templates_spec.js)47
-rw-r--r--spec/frontend/pipelines/components/pipelines_list/empty_state/ios_templates_spec.js138
-rw-r--r--spec/frontend/pipelines/components/pipelines_list/empty_state/pipelines_ci_templates_spec.js (renamed from spec/frontend/pipelines/empty_state/pipelines_ci_templates_spec.js)5
-rw-r--r--spec/frontend/pipelines/components/pipelines_list/pipeline_stage_spec.js6
-rw-r--r--spec/frontend/pipelines/empty_state_spec.js60
-rw-r--r--spec/frontend/pipelines/graph/graph_component_wrapper_spec.js27
-rw-r--r--spec/frontend/pipelines/graph/job_item_spec.js184
-rw-r--r--spec/frontend/pipelines/graph/linked_pipeline_spec.js336
-rw-r--r--spec/frontend/pipelines/graph/linked_pipelines_column_spec.js1
-rw-r--r--spec/frontend/pipelines/graph/linked_pipelines_mock_data.js5
-rw-r--r--spec/frontend/pipelines/graph/mock_data.js122
-rw-r--r--spec/frontend/pipelines/graph_shared/links_inner_spec.js15
-rw-r--r--spec/frontend/pipelines/header_component_spec.js51
-rw-r--r--spec/frontend/pipelines/mock_data.js215
-rw-r--r--spec/frontend/pipelines/pipeline_graph/utils_spec.js20
-rw-r--r--spec/frontend/pipelines/pipelines_spec.js2
-rw-r--r--spec/frontend/pipelines/test_reports/stores/actions_spec.js7
-rw-r--r--spec/frontend/pipelines/test_reports/stores/mutations_spec.js21
-rw-r--r--spec/frontend/pipelines/test_reports/test_suite_table_spec.js57
-rw-r--r--spec/frontend/pipelines/tokens/pipeline_branch_name_token_spec.js1
-rw-r--r--spec/frontend/pipelines/tokens/pipeline_source_token_spec.js1
-rw-r--r--spec/frontend/pipelines/tokens/pipeline_status_token_spec.js1
-rw-r--r--spec/frontend/pipelines/tokens/pipeline_tag_name_token_spec.js1
-rw-r--r--spec/frontend/pipelines/tokens/pipeline_trigger_author_token_spec.js1
-rw-r--r--spec/frontend/project_select_combo_button_spec.js31
-rw-r--r--spec/frontend/projects/commits/components/author_select_spec.js4
-rw-r--r--spec/frontend/projects/components/__snapshots__/project_delete_button_spec.js.snap2
-rw-r--r--spec/frontend/projects/components/shared/__snapshots__/delete_button_spec.js.snap1
-rw-r--r--spec/frontend/projects/new/components/deployment_target_select_spec.js4
-rw-r--r--spec/frontend/projects/new/components/new_project_push_tip_popover_spec.js4
-rw-r--r--spec/frontend/projects/pipelines/charts/components/ci_cd_analytics_charts_spec.js6
-rw-r--r--spec/frontend/projects/project_import_gitlab_project_spec.js4
-rw-r--r--spec/frontend/projects/project_new_spec.js7
-rw-r--r--spec/frontend/projects/projects_filterable_list_spec.js6
-rw-r--r--spec/frontend/projects/settings/access_dropdown_spec.js7
-rw-r--r--spec/frontend/projects/settings/topics/components/topics_token_selector_spec.js15
-rw-r--r--spec/frontend/projects/settings_service_desk/components/service_desk_setting_spec.js6
-rw-r--r--spec/frontend/prometheus_alerts/components/reset_key_spec.js99
-rw-r--r--spec/frontend/prometheus_metrics/custom_metrics_spec.js4
-rw-r--r--spec/frontend/prometheus_metrics/prometheus_metrics_spec.js7
-rw-r--r--spec/frontend/protected_branches/protected_branch_create_spec.js7
-rw-r--r--spec/frontend/protected_branches/protected_branch_edit_spec.js10
-rw-r--r--spec/frontend/read_more_spec.js7
-rw-r--r--spec/frontend/releases/components/app_edit_new_spec.js12
-rw-r--r--spec/frontend/releases/components/tag_field_new_spec.js28
-rw-r--r--spec/frontend/releases/stores/modules/detail/actions_spec.js46
-rw-r--r--spec/frontend/releases/stores/modules/detail/getters_spec.js32
-rw-r--r--spec/frontend/releases/stores/modules/detail/mutations_spec.js37
-rw-r--r--spec/frontend/reports/codequality_report/store/getters_spec.js4
-rw-r--r--spec/frontend/reports/components/report_link_spec.js81
-rw-r--r--spec/frontend/repository/components/__snapshots__/last_commit_spec.js.snap14
-rw-r--r--spec/frontend/repository/components/blob_content_viewer_spec.js9
-rw-r--r--spec/frontend/repository/components/breadcrumbs_spec.js3
-rw-r--r--spec/frontend/right_sidebar_spec.js6
-rw-r--r--spec/frontend/runner/admin_runner_edit/admin_runner_edit_app_spec.js35
-rw-r--r--spec/frontend/runner/admin_runner_show/admin_runner_show_app_spec.js52
-rw-r--r--spec/frontend/runner/admin_runners/admin_runners_app_spec.js82
-rw-r--r--spec/frontend/runner/components/registration/registration_dropdown_spec.js89
-rw-r--r--spec/frontend/runner/components/runner_delete_button_spec.js6
-rw-r--r--spec/frontend/runner/components/runner_details_spec.js3
-rw-r--r--spec/frontend/runner/components/runner_jobs_spec.js2
-rw-r--r--spec/frontend/runner/components/runner_projects_spec.js2
-rw-r--r--spec/frontend/runner/components/runner_update_form_spec.js61
-rw-r--r--spec/frontend/runner/group_runners/group_runners_app_spec.js82
-rw-r--r--spec/frontend/runner/local_storage_alert/save_alert_to_local_storage_spec.js24
-rw-r--r--spec/frontend/runner/local_storage_alert/show_alert_from_local_storage_spec.js40
-rw-r--r--spec/frontend/runner/mock_data.js24
-rw-r--r--spec/frontend/runner/runner_search_utils_spec.js12
-rw-r--r--spec/frontend/search/highlight_blob_search_result_spec.js7
-rw-r--r--spec/frontend/search_autocomplete_spec.js5
-rw-r--r--spec/frontend/security_configuration/components/app_spec.js75
-rw-r--r--spec/frontend/security_configuration/mock_data.js9
-rw-r--r--spec/frontend/self_monitor/components/__snapshots__/self_monitor_form_spec.js.snap1
-rw-r--r--spec/frontend/serverless/components/__snapshots__/empty_state_spec.js.snap22
-rw-r--r--spec/frontend/serverless/components/area_spec.js121
-rw-r--r--spec/frontend/serverless/components/empty_state_spec.js25
-rw-r--r--spec/frontend/serverless/components/environment_row_spec.js68
-rw-r--r--spec/frontend/serverless/components/function_details_spec.js100
-rw-r--r--spec/frontend/serverless/components/function_row_spec.js34
-rw-r--r--spec/frontend/serverless/components/functions_spec.js86
-rw-r--r--spec/frontend/serverless/components/missing_prometheus_spec.js38
-rw-r--r--spec/frontend/serverless/components/pod_box_spec.js22
-rw-r--r--spec/frontend/serverless/components/url_spec.js26
-rw-r--r--spec/frontend/serverless/mock_data.js145
-rw-r--r--spec/frontend/serverless/store/actions_spec.js80
-rw-r--r--spec/frontend/serverless/store/getters_spec.js43
-rw-r--r--spec/frontend/serverless/store/mutations_spec.js86
-rw-r--r--spec/frontend/serverless/utils.js17
-rw-r--r--spec/frontend/settings_panels_spec.js7
-rw-r--r--spec/frontend/shortcuts_spec.js7
-rw-r--r--spec/frontend/sidebar/components/assignees/uncollapsed_assignee_list_spec.js2
-rw-r--r--spec/frontend/sidebar/components/attention_requested_toggle_spec.js7
-rw-r--r--spec/frontend/sidebar/components/confidential/sidebar_confidentiality_content_spec.js18
-rw-r--r--spec/frontend/sidebar/components/confidential/sidebar_confidentiality_form_spec.js2
-rw-r--r--spec/frontend/sidebar/components/crm_contacts_spec.js11
-rw-r--r--spec/frontend/sidebar/components/reviewers/uncollapsed_reviewer_list_spec.js7
-rw-r--r--spec/frontend/sidebar/components/time_tracking/mock_data.js6
-rw-r--r--spec/frontend/sidebar/lock/issuable_lock_form_spec.js3
-rw-r--r--spec/frontend/sidebar/reviewers_spec.js1
-rw-r--r--spec/frontend/sidebar/sidebar_mediator_spec.js62
-rw-r--r--spec/frontend/single_file_diff_spec.js3
-rw-r--r--spec/frontend/smart_interval_spec.js7
-rw-r--r--spec/frontend/snippet/collapsible_input_spec.js6
-rw-r--r--spec/frontend/snippets/components/__snapshots__/snippet_description_edit_spec.js.snap3
-rw-r--r--spec/frontend/snippets/components/edit_spec.js209
-rw-r--r--spec/frontend/snippets/components/snippet_blob_actions_edit_spec.js36
-rw-r--r--spec/frontend/syntax_highlight_spec.js16
-rw-r--r--spec/frontend/tabs/index_spec.js8
-rw-r--r--spec/frontend/task_list_spec.js7
-rw-r--r--spec/frontend/tracking/tracking_spec.js12
-rw-r--r--spec/frontend/user_lists/components/user_lists_table_spec.js3
-rw-r--r--spec/frontend/user_popovers_spec.js45
-rw-r--r--spec/frontend/vue_alerts_spec.js6
-rw-r--r--spec/frontend/vue_mr_widget/components/added_commit_message_spec.js31
-rw-r--r--spec/frontend/vue_mr_widget/components/extensions/utils_spec.js22
-rw-r--r--spec/frontend/vue_mr_widget/components/mr_widget_header_spec.js144
-rw-r--r--spec/frontend/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js4
-rw-r--r--spec/frontend/vue_mr_widget/extensions/test_report/index_spec.js48
-rw-r--r--spec/frontend/vue_mr_widget/extentions/accessibility/index_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/color_picker/color_picker_spec.js41
-rw-r--r--spec/frontend/vue_shared/components/confidentiality_badge_spec.js52
-rw-r--r--spec/frontend/vue_shared/components/confirm_danger/confirm_danger_modal_spec.js15
-rw-r--r--spec/frontend/vue_shared/components/date_time_picker/date_time_picker_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/deployment_instance/deployment_instance_spec.js25
-rw-r--r--spec/frontend/vue_shared/components/dropdown/dropdown_hidden_input_spec.js36
-rw-r--r--spec/frontend/vue_shared/components/dropdown/dropdown_search_input_spec.js51
-rw-r--r--spec/frontend/vue_shared/components/file_finder/index_spec.js27
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_bar_root_spec.js51
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/author_token_spec.js1
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/base_token_spec.js1
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/branch_token_spec.js1
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/emoji_token_spec.js1
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/label_token_spec.js1
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/milestone_token_spec.js1
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/release_token_spec.js1
-rw-r--r--spec/frontend/vue_shared/components/gitlab_version_check_spec.js6
-rw-r--r--spec/frontend/vue_shared/components/markdown/field_spec.js31
-rw-r--r--spec/frontend/vue_shared/components/markdown/header_spec.js22
-rw-r--r--spec/frontend/vue_shared/components/markdown/toolbar_spec.js14
-rw-r--r--spec/frontend/vue_shared/components/metric_images/__snapshots__/metric_images_table_spec.js.snap2
-rw-r--r--spec/frontend/vue_shared/components/registry/list_item_spec.js14
-rw-r--r--spec/frontend/vue_shared/components/runner_aws_deployments/__snapshots__/runner_aws_deployments_modal_spec.js.snap1
-rw-r--r--spec/frontend/vue_shared/components/runner_instructions/runner_instructions_modal_spec.js34
-rw-r--r--spec/frontend/vue_shared/components/segmented_control_button_group_spec.js104
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view_spec.js6
-rw-r--r--spec/frontend/vue_shared/components/usage_quotas/usage_banner_spec.js62
-rw-r--r--spec/frontend/vue_shared/components/user_popover/user_popover_spec.js140
-rw-r--r--spec/frontend/vue_shared/directives/autofocusonshow_spec.js7
-rw-r--r--spec/frontend/vue_shared/issuable/list/components/issuable_bulk_edit_sidebar_spec.js4
-rw-r--r--spec/frontend/vue_shared/issuable/show/components/issuable_edit_form_spec.js1
-rw-r--r--spec/frontend/vue_shared/issuable/show/components/issuable_header_spec.js77
-rw-r--r--spec/frontend/vue_shared/issuable/show/components/issuable_show_root_spec.js7
-rw-r--r--spec/frontend/vue_shared/issuable/show/mock_data.js5
-rw-r--r--spec/frontend/vue_shared/issuable/sidebar/components/issuable_sidebar_root_spec.js6
-rw-r--r--spec/frontend/vue_shared/security_configuration/components/section_layout_spec.js (renamed from spec/frontend/security_configuration/components/section_layout_spec.js)11
-rw-r--r--spec/frontend/vue_shared/security_reports/mock_data.js6
-rw-r--r--spec/frontend/whats_new/components/feature_spec.js25
-rw-r--r--spec/frontend/whats_new/utils/notification_spec.js4
-rw-r--r--spec/frontend/wikis_spec.js6
-rw-r--r--spec/frontend/work_items/components/item_state_spec.js54
-rw-r--r--spec/frontend/work_items/components/work_item_actions_spec.js55
-rw-r--r--spec/frontend/work_items/components/work_item_detail_modal_spec.js100
-rw-r--r--spec/frontend/work_items/components/work_item_state_spec.js117
-rw-r--r--spec/frontend/work_items/components/work_item_title_spec.js42
-rw-r--r--spec/frontend/work_items/mock_data.js23
-rw-r--r--spec/frontend/work_items/pages/create_work_item_spec.js7
-rw-r--r--spec/frontend/work_items/pages/work_item_detail_spec.js32
-rw-r--r--spec/frontend/work_items/pages/work_item_root_spec.js60
-rw-r--r--spec/frontend/work_items/router_spec.js1
-rw-r--r--spec/frontend/zen_mode_spec.js7
489 files changed, 9850 insertions, 7051 deletions
diff --git a/spec/frontend/.eslintrc.yml b/spec/frontend/.eslintrc.yml
index e12c4e5e820..45639f4c948 100644
--- a/spec/frontend/.eslintrc.yml
+++ b/spec/frontend/.eslintrc.yml
@@ -11,9 +11,6 @@ settings:
import/resolver:
jest:
jestConfigFile: 'jest.config.js'
-globals:
- loadFixtures: false
- setFixtures: false
rules:
jest/expect-expect:
- off
@@ -21,8 +18,6 @@ rules:
- 'expect*'
- 'assert*'
- 'testAction'
- jest/no-test-callback:
- - off
"@gitlab/no-global-event-off":
- off
import/no-unresolved:
diff --git a/spec/frontend/__helpers__/dom_shims/clipboard.js b/spec/frontend/__helpers__/dom_shims/clipboard.js
new file mode 100644
index 00000000000..3bc1b059272
--- /dev/null
+++ b/spec/frontend/__helpers__/dom_shims/clipboard.js
@@ -0,0 +1,5 @@
+Object.defineProperty(navigator, 'clipboard', {
+ value: {
+ writeText: () => {},
+ },
+});
diff --git a/spec/frontend/__helpers__/dom_shims/index.js b/spec/frontend/__helpers__/dom_shims/index.js
index 9b70cb86b8b..742d55196b4 100644
--- a/spec/frontend/__helpers__/dom_shims/index.js
+++ b/spec/frontend/__helpers__/dom_shims/index.js
@@ -1,3 +1,4 @@
+import './clipboard';
import './create_object_url';
import './element_scroll_into_view';
import './element_scroll_by';
diff --git a/spec/frontend/__helpers__/fixtures.js b/spec/frontend/__helpers__/fixtures.js
index d8054d32fae..a6f7b37161e 100644
--- a/spec/frontend/__helpers__/fixtures.js
+++ b/spec/frontend/__helpers__/fixtures.js
@@ -20,24 +20,15 @@ Did you run bin/rake frontend:fixtures?`,
return fs.readFileSync(absolutePath, 'utf8');
}
-/**
- * @deprecated Use `import` to load a JSON fixture instead.
- * See https://docs.gitlab.com/ee/development/testing_guide/frontend_testing.html#use-fixtures,
- * https://gitlab.com/gitlab-org/gitlab/-/issues/339346.
- */
-export const getJSONFixture = (relativePath) => JSON.parse(getFixture(relativePath));
-
export const resetHTMLFixture = () => {
document.head.innerHTML = '';
document.body.innerHTML = '';
};
-export const setHTMLFixture = (htmlContent, resetHook = afterEach) => {
+export const setHTMLFixture = (htmlContent) => {
document.body.innerHTML = htmlContent;
- resetHook(resetHTMLFixture);
};
-export const loadHTMLFixture = (relativePath, resetHook = afterEach) => {
- const fileContent = getFixture(relativePath);
- setHTMLFixture(fileContent, resetHook);
+export const loadHTMLFixture = (relativePath) => {
+ setHTMLFixture(getFixture(relativePath));
};
diff --git a/spec/frontend/__helpers__/flush_promises.js b/spec/frontend/__helpers__/flush_promises.js
deleted file mode 100644
index eefc2ed7c17..00000000000
--- a/spec/frontend/__helpers__/flush_promises.js
+++ /dev/null
@@ -1,4 +0,0 @@
-export default function flushPromises() {
- // eslint-disable-next-line no-restricted-syntax
- return new Promise(setImmediate);
-}
diff --git a/spec/frontend/__helpers__/shared_test_setup.js b/spec/frontend/__helpers__/shared_test_setup.js
index 7b5df18ee0f..011e1142c76 100644
--- a/spec/frontend/__helpers__/shared_test_setup.js
+++ b/spec/frontend/__helpers__/shared_test_setup.js
@@ -6,7 +6,6 @@ import 'jquery';
import Translate from '~/vue_shared/translate';
import setWindowLocation from './set_window_location_helper';
import { setGlobalDateToFakeDate } from './fake_date';
-import { loadHTMLFixture, setHTMLFixture } from './fixtures';
import { TEST_HOST } from './test_constants';
import * as customMatchers from './matchers';
@@ -28,12 +27,6 @@ Vue.config.productionTip = false;
Vue.use(Translate);
-// convenience wrapper for migration from Karma
-Object.assign(global, {
- loadFixtures: loadHTMLFixture,
- setFixtures: setHTMLFixture,
-});
-
const JQUERY_MATCHERS_TO_EXCLUDE = ['toHaveLength', 'toExist'];
// custom-jquery-matchers was written for an old Jest version, we need to make it compatible
diff --git a/spec/frontend/__helpers__/user_mock_data_helper.js b/spec/frontend/__helpers__/user_mock_data_helper.js
index db747283d9e..29ce95a88e2 100644
--- a/spec/frontend/__helpers__/user_mock_data_helper.js
+++ b/spec/frontend/__helpers__/user_mock_data_helper.js
@@ -15,7 +15,7 @@ export default {
id: id + 1,
name: getRandomString(),
username: getRandomString(),
- user_path: getRandomUrl(),
+ web_url: getRandomUrl(),
});
id += 1;
diff --git a/spec/frontend/__helpers__/vuex_action_helper.js b/spec/frontend/__helpers__/vuex_action_helper.js
index 95a811d0385..ab2637d6024 100644
--- a/spec/frontend/__helpers__/vuex_action_helper.js
+++ b/spec/frontend/__helpers__/vuex_action_helper.js
@@ -1,5 +1,3 @@
-const noop = () => {};
-
/**
* Helper for testing action with expected mutations inspired in
* https://vuex.vuejs.org/en/testing.html
@@ -9,7 +7,6 @@ const noop = () => {};
* @param {Object} state will be provided to the action
* @param {Array} [expectedMutations=[]] mutations expected to be committed
* @param {Array} [expectedActions=[]] actions expected to be dispatched
- * @param {Function} [done=noop] to be executed after the tests
* @return {Promise}
*
* @example
@@ -27,20 +24,9 @@ const noop = () => {};
* { type: 'actionName', payload: {param: 'foobar'}},
* { type: 'actionName1'}
* ]
- * done,
* );
*
* @example
- * testAction(
- * actions.actionName, // action
- * { }, // mocked payload
- * state, //state
- * [ { type: types.MUTATION} ], // expected mutations
- * [], // expected actions
- * ).then(done)
- * .catch(done.fail);
- *
- * @example
* await testAction({
* action: actions.actionName,
* payload: { deleteListId: 1 },
@@ -56,24 +42,15 @@ export default (
stateArg,
expectedMutationsArg = [],
expectedActionsArg = [],
- doneArg = noop,
) => {
let action = actionArg;
let payload = payloadArg;
let state = stateArg;
let expectedMutations = expectedMutationsArg;
let expectedActions = expectedActionsArg;
- let done = doneArg;
if (typeof actionArg !== 'function') {
- ({
- action,
- payload,
- state,
- expectedMutations = [],
- expectedActions = [],
- done = noop,
- } = actionArg);
+ ({ action, payload, state, expectedMutations = [], expectedActions = [] } = actionArg);
}
const mutations = [];
@@ -109,7 +86,6 @@ export default (
mutations: expectedMutations,
actions: expectedActions,
});
- done();
};
const result = action(
@@ -117,8 +93,13 @@ export default (
payload,
);
- // eslint-disable-next-line no-restricted-syntax
- return (result || new Promise((resolve) => setImmediate(resolve)))
+ return (
+ result ||
+ new Promise((resolve) => {
+ // eslint-disable-next-line no-restricted-syntax
+ setImmediate(resolve);
+ })
+ )
.catch((error) => {
validateResults();
throw error;
diff --git a/spec/frontend/__helpers__/vuex_action_helper_spec.js b/spec/frontend/__helpers__/vuex_action_helper_spec.js
index b4f5a291774..5bb2b3b26e2 100644
--- a/spec/frontend/__helpers__/vuex_action_helper_spec.js
+++ b/spec/frontend/__helpers__/vuex_action_helper_spec.js
@@ -4,8 +4,8 @@ import axios from '~/lib/utils/axios_utils';
import testActionFn from './vuex_action_helper';
const testActionFnWithOptionsArg = (...args) => {
- const [action, payload, state, expectedMutations, expectedActions, done] = args;
- return testActionFn({ action, payload, state, expectedMutations, expectedActions, done });
+ const [action, payload, state, expectedMutations, expectedActions] = args;
+ return testActionFn({ action, payload, state, expectedMutations, expectedActions });
};
describe.each([testActionFn, testActionFnWithOptionsArg])(
@@ -14,7 +14,6 @@ describe.each([testActionFn, testActionFnWithOptionsArg])(
let originalExpect;
let assertion;
let mock;
- const noop = () => {};
beforeEach(() => {
mock = new MockAdapter(axios);
@@ -48,7 +47,7 @@ describe.each([testActionFn, testActionFnWithOptionsArg])(
assertion = { mutations: [], actions: [] };
- testAction(action, examplePayload, exampleState);
+ return testAction(action, examplePayload, exampleState);
});
describe('given a sync action', () => {
@@ -59,7 +58,7 @@ describe.each([testActionFn, testActionFnWithOptionsArg])(
assertion = { mutations: [{ type: 'MUTATION' }], actions: [] };
- testAction(action, null, {}, assertion.mutations, assertion.actions, noop);
+ return testAction(action, null, {}, assertion.mutations, assertion.actions);
});
it('mocks dispatching actions', () => {
@@ -69,26 +68,21 @@ describe.each([testActionFn, testActionFnWithOptionsArg])(
assertion = { actions: [{ type: 'ACTION' }], mutations: [] };
- testAction(action, null, {}, assertion.mutations, assertion.actions, noop);
+ return testAction(action, null, {}, assertion.mutations, assertion.actions);
});
- it('works with done callback once finished', (done) => {
+ it('returns a promise', () => {
assertion = { mutations: [], actions: [] };
- testAction(noop, null, {}, assertion.mutations, assertion.actions, done);
- });
+ const promise = testAction(() => {}, null, {}, assertion.mutations, assertion.actions);
- it('returns a promise', (done) => {
- assertion = { mutations: [], actions: [] };
+ originalExpect(promise instanceof Promise).toBeTruthy();
- testAction(noop, null, {}, assertion.mutations, assertion.actions)
- .then(done)
- .catch(done.fail);
+ return promise;
});
});
describe('given an async action (returning a promise)', () => {
- let lastError;
const data = { FOO: 'BAR' };
const asyncAction = ({ commit, dispatch }) => {
@@ -98,7 +92,6 @@ describe.each([testActionFn, testActionFnWithOptionsArg])(
.get(TEST_HOST)
.catch((error) => {
commit('ERROR');
- lastError = error;
throw error;
})
.then(() => {
@@ -107,46 +100,26 @@ describe.each([testActionFn, testActionFnWithOptionsArg])(
});
};
- beforeEach(() => {
- lastError = null;
- });
-
- it('works with done callback once finished', (done) => {
+ it('returns original data of successful promise while checking actions/mutations', async () => {
mock.onGet(TEST_HOST).replyOnce(200, 42);
assertion = { mutations: [{ type: 'SUCCESS' }], actions: [{ type: 'ACTION' }] };
- testAction(asyncAction, null, {}, assertion.mutations, assertion.actions, done);
+ const res = await testAction(asyncAction, null, {}, assertion.mutations, assertion.actions);
+ originalExpect(res).toEqual(data);
});
- it('returns original data of successful promise while checking actions/mutations', (done) => {
- mock.onGet(TEST_HOST).replyOnce(200, 42);
-
- assertion = { mutations: [{ type: 'SUCCESS' }], actions: [{ type: 'ACTION' }] };
-
- testAction(asyncAction, null, {}, assertion.mutations, assertion.actions)
- .then((res) => {
- originalExpect(res).toEqual(data);
- done();
- })
- .catch(done.fail);
- });
-
- it('returns original error of rejected promise while checking actions/mutations', (done) => {
+ it('returns original error of rejected promise while checking actions/mutations', async () => {
mock.onGet(TEST_HOST).replyOnce(500, '');
assertion = { mutations: [{ type: 'ERROR' }], actions: [{ type: 'ACTION' }] };
- testAction(asyncAction, null, {}, assertion.mutations, assertion.actions)
- .then(done.fail)
- .catch((error) => {
- originalExpect(error).toBe(lastError);
- done();
- });
+ const err = testAction(asyncAction, null, {}, assertion.mutations, assertion.actions);
+ await originalExpect(err).rejects.toEqual(new Error('Request failed with status code 500'));
});
});
- it('works with async actions not returning promises', (done) => {
+ it('works with actions not returning promises', () => {
const data = { FOO: 'BAR' };
const asyncAction = ({ commit, dispatch }) => {
@@ -168,7 +141,7 @@ describe.each([testActionFn, testActionFnWithOptionsArg])(
assertion = { mutations: [{ type: 'SUCCESS' }], actions: [{ type: 'ACTION' }] };
- testAction(asyncAction, null, {}, assertion.mutations, assertion.actions, done);
+ return testAction(asyncAction, null, {}, assertion.mutations, assertion.actions);
});
},
);
diff --git a/spec/frontend/__helpers__/wait_for_promises.js b/spec/frontend/__helpers__/wait_for_promises.js
index 2fd1cc6ba0d..753c3c5d92b 100644
--- a/spec/frontend/__helpers__/wait_for_promises.js
+++ b/spec/frontend/__helpers__/wait_for_promises.js
@@ -1 +1,4 @@
-export default () => new Promise((resolve) => requestAnimationFrame(resolve));
+export default () =>
+ new Promise((resolve) => {
+ requestAnimationFrame(resolve);
+ });
diff --git a/spec/frontend/activities_spec.js b/spec/frontend/activities_spec.js
index 00519148b30..ebace21217a 100644
--- a/spec/frontend/activities_spec.js
+++ b/spec/frontend/activities_spec.js
@@ -1,6 +1,7 @@
/* eslint-disable no-unused-expressions, no-prototype-builtins, no-new, no-shadow */
import $ from 'jquery';
+import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import Activities from '~/activities';
import Pager from '~/pager';
@@ -38,11 +39,15 @@ describe('Activities', () => {
}
beforeEach(() => {
- loadFixtures(fixtureTemplate);
+ loadHTMLFixture(fixtureTemplate);
jest.spyOn(Pager, 'init').mockImplementation(() => {});
new Activities();
});
+ afterEach(() => {
+ resetHTMLFixture();
+ });
+
for (let i = 0; i < filters.length; i += 1) {
((i) => {
describe(`when selecting ${getEventName(i)}`, () => {
diff --git a/spec/frontend/add_context_commits_modal/components/__snapshots__/add_context_commits_modal_spec.js.snap b/spec/frontend/add_context_commits_modal/components/__snapshots__/add_context_commits_modal_spec.js.snap
index 82114077455..3fdbacb6efa 100644
--- a/spec/frontend/add_context_commits_modal/components/__snapshots__/add_context_commits_modal_spec.js.snap
+++ b/spec/frontend/add_context_commits_modal/components/__snapshots__/add_context_commits_modal_spec.js.snap
@@ -2,6 +2,7 @@
exports[`AddContextCommitsModal renders modal with 2 tabs 1`] = `
<gl-modal-stub
+ arialabel=""
body-class="add-review-item pt-0"
cancel-variant="light"
dismisslabel="Close"
diff --git a/spec/frontend/admin/applications/components/delete_application_spec.js b/spec/frontend/admin/applications/components/delete_application_spec.js
index 20119b64952..1a400a101b5 100644
--- a/spec/frontend/admin/applications/components/delete_application_spec.js
+++ b/spec/frontend/admin/applications/components/delete_application_spec.js
@@ -1,5 +1,6 @@
import { GlModal, GlSprintf } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import DeleteApplication from '~/admin/applications/components/delete_application.vue';
const path = 'application/path/1';
@@ -22,7 +23,7 @@ describe('DeleteApplication', () => {
const findForm = () => wrapper.find('form');
beforeEach(() => {
- setFixtures(`
+ setHTMLFixture(`
<button class="js-application-delete-button" data-path="${path}" data-name="${name}">Destroy</button>
`);
@@ -31,6 +32,7 @@ describe('DeleteApplication', () => {
afterEach(() => {
wrapper.destroy();
+ resetHTMLFixture();
});
describe('the modal component', () => {
diff --git a/spec/frontend/admin/background_migrations/components/database_listbox_spec.js b/spec/frontend/admin/background_migrations/components/database_listbox_spec.js
new file mode 100644
index 00000000000..3778943872e
--- /dev/null
+++ b/spec/frontend/admin/background_migrations/components/database_listbox_spec.js
@@ -0,0 +1,57 @@
+import { GlListbox } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import BackgroundMigrationsDatabaseListbox from '~/admin/background_migrations/components/database_listbox.vue';
+import { visitUrl, setUrlParams } from '~/lib/utils/url_utility';
+import { MOCK_DATABASES, MOCK_SELECTED_DATABASE } from '../mock_data';
+
+jest.mock('~/lib/utils/url_utility', () => ({
+ visitUrl: jest.fn(),
+ setUrlParams: jest.fn(),
+}));
+
+describe('BackgroundMigrationsDatabaseListbox', () => {
+ let wrapper;
+
+ const defaultProps = {
+ databases: MOCK_DATABASES,
+ selectedDatabase: MOCK_SELECTED_DATABASE,
+ };
+
+ const createComponent = (props = {}) => {
+ wrapper = shallowMount(BackgroundMigrationsDatabaseListbox, {
+ propsData: {
+ ...defaultProps,
+ ...props,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ const findGlListbox = () => wrapper.findComponent(GlListbox);
+
+ describe('template always', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders GlListbox', () => {
+ expect(findGlListbox().exists()).toBe(true);
+ });
+ });
+
+ describe('actions', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('selecting a listbox item fires visitUrl with the database param', () => {
+ findGlListbox().vm.$emit('select', MOCK_DATABASES[1].value);
+
+ expect(setUrlParams).toHaveBeenCalledWith({ database: MOCK_DATABASES[1].value });
+ expect(visitUrl).toHaveBeenCalled();
+ });
+ });
+});
diff --git a/spec/frontend/admin/background_migrations/mock_data.js b/spec/frontend/admin/background_migrations/mock_data.js
new file mode 100644
index 00000000000..fbb1718f6b8
--- /dev/null
+++ b/spec/frontend/admin/background_migrations/mock_data.js
@@ -0,0 +1,6 @@
+export const MOCK_DATABASES = [
+ { value: 'main', text: 'main' },
+ { value: 'ci', text: 'ci' },
+];
+
+export const MOCK_SELECTED_DATABASE = 'main';
diff --git a/spec/frontend/admin/users/new_spec.js b/spec/frontend/admin/users/new_spec.js
index 692c583dca8..5e5763822a8 100644
--- a/spec/frontend/admin/users/new_spec.js
+++ b/spec/frontend/admin/users/new_spec.js
@@ -4,6 +4,7 @@ import {
ID_USER_EXTERNAL,
ID_WARNING,
} from '~/admin/users/new';
+import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
describe('admin/users/new', () => {
const FIXTURE = 'admin/users/new_with_internal_user_regex.html';
@@ -13,7 +14,7 @@ describe('admin/users/new', () => {
let elWarningMessage;
beforeEach(() => {
- loadFixtures(FIXTURE);
+ loadHTMLFixture(FIXTURE);
setupInternalUserRegexHandler();
elExternal = document.getElementById(ID_USER_EXTERNAL);
@@ -23,6 +24,10 @@ describe('admin/users/new', () => {
elExternal.checked = true;
});
+ afterEach(() => {
+ resetHTMLFixture();
+ });
+
const changeEmail = (val) => {
elUserEmail.value = val;
elUserEmail.dispatchEvent(new Event('input'));
diff --git a/spec/frontend/alert_handler_spec.js b/spec/frontend/alert_handler_spec.js
index 228053b1b2b..ba8e5bcb202 100644
--- a/spec/frontend/alert_handler_spec.js
+++ b/spec/frontend/alert_handler_spec.js
@@ -1,4 +1,4 @@
-import { setHTMLFixture } from 'helpers/fixtures';
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import initAlertHandler from '~/alert_handler';
describe('Alert Handler', () => {
@@ -25,6 +25,10 @@ describe('Alert Handler', () => {
initAlertHandler();
});
+ afterEach(() => {
+ resetHTMLFixture();
+ });
+
it('should render the alert', () => {
expect(findFirstAlert()).not.toBe(null);
});
@@ -41,6 +45,10 @@ describe('Alert Handler', () => {
initAlertHandler();
});
+ afterEach(() => {
+ resetHTMLFixture();
+ });
+
it('should render two alerts', () => {
expect(findAllAlerts()).toHaveLength(2);
});
@@ -57,6 +65,10 @@ describe('Alert Handler', () => {
initAlertHandler();
});
+ afterEach(() => {
+ resetHTMLFixture();
+ });
+
it('should render the banner', () => {
expect(findFirstBanner()).not.toBe(null);
});
@@ -78,6 +90,10 @@ describe('Alert Handler', () => {
initAlertHandler();
});
+ afterEach(() => {
+ resetHTMLFixture();
+ });
+
it('should render the banner', () => {
expect(findFirstAlert()).not.toBe(null);
});
diff --git a/spec/frontend/analytics/shared/components/metric_popover_spec.js b/spec/frontend/analytics/shared/components/metric_popover_spec.js
index b799c911488..ffec77c2708 100644
--- a/spec/frontend/analytics/shared/components/metric_popover_spec.js
+++ b/spec/frontend/analytics/shared/components/metric_popover_spec.js
@@ -6,7 +6,7 @@ const MOCK_METRIC = {
key: 'deployment-frequency',
label: 'Deployment Frequency',
value: '10.0',
- unit: 'per day',
+ unit: '/day',
description: 'Average number of deployments to production per day.',
links: [],
};
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 386fb4eb616..69918c1db65 100644
--- a/spec/frontend/analytics/shared/components/projects_dropdown_filter_spec.js
+++ b/spec/frontend/analytics/shared/components/projects_dropdown_filter_spec.js
@@ -1,4 +1,4 @@
-import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
+import { GlDropdown, GlDropdownItem, GlTruncate } from '@gitlab/ui';
import { nextTick } from 'vue';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import { stubComponent } from 'helpers/stub_component';
@@ -76,8 +76,8 @@ describe('ProjectsDropdownFilter component', () => {
const findHighlightedItems = () => wrapper.findByTestId('vsa-highlighted-items');
const findUnhighlightedItems = () => wrapper.findByTestId('vsa-default-items');
- const findHighlightedItemsTitle = () => wrapper.findByText('Selected');
const findClearAllButton = () => wrapper.findByText('Clear all');
+ const findSelectedProjectsLabel = () => wrapper.findComponent(GlTruncate);
const findDropdown = () => wrapper.find(GlDropdown);
@@ -158,8 +158,8 @@ describe('ProjectsDropdownFilter component', () => {
expect(findSelectedDropdownItems().length).toBe(0);
});
- it('does not render the highlighted items title', () => {
- expect(findHighlightedItemsTitle().exists()).toBe(false);
+ it('renders the default project label text', () => {
+ expect(findSelectedProjectsLabel().text()).toBe('Select projects');
});
it('does not render the clear all button', () => {
@@ -180,7 +180,7 @@ describe('ProjectsDropdownFilter component', () => {
});
it('renders the highlighted items title', () => {
- expect(findHighlightedItemsTitle().exists()).toBe(true);
+ expect(findSelectedProjectsLabel().text()).toBe(projects[0].name);
});
it('renders the clear all button', () => {
@@ -190,13 +190,12 @@ describe('ProjectsDropdownFilter component', () => {
it('clears all selected items when the clear all button is clicked', async () => {
await selectDropdownItemAtIndex(1);
- expect(wrapper.text()).toContain('2 projects selected');
+ expect(findSelectedProjectsLabel().text()).toBe('2 projects selected');
findClearAllButton().trigger('click');
await nextTick();
- expect(wrapper.text()).not.toContain('2 projects selected');
- expect(wrapper.text()).toContain('Select projects');
+ expect(findSelectedProjectsLabel().text()).toBe('Select projects');
});
});
});
diff --git a/spec/frontend/api/tags_api_spec.js b/spec/frontend/api/tags_api_spec.js
new file mode 100644
index 00000000000..a7436bf6a50
--- /dev/null
+++ b/spec/frontend/api/tags_api_spec.js
@@ -0,0 +1,37 @@
+import MockAdapter from 'axios-mock-adapter';
+import * as tagsApi from '~/api/tags_api';
+import axios from '~/lib/utils/axios_utils';
+import httpStatus from '~/lib/utils/http_status';
+
+describe('~/api/tags_api.js', () => {
+ let mock;
+ let originalGon;
+
+ const projectId = 1;
+
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+
+ originalGon = window.gon;
+ window.gon = { api_version: 'v7' };
+ });
+
+ afterEach(() => {
+ mock.restore();
+ window.gon = originalGon;
+ });
+
+ describe('getTag', () => {
+ it('fetches a tag of a given tag name of a particular project', () => {
+ const tagName = 'tag-name';
+ const expectedUrl = `/api/v7/projects/${projectId}/repository/tags/${tagName}`;
+ mock.onGet(expectedUrl).reply(httpStatus.OK, {
+ name: tagName,
+ });
+
+ return tagsApi.getTag(projectId, tagName).then(({ data }) => {
+ expect(data.name).toBe(tagName);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/api/user_api_spec.js b/spec/frontend/api/user_api_spec.js
new file mode 100644
index 00000000000..ee7194bdf5f
--- /dev/null
+++ b/spec/frontend/api/user_api_spec.js
@@ -0,0 +1,50 @@
+import MockAdapter from 'axios-mock-adapter';
+
+import { followUser, unfollowUser } from '~/api/user_api';
+import axios from '~/lib/utils/axios_utils';
+
+describe('~/api/user_api', () => {
+ let axiosMock;
+ let originalGon;
+
+ beforeEach(() => {
+ axiosMock = new MockAdapter(axios);
+
+ originalGon = window.gon;
+ window.gon = { api_version: 'v4' };
+ });
+
+ afterEach(() => {
+ axiosMock.restore();
+ axiosMock.resetHistory();
+ window.gon = originalGon;
+ });
+
+ describe('followUser', () => {
+ it('calls correct URL and returns expected response', async () => {
+ const expectedUrl = '/api/v4/users/1/follow';
+ const expectedResponse = { message: 'Success' };
+
+ axiosMock.onPost(expectedUrl).replyOnce(200, expectedResponse);
+
+ await expect(followUser(1)).resolves.toEqual(
+ expect.objectContaining({ data: expectedResponse }),
+ );
+ expect(axiosMock.history.post[0].url).toBe(expectedUrl);
+ });
+ });
+
+ describe('unfollowUser', () => {
+ it('calls correct URL and returns expected response', async () => {
+ const expectedUrl = '/api/v4/users/1/unfollow';
+ const expectedResponse = { message: 'Success' };
+
+ axiosMock.onPost(expectedUrl).replyOnce(200, expectedResponse);
+
+ await expect(unfollowUser(1)).resolves.toEqual(
+ expect.objectContaining({ data: expectedResponse }),
+ );
+ expect(axiosMock.history.post[0].url).toBe(expectedUrl);
+ });
+ });
+});
diff --git a/spec/frontend/api_spec.js b/spec/frontend/api_spec.js
index 85332bf21d8..5f162f498c4 100644
--- a/spec/frontend/api_spec.js
+++ b/spec/frontend/api_spec.js
@@ -1593,6 +1593,38 @@ describe('Api', () => {
});
});
+ describe('uploadProjectSecureFile', () => {
+ it('uploads a secure file to a project', async () => {
+ const projectId = 1;
+ const secureFile = {
+ id: projectId,
+ title: 'File Name',
+ permissions: 'read_only',
+ checksum: '12345',
+ checksum_algorithm: 'sha256',
+ created_at: '2022-02-21T15:27:18',
+ };
+
+ const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects/${projectId}/secure_files`;
+ mock.onPost(expectedUrl).reply(httpStatus.OK, secureFile);
+ const { data } = await Api.uploadProjectSecureFile(projectId, 'some data');
+
+ expect(data).toEqual(secureFile);
+ });
+ });
+
+ describe('deleteProjectSecureFile', () => {
+ it('removes a secure file from a project', async () => {
+ const projectId = 1;
+ const secureFileId = 2;
+
+ const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects/${projectId}/secure_files/${secureFileId}`;
+ mock.onDelete(expectedUrl).reply(httpStatus.NO_CONTENT, '');
+ const { data } = await Api.deleteProjectSecureFile(projectId, secureFileId);
+ expect(data).toEqual('');
+ });
+ });
+
describe('dependency proxy cache', () => {
it('schedules the cache list for deletion', async () => {
const groupId = 1;
diff --git a/spec/frontend/attention_requests/components/navigation_popover_spec.js b/spec/frontend/attention_requests/components/navigation_popover_spec.js
index d0231afbdc4..e4d53d5dbdb 100644
--- a/spec/frontend/attention_requests/components/navigation_popover_spec.js
+++ b/spec/frontend/attention_requests/components/navigation_popover_spec.js
@@ -2,6 +2,7 @@ import { shallowMount } from '@vue/test-utils';
import { GlPopover, GlButton, GlSprintf, GlIcon } from '@gitlab/ui';
import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
import NavigationPopover from '~/attention_requests/components/navigation_popover.vue';
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import { makeMockUserCalloutDismisser } from 'helpers/mock_user_callout_dismisser';
let wrapper;
@@ -29,13 +30,14 @@ function createComponent(provideData = {}, shouldShowCallout = true) {
describe('Attention requests navigation popover', () => {
beforeEach(() => {
- setFixtures('<div><div class="js-test-popover"></div><div class="js-test"></div></div>');
+ setHTMLFixture('<div><div class="js-test-popover"></div><div class="js-test"></div></div>');
dismiss = jest.fn();
});
afterEach(() => {
wrapper.destroy();
wrapper = null;
+ resetHTMLFixture();
});
it('hides popover if callout is disabled', () => {
diff --git a/spec/frontend/authentication/u2f/authenticate_spec.js b/spec/frontend/authentication/u2f/authenticate_spec.js
index 31782899ce4..3ae7fcf1c49 100644
--- a/spec/frontend/authentication/u2f/authenticate_spec.js
+++ b/spec/frontend/authentication/u2f/authenticate_spec.js
@@ -1,4 +1,5 @@
import $ from 'jquery';
+import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import U2FAuthenticate from '~/authentication/u2f/authenticate';
import 'vendor/u2f';
import MockU2FDevice from './mock_u2f_device';
@@ -9,7 +10,7 @@ describe('U2FAuthenticate', () => {
let component;
beforeEach(() => {
- loadFixtures('u2f/authenticate.html');
+ loadHTMLFixture('u2f/authenticate.html');
u2fDevice = new MockU2FDevice();
container = $('#js-authenticate-token-2fa');
component = new U2FAuthenticate(
@@ -23,6 +24,10 @@ describe('U2FAuthenticate', () => {
);
});
+ afterEach(() => {
+ resetHTMLFixture();
+ });
+
describe('with u2f unavailable', () => {
let oldu2f;
diff --git a/spec/frontend/authentication/u2f/register_spec.js b/spec/frontend/authentication/u2f/register_spec.js
index 810396aa9fd..7ae3a2734cb 100644
--- a/spec/frontend/authentication/u2f/register_spec.js
+++ b/spec/frontend/authentication/u2f/register_spec.js
@@ -1,4 +1,5 @@
import $ from 'jquery';
+import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import U2FRegister from '~/authentication/u2f/register';
import 'vendor/u2f';
import MockU2FDevice from './mock_u2f_device';
@@ -9,13 +10,17 @@ describe('U2FRegister', () => {
let component;
beforeEach(() => {
- loadFixtures('u2f/register.html');
+ loadHTMLFixture('u2f/register.html');
u2fDevice = new MockU2FDevice();
container = $('#js-register-token-2fa');
component = new U2FRegister(container, {});
return component.start();
});
+ afterEach(() => {
+ resetHTMLFixture();
+ });
+
it('allows registering a U2F device', () => {
const setupButton = container.find('#js-setup-token-2fa-device');
diff --git a/spec/frontend/authentication/webauthn/authenticate_spec.js b/spec/frontend/authentication/webauthn/authenticate_spec.js
index 8b27560bbbe..b1f4e43e56d 100644
--- a/spec/frontend/authentication/webauthn/authenticate_spec.js
+++ b/spec/frontend/authentication/webauthn/authenticate_spec.js
@@ -1,4 +1,5 @@
import $ from 'jquery';
+import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import waitForPromises from 'helpers/wait_for_promises';
import WebAuthnAuthenticate from '~/authentication/webauthn/authenticate';
import MockWebAuthnDevice from './mock_webauthn_device';
@@ -34,7 +35,7 @@ describe('WebAuthnAuthenticate', () => {
};
beforeEach(() => {
- loadFixtures('webauthn/authenticate.html');
+ loadHTMLFixture('webauthn/authenticate.html');
fallbackElement = document.createElement('div');
fallbackElement.classList.add('js-2fa-form');
webAuthnDevice = new MockWebAuthnDevice();
@@ -62,6 +63,10 @@ describe('WebAuthnAuthenticate', () => {
submitSpy = jest.spyOn(HTMLFormElement.prototype, 'submit');
});
+ afterEach(() => {
+ resetHTMLFixture();
+ });
+
describe('with webauthn unavailable', () => {
let oldGetCredentials;
diff --git a/spec/frontend/authentication/webauthn/register_spec.js b/spec/frontend/authentication/webauthn/register_spec.js
index 0f8ea2b635f..95cb993fc70 100644
--- a/spec/frontend/authentication/webauthn/register_spec.js
+++ b/spec/frontend/authentication/webauthn/register_spec.js
@@ -1,4 +1,5 @@
import $ from 'jquery';
+import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import setWindowLocation from 'helpers/set_window_location_helper';
import waitForPromises from 'helpers/wait_for_promises';
import WebAuthnRegister from '~/authentication/webauthn/register';
@@ -23,7 +24,7 @@ describe('WebAuthnRegister', () => {
let component;
beforeEach(() => {
- loadFixtures('webauthn/register.html');
+ loadHTMLFixture('webauthn/register.html');
webAuthnDevice = new MockWebAuthnDevice();
container = $('#js-register-token-2fa');
component = new WebAuthnRegister(container, {
@@ -41,6 +42,10 @@ describe('WebAuthnRegister', () => {
component.start();
});
+ afterEach(() => {
+ resetHTMLFixture();
+ });
+
const findSetupButton = () => container.find('#js-setup-token-2fa-device');
const findMessage = () => container.find('p');
const findDeviceResponse = () => container.find('#js-device-response');
diff --git a/spec/frontend/awards_handler_spec.js b/spec/frontend/awards_handler_spec.js
index c4002ec11f3..5d657745615 100644
--- a/spec/frontend/awards_handler_spec.js
+++ b/spec/frontend/awards_handler_spec.js
@@ -1,5 +1,6 @@
import $ from 'jquery';
-import Cookies from 'js-cookie';
+import Cookies from '~/lib/utils/cookies';
+import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import { initEmojiMock, clearEmojiMock } from 'helpers/emoji';
import { useFakeRequestAnimationFrame } from 'helpers/fake_request_animation_frame';
import loadAwardsHandler from '~/awards_handler';
@@ -75,7 +76,7 @@ describe('AwardsHandler', () => {
beforeEach(async () => {
await initEmojiMock(emojiData);
- loadFixtures('snippets/show.html');
+ loadHTMLFixture('snippets/show.html');
awardsHandler = await loadAwardsHandler(true);
jest.spyOn(awardsHandler, 'postEmoji').mockImplementation((button, url, emoji, cb) => cb());
@@ -91,6 +92,8 @@ describe('AwardsHandler', () => {
$('body').removeAttr('data-page');
awardsHandler.destroy();
+
+ resetHTMLFixture();
});
describe('::showEmojiMenu', () => {
diff --git a/spec/frontend/badges/components/badge_form_spec.js b/spec/frontend/badges/components/badge_form_spec.js
index ba2ec775b61..6d8a00eb50b 100644
--- a/spec/frontend/badges/components/badge_form_spec.js
+++ b/spec/frontend/badges/components/badge_form_spec.js
@@ -1,5 +1,6 @@
import MockAdapter from 'axios-mock-adapter';
import Vue, { nextTick } from 'vue';
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import { DUMMY_IMAGE_URL, TEST_HOST } from 'helpers/test_constants';
import { mountComponentWithStore } from 'helpers/vue_mount_component_helper';
import BadgeForm from '~/badges/components/badge_form.vue';
@@ -16,7 +17,7 @@ describe('BadgeForm component', () => {
let vm;
beforeEach(() => {
- setFixtures(`
+ setHTMLFixture(`
<div id="dummy-element"></div>
`);
@@ -26,6 +27,7 @@ describe('BadgeForm component', () => {
afterEach(() => {
vm.$destroy();
axiosMock.restore();
+ resetHTMLFixture();
});
describe('methods', () => {
diff --git a/spec/frontend/badges/components/badge_list_row_spec.js b/spec/frontend/badges/components/badge_list_row_spec.js
index 0fb0fa86a02..ad8426f3168 100644
--- a/spec/frontend/badges/components/badge_list_row_spec.js
+++ b/spec/frontend/badges/components/badge_list_row_spec.js
@@ -1,4 +1,5 @@
import Vue, { nextTick } from 'vue';
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import { mountComponentWithStore } from 'helpers/vue_mount_component_helper';
import BadgeListRow from '~/badges/components/badge_list_row.vue';
import { GROUP_BADGE, PROJECT_BADGE } from '~/badges/constants';
@@ -11,7 +12,7 @@ describe('BadgeListRow component', () => {
let vm;
beforeEach(() => {
- setFixtures(`
+ setHTMLFixture(`
<div id="delete-badge-modal" class="modal"></div>
<div id="dummy-element"></div>
`);
@@ -29,6 +30,7 @@ describe('BadgeListRow component', () => {
afterEach(() => {
vm.$destroy();
+ resetHTMLFixture();
});
it('renders the badge', () => {
diff --git a/spec/frontend/badges/components/badge_list_spec.js b/spec/frontend/badges/components/badge_list_spec.js
index 39fa502b207..32cd9483ef8 100644
--- a/spec/frontend/badges/components/badge_list_spec.js
+++ b/spec/frontend/badges/components/badge_list_spec.js
@@ -1,4 +1,5 @@
import Vue, { nextTick } from 'vue';
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import { mountComponentWithStore } from 'helpers/vue_mount_component_helper';
import BadgeList from '~/badges/components/badge_list.vue';
import { GROUP_BADGE, PROJECT_BADGE } from '~/badges/constants';
@@ -11,7 +12,7 @@ describe('BadgeList component', () => {
let vm;
beforeEach(() => {
- setFixtures('<div id="dummy-element"></div>');
+ setHTMLFixture('<div id="dummy-element"></div>');
const badges = [];
for (let id = 0; id < numberOfDummyBadges; id += 1) {
badges.push({ id, ...createDummyBadge() });
@@ -34,6 +35,7 @@ describe('BadgeList component', () => {
afterEach(() => {
vm.$destroy();
+ resetHTMLFixture();
});
it('renders a header with the badge count', () => {
diff --git a/spec/frontend/badges/components/badge_spec.js b/spec/frontend/badges/components/badge_spec.js
index fe4cf8ce8eb..19b3a9f23a6 100644
--- a/spec/frontend/badges/components/badge_spec.js
+++ b/spec/frontend/badges/components/badge_spec.js
@@ -1,4 +1,5 @@
import Vue, { nextTick } from 'vue';
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import mountComponent from 'helpers/vue_mount_component_helper';
import { DUMMY_IMAGE_URL, TEST_HOST } from 'spec/test_constants';
import Badge from '~/badges/components/badge.vue';
@@ -90,10 +91,14 @@ describe('Badge component', () => {
describe('behavior', () => {
beforeEach(() => {
- setFixtures('<div id="dummy-element"></div>');
+ setHTMLFixture('<div id="dummy-element"></div>');
return createComponent({ ...dummyProps }, '#dummy-element');
});
+ afterEach(() => {
+ resetHTMLFixture();
+ });
+
it('shows a badge image after loading', () => {
expect(vm.isLoading).toBe(false);
expect(vm.hasError).toBe(false);
diff --git a/spec/frontend/badges/store/actions_spec.js b/spec/frontend/badges/store/actions_spec.js
index 02e1b8e65e4..b799273ff63 100644
--- a/spec/frontend/badges/store/actions_spec.js
+++ b/spec/frontend/badges/store/actions_spec.js
@@ -371,10 +371,8 @@ describe('Badges store actions', () => {
const url = axios.get.mock.calls[0][0];
expect(url).toMatch(new RegExp(`^${dummyEndpointUrl}/render?`));
- expect(url).toMatch(
- new RegExp('\\?link_url=%3Cscript%3EI%20am%20dangerous!%3C%2Fscript%3E&'),
- );
- expect(url).toMatch(new RegExp('&image_url=%26make-sandwich%3Dtrue$'));
+ expect(url).toMatch(/\\?link_url=%3Cscript%3EI%20am%20dangerous!%3C%2Fscript%3E&/);
+ expect(url).toMatch(/&image_url=%26make-sandwich%3Dtrue$/);
});
it('dispatches requestRenderedBadge and receiveRenderedBadge for successful response', async () => {
diff --git a/spec/frontend/behaviors/autosize_spec.js b/spec/frontend/behaviors/autosize_spec.js
index a9dbee7fd08..7008b7b2eb6 100644
--- a/spec/frontend/behaviors/autosize_spec.js
+++ b/spec/frontend/behaviors/autosize_spec.js
@@ -1,4 +1,5 @@
import '~/behaviors/autosize';
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
jest.mock('~/helpers/startup_css_helper', () => {
return {
@@ -20,19 +21,22 @@ jest.mock('~/helpers/startup_css_helper', () => {
describe('Autosize behavior', () => {
beforeEach(() => {
- setFixtures('<textarea class="js-autosize"></textarea>');
+ setHTMLFixture('<textarea class="js-autosize"></textarea>');
+ });
+
+ afterEach(() => {
+ resetHTMLFixture();
});
it('is applied to the textarea', () => {
// This is the second part of the Hack:
// Because we are forcing the mock for WaitForCSSLoaded and the very end of our callstack
// to call its callback. This querySelector needs to go to the very end of our callstack
- // as well, if we would not have this setTimeout Function here, the querySelector
- // would run before the mockImplementation called its callBack Function
- // the DOM Manipulation didn't happen yet and the test would fail.
- setTimeout(() => {
- const textarea = document.querySelector('textarea');
- expect(textarea.classList).toContain('js-autosize-initialized');
- }, 0);
+ // as well, if we would not have this jest.runOnlyPendingTimers here, the querySelector
+ // would not run and the test would fail.
+ jest.runOnlyPendingTimers();
+
+ const textarea = document.querySelector('textarea');
+ expect(textarea.classList).toContain('js-autosize-initialized');
});
});
diff --git a/spec/frontend/behaviors/copy_as_gfm_spec.js b/spec/frontend/behaviors/copy_as_gfm_spec.js
index c96db09cc76..2032faa1c33 100644
--- a/spec/frontend/behaviors/copy_as_gfm_spec.js
+++ b/spec/frontend/behaviors/copy_as_gfm_spec.js
@@ -8,6 +8,9 @@ describe('CopyAsGFM', () => {
beforeEach(() => {
target = document.createElement('input');
target.value = 'This is code: ';
+
+ // needed for the underlying insertText to work
+ document.execCommand = jest.fn(() => false);
});
// When GFM code is copied, we put the regular plain text
diff --git a/spec/frontend/behaviors/date_picker_spec.js b/spec/frontend/behaviors/date_picker_spec.js
index 9f7701a0366..363052ad7fb 100644
--- a/spec/frontend/behaviors/date_picker_spec.js
+++ b/spec/frontend/behaviors/date_picker_spec.js
@@ -1,4 +1,5 @@
import * as Pikaday from 'pikaday';
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import initDatePickers from '~/behaviors/date_picker';
import * as utils from '~/lib/utils/datetime_utility';
@@ -12,7 +13,7 @@ describe('date_picker behavior', () => {
beforeEach(() => {
pikadayMock = jest.spyOn(Pikaday, 'default');
parseMock = jest.spyOn(utils, 'parsePikadayDate');
- setFixtures(`
+ setHTMLFixture(`
<div>
<input class="datepicker" value="2020-10-01" />
</div>
@@ -21,6 +22,10 @@ describe('date_picker behavior', () => {
</div>`);
});
+ afterEach(() => {
+ resetHTMLFixture();
+ });
+
it('Instantiates Pickaday for every instance of a .datepicker class', () => {
initDatePickers();
diff --git a/spec/frontend/behaviors/load_startup_css_spec.js b/spec/frontend/behaviors/load_startup_css_spec.js
index 59f49585645..e9e4c06732f 100644
--- a/spec/frontend/behaviors/load_startup_css_spec.js
+++ b/spec/frontend/behaviors/load_startup_css_spec.js
@@ -1,4 +1,4 @@
-import { setHTMLFixture } from 'helpers/fixtures';
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import { loadStartupCSS } from '~/behaviors/load_startup_css';
describe('behaviors/load_startup_css', () => {
@@ -25,6 +25,10 @@ describe('behaviors/load_startup_css', () => {
loadStartupCSS();
});
+ afterEach(() => {
+ resetHTMLFixture();
+ });
+
it('does nothing at first', () => {
expect(loadListener).not.toHaveBeenCalled();
});
diff --git a/spec/frontend/behaviors/markdown/highlight_current_user_spec.js b/spec/frontend/behaviors/markdown/highlight_current_user_spec.js
index 3305ddc412d..38d19ac3808 100644
--- a/spec/frontend/behaviors/markdown/highlight_current_user_spec.js
+++ b/spec/frontend/behaviors/markdown/highlight_current_user_spec.js
@@ -1,3 +1,4 @@
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import highlightCurrentUser from '~/behaviors/markdown/highlight_current_user';
describe('highlightCurrentUser', () => {
@@ -5,7 +6,7 @@ describe('highlightCurrentUser', () => {
let elements;
beforeEach(() => {
- setFixtures(`
+ setHTMLFixture(`
<div id="dummy-root-element">
<div data-user="1">@first</div>
<div data-user="2">@second</div>
@@ -15,6 +16,10 @@ describe('highlightCurrentUser', () => {
elements = rootElement.querySelectorAll('[data-user]');
});
+ afterEach(() => {
+ resetHTMLFixture();
+ });
+
describe('without current user', () => {
beforeEach(() => {
window.gon = window.gon || {};
diff --git a/spec/frontend/behaviors/markdown/render_sandboxed_mermaid_spec.js b/spec/frontend/behaviors/markdown/render_sandboxed_mermaid_spec.js
new file mode 100644
index 00000000000..2b9442162aa
--- /dev/null
+++ b/spec/frontend/behaviors/markdown/render_sandboxed_mermaid_spec.js
@@ -0,0 +1,34 @@
+import $ from 'jquery';
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
+import renderMermaid from '~/behaviors/markdown/render_sandboxed_mermaid';
+
+describe('Render mermaid diagrams for Gitlab Flavoured Markdown', () => {
+ it('Does something', () => {
+ document.body.dataset.page = '';
+ setHTMLFixture(`
+ <div class="gl-relative markdown-code-block js-markdown-code">
+ <pre data-sourcepos="1:1-7:3" class="code highlight js-syntax-highlight language-mermaid white" lang="mermaid" id="code-4">
+ <code class="js-render-mermaid">
+ <span id="LC1" class="line" lang="mermaid">graph TD;</span>
+ <span id="LC2" class="line" lang="mermaid">A--&gt;B</span>
+ <span id="LC3" class="line" lang="mermaid">A--&gt;C</span>
+ <span id="LC4" class="line" lang="mermaid">B--&gt;D</span>
+ <span id="LC5" class="line" lang="mermaid">C--&gt;D</span>
+ </code>
+ </pre>
+ <copy-code>
+ <button type="button" class="btn btn-default btn-md gl-button btn-icon has-tooltip" data-title="Copy to clipboard" data-clipboard-target="pre#code-4">
+ <svg><use xlink:href="/assets/icons-7f1680a3670112fe4c8ef57b9dfb93f0f61b43a2a479d7abd6c83bcb724b9201.svg#copy-to-clipboard"></use></svg>
+ </button>
+ </copy-code>
+ </div>`);
+ const els = $('pre.js-syntax-highlight').find('.js-render-mermaid');
+
+ renderMermaid(els);
+
+ jest.runAllTimers();
+ expect(document.querySelector('pre.js-syntax-highlight').classList).toContain('gl-sr-only');
+
+ resetHTMLFixture();
+ });
+});
diff --git a/spec/frontend/behaviors/quick_submit_spec.js b/spec/frontend/behaviors/quick_submit_spec.js
index 86a85831c6b..317c671cd2b 100644
--- a/spec/frontend/behaviors/quick_submit_spec.js
+++ b/spec/frontend/behaviors/quick_submit_spec.js
@@ -1,4 +1,5 @@
import $ from 'jquery';
+import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import '~/behaviors/quick_submit';
describe('Quick Submit behavior', () => {
@@ -7,7 +8,7 @@ describe('Quick Submit behavior', () => {
const keydownEvent = (options = { keyCode: 13, metaKey: true }) => $.Event('keydown', options);
beforeEach(() => {
- loadFixtures('snippets/show.html');
+ loadHTMLFixture('snippets/show.html');
testContext = {};
@@ -24,6 +25,10 @@ describe('Quick Submit behavior', () => {
testContext.textarea = $('.js-quick-submit textarea').first();
});
+ afterEach(() => {
+ resetHTMLFixture();
+ });
+
it('does not respond to other keyCodes', () => {
testContext.textarea.trigger(
keydownEvent({
diff --git a/spec/frontend/behaviors/requires_input_spec.js b/spec/frontend/behaviors/requires_input_spec.js
index bb22133ae44..f2f68f17d1c 100644
--- a/spec/frontend/behaviors/requires_input_spec.js
+++ b/spec/frontend/behaviors/requires_input_spec.js
@@ -1,14 +1,19 @@
import $ from 'jquery';
+import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import '~/behaviors/requires_input';
describe('requiresInput', () => {
let submitButton;
beforeEach(() => {
- loadFixtures('branches/new_branch.html');
+ loadHTMLFixture('branches/new_branch.html');
submitButton = $('button[type="submit"]');
});
+ afterEach(() => {
+ resetHTMLFixture();
+ });
+
it('disables submit when any field is required', () => {
$('.js-requires-input').requiresInput();
diff --git a/spec/frontend/behaviors/shortcuts/shortcuts_issuable_spec.js b/spec/frontend/behaviors/shortcuts/shortcuts_issuable_spec.js
index e1811247124..e6e587ff44b 100644
--- a/spec/frontend/behaviors/shortcuts/shortcuts_issuable_spec.js
+++ b/spec/frontend/behaviors/shortcuts/shortcuts_issuable_spec.js
@@ -1,5 +1,5 @@
import $ from 'jquery';
-import Mousetrap from 'mousetrap';
+import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import waitForPromises from 'helpers/wait_for_promises';
import initCopyAsGFM, { CopyAsGFM } from '~/behaviors/markdown/copy_as_gfm';
import ShortcutsIssuable from '~/behaviors/shortcuts/shortcuts_issuable';
@@ -12,7 +12,6 @@ jest.mock('~/lib/utils/common_utils', () => ({
describe('ShortcutsIssuable', () => {
const snippetShowFixtureName = 'snippets/show.html';
- const mrShowFixtureName = 'merge_requests/merge_request_of_current_user.html';
beforeAll(() => {
initCopyAsGFM();
@@ -25,7 +24,7 @@ describe('ShortcutsIssuable', () => {
const FORM_SELECTOR = '.js-main-target-form .js-vue-comment-form';
beforeEach(() => {
- loadFixtures(snippetShowFixtureName);
+ loadHTMLFixture(snippetShowFixtureName);
$('body').append(
`<div class="js-main-target-form">
<textarea class="js-vue-comment-form"></textarea>
@@ -40,6 +39,7 @@ describe('ShortcutsIssuable', () => {
$(FORM_SELECTOR).remove();
delete window.shortcut;
+ resetHTMLFixture();
});
// Stub getSelectedFragment to return a node with the provided HTML.
@@ -280,55 +280,4 @@ describe('ShortcutsIssuable', () => {
});
});
});
-
- describe('copyBranchName', () => {
- let sidebarCollapsedBtn;
- let sidebarExpandedBtn;
-
- beforeEach(() => {
- loadFixtures(mrShowFixtureName);
-
- window.shortcut = new ShortcutsIssuable();
-
- [sidebarCollapsedBtn, sidebarExpandedBtn] = document.querySelectorAll(
- '.js-sidebar-source-branch button',
- );
-
- [sidebarCollapsedBtn, sidebarExpandedBtn].forEach((btn) => jest.spyOn(btn, 'click'));
- });
-
- afterEach(() => {
- delete window.shortcut;
- });
-
- describe('when the sidebar is expanded', () => {
- beforeEach(() => {
- // simulate the applied CSS styles when the
- // sidebar is expanded
- sidebarCollapsedBtn.style.display = 'none';
-
- Mousetrap.trigger('b');
- });
-
- it('clicks the "expanded" version of the copy source branch button', () => {
- expect(sidebarExpandedBtn.click).toHaveBeenCalled();
- expect(sidebarCollapsedBtn.click).not.toHaveBeenCalled();
- });
- });
-
- describe('when the sidebar is collapsed', () => {
- beforeEach(() => {
- // simulate the applied CSS styles when the
- // sidebar is collapsed
- sidebarExpandedBtn.style.display = 'none';
-
- Mousetrap.trigger('b');
- });
-
- it('clicks the "collapsed" version of the copy source branch button', () => {
- expect(sidebarCollapsedBtn.click).toHaveBeenCalled();
- expect(sidebarExpandedBtn.click).not.toHaveBeenCalled();
- });
- });
- });
});
diff --git a/spec/frontend/blob/blob_file_dropzone_spec.js b/spec/frontend/blob/blob_file_dropzone_spec.js
index 47c90030e18..d6fc824258b 100644
--- a/spec/frontend/blob/blob_file_dropzone_spec.js
+++ b/spec/frontend/blob/blob_file_dropzone_spec.js
@@ -1,4 +1,5 @@
import $ from 'jquery';
+import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import BlobFileDropzone from '~/blob/blob_file_dropzone';
describe('BlobFileDropzone', () => {
@@ -6,7 +7,7 @@ describe('BlobFileDropzone', () => {
let replaceFileButton;
beforeEach(() => {
- loadFixtures('blob/show.html');
+ loadHTMLFixture('blob/show.html');
const form = $('.js-upload-blob-form');
// eslint-disable-next-line no-new
new BlobFileDropzone(form, 'POST');
@@ -15,6 +16,10 @@ describe('BlobFileDropzone', () => {
replaceFileButton = $('#submit-all');
});
+ afterEach(() => {
+ resetHTMLFixture();
+ });
+
describe('submit button', () => {
it('requires file', () => {
jest.spyOn(window, 'alert').mockImplementation(() => {});
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 5926836d9c1..b430dc15557 100644
--- a/spec/frontend/blob/components/__snapshots__/blob_header_spec.js.snap
+++ b/spec/frontend/blob/components/__snapshots__/blob_header_spec.js.snap
@@ -18,7 +18,7 @@ exports[`Blob Header Default Actions rendering matches the snapshot 1`] = `
</div>
<div
- class="gl-sm-display-flex file-actions"
+ class="gl-display-flex gl-flex-wrap file-actions"
>
<viewer-switcher-stub
docicon="document"
diff --git a/spec/frontend/blob/components/table_contents_spec.js b/spec/frontend/blob/components/table_contents_spec.js
index ade35d39b4f..358ac31819c 100644
--- a/spec/frontend/blob/components/table_contents_spec.js
+++ b/spec/frontend/blob/components/table_contents_spec.js
@@ -1,6 +1,7 @@
import { GlDropdownItem } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import TableContents from '~/blob/components/table_contents.vue';
let wrapper;
@@ -17,7 +18,7 @@ async function setLoaded(loaded) {
describe('Markdown table of contents component', () => {
beforeEach(() => {
- setFixtures(`
+ setHTMLFixture(`
<div class="blob-viewer" data-type="rich" data-loaded="false">
<h1><a href="#1"></a>Hello</h1>
<h2><a href="#2"></a>World</h2>
@@ -29,6 +30,7 @@ describe('Markdown table of contents component', () => {
afterEach(() => {
wrapper.destroy();
+ resetHTMLFixture();
});
describe('not loaded', () => {
diff --git a/spec/frontend/blob/file_template_mediator_spec.js b/spec/frontend/blob/file_template_mediator_spec.js
index 44e12deb564..907a3c97799 100644
--- a/spec/frontend/blob/file_template_mediator_spec.js
+++ b/spec/frontend/blob/file_template_mediator_spec.js
@@ -1,3 +1,4 @@
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import TemplateSelectorMediator from '~/blob/file_template_mediator';
describe('Template Selector Mediator', () => {
@@ -11,7 +12,7 @@ describe('Template Selector Mediator', () => {
}))();
beforeEach(() => {
- setFixtures('<div class="file-editor"><input class="js-file-path-name-input" /></div>');
+ setHTMLFixture('<div class="file-editor"><input class="js-file-path-name-input" /></div>');
input = document.querySelector('.js-file-path-name-input');
mediator = new TemplateSelectorMediator({
editor,
@@ -20,6 +21,10 @@ describe('Template Selector Mediator', () => {
});
});
+ afterEach(() => {
+ resetHTMLFixture();
+ });
+
it('fills out the input field', () => {
expect(input.value).toBe('');
mediator.setFilename(newFileName);
diff --git a/spec/frontend/blob/file_template_selector_spec.js b/spec/frontend/blob/file_template_selector_spec.js
index 2ab3b3ebc82..65444e86efd 100644
--- a/spec/frontend/blob/file_template_selector_spec.js
+++ b/spec/frontend/blob/file_template_selector_spec.js
@@ -1,10 +1,11 @@
-import $ from 'jquery';
import FileTemplateSelector from '~/blob/file_template_selector';
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
describe('FileTemplateSelector', () => {
let subject;
- let dropdown;
- let wrapper;
+
+ const dropdown = '.dropdown';
+ const wrapper = '.wrapper';
const createSubject = () => {
subject = new FileTemplateSelector({});
@@ -17,13 +18,16 @@ describe('FileTemplateSelector', () => {
afterEach(() => {
subject = null;
+ resetHTMLFixture();
});
describe('show method', () => {
beforeEach(() => {
- dropdown = document.createElement('div');
- wrapper = document.createElement('div');
- wrapper.classList.add('hidden');
+ setHTMLFixture(`
+ <div class="wrapper hidden">
+ <div class="dropdown"></div>
+ </div>
+ `);
createSubject();
});
@@ -37,25 +41,24 @@ describe('FileTemplateSelector', () => {
it('does not call init on subsequent calls', () => {
jest.spyOn(subject, 'init');
subject.show();
- subject.show();
expect(subject.init).toHaveBeenCalledTimes(1);
});
- it('removes hidden class from $wrapper', () => {
- expect($(wrapper).hasClass('hidden')).toBe(true);
+ it('removes hidden class from wrapper', () => {
+ subject.init();
+ expect(subject.wrapper.classList.contains('hidden')).toBe(true);
subject.show();
-
- expect($(wrapper).hasClass('hidden')).toBe(false);
+ expect(subject.wrapper.classList.contains('hidden')).toBe(false);
});
it('sets the focus on the dropdown', async () => {
subject.show();
- jest.spyOn(subject.$dropdown, 'focus');
+ jest.spyOn(subject.dropdown, 'focus');
jest.runAllTimers();
- expect(subject.$dropdown.focus).toHaveBeenCalled();
+ expect(subject.dropdown.focus).toHaveBeenCalled();
});
});
});
diff --git a/spec/frontend/blob/line_highlighter_spec.js b/spec/frontend/blob/line_highlighter_spec.js
index 330f1f3137e..21d4e8db503 100644
--- a/spec/frontend/blob/line_highlighter_spec.js
+++ b/spec/frontend/blob/line_highlighter_spec.js
@@ -1,6 +1,7 @@
/* eslint-disable no-return-assign, no-new, no-underscore-dangle */
import $ from 'jquery';
+import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import LineHighlighter from '~/blob/line_highlighter';
import * as utils from '~/lib/utils/common_utils';
@@ -14,8 +15,9 @@ describe('LineHighlighter', () => {
const e = $.Event('click', eventData);
return $(`#L${number}`).trigger(e);
};
+
beforeEach(() => {
- loadFixtures('static/line_highlighter.html');
+ loadHTMLFixture('static/line_highlighter.html');
testContext.class = new LineHighlighter();
testContext.css = testContext.class.highlightLineClass;
return (testContext.spies = {
@@ -25,6 +27,10 @@ describe('LineHighlighter', () => {
});
});
+ afterEach(() => {
+ resetHTMLFixture();
+ });
+
describe('behavior', () => {
it('highlights one line given in the URL hash', () => {
new LineHighlighter({ hash: '#L13' });
diff --git a/spec/frontend/blob/openapi/index_spec.js b/spec/frontend/blob/openapi/index_spec.js
new file mode 100644
index 00000000000..53220809f80
--- /dev/null
+++ b/spec/frontend/blob/openapi/index_spec.js
@@ -0,0 +1,28 @@
+import { SwaggerUIBundle } from 'swagger-ui-dist';
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
+import renderOpenApi from '~/blob/openapi';
+
+jest.mock('swagger-ui-dist');
+
+describe('OpenAPI blob viewer', () => {
+ const id = 'js-openapi-viewer';
+ const mockEndpoint = 'some/endpoint';
+
+ beforeEach(() => {
+ setHTMLFixture(`<div id="${id}" data-endpoint="${mockEndpoint}"></div>`);
+ renderOpenApi();
+ });
+
+ afterEach(() => {
+ resetHTMLFixture();
+ });
+
+ it('initializes SwaggerUI with the correct configuration', () => {
+ expect(SwaggerUIBundle).toHaveBeenCalledWith({
+ url: mockEndpoint,
+ dom_id: `#${id}`,
+ deepLinking: true,
+ displayOperationId: true,
+ });
+ });
+});
diff --git a/spec/frontend/blob/pipeline_tour_success_modal_spec.js b/spec/frontend/blob/pipeline_tour_success_modal_spec.js
index f4af57de41f..750dd8f0a72 100644
--- a/spec/frontend/blob/pipeline_tour_success_modal_spec.js
+++ b/spec/frontend/blob/pipeline_tour_success_modal_spec.js
@@ -1,6 +1,6 @@
import { GlSprintf, GlModal, GlLink } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import Cookies from 'js-cookie';
+import Cookies from '~/lib/utils/cookies';
import { stubComponent } from 'helpers/stub_component';
import { mockTracking, triggerEvent, unmockTracking } from 'helpers/tracking_helper';
import pipelineTourSuccess from '~/blob/pipeline_tour_success_modal.vue';
diff --git a/spec/frontend/blob/sketch/index_spec.js b/spec/frontend/blob/sketch/index_spec.js
index 7424897b22c..5e1922a24f4 100644
--- a/spec/frontend/blob/sketch/index_spec.js
+++ b/spec/frontend/blob/sketch/index_spec.js
@@ -1,20 +1,34 @@
-import JSZip from 'jszip';
import SketchLoader from '~/blob/sketch';
-
-jest.mock('jszip');
+import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
+import waitForPromises from 'helpers/wait_for_promises';
+
+jest.mock('jszip', () => {
+ return {
+ loadAsync: jest.fn().mockResolvedValue({
+ files: {
+ 'previews/preview.png': {
+ async: jest.fn().mockResolvedValue('foo'),
+ },
+ },
+ }),
+ };
+});
describe('Sketch viewer', () => {
beforeEach(() => {
- loadFixtures('static/sketch_viewer.html');
+ loadHTMLFixture('static/sketch_viewer.html');
+ });
+
+ afterEach(() => {
+ resetHTMLFixture();
});
describe('with error message', () => {
- beforeEach((done) => {
+ beforeEach(() => {
jest.spyOn(SketchLoader.prototype, 'getZipFile').mockImplementation(
() =>
new Promise((resolve, reject) => {
reject();
- done();
}),
);
@@ -35,26 +49,12 @@ describe('Sketch viewer', () => {
});
describe('success', () => {
- beforeEach((done) => {
- const loadAsyncMock = {
- files: {
- 'previews/preview.png': {
- async: jest.fn(),
- },
- },
- };
-
- loadAsyncMock.files['previews/preview.png'].async.mockImplementation(
- () =>
- new Promise((resolve) => {
- resolve('foo');
- done();
- }),
- );
-
+ beforeEach(() => {
jest.spyOn(SketchLoader.prototype, 'getZipFile').mockResolvedValue();
- jest.spyOn(JSZip, 'loadAsync').mockResolvedValue(loadAsyncMock);
- return new SketchLoader(document.getElementById('js-sketch-viewer'));
+ // eslint-disable-next-line no-new
+ new SketchLoader(document.getElementById('js-sketch-viewer'));
+
+ return waitForPromises();
});
it('does not render error message', () => {
diff --git a/spec/frontend/blob/viewer/index_spec.js b/spec/frontend/blob/viewer/index_spec.js
index fe55a537b89..5f6baf3f63d 100644
--- a/spec/frontend/blob/viewer/index_spec.js
+++ b/spec/frontend/blob/viewer/index_spec.js
@@ -2,6 +2,7 @@
import MockAdapter from 'axios-mock-adapter';
import $ from 'jquery';
+import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import { setTestTimeout } from 'helpers/timeout';
import { BlobViewer } from '~/blob/viewer/index';
import axios from '~/lib/utils/axios_utils';
@@ -26,7 +27,7 @@ describe('Blob viewer', () => {
$.fn.extend(jQueryMock);
mock = new MockAdapter(axios);
- loadFixtures('blob/show_readme.html');
+ loadHTMLFixture('blob/show_readme.html');
$('#modal-upload-blob').remove();
mock.onGet(/blob\/.+\/README\.md/).reply(200, {
@@ -39,6 +40,8 @@ describe('Blob viewer', () => {
afterEach(() => {
mock.restore();
window.location.hash = '';
+
+ resetHTMLFixture();
});
it('loads source file after switching views', async () => {
diff --git a/spec/frontend/blob_edit/blob_bundle_spec.js b/spec/frontend/blob_edit/blob_bundle_spec.js
index 2c9ddfaf867..644539308c2 100644
--- a/spec/frontend/blob_edit/blob_bundle_spec.js
+++ b/spec/frontend/blob_edit/blob_bundle_spec.js
@@ -1,4 +1,5 @@
import $ from 'jquery';
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
import waitForPromises from 'helpers/wait_for_promises';
import blobBundle from '~/blob_edit/blob_bundle';
@@ -14,15 +15,17 @@ describe('BlobBundle', () => {
});
it('loads SourceEditor for the edit screen', async () => {
- setFixtures(`<div class="js-edit-blob-form"></div>`);
+ setHTMLFixture(`<div class="js-edit-blob-form"></div>`);
blobBundle();
await waitForPromises();
expect(SourceEditor).toHaveBeenCalled();
+
+ resetHTMLFixture();
});
describe('No Suggest Popover', () => {
beforeEach(() => {
- setFixtures(`
+ setHTMLFixture(`
<div class="js-edit-blob-form" data-blob-filename="blah">
<button class="js-commit-button"></button>
<button id='cancel-changes'></button>
@@ -31,6 +34,10 @@ describe('BlobBundle', () => {
blobBundle();
});
+ afterEach(() => {
+ resetHTMLFixture();
+ });
+
it('sets the window beforeunload listener to a function returning a string', () => {
expect(window.onbeforeunload()).toBe('');
});
@@ -52,7 +59,7 @@ describe('BlobBundle', () => {
let trackingSpy;
beforeEach(() => {
- setFixtures(`
+ setHTMLFixture(`
<div class="js-edit-blob-form" data-blob-filename="blah" id="target">
<div class="js-suggest-gitlab-ci-yml"
data-target="#target"
@@ -73,6 +80,7 @@ describe('BlobBundle', () => {
afterEach(() => {
unmockTracking();
+ resetHTMLFixture();
});
it('sends a tracking event when the commit button is clicked', () => {
diff --git a/spec/frontend/blob_edit/edit_blob_spec.js b/spec/frontend/blob_edit/edit_blob_spec.js
index 9c974e79e6e..c031cae11df 100644
--- a/spec/frontend/blob_edit/edit_blob_spec.js
+++ b/spec/frontend/blob_edit/edit_blob_spec.js
@@ -1,9 +1,12 @@
+import { Emitter } from 'monaco-editor';
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import waitForPromises from 'helpers/wait_for_promises';
import EditBlob from '~/blob_edit/edit_blob';
import { SourceEditorExtension } from '~/editor/extensions/source_editor_extension_base';
import { FileTemplateExtension } from '~/editor/extensions/source_editor_file_template_ext';
import { EditorMarkdownExtension } from '~/editor/extensions/source_editor_markdown_ext';
import { EditorMarkdownPreviewExtension } from '~/editor/extensions/source_editor_markdown_livepreview_ext';
+import { ToolbarExtension } from '~/editor/extensions/source_editor_toolbar_ext';
import SourceEditor from '~/editor/source_editor';
jest.mock('~/editor/source_editor');
@@ -11,11 +14,13 @@ jest.mock('~/editor/extensions/source_editor_extension_base');
jest.mock('~/editor/extensions/source_editor_file_template_ext');
jest.mock('~/editor/extensions/source_editor_markdown_ext');
jest.mock('~/editor/extensions/source_editor_markdown_livepreview_ext');
+jest.mock('~/editor/extensions/source_editor_toolbar_ext');
const PREVIEW_MARKDOWN_PATH = '/foo/bar/preview_markdown';
const defaultExtensions = [
{ definition: SourceEditorExtension },
{ definition: FileTemplateExtension },
+ { definition: ToolbarExtension },
];
const markdownExtensions = [
{ definition: EditorMarkdownExtension },
@@ -26,15 +31,20 @@ const markdownExtensions = [
];
describe('Blob Editing', () => {
- const useMock = jest.fn();
+ let blobInstance;
+ const useMock = jest.fn(() => markdownExtensions);
+ const unuseMock = jest.fn();
+ const emitter = new Emitter();
const mockInstance = {
use: useMock,
+ unuse: unuseMock,
setValue: jest.fn(),
getValue: jest.fn().mockReturnValue('test value'),
focus: jest.fn(),
+ onDidChangeModelLanguage: emitter.event,
};
beforeEach(() => {
- setFixtures(`
+ setHTMLFixture(`
<form class="js-edit-blob-form">
<div id="file_path"></div>
<div id="editor"></div>
@@ -44,17 +54,18 @@ describe('Blob Editing', () => {
jest.spyOn(SourceEditor.prototype, 'createInstance').mockReturnValue(mockInstance);
});
afterEach(() => {
- SourceEditorExtension.mockClear();
- EditorMarkdownExtension.mockClear();
- EditorMarkdownPreviewExtension.mockClear();
- FileTemplateExtension.mockClear();
+ jest.clearAllMocks();
+ unuseMock.mockClear();
+ useMock.mockClear();
+ resetHTMLFixture();
});
const editorInst = (isMarkdown) => {
- return new EditBlob({
+ blobInstance = new EditBlob({
isMarkdown,
previewMarkdownPath: PREVIEW_MARKDOWN_PATH,
});
+ return blobInstance;
};
const initEditor = async (isMarkdown = false) => {
@@ -79,6 +90,22 @@ describe('Blob Editing', () => {
expect(useMock).toHaveBeenCalledTimes(2);
expect(useMock.mock.calls[1]).toEqual([markdownExtensions]);
});
+
+ it('correctly handles switching from markdown and un-uses markdown extensions', async () => {
+ await initEditor(true);
+ expect(unuseMock).not.toHaveBeenCalled();
+ await emitter.fire({ newLanguage: 'plaintext', oldLanguage: 'markdown' });
+ expect(unuseMock).toHaveBeenCalledWith(markdownExtensions);
+ });
+
+ it('correctly handles switching from non-markdown to markdown extensions', async () => {
+ const mdSpy = jest.fn();
+ await initEditor();
+ blobInstance.fetchMarkdownExtension = mdSpy;
+ expect(mdSpy).not.toHaveBeenCalled();
+ await emitter.fire({ newLanguage: 'markdown', oldLanguage: 'plaintext' });
+ expect(mdSpy).toHaveBeenCalled();
+ });
});
it('adds trailing newline to the blob content on submit', async () => {
diff --git a/spec/frontend/boards/board_card_inner_spec.js b/spec/frontend/boards/board_card_inner_spec.js
index 677978d31ca..c6de3ee69f3 100644
--- a/spec/frontend/boards/board_card_inner_spec.js
+++ b/spec/frontend/boards/board_card_inner_spec.js
@@ -2,12 +2,15 @@ import { GlLabel, GlLoadingIcon, GlTooltip } from '@gitlab/ui';
import { range } from 'lodash';
import Vuex from 'vuex';
import { nextTick } from 'vue';
+import setWindowLocation from 'helpers/set_window_location_helper';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import BoardBlockedIcon from '~/boards/components/board_blocked_icon.vue';
import BoardCardInner from '~/boards/components/board_card_inner.vue';
import { issuableTypes } from '~/boards/constants';
+import eventHub from '~/boards/eventhub';
import defaultStore from '~/boards/stores';
+import { updateHistory } from '~/lib/utils/url_utility';
import { mockLabelList, mockIssue, mockIssueFullPath } from './mock_data';
jest.mock('~/lib/utils/url_utility');
@@ -34,7 +37,7 @@ describe('Board card component', () => {
let list;
let store;
- const findBoardBlockedIcon = () => wrapper.find(BoardBlockedIcon);
+ const findBoardBlockedIcon = () => wrapper.findComponent(BoardBlockedIcon);
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findEpicCountablesTotalTooltip = () => wrapper.findComponent(GlTooltip);
const findEpicCountables = () => wrapper.findByTestId('epic-countables');
@@ -45,9 +48,14 @@ describe('Board card component', () => {
const findEpicProgressTooltip = () => wrapper.findByTestId('epic-progress-tooltip-content');
const findHiddenIssueIcon = () => wrapper.findByTestId('hidden-icon');
+ const performSearchMock = jest.fn();
+
const createStore = ({ isEpicBoard = false, isProjectBoard = false } = {}) => {
store = new Vuex.Store({
...defaultStore,
+ actions: {
+ performSearch: performSearchMock,
+ },
state: {
...defaultStore.state,
issuableType: issuableTypes.issue,
@@ -70,7 +78,6 @@ describe('Board card component', () => {
...props,
},
stubs: {
- GlLabel: true,
GlLoadingIcon: true,
},
directives: {
@@ -179,7 +186,7 @@ describe('Board card component', () => {
describe('confidential issue', () => {
beforeEach(() => {
- wrapper.setProps({
+ createWrapper({
item: {
...wrapper.props('item'),
confidential: true,
@@ -194,7 +201,7 @@ describe('Board card component', () => {
describe('hidden issue', () => {
beforeEach(() => {
- wrapper.setProps({
+ createWrapper({
item: {
...wrapper.props('item'),
hidden: true,
@@ -219,7 +226,7 @@ describe('Board card component', () => {
describe('with assignee', () => {
describe('with avatar', () => {
beforeEach(() => {
- wrapper.setProps({
+ createWrapper({
item: {
...wrapper.props('item'),
assignees: [user],
@@ -272,7 +279,7 @@ describe('Board card component', () => {
beforeEach(() => {
global.gon.default_avatar_url = 'default_avatar';
- wrapper.setProps({
+ createWrapper({
item: {
...wrapper.props('item'),
assignees: [
@@ -301,7 +308,7 @@ describe('Board card component', () => {
describe('multiple assignees', () => {
beforeEach(() => {
- wrapper.setProps({
+ createWrapper({
item: {
...wrapper.props('item'),
assignees: [
@@ -342,7 +349,7 @@ describe('Board card component', () => {
avatarUrl: 'test_image',
});
- wrapper.setProps({
+ createWrapper({
item: {
...wrapper.props('item'),
assignees,
@@ -368,7 +375,7 @@ describe('Board card component', () => {
avatarUrl: 'test_image',
})),
];
- wrapper.setProps({
+ createWrapper({
item: {
...wrapper.props('item'),
assignees,
@@ -384,31 +391,74 @@ describe('Board card component', () => {
describe('labels', () => {
beforeEach(() => {
- wrapper.setProps({ item: { ...issue, labels: [list.label, label1] } });
+ createWrapper({ item: { ...issue, labels: [list.label, label1] } });
});
it('does not render list label but renders all other labels', () => {
- expect(wrapper.findAll(GlLabel).length).toBe(1);
- const label = wrapper.find(GlLabel);
+ expect(wrapper.findAllComponents(GlLabel).length).toBe(1);
+ const label = wrapper.findComponent(GlLabel);
expect(label.props('title')).toEqual(label1.title);
expect(label.props('description')).toEqual(label1.description);
expect(label.props('backgroundColor')).toEqual(label1.color);
});
it('does not render label if label does not have an ID', async () => {
- wrapper.setProps({ item: { ...issue, labels: [label1, { title: 'closed' }] } });
+ createWrapper({ item: { ...issue, labels: [label1, { title: 'closed' }] } });
await nextTick();
- expect(wrapper.findAll(GlLabel).length).toBe(1);
+ expect(wrapper.findAllComponents(GlLabel).length).toBe(1);
expect(wrapper.text()).not.toContain('closed');
});
+ });
- describe('when label params arent set', () => {
- it('passes the right target to GlLabel', () => {
- expect(wrapper.findAll(GlLabel).at(0).props('target')).toEqual(
- '?label_name[]=testing%20123',
- );
+ describe('filterByLabel method', () => {
+ beforeEach(() => {
+ createWrapper({
+ item: {
+ ...issue,
+ labels: [label1],
+ },
+ updateFilters: true,
+ });
+ });
+
+ describe('when selected label is not in the filter', () => {
+ beforeEach(() => {
+ setWindowLocation('?');
+ wrapper.findComponent(GlLabel).vm.$emit('click', label1);
+ });
+
+ it('calls updateHistory', () => {
+ expect(updateHistory).toHaveBeenCalledTimes(1);
+ });
+
+ it('dispatches performSearch vuex action', () => {
+ expect(performSearchMock).toHaveBeenCalledTimes(1);
+ });
+
+ it('emits updateTokens event', () => {
+ expect(eventHub.$emit).toHaveBeenCalledTimes(1);
+ expect(eventHub.$emit).toHaveBeenCalledWith('updateTokens');
+ });
+ });
+
+ describe('when selected label is already in the filter', () => {
+ beforeEach(() => {
+ setWindowLocation('?label_name[]=testing%20123');
+ wrapper.findComponent(GlLabel).vm.$emit('click', label1);
+ });
+
+ it('does not call updateHistory', () => {
+ expect(updateHistory).not.toHaveBeenCalled();
+ });
+
+ it('does not dispatch performSearch vuex action', () => {
+ expect(performSearchMock).not.toHaveBeenCalled();
+ });
+
+ it('does not emit updateTokens event', () => {
+ expect(eventHub.$emit).not.toHaveBeenCalled();
});
});
});
diff --git a/spec/frontend/boards/components/board_list_header_spec.js b/spec/frontend/boards/components/board_list_header_spec.js
index 14870ec76a2..2f9677680eb 100644
--- a/spec/frontend/boards/components/board_list_header_spec.js
+++ b/spec/frontend/boards/components/board_list_header_spec.js
@@ -132,7 +132,7 @@ describe('Board List Header Component', () => {
const icon = findCaret();
- expect(icon.props('icon')).toBe('chevron-right');
+ expect(icon.props('icon')).toBe('chevron-down');
});
it('should display expand icon when column is collapsed', async () => {
@@ -140,7 +140,7 @@ describe('Board List Header Component', () => {
const icon = findCaret();
- expect(icon.props('icon')).toBe('chevron-down');
+ expect(icon.props('icon')).toBe('chevron-right');
});
it('should dispatch toggleListCollapse when clicking the collapse icon', async () => {
diff --git a/spec/frontend/boards/mock_data.js b/spec/frontend/boards/mock_data.js
index ec9342cffc2..26ad9790840 100644
--- a/spec/frontend/boards/mock_data.js
+++ b/spec/frontend/boards/mock_data.js
@@ -17,6 +17,10 @@ export const mockBoard = {
id: 'gid://gitlab/Iteration/124',
title: 'Iteration 9',
},
+ iterationCadence: {
+ id: 'gid://gitlab/Iteration::Cadence/134',
+ title: 'Cadence 3',
+ },
assignee: {
id: 'gid://gitlab/User/1',
username: 'admin',
@@ -32,6 +36,7 @@ export const mockBoardConfig = {
milestoneTitle: '14.9',
iterationId: 'gid://gitlab/Iteration/124',
iterationTitle: 'Iteration 9',
+ iterationCadenceId: 'gid://gitlab/Iteration::Cadence/134',
assigneeId: 'gid://gitlab/User/1',
assigneeUsername: 'admin',
labels: [{ id: 'gid://gitlab/Label/32', title: 'Deliverable' }],
diff --git a/spec/frontend/boards/project_select_spec.js b/spec/frontend/boards/project_select_spec.js
index 05dc7d28eaa..bd79060c54f 100644
--- a/spec/frontend/boards/project_select_spec.js
+++ b/spec/frontend/boards/project_select_spec.js
@@ -1,7 +1,14 @@
-import { GlDropdown, GlDropdownItem, GlSearchBoxByType, GlLoadingIcon } from '@gitlab/ui';
+import {
+ GlDropdown,
+ GlDropdownItem,
+ GlSearchBoxByType,
+ GlLoadingIcon,
+ GlFormInput,
+} from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
+import waitForPromises from 'helpers/wait_for_promises';
import ProjectSelect from '~/boards/components/project_select.vue';
import defaultState from '~/boards/stores/state';
@@ -61,6 +68,7 @@ describe('ProjectSelect component', () => {
provide: {
groupId: 1,
},
+ attachTo: document.body,
});
};
@@ -120,6 +128,17 @@ describe('ProjectSelect component', () => {
it('does not render empty search result message', () => {
expect(findEmptySearchMessage().exists()).toBe(false);
});
+
+ it('focuses on the search input', async () => {
+ const dropdownToggle = findGlDropdown().find('.dropdown-toggle');
+
+ await dropdownToggle.trigger('click');
+ await waitForPromises();
+ await nextTick();
+
+ const searchInput = findGlDropdown().findComponent(GlFormInput).element;
+ expect(document.activeElement).toEqual(searchInput);
+ });
});
describe('when no projects are being returned', () => {
diff --git a/spec/frontend/boards/stores/getters_spec.js b/spec/frontend/boards/stores/getters_spec.js
index b30968c45d7..304f2aad98e 100644
--- a/spec/frontend/boards/stores/getters_spec.js
+++ b/spec/frontend/boards/stores/getters_spec.js
@@ -215,4 +215,33 @@ describe('Boards - Getters', () => {
expect(getters.isEpicBoard()).toBe(false);
});
});
+
+ describe('hasScope', () => {
+ const boardConfig = {
+ labels: [],
+ assigneeId: null,
+ iterationCadenceId: null,
+ iterationId: null,
+ milestoneId: null,
+ weight: null,
+ };
+
+ it('returns false when boardConfig is empty', () => {
+ const state = { boardConfig };
+
+ expect(getters.hasScope(state)).toBe(false);
+ });
+
+ it('returns true when boardScope has a label', () => {
+ const state = { boardConfig: { ...boardConfig, labels: ['foo'] } };
+
+ expect(getters.hasScope(state)).toBe(true);
+ });
+
+ it('returns true when boardConfig has a value other than null', () => {
+ const state = { boardConfig: { ...boardConfig, assigneeId: 3 } };
+
+ expect(getters.hasScope(state)).toBe(true);
+ });
+ });
});
diff --git a/spec/frontend/bootstrap_jquery_spec.js b/spec/frontend/bootstrap_jquery_spec.js
index d5d592e3839..15186600a8a 100644
--- a/spec/frontend/bootstrap_jquery_spec.js
+++ b/spec/frontend/bootstrap_jquery_spec.js
@@ -1,10 +1,15 @@
import $ from 'jquery';
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import '~/commons/bootstrap';
describe('Bootstrap jQuery extensions', () => {
describe('disable', () => {
beforeEach(() => {
- setFixtures('<input type="text" />');
+ setHTMLFixture('<input type="text" />');
+ });
+
+ afterEach(() => {
+ resetHTMLFixture();
});
it('adds the disabled attribute', () => {
@@ -24,7 +29,11 @@ describe('Bootstrap jQuery extensions', () => {
describe('enable', () => {
beforeEach(() => {
- setFixtures('<input type="text" disabled="disabled" class="disabled" />');
+ setHTMLFixture('<input type="text" disabled="disabled" class="disabled" />');
+ });
+
+ afterEach(() => {
+ resetHTMLFixture();
});
it('removes the disabled attribute', () => {
diff --git a/spec/frontend/bootstrap_linked_tabs_spec.js b/spec/frontend/bootstrap_linked_tabs_spec.js
index 30fb140bc69..5ee1ca32141 100644
--- a/spec/frontend/bootstrap_linked_tabs_spec.js
+++ b/spec/frontend/bootstrap_linked_tabs_spec.js
@@ -1,8 +1,13 @@
+import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import LinkedTabs from '~/lib/utils/bootstrap_linked_tabs';
describe('Linked Tabs', () => {
beforeEach(() => {
- loadFixtures('static/linked_tabs.html');
+ loadHTMLFixture('static/linked_tabs.html');
+ });
+
+ afterEach(() => {
+ resetHTMLFixture();
});
describe('when is initialized', () => {
diff --git a/spec/frontend/branches/components/delete_branch_modal_spec.js b/spec/frontend/branches/components/delete_branch_modal_spec.js
index 0c6111bda9e..2b8c8d408c4 100644
--- a/spec/frontend/branches/components/delete_branch_modal_spec.js
+++ b/spec/frontend/branches/components/delete_branch_modal_spec.js
@@ -49,7 +49,7 @@ const findForm = () => wrapper.find('form');
describe('Delete branch modal', () => {
const expectedUnmergedWarning =
- 'This branch hasn’t been merged into default. To avoid data loss, consider merging this branch before deleting it.';
+ "This branch hasn't been merged into default. To avoid data loss, consider merging this branch before deleting it.";
afterEach(() => {
wrapper.destroy();
@@ -110,7 +110,7 @@ describe('Delete branch modal', () => {
"You're about to permanently delete the protected branch test_modal.";
const expectedMessageProtected = `${expectedWarningProtected} ${expectedUnmergedWarning}`;
const expectedConfirmationText =
- 'Once you confirm and press Yes, delete protected branch, it cannot be undone or recovered. Please type the following to confirm: test_modal';
+ 'After you confirm and select Yes, delete protected branch, you cannot recover this branch. Please type the following to confirm: test_modal';
beforeEach(() => {
createComponent({ isProtectedBranch: true });
diff --git a/spec/frontend/broadcast_notification_spec.js b/spec/frontend/broadcast_notification_spec.js
index cd947cd417a..5b9541dedfb 100644
--- a/spec/frontend/broadcast_notification_spec.js
+++ b/spec/frontend/broadcast_notification_spec.js
@@ -1,4 +1,5 @@
-import Cookies from 'js-cookie';
+import Cookies from '~/lib/utils/cookies';
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import initBroadcastNotifications from '~/broadcast_notification';
describe('broadcast message on dismiss', () => {
@@ -9,7 +10,7 @@ describe('broadcast message on dismiss', () => {
const endsAt = '2020-01-01T00:00:00Z';
beforeEach(() => {
- setFixtures(`
+ setHTMLFixture(`
<div class="js-broadcast-notification-1">
<button class="js-dismiss-current-broadcast-notification" data-id="1" data-expire-date="${endsAt}"></button>
</div>
@@ -18,6 +19,10 @@ describe('broadcast message on dismiss', () => {
initBroadcastNotifications();
});
+ afterEach(() => {
+ resetHTMLFixture();
+ });
+
it('removes broadcast message', () => {
dismiss();
diff --git a/spec/frontend/ci_secure_files/components/secure_files_list_spec.js b/spec/frontend/ci_secure_files/components/secure_files_list_spec.js
index 042376c71e8..ad5f8a56ced 100644
--- a/spec/frontend/ci_secure_files/components/secure_files_list_spec.js
+++ b/spec/frontend/ci_secure_files/components/secure_files_list_spec.js
@@ -10,6 +10,7 @@ import { secureFiles } from '../mock_data';
const dummyApiVersion = 'v3000';
const dummyProjectId = 1;
+const fileSizeLimit = 5;
const dummyUrlRoot = '/gitlab';
const dummyGon = {
api_version: dummyApiVersion,
@@ -33,9 +34,13 @@ describe('SecureFilesList', () => {
window.gon = originalGon;
});
- const createWrapper = (props = {}) => {
+ const createWrapper = (admin = true, props = {}) => {
wrapper = mount(SecureFilesList, {
- provide: { projectId: dummyProjectId },
+ provide: {
+ projectId: dummyProjectId,
+ admin,
+ fileSizeLimit,
+ },
...props,
});
};
@@ -46,6 +51,8 @@ describe('SecureFilesList', () => {
const findHeaderAt = (i) => wrapper.findAll('thead th').at(i);
const findPagination = () => wrapper.findAll('ul.pagination');
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
+ const findUploadButton = () => wrapper.findAll('span.gl-button-text');
+ const findDeleteButton = () => wrapper.findAll('tbody tr td button.btn-danger');
describe('when secure files exist in a project', () => {
beforeEach(async () => {
@@ -57,7 +64,7 @@ describe('SecureFilesList', () => {
});
it('displays a table with expected headers', () => {
- const headers = ['Filename', 'Permissions', 'Uploaded'];
+ const headers = ['Filename', 'Uploaded'];
headers.forEach((header, i) => {
expect(findHeaderAt(i).text()).toBe(header);
});
@@ -69,8 +76,7 @@ describe('SecureFilesList', () => {
const [secureFile] = secureFiles;
expect(findCell(0, 0).text()).toBe(secureFile.name);
- expect(findCell(0, 1).text()).toBe(secureFile.permissions);
- expect(findCell(0, 2).find(TimeAgoTooltip).props('time')).toBe(secureFile.created_at);
+ expect(findCell(0, 1).find(TimeAgoTooltip).props('time')).toBe(secureFile.created_at);
});
});
@@ -84,7 +90,7 @@ describe('SecureFilesList', () => {
});
it('displays a table with expected headers', () => {
- const headers = ['Filename', 'Permissions', 'Uploaded'];
+ const headers = ['Filename', 'Uploaded'];
headers.forEach((header, i) => {
expect(findHeaderAt(i).text()).toBe(header);
});
@@ -136,4 +142,42 @@ describe('SecureFilesList', () => {
expect(findLoadingIcon().exists()).toBe(false);
});
});
+
+ describe('admin permissions', () => {
+ describe('with admin permissions', () => {
+ beforeEach(async () => {
+ mock = new MockAdapter(axios);
+ mock.onGet(expectedUrl).reply(200, secureFiles);
+
+ createWrapper();
+ await waitForPromises();
+ });
+
+ it('displays the upload button', () => {
+ expect(findUploadButton().exists()).toBe(true);
+ });
+
+ it('displays a delete button', () => {
+ expect(findDeleteButton().exists()).toBe(true);
+ });
+ });
+
+ describe('without admin permissions', () => {
+ beforeEach(async () => {
+ mock = new MockAdapter(axios);
+ mock.onGet(expectedUrl).reply(200, secureFiles);
+
+ createWrapper(false);
+ await waitForPromises();
+ });
+
+ it('does not display the upload button', () => {
+ expect(findUploadButton().exists()).toBe(false);
+ });
+
+ it('does not display a delete button', () => {
+ expect(findDeleteButton().exists()).toBe(false);
+ });
+ });
+ });
});
diff --git a/spec/frontend/ci_variable_list/ci_variable_list/ci_variable_list_spec.js b/spec/frontend/ci_variable_list/ci_variable_list/ci_variable_list_spec.js
index 1bca21b1d57..2210b0f48d6 100644
--- a/spec/frontend/ci_variable_list/ci_variable_list/ci_variable_list_spec.js
+++ b/spec/frontend/ci_variable_list/ci_variable_list/ci_variable_list_spec.js
@@ -1,4 +1,5 @@
import $ from 'jquery';
+import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import VariableList from '~/ci_variable_list/ci_variable_list';
const HIDE_CLASS = 'hide';
@@ -10,7 +11,7 @@ describe('VariableList', () => {
describe('with only key/value inputs', () => {
describe('with no variables', () => {
beforeEach(() => {
- loadFixtures('pipeline_schedules/edit.html');
+ loadHTMLFixture('pipeline_schedules/edit.html');
$wrapper = $('.js-ci-variable-list-section');
variableList = new VariableList({
@@ -20,6 +21,10 @@ describe('VariableList', () => {
variableList.init();
});
+ afterEach(() => {
+ resetHTMLFixture();
+ });
+
it('should remove the row when clicking the remove button', () => {
$wrapper.find('.js-row-remove-button').trigger('click');
@@ -64,7 +69,7 @@ describe('VariableList', () => {
describe('with persisted variables', () => {
beforeEach(() => {
- loadFixtures('pipeline_schedules/edit_with_variables.html');
+ loadHTMLFixture('pipeline_schedules/edit_with_variables.html');
$wrapper = $('.js-ci-variable-list-section');
variableList = new VariableList({
@@ -74,6 +79,10 @@ describe('VariableList', () => {
variableList.init();
});
+ afterEach(() => {
+ resetHTMLFixture();
+ });
+
it('should have "Reveal values" button initially when there are already variables', () => {
expect($wrapper.find('.js-secret-value-reveal-button').text()).toBe('Reveal values');
});
@@ -97,7 +106,7 @@ describe('VariableList', () => {
describe('toggleEnableRow method', () => {
beforeEach(() => {
- loadFixtures('pipeline_schedules/edit_with_variables.html');
+ loadHTMLFixture('pipeline_schedules/edit_with_variables.html');
$wrapper = $('.js-ci-variable-list-section');
variableList = new VariableList({
@@ -107,6 +116,10 @@ describe('VariableList', () => {
variableList.init();
});
+ afterEach(() => {
+ resetHTMLFixture();
+ });
+
it('should disable all key inputs', () => {
expect($wrapper.find('.js-ci-variable-input-key:not([disabled])').length).toBe(3);
diff --git a/spec/frontend/ci_variable_list/ci_variable_list/native_form_variable_list_spec.js b/spec/frontend/ci_variable_list/ci_variable_list/native_form_variable_list_spec.js
index eee1362440d..57f666e29d6 100644
--- a/spec/frontend/ci_variable_list/ci_variable_list/native_form_variable_list_spec.js
+++ b/spec/frontend/ci_variable_list/ci_variable_list/native_form_variable_list_spec.js
@@ -1,11 +1,12 @@
import $ from 'jquery';
+import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import setupNativeFormVariableList from '~/ci_variable_list/native_form_variable_list';
describe('NativeFormVariableList', () => {
let $wrapper;
beforeEach(() => {
- loadFixtures('pipeline_schedules/edit.html');
+ loadHTMLFixture('pipeline_schedules/edit.html');
$wrapper = $('.js-ci-variable-list-section');
setupNativeFormVariableList({
@@ -14,6 +15,10 @@ describe('NativeFormVariableList', () => {
});
});
+ afterEach(() => {
+ resetHTMLFixture();
+ });
+
describe('onFormSubmit', () => {
it('should clear out the `name` attribute on the inputs for the last empty row on form submission (avoid BE validation)', () => {
const $row = $wrapper.find('.js-row');
diff --git a/spec/frontend/ci_variable_list/components/ci_variable_modal_spec.js b/spec/frontend/ci_variable_list/components/ci_variable_modal_spec.js
index 2fedbbecd64..d26378d9382 100644
--- a/spec/frontend/ci_variable_list/components/ci_variable_modal_spec.js
+++ b/spec/frontend/ci_variable_list/components/ci_variable_modal_spec.js
@@ -5,7 +5,12 @@ import Vuex from 'vuex';
import { mockTracking } from 'helpers/tracking_helper';
import CiEnvironmentsDropdown from '~/ci_variable_list/components/ci_environments_dropdown.vue';
import CiVariableModal from '~/ci_variable_list/components/ci_variable_modal.vue';
-import { AWS_ACCESS_KEY_ID, EVENT_LABEL, EVENT_ACTION } from '~/ci_variable_list/constants';
+import {
+ AWS_ACCESS_KEY_ID,
+ EVENT_LABEL,
+ EVENT_ACTION,
+ ENVIRONMENT_SCOPE_LINK_TITLE,
+} from '~/ci_variable_list/constants';
import createStore from '~/ci_variable_list/store';
import mockData from '../services/mock_data';
import ModalStub from '../stubs';
@@ -20,7 +25,11 @@ describe('Ci variable modal', () => {
const maskableRegex = '^[a-zA-Z0-9_+=/@:.~-]{8,}$';
const createComponent = (method, options = {}) => {
- store = createStore({ maskableRegex, isGroup: options.isGroup });
+ store = createStore({
+ maskableRegex,
+ isGroup: options.isGroup,
+ environmentScopeLink: '/help/environments',
+ });
wrapper = method(CiVariableModal, {
attachTo: document.body,
stubs: {
@@ -213,6 +222,15 @@ describe('Ci variable modal', () => {
});
});
});
+
+ it('renders a link to documentation on scopes', () => {
+ createComponent(mount);
+
+ const link = wrapper.find('[data-testid="environment-scope-link"]');
+
+ expect(link.attributes('title')).toBe(ENVIRONMENT_SCOPE_LINK_TITLE);
+ expect(link.attributes('href')).toBe('/help/environments');
+ });
});
describe('Validations', () => {
diff --git a/spec/frontend/clusters/agents/components/revoke_token_button_spec.js b/spec/frontend/clusters/agents/components/revoke_token_button_spec.js
new file mode 100644
index 00000000000..6521221cbd7
--- /dev/null
+++ b/spec/frontend/clusters/agents/components/revoke_token_button_spec.js
@@ -0,0 +1,239 @@
+import { GlButton, GlModal, GlFormInput, GlTooltip } from '@gitlab/ui';
+import Vue, { nextTick } from 'vue';
+import VueApollo from 'vue-apollo';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { ENTER_KEY } from '~/lib/utils/keys';
+import RevokeTokenButton from '~/clusters/agents/components/revoke_token_button.vue';
+import getClusterAgentQuery from '~/clusters/agents/graphql/queries/get_cluster_agent.query.graphql';
+import revokeTokenMutation from '~/clusters/agents/graphql/mutations/revoke_token.mutation.graphql';
+import { TOKEN_STATUS_ACTIVE, MAX_LIST_COUNT } from '~/clusters/agents/constants';
+import { getTokenResponse, mockRevokeResponse, mockErrorRevokeResponse } from '../../mock_data';
+
+Vue.use(VueApollo);
+
+describe('RevokeTokenButton', () => {
+ let wrapper;
+ let toast;
+ let apolloProvider;
+ let revokeSpy;
+
+ const token = {
+ id: 'token-id',
+ name: 'token-name',
+ };
+ const cursor = {
+ first: MAX_LIST_COUNT,
+ last: null,
+ };
+ const agentName = 'cluster-agent';
+ const projectPath = 'path/to/project';
+
+ const defaultProvide = {
+ agentName,
+ projectPath,
+ canAdminCluster: true,
+ };
+ const propsData = {
+ token,
+ cursor,
+ };
+
+ const findModal = () => wrapper.findComponent(GlModal);
+ const findRevokeBtn = () => wrapper.findComponent(GlButton);
+ const findInput = () => wrapper.findComponent(GlFormInput);
+ const findTooltip = () => wrapper.findComponent(GlTooltip);
+ const findPrimaryAction = () => findModal().props('actionPrimary');
+ const findPrimaryActionAttributes = (attr) => findPrimaryAction().attributes[0][attr];
+
+ const createMockApolloProvider = ({ mutationResponse }) => {
+ revokeSpy = jest.fn().mockResolvedValue(mutationResponse);
+
+ return createMockApollo([[revokeTokenMutation, revokeSpy]]);
+ };
+
+ const writeQuery = () => {
+ apolloProvider.clients.defaultClient.cache.writeQuery({
+ query: getClusterAgentQuery,
+ variables: {
+ agentName,
+ projectPath,
+ tokenStatus: TOKEN_STATUS_ACTIVE,
+ ...cursor,
+ },
+ data: getTokenResponse.data,
+ });
+ };
+
+ const createWrapper = async ({
+ mutationResponse = mockRevokeResponse,
+ provideData = {},
+ } = {}) => {
+ apolloProvider = createMockApolloProvider({ mutationResponse });
+
+ toast = jest.fn();
+
+ wrapper = shallowMountExtended(RevokeTokenButton, {
+ apolloProvider,
+ provide: {
+ ...defaultProvide,
+ ...provideData,
+ },
+ propsData,
+ stubs: {
+ GlModal,
+ GlTooltip,
+ },
+ mocks: { $toast: { show: toast } },
+ });
+ wrapper.vm.$refs.modal.hide = jest.fn();
+
+ writeQuery();
+ await nextTick();
+ };
+
+ const submitTokenToRevoke = async () => {
+ findRevokeBtn().vm.$emit('click');
+ findInput().vm.$emit('input', token.name);
+ await findModal().vm.$emit('primary');
+ await waitForPromises();
+ };
+
+ beforeEach(() => {
+ createWrapper();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ apolloProvider = null;
+ revokeSpy = null;
+ });
+
+ describe('revoke token action', () => {
+ it('displays a revoke button', () => {
+ expect(findRevokeBtn().attributes('aria-label')).toBe('Revoke token');
+ });
+
+ describe('when user cannot revoke token', () => {
+ beforeEach(() => {
+ createWrapper({ provideData: { canAdminCluster: false } });
+ });
+
+ it('disabled the button', () => {
+ expect(findRevokeBtn().attributes('disabled')).toBe('true');
+ });
+
+ it('shows a disabled tooltip', () => {
+ expect(findTooltip().attributes('title')).toBe(
+ 'Requires a Maintainer or greater role to perform this action',
+ );
+ });
+ });
+
+ describe('when user can create a token and clicks the button', () => {
+ beforeEach(() => {
+ findRevokeBtn().vm.$emit('click');
+ });
+
+ it('displays a delete confirmation modal', () => {
+ expect(findModal().isVisible()).toBe(true);
+ });
+
+ describe.each`
+ condition | tokenName | isDisabled | mutationCalled
+ ${'the input with token name is missing'} | ${''} | ${true} | ${false}
+ ${'the input with token name is incorrect'} | ${'wrong-name'} | ${true} | ${false}
+ ${'the input with token name is correct'} | ${token.name} | ${false} | ${true}
+ `('when $condition', ({ tokenName, isDisabled, mutationCalled }) => {
+ beforeEach(() => {
+ findRevokeBtn().vm.$emit('click');
+ findInput().vm.$emit('input', tokenName);
+ });
+
+ 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 revoke mutation', () => {
+ expect(revokeSpy).toHaveBeenCalledWith({ input: { id: token.id } });
+ });
+ } else {
+ it("doesn't call the revoke mutation", () => {
+ expect(revokeSpy).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 revoke mutation', () => {
+ expect(revokeSpy).toHaveBeenCalledWith({ input: { id: token.id } });
+ });
+ } else {
+ it("doesn't call the revoke mutation", () => {
+ expect(revokeSpy).not.toHaveBeenCalled();
+ });
+ }
+ });
+ });
+ });
+
+ describe('when token was revoked successfully', () => {
+ beforeEach(async () => {
+ await submitTokenToRevoke();
+ });
+
+ it('calls the toast action', () => {
+ expect(toast).toHaveBeenCalledWith(`${token.name} successfully revoked`);
+ });
+ });
+
+ describe('when getting an error revoking token', () => {
+ beforeEach(async () => {
+ await createWrapper({ mutationResponse: mockErrorRevokeResponse });
+ await submitTokenToRevoke();
+ });
+
+ it('displays the error message', () => {
+ expect(toast).toHaveBeenCalledWith('could not revoke token');
+ });
+ });
+
+ describe('when the revoke modal was closed', () => {
+ beforeEach(async () => {
+ const loadingResponse = new Promise(() => {});
+ await createWrapper({ mutationResponse: loadingResponse });
+ await submitTokenToRevoke();
+ });
+
+ it('reenables the button', async () => {
+ expect(findPrimaryActionAttributes('loading')).toBe(true);
+ expect(findRevokeBtn().attributes('disabled')).toBe('true');
+
+ await findModal().vm.$emit('hide');
+
+ expect(findPrimaryActionAttributes('loading')).toBe(false);
+ expect(findRevokeBtn().attributes('disabled')).toBeUndefined();
+ });
+
+ it('clears the token name input', async () => {
+ expect(findInput().attributes('value')).toBe(token.name);
+
+ await findModal().vm.$emit('hide');
+
+ expect(findInput().attributes('value')).toBeUndefined();
+ });
+ });
+ });
+});
diff --git a/spec/frontend/clusters/clusters_bundle_spec.js b/spec/frontend/clusters/clusters_bundle_spec.js
index 2a0610b1b0a..b5345ea8915 100644
--- a/spec/frontend/clusters/clusters_bundle_spec.js
+++ b/spec/frontend/clusters/clusters_bundle_spec.js
@@ -1,5 +1,5 @@
import MockAdapter from 'axios-mock-adapter';
-import { loadHTMLFixture } from 'helpers/fixtures';
+import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import { useMockLocationHelper } from 'helpers/mock_window_location_helper';
import { setTestTimeout } from 'helpers/timeout';
import Clusters from '~/clusters/clusters_bundle';
@@ -27,19 +27,17 @@ describe('Clusters', () => {
beforeEach(() => {
loadHTMLFixture('clusters/show_cluster.html');
- });
- beforeEach(() => {
mockGetClusterStatusRequest();
- });
- beforeEach(() => {
cluster = new Clusters();
});
afterEach(() => {
cluster.destroy();
mock.restore();
+
+ resetHTMLFixture();
});
describe('class constructor', () => {
diff --git a/spec/frontend/clusters/components/__snapshots__/new_cluster_spec.js.snap b/spec/frontend/clusters/components/__snapshots__/new_cluster_spec.js.snap
index 0bec2a5934e..656e72baf77 100644
--- a/spec/frontend/clusters/components/__snapshots__/new_cluster_spec.js.snap
+++ b/spec/frontend/clusters/components/__snapshots__/new_cluster_spec.js.snap
@@ -3,7 +3,7 @@
exports[`NewCluster renders the cluster component correctly 1`] = `
"<div class=\\"gl-pt-4\\">
<h4>Enter your Kubernetes cluster certificate details</h4>
- <p>Enter details about your cluster. <b-link-stub href=\\"/some/help/path\\" target=\\"_blank\\" event=\\"click\\" routertag=\\"a\\" class=\\"gl-link\\">How do I use a certificate to connect to my cluster?</b-link-stub>
+ <p>Enter details about your cluster. <b-link-stub href=\\"/help/user/project/clusters/add_existing_cluster\\" event=\\"click\\" routertag=\\"a\\" class=\\"gl-link\\">How do I use a certificate to connect to my cluster?</b-link-stub>
</p>
</div>"
`;
diff --git a/spec/frontend/clusters/components/new_cluster_spec.js b/spec/frontend/clusters/components/new_cluster_spec.js
index b62e678154c..f9df70b9f87 100644
--- a/spec/frontend/clusters/components/new_cluster_spec.js
+++ b/spec/frontend/clusters/components/new_cluster_spec.js
@@ -2,15 +2,13 @@ import { GlLink, GlSprintf } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
import NewCluster from '~/clusters/components/new_cluster.vue';
-import createClusterStore from '~/clusters/stores/new_cluster';
+import { helpPagePath } from '~/helpers/help_page_helper';
describe('NewCluster', () => {
- let store;
let wrapper;
const createWrapper = async () => {
- store = createClusterStore({ clusterConnectHelpPath: '/some/help/path' });
- wrapper = shallowMount(NewCluster, { store, stubs: { GlLink, GlSprintf } });
+ wrapper = shallowMount(NewCluster, { stubs: { GlLink, GlSprintf } });
await nextTick();
};
@@ -35,6 +33,8 @@ describe('NewCluster', () => {
});
it('renders a valid help link set by the backend', () => {
- expect(findLink().attributes('href')).toBe('/some/help/path');
+ expect(findLink().attributes('href')).toBe(
+ helpPagePath('user/project/clusters/add_existing_cluster'),
+ );
});
});
diff --git a/spec/frontend/clusters/forms/components/integration_form_spec.js b/spec/frontend/clusters/forms/components/integration_form_spec.js
index dd278bcd2ce..67d442bfdc5 100644
--- a/spec/frontend/clusters/forms/components/integration_form_spec.js
+++ b/spec/frontend/clusters/forms/components/integration_form_spec.js
@@ -22,7 +22,7 @@ describe('ClusterIntegrationForm', () => {
store: createStore(storeValues),
provide: {
autoDevopsHelpPath: 'topics/autodevops/index',
- externalEndpointHelpPath: 'user/clusters/applications.md',
+ externalEndpointHelpPath: 'user/project/clusters/index.md#base-domain',
},
});
};
diff --git a/spec/frontend/create_cluster/gke_cluster_namespace/gke_cluster_namespace_spec.js b/spec/frontend/clusters/gke_cluster_namespace/gke_cluster_namespace_spec.js
index c22167a078c..eeb876a608f 100644
--- a/spec/frontend/create_cluster/gke_cluster_namespace/gke_cluster_namespace_spec.js
+++ b/spec/frontend/clusters/gke_cluster_namespace/gke_cluster_namespace_spec.js
@@ -1,4 +1,5 @@
-import initGkeNamespace from '~/create_cluster/gke_cluster_namespace';
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
+import initGkeNamespace from '~/clusters/gke_cluster_namespace';
describe('GKE cluster namespace', () => {
const changeEvent = new Event('change');
@@ -10,7 +11,7 @@ describe('GKE cluster namespace', () => {
let glManaged;
beforeEach(() => {
- setFixtures(`
+ setHTMLFixture(`
<input class="js-gl-managed" type="checkbox" value="1" checked />
<div class="js-namespace">
<input type="text" />
@@ -27,6 +28,10 @@ describe('GKE cluster namespace', () => {
initGkeNamespace();
});
+ afterEach(() => {
+ resetHTMLFixture();
+ });
+
describe('GKE cluster namespace toggles', () => {
it('initially displays the GitLab-managed label and input', () => {
expect(isHidden(glManaged)).toEqual(false);
diff --git a/spec/frontend/clusters/mock_data.js b/spec/frontend/clusters/mock_data.js
index 63840486d0d..f3736f03e03 100644
--- a/spec/frontend/clusters/mock_data.js
+++ b/spec/frontend/clusters/mock_data.js
@@ -220,3 +220,15 @@ export const getTokenResponse = {
},
},
};
+
+export const mockRevokeResponse = {
+ data: { clusterAgentTokenRevoke: { errors: [] } },
+};
+
+export const mockErrorRevokeResponse = {
+ data: {
+ clusterAgentTokenRevoke: {
+ errors: ['could not revoke token'],
+ },
+ },
+};
diff --git a/spec/frontend/clusters_list/components/agent_table_spec.js b/spec/frontend/clusters_list/components/agent_table_spec.js
index a466a35428a..2a43b45a2f5 100644
--- a/spec/frontend/clusters_list/components/agent_table_spec.js
+++ b/spec/frontend/clusters_list/components/agent_table_spec.js
@@ -13,6 +13,7 @@ const defaultConfigHelpUrl =
const provideData = {
gitlabVersion: '14.8',
+ kasVersion: '14.8',
};
const propsData = {
agents: clusterAgents,
@@ -26,7 +27,7 @@ const outdatedTitle = I18N_AGENT_TABLE.versionOutdatedTitle;
const mismatchTitle = I18N_AGENT_TABLE.versionMismatchTitle;
const mismatchOutdatedTitle = I18N_AGENT_TABLE.versionMismatchOutdatedTitle;
const outdatedText = sprintf(I18N_AGENT_TABLE.versionOutdatedText, {
- version: provideData.gitlabVersion,
+ version: provideData.kasVersion,
});
const mismatchText = I18N_AGENT_TABLE.versionMismatchText;
diff --git a/spec/frontend/clusters_list/components/clusters_actions_spec.js b/spec/frontend/clusters_list/components/clusters_actions_spec.js
index 21dcc66c639..f4ee3f93cb5 100644
--- a/spec/frontend/clusters_list/components/clusters_actions_spec.js
+++ b/spec/frontend/clusters_list/components/clusters_actions_spec.js
@@ -1,4 +1,4 @@
-import { GlDropdown, GlDropdownItem, GlTooltip } from '@gitlab/ui';
+import { GlButton, GlDropdown, GlDropdownItem, GlTooltip } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import ClustersActions from '~/clusters_list/components/clusters_actions.vue';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
@@ -7,12 +7,10 @@ import { INSTALL_AGENT_MODAL_ID, CLUSTERS_ACTIONS } from '~/clusters_list/consta
describe('ClustersActionsComponent', () => {
let wrapper;
- const newClusterPath = 'path/to/add/cluster';
const addClusterPath = 'path/to/connect/existing/cluster';
const newClusterDocsPath = 'path/to/create/new/cluster';
const defaultProvide = {
- newClusterPath,
addClusterPath,
newClusterDocsPath,
canAddCluster: true,
@@ -20,13 +18,13 @@ describe('ClustersActionsComponent', () => {
certificateBasedClustersEnabled: true,
};
+ const findButton = () => wrapper.findComponent(GlButton);
const findDropdown = () => wrapper.findComponent(GlDropdown);
const findDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
const findTooltip = () => wrapper.findComponent(GlTooltip);
const findDropdownItemIds = () =>
findDropdownItems().wrappers.map((x) => x.attributes('data-testid'));
const findDropdownItemTexts = () => findDropdownItems().wrappers.map((x) => x.text());
- const findNewClusterLink = () => wrapper.findByTestId('new-cluster-link');
const findNewClusterDocsLink = () => wrapper.findByTestId('create-cluster-link');
const findConnectClusterLink = () => wrapper.findByTestId('connect-cluster-link');
@@ -62,26 +60,6 @@ describe('ClustersActionsComponent', () => {
expect(findTooltip().exists()).toBe(false);
});
- describe('when user cannot add clusters', () => {
- beforeEach(() => {
- createWrapper({ canAddCluster: false });
- });
-
- it('disables dropdown', () => {
- expect(findDropdown().props('disabled')).toBe(true);
- });
-
- it('shows tooltip explaining why dropdown is disabled', () => {
- expect(findTooltip().attributes('title')).toBe(CLUSTERS_ACTIONS.dropdownDisabledHint);
- });
-
- it('does not bind split dropdown button', () => {
- const binding = getBinding(findDropdown().element, 'gl-modal-directive');
-
- expect(binding.value).toBe(false);
- });
- });
-
describe('when on project level', () => {
it(`displays default action as ${CLUSTERS_ACTIONS.connectWithAgent}`, () => {
expect(findDropdown().props('text')).toBe(CLUSTERS_ACTIONS.connectWithAgent);
@@ -93,27 +71,41 @@ describe('ClustersActionsComponent', () => {
expect(binding.value).toBe(INSTALL_AGENT_MODAL_ID);
});
- it('renders a dropdown with 3 actions items', () => {
- expect(findDropdownItemIds()).toEqual([
- 'create-cluster-link',
- 'new-cluster-link',
- 'connect-cluster-link',
- ]);
+ it('renders a dropdown with 2 actions items', () => {
+ expect(findDropdownItemIds()).toEqual(['create-cluster-link', 'connect-cluster-link']);
});
it('renders correct texts for the dropdown items', () => {
expect(findDropdownItemTexts()).toEqual([
CLUSTERS_ACTIONS.createCluster,
- CLUSTERS_ACTIONS.createClusterCertificate,
CLUSTERS_ACTIONS.connectClusterCertificate,
]);
});
it('renders correct href attributes for the links', () => {
expect(findNewClusterDocsLink().attributes('href')).toBe(newClusterDocsPath);
- expect(findNewClusterLink().attributes('href')).toBe(newClusterPath);
expect(findConnectClusterLink().attributes('href')).toBe(addClusterPath);
});
+
+ describe('when user cannot add clusters', () => {
+ beforeEach(() => {
+ createWrapper({ canAddCluster: false });
+ });
+
+ it('disables dropdown', () => {
+ expect(findDropdown().props('disabled')).toBe(true);
+ });
+
+ it('shows tooltip explaining why dropdown is disabled', () => {
+ expect(findTooltip().attributes('title')).toBe(CLUSTERS_ACTIONS.actionsDisabledHint);
+ });
+
+ it('does not bind split dropdown button', () => {
+ const binding = getBinding(findDropdown().element, 'gl-modal-directive');
+
+ expect(binding.value).toBe(false);
+ });
+ });
});
describe('when on group or admin level', () => {
@@ -121,26 +113,34 @@ describe('ClustersActionsComponent', () => {
createWrapper({ displayClusterAgents: false });
});
- it(`displays default action as ${CLUSTERS_ACTIONS.connectClusterDeprecated}`, () => {
- expect(findDropdown().props('text')).toBe(CLUSTERS_ACTIONS.connectClusterDeprecated);
+ it("doesn't render a dropdown", () => {
+ expect(findDropdown().exists()).toBe(false);
});
- it('renders a dropdown with 1 action item', () => {
- expect(findDropdownItemIds()).toEqual(['new-cluster-link']);
+ it('render an action button', () => {
+ expect(findButton().exists()).toBe(true);
});
- it('renders correct text for the dropdown item', () => {
- expect(findDropdownItemTexts()).toEqual([CLUSTERS_ACTIONS.createClusterDeprecated]);
+ it(`displays default action as ${CLUSTERS_ACTIONS.connectClusterDeprecated}`, () => {
+ expect(findButton().text()).toBe(CLUSTERS_ACTIONS.connectClusterDeprecated);
});
- it('renders correct href attributes for the links', () => {
- expect(findNewClusterLink().attributes('href')).toBe(newClusterPath);
+ it('renders correct href attribute for the button', () => {
+ expect(findButton().attributes('href')).toBe(addClusterPath);
});
- it('does not bind dropdown button to modal', () => {
- const binding = getBinding(findDropdown().element, 'gl-modal-directive');
+ describe('when user cannot add clusters', () => {
+ beforeEach(() => {
+ createWrapper({ displayClusterAgents: false, canAddCluster: false });
+ });
+
+ it('disables action button', () => {
+ expect(findButton().props('disabled')).toBe(true);
+ });
- expect(binding.value).toBe(false);
+ it('shows tooltip explaining why dropdown is disabled', () => {
+ expect(findTooltip().attributes('title')).toBe(CLUSTERS_ACTIONS.actionsDisabledHint);
+ });
});
});
});
diff --git a/spec/frontend/code_navigation/components/app_spec.js b/spec/frontend/code_navigation/components/app_spec.js
index f2f97092c5a..b85047dc816 100644
--- a/spec/frontend/code_navigation/components/app_spec.js
+++ b/spec/frontend/code_navigation/components/app_spec.js
@@ -1,6 +1,8 @@
import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import Vuex from 'vuex';
+
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import App from '~/code_navigation/components/app.vue';
import Popover from '~/code_navigation/components/popover.vue';
import createState from '~/code_navigation/store/state';
@@ -75,12 +77,14 @@ describe('Code navigation app component', () => {
});
it('calls showDefinition when clicking blob viewer', () => {
- setFixtures('<div class="blob-viewer"></div>');
+ setHTMLFixture('<div class="blob-viewer"></div>');
factory();
document.querySelector('.blob-viewer').click();
expect(showDefinition).toHaveBeenCalled();
+
+ resetHTMLFixture();
});
});
diff --git a/spec/frontend/code_navigation/store/actions_spec.js b/spec/frontend/code_navigation/store/actions_spec.js
index c26416aca94..c47a9e697b6 100644
--- a/spec/frontend/code_navigation/store/actions_spec.js
+++ b/spec/frontend/code_navigation/store/actions_spec.js
@@ -1,4 +1,5 @@
import MockAdapter from 'axios-mock-adapter';
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import testAction from 'helpers/vuex_action_helper';
import actions from '~/code_navigation/store/actions';
import { setCurrentHoverElement, addInteractionClass } from '~/code_navigation/utils';
@@ -174,12 +175,16 @@ describe('Code navigation actions', () => {
let target;
beforeEach(() => {
- setFixtures(
+ setHTMLFixture(
'<div data-path="index.js"><div class="line"><div class="js-test"></div></div></div>',
);
target = document.querySelector('.js-test');
});
+ afterEach(() => {
+ resetHTMLFixture();
+ });
+
it('returns early when no data exists', () => {
return testAction(actions.showDefinition, { target }, {}, [], []);
});
diff --git a/spec/frontend/code_navigation/utils/index_spec.js b/spec/frontend/code_navigation/utils/index_spec.js
index 682c8bce8c5..b8448709f0b 100644
--- a/spec/frontend/code_navigation/utils/index_spec.js
+++ b/spec/frontend/code_navigation/utils/index_spec.js
@@ -1,3 +1,4 @@
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import {
cachedData,
getCurrentHoverElement,
@@ -35,11 +36,15 @@ describe('setCurrentHoverElement', () => {
describe('addInteractionClass', () => {
beforeEach(() => {
- setFixtures(
+ setHTMLFixture(
'<div data-path="index.js"><div class="blob-content"><div id="LC1" class="line"><span>console</span><span>.</span><span>log</span></div><div id="LC2" class="line"><span>function</span></div></div></div>',
);
});
+ afterEach(() => {
+ resetHTMLFixture();
+ });
+
it.each`
line | char | index
${0} | ${0} | ${0}
@@ -59,7 +64,7 @@ describe('addInteractionClass', () => {
describe('wrapTextNodes', () => {
beforeEach(() => {
- setFixtures(
+ setHTMLFixture(
'<div data-path="index.js"><div class="blob-content"><div id="LC1" class="line"> Text </div></div></div>',
);
});
diff --git a/spec/frontend/commits_spec.js b/spec/frontend/commits_spec.js
index a049a6997f0..db1516ed4ec 100644
--- a/spec/frontend/commits_spec.js
+++ b/spec/frontend/commits_spec.js
@@ -1,6 +1,7 @@
import MockAdapter from 'axios-mock-adapter';
import $ from 'jquery';
import 'vendor/jquery.endless-scroll';
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import CommitsList from '~/commits';
import axios from '~/lib/utils/axios_utils';
import Pager from '~/pager';
@@ -9,7 +10,7 @@ describe('Commits List', () => {
let commitsList;
beforeEach(() => {
- setFixtures(`
+ setHTMLFixture(`
<form class="commits-search-form" action="/h5bp/html5-boilerplate/commits/main">
<input id="commits-search">
</form>
@@ -19,6 +20,10 @@ describe('Commits List', () => {
commitsList = new CommitsList(25);
});
+ afterEach(() => {
+ resetHTMLFixture();
+ });
+
it('should be defined', () => {
expect(CommitsList).toBeDefined();
});
diff --git a/spec/frontend/commons/nav/user_merge_requests_spec.js b/spec/frontend/commons/nav/user_merge_requests_spec.js
index 8f974051232..f660cc8e9de 100644
--- a/spec/frontend/commons/nav/user_merge_requests_spec.js
+++ b/spec/frontend/commons/nav/user_merge_requests_spec.js
@@ -1,3 +1,4 @@
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import * as UserApi from '~/api/user_api';
import {
openUserCountsBroadcast,
@@ -24,11 +25,15 @@ describe('User Merge Requests', () => {
newBroadcastChannelMock = jest.fn().mockImplementation(() => channelMock);
global.BroadcastChannel = newBroadcastChannelMock;
- setFixtures(
+ setHTMLFixture(
`<div><div class="${MR_COUNT_CLASS}">0</div><div class="js-assigned-mr-count"></div><div class="js-reviewer-mr-count"></div></div>`,
);
});
+ afterEach(() => {
+ resetHTMLFixture();
+ });
+
const findMRCountText = () => document.body.querySelector(`.${MR_COUNT_CLASS}`).textContent;
describe('refreshUserMergeRequestCounts', () => {
diff --git a/spec/frontend/content_editor/components/__snapshots__/toolbar_button_spec.js.snap b/spec/frontend/content_editor/components/__snapshots__/toolbar_button_spec.js.snap
index e508cddd6f9..a63cca006da 100644
--- a/spec/frontend/content_editor/components/__snapshots__/toolbar_button_spec.js.snap
+++ b/spec/frontend/content_editor/components/__snapshots__/toolbar_button_spec.js.snap
@@ -1,7 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`content_editor/components/toolbar_button displays tertiary, small button with a provided label and icon 1`] = `
-"<b-button-stub size=\\"sm\\" variant=\\"default\\" type=\\"button\\" tag=\\"button\\" aria-label=\\"Bold\\" title=\\"Bold\\" class=\\"gl-button btn-default-tertiary btn-icon\\">
+exports[`content_editor/components/toolbar_button displays tertiary, medium button with a provided label and icon 1`] = `
+"<b-button-stub size=\\"md\\" variant=\\"default\\" type=\\"button\\" tag=\\"button\\" aria-label=\\"Bold\\" title=\\"Bold\\" class=\\"gl-button btn-default-tertiary btn-icon\\">
<!---->
<gl-icon-stub name=\\"bold\\" size=\\"16\\" class=\\"gl-button-icon\\"></gl-icon-stub>
<!---->
diff --git a/spec/frontend/content_editor/components/code_block_bubble_menu_spec.js b/spec/frontend/content_editor/components/bubble_menus/code_block_spec.js
index 074c311495f..3a15ea45f40 100644
--- a/spec/frontend/content_editor/components/code_block_bubble_menu_spec.js
+++ b/spec/frontend/content_editor/components/bubble_menus/code_block_spec.js
@@ -1,14 +1,14 @@
import { BubbleMenu } from '@tiptap/vue-2';
-import { GlButton, GlDropdown, GlDropdownItem, GlSearchBoxByType } from '@gitlab/ui';
+import { GlDropdown, GlDropdownItem, GlSearchBoxByType } from '@gitlab/ui';
import Vue from 'vue';
import { mountExtended } from 'helpers/vue_test_utils_helper';
-import CodeBlockBubbleMenu from '~/content_editor/components/code_block_bubble_menu.vue';
+import CodeBlockBubbleMenu from '~/content_editor/components/bubble_menus/code_block.vue';
import eventHubFactory from '~/helpers/event_hub_factory';
import CodeBlockHighlight from '~/content_editor/extensions/code_block_highlight';
import codeBlockLanguageLoader from '~/content_editor/services/code_block_language_loader';
-import { createTestEditor, emitEditorEvent } from '../test_utils';
+import { createTestEditor, emitEditorEvent } from '../../test_utils';
-describe('content_editor/components/code_block_bubble_menu', () => {
+describe('content_editor/components/bubble_menus/code_block', () => {
let wrapper;
let tiptapEditor;
let bubbleMenu;
@@ -52,7 +52,7 @@ describe('content_editor/components/code_block_bubble_menu', () => {
await emitEditorEvent({ event: 'transaction', tiptapEditor });
expect(bubbleMenu.props('editor')).toBe(tiptapEditor);
- expect(bubbleMenu.classes()).toEqual(['gl-shadow', 'gl-rounded-base']);
+ expect(bubbleMenu.classes()).toEqual(['gl-shadow', 'gl-rounded-base', 'gl-bg-white']);
});
it('selects plaintext language by default', async () => {
@@ -82,12 +82,26 @@ describe('content_editor/components/code_block_bubble_menu', () => {
expect(wrapper.findComponent(GlDropdown).props('text')).toBe('Custom (nomnoml)');
});
- it('delete button deletes the code block', async () => {
- tiptapEditor.commands.insertContent('<pre lang="javascript">var a = 2;</pre>');
+ describe('copy button', () => {
+ it('copies the text of the code block', async () => {
+ jest.spyOn(navigator.clipboard, 'writeText');
+
+ tiptapEditor.commands.insertContent('<pre lang="javascript">var a = Math.PI / 2;</pre>');
+
+ await wrapper.findByTestId('copy-code-block').vm.$emit('click');
- await wrapper.findComponent(GlButton).vm.$emit('click');
+ expect(navigator.clipboard.writeText).toHaveBeenCalledWith('var a = Math.PI / 2;');
+ });
+ });
- expect(tiptapEditor.getText()).toBe('');
+ describe('delete button', () => {
+ it('deletes the code block', async () => {
+ tiptapEditor.commands.insertContent('<pre lang="javascript">var a = 2;</pre>');
+
+ await wrapper.findByTestId('delete-code-block').vm.$emit('click');
+
+ expect(tiptapEditor.getText()).toBe('');
+ });
});
describe('when opened and search is changed', () => {
@@ -110,7 +124,7 @@ describe('content_editor/components/code_block_bubble_menu', () => {
describe('when dropdown item is clicked', () => {
beforeEach(async () => {
- jest.spyOn(codeBlockLanguageLoader, 'loadLanguages').mockResolvedValue();
+ jest.spyOn(codeBlockLanguageLoader, 'loadLanguage').mockResolvedValue();
findDropdownItems().at(1).vm.$emit('click');
@@ -118,7 +132,7 @@ describe('content_editor/components/code_block_bubble_menu', () => {
});
it('loads language', () => {
- expect(codeBlockLanguageLoader.loadLanguages).toHaveBeenCalledWith(['java']);
+ expect(codeBlockLanguageLoader.loadLanguage).toHaveBeenCalledWith('java');
});
it('sets code block', () => {
diff --git a/spec/frontend/content_editor/components/formatting_bubble_menu_spec.js b/spec/frontend/content_editor/components/bubble_menus/formatting_spec.js
index 192ddee78c6..6479c0ba008 100644
--- a/spec/frontend/content_editor/components/formatting_bubble_menu_spec.js
+++ b/spec/frontend/content_editor/components/bubble_menus/formatting_spec.js
@@ -1,15 +1,15 @@
import { BubbleMenu } from '@tiptap/vue-2';
import { mockTracking } from 'helpers/tracking_helper';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import FormattingBubbleMenu from '~/content_editor/components/formatting_bubble_menu.vue';
+import FormattingBubbleMenu from '~/content_editor/components/bubble_menus/formatting.vue';
import {
BUBBLE_MENU_TRACKING_ACTION,
CONTENT_EDITOR_TRACKING_LABEL,
} from '~/content_editor/constants';
-import { createTestEditor } from '../test_utils';
+import { createTestEditor } from '../../test_utils';
-describe('content_editor/components/formatting_bubble_menu', () => {
+describe('content_editor/components/bubble_menus/formatting', () => {
let wrapper;
let trackingSpy;
let tiptapEditor;
@@ -42,15 +42,16 @@ describe('content_editor/components/formatting_bubble_menu', () => {
const bubbleMenu = wrapper.findComponent(BubbleMenu);
expect(bubbleMenu.props().editor).toBe(tiptapEditor);
- expect(bubbleMenu.classes()).toEqual(['gl-shadow', 'gl-rounded-base']);
+ expect(bubbleMenu.classes()).toEqual(['gl-shadow', 'gl-rounded-base', 'gl-bg-white']);
});
describe.each`
testId | controlProps
- ${'bold'} | ${{ contentType: 'bold', iconName: 'bold', label: 'Bold text', editorCommand: 'toggleBold', size: 'medium', category: 'primary' }}
- ${'italic'} | ${{ contentType: 'italic', iconName: 'italic', label: 'Italic text', editorCommand: 'toggleItalic', size: 'medium', category: 'primary' }}
- ${'strike'} | ${{ contentType: 'strike', iconName: 'strikethrough', label: 'Strikethrough', editorCommand: 'toggleStrike', size: 'medium', category: 'primary' }}
- ${'code'} | ${{ contentType: 'code', iconName: 'code', label: 'Code', editorCommand: 'toggleCode', size: 'medium', category: 'primary' }}
+ ${'bold'} | ${{ contentType: 'bold', iconName: 'bold', label: 'Bold text', editorCommand: 'toggleBold', size: 'medium', category: 'tertiary' }}
+ ${'italic'} | ${{ contentType: 'italic', iconName: 'italic', label: 'Italic text', editorCommand: 'toggleItalic', size: 'medium', category: 'tertiary' }}
+ ${'strike'} | ${{ contentType: 'strike', iconName: 'strikethrough', label: 'Strikethrough', editorCommand: 'toggleStrike', size: 'medium', category: 'tertiary' }}
+ ${'code'} | ${{ contentType: 'code', iconName: 'code', label: 'Code', editorCommand: 'toggleCode', size: 'medium', category: 'tertiary' }}
+ ${'link'} | ${{ contentType: 'link', iconName: 'link', label: 'Insert link', editorCommand: 'toggleLink', editorCommandParams: { href: '' }, size: 'medium', category: 'tertiary' }}
`('given a $testId toolbar control', ({ testId, controlProps }) => {
beforeEach(() => {
buildWrapper();
@@ -60,7 +61,7 @@ describe('content_editor/components/formatting_bubble_menu', () => {
expect(wrapper.findByTestId(testId).exists()).toBe(true);
Object.keys(controlProps).forEach((propName) => {
- expect(wrapper.findByTestId(testId).props(propName)).toBe(controlProps[propName]);
+ expect(wrapper.findByTestId(testId).props(propName)).toEqual(controlProps[propName]);
});
});
diff --git a/spec/frontend/content_editor/components/bubble_menus/link_spec.js b/spec/frontend/content_editor/components/bubble_menus/link_spec.js
new file mode 100644
index 00000000000..ba6d8da9584
--- /dev/null
+++ b/spec/frontend/content_editor/components/bubble_menus/link_spec.js
@@ -0,0 +1,227 @@
+import { GlLink, GlForm } from '@gitlab/ui';
+import { BubbleMenu } from '@tiptap/vue-2';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import LinkBubbleMenu from '~/content_editor/components/bubble_menus/link.vue';
+import eventHubFactory from '~/helpers/event_hub_factory';
+import Link from '~/content_editor/extensions/link';
+import { createTestEditor, emitEditorEvent } from '../../test_utils';
+
+const createFakeEvent = () => ({ preventDefault: jest.fn(), stopPropagation: jest.fn() });
+
+describe('content_editor/components/bubble_menus/link', () => {
+ let wrapper;
+ let tiptapEditor;
+ let contentEditor;
+ let bubbleMenu;
+ let eventHub;
+
+ const buildEditor = () => {
+ tiptapEditor = createTestEditor({ extensions: [Link] });
+ contentEditor = { resolveUrl: jest.fn() };
+ eventHub = eventHubFactory();
+ };
+
+ const buildWrapper = () => {
+ wrapper = mountExtended(LinkBubbleMenu, {
+ provide: {
+ tiptapEditor,
+ contentEditor,
+ eventHub,
+ },
+ });
+ };
+
+ const expectLinkButtonsToExist = (exist = true) => {
+ expect(wrapper.findComponent(GlLink).exists()).toBe(exist);
+ expect(wrapper.findByTestId('copy-link-url').exists()).toBe(exist);
+ expect(wrapper.findByTestId('edit-link').exists()).toBe(exist);
+ expect(wrapper.findByTestId('remove-link').exists()).toBe(exist);
+ };
+
+ beforeEach(async () => {
+ buildEditor();
+ buildWrapper();
+
+ tiptapEditor
+ .chain()
+ .insertContent(
+ 'Download <a href="/path/to/project/-/wikis/uploads/my_file.pdf" data-canonical-src="uploads/my_file.pdf" title="Click here to download">PDF File</a>',
+ )
+ .setTextSelection(14) // put cursor in the middle of the link
+ .run();
+
+ await emitEditorEvent({ event: 'transaction', tiptapEditor });
+
+ bubbleMenu = wrapper.findComponent(BubbleMenu);
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('renders bubble menu component', async () => {
+ expect(bubbleMenu.props('editor')).toBe(tiptapEditor);
+ expect(bubbleMenu.classes()).toEqual(['gl-shadow', 'gl-rounded-base', 'gl-bg-white']);
+ });
+
+ it('shows a clickable link to the URL in the link node', async () => {
+ const link = wrapper.findComponent(GlLink);
+ expect(link.attributes()).toEqual(
+ expect.objectContaining({
+ href: '/path/to/project/-/wikis/uploads/my_file.pdf',
+ 'aria-label': 'uploads/my_file.pdf',
+ title: 'uploads/my_file.pdf',
+ target: '_blank',
+ }),
+ );
+ expect(link.text()).toBe('uploads/my_file.pdf');
+ });
+
+ describe('copy button', () => {
+ it('copies the canonical link to clipboard', async () => {
+ jest.spyOn(navigator.clipboard, 'writeText');
+
+ await wrapper.findByTestId('copy-link-url').vm.$emit('click');
+
+ expect(navigator.clipboard.writeText).toHaveBeenCalledWith('uploads/my_file.pdf');
+ });
+ });
+
+ describe('remove link button', () => {
+ it('removes the link', async () => {
+ await wrapper.findByTestId('remove-link').vm.$emit('click');
+
+ expect(tiptapEditor.getHTML()).toBe('<p>Download PDF File</p>');
+ });
+ });
+
+ describe('for a placeholder link', () => {
+ beforeEach(async () => {
+ tiptapEditor
+ .chain()
+ .clearContent()
+ .insertContent('Dummy link')
+ .selectAll()
+ .setLink({ href: '' })
+ .setTextSelection(4)
+ .run();
+
+ await emitEditorEvent({ event: 'transaction', tiptapEditor });
+ });
+
+ it('directly opens the edit form for a placeholder link', async () => {
+ expectLinkButtonsToExist(false);
+
+ expect(wrapper.findComponent(GlForm).exists()).toBe(true);
+ });
+
+ it('removes the link on clicking apply (if no change)', async () => {
+ await wrapper.findComponent(GlForm).vm.$emit('submit', createFakeEvent());
+
+ expect(tiptapEditor.getHTML()).toBe('<p>Dummy link</p>');
+ });
+
+ it('removes the link on clicking cancel', async () => {
+ await wrapper.findByTestId('cancel-link').vm.$emit('click');
+
+ expect(tiptapEditor.getHTML()).toBe('<p>Dummy link</p>');
+ });
+ });
+
+ describe('edit button', () => {
+ let linkHrefInput;
+ let linkTitleInput;
+
+ beforeEach(async () => {
+ await wrapper.findByTestId('edit-link').vm.$emit('click');
+
+ linkHrefInput = wrapper.findByTestId('link-href');
+ linkTitleInput = wrapper.findByTestId('link-title');
+ });
+
+ it('hides the link and copy/edit/remove link buttons', async () => {
+ expectLinkButtonsToExist(false);
+ });
+
+ it('shows a form to edit the link', () => {
+ expect(wrapper.findComponent(GlForm).exists()).toBe(true);
+
+ expect(linkHrefInput.element.value).toBe('uploads/my_file.pdf');
+ expect(linkTitleInput.element.value).toBe('Click here to download');
+ });
+
+ it('extends selection to select the entire link', () => {
+ const { from, to } = tiptapEditor.state.selection;
+
+ expect(from).toBe(10);
+ expect(to).toBe(18);
+ });
+
+ it('shows the copy/edit/remove link buttons again if selection changes to another non-link and then back again to a link', async () => {
+ expectLinkButtonsToExist(false);
+
+ tiptapEditor.commands.setTextSelection(3);
+ await emitEditorEvent({ event: 'transaction', tiptapEditor });
+
+ tiptapEditor.commands.setTextSelection(14);
+ await emitEditorEvent({ event: 'transaction', tiptapEditor });
+
+ expectLinkButtonsToExist(true);
+ expect(wrapper.findComponent(GlForm).exists()).toBe(false);
+ });
+
+ describe('after making changes in the form and clicking apply', () => {
+ beforeEach(async () => {
+ linkHrefInput.setValue('https://google.com');
+ linkTitleInput.setValue('Search Google');
+
+ contentEditor.resolveUrl.mockResolvedValue('https://google.com');
+
+ await wrapper.findComponent(GlForm).vm.$emit('submit', createFakeEvent());
+ });
+
+ it('updates prosemirror doc with new link', async () => {
+ expect(tiptapEditor.getHTML()).toBe(
+ '<p>Download <a target="_blank" rel="noopener noreferrer nofollow" href="https://google.com" title="Search Google" canonicalsrc="https://google.com">PDF File</a></p>',
+ );
+ });
+
+ it('updates the link in the bubble menu', () => {
+ const link = wrapper.findComponent(GlLink);
+ expect(link.attributes()).toEqual(
+ expect.objectContaining({
+ href: 'https://google.com',
+ 'aria-label': 'https://google.com',
+ title: 'https://google.com',
+ target: '_blank',
+ }),
+ );
+ expect(link.text()).toBe('https://google.com');
+ });
+ });
+
+ describe('after making changes in the form and clicking cancel', () => {
+ beforeEach(async () => {
+ linkHrefInput.setValue('https://google.com');
+ linkTitleInput.setValue('Search Google');
+
+ await wrapper.findByTestId('cancel-link').vm.$emit('click');
+ });
+
+ it('hides the form and shows the copy/edit/remove link buttons', () => {
+ expectLinkButtonsToExist();
+ });
+
+ it('resets the form with old values of the link from prosemirror', async () => {
+ // click edit once again to show the form back
+ await wrapper.findByTestId('edit-link').vm.$emit('click');
+
+ linkHrefInput = wrapper.findByTestId('link-href');
+ linkTitleInput = wrapper.findByTestId('link-title');
+
+ expect(linkHrefInput.element.value).toBe('uploads/my_file.pdf');
+ expect(linkTitleInput.element.value).toBe('Click here to download');
+ });
+ });
+ });
+});
diff --git a/spec/frontend/content_editor/components/bubble_menus/media_spec.js b/spec/frontend/content_editor/components/bubble_menus/media_spec.js
new file mode 100644
index 00000000000..8839caea80e
--- /dev/null
+++ b/spec/frontend/content_editor/components/bubble_menus/media_spec.js
@@ -0,0 +1,234 @@
+import { GlLink, GlForm } from '@gitlab/ui';
+import { BubbleMenu } from '@tiptap/vue-2';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import MediaBubbleMenu from '~/content_editor/components/bubble_menus/media.vue';
+import eventHubFactory from '~/helpers/event_hub_factory';
+import Image from '~/content_editor/extensions/image';
+import Audio from '~/content_editor/extensions/audio';
+import Video from '~/content_editor/extensions/video';
+import { createTestEditor, emitEditorEvent, mockChainedCommands } from '../../test_utils';
+import {
+ PROJECT_WIKI_ATTACHMENT_IMAGE_HTML,
+ PROJECT_WIKI_ATTACHMENT_AUDIO_HTML,
+ PROJECT_WIKI_ATTACHMENT_VIDEO_HTML,
+} from '../../test_constants';
+
+const TIPTAP_IMAGE_HTML = `<p>
+ <img src="https://gitlab.com/favicon.png" alt="gitlab favicon" title="gitlab favicon" data-canonical-src="https://gitlab.com/favicon.png">
+</p>`;
+
+const TIPTAP_AUDIO_HTML = `<p>
+ <span class="media-container audio-container"><audio src="https://gitlab.com/favicon.png" controls="true" data-setup="{}" data-title="gitlab favicon"></audio><a href="https://gitlab.com/favicon.png">gitlab favicon</a></span>
+</p>`;
+
+const TIPTAP_VIDEO_HTML = `<p>
+ <span class="media-container video-container"><video src="https://gitlab.com/favicon.png" controls="true" data-setup="{}" data-title="gitlab favicon"></video><a href="https://gitlab.com/favicon.png">gitlab favicon</a></span>
+</p>`;
+
+const createFakeEvent = () => ({ preventDefault: jest.fn(), stopPropagation: jest.fn() });
+
+describe.each`
+ mediaType | mediaHTML | filePath | mediaOutputHTML
+ ${'image'} | ${PROJECT_WIKI_ATTACHMENT_IMAGE_HTML} | ${'test-file.png'} | ${TIPTAP_IMAGE_HTML}
+ ${'audio'} | ${PROJECT_WIKI_ATTACHMENT_AUDIO_HTML} | ${'test-file.mp3'} | ${TIPTAP_AUDIO_HTML}
+ ${'video'} | ${PROJECT_WIKI_ATTACHMENT_VIDEO_HTML} | ${'test-file.mp4'} | ${TIPTAP_VIDEO_HTML}
+`(
+ 'content_editor/components/bubble_menus/media ($mediaType)',
+ ({ mediaType, mediaHTML, filePath, mediaOutputHTML }) => {
+ let wrapper;
+ let tiptapEditor;
+ let contentEditor;
+ let bubbleMenu;
+ let eventHub;
+
+ const buildEditor = () => {
+ tiptapEditor = createTestEditor({ extensions: [Image, Audio, Video] });
+ contentEditor = { resolveUrl: jest.fn() };
+ eventHub = eventHubFactory();
+ };
+
+ const buildWrapper = () => {
+ wrapper = mountExtended(MediaBubbleMenu, {
+ provide: {
+ tiptapEditor,
+ contentEditor,
+ eventHub,
+ },
+ });
+ };
+
+ const selectFile = async (file) => {
+ const input = wrapper.find({ ref: 'fileSelector' });
+
+ // override the property definition because `input.files` isn't directly modifyable
+ Object.defineProperty(input.element, 'files', { value: [file], writable: true });
+ await input.trigger('change');
+ };
+
+ const expectLinkButtonsToExist = (exist = true) => {
+ expect(wrapper.findComponent(GlLink).exists()).toBe(exist);
+ expect(wrapper.findByTestId('copy-media-src').exists()).toBe(exist);
+ expect(wrapper.findByTestId('edit-media').exists()).toBe(exist);
+ expect(wrapper.findByTestId('delete-media').exists()).toBe(exist);
+ };
+
+ beforeEach(async () => {
+ buildEditor();
+ buildWrapper();
+
+ tiptapEditor
+ .chain()
+ .insertContent(mediaHTML)
+ .setNodeSelection(4) // select the media
+ .run();
+
+ contentEditor.resolveUrl.mockResolvedValue(`/group1/project1/-/wikis/${filePath}`);
+
+ await emitEditorEvent({ event: 'transaction', tiptapEditor });
+
+ bubbleMenu = wrapper.findComponent(BubbleMenu);
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('renders bubble menu component', async () => {
+ expect(bubbleMenu.props('editor')).toBe(tiptapEditor);
+ expect(bubbleMenu.classes()).toEqual(['gl-shadow', 'gl-rounded-base', 'gl-bg-white']);
+ });
+
+ it('shows a clickable link to the image', async () => {
+ const link = wrapper.findComponent(GlLink);
+ expect(link.attributes()).toEqual(
+ expect.objectContaining({
+ href: `/group1/project1/-/wikis/${filePath}`,
+ 'aria-label': filePath,
+ title: filePath,
+ target: '_blank',
+ }),
+ );
+ expect(link.text()).toBe(filePath);
+ });
+
+ describe('copy button', () => {
+ it(`copies the canonical link to the ${mediaType} to clipboard`, async () => {
+ jest.spyOn(navigator.clipboard, 'writeText');
+
+ await wrapper.findByTestId('copy-media-src').vm.$emit('click');
+
+ expect(navigator.clipboard.writeText).toHaveBeenCalledWith(filePath);
+ });
+ });
+
+ describe(`remove ${mediaType} button`, () => {
+ it(`removes the ${mediaType}`, async () => {
+ await wrapper.findByTestId('delete-media').vm.$emit('click');
+
+ expect(tiptapEditor.getHTML()).toBe('<p>\n \n</p>');
+ });
+ });
+
+ describe(`replace ${mediaType} button`, () => {
+ it('uploads and replaces the selected image when file input changes', async () => {
+ const commands = mockChainedCommands(tiptapEditor, [
+ 'focus',
+ 'deleteSelection',
+ 'uploadAttachment',
+ 'run',
+ ]);
+ const file = new File(['foo'], 'foo.png', { type: 'image/png' });
+
+ await wrapper.findByTestId('replace-media').vm.$emit('click');
+ await selectFile(file);
+
+ expect(commands.focus).toHaveBeenCalled();
+ expect(commands.deleteSelection).toHaveBeenCalled();
+ expect(commands.uploadAttachment).toHaveBeenCalledWith({ file });
+ expect(commands.run).toHaveBeenCalled();
+ });
+ });
+
+ describe('edit button', () => {
+ let mediaSrcInput;
+ let mediaTitleInput;
+ let mediaAltInput;
+
+ beforeEach(async () => {
+ await wrapper.findByTestId('edit-media').vm.$emit('click');
+
+ mediaSrcInput = wrapper.findByTestId('media-src');
+ mediaTitleInput = wrapper.findByTestId('media-title');
+ mediaAltInput = wrapper.findByTestId('media-alt');
+ });
+
+ it('hides the link and copy/edit/remove link buttons', async () => {
+ expectLinkButtonsToExist(false);
+ });
+
+ it(`shows a form to edit the ${mediaType} src/title/alt`, () => {
+ expect(wrapper.findComponent(GlForm).exists()).toBe(true);
+
+ expect(mediaSrcInput.element.value).toBe(filePath);
+ expect(mediaTitleInput.element.value).toBe('');
+ expect(mediaAltInput.element.value).toBe('test-file');
+ });
+
+ describe('after making changes in the form and clicking apply', () => {
+ beforeEach(async () => {
+ mediaSrcInput.setValue('https://gitlab.com/favicon.png');
+ mediaAltInput.setValue('gitlab favicon');
+ mediaTitleInput.setValue('gitlab favicon');
+
+ contentEditor.resolveUrl.mockResolvedValue('https://gitlab.com/favicon.png');
+
+ await wrapper.findComponent(GlForm).vm.$emit('submit', createFakeEvent());
+ });
+
+ it(`updates prosemirror doc with new src to the ${mediaType}`, async () => {
+ expect(tiptapEditor.getHTML()).toBe(mediaOutputHTML);
+ });
+
+ it(`updates the link to the ${mediaType} in the bubble menu`, () => {
+ const link = wrapper.findComponent(GlLink);
+ expect(link.attributes()).toEqual(
+ expect.objectContaining({
+ href: 'https://gitlab.com/favicon.png',
+ 'aria-label': 'https://gitlab.com/favicon.png',
+ title: 'https://gitlab.com/favicon.png',
+ target: '_blank',
+ }),
+ );
+ expect(link.text()).toBe('https://gitlab.com/favicon.png');
+ });
+ });
+
+ describe('after making changes in the form and clicking cancel', () => {
+ beforeEach(async () => {
+ mediaSrcInput.setValue('https://gitlab.com/favicon.png');
+ mediaAltInput.setValue('gitlab favicon');
+ mediaTitleInput.setValue('gitlab favicon');
+
+ await wrapper.findByTestId('cancel-editing-media').vm.$emit('click');
+ });
+
+ it('hides the form and shows the copy/edit/remove link buttons', () => {
+ expectLinkButtonsToExist();
+ });
+
+ it(`resets the form with old values of the ${mediaType} from prosemirror`, async () => {
+ // click edit once again to show the form back
+ await wrapper.findByTestId('edit-media').vm.$emit('click');
+
+ mediaSrcInput = wrapper.findByTestId('media-src');
+ mediaTitleInput = wrapper.findByTestId('media-title');
+ mediaAltInput = wrapper.findByTestId('media-alt');
+
+ expect(mediaSrcInput.element.value).toBe(filePath);
+ expect(mediaAltInput.element.value).toBe('test-file');
+ expect(mediaTitleInput.element.value).toBe('');
+ });
+ });
+ });
+ },
+);
diff --git a/spec/frontend/content_editor/components/content_editor_spec.js b/spec/frontend/content_editor/components/content_editor_spec.js
index 73fcfeab8bc..9ee3b017831 100644
--- a/spec/frontend/content_editor/components/content_editor_spec.js
+++ b/spec/frontend/content_editor/components/content_editor_spec.js
@@ -4,7 +4,7 @@ import ContentEditor from '~/content_editor/components/content_editor.vue';
import ContentEditorAlert from '~/content_editor/components/content_editor_alert.vue';
import ContentEditorProvider from '~/content_editor/components/content_editor_provider.vue';
import EditorStateObserver from '~/content_editor/components/editor_state_observer.vue';
-import FormattingBubbleMenu from '~/content_editor/components/formatting_bubble_menu.vue';
+import FormattingBubbleMenu from '~/content_editor/components/bubble_menus/formatting.vue';
import TopToolbar from '~/content_editor/components/top_toolbar.vue';
import LoadingIndicator from '~/content_editor/components/loading_indicator.vue';
import { emitEditorEvent } from '../test_utils';
diff --git a/spec/frontend/content_editor/components/toolbar_button_spec.js b/spec/frontend/content_editor/components/toolbar_button_spec.js
index ce50482302d..1f1f7b338c6 100644
--- a/spec/frontend/content_editor/components/toolbar_button_spec.js
+++ b/spec/frontend/content_editor/components/toolbar_button_spec.js
@@ -46,7 +46,7 @@ describe('content_editor/components/toolbar_button', () => {
wrapper.destroy();
});
- it('displays tertiary, small button with a provided label and icon', () => {
+ it('displays tertiary, medium button with a provided label and icon', () => {
buildWrapper();
expect(findButton().html()).toMatchSnapshot();
diff --git a/spec/frontend/content_editor/components/wrappers/frontmatter_spec.js b/spec/frontend/content_editor/components/wrappers/code_block_spec.js
index 415f1314a36..a564959a3a6 100644
--- a/spec/frontend/content_editor/components/wrappers/frontmatter_spec.js
+++ b/spec/frontend/content_editor/components/wrappers/code_block_spec.js
@@ -1,20 +1,33 @@
+import { nextTick } from 'vue';
import { NodeViewWrapper, NodeViewContent } from '@tiptap/vue-2';
import { shallowMount } from '@vue/test-utils';
-import FrontmatterWrapper from '~/content_editor/components/wrappers/frontmatter.vue';
+import CodeBlockWrapper from '~/content_editor/components/wrappers/code_block.vue';
+import codeBlockLanguageLoader from '~/content_editor/services/code_block_language_loader';
-describe('content/components/wrappers/frontmatter', () => {
+jest.mock('~/content_editor/services/code_block_language_loader');
+
+describe('content/components/wrappers/code_block', () => {
+ const language = 'yaml';
let wrapper;
+ let updateAttributesFn;
+
+ const createWrapper = async (nodeAttrs = { language }) => {
+ updateAttributesFn = jest.fn();
- const createWrapper = async (nodeAttrs = { language: 'yaml' }) => {
- wrapper = shallowMount(FrontmatterWrapper, {
+ wrapper = shallowMount(CodeBlockWrapper, {
propsData: {
node: {
attrs: nodeAttrs,
},
+ updateAttributes: updateAttributesFn,
},
});
};
+ beforeEach(() => {
+ codeBlockLanguageLoader.findLanguageBySyntax.mockReturnValue({ syntax: language });
+ });
+
afterEach(() => {
wrapper.destroy();
});
@@ -38,11 +51,21 @@ describe('content/components/wrappers/frontmatter', () => {
});
it('renders label indicating that code block is frontmatter', () => {
- createWrapper();
+ createWrapper({ isFrontmatter: true, language });
const label = wrapper.find('[data-testid="frontmatter-label"]');
expect(label.text()).toEqual('frontmatter:yaml');
expect(label.classes()).toEqual(['gl-absolute', 'gl-top-0', 'gl-right-3']);
});
+
+ it('loads code block’s syntax highlight language', async () => {
+ createWrapper();
+
+ expect(codeBlockLanguageLoader.loadLanguage).toHaveBeenCalledWith(language);
+
+ await nextTick();
+
+ expect(updateAttributesFn).toHaveBeenCalledWith({ language });
+ });
});
diff --git a/spec/frontend/content_editor/components/wrappers/media_spec.js b/spec/frontend/content_editor/components/wrappers/media_spec.js
deleted file mode 100644
index 3e95e2f3914..00000000000
--- a/spec/frontend/content_editor/components/wrappers/media_spec.js
+++ /dev/null
@@ -1,69 +0,0 @@
-import { GlLoadingIcon } from '@gitlab/ui';
-import { NodeViewWrapper } from '@tiptap/vue-2';
-import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import MediaWrapper from '~/content_editor/components/wrappers/media.vue';
-
-describe('content/components/wrappers/media', () => {
- let wrapper;
-
- const createWrapper = async (nodeAttrs = {}) => {
- wrapper = shallowMountExtended(MediaWrapper, {
- propsData: {
- node: {
- attrs: nodeAttrs,
- type: {
- name: 'image',
- },
- },
- },
- });
- };
- const findMedia = () => wrapper.findByTestId('media');
- const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- it('renders a node-view-wrapper with display-inline-block class', () => {
- createWrapper();
-
- expect(wrapper.findComponent(NodeViewWrapper).classes()).toContain('gl-display-inline-block');
- });
-
- it('renders an image that displays the node src', () => {
- const src = 'foobar.png';
-
- createWrapper({ src });
-
- expect(findMedia().attributes().src).toBe(src);
- });
-
- describe('when uploading', () => {
- beforeEach(() => {
- createWrapper({ uploading: true });
- });
-
- it('renders a gl-loading-icon component', () => {
- expect(findLoadingIcon().exists()).toBe(true);
- });
-
- it('adds gl-opacity-5 class selector to the media tag', () => {
- expect(findMedia().classes()).toContain('gl-opacity-5');
- });
- });
-
- describe('when not uploading', () => {
- beforeEach(() => {
- createWrapper({ uploading: false });
- });
-
- it('does not render a gl-loading-icon component', () => {
- expect(findLoadingIcon().exists()).toBe(false);
- });
-
- it('does not add gl-opacity-5 class selector to the media tag', () => {
- expect(findMedia().classes()).not.toContain('gl-opacity-5');
- });
- });
-});
diff --git a/spec/frontend/content_editor/extensions/attachment_spec.js b/spec/frontend/content_editor/extensions/attachment_spec.js
index d3c42104e47..d528096be34 100644
--- a/spec/frontend/content_editor/extensions/attachment_spec.js
+++ b/spec/frontend/content_editor/extensions/attachment_spec.js
@@ -11,32 +11,12 @@ import { VARIANT_DANGER } from '~/flash';
import httpStatus from '~/lib/utils/http_status';
import eventHubFactory from '~/helpers/event_hub_factory';
import { createTestEditor, createDocBuilder } from '../test_utils';
-
-const PROJECT_WIKI_ATTACHMENT_IMAGE_HTML = `<p data-sourcepos="1:1-1:27" dir="auto">
- <a class="no-attachment-icon" href="/group1/project1/-/wikis/test-file.png" target="_blank" rel="noopener noreferrer" data-canonical-src="test-file.png">
- <img alt="test-file" class="lazy" data-src="/group1/project1/-/wikis/test-file.png" data-canonical-src="test-file.png">
- </a>
-</p>`;
-
-const PROJECT_WIKI_ATTACHMENT_VIDEO_HTML = `<p data-sourcepos="1:1-1:132" dir="auto">
- <span class="media-container video-container">
- <video src="/group1/project1/-/wikis/test-file.mp4" controls="true" data-setup="{}" data-title="test-file" width="400" preload="metadata" data-canonical-src="test-file.mp4">
- </video>
- <a href="/himkp/test/-/wikis/test-file.mp4" target="_blank" rel="noopener noreferrer" title="Download 'test-file'" data-canonical-src="test-file.mp4">test-file</a>
- </span>
-</p>`;
-
-const PROJECT_WIKI_ATTACHMENT_AUDIO_HTML = `<p data-sourcepos="3:1-3:74" dir="auto">
- <span class="media-container audio-container">
- <audio src="/himkp/test/-/wikis/test-file.mp3" controls="true" data-setup="{}" data-title="test-file" data-canonical-src="test-file.mp3">
- </audio>
- <a href="/himkp/test/-/wikis/test-file.mp3" target="_blank" rel="noopener noreferrer" title="Download 'test-file'" data-canonical-src="test-file.mp3">test-file</a>
- </span>
-</p>`;
-
-const PROJECT_WIKI_ATTACHMENT_LINK_HTML = `<p data-sourcepos="1:1-1:26" dir="auto">
- <a href="/group1/project1/-/wikis/test-file.zip" data-canonical-src="test-file.zip">test-file</a>
-</p>`;
+import {
+ PROJECT_WIKI_ATTACHMENT_IMAGE_HTML,
+ PROJECT_WIKI_ATTACHMENT_AUDIO_HTML,
+ PROJECT_WIKI_ATTACHMENT_VIDEO_HTML,
+ PROJECT_WIKI_ATTACHMENT_LINK_HTML,
+} from '../test_constants';
describe('content_editor/extensions/attachment', () => {
let tiptapEditor;
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 02e5b1dc271..fc8460c7f84 100644
--- a/spec/frontend/content_editor/extensions/code_block_highlight_spec.js
+++ b/spec/frontend/content_editor/extensions/code_block_highlight_spec.js
@@ -1,4 +1,5 @@
import CodeBlockHighlight from '~/content_editor/extensions/code_block_highlight';
+import languageLoader from '~/content_editor/services/code_block_language_loader';
import { createTestEditor, createDocBuilder, triggerNodeInputRule } from '../test_utils';
const CODE_BLOCK_HTML = `<pre class="code highlight js-syntax-highlight language-javascript" lang="javascript" v-pre="true">
@@ -9,20 +10,20 @@ const CODE_BLOCK_HTML = `<pre class="code highlight js-syntax-highlight language
</code>
</pre>`;
+jest.mock('~/content_editor/services/code_block_language_loader');
+
describe('content_editor/extensions/code_block_highlight', () => {
let parsedCodeBlockHtmlFixture;
let tiptapEditor;
let doc;
let codeBlock;
- let languageLoader;
const parseHTML = (html) => new DOMParser().parseFromString(html, 'text/html');
const preElement = () => parsedCodeBlockHtmlFixture.querySelector('pre');
beforeEach(() => {
- languageLoader = { loadLanguages: jest.fn() };
tiptapEditor = createTestEditor({
- extensions: [CodeBlockHighlight.configure({ languageLoader })],
+ extensions: [CodeBlockHighlight],
});
({
@@ -70,6 +71,8 @@ describe('content_editor/extensions/code_block_highlight', () => {
const language = 'javascript';
beforeEach(() => {
+ languageLoader.loadLanguageFromInputRule.mockReturnValueOnce({ language });
+
triggerNodeInputRule({
tiptapEditor,
inputRuleText: `${inputRule}${language} `,
@@ -83,7 +86,9 @@ describe('content_editor/extensions/code_block_highlight', () => {
});
it('loads language when language loader is available', () => {
- expect(languageLoader.loadLanguages).toHaveBeenCalledWith([language]);
+ expect(languageLoader.loadLanguageFromInputRule).toHaveBeenCalledWith(
+ expect.arrayContaining([`${inputRule}${language} `, language]),
+ );
});
});
});
diff --git a/spec/frontend/content_editor/extensions/diagram_spec.js b/spec/frontend/content_editor/extensions/diagram_spec.js
new file mode 100644
index 00000000000..b8d9e0b5aeb
--- /dev/null
+++ b/spec/frontend/content_editor/extensions/diagram_spec.js
@@ -0,0 +1,16 @@
+import Diagram from '~/content_editor/extensions/diagram';
+import CodeBlockHighlight from '~/content_editor/extensions/code_block_highlight';
+
+describe('content_editor/extensions/diagram', () => {
+ it('inherits from code block highlight extension', () => {
+ expect(Diagram.parent).toBe(CodeBlockHighlight);
+ });
+
+ it('sets isDiagram attribute to true by default', () => {
+ expect(Diagram.config.addAttributes()).toEqual(
+ expect.objectContaining({
+ isDiagram: { default: true },
+ }),
+ );
+ });
+});
diff --git a/spec/frontend/content_editor/extensions/frontmatter_spec.js b/spec/frontend/content_editor/extensions/frontmatter_spec.js
index 4f80c2cb81a..9bd29070858 100644
--- a/spec/frontend/content_editor/extensions/frontmatter_spec.js
+++ b/spec/frontend/content_editor/extensions/frontmatter_spec.js
@@ -22,6 +22,10 @@ describe('content_editor/extensions/frontmatter', () => {
}));
});
+ it('inherits from code block highlight extension', () => {
+ expect(Frontmatter.parent).toBe(CodeBlockHighlight);
+ });
+
it('does not insert a frontmatter block when executing code block input rule', () => {
const expectedDoc = doc(codeBlock({ language: 'plaintext' }, ''));
const inputRuleText = '``` ';
@@ -31,6 +35,14 @@ describe('content_editor/extensions/frontmatter', () => {
expect(tiptapEditor.getJSON()).toEqual(expectedDoc.toJSON());
});
+ it('sets isFrontmatter attribute to true by default', () => {
+ expect(Frontmatter.config.addAttributes()).toEqual(
+ expect.objectContaining({
+ isFrontmatter: { default: true },
+ }),
+ );
+ });
+
it.each`
command | result | resultDesc
${'toggleCodeBlock'} | ${() => doc(codeBlock(''))} | ${'code block element'}
diff --git a/spec/frontend/content_editor/extensions/paste_markdown_spec.js b/spec/frontend/content_editor/extensions/paste_markdown_spec.js
index 8f734c7dabc..5d46c2c0650 100644
--- a/spec/frontend/content_editor/extensions/paste_markdown_spec.js
+++ b/spec/frontend/content_editor/extensions/paste_markdown_spec.js
@@ -1,4 +1,7 @@
import PasteMarkdown from '~/content_editor/extensions/paste_markdown';
+import CodeBlockHighlight from '~/content_editor/extensions/code_block_highlight';
+import Diagram from '~/content_editor/extensions/diagram';
+import Frontmatter from '~/content_editor/extensions/frontmatter';
import Bold from '~/content_editor/extensions/bold';
import { VARIANT_DANGER } from '~/flash';
import eventHubFactory from '~/helpers/event_hub_factory';
@@ -11,6 +14,12 @@ import {
import waitForPromises from 'helpers/wait_for_promises';
import { createTestEditor, createDocBuilder, waitUntilNextDocTransaction } from '../test_utils';
+const CODE_BLOCK_HTML = '<pre lang="javascript">var a = 2;</pre>';
+const DIAGRAM_HTML =
+ '<img data-diagram="nomnoml" data-diagram-src="data:text/plain;base64,WzxmcmFtZT5EZWNvcmF0b3IgcGF0dGVybl0=">';
+const FRONTMATTER_HTML = '<pre lang="yaml" data-lang-params="frontmatter">key: value</pre>';
+const PARAGRAPH_HTML = '<p>Just a regular paragraph</p>';
+
describe('content_editor/extensions/paste_markdown', () => {
let tiptapEditor;
let doc;
@@ -27,7 +36,13 @@ describe('content_editor/extensions/paste_markdown', () => {
jest.spyOn(eventHub, '$emit');
tiptapEditor = createTestEditor({
- extensions: [PasteMarkdown.configure({ renderMarkdown, eventHub }), Bold],
+ extensions: [
+ Bold,
+ CodeBlockHighlight,
+ Diagram,
+ Frontmatter,
+ PasteMarkdown.configure({ renderMarkdown, eventHub }),
+ ],
});
({
@@ -35,7 +50,7 @@ describe('content_editor/extensions/paste_markdown', () => {
} = createDocBuilder({
tiptapEditor,
names: {
- Bold: { markType: Bold.name },
+ bold: { markType: Bold.name },
},
}));
});
@@ -47,13 +62,11 @@ describe('content_editor/extensions/paste_markdown', () => {
};
const triggerPasteEventHandler = (event) => {
- let handled = false;
-
- tiptapEditor.view.someProp('handlePaste', (eventHandler) => {
- handled = eventHandler(tiptapEditor.view, event);
+ return new Promise((resolve) => {
+ tiptapEditor.view.someProp('handlePaste', (eventHandler) => {
+ resolve(eventHandler(tiptapEditor.view, event));
+ });
});
-
- return handled;
};
const triggerPasteEventHandlerAndWaitForTransaction = (event) => {
@@ -73,8 +86,20 @@ describe('content_editor/extensions/paste_markdown', () => {
${['text/plain', 'text/html']} | ${{}} | ${false} | ${'doesn’t handle html format'}
${['text/plain', 'text/html', 'vscode-editor-data']} | ${{ 'vscode-editor-data': '{ "mode": "markdown" }' }} | ${true} | ${'handles vscode markdown'}
${['text/plain', 'text/html', 'vscode-editor-data']} | ${{ 'vscode-editor-data': '{ "mode": "ruby" }' }} | ${false} | ${'doesn’t vscode code snippet'}
- `('$desc', ({ types, handled, data }) => {
- expect(triggerPasteEventHandler(buildClipboardEvent({ types, data }))).toBe(handled);
+ `('$desc', async ({ types, handled, data }) => {
+ expect(await triggerPasteEventHandler(buildClipboardEvent({ types, data }))).toBe(handled);
+ });
+
+ it.each`
+ nodeType | html | handled | desc
+ ${'codeBlock'} | ${CODE_BLOCK_HTML} | ${false} | ${'does not handle'}
+ ${'diagram'} | ${DIAGRAM_HTML} | ${false} | ${'does not handle'}
+ ${'frontmatter'} | ${FRONTMATTER_HTML} | ${false} | ${'does not handle'}
+ ${'paragraph'} | ${PARAGRAPH_HTML} | ${true} | ${'handles'}
+ `('$desc paste if currently a `$nodeType` is in focus', async ({ html, handled }) => {
+ tiptapEditor.commands.insertContent(html);
+
+ expect(await triggerPasteEventHandler(buildClipboardEvent())).toBe(handled);
});
describe('when pasting raw markdown source', () => {
@@ -105,16 +130,14 @@ describe('content_editor/extensions/paste_markdown', () => {
});
it(`triggers ${LOADING_ERROR_EVENT} event`, async () => {
- triggerPasteEventHandler(buildClipboardEvent());
-
+ await triggerPasteEventHandler(buildClipboardEvent());
await waitForPromises();
expect(eventHub.$emit).toHaveBeenCalledWith(LOADING_ERROR_EVENT);
});
it(`triggers ${ALERT_EVENT} event`, async () => {
- triggerPasteEventHandler(buildClipboardEvent());
-
+ await triggerPasteEventHandler(buildClipboardEvent());
await waitForPromises();
expect(eventHub.$emit).toHaveBeenCalledWith(ALERT_EVENT, {
diff --git a/spec/frontend/content_editor/remark_markdown_processing_spec.js b/spec/frontend/content_editor/remark_markdown_processing_spec.js
new file mode 100644
index 00000000000..6348b97d918
--- /dev/null
+++ b/spec/frontend/content_editor/remark_markdown_processing_spec.js
@@ -0,0 +1,248 @@
+import Bold from '~/content_editor/extensions/bold';
+import Blockquote from '~/content_editor/extensions/blockquote';
+import BulletList from '~/content_editor/extensions/bullet_list';
+import Code from '~/content_editor/extensions/code';
+import CodeBlockHighlight from '~/content_editor/extensions/code_block_highlight';
+import HardBreak from '~/content_editor/extensions/hard_break';
+import Heading from '~/content_editor/extensions/heading';
+import HorizontalRule from '~/content_editor/extensions/horizontal_rule';
+import Image from '~/content_editor/extensions/image';
+import Italic from '~/content_editor/extensions/italic';
+import Link from '~/content_editor/extensions/link';
+import ListItem from '~/content_editor/extensions/list_item';
+import OrderedList from '~/content_editor/extensions/ordered_list';
+import Sourcemap from '~/content_editor/extensions/sourcemap';
+import remarkMarkdownDeserializer from '~/content_editor/services/remark_markdown_deserializer';
+import markdownSerializer from '~/content_editor/services/markdown_serializer';
+
+import { createTestEditor } from './test_utils';
+
+const tiptapEditor = createTestEditor({
+ extensions: [
+ Blockquote,
+ Bold,
+ BulletList,
+ Code,
+ CodeBlockHighlight,
+ HardBreak,
+ Heading,
+ HorizontalRule,
+ Image,
+ Italic,
+ Link,
+ ListItem,
+ OrderedList,
+ Sourcemap,
+ ],
+});
+
+describe('Client side Markdown processing', () => {
+ const deserialize = async (content) => {
+ const { document } = await remarkMarkdownDeserializer().deserialize({
+ schema: tiptapEditor.schema,
+ content,
+ });
+
+ return document;
+ };
+
+ const serialize = (document) =>
+ markdownSerializer({}).serialize({
+ doc: document,
+ pristineDoc: document,
+ });
+
+ it.each([
+ {
+ markdown: '__bold text__',
+ },
+ {
+ markdown: '**bold text**',
+ },
+ {
+ markdown: '<strong>bold text</strong>',
+ },
+ {
+ markdown: '<b>bold text</b>',
+ },
+ {
+ markdown: '_italic text_',
+ },
+ {
+ markdown: '*italic text*',
+ },
+ {
+ markdown: '<em>italic text</em>',
+ },
+ {
+ markdown: '<i>italic text</i>',
+ },
+ {
+ markdown: '`inline code`',
+ },
+ {
+ markdown: '**`inline code bold`**',
+ },
+ {
+ markdown: '__`inline code italics`__',
+ },
+ {
+ markdown: '[GitLab](https://gitlab.com "Go to GitLab")',
+ },
+ {
+ markdown: '**[GitLab](https://gitlab.com "Go to GitLab")**',
+ },
+ {
+ markdown: `
+This is a paragraph with a\\
+hard line break`,
+ },
+ {
+ markdown: '![GitLab Logo](https://gitlab.com/logo.png "GitLab Logo")',
+ },
+ {
+ markdown: '---',
+ },
+ {
+ markdown: '***',
+ },
+ {
+ markdown: '___',
+ },
+ {
+ markdown: '<hr>',
+ },
+ {
+ markdown: '# Heading 1',
+ },
+ {
+ markdown: '## Heading 2',
+ },
+ {
+ markdown: '### Heading 3',
+ },
+ {
+ markdown: '#### Heading 4',
+ },
+ {
+ markdown: '##### Heading 5',
+ },
+ {
+ markdown: '###### Heading 6',
+ },
+
+ {
+ markdown: `
+ Heading
+ one
+ ======
+ `,
+ },
+ {
+ markdown: `
+ Heading
+ two
+ -------
+ `,
+ },
+ {
+ markdown: `
+ - List item 1
+ - List item 2
+ `,
+ },
+ {
+ markdown: `
+ * List item 1
+ * List item 2
+ `,
+ },
+ {
+ markdown: `
+ + List item 1
+ + List item 2
+ `,
+ },
+ {
+ markdown: `
+ 1. List item 1
+ 1. List item 2
+ `,
+ },
+ {
+ markdown: `
+ 1. List item 1
+ 2. List item 2
+ `,
+ },
+ {
+ markdown: `
+ 1) List item 1
+ 2) List item 2
+ `,
+ },
+ {
+ markdown: `
+ - List item 1
+ - Sub list item 1
+ `,
+ },
+ {
+ markdown: `
+ - List item 1 paragraph 1
+
+ List item 1 paragraph 2
+ - List item 2
+ `,
+ },
+ {
+ markdown: `
+ > This is a blockquote
+ `,
+ },
+ {
+ markdown: `
+ > - List item 1
+ > - List item 2
+ `,
+ },
+ {
+ markdown: `
+ const fn = () => 'GitLab';
+ `,
+ },
+ {
+ markdown: `
+ \`\`\`javascript
+ const fn = () => 'GitLab';
+ \`\`\`\
+ `,
+ },
+ {
+ markdown: `
+ ~~~javascript
+ const fn = () => 'GitLab';
+ ~~~
+ `,
+ },
+ {
+ markdown: `
+ \`\`\`
+ \`\`\`\
+ `,
+ },
+ {
+ markdown: `
+ \`\`\`javascript
+ const fn = () => 'GitLab';
+
+ \`\`\`\
+ `,
+ },
+ ])('processes %s correctly', async ({ markdown }) => {
+ const trimmed = markdown.trim();
+ const document = await deserialize(trimmed);
+
+ expect(serialize(document)).toEqual(trimmed);
+ });
+});
diff --git a/spec/frontend/content_editor/services/asset_resolver_spec.js b/spec/frontend/content_editor/services/asset_resolver_spec.js
new file mode 100644
index 00000000000..f4e7d9bf881
--- /dev/null
+++ b/spec/frontend/content_editor/services/asset_resolver_spec.js
@@ -0,0 +1,23 @@
+import createAssetResolver from '~/content_editor/services/asset_resolver';
+
+describe('content_editor/services/asset_resolver', () => {
+ let renderMarkdown;
+ let assetResolver;
+
+ beforeEach(() => {
+ renderMarkdown = jest.fn();
+ assetResolver = createAssetResolver({ renderMarkdown });
+ });
+
+ describe('resolveUrl', () => {
+ it('resolves a canonical url to an absolute url', async () => {
+ renderMarkdown.mockResolvedValue(
+ '<p><a href="/group1/project1/-/wikis/test-file.png" data-canonical-src="test-file.png">link</a></p>',
+ );
+
+ expect(await assetResolver.resolveUrl('test-file.png')).toBe(
+ '/group1/project1/-/wikis/test-file.png',
+ );
+ });
+ });
+});
diff --git a/spec/frontend/content_editor/services/code_block_language_loader_spec.js b/spec/frontend/content_editor/services/code_block_language_loader_spec.js
index 905c1685b94..943de327762 100644
--- a/spec/frontend/content_editor/services/code_block_language_loader_spec.js
+++ b/spec/frontend/content_editor/services/code_block_language_loader_spec.js
@@ -53,47 +53,25 @@ describe('content_editor/services/code_block_language_loader', () => {
});
});
- describe('loadLanguages', () => {
+ describe('loadLanguage', () => {
it('loads highlight.js language packages identified by a list of languages', async () => {
- const languages = ['javascript', 'ruby'];
+ const language = 'javascript';
- await languageLoader.loadLanguages(languages);
+ await languageLoader.loadLanguage(language);
- languages.forEach((language) => {
- expect(lowlight.registerLanguage).toHaveBeenCalledWith(language, expect.any(Function));
- });
+ expect(lowlight.registerLanguage).toHaveBeenCalledWith(language, expect.any(Function));
});
describe('when language is already registered', () => {
it('does not load the language again', async () => {
- const languages = ['javascript'];
-
- await languageLoader.loadLanguages(languages);
- await languageLoader.loadLanguages(languages);
+ await languageLoader.loadLanguage('javascript');
+ await languageLoader.loadLanguage('javascript');
expect(lowlight.registerLanguage).toHaveBeenCalledTimes(1);
});
});
});
- describe('loadLanguagesFromDOM', () => {
- it('loads highlight.js language packages identified by pre tags in a DOM fragment', async () => {
- const parser = new DOMParser();
- const { body } = parser.parseFromString(
- `
- <pre lang="javascript"></pre>
- <pre lang="ruby"></pre>
- `,
- 'text/html',
- );
-
- await languageLoader.loadLanguagesFromDOM(body);
-
- expect(lowlight.registerLanguage).toHaveBeenCalledWith('javascript', expect.any(Function));
- expect(lowlight.registerLanguage).toHaveBeenCalledWith('ruby', expect.any(Function));
- });
- });
-
describe('loadLanguageFromInputRule', () => {
it('loads highlight.js language packages identified from the input rule', async () => {
const match = new RegExp(backtickInputRegex).exec('```js ');
@@ -112,7 +90,7 @@ describe('content_editor/services/code_block_language_loader', () => {
expect(languageLoader.isLanguageLoaded(language)).toBe(false);
- await languageLoader.loadLanguages([language]);
+ await languageLoader.loadLanguage(language);
expect(languageLoader.isLanguageLoaded(language)).toBe(true);
});
diff --git a/spec/frontend/content_editor/services/content_editor_spec.js b/spec/frontend/content_editor/services/content_editor_spec.js
index 5b7a27b501d..a3553e612ca 100644
--- a/spec/frontend/content_editor/services/content_editor_spec.js
+++ b/spec/frontend/content_editor/services/content_editor_spec.js
@@ -11,7 +11,6 @@ describe('content_editor/services/content_editor', () => {
let contentEditor;
let serializer;
let deserializer;
- let languageLoader;
let eventHub;
let doc;
let p;
@@ -26,16 +25,14 @@ describe('content_editor/services/content_editor', () => {
tiptapEditor,
}));
- serializer = { deserialize: jest.fn() };
+ serializer = { serialize: jest.fn() };
deserializer = { deserialize: jest.fn() };
- languageLoader = { loadLanguagesFromDOM: jest.fn() };
eventHub = eventHubFactory();
contentEditor = new ContentEditor({
tiptapEditor,
serializer,
deserializer,
eventHub,
- languageLoader,
});
});
@@ -51,12 +48,12 @@ describe('content_editor/services/content_editor', () => {
describe('when setSerializedContent succeeds', () => {
let document;
- const dom = {};
+ const languages = ['javascript'];
const testMarkdown = '**bold text**';
beforeEach(() => {
document = doc(p('document'));
- deserializer.deserialize.mockResolvedValueOnce({ document, dom });
+ deserializer.deserialize.mockResolvedValueOnce({ document, languages });
});
it('emits loadingContent and loadingSuccess event in the eventHub', () => {
@@ -77,12 +74,6 @@ describe('content_editor/services/content_editor', () => {
expect(contentEditor.tiptapEditor.state.doc.toJSON()).toEqual(document.toJSON());
});
-
- it('passes deserialized DOM document to language loader', async () => {
- await contentEditor.setSerializedContent(testMarkdown);
-
- expect(languageLoader.loadLanguagesFromDOM).toHaveBeenCalledWith(dom);
- });
});
describe('when setSerializedContent fails', () => {
diff --git a/spec/frontend/content_editor/services/create_content_editor_spec.js b/spec/frontend/content_editor/services/create_content_editor_spec.js
index 6b2f28b3306..e1a30819ac8 100644
--- a/spec/frontend/content_editor/services/create_content_editor_spec.js
+++ b/spec/frontend/content_editor/services/create_content_editor_spec.js
@@ -1,8 +1,12 @@
import { PROVIDE_SERIALIZER_OR_RENDERER_ERROR } from '~/content_editor/constants';
import { createContentEditor } from '~/content_editor/services/create_content_editor';
+import createGlApiMarkdownDeserializer from '~/content_editor/services/gl_api_markdown_deserializer';
+import createRemarkMarkdownDeserializer from '~/content_editor/services/remark_markdown_deserializer';
import { createTestContentEditorExtension } from '../test_utils';
jest.mock('~/emoji');
+jest.mock('~/content_editor/services/remark_markdown_deserializer');
+jest.mock('~/content_editor/services/gl_api_markdown_deserializer');
describe('content_editor/services/create_content_editor', () => {
let renderMarkdown;
@@ -11,9 +15,36 @@ describe('content_editor/services/create_content_editor', () => {
beforeEach(() => {
renderMarkdown = jest.fn();
+ window.gon = {
+ features: {
+ preserveUnchangedMarkdown: false,
+ },
+ };
editor = createContentEditor({ renderMarkdown, uploadsPath });
});
+ describe('when preserveUnchangedMarkdown feature is on', () => {
+ beforeEach(() => {
+ window.gon.features.preserveUnchangedMarkdown = true;
+ });
+
+ it('provides a remark markdown deserializer to the content editor class', () => {
+ createContentEditor({ renderMarkdown, uploadsPath });
+ expect(createRemarkMarkdownDeserializer).toHaveBeenCalled();
+ });
+ });
+
+ describe('when preserveUnchangedMarkdown feature is off', () => {
+ beforeEach(() => {
+ window.gon.features.preserveUnchangedMarkdown = false;
+ });
+
+ it('provides a gl api markdown deserializer to the content editor class', () => {
+ createContentEditor({ renderMarkdown, uploadsPath });
+ expect(createGlApiMarkdownDeserializer).toHaveBeenCalledWith({ render: renderMarkdown });
+ });
+ });
+
it('sets gl-outline-0! class selector to the tiptapEditor instance', () => {
expect(editor.tiptapEditor.options.editorProps).toMatchObject({
attributes: {
@@ -22,30 +53,19 @@ describe('content_editor/services/create_content_editor', () => {
});
});
- it('provides the renderMarkdown function to the markdown serializer', async () => {
- const serializedContent = '**bold text**';
-
- renderMarkdown.mockReturnValueOnce('<p><b>bold text</b></p>');
-
- await editor.setSerializedContent(serializedContent);
-
- expect(renderMarkdown).toHaveBeenCalledWith(serializedContent);
- });
-
it('allows providing external content editor extensions', async () => {
const labelReference = 'this is a ~group::editor';
const { tiptapExtension, serializer } = createTestContentEditorExtension();
- renderMarkdown.mockReturnValueOnce(
- '<p>this is a <span data-reference="label" data-label-name="group::editor">group::editor</span></p>',
- );
editor = createContentEditor({
renderMarkdown,
extensions: [tiptapExtension],
serializerConfig: { nodes: { [tiptapExtension.name]: serializer } },
});
- await editor.setSerializedContent(labelReference);
+ editor.tiptapEditor.commands.setContent(
+ '<p>this is a <span data-reference="label" data-label-name="group::editor">group::editor</span></p>',
+ );
expect(editor.getSerializedContent()).toBe(labelReference);
});
diff --git a/spec/frontend/content_editor/services/markdown_deserializer_spec.js b/spec/frontend/content_editor/services/gl_api_markdown_deserializer_spec.js
index bea43a0effc..5458a42532f 100644
--- a/spec/frontend/content_editor/services/markdown_deserializer_spec.js
+++ b/spec/frontend/content_editor/services/gl_api_markdown_deserializer_spec.js
@@ -1,8 +1,8 @@
-import createMarkdownDeserializer from '~/content_editor/services/markdown_deserializer';
+import createMarkdownDeserializer from '~/content_editor/services/gl_api_markdown_deserializer';
import Bold from '~/content_editor/extensions/bold';
import { createTestEditor, createDocBuilder } from '../test_utils';
-describe('content_editor/services/markdown_deserializer', () => {
+describe('content_editor/services/gl_api_markdown_deserializer', () => {
let renderMarkdown;
let doc;
let p;
@@ -32,7 +32,9 @@ describe('content_editor/services/markdown_deserializer', () => {
beforeEach(async () => {
const deserializer = createMarkdownDeserializer({ render: renderMarkdown });
- renderMarkdown.mockResolvedValueOnce(`<p><strong>${text}</strong></p>`);
+ renderMarkdown.mockResolvedValueOnce(
+ `<p><strong>${text}</strong></p><pre lang="javascript"></pre>`,
+ );
result = await deserializer.deserialize({
content: 'content',
@@ -40,13 +42,9 @@ describe('content_editor/services/markdown_deserializer', () => {
});
});
it('transforms HTML returned by render function to a ProseMirror document', async () => {
- const expectedDoc = doc(p(bold(text)));
+ const document = doc(p(bold(text)));
- expect(result.document.toJSON()).toEqual(expectedDoc.toJSON());
- });
-
- it('returns parsed HTML as a DOM object', () => {
- expect(result.dom.innerHTML).toEqual(`<p><strong>${text}</strong></p><!--content-->`);
+ expect(result.document.toJSON()).toEqual(document.toJSON());
});
});
diff --git a/spec/frontend/content_editor/services/markdown_serializer_spec.js b/spec/frontend/content_editor/services/markdown_serializer_spec.js
index 2b76dc6c984..25b7483f234 100644
--- a/spec/frontend/content_editor/services/markdown_serializer_spec.js
+++ b/spec/frontend/content_editor/services/markdown_serializer_spec.js
@@ -24,6 +24,7 @@ import Link from '~/content_editor/extensions/link';
import ListItem from '~/content_editor/extensions/list_item';
import OrderedList from '~/content_editor/extensions/ordered_list';
import Paragraph from '~/content_editor/extensions/paragraph';
+import Sourcemap from '~/content_editor/extensions/sourcemap';
import Strike from '~/content_editor/extensions/strike';
import Table from '~/content_editor/extensions/table';
import TableCell from '~/content_editor/extensions/table_cell';
@@ -32,6 +33,7 @@ import TableRow from '~/content_editor/extensions/table_row';
import TaskItem from '~/content_editor/extensions/task_item';
import TaskList from '~/content_editor/extensions/task_list';
import markdownSerializer from '~/content_editor/services/markdown_serializer';
+import remarkMarkdownDeserializer from '~/content_editor/services/remark_markdown_deserializer';
import { createTestEditor, createDocBuilder } from '../test_utils';
jest.mock('~/emoji');
@@ -63,6 +65,7 @@ const tiptapEditor = createTestEditor({
Link,
ListItem,
OrderedList,
+ Sourcemap,
Strike,
Table,
TableCell,
@@ -151,8 +154,7 @@ const {
const serialize = (...content) =>
markdownSerializer({}).serialize({
- schema: tiptapEditor.schema,
- content: doc(...content).toJSON(),
+ doc: doc(...content),
});
describe('markdownSerializer', () => {
@@ -1159,4 +1161,42 @@ Oranges are orange [^1]
`.trim(),
);
});
+
+ it.each`
+ mark | content | modifiedContent
+ ${'bold'} | ${'**bold**'} | ${'**bold modified**'}
+ ${'bold'} | ${'__bold__'} | ${'__bold modified__'}
+ ${'bold'} | ${'<strong>bold</strong>'} | ${'<strong>bold modified</strong>'}
+ ${'bold'} | ${'<b>bold</b>'} | ${'<b>bold modified</b>'}
+ ${'italic'} | ${'_italic_'} | ${'_italic modified_'}
+ ${'italic'} | ${'*italic*'} | ${'*italic modified*'}
+ ${'italic'} | ${'<em>italic</em>'} | ${'<em>italic modified</em>'}
+ ${'italic'} | ${'<i>italic</i>'} | ${'<i>italic modified</i>'}
+ ${'link'} | ${'[gitlab](https://gitlab.com)'} | ${'[gitlab modified](https://gitlab.com)'}
+ ${'link'} | ${'<a href="https://gitlab.com">link</a>'} | ${'<a href="https://gitlab.com">link modified</a>'}
+ ${'code'} | ${'`code`'} | ${'`code modified`'}
+ ${'code'} | ${'<code>code</code>'} | ${'<code>code modified</code>'}
+ `(
+ 'preserves original $mark syntax when sourceMarkdown is available',
+ async ({ content, modifiedContent }) => {
+ const { document } = await remarkMarkdownDeserializer().deserialize({
+ schema: tiptapEditor.schema,
+ content,
+ });
+
+ tiptapEditor
+ .chain()
+ .setContent(document.toJSON())
+ // changing the document ensures that block preservation doesn’t yield false positives
+ .insertContent(' modified')
+ .run();
+
+ const serialized = markdownSerializer({}).serialize({
+ pristineDoc: document,
+ doc: tiptapEditor.state.doc,
+ });
+
+ expect(serialized).toEqual(modifiedContent);
+ },
+ );
});
diff --git a/spec/frontend/content_editor/services/markdown_sourcemap_spec.js b/spec/frontend/content_editor/services/markdown_sourcemap_spec.js
index abd9588daff..8a304c73163 100644
--- a/spec/frontend/content_editor/services/markdown_sourcemap_spec.js
+++ b/spec/frontend/content_editor/services/markdown_sourcemap_spec.js
@@ -2,7 +2,7 @@ import { Extension } from '@tiptap/core';
import BulletList from '~/content_editor/extensions/bullet_list';
import ListItem from '~/content_editor/extensions/list_item';
import Paragraph from '~/content_editor/extensions/paragraph';
-import markdownDeserializer from '~/content_editor/services/markdown_deserializer';
+import markdownDeserializer from '~/content_editor/services/gl_api_markdown_deserializer';
import { getMarkdownSource, getFullSource } from '~/content_editor/services/markdown_sourcemap';
import { createTestEditor, createDocBuilder } from '../test_utils';
diff --git a/spec/frontend/content_editor/test_constants.js b/spec/frontend/content_editor/test_constants.js
new file mode 100644
index 00000000000..45a0e4a8bd1
--- /dev/null
+++ b/spec/frontend/content_editor/test_constants.js
@@ -0,0 +1,25 @@
+export const PROJECT_WIKI_ATTACHMENT_IMAGE_HTML = `<p data-sourcepos="1:1-1:27" dir="auto">
+ <a class="no-attachment-icon" href="/group1/project1/-/wikis/test-file.png" target="_blank" rel="noopener noreferrer" data-canonical-src="test-file.png">
+ <img alt="test-file" class="lazy" data-src="/group1/project1/-/wikis/test-file.png" data-canonical-src="test-file.png">
+ </a>
+</p>`;
+
+export const PROJECT_WIKI_ATTACHMENT_VIDEO_HTML = `<p data-sourcepos="1:1-1:132" dir="auto">
+ <span class="media-container video-container">
+ <video src="/group1/project1/-/wikis/test-file.mp4" controls="true" data-setup="{}" data-title="test-file" width="400" preload="metadata" data-canonical-src="test-file.mp4">
+ </video>
+ <a href="/group1/project1/-/wikis/test-file.mp4" target="_blank" rel="noopener noreferrer" title="Download 'test-file'" data-canonical-src="test-file.mp4">test-file</a>
+ </span>
+</p>`;
+
+export const PROJECT_WIKI_ATTACHMENT_AUDIO_HTML = `<p data-sourcepos="3:1-3:74" dir="auto">
+ <span class="media-container audio-container">
+ <audio src="/group1/project1/-/wikis/test-file.mp3" controls="true" data-setup="{}" data-title="test-file" data-canonical-src="test-file.mp3">
+ </audio>
+ <a href="/group1/project1/-/wikis/test-file.mp3" target="_blank" rel="noopener noreferrer" title="Download 'test-file'" data-canonical-src="test-file.mp3">test-file</a>
+ </span>
+</p>`;
+
+export const PROJECT_WIKI_ATTACHMENT_LINK_HTML = `<p data-sourcepos="1:1-1:26" dir="auto">
+ <a href="/group1/project1/-/wikis/test-file.zip" data-canonical-src="test-file.zip">test-file</a>
+</p>`;
diff --git a/spec/frontend/content_editor/test_utils.js b/spec/frontend/content_editor/test_utils.js
index dde9d738235..4ed1ed97cbd 100644
--- a/spec/frontend/content_editor/test_utils.js
+++ b/spec/frontend/content_editor/test_utils.js
@@ -151,7 +151,7 @@ export const triggerMarkInputRule = ({ tiptapEditor, inputRuleText }) => {
* @param {*} params.action A function that triggers a transaction in the tiptap Editor
* @returns A promise that resolves when the transaction completes
*/
-export const waitUntilNextDocTransaction = ({ tiptapEditor, action }) => {
+export const waitUntilNextDocTransaction = ({ tiptapEditor, action = () => {} }) => {
return new Promise((resolve) => {
const handleTransaction = () => {
tiptapEditor.off('update', handleTransaction);
diff --git a/spec/frontend/create_cluster/components/cluster_form_dropdown_spec.js b/spec/frontend/create_cluster/components/cluster_form_dropdown_spec.js
deleted file mode 100644
index 2f835867f5f..00000000000
--- a/spec/frontend/create_cluster/components/cluster_form_dropdown_spec.js
+++ /dev/null
@@ -1,214 +0,0 @@
-import { GlIcon } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
-import $ from 'jquery';
-
-import { nextTick } from 'vue';
-import ClusterFormDropdown from '~/create_cluster/components/cluster_form_dropdown.vue';
-import DropdownButton from '~/vue_shared/components/dropdown/dropdown_button.vue';
-import DropdownSearchInput from '~/vue_shared/components/dropdown/dropdown_search_input.vue';
-
-describe('ClusterFormDropdown', () => {
- let wrapper;
- const firstItem = { name: 'item 1', value: '1' };
- const secondItem = { name: 'item 2', value: '2' };
- const items = [firstItem, secondItem, { name: 'item 3', value: '3' }];
-
- beforeEach(() => {
- wrapper = shallowMount(ClusterFormDropdown);
- });
- afterEach(() => wrapper.destroy());
-
- describe('when initial value is provided', () => {
- it('sets selectedItem to initial value', async () => {
- wrapper.setProps({ items, value: secondItem.value });
-
- await nextTick();
- expect(wrapper.find(DropdownButton).props('toggleText')).toEqual(secondItem.name);
- });
- });
-
- describe('when no item is selected', () => {
- it('displays placeholder text', async () => {
- const placeholder = 'placeholder';
-
- wrapper.setProps({ placeholder });
-
- await nextTick();
- expect(wrapper.find(DropdownButton).props('toggleText')).toEqual(placeholder);
- });
- });
-
- describe('when an item is selected', () => {
- beforeEach(async () => {
- wrapper.setProps({ items });
- await nextTick();
- wrapper.findAll('.js-dropdown-item').at(1).trigger('click');
- await nextTick();
- });
-
- it('emits input event with selected item', () => {
- expect(wrapper.emitted('input')[0]).toEqual([secondItem.value]);
- });
- });
-
- describe('when multiple items are selected', () => {
- const value = ['1'];
-
- beforeEach(async () => {
- wrapper.setProps({ items, multiple: true, value });
-
- await nextTick();
- wrapper.findAll('.js-dropdown-item').at(0).trigger('click');
-
- await nextTick();
- wrapper.findAll('.js-dropdown-item').at(1).trigger('click');
-
- await nextTick();
- });
-
- it('emits input event with an array of selected items', () => {
- expect(wrapper.emitted('input')[1]).toEqual([[firstItem.value, secondItem.value]]);
- });
- });
-
- describe('when multiple items can be selected', () => {
- beforeEach(async () => {
- wrapper.setProps({ items, multiple: true, value: firstItem.value });
- await nextTick();
- });
-
- it('displays a checked GlIcon next to the item', () => {
- expect(wrapper.find(GlIcon).classes()).not.toContain('invisible');
- expect(wrapper.find(GlIcon).props('name')).toBe('mobile-issue-close');
- });
- });
-
- describe('when multiple values can be selected and initial value is null', () => {
- it('emits input event with an array of a single selected item', async () => {
- wrapper.setProps({ items, multiple: true, value: null });
-
- await nextTick();
- wrapper.findAll('.js-dropdown-item').at(0).trigger('click');
-
- expect(wrapper.emitted('input')[0]).toEqual([[firstItem.value]]);
- });
- });
-
- describe('when an item is selected and has a custom label property', () => {
- it('displays selected item custom label', async () => {
- const labelProperty = 'customLabel';
- const label = 'Name';
- const currentValue = '1';
- const customLabelItems = [{ [labelProperty]: label, value: currentValue }];
-
- wrapper.setProps({ labelProperty, items: customLabelItems, value: currentValue });
-
- await nextTick();
- expect(wrapper.find(DropdownButton).props('toggleText')).toEqual(label);
- });
- });
-
- describe('when loading', () => {
- it('dropdown button isLoading', async () => {
- await wrapper.setProps({ loading: true });
- expect(wrapper.find(DropdownButton).props('isLoading')).toBe(true);
- });
- });
-
- describe('when loading and loadingText is provided', () => {
- it('uses loading text as toggle button text', async () => {
- const loadingText = 'loading text';
-
- wrapper.setProps({ loading: true, loadingText });
-
- await nextTick();
- expect(wrapper.find(DropdownButton).props('toggleText')).toEqual(loadingText);
- });
- });
-
- describe('when disabled', () => {
- it('dropdown button isDisabled', async () => {
- wrapper.setProps({ disabled: true });
-
- await nextTick();
- expect(wrapper.find(DropdownButton).props('isDisabled')).toBe(true);
- });
- });
-
- describe('when disabled and disabledText is provided', () => {
- it('uses disabled text as toggle button text', async () => {
- const disabledText = 'disabled text';
-
- wrapper.setProps({ disabled: true, disabledText });
-
- await nextTick();
- expect(wrapper.find(DropdownButton).props('toggleText')).toBe(disabledText);
- });
- });
-
- describe('when has errors', () => {
- it('sets border-danger class selector to dropdown toggle', async () => {
- wrapper.setProps({ hasErrors: true });
-
- await nextTick();
- expect(wrapper.find(DropdownButton).classes('border-danger')).toBe(true);
- });
- });
-
- describe('when has errors and an error message', () => {
- it('displays error message', async () => {
- const errorMessage = 'error message';
-
- wrapper.setProps({ hasErrors: true, errorMessage });
-
- await nextTick();
- expect(wrapper.find('.js-eks-dropdown-error-message').text()).toEqual(errorMessage);
- });
- });
-
- describe('when no results are available', () => {
- it('displays empty text', async () => {
- const emptyText = 'error message';
-
- wrapper.setProps({ items: [], emptyText });
-
- await nextTick();
- expect(wrapper.find('.js-empty-text').text()).toEqual(emptyText);
- });
- });
-
- it('displays search field placeholder', async () => {
- const searchFieldPlaceholder = 'Placeholder';
-
- wrapper.setProps({ searchFieldPlaceholder });
-
- await nextTick();
- expect(wrapper.find(DropdownSearchInput).props('placeholderText')).toEqual(
- searchFieldPlaceholder,
- );
- });
-
- it('it filters results by search query', async () => {
- 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 });
-
- await nextTick();
- expect(wrapper.findAll('.js-dropdown-item').length).toEqual(1);
- expect(wrapper.find('.js-dropdown-item').text()).toEqual(secondItem.name);
- });
-
- it('focuses dropdown search input when dropdown is displayed', async () => {
- const dropdownEl = wrapper.find('.dropdown').element;
-
- expect(wrapper.find(DropdownSearchInput).props('focused')).toBe(false);
-
- $(dropdownEl).trigger('shown.bs.dropdown');
-
- await nextTick();
- expect(wrapper.find(DropdownSearchInput).props('focused')).toBe(true);
- });
-});
diff --git a/spec/frontend/create_cluster/eks_cluster/components/create_eks_cluster_spec.js b/spec/frontend/create_cluster/eks_cluster/components/create_eks_cluster_spec.js
deleted file mode 100644
index c8020cf8308..00000000000
--- a/spec/frontend/create_cluster/eks_cluster/components/create_eks_cluster_spec.js
+++ /dev/null
@@ -1,98 +0,0 @@
-import { shallowMount } from '@vue/test-utils';
-import Vue from 'vue';
-import Vuex from 'vuex';
-
-import CreateEksCluster from '~/create_cluster/eks_cluster/components/create_eks_cluster.vue';
-import EksClusterConfigurationForm from '~/create_cluster/eks_cluster/components/eks_cluster_configuration_form.vue';
-import ServiceCredentialsForm from '~/create_cluster/eks_cluster/components/service_credentials_form.vue';
-
-Vue.use(Vuex);
-
-describe('CreateEksCluster', () => {
- let vm;
- let state;
- const gitlabManagedClusterHelpPath = 'gitlab-managed-cluster-help-path';
- const namespacePerEnvironmentHelpPath = 'namespace-per-environment-help-path';
- const accountAndExternalIdsHelpPath = 'account-and-external-id-help-path';
- const createRoleArnHelpPath = 'role-arn-help-path';
- const kubernetesIntegrationHelpPath = 'kubernetes-integration';
- const externalLinkIcon = 'external-link';
-
- beforeEach(() => {
- state = { hasCredentials: false };
- const store = new Vuex.Store({
- state,
- });
-
- vm = shallowMount(CreateEksCluster, {
- propsData: {
- gitlabManagedClusterHelpPath,
- namespacePerEnvironmentHelpPath,
- accountAndExternalIdsHelpPath,
- createRoleArnHelpPath,
- externalLinkIcon,
- kubernetesIntegrationHelpPath,
- },
- store,
- });
- });
- afterEach(() => vm.destroy());
-
- describe('when credentials are provided', () => {
- beforeEach(() => {
- state.hasCredentials = true;
- });
-
- it('displays eks cluster configuration form when credentials are valid', () => {
- expect(vm.find(EksClusterConfigurationForm).exists()).toBe(true);
- });
-
- describe('passes to the cluster configuration form', () => {
- it('help url for kubernetes integration documentation', () => {
- expect(vm.find(EksClusterConfigurationForm).props('gitlabManagedClusterHelpPath')).toBe(
- gitlabManagedClusterHelpPath,
- );
- });
-
- it('help url for namespace per environment cluster documentation', () => {
- expect(vm.find(EksClusterConfigurationForm).props('namespacePerEnvironmentHelpPath')).toBe(
- namespacePerEnvironmentHelpPath,
- );
- });
-
- it('help url for gitlab managed cluster documentation', () => {
- expect(vm.find(EksClusterConfigurationForm).props('kubernetesIntegrationHelpPath')).toBe(
- kubernetesIntegrationHelpPath,
- );
- });
- });
- });
-
- describe('when credentials are invalid', () => {
- beforeEach(() => {
- state.hasCredentials = false;
- });
-
- it('displays service credentials form', () => {
- expect(vm.find(ServiceCredentialsForm).exists()).toBe(true);
- });
-
- describe('passes to the service credentials form', () => {
- it('help url for account and external ids', () => {
- expect(vm.find(ServiceCredentialsForm).props('accountAndExternalIdsHelpPath')).toBe(
- accountAndExternalIdsHelpPath,
- );
- });
-
- it('external link icon', () => {
- expect(vm.find(ServiceCredentialsForm).props('externalLinkIcon')).toBe(externalLinkIcon);
- });
-
- it('help url to create a role ARN', () => {
- expect(vm.find(ServiceCredentialsForm).props('createRoleArnHelpPath')).toBe(
- createRoleArnHelpPath,
- );
- });
- });
- });
-});
diff --git a/spec/frontend/create_cluster/eks_cluster/components/eks_cluster_configuration_form_spec.js b/spec/frontend/create_cluster/eks_cluster/components/eks_cluster_configuration_form_spec.js
deleted file mode 100644
index 1509d26c99d..00000000000
--- a/spec/frontend/create_cluster/eks_cluster/components/eks_cluster_configuration_form_spec.js
+++ /dev/null
@@ -1,562 +0,0 @@
-import { GlFormCheckbox } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
-import Vue, { nextTick } from 'vue';
-import Vuex from 'vuex';
-
-import EksClusterConfigurationForm from '~/create_cluster/eks_cluster/components/eks_cluster_configuration_form.vue';
-import eksClusterFormState from '~/create_cluster/eks_cluster/store/state';
-import clusterDropdownStoreState from '~/create_cluster/store/cluster_dropdown/state';
-
-Vue.use(Vuex);
-
-describe('EksClusterConfigurationForm', () => {
- let store;
- let actions;
- let getters;
- let state;
- let rolesState;
- let vpcsState;
- let subnetsState;
- let keyPairsState;
- let securityGroupsState;
- let instanceTypesState;
- let vpcsActions;
- let rolesActions;
- let subnetsActions;
- let keyPairsActions;
- let securityGroupsActions;
- let vm;
-
- const createStore = (config = {}) => {
- actions = {
- createCluster: jest.fn(),
- setClusterName: jest.fn(),
- setEnvironmentScope: jest.fn(),
- setKubernetesVersion: jest.fn(),
- setRegion: jest.fn(),
- setVpc: jest.fn(),
- setSubnet: jest.fn(),
- setRole: jest.fn(),
- setKeyPair: jest.fn(),
- setSecurityGroup: jest.fn(),
- setInstanceType: jest.fn(),
- setNodeCount: jest.fn(),
- setGitlabManagedCluster: jest.fn(),
- };
- keyPairsActions = {
- fetchItems: jest.fn(),
- };
- vpcsActions = {
- fetchItems: jest.fn(),
- };
- subnetsActions = {
- fetchItems: jest.fn(),
- };
- rolesActions = {
- fetchItems: jest.fn(),
- };
- securityGroupsActions = {
- fetchItems: jest.fn(),
- };
- state = {
- ...eksClusterFormState(),
- ...config.initialState,
- };
- rolesState = {
- ...clusterDropdownStoreState(),
- ...config.rolesState,
- };
- vpcsState = {
- ...clusterDropdownStoreState(),
- ...config.vpcsState,
- };
- subnetsState = {
- ...clusterDropdownStoreState(),
- ...config.subnetsState,
- };
- keyPairsState = {
- ...clusterDropdownStoreState(),
- ...config.keyPairsState,
- };
- securityGroupsState = {
- ...clusterDropdownStoreState(),
- ...config.securityGroupsState,
- };
- instanceTypesState = {
- ...clusterDropdownStoreState(),
- ...config.instanceTypesState,
- };
- getters = {
- subnetValid: config?.getters?.subnetValid || (() => false),
- };
- store = new Vuex.Store({
- state,
- getters,
- actions,
- modules: {
- vpcs: {
- namespaced: true,
- state: vpcsState,
- actions: vpcsActions,
- },
- subnets: {
- namespaced: true,
- state: subnetsState,
- actions: subnetsActions,
- },
- roles: {
- namespaced: true,
- state: rolesState,
- actions: rolesActions,
- },
- keyPairs: {
- namespaced: true,
- state: keyPairsState,
- actions: keyPairsActions,
- },
- securityGroups: {
- namespaced: true,
- state: securityGroupsState,
- actions: securityGroupsActions,
- },
- instanceTypes: {
- namespaced: true,
- state: instanceTypesState,
- },
- },
- });
- };
-
- const createValidStateStore = (initialState) => {
- createStore({
- initialState: {
- clusterName: 'cluster name',
- environmentScope: '*',
- kubernetesVersion: '1.16',
- selectedRegion: 'region',
- selectedRole: 'role',
- selectedKeyPair: 'key pair',
- selectedVpc: 'vpc',
- selectedSubnet: ['subnet 1', 'subnet 2'],
- selectedSecurityGroup: 'group',
- selectedInstanceType: 'small-1',
- ...initialState,
- },
- getters: {
- subnetValid: () => true,
- },
- });
- };
-
- const buildWrapper = () => {
- vm = shallowMount(EksClusterConfigurationForm, {
- store,
- propsData: {
- gitlabManagedClusterHelpPath: '',
- namespacePerEnvironmentHelpPath: '',
- kubernetesIntegrationHelpPath: '',
- externalLinkIcon: '',
- },
- });
- };
-
- beforeEach(() => {
- createStore();
- buildWrapper();
- });
-
- afterEach(() => {
- vm.destroy();
- });
-
- const findCreateClusterButton = () => vm.find('.js-create-cluster');
- const findClusterNameInput = () => vm.find('[id=eks-cluster-name]');
- const findEnvironmentScopeInput = () => vm.find('[id=eks-environment-scope]');
- const findKubernetesVersionDropdown = () => vm.find('[field-id="eks-kubernetes-version"]');
- const findKeyPairDropdown = () => vm.find('[field-id="eks-key-pair"]');
- const findVpcDropdown = () => vm.find('[field-id="eks-vpc"]');
- const findSubnetDropdown = () => vm.find('[field-id="eks-subnet"]');
- const findRoleDropdown = () => vm.find('[field-id="eks-role"]');
- const findSecurityGroupDropdown = () => vm.find('[field-id="eks-security-group"]');
- const findInstanceTypeDropdown = () => vm.find('[field-id="eks-instance-type"');
- const findNodeCountInput = () => vm.find('[id="eks-node-count"]');
- const findGitlabManagedClusterCheckbox = () => vm.find(GlFormCheckbox);
-
- describe('when mounted', () => {
- it('fetches available roles', () => {
- expect(rolesActions.fetchItems).toHaveBeenCalled();
- });
-
- describe('when fetching vpcs and key pairs', () => {
- const region = 'us-west-2';
-
- beforeEach(() => {
- createValidStateStore({ selectedRegion: region });
- buildWrapper();
- });
-
- it('fetches available vpcs', () => {
- expect(vpcsActions.fetchItems).toHaveBeenCalledWith(expect.anything(), { region });
- });
-
- it('fetches available key pairs', () => {
- expect(keyPairsActions.fetchItems).toHaveBeenCalledWith(expect.anything(), { region });
- });
-
- it('cleans selected vpc', () => {
- expect(actions.setVpc).toHaveBeenCalledWith(expect.anything(), { vpc: null });
- });
-
- it('cleans selected key pair', () => {
- expect(actions.setKeyPair).toHaveBeenCalledWith(expect.anything(), { keyPair: null });
- });
-
- it('cleans selected subnet', () => {
- expect(actions.setSubnet).toHaveBeenCalledWith(expect.anything(), { subnet: [] });
- });
-
- it('cleans selected security group', () => {
- expect(actions.setSecurityGroup).toHaveBeenCalledWith(expect.anything(), {
- securityGroup: null,
- });
- });
- });
- });
-
- it('sets isLoadingRoles to RoleDropdown loading property', async () => {
- rolesState.isLoadingItems = true;
-
- await nextTick();
- expect(findRoleDropdown().props('loading')).toBe(rolesState.isLoadingItems);
- });
-
- it('sets roles to RoleDropdown items property', () => {
- expect(findRoleDropdown().props('items')).toBe(rolesState.items);
- });
-
- it('sets RoleDropdown hasErrors to true when loading roles failed', async () => {
- rolesState.loadingItemsError = new Error();
-
- await nextTick();
- expect(findRoleDropdown().props('hasErrors')).toEqual(true);
- });
-
- it('disables KeyPairDropdown when no region is selected', () => {
- expect(findKeyPairDropdown().props('disabled')).toBe(true);
- });
-
- it('enables KeyPairDropdown when no region is selected', async () => {
- state.selectedRegion = { name: 'west-1 ' };
-
- await nextTick();
- expect(findKeyPairDropdown().props('disabled')).toBe(false);
- });
-
- it('sets isLoadingKeyPairs to KeyPairDropdown loading property', async () => {
- keyPairsState.isLoadingItems = true;
-
- await nextTick();
- expect(findKeyPairDropdown().props('loading')).toBe(keyPairsState.isLoadingItems);
- });
-
- it('sets keyPairs to KeyPairDropdown items property', () => {
- expect(findKeyPairDropdown().props('items')).toBe(keyPairsState.items);
- });
-
- it('sets KeyPairDropdown hasErrors to true when loading key pairs fails', async () => {
- keyPairsState.loadingItemsError = new Error();
-
- await nextTick();
- expect(findKeyPairDropdown().props('hasErrors')).toEqual(true);
- });
-
- it('disables VpcDropdown when no region is selected', () => {
- expect(findVpcDropdown().props('disabled')).toBe(true);
- });
-
- it('enables VpcDropdown when no region is selected', async () => {
- state.selectedRegion = { name: 'west-1 ' };
-
- await nextTick();
- expect(findVpcDropdown().props('disabled')).toBe(false);
- });
-
- it('sets isLoadingVpcs to VpcDropdown loading property', async () => {
- vpcsState.isLoadingItems = true;
-
- await nextTick();
- expect(findVpcDropdown().props('loading')).toBe(vpcsState.isLoadingItems);
- });
-
- it('sets vpcs to VpcDropdown items property', () => {
- expect(findVpcDropdown().props('items')).toBe(vpcsState.items);
- });
-
- it('sets VpcDropdown hasErrors to true when loading vpcs fails', async () => {
- vpcsState.loadingItemsError = new Error();
-
- await nextTick();
- expect(findVpcDropdown().props('hasErrors')).toEqual(true);
- });
-
- it('disables SubnetDropdown when no vpc is selected', () => {
- expect(findSubnetDropdown().props('disabled')).toBe(true);
- });
-
- it('enables SubnetDropdown when a vpc is selected', async () => {
- state.selectedVpc = { name: 'vpc-1 ' };
-
- await nextTick();
- expect(findSubnetDropdown().props('disabled')).toBe(false);
- });
-
- it('sets isLoadingSubnets to SubnetDropdown loading property', async () => {
- subnetsState.isLoadingItems = true;
-
- await nextTick();
- expect(findSubnetDropdown().props('loading')).toBe(subnetsState.isLoadingItems);
- });
-
- it('sets subnets to SubnetDropdown items property', () => {
- expect(findSubnetDropdown().props('items')).toBe(subnetsState.items);
- });
-
- it('displays a validation error in the subnet dropdown when loading subnets fails', () => {
- createStore({
- subnetsState: {
- loadingItemsError: new Error(),
- },
- });
- buildWrapper();
-
- expect(findSubnetDropdown().props('hasErrors')).toEqual(true);
- });
-
- it('displays a validation error in the subnet dropdown when a single subnet is selected', () => {
- createStore({
- initialState: {
- selectedSubnet: ['subnet 1'],
- },
- });
- buildWrapper();
-
- expect(findSubnetDropdown().props('hasErrors')).toEqual(true);
- expect(findSubnetDropdown().props('errorMessage')).toEqual(
- 'You should select at least two subnets',
- );
- });
-
- it('disables SecurityGroupDropdown when no vpc is selected', () => {
- expect(findSecurityGroupDropdown().props('disabled')).toBe(true);
- });
-
- it('enables SecurityGroupDropdown when a vpc is selected', async () => {
- state.selectedVpc = { name: 'vpc-1 ' };
-
- await nextTick();
- expect(findSecurityGroupDropdown().props('disabled')).toBe(false);
- });
-
- it('sets isLoadingSecurityGroups to SecurityGroupDropdown loading property', async () => {
- securityGroupsState.isLoadingItems = true;
-
- await nextTick();
- expect(findSecurityGroupDropdown().props('loading')).toBe(securityGroupsState.isLoadingItems);
- });
-
- it('sets securityGroups to SecurityGroupDropdown items property', () => {
- expect(findSecurityGroupDropdown().props('items')).toBe(securityGroupsState.items);
- });
-
- it('sets SecurityGroupDropdown hasErrors to true when loading security groups fails', async () => {
- securityGroupsState.loadingItemsError = new Error();
-
- await nextTick();
- expect(findSecurityGroupDropdown().props('hasErrors')).toEqual(true);
- });
-
- it('dispatches setClusterName when cluster name input changes', () => {
- const clusterName = 'name';
-
- findClusterNameInput().vm.$emit('input', clusterName);
-
- expect(actions.setClusterName).toHaveBeenCalledWith(expect.anything(), { clusterName });
- });
-
- it('dispatches setEnvironmentScope when environment scope input changes', () => {
- const environmentScope = 'production';
-
- findEnvironmentScopeInput().vm.$emit('input', environmentScope);
-
- expect(actions.setEnvironmentScope).toHaveBeenCalledWith(expect.anything(), {
- environmentScope,
- });
- });
-
- it('dispatches setKubernetesVersion when kubernetes version dropdown changes', () => {
- const kubernetesVersion = { name: '1.11' };
-
- findKubernetesVersionDropdown().vm.$emit('input', kubernetesVersion);
-
- expect(actions.setKubernetesVersion).toHaveBeenCalledWith(expect.anything(), {
- kubernetesVersion,
- });
- });
-
- it('dispatches setGitlabManagedCluster when gitlab managed cluster input changes', () => {
- const gitlabManagedCluster = false;
-
- findGitlabManagedClusterCheckbox().vm.$emit('input', gitlabManagedCluster);
-
- expect(actions.setGitlabManagedCluster).toHaveBeenCalledWith(expect.anything(), {
- gitlabManagedCluster,
- });
- });
-
- describe('when vpc is selected', () => {
- const vpc = { name: 'vpc-1' };
- const region = 'east-1';
-
- beforeEach(() => {
- state.selectedRegion = region;
- findVpcDropdown().vm.$emit('input', vpc);
- });
-
- it('dispatches setVpc action', () => {
- expect(actions.setVpc).toHaveBeenCalledWith(expect.anything(), { vpc });
- });
-
- it('cleans selected subnet', () => {
- expect(actions.setSubnet).toHaveBeenCalledWith(expect.anything(), { subnet: [] });
- });
-
- it('cleans selected security group', () => {
- expect(actions.setSecurityGroup).toHaveBeenCalledWith(expect.anything(), {
- securityGroup: null,
- });
- });
-
- it('dispatches fetchSubnets action', () => {
- expect(subnetsActions.fetchItems).toHaveBeenCalledWith(expect.anything(), { vpc, region });
- });
-
- it('dispatches fetchSecurityGroups action', () => {
- expect(securityGroupsActions.fetchItems).toHaveBeenCalledWith(expect.anything(), {
- vpc,
- region,
- });
- });
- });
-
- describe('when a subnet is selected', () => {
- const subnet = { name: 'subnet-1' };
-
- beforeEach(() => {
- findSubnetDropdown().vm.$emit('input', subnet);
- });
-
- it('dispatches setSubnet action', () => {
- expect(actions.setSubnet).toHaveBeenCalledWith(expect.anything(), { subnet });
- });
- });
-
- describe('when role is selected', () => {
- const role = { name: 'admin' };
-
- beforeEach(() => {
- findRoleDropdown().vm.$emit('input', role);
- });
-
- it('dispatches setRole action', () => {
- expect(actions.setRole).toHaveBeenCalledWith(expect.anything(), { role });
- });
- });
-
- describe('when key pair is selected', () => {
- const keyPair = { name: 'key pair' };
-
- beforeEach(() => {
- findKeyPairDropdown().vm.$emit('input', keyPair);
- });
-
- it('dispatches setKeyPair action', () => {
- expect(actions.setKeyPair).toHaveBeenCalledWith(expect.anything(), { keyPair });
- });
- });
-
- describe('when security group is selected', () => {
- const securityGroup = { name: 'default group' };
-
- beforeEach(() => {
- findSecurityGroupDropdown().vm.$emit('input', securityGroup);
- });
-
- it('dispatches setSecurityGroup action', () => {
- expect(actions.setSecurityGroup).toHaveBeenCalledWith(expect.anything(), { securityGroup });
- });
- });
-
- describe('when instance type is selected', () => {
- const instanceType = 'small-1';
-
- beforeEach(() => {
- findInstanceTypeDropdown().vm.$emit('input', instanceType);
- });
-
- it('dispatches setInstanceType action', () => {
- expect(actions.setInstanceType).toHaveBeenCalledWith(expect.anything(), { instanceType });
- });
- });
-
- it('dispatches setNodeCount when node count input changes', () => {
- const nodeCount = 5;
-
- findNodeCountInput().vm.$emit('input', nodeCount);
-
- expect(actions.setNodeCount).toHaveBeenCalledWith(expect.anything(), { nodeCount });
- });
-
- describe('when all cluster configuration fields are set', () => {
- it('enables create cluster button', () => {
- createValidStateStore();
- buildWrapper();
- expect(findCreateClusterButton().props('disabled')).toBe(false);
- });
- });
-
- describe('when at least one cluster configuration field is not set', () => {
- beforeEach(() => {
- createValidStateStore({
- clusterName: null,
- });
- buildWrapper();
- });
-
- it('disables create cluster button', () => {
- expect(findCreateClusterButton().props('disabled')).toBe(true);
- });
- });
-
- describe('when is creating cluster', () => {
- beforeEach(() => {
- createValidStateStore({
- isCreatingCluster: true,
- });
- buildWrapper();
- });
-
- it('sets create cluster button as loading', () => {
- expect(findCreateClusterButton().props('loading')).toBe(true);
- });
- });
-
- describe('clicking create cluster button', () => {
- beforeEach(() => {
- findCreateClusterButton().vm.$emit('click');
- });
-
- it('dispatches createCluster action', () => {
- expect(actions.createCluster).toHaveBeenCalled();
- });
- });
-});
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
deleted file mode 100644
index 0d823a18012..00000000000
--- a/spec/frontend/create_cluster/eks_cluster/components/service_credentials_form_spec.js
+++ /dev/null
@@ -1,124 +0,0 @@
-import { GlButton } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
-import Vue, { nextTick } from 'vue';
-import Vuex from 'vuex';
-import ServiceCredentialsForm from '~/create_cluster/eks_cluster/components/service_credentials_form.vue';
-import eksClusterState from '~/create_cluster/eks_cluster/store/state';
-
-Vue.use(Vuex);
-
-describe('ServiceCredentialsForm', () => {
- let vm;
- let state;
- let createRoleAction;
- const accountId = 'accountId';
- const externalId = 'externalId';
-
- beforeEach(() => {
- state = Object.assign(eksClusterState(), {
- accountId,
- externalId,
- });
- createRoleAction = jest.fn();
-
- const store = new Vuex.Store({
- state,
- actions: {
- createRole: createRoleAction,
- },
- });
- vm = shallowMount(ServiceCredentialsForm, {
- propsData: {
- accountAndExternalIdsHelpPath: '',
- createRoleArnHelpPath: '',
- externalLinkIcon: '',
- },
- store,
- });
- });
- afterEach(() => vm.destroy());
-
- const findAccountIdInput = () => vm.find('#gitlab-account-id');
- const findCopyAccountIdButton = () => vm.find('.js-copy-account-id-button');
- const findExternalIdInput = () => vm.find('#eks-external-id');
- const findCopyExternalIdButton = () => vm.find('.js-copy-external-id-button');
- const findInvalidCredentials = () => vm.find('.js-invalid-credentials');
- const findSubmitButton = () => vm.find(GlButton);
-
- it('displays provided account id', () => {
- expect(findAccountIdInput().attributes('value')).toBe(accountId);
- });
-
- it('allows to copy account id', () => {
- expect(findCopyAccountIdButton().props('text')).toBe(accountId);
- });
-
- it('displays provided external id', () => {
- expect(findExternalIdInput().attributes('value')).toBe(externalId);
- });
-
- it('allows to copy external id', () => {
- expect(findCopyExternalIdButton().props('text')).toBe(externalId);
- });
-
- it('disables submit button when role ARN is not provided', () => {
- expect(findSubmitButton().attributes('disabled')).toBeTruthy();
- });
-
- it('enables submit button when role ARN is not provided', async () => {
- // 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' });
-
- await nextTick();
- expect(findSubmitButton().attributes('disabled')).toBeFalsy();
- });
-
- 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'));
-
- expect(createRoleAction).toHaveBeenCalled();
- });
-
- describe('when is creating role', () => {
- beforeEach(async () => {
- // 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;
-
- await nextTick();
- });
-
- it('disables submit button', () => {
- expect(findSubmitButton().props('disabled')).toBe(true);
- });
-
- it('sets submit button as loading', () => {
- expect(findSubmitButton().props('loading')).toBe(true);
- });
-
- it('displays Authenticating label on submit button', () => {
- expect(findSubmitButton().text()).toBe('Authenticating');
- });
- });
-
- describe('when role can’t be created', () => {
- beforeEach(() => {
- state.createRoleError = 'Invalid credentials';
- });
-
- it('displays invalid role warning banner', () => {
- expect(findInvalidCredentials().exists()).toBe(true);
- });
-
- it('displays invalid role error message', () => {
- expect(findInvalidCredentials().text()).toContain(state.createRoleError);
- });
- });
-});
diff --git a/spec/frontend/create_cluster/eks_cluster/services/aws_services_facade_spec.js b/spec/frontend/create_cluster/eks_cluster/services/aws_services_facade_spec.js
deleted file mode 100644
index 7b93b6d0a09..00000000000
--- a/spec/frontend/create_cluster/eks_cluster/services/aws_services_facade_spec.js
+++ /dev/null
@@ -1,178 +0,0 @@
-import EC2 from 'aws-sdk/clients/ec2';
-import AWS from 'aws-sdk/global';
-import {
- setAWSConfig,
- fetchRoles,
- fetchKeyPairs,
- fetchVpcs,
- fetchSubnets,
- fetchSecurityGroups,
-} from '~/create_cluster/eks_cluster/services/aws_services_facade';
-
-const mockListRolesPromise = jest.fn();
-const mockDescribeRegionsPromise = jest.fn();
-const mockDescribeKeyPairsPromise = jest.fn();
-const mockDescribeVpcsPromise = jest.fn();
-const mockDescribeSubnetsPromise = jest.fn();
-const mockDescribeSecurityGroupsPromise = jest.fn();
-
-jest.mock('aws-sdk/clients/iam', () =>
- jest.fn().mockImplementation(() => ({
- listRoles: jest.fn().mockReturnValue({ promise: mockListRolesPromise }),
- })),
-);
-
-jest.mock('aws-sdk/clients/ec2', () =>
- jest.fn().mockImplementation(() => ({
- describeRegions: jest.fn().mockReturnValue({ promise: mockDescribeRegionsPromise }),
- describeKeyPairs: jest.fn().mockReturnValue({ promise: mockDescribeKeyPairsPromise }),
- describeVpcs: jest.fn().mockReturnValue({ promise: mockDescribeVpcsPromise }),
- describeSubnets: jest.fn().mockReturnValue({ promise: mockDescribeSubnetsPromise }),
- describeSecurityGroups: jest
- .fn()
- .mockReturnValue({ promise: mockDescribeSecurityGroupsPromise }),
- })),
-);
-
-describe('awsServicesFacade', () => {
- let region;
- let vpc;
-
- beforeEach(() => {
- region = 'west-1';
- vpc = 'vpc-2';
- });
-
- it('setAWSConfig configures AWS SDK with provided credentials', () => {
- const awsCredentials = {
- accessKeyId: 'access-key',
- secretAccessKey: 'secret-key',
- sessionToken: 'session-token',
- region,
- };
-
- setAWSConfig({ awsCredentials });
-
- expect(AWS.config).toEqual(awsCredentials);
- });
-
- describe('when fetchRoles succeeds', () => {
- let roles;
- let rolesOutput;
-
- beforeEach(() => {
- roles = [
- { RoleName: 'admin', Arn: 'aws::admin' },
- { RoleName: 'read-only', Arn: 'aws::read-only' },
- ];
- rolesOutput = roles.map(({ RoleName: name, Arn: value }) => ({ name, value }));
-
- mockListRolesPromise.mockResolvedValueOnce({ Roles: roles });
- });
-
- it('return list of regions where each item has a name and value', () => {
- return expect(fetchRoles()).resolves.toEqual(rolesOutput);
- });
- });
-
- describe('when fetchKeyPairs succeeds', () => {
- let keyPairs;
- let keyPairsOutput;
-
- beforeEach(() => {
- keyPairs = [{ KeyName: 'key-pair' }, { KeyName: 'key-pair-2' }];
- keyPairsOutput = keyPairs.map(({ KeyName: name }) => ({ name, value: name }));
-
- mockDescribeKeyPairsPromise.mockResolvedValueOnce({ KeyPairs: keyPairs });
- });
-
- it('instantatiates ec2 service with provided region', () => {
- fetchKeyPairs({ region });
- expect(EC2).toHaveBeenCalledWith({ region });
- });
-
- it('return list of key pairs where each item has a name and value', () => {
- return expect(fetchKeyPairs({ region })).resolves.toEqual(keyPairsOutput);
- });
- });
-
- describe('when fetchVpcs succeeds', () => {
- let vpcs;
- let vpcsOutput;
-
- beforeEach(() => {
- vpcs = [
- { VpcId: 'vpc-1', Tags: [] },
- { VpcId: 'vpc-2', Tags: [] },
- ];
- vpcsOutput = vpcs.map(({ VpcId: vpcId }) => ({ name: vpcId, value: vpcId }));
-
- mockDescribeVpcsPromise.mockResolvedValueOnce({ Vpcs: vpcs });
- });
-
- it('instantatiates ec2 service with provided region', () => {
- fetchVpcs({ region });
- expect(EC2).toHaveBeenCalledWith({ region });
- });
-
- it('return list of vpcs where each item has a name and value', () => {
- return expect(fetchVpcs({ region })).resolves.toEqual(vpcsOutput);
- });
- });
-
- describe('when vpcs has a Name tag', () => {
- const vpcName = 'vpc name';
- const vpcId = 'vpc id';
- let vpcs;
- let vpcsOutput;
-
- beforeEach(() => {
- vpcs = [{ VpcId: vpcId, Tags: [{ Key: 'Name', Value: vpcName }] }];
- vpcsOutput = [{ name: vpcName, value: vpcId }];
-
- mockDescribeVpcsPromise.mockResolvedValueOnce({ Vpcs: vpcs });
- });
-
- it('uses name tag value as the vpc name', () => {
- return expect(fetchVpcs({ region })).resolves.toEqual(vpcsOutput);
- });
- });
-
- describe('when fetchSubnets succeeds', () => {
- let subnets;
- let subnetsOutput;
-
- beforeEach(() => {
- subnets = [{ SubnetId: 'subnet-1' }, { SubnetId: 'subnet-2' }];
- subnetsOutput = subnets.map(({ SubnetId }) => ({ name: SubnetId, value: SubnetId }));
-
- mockDescribeSubnetsPromise.mockResolvedValueOnce({ Subnets: subnets });
- });
-
- it('return list of subnets where each item has a name and value', () => {
- return expect(fetchSubnets({ region, vpc })).resolves.toEqual(subnetsOutput);
- });
- });
-
- describe('when fetchSecurityGroups succeeds', () => {
- let securityGroups;
- let securityGroupsOutput;
-
- beforeEach(() => {
- securityGroups = [
- { GroupName: 'admin group', GroupId: 'group-1' },
- { GroupName: 'basic group', GroupId: 'group-2' },
- ];
- securityGroupsOutput = securityGroups.map(({ GroupId: value, GroupName: name }) => ({
- name,
- value,
- }));
-
- mockDescribeSecurityGroupsPromise.mockResolvedValueOnce({ SecurityGroups: securityGroups });
- });
-
- it('return list of security groups where each item has a name and value', () => {
- return expect(fetchSecurityGroups({ region, vpc })).resolves.toEqual(securityGroupsOutput);
- });
- });
-});
diff --git a/spec/frontend/create_cluster/eks_cluster/store/actions_spec.js b/spec/frontend/create_cluster/eks_cluster/store/actions_spec.js
deleted file mode 100644
index 8d7b22fe4ff..00000000000
--- a/spec/frontend/create_cluster/eks_cluster/store/actions_spec.js
+++ /dev/null
@@ -1,366 +0,0 @@
-import MockAdapter from 'axios-mock-adapter';
-import { useMockLocationHelper } from 'helpers/mock_window_location_helper';
-import testAction from 'helpers/vuex_action_helper';
-import { DEFAULT_REGION } from '~/create_cluster/eks_cluster/constants';
-import * as actions from '~/create_cluster/eks_cluster/store/actions';
-import {
- SET_CLUSTER_NAME,
- SET_ENVIRONMENT_SCOPE,
- SET_KUBERNETES_VERSION,
- SET_REGION,
- SET_VPC,
- SET_KEY_PAIR,
- SET_SUBNET,
- SET_ROLE,
- SET_SECURITY_GROUP,
- SET_GITLAB_MANAGED_CLUSTER,
- SET_NAMESPACE_PER_ENVIRONMENT,
- SET_INSTANCE_TYPE,
- SET_NODE_COUNT,
- REQUEST_CREATE_ROLE,
- CREATE_ROLE_SUCCESS,
- CREATE_ROLE_ERROR,
- REQUEST_CREATE_CLUSTER,
- CREATE_CLUSTER_ERROR,
-} from '~/create_cluster/eks_cluster/store/mutation_types';
-import createState from '~/create_cluster/eks_cluster/store/state';
-import createFlash from '~/flash';
-import axios from '~/lib/utils/axios_utils';
-
-jest.mock('~/flash');
-
-describe('EKS Cluster Store Actions', () => {
- let clusterName;
- let environmentScope;
- let kubernetesVersion;
- let region;
- let vpc;
- let subnet;
- let role;
- let keyPair;
- let securityGroup;
- let instanceType;
- let nodeCount;
- let gitlabManagedCluster;
- let namespacePerEnvironment;
- let mock;
- let state;
- let newClusterUrl;
-
- beforeEach(() => {
- clusterName = 'my cluster';
- environmentScope = 'production';
- kubernetesVersion = '1.16';
- region = 'regions-1';
- vpc = 'vpc-1';
- subnet = 'subnet-1';
- role = 'role-1';
- keyPair = 'key-pair-1';
- securityGroup = 'default group';
- instanceType = 'small-1';
- nodeCount = '5';
- gitlabManagedCluster = true;
- namespacePerEnvironment = true;
-
- newClusterUrl = '/clusters/1';
-
- state = {
- ...createState(),
- createRolePath: '/clusters/roles/',
- createClusterPath: '/clusters/',
- };
- });
-
- beforeEach(() => {
- mock = new MockAdapter(axios);
- });
-
- afterEach(() => {
- mock.restore();
- });
-
- it.each`
- action | mutation | payload | payloadDescription
- ${'setClusterName'} | ${SET_CLUSTER_NAME} | ${{ clusterName }} | ${'cluster name'}
- ${'setEnvironmentScope'} | ${SET_ENVIRONMENT_SCOPE} | ${{ environmentScope }} | ${'environment scope'}
- ${'setKubernetesVersion'} | ${SET_KUBERNETES_VERSION} | ${{ kubernetesVersion }} | ${'kubernetes version'}
- ${'setRole'} | ${SET_ROLE} | ${{ role }} | ${'role'}
- ${'setRegion'} | ${SET_REGION} | ${{ region }} | ${'region'}
- ${'setKeyPair'} | ${SET_KEY_PAIR} | ${{ keyPair }} | ${'key pair'}
- ${'setVpc'} | ${SET_VPC} | ${{ vpc }} | ${'vpc'}
- ${'setSubnet'} | ${SET_SUBNET} | ${{ subnet }} | ${'subnet'}
- ${'setSecurityGroup'} | ${SET_SECURITY_GROUP} | ${{ securityGroup }} | ${'securityGroup'}
- ${'setInstanceType'} | ${SET_INSTANCE_TYPE} | ${{ instanceType }} | ${'instance type'}
- ${'setNodeCount'} | ${SET_NODE_COUNT} | ${{ nodeCount }} | ${'node count'}
- ${'setGitlabManagedCluster'} | ${SET_GITLAB_MANAGED_CLUSTER} | ${gitlabManagedCluster} | ${'gitlab managed cluster'}
- ${'setNamespacePerEnvironment'} | ${SET_NAMESPACE_PER_ENVIRONMENT} | ${namespacePerEnvironment} | ${'namespace per environment'}
- `(`$action commits $mutation with $payloadDescription payload`, (data) => {
- const { action, mutation, payload } = data;
-
- testAction(actions[action], payload, state, [{ type: mutation, payload }]);
- });
-
- describe('createRole', () => {
- const payload = {
- roleArn: 'role_arn',
- externalId: 'externalId',
- };
- const response = {
- accessKeyId: 'access-key-id',
- secretAccessKey: 'secret-key-id',
- };
-
- describe('when request succeeds with default region', () => {
- beforeEach(() => {
- mock
- .onPost(state.createRolePath, {
- role_arn: payload.roleArn,
- role_external_id: payload.externalId,
- region: DEFAULT_REGION,
- })
- .reply(201, response);
- });
-
- it('dispatches createRoleSuccess action', () =>
- testAction(
- actions.createRole,
- payload,
- state,
- [],
- [
- { type: 'requestCreateRole' },
- {
- type: 'createRoleSuccess',
- payload: {
- region: DEFAULT_REGION,
- ...response,
- },
- },
- ],
- ));
- });
-
- describe('when request succeeds with custom region', () => {
- const customRegion = 'custom-region';
-
- beforeEach(() => {
- mock
- .onPost(state.createRolePath, {
- role_arn: payload.roleArn,
- role_external_id: payload.externalId,
- region: customRegion,
- })
- .reply(201, response);
- });
-
- it('dispatches createRoleSuccess action', () =>
- testAction(
- actions.createRole,
- {
- selectedRegion: customRegion,
- ...payload,
- },
- state,
- [],
- [
- { type: 'requestCreateRole' },
- {
- type: 'createRoleSuccess',
- payload: {
- region: customRegion,
- ...response,
- },
- },
- ],
- ));
- });
-
- describe('when request fails', () => {
- let error;
-
- beforeEach(() => {
- error = new Error('Request failed with status code 400');
- mock
- .onPost(state.createRolePath, {
- role_arn: payload.roleArn,
- role_external_id: payload.externalId,
- region: DEFAULT_REGION,
- })
- .reply(400, null);
- });
-
- it('dispatches createRoleError action', () =>
- testAction(
- actions.createRole,
- payload,
- state,
- [],
- [{ type: 'requestCreateRole' }, { type: 'createRoleError', payload: { error } }],
- ));
- });
-
- describe('when request fails with a message', () => {
- beforeEach(() => {
- const errResp = { message: 'Something failed' };
-
- mock
- .onPost(state.createRolePath, {
- role_arn: payload.roleArn,
- role_external_id: payload.externalId,
- region: DEFAULT_REGION,
- })
- .reply(4, errResp);
- });
-
- it('dispatches createRoleError action', () =>
- testAction(
- actions.createRole,
- payload,
- state,
- [],
- [
- { type: 'requestCreateRole' },
- { type: 'createRoleError', payload: { error: 'Something failed' } },
- ],
- ));
- });
- });
-
- describe('requestCreateRole', () => {
- it('commits requestCreaterole mutation', () => {
- testAction(actions.requestCreateRole, null, state, [{ type: REQUEST_CREATE_ROLE }]);
- });
- });
-
- describe('createRoleSuccess', () => {
- it('sets region and commits createRoleSuccess mutation', () => {
- testAction(
- actions.createRoleSuccess,
- { region },
- state,
- [{ type: CREATE_ROLE_SUCCESS }],
- [{ type: 'setRegion', payload: { region } }],
- );
- });
- });
-
- describe('createRoleError', () => {
- it('commits createRoleError mutation', () => {
- const payload = {
- error: new Error(),
- };
-
- testAction(actions.createRoleError, payload, state, [{ type: CREATE_ROLE_ERROR, payload }]);
- });
- });
-
- describe('createCluster', () => {
- let requestPayload;
-
- beforeEach(() => {
- requestPayload = {
- name: clusterName,
- environment_scope: environmentScope,
- managed: gitlabManagedCluster,
- namespace_per_environment: namespacePerEnvironment,
- provider_aws_attributes: {
- kubernetes_version: kubernetesVersion,
- region,
- vpc_id: vpc,
- subnet_ids: subnet,
- role_arn: role,
- key_name: keyPair,
- security_group_id: securityGroup,
- instance_type: instanceType,
- num_nodes: nodeCount,
- },
- };
- state = Object.assign(createState(), {
- clusterName,
- environmentScope,
- kubernetesVersion,
- selectedRegion: region,
- selectedVpc: vpc,
- selectedSubnet: subnet,
- selectedRole: role,
- selectedKeyPair: keyPair,
- selectedSecurityGroup: securityGroup,
- selectedInstanceType: instanceType,
- nodeCount,
- gitlabManagedCluster,
- namespacePerEnvironment,
- });
- });
-
- describe('when request succeeds', () => {
- beforeEach(() => {
- mock.onPost(state.createClusterPath, requestPayload).reply(201, null, {
- location: '/clusters/1',
- });
- });
-
- it('dispatches createClusterSuccess action', () =>
- testAction(
- actions.createCluster,
- null,
- state,
- [],
- [
- { type: 'requestCreateCluster' },
- { type: 'createClusterSuccess', payload: newClusterUrl },
- ],
- ));
- });
-
- describe('when request fails', () => {
- let response;
-
- beforeEach(() => {
- response = 'Request failed with status code 400';
- mock.onPost(state.createClusterPath, requestPayload).reply(400, response);
- });
-
- it('dispatches createRoleError action', () =>
- testAction(
- actions.createCluster,
- null,
- state,
- [],
- [{ type: 'requestCreateCluster' }, { type: 'createClusterError', payload: response }],
- ));
- });
- });
-
- describe('requestCreateCluster', () => {
- it('commits requestCreateCluster mutation', () => {
- testAction(actions.requestCreateCluster, null, state, [{ type: REQUEST_CREATE_CLUSTER }]);
- });
- });
-
- describe('createClusterSuccess', () => {
- useMockLocationHelper();
-
- it('redirects to the new cluster URL', () => {
- actions.createClusterSuccess(null, newClusterUrl);
-
- expect(window.location.assign).toHaveBeenCalledWith(newClusterUrl);
- });
- });
-
- describe('createClusterError', () => {
- let payload;
-
- beforeEach(() => {
- payload = { name: ['Create cluster failed'] };
- });
-
- it('commits createClusterError mutation and displays flash message', () =>
- testAction(actions.createClusterError, payload, state, [
- { type: CREATE_CLUSTER_ERROR, payload },
- ]).then(() => {
- expect(createFlash).toHaveBeenCalledWith({
- message: payload.name[0],
- });
- }));
- });
-});
diff --git a/spec/frontend/create_cluster/eks_cluster/store/getters_spec.js b/spec/frontend/create_cluster/eks_cluster/store/getters_spec.js
deleted file mode 100644
index 46c37961dd3..00000000000
--- a/spec/frontend/create_cluster/eks_cluster/store/getters_spec.js
+++ /dev/null
@@ -1,13 +0,0 @@
-import { subnetValid } from '~/create_cluster/eks_cluster/store/getters';
-
-describe('EKS Cluster Store Getters', () => {
- describe('subnetValid', () => {
- it('returns true if there are 2 or more selected subnets', () => {
- expect(subnetValid({ selectedSubnet: [1, 2] })).toBe(true);
- });
-
- it.each([[[], [1]]])('returns false if there are 1 or less selected subnets', (subnets) => {
- expect(subnetValid({ selectedSubnet: subnets })).toBe(false);
- });
- });
-});
diff --git a/spec/frontend/create_cluster/eks_cluster/store/mutations_spec.js b/spec/frontend/create_cluster/eks_cluster/store/mutations_spec.js
deleted file mode 100644
index 54d66e79be7..00000000000
--- a/spec/frontend/create_cluster/eks_cluster/store/mutations_spec.js
+++ /dev/null
@@ -1,161 +0,0 @@
-import {
- SET_CLUSTER_NAME,
- SET_ENVIRONMENT_SCOPE,
- SET_KUBERNETES_VERSION,
- SET_REGION,
- SET_VPC,
- SET_KEY_PAIR,
- SET_SUBNET,
- SET_ROLE,
- SET_SECURITY_GROUP,
- SET_INSTANCE_TYPE,
- SET_NODE_COUNT,
- SET_GITLAB_MANAGED_CLUSTER,
- REQUEST_CREATE_ROLE,
- CREATE_ROLE_SUCCESS,
- CREATE_ROLE_ERROR,
- REQUEST_CREATE_CLUSTER,
- CREATE_CLUSTER_ERROR,
-} from '~/create_cluster/eks_cluster/store/mutation_types';
-import mutations from '~/create_cluster/eks_cluster/store/mutations';
-import createState from '~/create_cluster/eks_cluster/store/state';
-
-describe('Create EKS cluster store mutations', () => {
- let clusterName;
- let environmentScope;
- let kubernetesVersion;
- let state;
- let region;
- let vpc;
- let subnet;
- let role;
- let keyPair;
- let securityGroup;
- let instanceType;
- let nodeCount;
- let gitlabManagedCluster;
-
- beforeEach(() => {
- clusterName = 'my cluster';
- environmentScope = 'production';
- kubernetesVersion = '11.1';
- region = { name: 'regions-1' };
- vpc = { name: 'vpc-1' };
- subnet = { name: 'subnet-1' };
- role = { name: 'role-1' };
- keyPair = { name: 'key pair' };
- securityGroup = { name: 'default group' };
- instanceType = 'small-1';
- nodeCount = '5';
- gitlabManagedCluster = false;
-
- state = createState();
- });
-
- it.each`
- mutation | mutatedProperty | payload | expectedValue | expectedValueDescription
- ${SET_CLUSTER_NAME} | ${'clusterName'} | ${{ clusterName }} | ${clusterName} | ${'cluster name'}
- ${SET_ENVIRONMENT_SCOPE} | ${'environmentScope'} | ${{ environmentScope }} | ${environmentScope} | ${'environment scope'}
- ${SET_KUBERNETES_VERSION} | ${'kubernetesVersion'} | ${{ kubernetesVersion }} | ${kubernetesVersion} | ${'kubernetes version'}
- ${SET_ROLE} | ${'selectedRole'} | ${{ role }} | ${role} | ${'selected role payload'}
- ${SET_REGION} | ${'selectedRegion'} | ${{ region }} | ${region} | ${'selected region payload'}
- ${SET_KEY_PAIR} | ${'selectedKeyPair'} | ${{ keyPair }} | ${keyPair} | ${'selected key pair payload'}
- ${SET_VPC} | ${'selectedVpc'} | ${{ vpc }} | ${vpc} | ${'selected vpc payload'}
- ${SET_SUBNET} | ${'selectedSubnet'} | ${{ subnet }} | ${subnet} | ${'selected subnet payload'}
- ${SET_SECURITY_GROUP} | ${'selectedSecurityGroup'} | ${{ securityGroup }} | ${securityGroup} | ${'selected security group payload'}
- ${SET_INSTANCE_TYPE} | ${'selectedInstanceType'} | ${{ instanceType }} | ${instanceType} | ${'selected instance type payload'}
- ${SET_NODE_COUNT} | ${'nodeCount'} | ${{ nodeCount }} | ${nodeCount} | ${'node count payload'}
- ${SET_GITLAB_MANAGED_CLUSTER} | ${'gitlabManagedCluster'} | ${{ gitlabManagedCluster }} | ${gitlabManagedCluster} | ${'gitlab managed cluster'}
- `(`$mutation sets $mutatedProperty to $expectedValueDescription`, (data) => {
- const { mutation, mutatedProperty, payload, expectedValue } = data;
-
- mutations[mutation](state, payload);
- expect(state[mutatedProperty]).toBe(expectedValue);
- });
-
- describe(`mutation ${REQUEST_CREATE_ROLE}`, () => {
- beforeEach(() => {
- mutations[REQUEST_CREATE_ROLE](state);
- });
-
- it('sets isCreatingRole to true', () => {
- expect(state.isCreatingRole).toBe(true);
- });
-
- it('sets createRoleError to null', () => {
- expect(state.createRoleError).toBe(null);
- });
-
- it('sets hasCredentials to false', () => {
- expect(state.hasCredentials).toBe(false);
- });
- });
-
- describe(`mutation ${CREATE_ROLE_SUCCESS}`, () => {
- beforeEach(() => {
- mutations[CREATE_ROLE_SUCCESS](state);
- });
-
- it('sets isCreatingRole to false', () => {
- expect(state.isCreatingRole).toBe(false);
- });
-
- it('sets createRoleError to null', () => {
- expect(state.createRoleError).toBe(null);
- });
-
- it('sets hasCredentials to false', () => {
- expect(state.hasCredentials).toBe(true);
- });
- });
-
- describe(`mutation ${CREATE_ROLE_ERROR}`, () => {
- const error = new Error();
-
- beforeEach(() => {
- mutations[CREATE_ROLE_ERROR](state, { error });
- });
-
- it('sets isCreatingRole to false', () => {
- expect(state.isCreatingRole).toBe(false);
- });
-
- it('sets createRoleError to the error object', () => {
- expect(state.createRoleError).toBe(error);
- });
-
- it('sets hasCredentials to false', () => {
- expect(state.hasCredentials).toBe(false);
- });
- });
-
- describe(`mutation ${REQUEST_CREATE_CLUSTER}`, () => {
- beforeEach(() => {
- mutations[REQUEST_CREATE_CLUSTER](state);
- });
-
- it('sets isCreatingCluster to true', () => {
- expect(state.isCreatingCluster).toBe(true);
- });
-
- it('sets createClusterError to null', () => {
- expect(state.createClusterError).toBe(null);
- });
- });
-
- describe(`mutation ${CREATE_CLUSTER_ERROR}`, () => {
- const error = new Error();
-
- beforeEach(() => {
- mutations[CREATE_CLUSTER_ERROR](state, { error });
- });
-
- it('sets isCreatingRole to false', () => {
- expect(state.isCreatingCluster).toBe(false);
- });
-
- it('sets createRoleError to the error object', () => {
- expect(state.createClusterError).toBe(error);
- });
- });
-});
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
deleted file mode 100644
index f46b84da939..00000000000
--- a/spec/frontend/create_cluster/gke_cluster/components/gke_machine_type_dropdown_spec.js
+++ /dev/null
@@ -1,129 +0,0 @@
-import { shallowMount } from '@vue/test-utils';
-import Vue, { nextTick } from 'vue';
-import Vuex from 'vuex';
-import GkeMachineTypeDropdown from '~/create_cluster/gke_cluster/components/gke_machine_type_dropdown.vue';
-import createState from '~/create_cluster/gke_cluster/store/state';
-import DropdownButton from '~/vue_shared/components/dropdown/dropdown_button.vue';
-import DropdownHiddenInput from '~/vue_shared/components/dropdown/dropdown_hidden_input.vue';
-import { selectedMachineTypeMock, gapiMachineTypesResponseMock } from '../mock_data';
-
-const componentConfig = {
- fieldId: 'cluster_provider_gcp_attributes_gcp_machine_type',
- fieldName: 'cluster[provider_gcp_attributes][gcp_machine_type]',
-};
-const setMachineType = jest.fn();
-
-const LABELS = {
- LOADING: 'Fetching machine types',
- DISABLED_NO_PROJECT: 'Select project and zone to choose machine type',
- DISABLED_NO_ZONE: 'Select zone to choose machine type',
- DEFAULT: 'Select machine type',
-};
-
-Vue.use(Vuex);
-
-const createComponent = (store, propsData = componentConfig) =>
- shallowMount(GkeMachineTypeDropdown, {
- propsData,
- store,
- });
-
-const createStore = (initialState = {}, getters = {}) =>
- new Vuex.Store({
- state: {
- ...createState(),
- ...initialState,
- },
- getters: {
- hasZone: () => false,
- ...getters,
- },
- actions: {
- setMachineType,
- },
- });
-
-describe('GkeMachineTypeDropdown', () => {
- let wrapper;
- let store;
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- const dropdownButtonLabel = () => wrapper.find(DropdownButton).props('toggleText');
- const dropdownHiddenInputValue = () => wrapper.find(DropdownHiddenInput).props('value');
-
- describe('shows various toggle text depending on state', () => {
- it('returns disabled state toggle text when no project and zone are selected', () => {
- store = createStore({
- projectHasBillingEnabled: false,
- });
- wrapper = createComponent(store);
-
- expect(dropdownButtonLabel()).toBe(LABELS.DISABLED_NO_PROJECT);
- });
-
- it('returns disabled state toggle text when no zone is selected', () => {
- store = createStore({
- projectHasBillingEnabled: true,
- });
- wrapper = createComponent(store);
-
- expect(dropdownButtonLabel()).toBe(LABELS.DISABLED_NO_ZONE);
- });
-
- it('returns loading toggle text', async () => {
- 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 });
-
- await nextTick();
- expect(dropdownButtonLabel()).toBe(LABELS.LOADING);
- });
-
- it('returns default toggle text', () => {
- store = createStore(
- {
- projectHasBillingEnabled: true,
- },
- { hasZone: () => true },
- );
- wrapper = createComponent(store);
-
- expect(dropdownButtonLabel()).toBe(LABELS.DEFAULT);
- });
-
- it('returns machine type name if machine type selected', () => {
- store = createStore(
- {
- projectHasBillingEnabled: true,
- selectedMachineType: selectedMachineTypeMock,
- },
- { hasZone: () => true },
- );
- wrapper = createComponent(store);
-
- expect(dropdownButtonLabel()).toBe(selectedMachineTypeMock);
- });
- });
-
- describe('form input', () => {
- it('reflects new value when dropdown item is clicked', async () => {
- store = createStore({
- machineTypes: gapiMachineTypesResponseMock.items,
- });
- wrapper = createComponent(store);
-
- expect(dropdownHiddenInputValue()).toBe('');
-
- wrapper.find('.dropdown-content button').trigger('click');
-
- await nextTick();
- expect(setMachineType).toHaveBeenCalledWith(expect.anything(), selectedMachineTypeMock);
- });
- });
-});
diff --git a/spec/frontend/create_cluster/gke_cluster/components/gke_network_dropdown_spec.js b/spec/frontend/create_cluster/gke_cluster/components/gke_network_dropdown_spec.js
deleted file mode 100644
index addb0ef72a0..00000000000
--- a/spec/frontend/create_cluster/gke_cluster/components/gke_network_dropdown_spec.js
+++ /dev/null
@@ -1,137 +0,0 @@
-import { shallowMount } from '@vue/test-utils';
-import Vue from 'vue';
-import Vuex from 'vuex';
-import ClusterFormDropdown from '~/create_cluster/components/cluster_form_dropdown.vue';
-import GkeNetworkDropdown from '~/create_cluster/gke_cluster/components/gke_network_dropdown.vue';
-import createClusterDropdownState from '~/create_cluster/store/cluster_dropdown/state';
-
-Vue.use(Vuex);
-
-describe('GkeNetworkDropdown', () => {
- let wrapper;
- let store;
- const defaultProps = { fieldName: 'field-name' };
- const selectedNetwork = { selfLink: '123456' };
- const projectId = '6789';
- const region = 'east-1';
- const setNetwork = jest.fn();
- const setSubnetwork = jest.fn();
- const fetchSubnetworks = jest.fn();
-
- const buildStore = ({ clusterDropdownState } = {}) =>
- new Vuex.Store({
- state: {
- selectedNetwork,
- },
- actions: {
- setNetwork,
- setSubnetwork,
- },
- getters: {
- hasZone: () => false,
- region: () => region,
- projectId: () => projectId,
- },
- modules: {
- networks: {
- namespaced: true,
- state: {
- ...createClusterDropdownState(),
- ...(clusterDropdownState || {}),
- },
- },
- subnetworks: {
- namespaced: true,
- actions: {
- fetchItems: fetchSubnetworks,
- },
- },
- },
- });
-
- const buildWrapper = (propsData = defaultProps) =>
- shallowMount(GkeNetworkDropdown, {
- propsData,
- store,
- });
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- it('sets correct field-name', () => {
- const fieldName = 'field-name';
-
- store = buildStore();
- wrapper = buildWrapper({ fieldName });
-
- expect(wrapper.find(ClusterFormDropdown).props('fieldName')).toBe(fieldName);
- });
-
- it('sets selected network as the dropdown value', () => {
- store = buildStore();
- wrapper = buildWrapper();
-
- expect(wrapper.find(ClusterFormDropdown).props('value')).toBe(selectedNetwork);
- });
-
- it('maps networks store items to the dropdown items property', () => {
- const items = [{ name: 'network' }];
-
- store = buildStore({ clusterDropdownState: { items } });
- wrapper = buildWrapper();
-
- expect(wrapper.find(ClusterFormDropdown).props('items')).toBe(items);
- });
-
- describe('when network dropdown store is loading items', () => {
- it('sets network dropdown as loading', () => {
- store = buildStore({ clusterDropdownState: { isLoadingItems: true } });
- wrapper = buildWrapper();
-
- expect(wrapper.find(ClusterFormDropdown).props('loading')).toBe(true);
- });
- });
-
- describe('when there is no selected zone', () => {
- it('disables the network dropdown', () => {
- store = buildStore();
- wrapper = buildWrapper();
-
- expect(wrapper.find(ClusterFormDropdown).props('disabled')).toBe(true);
- });
- });
-
- describe('when an error occurs while loading networks', () => {
- it('sets the network dropdown as having errors', () => {
- store = buildStore({ clusterDropdownState: { loadingItemsError: new Error() } });
- wrapper = buildWrapper();
-
- expect(wrapper.find(ClusterFormDropdown).props('hasErrors')).toBe(true);
- });
- });
-
- describe('when dropdown emits input event', () => {
- beforeEach(() => {
- store = buildStore();
- wrapper = buildWrapper();
- wrapper.find(ClusterFormDropdown).vm.$emit('input', selectedNetwork);
- });
-
- it('cleans selected subnetwork', () => {
- expect(setSubnetwork).toHaveBeenCalledWith(expect.anything(), '');
- });
-
- it('dispatches the setNetwork action', () => {
- expect(setNetwork).toHaveBeenCalledWith(expect.anything(), selectedNetwork);
- });
-
- it('fetches subnetworks for the selected project, region, and network', () => {
- expect(fetchSubnetworks).toHaveBeenCalledWith(expect.anything(), {
- project: projectId,
- region,
- network: selectedNetwork.selfLink,
- });
- });
- });
-});
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
deleted file mode 100644
index 36f8d4bd1e8..00000000000
--- a/spec/frontend/create_cluster/gke_cluster/components/gke_project_id_dropdown_spec.js
+++ /dev/null
@@ -1,137 +0,0 @@
-import { shallowMount } from '@vue/test-utils';
-import Vue, { nextTick } from 'vue';
-import Vuex from 'vuex';
-import GkeProjectIdDropdown from '~/create_cluster/gke_cluster/components/gke_project_id_dropdown.vue';
-import createState from '~/create_cluster/gke_cluster/store/state';
-import DropdownButton from '~/vue_shared/components/dropdown/dropdown_button.vue';
-import DropdownHiddenInput from '~/vue_shared/components/dropdown/dropdown_hidden_input.vue';
-import { selectedProjectMock, gapiProjectsResponseMock } from '../mock_data';
-
-const componentConfig = {
- docsUrl: 'https://console.cloud.google.com/home/dashboard',
- fieldId: 'cluster_provider_gcp_attributes_gcp_project_id',
- fieldName: 'cluster[provider_gcp_attributes][gcp_project_id]',
-};
-
-const LABELS = {
- LOADING: 'Fetching projects',
- VALIDATING_PROJECT_BILLING: 'Validating project billing status',
- DEFAULT: 'Select project',
- EMPTY: 'No projects found',
-};
-
-Vue.use(Vuex);
-
-describe('GkeProjectIdDropdown', () => {
- let wrapper;
- let vuexStore;
- let setProject;
-
- beforeEach(() => {
- setProject = jest.fn();
- });
-
- const createStore = (initialState = {}, getters = {}) =>
- new Vuex.Store({
- state: {
- ...createState(),
- ...initialState,
- },
- actions: {
- fetchProjects: jest.fn().mockResolvedValueOnce([]),
- setProject,
- },
- getters: {
- hasProject: () => false,
- ...getters,
- },
- });
-
- const createComponent = (store, propsData = componentConfig) =>
- shallowMount(GkeProjectIdDropdown, {
- propsData,
- store,
- });
-
- const bootstrap = (initialState, getters) => {
- vuexStore = createStore(initialState, getters);
- wrapper = createComponent(vuexStore);
- };
-
- const dropdownButtonLabel = () => wrapper.find(DropdownButton).props('toggleText');
- const dropdownHiddenInputValue = () => wrapper.find(DropdownHiddenInput).props('value');
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- describe('toggleText', () => {
- it('returns loading toggle text', () => {
- bootstrap();
-
- expect(dropdownButtonLabel()).toBe(LABELS.LOADING);
- });
-
- it('returns project billing validation text', () => {
- bootstrap({ isValidatingProjectBilling: true });
-
- expect(dropdownButtonLabel()).toBe(LABELS.VALIDATING_PROJECT_BILLING);
- });
-
- it('returns default toggle text', async () => {
- 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 });
-
- await nextTick();
- expect(dropdownButtonLabel()).toBe(LABELS.DEFAULT);
- });
-
- it('returns project name if project selected', async () => {
- bootstrap(
- {
- selectedProject: selectedProjectMock,
- },
- {
- 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 });
-
- await nextTick();
- expect(dropdownButtonLabel()).toBe(selectedProjectMock.name);
- });
-
- it('returns empty toggle text', async () => {
- 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 });
-
- await nextTick();
- expect(dropdownButtonLabel()).toBe(LABELS.EMPTY);
- });
- });
-
- describe('selectItem', () => {
- it('reflects new value when dropdown item is clicked', async () => {
- bootstrap({ projects: gapiProjectsResponseMock.projects });
-
- expect(dropdownHiddenInputValue()).toBe('');
-
- wrapper.find('.dropdown-content button').trigger('click');
-
- await nextTick();
- expect(setProject).toHaveBeenCalledWith(
- expect.anything(),
- gapiProjectsResponseMock.projects[0],
- );
- });
- });
-});
diff --git a/spec/frontend/create_cluster/gke_cluster/components/gke_submit_button_spec.js b/spec/frontend/create_cluster/gke_cluster/components/gke_submit_button_spec.js
deleted file mode 100644
index 2bf9158628c..00000000000
--- a/spec/frontend/create_cluster/gke_cluster/components/gke_submit_button_spec.js
+++ /dev/null
@@ -1,51 +0,0 @@
-import { shallowMount } from '@vue/test-utils';
-import Vue from 'vue';
-import Vuex from 'vuex';
-import GkeSubmitButton from '~/create_cluster/gke_cluster/components/gke_submit_button.vue';
-
-Vue.use(Vuex);
-
-describe('GkeSubmitButton', () => {
- let wrapper;
- let store;
- let hasValidData;
-
- const buildStore = () =>
- new Vuex.Store({
- getters: {
- hasValidData,
- },
- });
-
- const buildWrapper = () =>
- shallowMount(GkeSubmitButton, {
- store,
- });
-
- const bootstrap = () => {
- store = buildStore();
- wrapper = buildWrapper();
- };
-
- beforeEach(() => {
- hasValidData = jest.fn();
- });
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- it('is disabled when hasValidData is false', () => {
- hasValidData.mockReturnValueOnce(false);
- bootstrap();
-
- expect(wrapper.attributes('disabled')).toBe('disabled');
- });
-
- it('is not disabled when hasValidData is true', () => {
- hasValidData.mockReturnValueOnce(true);
- bootstrap();
-
- expect(wrapper.attributes('disabled')).toBeFalsy();
- });
-});
diff --git a/spec/frontend/create_cluster/gke_cluster/components/gke_subnetwork_dropdown_spec.js b/spec/frontend/create_cluster/gke_cluster/components/gke_subnetwork_dropdown_spec.js
deleted file mode 100644
index 9df680d94b5..00000000000
--- a/spec/frontend/create_cluster/gke_cluster/components/gke_subnetwork_dropdown_spec.js
+++ /dev/null
@@ -1,111 +0,0 @@
-import { shallowMount } from '@vue/test-utils';
-import Vue from 'vue';
-import Vuex from 'vuex';
-import ClusterFormDropdown from '~/create_cluster/components/cluster_form_dropdown.vue';
-import GkeSubnetworkDropdown from '~/create_cluster/gke_cluster/components/gke_subnetwork_dropdown.vue';
-import createClusterDropdownState from '~/create_cluster/store/cluster_dropdown/state';
-
-Vue.use(Vuex);
-
-describe('GkeSubnetworkDropdown', () => {
- let wrapper;
- let store;
- const defaultProps = { fieldName: 'field-name' };
- const selectedSubnetwork = '123456';
- const setSubnetwork = jest.fn();
-
- const buildStore = ({ clusterDropdownState } = {}) =>
- new Vuex.Store({
- state: {
- selectedSubnetwork,
- },
- actions: {
- setSubnetwork,
- },
- getters: {
- hasNetwork: () => false,
- },
- modules: {
- subnetworks: {
- namespaced: true,
- state: {
- ...createClusterDropdownState(),
- ...(clusterDropdownState || {}),
- },
- },
- },
- });
-
- const buildWrapper = (propsData = defaultProps) =>
- shallowMount(GkeSubnetworkDropdown, {
- propsData,
- store,
- });
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- it('sets correct field-name', () => {
- const fieldName = 'field-name';
-
- store = buildStore();
- wrapper = buildWrapper({ fieldName });
-
- expect(wrapper.find(ClusterFormDropdown).props('fieldName')).toBe(fieldName);
- });
-
- it('sets selected subnetwork as the dropdown value', () => {
- store = buildStore();
- wrapper = buildWrapper();
-
- expect(wrapper.find(ClusterFormDropdown).props('value')).toBe(selectedSubnetwork);
- });
-
- it('maps subnetworks store items to the dropdown items property', () => {
- const items = [{ name: 'subnetwork' }];
-
- store = buildStore({ clusterDropdownState: { items } });
- wrapper = buildWrapper();
-
- expect(wrapper.find(ClusterFormDropdown).props('items')).toBe(items);
- });
-
- describe('when subnetwork dropdown store is loading items', () => {
- it('sets subnetwork dropdown as loading', () => {
- store = buildStore({ clusterDropdownState: { isLoadingItems: true } });
- wrapper = buildWrapper();
-
- expect(wrapper.find(ClusterFormDropdown).props('loading')).toBe(true);
- });
- });
-
- describe('when there is no selected network', () => {
- it('disables the subnetwork dropdown', () => {
- store = buildStore();
- wrapper = buildWrapper();
-
- expect(wrapper.find(ClusterFormDropdown).props('disabled')).toBe(true);
- });
- });
-
- describe('when an error occurs while loading subnetworks', () => {
- it('sets the subnetwork dropdown as having errors', () => {
- store = buildStore({ clusterDropdownState: { loadingItemsError: new Error() } });
- wrapper = buildWrapper();
-
- expect(wrapper.find(ClusterFormDropdown).props('hasErrors')).toBe(true);
- });
- });
-
- describe('when dropdown emits input event', () => {
- it('dispatches the setSubnetwork action', () => {
- store = buildStore();
- wrapper = buildWrapper();
-
- wrapper.find(ClusterFormDropdown).vm.$emit('input', selectedSubnetwork);
-
- expect(setSubnetwork).toHaveBeenCalledWith(expect.anything(), selectedSubnetwork);
- });
- });
-});
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
deleted file mode 100644
index 7b4c228b879..00000000000
--- a/spec/frontend/create_cluster/gke_cluster/components/gke_zone_dropdown_spec.js
+++ /dev/null
@@ -1,103 +0,0 @@
-import { shallowMount } from '@vue/test-utils';
-import { nextTick } from 'vue';
-import GkeZoneDropdown from '~/create_cluster/gke_cluster/components/gke_zone_dropdown.vue';
-import { createStore } from '~/create_cluster/gke_cluster/store';
-import {
- SET_PROJECT,
- SET_ZONES,
- SET_PROJECT_BILLING_STATUS,
-} from '~/create_cluster/gke_cluster/store/mutation_types';
-import DropdownButton from '~/vue_shared/components/dropdown/dropdown_button.vue';
-import DropdownHiddenInput from '~/vue_shared/components/dropdown/dropdown_hidden_input.vue';
-import { selectedZoneMock, selectedProjectMock, gapiZonesResponseMock } from '../mock_data';
-
-const propsData = {
- fieldId: 'cluster_provider_gcp_attributes_gcp_zone',
- fieldName: 'cluster[provider_gcp_attributes][gcp_zone]',
-};
-
-const LABELS = {
- LOADING: 'Fetching zones',
- DISABLED: 'Select project to choose zone',
- DEFAULT: 'Select zone',
-};
-
-describe('GkeZoneDropdown', () => {
- let store;
- let wrapper;
-
- beforeEach(() => {
- store = createStore();
- wrapper = shallowMount(GkeZoneDropdown, { propsData, store });
- });
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- describe('toggleText', () => {
- let dropdownButton;
-
- beforeEach(() => {
- dropdownButton = wrapper.find(DropdownButton);
- });
-
- it('returns disabled state toggle text', () => {
- expect(dropdownButton.props('toggleText')).toBe(LABELS.DISABLED);
- });
-
- describe('isLoading', () => {
- 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({ isLoading: true });
- await nextTick();
- });
-
- it('returns loading toggle text', () => {
- expect(dropdownButton.props('toggleText')).toBe(LABELS.LOADING);
- });
- });
-
- describe('project is set', () => {
- beforeEach(async () => {
- wrapper.vm.$store.commit(SET_PROJECT, selectedProjectMock);
- wrapper.vm.$store.commit(SET_PROJECT_BILLING_STATUS, true);
- await nextTick();
- });
-
- it('returns default toggle text', () => {
- expect(dropdownButton.props('toggleText')).toBe(LABELS.DEFAULT);
- });
- });
-
- describe('project is selected', () => {
- beforeEach(async () => {
- wrapper.vm.setItem(selectedZoneMock);
- await nextTick();
- });
-
- it('returns project name if project selected', () => {
- expect(dropdownButton.props('toggleText')).toBe(selectedZoneMock);
- });
- });
- });
-
- describe('selectItem', () => {
- beforeEach(async () => {
- wrapper.vm.$store.commit(SET_ZONES, gapiZonesResponseMock.items);
- await nextTick();
- });
-
- it('reflects new value when dropdown item is clicked', async () => {
- const dropdown = wrapper.find(DropdownHiddenInput);
-
- expect(dropdown.attributes('value')).toBe('');
-
- wrapper.find('.dropdown-content button').trigger('click');
-
- await nextTick();
- expect(dropdown.attributes('value')).toBe(selectedZoneMock);
- });
- });
-});
diff --git a/spec/frontend/create_cluster/gke_cluster/gapi_loader_spec.js b/spec/frontend/create_cluster/gke_cluster/gapi_loader_spec.js
deleted file mode 100644
index 9e4d6996340..00000000000
--- a/spec/frontend/create_cluster/gke_cluster/gapi_loader_spec.js
+++ /dev/null
@@ -1,47 +0,0 @@
-import gapiLoader from '~/create_cluster/gke_cluster/gapi_loader';
-
-describe('gapiLoader', () => {
- // A mock for document.head.appendChild to intercept the script tag injection.
- let mockDOMHeadAppendChild;
-
- beforeEach(() => {
- mockDOMHeadAppendChild = jest.spyOn(document.head, 'appendChild');
- });
-
- afterEach(() => {
- mockDOMHeadAppendChild.mockRestore();
- delete window.gapi;
- delete window.gapiPromise;
- delete window.onGapiLoad;
- });
-
- it('returns a promise', () => {
- expect(gapiLoader()).toBeInstanceOf(Promise);
- });
-
- it('returns the same promise when already loading', () => {
- const first = gapiLoader();
- const second = gapiLoader();
- expect(first).toBe(second);
- });
-
- it('resolves the promise when the script loads correctly', async () => {
- mockDOMHeadAppendChild.mockImplementationOnce((script) => {
- script.removeAttribute('src');
- script.appendChild(
- document.createTextNode(`window.gapi = 'hello gapi'; window.onGapiLoad()`),
- );
- document.head.appendChild(script);
- });
- await expect(gapiLoader()).resolves.toBe('hello gapi');
- expect(mockDOMHeadAppendChild).toHaveBeenCalled();
- });
-
- it('rejects the promise when the script fails loading', async () => {
- mockDOMHeadAppendChild.mockImplementationOnce((script) => {
- script.onerror(new Error('hello error'));
- });
- await expect(gapiLoader()).rejects.toThrow('hello error');
- expect(mockDOMHeadAppendChild).toHaveBeenCalled();
- });
-});
diff --git a/spec/frontend/create_cluster/gke_cluster/helpers.js b/spec/frontend/create_cluster/gke_cluster/helpers.js
deleted file mode 100644
index 026e99fa8f4..00000000000
--- a/spec/frontend/create_cluster/gke_cluster/helpers.js
+++ /dev/null
@@ -1,64 +0,0 @@
-import {
- gapiProjectsResponseMock,
- gapiZonesResponseMock,
- gapiMachineTypesResponseMock,
-} from './mock_data';
-
-const cloudbilling = {
- projects: {
- getBillingInfo: jest.fn(
- () =>
- new Promise((resolve) => {
- resolve({
- result: { billingEnabled: true },
- });
- }),
- ),
- },
-};
-
-const cloudresourcemanager = {
- projects: {
- list: jest.fn(
- () =>
- new Promise((resolve) => {
- resolve({
- result: { ...gapiProjectsResponseMock },
- });
- }),
- ),
- },
-};
-
-const compute = {
- zones: {
- list: jest.fn(
- () =>
- new Promise((resolve) => {
- resolve({
- result: { ...gapiZonesResponseMock },
- });
- }),
- ),
- },
- machineTypes: {
- list: jest.fn(
- () =>
- new Promise((resolve) => {
- resolve({
- result: { ...gapiMachineTypesResponseMock },
- });
- }),
- ),
- },
-};
-
-const gapi = {
- client: {
- cloudbilling,
- cloudresourcemanager,
- compute,
- },
-};
-
-export { gapi as default };
diff --git a/spec/frontend/create_cluster/gke_cluster/mock_data.js b/spec/frontend/create_cluster/gke_cluster/mock_data.js
deleted file mode 100644
index d9f5dbc636f..00000000000
--- a/spec/frontend/create_cluster/gke_cluster/mock_data.js
+++ /dev/null
@@ -1,75 +0,0 @@
-export const emptyProjectMock = {
- projectId: '',
- name: '',
-};
-
-export const selectedProjectMock = {
- projectId: 'gcp-project-123',
- name: 'gcp-project',
-};
-
-export const selectedZoneMock = 'us-central1-a';
-
-export const selectedMachineTypeMock = 'n1-standard-2';
-
-export const gapiProjectsResponseMock = {
- projects: [
- {
- projectNumber: '1234',
- projectId: 'gcp-project-123',
- lifecycleState: 'ACTIVE',
- name: 'gcp-project',
- createTime: '2017-12-16T01:48:29.129Z',
- parent: {
- type: 'organization',
- id: '12345',
- },
- },
- ],
-};
-
-export const gapiZonesResponseMock = {
- kind: 'compute#zoneList',
- id: 'projects/gitlab-internal-153318/zones',
- items: [
- {
- kind: 'compute#zone',
- id: '2000',
- creationTimestamp: '1969-12-31T16:00:00.000-08:00',
- name: 'us-central1-a',
- description: 'us-central1-a',
- status: 'UP',
- region:
- 'https://www.googleapis.com/compute/v1/projects/gitlab-internal-153318/regions/us-central1',
- selfLink:
- 'https://www.googleapis.com/compute/v1/projects/gitlab-internal-153318/zones/us-central1-a',
- availableCpuPlatforms: ['Intel Skylake', 'Intel Broadwell', 'Intel Sandy Bridge'],
- },
- ],
- selfLink: 'https://www.googleapis.com/compute/v1/projects/gitlab-internal-153318/zones',
-};
-
-export const gapiMachineTypesResponseMock = {
- kind: 'compute#machineTypeList',
- id: 'projects/gitlab-internal-153318/zones/us-central1-a/machineTypes',
- items: [
- {
- kind: 'compute#machineType',
- id: '3002',
- creationTimestamp: '1969-12-31T16:00:00.000-08:00',
- name: 'n1-standard-2',
- description: '2 vCPUs, 7.5 GB RAM',
- guestCpus: 2,
- memoryMb: 7680,
- imageSpaceGb: 10,
- maximumPersistentDisks: 64,
- maximumPersistentDisksSizeGb: '65536',
- zone: 'us-central1-a',
- selfLink:
- 'https://www.googleapis.com/compute/v1/projects/gitlab-internal-153318/zones/us-central1-a/machineTypes/n1-standard-2',
- isSharedCpu: false,
- },
- ],
- selfLink:
- 'https://www.googleapis.com/compute/v1/projects/gitlab-internal-153318/zones/us-central1-a/machineTypes',
-};
diff --git a/spec/frontend/create_cluster/gke_cluster/stores/actions_spec.js b/spec/frontend/create_cluster/gke_cluster/stores/actions_spec.js
deleted file mode 100644
index c365cb6a9f4..00000000000
--- a/spec/frontend/create_cluster/gke_cluster/stores/actions_spec.js
+++ /dev/null
@@ -1,141 +0,0 @@
-import testAction from 'helpers/vuex_action_helper';
-import * as actions from '~/create_cluster/gke_cluster/store/actions';
-import * as types from '~/create_cluster/gke_cluster/store/mutation_types';
-import createState from '~/create_cluster/gke_cluster/store/state';
-import gapi from '../helpers';
-import {
- selectedProjectMock,
- selectedZoneMock,
- selectedMachineTypeMock,
- gapiProjectsResponseMock,
- gapiZonesResponseMock,
- gapiMachineTypesResponseMock,
-} from '../mock_data';
-
-describe('GCP Cluster Dropdown Store Actions', () => {
- describe('setProject', () => {
- it('should set project', () => {
- return testAction(
- actions.setProject,
- selectedProjectMock,
- { selectedProject: {} },
- [{ type: 'SET_PROJECT', payload: selectedProjectMock }],
- [],
- );
- });
- });
-
- describe('setZone', () => {
- it('should set zone', () => {
- return testAction(
- actions.setZone,
- selectedZoneMock,
- { selectedZone: '' },
- [{ type: 'SET_ZONE', payload: selectedZoneMock }],
- [],
- );
- });
- });
-
- describe('setMachineType', () => {
- it('should set machine type', () => {
- return testAction(
- actions.setMachineType,
- selectedMachineTypeMock,
- { selectedMachineType: '' },
- [{ type: 'SET_MACHINE_TYPE', payload: selectedMachineTypeMock }],
- [],
- );
- });
- });
-
- describe('setIsValidatingProjectBilling', () => {
- it('should set machine type', () => {
- return testAction(
- actions.setIsValidatingProjectBilling,
- true,
- { isValidatingProjectBilling: null },
- [{ type: 'SET_IS_VALIDATING_PROJECT_BILLING', payload: true }],
- [],
- );
- });
- });
-
- describe('async fetch methods', () => {
- let originalGapi;
-
- beforeAll(() => {
- originalGapi = window.gapi;
- window.gapi = gapi;
- window.gapiPromise = Promise.resolve(gapi);
- });
-
- afterAll(() => {
- window.gapi = originalGapi;
- delete window.gapiPromise;
- });
-
- describe('fetchProjects', () => {
- it('fetches projects from Google API', () => {
- const state = createState();
-
- return testAction(
- actions.fetchProjects,
- null,
- state,
- [{ type: types.SET_PROJECTS, payload: gapiProjectsResponseMock.projects }],
- [],
- );
- });
- });
-
- describe('validateProjectBilling', () => {
- it('checks project billing status from Google API', () => {
- return testAction(
- actions.validateProjectBilling,
- true,
- {
- selectedProject: selectedProjectMock,
- selectedZone: '',
- selectedMachineType: '',
- projectHasBillingEnabled: null,
- },
- [
- { type: 'SET_ZONE', payload: '' },
- { type: 'SET_MACHINE_TYPE', payload: '' },
- { type: 'SET_PROJECT_BILLING_STATUS', payload: true },
- ],
- [{ type: 'setIsValidatingProjectBilling', payload: false }],
- );
- });
- });
-
- describe('fetchZones', () => {
- it('fetches zones from Google API', () => {
- const state = createState();
-
- return testAction(
- actions.fetchZones,
- null,
- state,
- [{ type: types.SET_ZONES, payload: gapiZonesResponseMock.items }],
- [],
- );
- });
- });
-
- describe('fetchMachineTypes', () => {
- it('fetches machine types from Google API', () => {
- const state = createState();
-
- return testAction(
- actions.fetchMachineTypes,
- null,
- state,
- [{ type: types.SET_MACHINE_TYPES, payload: gapiMachineTypesResponseMock.items }],
- [],
- );
- });
- });
- });
-});
diff --git a/spec/frontend/create_cluster/gke_cluster/stores/getters_spec.js b/spec/frontend/create_cluster/gke_cluster/stores/getters_spec.js
deleted file mode 100644
index 39106c3f6ca..00000000000
--- a/spec/frontend/create_cluster/gke_cluster/stores/getters_spec.js
+++ /dev/null
@@ -1,103 +0,0 @@
-import {
- hasProject,
- hasZone,
- hasMachineType,
- hasValidData,
-} from '~/create_cluster/gke_cluster/store/getters';
-import { selectedProjectMock, selectedZoneMock, selectedMachineTypeMock } from '../mock_data';
-
-describe('GCP Cluster Dropdown Store Getters', () => {
- let state;
-
- describe('valid states', () => {
- beforeEach(() => {
- state = {
- projectHasBillingEnabled: true,
- selectedProject: selectedProjectMock,
- selectedZone: selectedZoneMock,
- selectedMachineType: selectedMachineTypeMock,
- };
- });
-
- describe('hasProject', () => {
- it('should return true when project is selected', () => {
- expect(hasProject(state)).toEqual(true);
- });
- });
-
- describe('hasZone', () => {
- it('should return true when zone is selected', () => {
- expect(hasZone(state)).toEqual(true);
- });
- });
-
- describe('hasMachineType', () => {
- it('should return true when machine type is selected', () => {
- expect(hasMachineType(state)).toEqual(true);
- });
- });
-
- describe('hasValidData', () => {
- it('should return true when a project, zone and machine type are selected', () => {
- expect(hasValidData(state, { hasZone: true, hasMachineType: true })).toEqual(true);
- });
- });
- });
-
- describe('invalid states', () => {
- beforeEach(() => {
- state = {
- selectedProject: {
- projectId: '',
- name: '',
- },
- selectedZone: '',
- selectedMachineType: '',
- };
- });
-
- describe('hasProject', () => {
- it('should return false when project is not selected', () => {
- expect(hasProject(state)).toEqual(false);
- });
- });
-
- describe('hasZone', () => {
- it('should return false when zone is not selected', () => {
- expect(hasZone(state)).toEqual(false);
- });
- });
-
- describe('hasMachineType', () => {
- it('should return false when machine type is not selected', () => {
- expect(hasMachineType(state)).toEqual(false);
- });
- });
-
- describe('hasValidData', () => {
- let getters;
-
- beforeEach(() => {
- getters = { hasZone: true, hasMachineType: true };
- });
-
- it('should return false when project is not billable', () => {
- state.projectHasBillingEnabled = false;
-
- expect(hasValidData(state, getters)).toEqual(false);
- });
-
- it('should return false when zone is not selected', () => {
- getters.hasZone = false;
-
- expect(hasValidData(state, getters)).toEqual(false);
- });
-
- it('should return false when machine type is not selected', () => {
- getters.hasMachineType = false;
-
- expect(hasValidData(state, getters)).toEqual(false);
- });
- });
- });
-});
diff --git a/spec/frontend/create_cluster/gke_cluster/stores/mutations_spec.js b/spec/frontend/create_cluster/gke_cluster/stores/mutations_spec.js
deleted file mode 100644
index 4493d49af43..00000000000
--- a/spec/frontend/create_cluster/gke_cluster/stores/mutations_spec.js
+++ /dev/null
@@ -1,32 +0,0 @@
-import * as types from '~/create_cluster/gke_cluster/store/mutation_types';
-import mutations from '~/create_cluster/gke_cluster/store/mutations';
-import createState from '~/create_cluster/gke_cluster/store/state';
-import {
- gapiProjectsResponseMock,
- gapiZonesResponseMock,
- gapiMachineTypesResponseMock,
-} from '../mock_data';
-
-describe('GCP Cluster Dropdown Store Mutations', () => {
- describe.each`
- mutation | stateProperty | mockData
- ${types.SET_PROJECTS} | ${'projects'} | ${gapiProjectsResponseMock.projects}
- ${types.SET_ZONES} | ${'zones'} | ${gapiZonesResponseMock.items}
- ${types.SET_MACHINE_TYPES} | ${'machineTypes'} | ${gapiMachineTypesResponseMock.items}
- ${types.SET_MACHINE_TYPE} | ${'selectedMachineType'} | ${gapiMachineTypesResponseMock.items[0].name}
- ${types.SET_ZONE} | ${'selectedZone'} | ${gapiZonesResponseMock.items[0].name}
- ${types.SET_PROJECT} | ${'selectedProject'} | ${gapiProjectsResponseMock.projects[0]}
- ${types.SET_PROJECT_BILLING_STATUS} | ${'projectHasBillingEnabled'} | ${true}
- ${types.SET_IS_VALIDATING_PROJECT_BILLING} | ${'isValidatingProjectBilling'} | ${true}
- `('$mutation', ({ mutation, stateProperty, mockData }) => {
- it(`should set the mutation payload to the ${stateProperty} state property`, () => {
- const state = createState();
-
- expect(state[stateProperty]).not.toBe(mockData);
-
- mutations[mutation](state, mockData);
-
- expect(state[stateProperty]).toBe(mockData);
- });
- });
-});
diff --git a/spec/frontend/create_cluster/init_create_cluster_spec.js b/spec/frontend/create_cluster/init_create_cluster_spec.js
deleted file mode 100644
index 42d1ceed864..00000000000
--- a/spec/frontend/create_cluster/init_create_cluster_spec.js
+++ /dev/null
@@ -1,77 +0,0 @@
-import initGkeDropdowns from '~/create_cluster/gke_cluster';
-import initGkeNamespace from '~/create_cluster/gke_cluster_namespace';
-import initCreateCluster from '~/create_cluster/init_create_cluster';
-import PersistentUserCallout from '~/persistent_user_callout';
-
-// This import is loaded dynamically in `init_create_cluster`.
-// Let's eager import it here so that the first spec doesn't timeout.
-// https://gitlab.com/gitlab-org/gitlab/issues/118499
-import '~/create_cluster/eks_cluster';
-
-jest.mock('~/create_cluster/gke_cluster', () => jest.fn());
-jest.mock('~/create_cluster/gke_cluster_namespace', () => jest.fn());
-jest.mock('~/persistent_user_callout', () => ({
- factory: jest.fn(),
-}));
-
-describe('initCreateCluster', () => {
- let document;
- let gon;
-
- beforeEach(() => {
- document = {
- body: { dataset: {} },
- querySelector: jest.fn(),
- };
- gon = { features: {} };
- });
-
- afterEach(() => {
- jest.clearAllMocks();
- });
-
- describe.each`
- pageSuffix | page
- ${':clusters:new'} | ${'project:clusters:new'}
- ${':clusters:create_gcp'} | ${'groups:clusters:create_gcp'}
- ${':clusters:create_user'} | ${'admin:clusters:create_user'}
- `('when cluster page ends in $pageSuffix', ({ page }) => {
- beforeEach(() => {
- document.body.dataset = { page };
-
- initCreateCluster(document, gon);
- });
-
- it('initializes create GKE cluster app', () => {
- expect(initGkeDropdowns).toHaveBeenCalled();
- });
-
- it('initializes gcp signup offer banner', () => {
- expect(PersistentUserCallout.factory).toHaveBeenCalled();
- });
- });
-
- describe('when creating a project level cluster', () => {
- it('initializes gke namespace app', () => {
- document.body.dataset.page = 'project:clusters:new';
-
- initCreateCluster(document, gon);
-
- expect(initGkeNamespace).toHaveBeenCalled();
- });
- });
-
- describe.each`
- clusterLevel | page
- ${'group level'} | ${'groups:clusters:new'}
- ${'instance level'} | ${'admin:clusters:create_gcp'}
- `('when creating a $clusterLevel cluster', ({ page }) => {
- it('does not initialize gke namespace app', () => {
- document.body.dataset = { page };
-
- initCreateCluster(document, gon);
-
- expect(initGkeNamespace).not.toHaveBeenCalled();
- });
- });
-});
diff --git a/spec/frontend/create_cluster/store/cluster_dropdown/actions_spec.js b/spec/frontend/create_cluster/store/cluster_dropdown/actions_spec.js
deleted file mode 100644
index c0e8b11cf1e..00000000000
--- a/spec/frontend/create_cluster/store/cluster_dropdown/actions_spec.js
+++ /dev/null
@@ -1,95 +0,0 @@
-import testAction from 'helpers/vuex_action_helper';
-
-import actionsFactory from '~/create_cluster/store/cluster_dropdown/actions';
-import * as types from '~/create_cluster/store/cluster_dropdown/mutation_types';
-import createState from '~/create_cluster/store/cluster_dropdown/state';
-
-describe('Cluster dropdown Store Actions', () => {
- const items = [{ name: 'item 1' }];
- let fetchFn;
- let actions;
-
- beforeEach(() => {
- fetchFn = jest.fn();
- actions = actionsFactory(fetchFn);
- });
-
- describe('fetchItems', () => {
- describe('on success', () => {
- beforeEach(() => {
- fetchFn.mockResolvedValueOnce(items);
- actions = actionsFactory(fetchFn);
- });
-
- it('dispatches success with received items', () =>
- testAction(
- actions.fetchItems,
- null,
- createState(),
- [],
- [
- { type: 'requestItems' },
- {
- type: 'receiveItemsSuccess',
- payload: { items },
- },
- ],
- ));
- });
-
- describe('on failure', () => {
- const error = new Error('Could not fetch items');
-
- beforeEach(() => {
- fetchFn.mockRejectedValueOnce(error);
- });
-
- it('dispatches success with received items', () =>
- testAction(
- actions.fetchItems,
- null,
- createState(),
- [],
- [
- { type: 'requestItems' },
- {
- type: 'receiveItemsError',
- payload: { error },
- },
- ],
- ));
- });
- });
-
- describe('requestItems', () => {
- it(`commits ${types.REQUEST_ITEMS} mutation`, () =>
- testAction(actions.requestItems, null, createState(), [{ type: types.REQUEST_ITEMS }]));
- });
-
- describe('receiveItemsSuccess', () => {
- it(`commits ${types.RECEIVE_ITEMS_SUCCESS} mutation`, () =>
- testAction(actions.receiveItemsSuccess, { items }, createState(), [
- {
- type: types.RECEIVE_ITEMS_SUCCESS,
- payload: {
- items,
- },
- },
- ]));
- });
-
- describe('receiveItemsError', () => {
- it(`commits ${types.RECEIVE_ITEMS_ERROR} mutation`, () => {
- const error = new Error('Error fetching items');
-
- testAction(actions.receiveItemsError, { error }, createState(), [
- {
- type: types.RECEIVE_ITEMS_ERROR,
- payload: {
- error,
- },
- },
- ]);
- });
- });
-});
diff --git a/spec/frontend/create_cluster/store/cluster_dropdown/mutations_spec.js b/spec/frontend/create_cluster/store/cluster_dropdown/mutations_spec.js
deleted file mode 100644
index 197fcfc2600..00000000000
--- a/spec/frontend/create_cluster/store/cluster_dropdown/mutations_spec.js
+++ /dev/null
@@ -1,36 +0,0 @@
-import {
- REQUEST_ITEMS,
- RECEIVE_ITEMS_SUCCESS,
- RECEIVE_ITEMS_ERROR,
-} from '~/create_cluster/store/cluster_dropdown/mutation_types';
-import mutations from '~/create_cluster/store/cluster_dropdown/mutations';
-import createState from '~/create_cluster/store/cluster_dropdown/state';
-
-describe('Cluster dropdown store mutations', () => {
- let state;
- let emptyPayload;
- let items;
- let error;
-
- beforeEach(() => {
- emptyPayload = {};
- items = [{ name: 'item 1' }];
- error = new Error('could not load error');
- state = createState();
- });
-
- it.each`
- mutation | mutatedProperty | payload | expectedValue | expectedValueDescription
- ${REQUEST_ITEMS} | ${'isLoadingItems'} | ${emptyPayload} | ${true} | ${true}
- ${REQUEST_ITEMS} | ${'loadingItemsError'} | ${emptyPayload} | ${null} | ${null}
- ${RECEIVE_ITEMS_SUCCESS} | ${'isLoadingItems'} | ${{ items }} | ${false} | ${false}
- ${RECEIVE_ITEMS_SUCCESS} | ${'items'} | ${{ items }} | ${items} | ${'items payload'}
- ${RECEIVE_ITEMS_ERROR} | ${'isLoadingItems'} | ${{ error }} | ${false} | ${false}
- ${RECEIVE_ITEMS_ERROR} | ${'error'} | ${{ error }} | ${error} | ${'received error object'}
- `(`$mutation sets $mutatedProperty to $expectedValueDescription`, (data) => {
- const { mutation, mutatedProperty, payload, expectedValue } = data;
-
- mutations[mutation](state, payload);
- expect(state[mutatedProperty]).toBe(expectedValue);
- });
-});
diff --git a/spec/frontend/create_item_dropdown_spec.js b/spec/frontend/create_item_dropdown_spec.js
index 143ccb9b930..aea4bc6017d 100644
--- a/spec/frontend/create_item_dropdown_spec.js
+++ b/spec/frontend/create_item_dropdown_spec.js
@@ -1,4 +1,5 @@
import $ from 'jquery';
+import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import CreateItemDropdown from '~/create_item_dropdown';
const DROPDOWN_ITEM_DATA = [
@@ -41,12 +42,13 @@ describe('CreateItemDropdown', () => {
}
beforeEach(() => {
- loadFixtures('static/create_item_dropdown.html');
+ loadHTMLFixture('static/create_item_dropdown.html');
$wrapperEl = $('.js-create-item-dropdown-fixture-root');
});
afterEach(() => {
$wrapperEl.remove();
+ resetHTMLFixture();
});
describe('items', () => {
diff --git a/spec/frontend/crm/contact_form_wrapper_spec.js b/spec/frontend/crm/contact_form_wrapper_spec.js
index 6307889a7aa..5e1743701e4 100644
--- a/spec/frontend/crm/contact_form_wrapper_spec.js
+++ b/spec/frontend/crm/contact_form_wrapper_spec.js
@@ -1,22 +1,23 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import createMockApollo from 'helpers/mock_apollo_helper';
import ContactFormWrapper from '~/crm/contacts/components/contact_form_wrapper.vue';
import ContactForm from '~/crm/components/form.vue';
import getGroupContactsQuery from '~/crm/contacts/components/graphql/get_group_contacts.query.graphql';
import createContactMutation from '~/crm/contacts/components/graphql/create_contact.mutation.graphql';
import updateContactMutation from '~/crm/contacts/components/graphql/update_contact.mutation.graphql';
+import getGroupOrganizationsQuery from '~/crm/organizations/components/graphql/get_group_organizations.query.graphql';
+import { getGroupContactsQueryResponse, getGroupOrganizationsQueryResponse } from './mock_data';
describe('Customer relations contact form wrapper', () => {
+ Vue.use(VueApollo);
let wrapper;
+ let fakeApollo;
const findContactForm = () => wrapper.findComponent(ContactForm);
- const $apollo = {
- queries: {
- contacts: {
- loading: false,
- },
- },
- };
const $route = {
params: {
id: 7,
@@ -33,56 +34,79 @@ describe('Customer relations contact form wrapper', () => {
groupFullPath: 'flightjs',
groupId: 26,
},
- mocks: {
- $apollo,
- $route,
- },
+ apolloProvider: fakeApollo,
+ mocks: { $route },
});
};
+ beforeEach(() => {
+ fakeApollo = createMockApollo([
+ [getGroupContactsQuery, jest.fn().mockResolvedValue(getGroupContactsQueryResponse)],
+ [getGroupOrganizationsQuery, jest.fn().mockResolvedValue(getGroupOrganizationsQueryResponse)],
+ ]);
+ });
+
afterEach(() => {
wrapper.destroy();
+ fakeApollo = null;
});
- describe('in edit mode', () => {
- it('should render contact form with correct props', () => {
- mountComponent({ isEditMode: true });
+ describe.each`
+ mode | title | successMessage | mutation | existingId
+ ${'edit'} | ${'Edit contact'} | ${'Contact has been updated.'} | ${updateContactMutation} | ${contacts[0].id}
+ ${'create'} | ${'New contact'} | ${'Contact has been added.'} | ${createContactMutation} | ${null}
+ `('in $mode mode', ({ mode, title, successMessage, mutation, existingId }) => {
+ beforeEach(() => {
+ const isEditMode = mode === 'edit';
+ mountComponent({ isEditMode });
- const contactForm = findContactForm();
- expect(contactForm.props('fields')).toHaveLength(5);
- expect(contactForm.props('title')).toBe('Edit contact');
- expect(contactForm.props('successMessage')).toBe('Contact has been updated.');
- expect(contactForm.props('mutation')).toBe(updateContactMutation);
- expect(contactForm.props('getQuery')).toMatchObject({
- query: getGroupContactsQuery,
- variables: { groupFullPath: 'flightjs' },
- });
- expect(contactForm.props('getQueryNodePath')).toBe('group.contacts');
- expect(contactForm.props('existingId')).toBe(contacts[0].id);
- expect(contactForm.props('additionalCreateParams')).toMatchObject({
- groupId: 'gid://gitlab/Group/26',
- });
+ return waitForPromises();
+ });
+
+ it('renders correct getQuery prop', () => {
+ expect(findContactForm().props('getQueryNodePath')).toBe('group.contacts');
});
- });
- describe('in create mode', () => {
- it('should render contact form with correct props', () => {
- mountComponent();
+ it('renders correct mutation prop', () => {
+ expect(findContactForm().props('mutation')).toBe(mutation);
+ });
- const contactForm = findContactForm();
- expect(contactForm.props('fields')).toHaveLength(5);
- expect(contactForm.props('title')).toBe('New contact');
- expect(contactForm.props('successMessage')).toBe('Contact has been added.');
- expect(contactForm.props('mutation')).toBe(createContactMutation);
- expect(contactForm.props('getQuery')).toMatchObject({
- query: getGroupContactsQuery,
- variables: { groupFullPath: 'flightjs' },
- });
- expect(contactForm.props('getQueryNodePath')).toBe('group.contacts');
- expect(contactForm.props('existingId')).toBeNull();
- expect(contactForm.props('additionalCreateParams')).toMatchObject({
+ it('renders correct additionalCreateParams prop', () => {
+ expect(findContactForm().props('additionalCreateParams')).toMatchObject({
groupId: 'gid://gitlab/Group/26',
});
});
+
+ it('renders correct existingId prop', () => {
+ expect(findContactForm().props('existingId')).toBe(existingId);
+ });
+
+ it('renders correct fields prop', () => {
+ expect(findContactForm().props('fields')).toEqual([
+ { 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: 'organizationId',
+ label: 'Organization',
+ values: [
+ { text: 'No organization', value: null },
+ { text: 'ABC Company', value: 'gid://gitlab/CustomerRelations::Organization/2' },
+ { text: 'GitLab', value: 'gid://gitlab/CustomerRelations::Organization/3' },
+ { text: 'Test Inc', value: 'gid://gitlab/CustomerRelations::Organization/1' },
+ ],
+ },
+ { name: 'description', label: 'Description' },
+ ]);
+ });
+
+ it('renders correct title prop', () => {
+ expect(findContactForm().props('title')).toBe(title);
+ });
+
+ it('renders correct successMessage prop', () => {
+ expect(findContactForm().props('successMessage')).toBe(successMessage);
+ });
});
});
diff --git a/spec/frontend/crm/contacts_root_spec.js b/spec/frontend/crm/contacts_root_spec.js
index b02d94e9cb1..3a6989a00f1 100644
--- a/spec/frontend/crm/contacts_root_spec.js
+++ b/spec/frontend/crm/contacts_root_spec.js
@@ -105,7 +105,7 @@ describe('Customer relations contacts root app', () => {
const issueLink = findIssuesLinks().at(0);
expect(issueLink.exists()).toBe(true);
- expect(issueLink.attributes('href')).toBe('/issues?scope=all&state=opened&crm_contact_id=16');
+ expect(issueLink.attributes('href')).toBe('/issues?crm_contact_id=16');
});
});
});
diff --git a/spec/frontend/crm/form_spec.js b/spec/frontend/crm/form_spec.js
index 5c349b24ea1..d39f0795f5f 100644
--- a/spec/frontend/crm/form_spec.js
+++ b/spec/frontend/crm/form_spec.js
@@ -1,4 +1,4 @@
-import { GlAlert } from '@gitlab/ui';
+import { GlAlert, GlFormInput, GlFormSelect, GlFormGroup } from '@gitlab/ui';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import VueRouter from 'vue-router';
@@ -100,6 +100,14 @@ describe('Reusable form component', () => {
{ name: 'email', label: 'Email', required: true },
{ name: 'phone', label: 'Phone' },
{ name: 'description', label: 'Description' },
+ {
+ name: 'organizationId',
+ label: 'Organization',
+ values: [
+ { key: 'gid://gitlab/CustomerRelations::Organization/1', value: 'GitLab' },
+ { key: 'gid://gitlab/CustomerRelations::Organization/2', value: 'ABC Corp' },
+ ],
+ },
],
getQuery: {
query: getGroupContactsQuery,
@@ -270,4 +278,51 @@ describe('Reusable form component', () => {
});
},
);
+
+ describe('edit form', () => {
+ beforeEach(() => {
+ mountContactUpdate();
+ });
+
+ it.each`
+ index | id | componentName | value
+ ${0} | ${'firstName'} | ${'GlFormInput'} | ${'Marty'}
+ ${1} | ${'lastName'} | ${'GlFormInput'} | ${'McFly'}
+ ${2} | ${'email'} | ${'GlFormInput'} | ${'example@gitlab.com'}
+ ${4} | ${'description'} | ${'GlFormInput'} | ${undefined}
+ ${3} | ${'phone'} | ${'GlFormInput'} | ${undefined}
+ ${5} | ${'organizationId'} | ${'GlFormSelect'} | ${'gid://gitlab/CustomerRelations::Organization/2'}
+ `(
+ 'should render a $componentName for #$id with the value "$value"',
+ ({ index, id, componentName, value }) => {
+ const component = componentName === 'GlFormInput' ? GlFormInput : GlFormSelect;
+ const findFormGroup = (at) => wrapper.findAllComponents(GlFormGroup).at(at);
+ const findFormElement = () => findFormGroup(index).find(component);
+
+ expect(findFormElement().attributes('id')).toBe(id);
+ expect(findFormElement().attributes('value')).toBe(value);
+ },
+ );
+
+ it('should include updated values in update mutation', () => {
+ wrapper.find('#firstName').vm.$emit('input', 'Michael');
+ wrapper
+ .find('#organizationId')
+ .vm.$emit('input', 'gid://gitlab/CustomerRelations::Organization/1');
+
+ findForm().trigger('submit');
+
+ expect(handler).toHaveBeenCalledWith('updateContact', {
+ input: {
+ description: null,
+ email: 'example@gitlab.com',
+ firstName: 'Michael',
+ id: 'gid://gitlab/CustomerRelations::Contact/12',
+ lastName: 'McFly',
+ organizationId: 'gid://gitlab/CustomerRelations::Organization/1',
+ phone: null,
+ },
+ });
+ });
+ });
});
diff --git a/spec/frontend/crm/organizations_root_spec.js b/spec/frontend/crm/organizations_root_spec.js
index 231208d938e..1780a5945a6 100644
--- a/spec/frontend/crm/organizations_root_spec.js
+++ b/spec/frontend/crm/organizations_root_spec.js
@@ -102,9 +102,7 @@ describe('Customer relations organizations root app', () => {
const issueLink = findIssuesLinks().at(0);
expect(issueLink.exists()).toBe(true);
- expect(issueLink.attributes('href')).toBe(
- '/issues?scope=all&state=opened&crm_organization_id=2',
- );
+ expect(issueLink.attributes('href')).toBe('/issues?crm_organization_id=2');
});
});
});
diff --git a/spec/frontend/cycle_analytics/base_spec.js b/spec/frontend/cycle_analytics/base_spec.js
index bdf35f904ed..7b1ef71da63 100644
--- a/spec/frontend/cycle_analytics/base_spec.js
+++ b/spec/frontend/cycle_analytics/base_spec.js
@@ -143,12 +143,9 @@ describe('Value stream analytics component', () => {
expect(findFilters().props()).toEqual({
groupId,
groupPath,
- canToggleAggregation: false,
endDate: createdBefore,
hasDateRangeFilter: true,
hasProjectFilter: false,
- isAggregationEnabled: false,
- isUpdatingAggregationData: false,
selectedProjects: [],
startDate: createdAfter,
});
diff --git a/spec/frontend/cycle_analytics/mock_data.js b/spec/frontend/cycle_analytics/mock_data.js
index c482bd4e910..1fe1dbbb75c 100644
--- a/spec/frontend/cycle_analytics/mock_data.js
+++ b/spec/frontend/cycle_analytics/mock_data.js
@@ -40,7 +40,7 @@ export const summary = [
{ value: '20', title: 'New Issues' },
{ value: null, title: 'Commits' },
{ value: null, title: 'Deploys' },
- { value: null, title: 'Deployment Frequency', unit: 'per day' },
+ { value: null, title: 'Deployment Frequency', unit: '/day' },
];
export const issueStage = {
@@ -130,7 +130,7 @@ export const convertedData = {
{ value: '20', title: 'New Issues' },
{ value: '-', title: 'Commits' },
{ value: '-', title: 'Deploys' },
- { value: '-', title: 'Deployment Frequency', unit: 'per day' },
+ { value: '-', title: 'Deployment Frequency', unit: '/day' },
],
};
diff --git a/spec/frontend/cycle_analytics/stage_table_spec.js b/spec/frontend/cycle_analytics/stage_table_spec.js
index 107fe5fc865..0d15d67866d 100644
--- a/spec/frontend/cycle_analytics/stage_table_spec.js
+++ b/spec/frontend/cycle_analytics/stage_table_spec.js
@@ -329,7 +329,7 @@ describe('StageTable', () => {
]);
});
- it('with sortDesc=false will toggle the direction field', async () => {
+ it('with sortDesc=false will toggle the direction field', () => {
expect(wrapper.emitted('handleUpdatePagination')).toBeUndefined();
triggerTableSort(false);
diff --git a/spec/frontend/cycle_analytics/value_stream_filters_spec.js b/spec/frontend/cycle_analytics/value_stream_filters_spec.js
index 5a0b046393a..6e96a6d756a 100644
--- a/spec/frontend/cycle_analytics/value_stream_filters_spec.js
+++ b/spec/frontend/cycle_analytics/value_stream_filters_spec.js
@@ -1,5 +1,4 @@
import { shallowMount } from '@vue/test-utils';
-import { GlToggle } from '@gitlab/ui';
import Daterange from '~/analytics/shared/components/daterange.vue';
import ProjectsDropdownFilter from '~/analytics/shared/components/projects_dropdown_filter.vue';
import FilterBar from '~/cycle_analytics/components/filter_bar.vue';
@@ -30,7 +29,6 @@ describe('ValueStreamFilters', () => {
const findProjectsDropdown = () => wrapper.findComponent(ProjectsDropdownFilter);
const findDateRangePicker = () => wrapper.findComponent(Daterange);
const findFilterBar = () => wrapper.findComponent(FilterBar);
- const findAggregationToggle = () => wrapper.findComponent(GlToggle);
beforeEach(() => {
wrapper = createComponent();
@@ -59,10 +57,6 @@ describe('ValueStreamFilters', () => {
expect(findDateRangePicker().exists()).toBe(true);
});
- it('will not render the aggregation toggle', () => {
- expect(findAggregationToggle().exists()).toBe(false);
- });
-
it('will emit `selectProject` when a project is selected', () => {
findProjectsDropdown().vm.$emit('selected');
@@ -94,52 +88,4 @@ describe('ValueStreamFilters', () => {
expect(findProjectsDropdown().exists()).toBe(false);
});
});
-
- describe('canToggleAggregation = true', () => {
- beforeEach(() => {
- wrapper = createComponent({ isAggregationEnabled: false, canToggleAggregation: true });
- });
-
- it('will render the aggregation toggle', () => {
- expect(findAggregationToggle().exists()).toBe(true);
- });
-
- it('will set the aggregation toggle to the `isAggregationEnabled` value', () => {
- expect(findAggregationToggle().props('value')).toBe(false);
-
- wrapper = createComponent({
- isAggregationEnabled: true,
- canToggleAggregation: true,
- });
-
- expect(findAggregationToggle().props('value')).toBe(true);
- });
-
- it('will emit `toggleAggregation` when the toggle is changed', async () => {
- expect(wrapper.emitted('toggleAggregation')).toBeUndefined();
-
- await findAggregationToggle().vm.$emit('change', true);
-
- expect(wrapper.emitted('toggleAggregation')).toHaveLength(1);
- expect(wrapper.emitted('toggleAggregation')).toEqual([[true]]);
- });
- });
-
- describe('isUpdatingAggregationData = true', () => {
- beforeEach(() => {
- wrapper = createComponent({ canToggleAggregation: true, isUpdatingAggregationData: true });
- });
-
- it('will disable the aggregation toggle', () => {
- expect(findAggregationToggle().props('disabled')).toBe(true);
- });
-
- it('will not emit `toggleAggregation` when the toggle is changed', async () => {
- expect(wrapper.emitted('toggleAggregation')).toBeUndefined();
-
- await findAggregationToggle().vm.$emit('change', true);
-
- expect(wrapper.emitted('toggleAggregation')).toBeUndefined();
- });
- });
});
diff --git a/spec/frontend/cycle_analytics/value_stream_metrics_spec.js b/spec/frontend/cycle_analytics/value_stream_metrics_spec.js
index 6199e61df0c..4a3e8146b13 100644
--- a/spec/frontend/cycle_analytics/value_stream_metrics_spec.js
+++ b/spec/frontend/cycle_analytics/value_stream_metrics_spec.js
@@ -1,11 +1,11 @@
import { GlDeprecatedSkeletonLoading as GlSkeletonLoading } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
import metricsData from 'test_fixtures/projects/analytics/value_stream_analytics/summary.json';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import ValueStreamMetrics from '~/analytics/shared/components/value_stream_metrics.vue';
import { METRIC_TYPE_SUMMARY } from '~/api/analytics_api';
-import { METRICS_POPOVER_CONTENT } from '~/analytics/shared/constants';
+import { VSA_METRICS_GROUPS, METRICS_POPOVER_CONTENT } from '~/analytics/shared/constants';
import { prepareTimeMetricsData } from '~/analytics/shared/utils';
import MetricTile from '~/analytics/shared/components/metric_tile.vue';
import createFlash from '~/flash';
@@ -27,7 +27,7 @@ describe('ValueStreamMetrics', () => {
});
const createComponent = (props = {}) => {
- return shallowMount(ValueStreamMetrics, {
+ return shallowMountExtended(ValueStreamMetrics, {
propsData: {
requestPath,
requestParams: {},
@@ -38,6 +38,7 @@ describe('ValueStreamMetrics', () => {
};
const findMetrics = () => wrapper.findAllComponents(MetricTile);
+ const findMetricsGroups = () => wrapper.findAllByTestId('vsa-metrics-group');
const expectToHaveRequest = (fields) => {
expect(mockGetValueStreamSummaryMetrics).toHaveBeenCalledWith({
@@ -63,24 +64,6 @@ describe('ValueStreamMetrics', () => {
expect(wrapper.findComponent(GlSkeletonLoading).exists()).toBe(true);
});
- it('renders hidden MetricTile 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 nextTick();
-
- const components = findMetrics();
-
- expect(components).toHaveLength(metricsData.length);
-
- metricsData.forEach((metric, index) => {
- expect(components.at(index).isVisible()).toBe(false);
- });
- });
-
describe('with data loaded', () => {
beforeEach(async () => {
await waitForPromises();
@@ -160,6 +143,27 @@ describe('ValueStreamMetrics', () => {
});
});
});
+
+ describe('groupBy', () => {
+ beforeEach(async () => {
+ mockGetValueStreamSummaryMetrics = jest.fn().mockResolvedValue({ data: metricsData });
+ wrapper = createComponent({ groupBy: VSA_METRICS_GROUPS });
+ await waitForPromises();
+ });
+
+ it('renders the metrics as separate groups', () => {
+ const groups = findMetricsGroups();
+ expect(groups).toHaveLength(VSA_METRICS_GROUPS.length);
+ });
+
+ it('renders titles for each group', () => {
+ const groups = findMetricsGroups();
+ groups.wrappers.forEach((g, index) => {
+ const { title } = VSA_METRICS_GROUPS[index];
+ expect(g.html()).toContain(title);
+ });
+ });
+ });
});
});
diff --git a/spec/frontend/deprecated_jquery_dropdown_spec.js b/spec/frontend/deprecated_jquery_dropdown_spec.js
index bec91fe5fc5..b18d53b317d 100644
--- a/spec/frontend/deprecated_jquery_dropdown_spec.js
+++ b/spec/frontend/deprecated_jquery_dropdown_spec.js
@@ -2,6 +2,7 @@
import $ from 'jquery';
import mockProjects from 'test_fixtures_static/projects.json';
+import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
import '~/lib/utils/common_utils';
import { visitUrl } from '~/lib/utils/url_utility';
@@ -64,7 +65,7 @@ describe('deprecatedJQueryDropdown', () => {
}
beforeEach(() => {
- loadFixtures('static/deprecated_jquery_dropdown.html');
+ loadHTMLFixture('static/deprecated_jquery_dropdown.html');
test.dropdownContainerElement = $('.dropdown.inline');
test.$dropdownMenuElement = $('.dropdown-menu', test.dropdownContainerElement);
test.projectsData = JSON.parse(JSON.stringify(mockProjects));
@@ -73,6 +74,8 @@ describe('deprecatedJQueryDropdown', () => {
afterEach(() => {
$('body').off('keydown');
test.dropdownContainerElement.off('keyup');
+
+ resetHTMLFixture();
});
it('should open on click', () => {
diff --git a/spec/frontend/design_management/components/design_notes/design_reply_form_spec.js b/spec/frontend/design_management/components/design_notes/design_reply_form_spec.js
index 0cef18c60de..d2d1fe6b2d8 100644
--- a/spec/frontend/design_management/components/design_notes/design_reply_form_spec.js
+++ b/spec/frontend/design_management/components/design_notes/design_reply_form_spec.js
@@ -31,10 +31,6 @@ describe('Design reply form component', () => {
});
}
- beforeEach(() => {
- gon.features = { markdownContinueLists: true };
- });
-
afterEach(() => {
wrapper.destroy();
});
diff --git a/spec/frontend/design_management/components/design_sidebar_spec.js b/spec/frontend/design_management/components/design_sidebar_spec.js
index a818a86bef6..e8426216c1c 100644
--- a/spec/frontend/design_management/components/design_sidebar_spec.js
+++ b/spec/frontend/design_management/components/design_sidebar_spec.js
@@ -1,7 +1,7 @@
import { GlCollapse, GlPopover } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import Cookies from 'js-cookie';
import { nextTick } from 'vue';
+import Cookies from '~/lib/utils/cookies';
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';
diff --git a/spec/frontend/design_management/pages/__snapshots__/index_spec.js.snap b/spec/frontend/design_management/pages/__snapshots__/index_spec.js.snap
index abd455ae750..243cc9d891d 100644
--- a/spec/frontend/design_management/pages/__snapshots__/index_spec.js.snap
+++ b/spec/frontend/design_management/pages/__snapshots__/index_spec.js.snap
@@ -16,6 +16,7 @@ exports[`Design management index page designs renders error 1`] = `
primarybuttontext=""
secondarybuttonlink=""
secondarybuttontext=""
+ showicon="true"
title=""
variant="danger"
>
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 31b3117cb6c..8f12dc8fb06 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
@@ -180,6 +180,7 @@ exports[`Design management design index page with error GlAlert is rendered in c
primarybuttontext=""
secondarybuttonlink=""
secondarybuttontext=""
+ showicon="true"
title=""
variant="danger"
>
diff --git a/spec/frontend/diffs/components/diff_expansion_cell_spec.js b/spec/frontend/diffs/components/diff_expansion_cell_spec.js
index cd472920bb9..bd538996349 100644
--- a/spec/frontend/diffs/components/diff_expansion_cell_spec.js
+++ b/spec/frontend/diffs/components/diff_expansion_cell_spec.js
@@ -20,7 +20,6 @@ function makeLoadMoreLinesPayload({
sinceLine,
toLine,
oldLineNumber,
- diffViewType,
fileHash,
nextLineNumbers = {},
unfold = false,
@@ -28,12 +27,11 @@ function makeLoadMoreLinesPayload({
isExpandDown = false,
}) {
return {
- endpoint: 'contextLinesPath',
+ endpoint: diffFileMockData.context_lines_path,
params: {
since: sinceLine,
to: toLine,
offset: toLine + 1 - oldLineNumber,
- view: diffViewType,
unfold,
bottom,
},
@@ -70,10 +68,11 @@ describe('DiffExpansionCell', () => {
const createComponent = (options = {}) => {
const defaults = {
fileHash: mockFile.file_hash,
- contextLinesPath: 'contextLinesPath',
line: mockLine,
isTop: false,
isBottom: false,
+ file: mockFile,
+ inline: true,
};
const propsData = { ...defaults, ...options };
@@ -124,7 +123,7 @@ describe('DiffExpansionCell', () => {
describe('any row', () => {
[
- { diffViewType: INLINE_DIFF_VIEW_TYPE, lineIndex: 8, file: { parallel_diff_lines: [] } },
+ { diffViewType: INLINE_DIFF_VIEW_TYPE, lineIndex: 8, file: cloneDeep(diffFileMockData) },
].forEach(({ diffViewType, file, lineIndex }) => {
describe(`with diffViewType (${diffViewType})`, () => {
beforeEach(() => {
@@ -140,12 +139,12 @@ describe('DiffExpansionCell', () => {
it('on expand all clicked, dispatch loadMoreLines', () => {
const oldLineNumber = mockLine.meta_data.old_pos;
const newLineNumber = mockLine.meta_data.new_pos;
- const previousIndex = getPreviousLineIndex(diffViewType, mockFile, {
+ const previousIndex = getPreviousLineIndex(mockFile, {
oldLineNumber,
newLineNumber,
});
- const wrapper = createComponent();
+ const wrapper = createComponent({ file });
findExpandAll(wrapper).click();
@@ -156,7 +155,6 @@ describe('DiffExpansionCell', () => {
toLine: newLineNumber - 1,
sinceLine: previousIndex,
oldLineNumber,
- diffViewType,
}),
);
});
@@ -168,7 +166,7 @@ describe('DiffExpansionCell', () => {
const oldLineNumber = mockLine.meta_data.old_pos;
const newLineNumber = mockLine.meta_data.new_pos;
- const wrapper = createComponent();
+ const wrapper = createComponent({ file });
findExpandUp(wrapper).trigger('click');
@@ -196,17 +194,16 @@ describe('DiffExpansionCell', () => {
mockLine.meta_data.old_pos = 200;
mockLine.meta_data.new_pos = 200;
- const wrapper = createComponent();
+ const wrapper = createComponent({ file });
findExpandDown(wrapper).trigger('click');
expect(store.dispatch).toHaveBeenCalledWith('diffs/loadMoreLines', {
- endpoint: 'contextLinesPath',
+ endpoint: diffFileMockData.context_lines_path,
params: {
since: 1,
to: 21, // the load amount, plus 1 line
offset: 0,
- view: diffViewType,
unfold: true,
bottom: true,
},
diff --git a/spec/frontend/diffs/store/actions_spec.js b/spec/frontend/diffs/store/actions_spec.js
index 3b567fbc704..cc595e58dda 100644
--- a/spec/frontend/diffs/store/actions_spec.js
+++ b/spec/frontend/diffs/store/actions_spec.js
@@ -1,5 +1,5 @@
import MockAdapter from 'axios-mock-adapter';
-import Cookies from 'js-cookie';
+import Cookies from '~/lib/utils/cookies';
import { useLocalStorageSpy } from 'helpers/local_storage_helper';
import { TEST_HOST } from 'helpers/test_constants';
import testAction from 'helpers/vuex_action_helper';
diff --git a/spec/frontend/diffs/store/mutations_spec.js b/spec/frontend/diffs/store/mutations_spec.js
index d8611b1ce1b..57e623b843d 100644
--- a/spec/frontend/diffs/store/mutations_spec.js
+++ b/spec/frontend/diffs/store/mutations_spec.js
@@ -131,7 +131,14 @@ describe('DiffsStoreMutations', () => {
const options = {
lineNumbers: { oldLineNumber: 1, newLineNumber: 2 },
contextLines: [
- { old_line: 1, new_line: 1, line_code: 'ff9200_1_1', discussions: [], hasForm: false },
+ {
+ old_line: 1,
+ new_line: 1,
+ line_code: 'ff9200_1_1',
+ discussions: [],
+ hasForm: false,
+ type: 'expanded',
+ },
],
fileHash: 'ff9200',
params: {
diff --git a/spec/frontend/diffs/store/utils_spec.js b/spec/frontend/diffs/store/utils_spec.js
index 03bcaab0d2b..8ae51a58819 100644
--- a/spec/frontend/diffs/store/utils_spec.js
+++ b/spec/frontend/diffs/store/utils_spec.js
@@ -51,21 +51,19 @@ describe('DiffsStoreUtils', () => {
});
describe('getPreviousLineIndex', () => {
- describe(`with diffViewType (inline) in split diffs`, () => {
- let diffFile;
+ let diffFile;
- beforeEach(() => {
- diffFile = { ...clone(diffFileMockData) };
- });
+ beforeEach(() => {
+ diffFile = { ...clone(diffFileMockData) };
+ });
- it('should return the correct previous line number', () => {
- expect(
- utils.getPreviousLineIndex(INLINE_DIFF_VIEW_TYPE, diffFile, {
- oldLineNumber: 3,
- newLineNumber: 5,
- }),
- ).toBe(4);
- });
+ it('should return the correct previous line number', () => {
+ expect(
+ utils.getPreviousLineIndex(diffFile, {
+ oldLineNumber: 3,
+ newLineNumber: 5,
+ }),
+ ).toBe(4);
});
});
diff --git a/spec/frontend/diffs/utils/diff_file_spec.js b/spec/frontend/diffs/utils/diff_file_spec.js
index 3223b6c2dab..778897be3ba 100644
--- a/spec/frontend/diffs/utils/diff_file_spec.js
+++ b/spec/frontend/diffs/utils/diff_file_spec.js
@@ -3,6 +3,7 @@ import {
getShortShaFromFile,
stats,
isNotDiffable,
+ match,
} from '~/diffs/utils/diff_file';
import { diffViewerModes } from '~/ide/constants';
import mockDiffFile from '../mock_data/diff_file';
@@ -149,6 +150,38 @@ describe('diff_file utilities', () => {
expect(preppedFile).not.toHaveProp('id');
});
+
+ it.each`
+ index
+ ${null}
+ ${undefined}
+ ${-1}
+ ${false}
+ ${true}
+ ${'idx'}
+ ${'42'}
+ `('does not set the order property if an invalid index ($index) is provided', ({ index }) => {
+ const preppedFile = prepareRawDiffFile({
+ file: files[0],
+ allFiles: files,
+ index,
+ });
+
+ /* expect.anything() doesn't match null or undefined */
+ expect(preppedFile).toEqual(expect.not.objectContaining({ order: null }));
+ expect(preppedFile).toEqual(expect.not.objectContaining({ order: undefined }));
+ expect(preppedFile).toEqual(expect.not.objectContaining({ order: expect.anything() }));
+ });
+
+ it('sets the provided valid index to the order property', () => {
+ const preppedFile = prepareRawDiffFile({
+ file: files[0],
+ allFiles: files,
+ index: 42,
+ });
+
+ expect(preppedFile).toEqual(expect.objectContaining({ order: 42 }));
+ });
});
describe('getShortShaFromFile', () => {
@@ -230,4 +263,42 @@ describe('diff_file utilities', () => {
expect(isNotDiffable(file)).toBe(false);
});
});
+
+ describe('match', () => {
+ const authorityFileId = '68296a4f-f1c7-445a-bd0e-6e3b02c4eec0';
+ const fih = 'file_identifier_hash';
+ const fihs = 'file identifier hashes';
+ let authorityFile;
+
+ beforeAll(() => {
+ const files = getDiffFiles();
+
+ authorityFile = prepareRawDiffFile({
+ file: files[0],
+ allFiles: files,
+ });
+
+ Object.freeze(authorityFile);
+ });
+
+ describe.each`
+ mode | comparisonFiles | keyName
+ ${'universal'} | ${[{ [fih]: 'ABC1' }, { id: 'foo' }, { id: authorityFileId }]} | ${'ids'}
+ ${'mr'} | ${[{ id: authorityFileId }, { [fih]: 'ABC2' }, { [fih]: 'ABC1' }]} | ${fihs}
+ `('$mode mode', ({ mode, comparisonFiles, keyName }) => {
+ it(`fails to match if files or ${keyName} aren't present`, () => {
+ expect(match({ fileA: authorityFile, fileB: undefined, mode })).toBe(false);
+ expect(match({ fileA: authorityFile, fileB: null, mode })).toBe(false);
+ expect(match({ fileA: authorityFile, fileB: comparisonFiles[0], mode })).toBe(false);
+ });
+
+ it(`fails to match if the ${keyName} aren't the same`, () => {
+ expect(match({ fileA: authorityFile, fileB: comparisonFiles[1], mode })).toBe(false);
+ });
+
+ it(`matches if the ${keyName} are the same`, () => {
+ expect(match({ fileA: authorityFile, fileB: comparisonFiles[2], mode })).toBe(true);
+ });
+ });
+ });
});
diff --git a/spec/frontend/diffs/utils/queue_events_spec.js b/spec/frontend/diffs/utils/queue_events_spec.js
index 007748d8b2c..ad2745f5188 100644
--- a/spec/frontend/diffs/utils/queue_events_spec.js
+++ b/spec/frontend/diffs/utils/queue_events_spec.js
@@ -1,11 +1,15 @@
import api from '~/api';
-import { DEFER_DURATION } from '~/diffs/constants';
+import { DEFER_DURATION, TRACKING_CAP_KEY, TRACKING_CAP_LENGTH } from '~/diffs/constants';
import { queueRedisHllEvents } from '~/diffs/utils/queue_events';
jest.mock('~/api', () => ({
trackRedisHllUserEvent: jest.fn(),
}));
+beforeAll(() => {
+ localStorage.clear();
+});
+
describe('diffs events queue', () => {
describe('queueRedisHllEvents', () => {
it('does not dispatch the event immediately', () => {
@@ -17,6 +21,7 @@ describe('diffs events queue', () => {
queueRedisHllEvents(['know_event']);
jest.advanceTimersByTime(DEFER_DURATION + 1);
expect(api.trackRedisHllUserEvent).toHaveBeenCalled();
+ expect(localStorage.getItem(TRACKING_CAP_KEY)).toBe(null);
});
it('increase defer duration based on the provided events count', () => {
@@ -32,5 +37,35 @@ describe('diffs events queue', () => {
deferDuration *= index + 1;
});
});
+
+ describe('with tracking cap verification', () => {
+ const currentTimestamp = Date.now();
+
+ beforeEach(() => {
+ localStorage.clear();
+ });
+
+ it('dispatches the event if cap value is not found', () => {
+ queueRedisHllEvents(['know_event'], { verifyCap: true });
+ jest.advanceTimersByTime(DEFER_DURATION + 1);
+ expect(api.trackRedisHllUserEvent).toHaveBeenCalled();
+ expect(localStorage.getItem(TRACKING_CAP_KEY)).toBe(currentTimestamp.toString());
+ });
+
+ it('dispatches the event if cap value is less than limit', () => {
+ localStorage.setItem(TRACKING_CAP_KEY, 1);
+ queueRedisHllEvents(['know_event'], { verifyCap: true });
+ jest.advanceTimersByTime(DEFER_DURATION + 1);
+ expect(api.trackRedisHllUserEvent).toHaveBeenCalled();
+ expect(localStorage.getItem(TRACKING_CAP_KEY)).toBe(currentTimestamp.toString());
+ });
+
+ it('does not dispatch the event if cap value is greater than limit', () => {
+ localStorage.setItem(TRACKING_CAP_KEY, currentTimestamp - (TRACKING_CAP_LENGTH + 1));
+ queueRedisHllEvents(['know_event'], { verifyCap: true });
+ jest.advanceTimersByTime(DEFER_DURATION + 1);
+ expect(api.trackRedisHllUserEvent).toHaveBeenCalled();
+ });
+ });
});
});
diff --git a/spec/frontend/dropzone_input_spec.js b/spec/frontend/dropzone_input_spec.js
index 11414e8890d..a633de9ef56 100644
--- a/spec/frontend/dropzone_input_spec.js
+++ b/spec/frontend/dropzone_input_spec.js
@@ -1,6 +1,7 @@
import MockAdapter from 'axios-mock-adapter';
import $ from 'jquery';
import mock from 'xhr-mock';
+import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import waitForPromises from 'helpers/wait_for_promises';
import { TEST_HOST } from 'spec/test_constants';
import PasteMarkdownTable from '~/behaviors/markdown/paste_markdown_table';
@@ -45,7 +46,7 @@ describe('dropzone_input', () => {
};
beforeEach(() => {
- loadFixtures('issues/new-issue.html');
+ loadHTMLFixture('issues/new-issue.html');
form = $('#new_issue');
form.data('uploads-path', TEST_UPLOAD_PATH);
@@ -54,6 +55,8 @@ describe('dropzone_input', () => {
afterEach(() => {
form = null;
+
+ resetHTMLFixture();
});
it('pastes Markdown tables', () => {
diff --git a/spec/frontend/editor/components/helpers.js b/spec/frontend/editor/components/helpers.js
index 3e6cd2a236d..12f90390c18 100644
--- a/spec/frontend/editor/components/helpers.js
+++ b/spec/frontend/editor/components/helpers.js
@@ -1,12 +1,28 @@
import { EDITOR_TOOLBAR_RIGHT_GROUP } from '~/editor/constants';
+import { apolloProvider } from '~/editor/components/source_editor_toolbar_graphql';
+import getToolbarItemsQuery from '~/editor/graphql/get_items.query.graphql';
export const buildButton = (id = 'foo-bar-btn', options = {}) => {
return {
__typename: 'Item',
id,
label: options.label || 'Foo Bar Button',
- icon: options.icon || 'foo-bar',
+ icon: options.icon || 'check',
selected: options.selected || false,
group: options.group || EDITOR_TOOLBAR_RIGHT_GROUP,
+ onClick: options.onClick || (() => {}),
+ category: options.category || 'primary',
+ selectedLabel: options.selectedLabel || 'smth',
};
};
+
+export const warmUpCacheWithItems = (items = []) => {
+ apolloProvider.defaultClient.cache.writeQuery({
+ query: getToolbarItemsQuery,
+ data: {
+ items: {
+ nodes: items,
+ },
+ },
+ });
+};
diff --git a/spec/frontend/editor/components/source_editor_toolbar_button_spec.js b/spec/frontend/editor/components/source_editor_toolbar_button_spec.js
index 5135091af4a..1475d451ab3 100644
--- a/spec/frontend/editor/components/source_editor_toolbar_button_spec.js
+++ b/spec/frontend/editor/components/source_editor_toolbar_button_spec.js
@@ -1,43 +1,26 @@
-import Vue, { nextTick } from 'vue';
-import VueApollo from 'vue-apollo';
+import { nextTick } from 'vue';
import { GlButton } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import createMockApollo from 'helpers/mock_apollo_helper';
import SourceEditorToolbarButton from '~/editor/components/source_editor_toolbar_button.vue';
-import getToolbarItemQuery from '~/editor/graphql/get_item.query.graphql';
-import updateToolbarItemMutation from '~/editor/graphql/update_item.mutation.graphql';
import { buildButton } from './helpers';
-Vue.use(VueApollo);
-
describe('Source Editor Toolbar button', () => {
let wrapper;
- let mockApollo;
const defaultBtn = buildButton();
const findButton = () => wrapper.findComponent(GlButton);
- const createComponentWithApollo = ({ propsData } = {}) => {
- mockApollo = createMockApollo();
- mockApollo.clients.defaultClient.cache.writeQuery({
- query: getToolbarItemQuery,
- variables: { id: defaultBtn.id },
- data: {
- item: {
- ...defaultBtn,
- },
- },
- });
-
+ const createComponent = (props = { button: defaultBtn }) => {
wrapper = shallowMount(SourceEditorToolbarButton, {
- propsData,
- apolloProvider: mockApollo,
+ propsData: {
+ ...props,
+ },
});
};
afterEach(() => {
wrapper.destroy();
- mockApollo = null;
+ wrapper = null;
});
describe('default', () => {
@@ -49,98 +32,51 @@ describe('Source Editor Toolbar button', () => {
category: 'secondary',
variant: 'info',
};
+
+ it('does not render the button if the props have not been passed', () => {
+ createComponent({});
+ expect(findButton().vm).toBeUndefined();
+ });
+
it('renders a default button without props', async () => {
- createComponentWithApollo();
+ createComponent();
const btn = findButton();
expect(btn.exists()).toBe(true);
expect(btn.props()).toMatchObject(defaultProps);
});
it('renders a button based on the props passed', async () => {
- createComponentWithApollo({
- propsData: {
- button: customProps,
- },
+ createComponent({
+ button: customProps,
});
const btn = findButton();
expect(btn.props()).toMatchObject(customProps);
});
});
- describe('button updates', () => {
- it('it properly updates button on Apollo cache update', async () => {
- const { id } = defaultBtn;
-
- createComponentWithApollo({
- propsData: {
- button: {
- id,
- },
- },
- });
-
- expect(findButton().props('selected')).toBe(false);
-
- mockApollo.clients.defaultClient.cache.writeQuery({
- query: getToolbarItemQuery,
- variables: { id },
- data: {
- item: {
- ...defaultBtn,
- selected: true,
- },
- },
- });
-
- jest.runOnlyPendingTimers();
- await nextTick();
-
- expect(findButton().props('selected')).toBe(true);
- });
- });
-
describe('click handler', () => {
- it('fires the click handler on the button when available', () => {
+ it('fires the click handler on the button when available', async () => {
const spy = jest.fn();
- createComponentWithApollo({
- propsData: {
- button: {
- onClick: spy,
- },
+ createComponent({
+ button: {
+ onClick: spy,
},
});
expect(spy).not.toHaveBeenCalled();
findButton().vm.$emit('click');
+
+ await nextTick();
expect(spy).toHaveBeenCalled();
});
- it('emits the "click" event', () => {
- createComponentWithApollo();
+ it('emits the "click" event', async () => {
+ createComponent();
jest.spyOn(wrapper.vm, '$emit');
expect(wrapper.vm.$emit).not.toHaveBeenCalled();
+
findButton().vm.$emit('click');
+ await nextTick();
+
expect(wrapper.vm.$emit).toHaveBeenCalledWith('click');
});
- it('triggers the mutation exposing the changed "selected" prop', () => {
- const { id } = defaultBtn;
- createComponentWithApollo({
- propsData: {
- button: {
- id,
- },
- },
- });
- jest.spyOn(wrapper.vm.$apollo, 'mutate');
- expect(wrapper.vm.$apollo.mutate).not.toHaveBeenCalled();
- findButton().vm.$emit('click');
- expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({
- mutation: updateToolbarItemMutation,
- variables: {
- id,
- propsToUpdate: {
- selected: true,
- },
- },
- });
- });
});
});
diff --git a/spec/frontend/editor/components/source_editor_toolbar_graphql_spec.js b/spec/frontend/editor/components/source_editor_toolbar_graphql_spec.js
new file mode 100644
index 00000000000..41c48aa0a58
--- /dev/null
+++ b/spec/frontend/editor/components/source_editor_toolbar_graphql_spec.js
@@ -0,0 +1,112 @@
+import { apolloProvider } from '~/editor/components/source_editor_toolbar_graphql';
+import getToolbarItemsQuery from '~/editor/graphql/get_items.query.graphql';
+import removeItemsMutation from '~/editor/graphql/remove_items.mutation.graphql';
+import updateToolbarItemMutation from '~/editor/graphql/update_item.mutation.graphql';
+import addToolbarItemsMutation from '~/editor/graphql/add_items.mutation.graphql';
+import { buildButton, warmUpCacheWithItems } from './helpers';
+
+describe('Source Editor toolbar Apollo client', () => {
+ const item1 = buildButton('foo');
+ const item2 = buildButton('bar');
+
+ const getItems = () =>
+ apolloProvider.defaultClient.cache.readQuery({ query: getToolbarItemsQuery })?.items?.nodes ||
+ [];
+ const getItem = (id) => {
+ return getItems().find((item) => item.id === id);
+ };
+
+ afterEach(() => {
+ apolloProvider.defaultClient.clearStore();
+ });
+
+ describe('Mutations', () => {
+ describe('addToolbarItems', () => {
+ function addButtons(items) {
+ return apolloProvider.defaultClient.mutate({
+ mutation: addToolbarItemsMutation,
+ variables: {
+ items,
+ },
+ });
+ }
+ it.each`
+ cache | idsToAdd | itemsToAdd | expectedResult | comment
+ ${[]} | ${'empty array'} | ${[]} | ${[]} | ${''}
+ ${[]} | ${'undefined'} | ${undefined} | ${[]} | ${''}
+ ${[]} | ${item2.id} | ${[item2]} | ${[item2]} | ${''}
+ ${[]} | ${item1.id} | ${[item1]} | ${[item1]} | ${''}
+ ${[]} | ${[item1.id, item2.id]} | ${[item1, item2]} | ${[item1, item2]} | ${''}
+ ${[]} | ${[item1.id]} | ${item1} | ${[item1]} | ${'does not fail if the item is an Object'}
+ ${[item2]} | ${[item1.id]} | ${item1} | ${[item2, item1]} | ${'does not fail if the item is an Object'}
+ ${[item1]} | ${[item2.id]} | ${[item2]} | ${[item1, item2]} | ${'correctly adds items to the pre-populated cache'}
+ `('adds $idsToAdd item(s) to $cache', async ({ cache, itemsToAdd, expectedResult }) => {
+ await warmUpCacheWithItems(cache);
+ await addButtons(itemsToAdd);
+ await expect(getItems()).toEqual(expectedResult);
+ });
+ });
+
+ describe('removeToolbarItems', () => {
+ function removeButtons(ids) {
+ return apolloProvider.defaultClient.mutate({
+ mutation: removeItemsMutation,
+ variables: {
+ ids,
+ },
+ });
+ }
+
+ it.each`
+ cache | cacheIds | toRemove | expected
+ ${[item1, item2]} | ${[item1.id, item2.id]} | ${[item1.id]} | ${[item2]}
+ ${[item1, item2]} | ${[item1.id, item2.id]} | ${[item2.id]} | ${[item1]}
+ ${[item1, item2]} | ${[item1.id, item2.id]} | ${[item1.id, item2.id]} | ${[]}
+ ${[item1]} | ${[item1.id]} | ${[item1.id]} | ${[]}
+ ${[item2]} | ${[item2.id]} | ${[]} | ${[item2]}
+ ${[]} | ${['undefined']} | ${[item1.id]} | ${[]}
+ ${[item1]} | ${[item1.id]} | ${[item2.id]} | ${[item1]}
+ `('removes $toRemove from the $cacheIds toolbar', async ({ cache, toRemove, expected }) => {
+ await warmUpCacheWithItems(cache);
+
+ expect(getItems()).toHaveLength(cache.length);
+
+ await removeButtons(toRemove);
+
+ expect(getItems()).toHaveLength(expected.length);
+ expect(getItems()).toEqual(expected);
+ });
+ });
+
+ describe('updateToolbarItem', () => {
+ function mutateButton(item, propsToUpdate = {}) {
+ return apolloProvider.defaultClient.mutate({
+ mutation: updateToolbarItemMutation,
+ variables: {
+ id: item.id,
+ propsToUpdate,
+ },
+ });
+ }
+
+ beforeEach(() => {
+ warmUpCacheWithItems([item1, item2]);
+ });
+
+ it('updates the toolbar items', async () => {
+ expect(getItem(item1.id).selected).toBe(false);
+ expect(getItem(item2.id).selected).toBe(false);
+
+ await mutateButton(item1, { selected: true });
+
+ expect(getItem(item1.id).selected).toBe(true);
+ expect(getItem(item2.id).selected).toBe(false);
+
+ await mutateButton(item2, { selected: true });
+
+ expect(getItem(item1.id).selected).toBe(true);
+ expect(getItem(item2.id).selected).toBe(true);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/editor/extensions/source_editor_toolbar_ext_spec.js b/spec/frontend/editor/extensions/source_editor_toolbar_ext_spec.js
new file mode 100644
index 00000000000..fa5a3b2987e
--- /dev/null
+++ b/spec/frontend/editor/extensions/source_editor_toolbar_ext_spec.js
@@ -0,0 +1,156 @@
+import Vue from 'vue';
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
+import SourceEditorToolbar from '~/editor/components/source_editor_toolbar.vue';
+import { ToolbarExtension } from '~/editor/extensions/source_editor_toolbar_ext';
+import EditorInstance from '~/editor/source_editor_instance';
+import { apolloProvider } from '~/editor/components/source_editor_toolbar_graphql';
+import { buildButton, warmUpCacheWithItems } from '../components/helpers';
+
+describe('Source Editor Toolbar Extension', () => {
+ let instance;
+
+ const createInstance = (baseInstance = {}) => {
+ return new EditorInstance(baseInstance);
+ };
+ const getDefaultEl = () => document.getElementById('editor-toolbar');
+ const getCustomEl = () => document.getElementById('custom-toolbar');
+ const item1 = buildButton('foo');
+ const item2 = buildButton('bar');
+
+ beforeEach(() => {
+ setHTMLFixture('<div id="editor-toolbar"></div><div id="custom-toolbar"></div>');
+ });
+
+ afterEach(() => {
+ apolloProvider.defaultClient.clearStore();
+ resetHTMLFixture();
+ });
+
+ describe('onSetup', () => {
+ beforeEach(() => {
+ instance = createInstance();
+ });
+
+ it.each`
+ id | type | prefix | expectedElFn
+ ${undefined} | ${'default'} | ${'Sets up'} | ${getDefaultEl}
+ ${'custom-toolbar'} | ${'custom'} | ${'Sets up'} | ${getCustomEl}
+ ${'non-existing'} | ${'default'} | ${'Does not set up'} | ${getDefaultEl}
+ `('Sets up the Vue application on $type node when node is $id', ({ id, expectedElFn }) => {
+ jest.spyOn(Vue, 'extend');
+ jest.spyOn(ToolbarExtension, 'setupVue');
+
+ const el = document.getElementById(id);
+ const expectedEl = expectedElFn();
+
+ instance.use({ definition: ToolbarExtension, setupOptions: { el } });
+
+ if (expectedEl) {
+ expect(ToolbarExtension.setupVue).toHaveBeenCalledWith(expectedEl);
+ expect(Vue.extend).toHaveBeenCalledWith(SourceEditorToolbar);
+ } else {
+ expect(ToolbarExtension.setupVue).not.toHaveBeenCalled();
+ }
+ });
+ });
+
+ describe('public API', () => {
+ beforeEach(async () => {
+ await warmUpCacheWithItems();
+ instance = createInstance();
+ instance.use({ definition: ToolbarExtension });
+ });
+
+ describe('getAllItems', () => {
+ it('returns the list of all toolbar items', async () => {
+ await expect(instance.toolbar.getAllItems()).toEqual([]);
+ await warmUpCacheWithItems([item1, item2]);
+ await expect(instance.toolbar.getAllItems()).toEqual([item1, item2]);
+ });
+ });
+
+ describe('getItem', () => {
+ it('returns a toolbar item by id', async () => {
+ await expect(instance.toolbar.getItem(item1.id)).toEqual(undefined);
+ await warmUpCacheWithItems([item1]);
+ await expect(instance.toolbar.getItem(item1.id)).toEqual(item1);
+ });
+ });
+
+ describe('addItems', () => {
+ it.each`
+ idsToAdd | itemsToAdd | expectedResult
+ ${'empty array'} | ${[]} | ${[]}
+ ${'undefined'} | ${undefined} | ${[]}
+ ${item2.id} | ${[item2]} | ${[item2]}
+ ${item1.id} | ${[item1]} | ${[item1]}
+ ${[item1.id, item2.id]} | ${[item1, item2]} | ${[item1, item2]}
+ `('adds $idsToAdd item(s) to cache', async ({ itemsToAdd, expectedResult }) => {
+ await instance.toolbar.addItems(itemsToAdd);
+ await expect(instance.toolbar.getAllItems()).toEqual(expectedResult);
+ });
+
+ it('correctly adds items to the pre-populated cache', async () => {
+ await warmUpCacheWithItems([item1]);
+ await instance.toolbar.addItems([item2]);
+ await expect(instance.toolbar.getAllItems()).toEqual([item1, item2]);
+ });
+
+ it('does not fail if the item is an Object', async () => {
+ await instance.toolbar.addItems(item1);
+ await expect(instance.toolbar.getAllItems()).toEqual([item1]);
+ });
+ });
+
+ describe('removeItems', () => {
+ beforeEach(async () => {
+ await warmUpCacheWithItems([item1, item2]);
+ });
+
+ it.each`
+ idsToRemove | expectedResult
+ ${undefined} | ${[item1, item2]}
+ ${[]} | ${[item1, item2]}
+ ${[item1.id]} | ${[item2]}
+ ${[item2.id]} | ${[item1]}
+ ${[item1.id, item2.id]} | ${[]}
+ `(
+ 'successfully removes $idsToRemove from [foo, bar]',
+ async ({ idsToRemove, expectedResult }) => {
+ await instance.toolbar.removeItems(idsToRemove);
+ await expect(instance.toolbar.getAllItems()).toEqual(expectedResult);
+ },
+ );
+ });
+
+ describe('updateItem', () => {
+ const updatedProp = {
+ icon: 'book',
+ };
+
+ beforeEach(async () => {
+ await warmUpCacheWithItems([item1, item2]);
+ });
+
+ it.each`
+ itemsToUpdate | idToUpdate | propsToUpdate | expectedResult
+ ${undefined} | ${'undefined'} | ${undefined} | ${[item1, item2]}
+ ${item2.id} | ${item2.id} | ${undefined} | ${[item1, item2]}
+ ${item2.id} | ${item2.id} | ${{}} | ${[item1, item2]}
+ ${[item1]} | ${item1.id} | ${updatedProp} | ${[{ ...item1, ...updatedProp }, item2]}
+ ${[item2]} | ${item2.id} | ${updatedProp} | ${[item1, { ...item2, ...updatedProp }]}
+ `(
+ 'updates $idToUpdate item in cache with $propsToUpdate',
+ async ({ idToUpdate, propsToUpdate, expectedResult }) => {
+ await instance.toolbar.updateItem(idToUpdate, propsToUpdate);
+ await expect(instance.toolbar.getAllItems()).toEqual(expectedResult);
+ if (propsToUpdate) {
+ await expect(instance.toolbar.getItem(idToUpdate)).toEqual(
+ expect.objectContaining(propsToUpdate),
+ );
+ }
+ },
+ );
+ });
+ });
+});
diff --git a/spec/frontend/editor/schema/ci/json_tests/positive_tests/gitlab-ci.json b/spec/frontend/editor/schema/ci/json_tests/positive_tests/gitlab-ci.json
index 89420bbc35f..666a4852957 100644
--- a/spec/frontend/editor/schema/ci/json_tests/positive_tests/gitlab-ci.json
+++ b/spec/frontend/editor/schema/ci/json_tests/positive_tests/gitlab-ci.json
@@ -97,7 +97,10 @@
"expire_in": "1 week",
"reports": {
"junit": "result.xml",
- "cobertura": "cobertura-coverage.xml",
+ "coverage_report": {
+ "coverage_format": "cobertura",
+ "path": "cobertura-coverage.xml"
+ },
"codequality": "codequality.json",
"sast": "sast.json",
"dependency_scanning": "scan.json",
@@ -147,7 +150,10 @@
"artifacts": {
"reports": {
"junit": ["result.xml"],
- "cobertura": ["cobertura-coverage.xml"],
+ "coverage_report": {
+ "coverage_format": "cobertura",
+ "path": "cobertura-coverage.xml"
+ },
"codequality": ["codequality.json"],
"sast": ["sast.json"],
"dependency_scanning": ["scan.json"],
diff --git a/spec/frontend/editor/source_editor_ci_schema_ext_spec.js b/spec/frontend/editor/source_editor_ci_schema_ext_spec.js
index 2f6d277ca75..9a14e1a55eb 100644
--- a/spec/frontend/editor/source_editor_ci_schema_ext_spec.js
+++ b/spec/frontend/editor/source_editor_ci_schema_ext_spec.js
@@ -1,4 +1,5 @@
import { languages } from 'monaco-editor';
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import { TEST_HOST } from 'helpers/test_constants';
import { CiSchemaExtension } from '~/editor/extensions/source_editor_ci_schema_ext';
import ciSchemaPath from '~/editor/schema/ci.json';
@@ -19,7 +20,7 @@ describe('~/editor/editor_ci_config_ext', () => {
let originalGitlabUrl;
const createMockEditor = ({ blobPath = defaultBlobPath } = {}) => {
- setFixtures('<div id="editor"></div>');
+ setHTMLFixture('<div id="editor"></div>');
editorEl = document.getElementById('editor');
editor = new SourceEditor();
instance = editor.createInstance({
@@ -45,7 +46,9 @@ describe('~/editor/editor_ci_config_ext', () => {
afterEach(() => {
instance.dispose();
+
editorEl.remove();
+ resetHTMLFixture();
});
describe('registerCiSchema', () => {
diff --git a/spec/frontend/editor/source_editor_extension_base_spec.js b/spec/frontend/editor/source_editor_extension_base_spec.js
index 6606557fd1f..eab39ccaba1 100644
--- a/spec/frontend/editor/source_editor_extension_base_spec.js
+++ b/spec/frontend/editor/source_editor_extension_base_spec.js
@@ -1,4 +1,5 @@
import { Range } from 'monaco-editor';
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import { useFakeRequestAnimationFrame } from 'helpers/fake_request_animation_frame';
import setWindowLocation from 'helpers/set_window_location_helper';
import {
@@ -39,12 +40,13 @@ describe('The basis for an Source Editor extension', () => {
};
beforeEach(() => {
- setFixtures(generateLines());
+ setHTMLFixture(generateLines());
event = generateEventMock();
});
afterEach(() => {
jest.clearAllMocks();
+ resetHTMLFixture();
});
describe('onUse callback', () => {
@@ -253,7 +255,7 @@ describe('The basis for an Source Editor extension', () => {
});
it('does not create a link if the event is triggered on a wrong node', () => {
- setFixtures('<div class="wrong-class">3</div>');
+ setHTMLFixture('<div class="wrong-class">3</div>');
SourceEditorExtension.createAnchor = jest.fn();
const wrongEvent = generateEventMock({ el: document.querySelector('.wrong-class') });
diff --git a/spec/frontend/editor/source_editor_markdown_ext_spec.js b/spec/frontend/editor/source_editor_markdown_ext_spec.js
index eecd23bff6e..3e8c287df2f 100644
--- a/spec/frontend/editor/source_editor_markdown_ext_spec.js
+++ b/spec/frontend/editor/source_editor_markdown_ext_spec.js
@@ -1,5 +1,6 @@
import MockAdapter from 'axios-mock-adapter';
import { Range, Position } from 'monaco-editor';
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import { EditorMarkdownExtension } from '~/editor/extensions/source_editor_markdown_ext';
import SourceEditor from '~/editor/source_editor';
import axios from '~/lib/utils/axios_utils';
@@ -27,7 +28,7 @@ describe('Markdown Extension for Source Editor', () => {
beforeEach(() => {
mockAxios = new MockAdapter(axios);
- setFixtures('<div id="editor" data-editor-loading></div>');
+ setHTMLFixture('<div id="editor" data-editor-loading></div>');
editorEl = document.getElementById('editor');
editor = new SourceEditor();
instance = editor.createInstance({
@@ -42,6 +43,8 @@ describe('Markdown Extension for Source Editor', () => {
instance.dispose();
editorEl.remove();
mockAxios.restore();
+
+ resetHTMLFixture();
});
describe('getSelectedText', () => {
diff --git a/spec/frontend/editor/source_editor_markdown_livepreview_ext_spec.js b/spec/frontend/editor/source_editor_markdown_livepreview_ext_spec.js
index c8d016e10ac..1926f3e268e 100644
--- a/spec/frontend/editor/source_editor_markdown_livepreview_ext_spec.js
+++ b/spec/frontend/editor/source_editor_markdown_livepreview_ext_spec.js
@@ -1,5 +1,5 @@
import MockAdapter from 'axios-mock-adapter';
-import { editor as monacoEditor } from 'monaco-editor';
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import waitForPromises from 'helpers/wait_for_promises';
import {
EXTENSION_MARKDOWN_PREVIEW_PANEL_CLASS,
@@ -30,7 +30,6 @@ describe('Markdown Live Preview Extension for Source Editor', () => {
const secondLine = 'multiline';
const thirdLine = 'string with some **markup**';
const text = `${firstLine}\n${secondLine}\n${thirdLine}`;
- const plaintextPath = 'foo.txt';
const markdownPath = 'foo.md';
const responseData = '<div>FooBar</div>';
@@ -41,7 +40,7 @@ describe('Markdown Live Preview Extension for Source Editor', () => {
beforeEach(() => {
mockAxios = new MockAdapter(axios);
- setFixtures('<div id="editor" data-editor-loading></div>');
+ setHTMLFixture('<div id="editor" data-editor-loading></div>');
editorEl = document.getElementById('editor');
editor = new SourceEditor();
instance = editor.createInstance({
@@ -49,6 +48,13 @@ describe('Markdown Live Preview Extension for Source Editor', () => {
blobPath: markdownPath,
blobContent: text,
});
+
+ instance.toolbar = {
+ addItems: jest.fn(),
+ updateItem: jest.fn(),
+ removeItems: jest.fn(),
+ };
+
extension = instance.use({
definition: EditorMarkdownPreviewExtension,
setupOptions: { previewMarkdownPath },
@@ -60,64 +66,20 @@ describe('Markdown Live Preview Extension for Source Editor', () => {
instance.dispose();
editorEl.remove();
mockAxios.restore();
+ resetHTMLFixture();
});
it('sets up the preview on the instance', () => {
expect(instance.markdownPreview).toEqual({
el: undefined,
- action: expect.any(Object),
+ actions: expect.any(Object),
shown: false,
modelChangeListener: undefined,
path: previewMarkdownPath,
+ actionShowPreviewCondition: expect.any(Object),
});
});
- describe('model language changes listener', () => {
- let cleanupSpy;
- let actionSpy;
-
- beforeEach(async () => {
- cleanupSpy = jest.fn();
- actionSpy = jest.fn();
- spyOnApi(extension, {
- cleanup: cleanupSpy,
- setupPreviewAction: actionSpy,
- });
- await togglePreview();
- });
-
- afterEach(() => {
- jest.clearAllMocks();
- });
-
- it('cleans up when switching away from markdown', () => {
- expect(cleanupSpy).not.toHaveBeenCalled();
- expect(actionSpy).not.toHaveBeenCalled();
-
- instance.updateModelLanguage(plaintextPath);
-
- expect(cleanupSpy).toHaveBeenCalled();
- expect(actionSpy).not.toHaveBeenCalled();
- });
-
- it.each`
- oldLanguage | newLanguage | setupCalledTimes
- ${'plaintext'} | ${'markdown'} | ${1}
- ${'markdown'} | ${'markdown'} | ${0}
- ${'markdown'} | ${'plaintext'} | ${0}
- ${'markdown'} | ${undefined} | ${0}
- ${undefined} | ${'markdown'} | ${1}
- `(
- 'correctly handles re-enabling of the action when switching from $oldLanguage to $newLanguage',
- ({ oldLanguage, newLanguage, setupCalledTimes } = {}) => {
- expect(actionSpy).not.toHaveBeenCalled();
- instance.updateModelLanguage(oldLanguage);
- instance.updateModelLanguage(newLanguage);
- expect(actionSpy).toHaveBeenCalledTimes(setupCalledTimes);
- },
- );
- });
-
describe('model change listener', () => {
let cleanupSpy;
let actionSpy;
@@ -142,33 +104,22 @@ describe('Markdown Live Preview Extension for Source Editor', () => {
expect(cleanupSpy).not.toHaveBeenCalled();
expect(actionSpy).not.toHaveBeenCalled();
});
-
- it('cleans up the preview when the model changes', () => {
- instance.setModel(monacoEditor.createModel('foo'));
- expect(cleanupSpy).toHaveBeenCalled();
- });
-
- it.each`
- language | setupCalledTimes
- ${'markdown'} | ${1}
- ${'plaintext'} | ${0}
- ${undefined} | ${0}
- `(
- 'correctly handles actions when the new model is $language',
- ({ language, setupCalledTimes } = {}) => {
- instance.setModel(monacoEditor.createModel('foo', language));
-
- expect(actionSpy).toHaveBeenCalledTimes(setupCalledTimes);
- },
- );
});
- describe('cleanup', () => {
+ describe('onBeforeUnuse', () => {
beforeEach(async () => {
mockAxios.onPost().reply(200, { body: responseData });
await togglePreview();
});
+ it('removes the registered buttons from the toolbar', () => {
+ expect(instance.toolbar.removeItems).not.toHaveBeenCalled();
+ instance.unuse(extension);
+ expect(instance.toolbar.removeItems).toHaveBeenCalledWith([
+ EXTENSION_MARKDOWN_PREVIEW_ACTION_ID,
+ ]);
+ });
+
it('disposes the modelChange listener and does not fetch preview on content changes', () => {
expect(instance.markdownPreview.modelChangeListener).toBeDefined();
const fetchPreviewSpy = jest.fn();
@@ -176,7 +127,7 @@ describe('Markdown Live Preview Extension for Source Editor', () => {
fetchPreview: fetchPreviewSpy,
});
- instance.cleanup();
+ instance.unuse(extension);
instance.setValue('Foo Bar');
jest.advanceTimersByTime(EXTENSION_MARKDOWN_PREVIEW_UPDATE_DELAY);
@@ -186,17 +137,11 @@ describe('Markdown Live Preview Extension for Source Editor', () => {
it('removes the contextual menu action', () => {
expect(instance.getAction(EXTENSION_MARKDOWN_PREVIEW_ACTION_ID)).toBeDefined();
- instance.cleanup();
+ instance.unuse(extension);
expect(instance.getAction(EXTENSION_MARKDOWN_PREVIEW_ACTION_ID)).toBe(null);
});
- it('toggles the `shown` flag', () => {
- expect(instance.markdownPreview.shown).toBe(true);
- instance.cleanup();
- expect(instance.markdownPreview.shown).toBe(false);
- });
-
it('toggles the panel only if the preview is visible', () => {
const { el: previewEl } = instance.markdownPreview;
const parentEl = previewEl.parentElement;
@@ -204,13 +149,13 @@ describe('Markdown Live Preview Extension for Source Editor', () => {
expect(previewEl).toBeVisible();
expect(parentEl.classList.contains(EXTENSION_MARKDOWN_PREVIEW_PANEL_PARENT_CLASS)).toBe(true);
- instance.cleanup();
+ instance.unuse(extension);
expect(previewEl).toBeHidden();
expect(parentEl.classList.contains(EXTENSION_MARKDOWN_PREVIEW_PANEL_PARENT_CLASS)).toBe(
false,
);
- instance.cleanup();
+ instance.unuse(extension);
expect(previewEl).toBeHidden();
expect(parentEl.classList.contains(EXTENSION_MARKDOWN_PREVIEW_PANEL_PARENT_CLASS)).toBe(
false,
@@ -222,12 +167,12 @@ describe('Markdown Live Preview Extension for Source Editor', () => {
expect(instance.markdownPreview.shown).toBe(true);
- instance.cleanup();
+ instance.unuse(extension);
const { width: newWidth } = instance.getLayoutInfo();
expect(newWidth === width / EXTENSION_MARKDOWN_PREVIEW_PANEL_WIDTH).toBe(true);
- instance.cleanup();
+ instance.unuse(extension);
expect(newWidth === width / EXTENSION_MARKDOWN_PREVIEW_PANEL_WIDTH).toBe(true);
});
});
@@ -305,6 +250,12 @@ describe('Markdown Live Preview Extension for Source Editor', () => {
mockAxios.onPost().reply(200, { body: responseData });
});
+ it('toggles the condition to toggle preview/hide actions in the context menu', () => {
+ expect(instance.markdownPreview.actionShowPreviewCondition.get()).toBe(true);
+ instance.togglePreview();
+ expect(instance.markdownPreview.actionShowPreviewCondition.get()).toBe(false);
+ });
+
it('toggles preview flag on instance', () => {
expect(instance.markdownPreview.shown).toBe(false);
diff --git a/spec/frontend/editor/source_editor_spec.js b/spec/frontend/editor/source_editor_spec.js
index 049cab3a83b..b3d914e6755 100644
--- a/spec/frontend/editor/source_editor_spec.js
+++ b/spec/frontend/editor/source_editor_spec.js
@@ -1,4 +1,5 @@
import { editor as monacoEditor, languages as monacoLanguages } from 'monaco-editor';
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import {
SOURCE_EDITOR_INSTANCE_ERROR_NO_EL,
URI_PREFIX,
@@ -33,7 +34,7 @@ describe('Base editor', () => {
const blobGlobalId = 'snippet_777';
beforeEach(() => {
- setFixtures('<div id="editor" data-editor-loading></div>');
+ setHTMLFixture('<div id="editor" data-editor-loading></div>');
editorEl = document.getElementById('editor');
defaultArguments = { el: editorEl, blobPath, blobContent, blobGlobalId };
editor = new SourceEditor();
@@ -45,6 +46,8 @@ describe('Base editor', () => {
monacoEditor.getModels().forEach((model) => {
model.dispose();
});
+
+ resetHTMLFixture();
});
const uriFilePath = joinPaths('/', URI_PREFIX, blobGlobalId, blobPath);
@@ -244,7 +247,7 @@ describe('Base editor', () => {
const readOnlyIndex = '78'; // readOnly option has the internal index of 78 in the editor's options
beforeEach(() => {
- setFixtures('<div id="editor1"></div><div id="editor2"></div>');
+ setHTMLFixture('<div id="editor1"></div><div id="editor2"></div>');
editorEl1 = document.getElementById('editor1');
editorEl2 = document.getElementById('editor2');
inst1Args = {
@@ -262,6 +265,7 @@ describe('Base editor', () => {
afterEach(() => {
editor.dispose();
+ resetHTMLFixture();
});
it('can initialize several instances of the same editor', () => {
diff --git a/spec/frontend/editor/source_editor_yaml_ext_spec.js b/spec/frontend/editor/source_editor_yaml_ext_spec.js
index b603b0e3a98..14ec7f8b93f 100644
--- a/spec/frontend/editor/source_editor_yaml_ext_spec.js
+++ b/spec/frontend/editor/source_editor_yaml_ext_spec.js
@@ -1,4 +1,5 @@
import { Document } from 'yaml';
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import SourceEditor from '~/editor/source_editor';
import { YamlEditorExtension } from '~/editor/extensions/source_editor_yaml_ext';
import { SourceEditorExtension } from '~/editor/extensions/source_editor_extension_base';
@@ -8,7 +9,7 @@ let baseExtension;
let yamlExtension;
const getEditorInstance = (editorInstanceOptions = {}) => {
- setFixtures('<div id="editor"></div>');
+ setHTMLFixture('<div id="editor"></div>');
return new SourceEditor().createInstance({
el: document.getElementById('editor'),
blobPath: '.gitlab-ci.yml',
@@ -18,7 +19,7 @@ const getEditorInstance = (editorInstanceOptions = {}) => {
};
const getEditorInstanceWithExtension = (extensionOptions = {}, editorInstanceOptions = {}) => {
- setFixtures('<div id="editor"></div>');
+ setHTMLFixture('<div id="editor"></div>');
const instance = getEditorInstance(editorInstanceOptions);
[baseExtension, yamlExtension] = instance.use([
{ definition: SourceEditorExtension },
@@ -35,6 +36,10 @@ const getEditorInstanceWithExtension = (extensionOptions = {}, editorInstanceOpt
};
describe('YamlCreatorExtension', () => {
+ afterEach(() => {
+ resetHTMLFixture();
+ });
+
describe('constructor', () => {
it('saves setupOptions options on the extension, but does not expose those to instance', () => {
const highlightPath = 'foo';
diff --git a/spec/frontend/editor/utils_spec.js b/spec/frontend/editor/utils_spec.js
index 97d3e9e081d..e561cad1086 100644
--- a/spec/frontend/editor/utils_spec.js
+++ b/spec/frontend/editor/utils_spec.js
@@ -1,4 +1,5 @@
import { editor as monacoEditor } from 'monaco-editor';
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import * as utils from '~/editor/utils';
import { DEFAULT_THEME } from '~/ide/lib/themes';
@@ -14,10 +15,14 @@ describe('Source Editor utils', () => {
describe('clearDomElement', () => {
beforeEach(() => {
- setFixtures('<div id="foo"><div id="bar">Foo</div></div>');
+ setHTMLFixture('<div id="foo"><div id="bar">Foo</div></div>');
el = document.getElementById('foo');
});
+ afterEach(() => {
+ resetHTMLFixture();
+ });
+
it('removes all child nodes from an element', () => {
expect(el.children.length).toBe(1);
utils.clearDomElement(el);
@@ -68,10 +73,14 @@ describe('Source Editor utils', () => {
beforeEach(() => {
jest.spyOn(monacoEditor, 'colorizeElement').mockImplementation();
jest.spyOn(monacoEditor, 'setTheme').mockImplementation();
- setFixtures('<pre id="foo"></pre>');
+ setHTMLFixture('<pre id="foo"></pre>');
el = document.getElementById('foo');
});
+ afterEach(() => {
+ resetHTMLFixture();
+ });
+
it('colorizes the element and applies the preference theme', () => {
expect(monacoEditor.colorizeElement).not.toHaveBeenCalled();
expect(monacoEditor.setTheme).not.toHaveBeenCalled();
diff --git a/spec/frontend/emoji/components/utils_spec.js b/spec/frontend/emoji/components/utils_spec.js
index 03eeb6b6bf7..56f514ee9a8 100644
--- a/spec/frontend/emoji/components/utils_spec.js
+++ b/spec/frontend/emoji/components/utils_spec.js
@@ -1,7 +1,7 @@
-import Cookies from 'js-cookie';
+import Cookies from '~/lib/utils/cookies';
import { getFrequentlyUsedEmojis, addToFrequentlyUsed } from '~/emoji/components/utils';
-jest.mock('js-cookie');
+jest.mock('~/lib/utils/cookies');
describe('getFrequentlyUsedEmojis', () => {
it('it returns null when no saved emojis set', () => {
diff --git a/spec/frontend/environments/environment_folder_spec.js b/spec/frontend/environments/environment_folder_spec.js
index f2027252f05..37b897bf65d 100644
--- a/spec/frontend/environments/environment_folder_spec.js
+++ b/spec/frontend/environments/environment_folder_spec.js
@@ -15,12 +15,13 @@ Vue.use(VueApollo);
describe('~/environments/components/environments_folder.vue', () => {
let wrapper;
let environmentFolderMock;
+ let intervalMock;
let nestedEnvironment;
const findLink = () => wrapper.findByRole('link', { name: s__('Environments|Show all') });
const createApolloProvider = () => {
- const mockResolvers = { Query: { folder: environmentFolderMock } };
+ const mockResolvers = { Query: { folder: environmentFolderMock, interval: intervalMock } };
return createMockApollo([], mockResolvers);
};
@@ -40,6 +41,8 @@ describe('~/environments/components/environments_folder.vue', () => {
environmentFolderMock = jest.fn();
[nestedEnvironment] = resolvedEnvironmentsApp.environments;
environmentFolderMock.mockReturnValue(resolvedFolder);
+ intervalMock = jest.fn();
+ intervalMock.mockReturnValue(2000);
});
afterEach(() => {
@@ -70,6 +73,8 @@ describe('~/environments/components/environments_folder.vue', () => {
beforeEach(() => {
collapse = wrapper.findComponent(GlCollapse);
icons = wrapper.findAllComponents(GlIcon);
+ jest.spyOn(wrapper.vm.$apollo.queries.folder, 'startPolling');
+ jest.spyOn(wrapper.vm.$apollo.queries.folder, 'stopPolling');
});
it('is collapsed by default', () => {
@@ -93,6 +98,8 @@ describe('~/environments/components/environments_folder.vue', () => {
expect(iconNames).toEqual(['angle-down', 'folder-open']);
expect(folderName.classes('gl-font-weight-bold')).toBe(true);
expect(link.attributes('href')).toBe(nestedEnvironment.latest.folderPath);
+
+ expect(wrapper.vm.$apollo.queries.folder.startPolling).toHaveBeenCalledWith(2000);
});
it('displays all environments when opened', async () => {
@@ -106,6 +113,16 @@ describe('~/environments/components/environments_folder.vue', () => {
.wrappers.map((w) => w.text());
expect(environments).toEqual(expect.arrayContaining(names));
});
+
+ it('stops polling on click', async () => {
+ await button.trigger('click');
+ expect(wrapper.vm.$apollo.queries.folder.startPolling).toHaveBeenCalledWith(2000);
+
+ const collapseButton = wrapper.findByRole('button', { name: __('Collapse') });
+ await collapseButton.trigger('click');
+
+ expect(wrapper.vm.$apollo.queries.folder.stopPolling).toHaveBeenCalled();
+ });
});
});
diff --git a/spec/frontend/filterable_list_spec.js b/spec/frontend/filterable_list_spec.js
index 3fd5d198e3a..e7197ac6dbf 100644
--- a/spec/frontend/filterable_list_spec.js
+++ b/spec/frontend/filterable_list_spec.js
@@ -1,4 +1,4 @@
-import { setHTMLFixture } from 'helpers/fixtures';
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import FilterableList from '~/filterable_list';
describe('FilterableList', () => {
@@ -20,6 +20,10 @@ describe('FilterableList', () => {
List = new FilterableList(form, filter, holder);
});
+ afterEach(() => {
+ resetHTMLFixture();
+ });
+
it('processes input parameters', () => {
expect(List.filterForm).toEqual(form);
expect(List.listFilterElement).toEqual(filter);
diff --git a/spec/frontend/filtered_search/dropdown_user_spec.js b/spec/frontend/filtered_search/dropdown_user_spec.js
index ee0eef6a1b6..26f12673f68 100644
--- a/spec/frontend/filtered_search/dropdown_user_spec.js
+++ b/spec/frontend/filtered_search/dropdown_user_spec.js
@@ -1,3 +1,4 @@
+import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import DropdownUser from '~/filtered_search/dropdown_user';
import DropdownUtils from '~/filtered_search/dropdown_utils';
import FilteredSearchTokenizer from '~/filtered_search/filtered_search_tokenizer';
@@ -80,7 +81,7 @@ describe('Dropdown User', () => {
let authorFilterDropdownElement;
beforeEach(() => {
- loadFixtures(fixtureTemplate);
+ loadHTMLFixture(fixtureTemplate);
authorFilterDropdownElement = document.querySelector('#js-dropdown-author');
const dummyInput = document.createElement('div');
dropdown = new DropdownUser({
@@ -89,6 +90,10 @@ describe('Dropdown User', () => {
});
});
+ afterEach(() => {
+ resetHTMLFixture();
+ });
+
const findCurrentUserElement = () =>
authorFilterDropdownElement.querySelector('.js-current-user');
diff --git a/spec/frontend/filtered_search/dropdown_utils_spec.js b/spec/frontend/filtered_search/dropdown_utils_spec.js
index 4c1e79eba42..2030b45b44c 100644
--- a/spec/frontend/filtered_search/dropdown_utils_spec.js
+++ b/spec/frontend/filtered_search/dropdown_utils_spec.js
@@ -1,3 +1,4 @@
+import { loadHTMLFixture, setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import FilteredSearchSpecHelper from 'helpers/filtered_search_spec_helper';
import DropdownUtils from '~/filtered_search/dropdown_utils';
import FilteredSearchDropdownManager from '~/filtered_search/filtered_search_dropdown_manager';
@@ -43,13 +44,17 @@ describe('Dropdown Utils', () => {
};
beforeEach(() => {
- setFixtures(`
+ setHTMLFixture(`
<input type="text" id="test" />
`);
input = document.getElementById('test');
});
+ afterEach(() => {
+ resetHTMLFixture();
+ });
+
it('should filter without symbol', () => {
input.value = 'roo';
@@ -142,7 +147,7 @@ describe('Dropdown Utils', () => {
let allowedKeys;
beforeEach(() => {
- setFixtures(`
+ setHTMLFixture(`
<ul class="tokens-container">
<li class="input-token">
<input class="filtered-search" type="text" id="test" />
@@ -350,7 +355,7 @@ describe('Dropdown Utils', () => {
let authorToken;
beforeEach(() => {
- loadFixtures(issuableListFixture);
+ loadHTMLFixture(issuableListFixture);
authorToken = FilteredSearchSpecHelper.createFilterVisualToken('author', '=', '@user');
const searchTermToken = FilteredSearchSpecHelper.createSearchVisualToken('search term');
diff --git a/spec/frontend/filtered_search/filtered_search_dropdown_manager_spec.js b/spec/frontend/filtered_search/filtered_search_dropdown_manager_spec.js
index e9ee69ca163..dff6d11a320 100644
--- a/spec/frontend/filtered_search/filtered_search_dropdown_manager_spec.js
+++ b/spec/frontend/filtered_search/filtered_search_dropdown_manager_spec.js
@@ -1,5 +1,6 @@
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import FilteredSearchDropdownManager from '~/filtered_search/filtered_search_dropdown_manager';
describe('Filtered Search Dropdown Manager', () => {
@@ -20,7 +21,7 @@ describe('Filtered Search Dropdown Manager', () => {
}
beforeEach(() => {
- setFixtures(`
+ setHTMLFixture(`
<ul class="tokens-container">
<li class="input-token">
<input class="filtered-search">
@@ -29,6 +30,10 @@ describe('Filtered Search Dropdown Manager', () => {
`);
});
+ afterEach(() => {
+ resetHTMLFixture();
+ });
+
describe('input has no existing value', () => {
it('should add just tokenName', () => {
FilteredSearchDropdownManager.addWordToInput({ tokenName: 'milestone' });
diff --git a/spec/frontend/filtered_search/filtered_search_manager_spec.js b/spec/frontend/filtered_search/filtered_search_manager_spec.js
index 911a507af4c..5e68725c03e 100644
--- a/spec/frontend/filtered_search/filtered_search_manager_spec.js
+++ b/spec/frontend/filtered_search/filtered_search_manager_spec.js
@@ -1,5 +1,5 @@
import FilteredSearchManager from 'ee_else_ce/filtered_search/filtered_search_manager';
-
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import FilteredSearchSpecHelper from 'helpers/filtered_search_spec_helper';
import DropdownUtils from '~/filtered_search/dropdown_utils';
import FilteredSearchDropdownManager from '~/filtered_search/filtered_search_dropdown_manager';
@@ -64,7 +64,7 @@ describe('Filtered Search Manager', () => {
}
beforeEach(() => {
- setFixtures(`
+ setHTMLFixture(`
<div class="filtered-search-box">
<form>
<ul class="tokens-container list-unstyled">
@@ -80,6 +80,10 @@ describe('Filtered Search Manager', () => {
jest.spyOn(FilteredSearchDropdownManager.prototype, 'setDropdown').mockImplementation();
});
+ afterEach(() => {
+ resetHTMLFixture();
+ });
+
const initializeManager = ({ useDefaultState } = {}) => {
jest.spyOn(FilteredSearchManager.prototype, 'loadSearchParamsFromURL').mockImplementation();
jest.spyOn(FilteredSearchManager.prototype, 'tokenChange').mockImplementation();
diff --git a/spec/frontend/filtered_search/filtered_search_visual_tokens_spec.js b/spec/frontend/filtered_search/filtered_search_visual_tokens_spec.js
index c4e125e96da..0e5c94edd05 100644
--- a/spec/frontend/filtered_search/filtered_search_visual_tokens_spec.js
+++ b/spec/frontend/filtered_search/filtered_search_visual_tokens_spec.js
@@ -1,5 +1,6 @@
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import FilteredSearchSpecHelper from 'helpers/filtered_search_spec_helper';
import waitForPromises from 'helpers/wait_for_promises';
import FilteredSearchVisualTokens from '~/filtered_search/filtered_search_visual_tokens';
@@ -24,7 +25,7 @@ describe('Filtered Search Visual Tokens', () => {
mock = new MockAdapter(axios);
mock.onGet().reply(200);
- setFixtures(`
+ setHTMLFixture(`
<ul class="tokens-container">
${FilteredSearchSpecHelper.createInputHTML()}
</ul>
@@ -35,6 +36,10 @@ describe('Filtered Search Visual Tokens', () => {
bugLabelToken = FilteredSearchSpecHelper.createFilterVisualToken('label', '=', '~bug');
});
+ afterEach(() => {
+ resetHTMLFixture();
+ });
+
describe('getLastVisualTokenBeforeInput', () => {
it('returns when there are no visual tokens', () => {
const { lastVisualToken, isLastVisualTokenValid } = subject.getLastVisualTokenBeforeInput();
@@ -241,7 +246,7 @@ describe('Filtered Search Visual Tokens', () => {
let tokenElement;
beforeEach(() => {
- setFixtures(`
+ setHTMLFixture(`
<div class="test-area">
${subject.createVisualTokenElementHTML('custom-token')}
</div>
diff --git a/spec/frontend/filtered_search/visual_token_value_spec.js b/spec/frontend/filtered_search/visual_token_value_spec.js
index bf526a8d371..e52ffa7bd9f 100644
--- a/spec/frontend/filtered_search/visual_token_value_spec.js
+++ b/spec/frontend/filtered_search/visual_token_value_spec.js
@@ -1,5 +1,6 @@
import { escape } from 'lodash';
import labelData from 'test_fixtures/labels/project_labels.json';
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import FilteredSearchSpecHelper from 'helpers/filtered_search_spec_helper';
import { TEST_HOST } from 'helpers/test_constants';
import DropdownUtils from '~/filtered_search/dropdown_utils';
@@ -28,7 +29,7 @@ describe('Filtered Search Visual Tokens', () => {
let bugLabelToken;
beforeEach(() => {
- setFixtures(`
+ setHTMLFixture(`
<ul class="tokens-container">
${FilteredSearchSpecHelper.createInputHTML()}
</ul>
@@ -39,6 +40,10 @@ describe('Filtered Search Visual Tokens', () => {
bugLabelToken = FilteredSearchSpecHelper.createFilterVisualToken('label', '=', '~bug');
});
+ afterEach(() => {
+ resetHTMLFixture();
+ });
+
describe('updateUserTokenAppearance', () => {
let usersCacheSpy;
diff --git a/spec/frontend/fixtures/api_merge_requests.rb b/spec/frontend/fixtures/api_merge_requests.rb
index 47321fbbeaa..75bc8c8df25 100644
--- a/spec/frontend/fixtures/api_merge_requests.rb
+++ b/spec/frontend/fixtures/api_merge_requests.rb
@@ -9,11 +9,13 @@ RSpec.describe API::MergeRequests, '(JavaScript fixtures)', type: :request do
let_it_be(:admin) { create(:admin, name: 'root') }
let_it_be(:namespace) { create(:namespace, name: 'gitlab-test' )}
let_it_be(:project) { create(:project, :repository, namespace: namespace, path: 'lorem-ipsum') }
+ let_it_be(:early_mrs) do
+ 4.times { |i| create(:merge_request, source_project: project, source_branch: "branch-#{i}") }
+ end
+
let_it_be(:mr) { create(:merge_request, source_project: project) }
it 'api/merge_requests/get.json' do
- 4.times { |i| create(:merge_request, source_project: project, source_branch: "branch-#{i}") }
-
get api("/projects/#{project.id}/merge_requests", admin)
expect(response).to be_successful
diff --git a/spec/frontend/fixtures/runner.rb b/spec/frontend/fixtures/runner.rb
index 25049ee4722..e17e73a93c4 100644
--- a/spec/frontend/fixtures/runner.rb
+++ b/spec/frontend/fixtures/runner.rb
@@ -67,7 +67,7 @@ RSpec.describe 'Runner (JavaScript fixtures)' do
end
describe GraphQL::Query, type: :request do
- runner_query = 'details/runner.query.graphql'
+ runner_query = 'show/runner.query.graphql'
let_it_be(:query) do
get_graphql_query_as_string("#{query_path}#{runner_query}")
@@ -91,7 +91,7 @@ RSpec.describe 'Runner (JavaScript fixtures)' do
end
describe GraphQL::Query, type: :request do
- runner_projects_query = 'details/runner_projects.query.graphql'
+ runner_projects_query = 'show/runner_projects.query.graphql'
let_it_be(:query) do
get_graphql_query_as_string("#{query_path}#{runner_projects_query}")
@@ -107,7 +107,23 @@ RSpec.describe 'Runner (JavaScript fixtures)' do
end
describe GraphQL::Query, type: :request do
- runner_jobs_query = 'details/runner_jobs.query.graphql'
+ runner_jobs_query = 'show/runner_jobs.query.graphql'
+
+ let_it_be(:query) do
+ get_graphql_query_as_string("#{query_path}#{runner_jobs_query}")
+ end
+
+ it "#{fixtures_path}#{runner_jobs_query}.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
+
+ describe GraphQL::Query, type: :request do
+ runner_jobs_query = 'edit/runner_form.query.graphql'
let_it_be(:query) do
get_graphql_query_as_string("#{query_path}#{runner_jobs_query}")
diff --git a/spec/frontend/flash_spec.js b/spec/frontend/flash_spec.js
index 942e2c330fa..6cd32ff6b40 100644
--- a/spec/frontend/flash_spec.js
+++ b/spec/frontend/flash_spec.js
@@ -1,5 +1,5 @@
import * as Sentry from '@sentry/browser';
-import { setHTMLFixture } from 'helpers/fixtures';
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import createFlash, {
hideFlash,
addDismissFlashClickListener,
@@ -93,7 +93,7 @@ describe('Flash', () => {
if (alert) {
alert.$destroy();
}
- document.querySelector('.flash-container')?.remove();
+ resetHTMLFixture();
});
it('adds alert element into the document by default', () => {
@@ -330,7 +330,7 @@ describe('Flash', () => {
});
afterEach(() => {
- document.querySelector('.js-content-wrapper').remove();
+ resetHTMLFixture();
});
it('adds flash alert element into the document by default', () => {
diff --git a/spec/frontend/frequent_items/components/app_spec.js b/spec/frontend/frequent_items/components/app_spec.js
index ba989bf53ab..32c66c0d288 100644
--- a/spec/frontend/frequent_items/components/app_spec.js
+++ b/spec/frontend/frequent_items/components/app_spec.js
@@ -6,7 +6,7 @@ import { mountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import App from '~/frequent_items/components/app.vue';
import FrequentItemsList from '~/frequent_items/components/frequent_items_list.vue';
-import { FREQUENT_ITEMS, HOUR_IN_MS } from '~/frequent_items/constants';
+import { FREQUENT_ITEMS, FIFTEEN_MINUTES_IN_MS } from '~/frequent_items/constants';
import eventHub from '~/frequent_items/event_hub';
import { createStore } from '~/frequent_items/store';
import { getTopFrequentItems } from '~/frequent_items/utils';
@@ -200,15 +200,15 @@ describe('Frequent Items App Component', () => {
]);
});
- it('should increase frequency, when created an hour later', () => {
- const hourLater = Date.now() + HOUR_IN_MS + 1;
+ it('should increase frequency, when created 15 minutes later', () => {
+ const fifteenMinutesLater = Date.now() + FIFTEEN_MINUTES_IN_MS + 1;
- jest.spyOn(Date, 'now').mockReturnValue(hourLater);
- createComponent({ currentItem: { ...TEST_PROJECT, lastAccessedOn: hourLater } });
+ jest.spyOn(Date, 'now').mockReturnValue(fifteenMinutesLater);
+ createComponent({ currentItem: { ...TEST_PROJECT, lastAccessedOn: fifteenMinutesLater } });
expect(getStoredProjects()).toEqual([
expect.objectContaining({
- lastAccessedOn: hourLater,
+ lastAccessedOn: fifteenMinutesLater,
frequency: 2,
}),
]);
diff --git a/spec/frontend/frequent_items/utils_spec.js b/spec/frontend/frequent_items/utils_spec.js
index 8c3841558f4..33c655a6ffd 100644
--- a/spec/frontend/frequent_items/utils_spec.js
+++ b/spec/frontend/frequent_items/utils_spec.js
@@ -1,5 +1,5 @@
import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
-import { HOUR_IN_MS, FREQUENT_ITEMS } from '~/frequent_items/constants';
+import { FIFTEEN_MINUTES_IN_MS, FREQUENT_ITEMS } from '~/frequent_items/constants';
import {
isMobile,
getTopFrequentItems,
@@ -67,8 +67,8 @@ describe('Frequent Items utils spec', () => {
describe('updateExistingFrequentItem', () => {
const LAST_ACCESSED = 1497979281815;
- const WITHIN_AN_HOUR = LAST_ACCESSED + HOUR_IN_MS;
- const OVER_AN_HOUR = WITHIN_AN_HOUR + 1;
+ const WITHIN_FIFTEEN_MINUTES = LAST_ACCESSED + FIFTEEN_MINUTES_IN_MS;
+ const OVER_FIFTEEN_MINUTES = WITHIN_FIFTEEN_MINUTES + 1;
const EXISTING_ITEM = Object.freeze({
...mockProject,
frequency: 1,
@@ -76,10 +76,10 @@ describe('Frequent Items utils spec', () => {
});
it.each`
- desc | existingProps | newProps | expected
- ${'updates item if accessed over an hour ago'} | ${{}} | ${{ lastAccessedOn: OVER_AN_HOUR }} | ${{ lastAccessedOn: Date.now(), frequency: 2 }}
- ${'does not update is accessed with an hour'} | ${{}} | ${{ lastAccessedOn: WITHIN_AN_HOUR }} | ${{ lastAccessedOn: EXISTING_ITEM.lastAccessedOn, frequency: 1 }}
- ${'updates if lastAccessedOn not found'} | ${{ lastAccessedOn: undefined }} | ${{ lastAccessedOn: WITHIN_AN_HOUR }} | ${{ lastAccessedOn: Date.now(), frequency: 2 }}
+ desc | existingProps | newProps | expected
+ ${'updates item if accessed over 15 minutes ago'} | ${{}} | ${{ lastAccessedOn: OVER_FIFTEEN_MINUTES }} | ${{ lastAccessedOn: Date.now(), frequency: 2 }}
+ ${'does not update is accessed with 15 minutes'} | ${{}} | ${{ lastAccessedOn: WITHIN_FIFTEEN_MINUTES }} | ${{ lastAccessedOn: EXISTING_ITEM.lastAccessedOn, frequency: 1 }}
+ ${'updates if lastAccessedOn not found'} | ${{ lastAccessedOn: undefined }} | ${{ lastAccessedOn: WITHIN_FIFTEEN_MINUTES }} | ${{ lastAccessedOn: Date.now(), frequency: 2 }}
`('$desc', ({ existingProps, newProps, expected }) => {
const newItem = {
...EXISTING_ITEM,
diff --git a/spec/frontend/gfm_auto_complete_spec.js b/spec/frontend/gfm_auto_complete_spec.js
index 1ab3286fe4c..aa98b2774ea 100644
--- a/spec/frontend/gfm_auto_complete_spec.js
+++ b/spec/frontend/gfm_auto_complete_spec.js
@@ -2,6 +2,7 @@
import MockAdapter from 'axios-mock-adapter';
import $ from 'jquery';
import labelsFixture from 'test_fixtures/autocomplete_sources/labels.json';
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import GfmAutoComplete, { membersBeforeSave, highlighter } from 'ee_else_ce/gfm_auto_complete';
import { initEmojiMock, clearEmojiMock } from 'helpers/emoji';
import '~/lib/utils/jquery_at_who';
@@ -722,7 +723,7 @@ describe('GfmAutoComplete', () => {
let $textarea;
beforeEach(() => {
- setFixtures('<textarea></textarea>');
+ setHTMLFixture('<textarea></textarea>');
autocomplete = new GfmAutoComplete(dataSources);
$textarea = $('textarea');
autocomplete.setup($textarea, { labels: true });
@@ -730,6 +731,7 @@ describe('GfmAutoComplete', () => {
afterEach(() => {
autocomplete.destroy();
+ resetHTMLFixture();
});
const triggerDropdown = (text) => {
diff --git a/spec/frontend/gl_field_errors_spec.js b/spec/frontend/gl_field_errors_spec.js
index ada3b34e6b1..92d04927ee5 100644
--- a/spec/frontend/gl_field_errors_spec.js
+++ b/spec/frontend/gl_field_errors_spec.js
@@ -1,4 +1,5 @@
import $ from 'jquery';
+import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import GlFieldErrors from '~/gl_field_errors';
describe('GL Style Field Errors', () => {
@@ -9,13 +10,17 @@ describe('GL Style Field Errors', () => {
});
beforeEach(() => {
- loadFixtures('static/gl_field_errors.html');
+ loadHTMLFixture('static/gl_field_errors.html');
const $form = $('form.gl-show-field-errors');
testContext.$form = $form;
testContext.fieldErrors = new GlFieldErrors($form);
});
+ afterEach(() => {
+ resetHTMLFixture();
+ });
+
it('should select the correct input elements', () => {
expect(testContext.$form).toBeDefined();
expect(testContext.$form.length).toBe(1);
diff --git a/spec/frontend/google_tag_manager/index_spec.js b/spec/frontend/google_tag_manager/index_spec.js
index de4a57a7319..6412fe8bb33 100644
--- a/spec/frontend/google_tag_manager/index_spec.js
+++ b/spec/frontend/google_tag_manager/index_spec.js
@@ -14,7 +14,7 @@ import {
trackTransaction,
trackAddToCartUsageTab,
} from '~/google_tag_manager';
-import { setHTMLFixture } from 'helpers/fixtures';
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import { logError } from '~/lib/logger';
jest.mock('~/lib/logger');
@@ -216,6 +216,10 @@ describe('~/google_tag_manager/index', () => {
subject();
});
+ afterEach(() => {
+ resetHTMLFixture();
+ });
+
it.each(expectedEvents)('when %p', ({ selector, trigger, expectation }) => {
expect(spy).not.toHaveBeenCalled();
@@ -443,6 +447,8 @@ describe('~/google_tag_manager/index', () => {
expect(spy).not.toHaveBeenCalled();
expect(logError).not.toHaveBeenCalled();
+
+ resetHTMLFixture();
});
});
@@ -468,6 +474,8 @@ describe('~/google_tag_manager/index', () => {
'Unexpected error while pushing to dataLayer',
pushError,
);
+
+ resetHTMLFixture();
});
});
});
diff --git a/spec/frontend/gpg_badges_spec.js b/spec/frontend/gpg_badges_spec.js
index 0bb50fc3e6f..0a1596b492d 100644
--- a/spec/frontend/gpg_badges_spec.js
+++ b/spec/frontend/gpg_badges_spec.js
@@ -1,4 +1,5 @@
import MockAdapter from 'axios-mock-adapter';
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import { TEST_HOST } from 'spec/test_constants';
import GpgBadges from '~/gpg_badges';
import axios from '~/lib/utils/axios_utils';
@@ -18,7 +19,7 @@ describe('GpgBadges', () => {
const dummyUrl = `${TEST_HOST}/dummy/signatures`;
const setForm = ({ utf8 = '✓', search = '' } = {}) => {
- setFixtures(`
+ setHTMLFixture(`
<form
class="commits-search-form js-signature-container" data-signatures-path="${dummyUrl}" action="${dummyUrl}"
method="get">
@@ -38,24 +39,27 @@ describe('GpgBadges', () => {
afterEach(() => {
mock.restore();
+ resetHTMLFixture();
});
it('does not make a request if there is no container element', async () => {
- setFixtures('');
+ setHTMLFixture('');
jest.spyOn(axios, 'get').mockImplementation(() => {});
await GpgBadges.fetch();
expect(axios.get).not.toHaveBeenCalled();
+ resetHTMLFixture();
});
it('throws an error if the endpoint is missing', async () => {
- setFixtures('<div class="js-signature-container"></div>');
+ setHTMLFixture('<div class="js-signature-container"></div>');
jest.spyOn(axios, 'get').mockImplementation(() => {});
await expect(GpgBadges.fetch()).rejects.toEqual(
new Error('Missing commit signatures endpoint!'),
);
expect(axios.get).not.toHaveBeenCalled();
+ resetHTMLFixture();
});
it('fetches commit signatures', async () => {
diff --git a/spec/frontend/group_settings/components/shared_runners_form_spec.js b/spec/frontend/group_settings/components/shared_runners_form_spec.js
index 26e9cd39cfd..70a22c86e62 100644
--- a/spec/frontend/group_settings/components/shared_runners_form_spec.js
+++ b/spec/frontend/group_settings/components/shared_runners_form_spec.js
@@ -1,47 +1,46 @@
-import { GlLoadingIcon, GlAlert } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
+import { GlAlert } from '@gitlab/ui';
import MockAxiosAdapter from 'axios-mock-adapter';
import { nextTick } from 'vue';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import SharedRunnersForm from '~/group_settings/components/shared_runners_form.vue';
import axios from '~/lib/utils/axios_utils';
-const provide = {
- updatePath: '/test/update',
- sharedRunnersAvailability: 'enabled',
- parentSharedRunnersAvailability: null,
- runnerDisabled: 'disabled',
- runnerEnabled: 'enabled',
- runnerAllowOverride: 'allow_override',
-};
-
-jest.mock('~/flash');
+const UPDATE_PATH = '/test/update';
+const RUNNER_ENABLED_VALUE = 'enabled';
+const RUNNER_DISABLED_VALUE = 'disabled_and_unoverridable';
+const RUNNER_ALLOW_OVERRIDE_VALUE = 'disabled_with_override';
describe('group_settings/components/shared_runners_form', () => {
let wrapper;
let mock;
- const createComponent = (provides = {}) => {
- wrapper = shallowMount(SharedRunnersForm, {
+ const createComponent = (provide = {}) => {
+ wrapper = shallowMountExtended(SharedRunnersForm, {
provide: {
+ updatePath: UPDATE_PATH,
+ sharedRunnersSetting: RUNNER_ENABLED_VALUE,
+ parentSharedRunnersSetting: null,
+ runnerEnabledValue: RUNNER_ENABLED_VALUE,
+ runnerDisabledValue: RUNNER_DISABLED_VALUE,
+ runnerAllowOverrideValue: RUNNER_ALLOW_OVERRIDE_VALUE,
...provide,
- ...provides,
},
});
};
- const findLoadingIcon = () => wrapper.find(GlLoadingIcon);
- const findErrorAlert = () => wrapper.find(GlAlert);
- const findEnabledToggle = () => wrapper.find('[data-testid="enable-runners-toggle"]');
- const findOverrideToggle = () => wrapper.find('[data-testid="override-runners-toggle"]');
- const changeToggle = (toggle) => toggle.vm.$emit('change', !toggle.props('value'));
+ const findAlert = (variant) =>
+ wrapper
+ .findAllComponents(GlAlert)
+ .filter((w) => w.props('variant') === variant)
+ .at(0);
+ const findSharedRunnersToggle = () => wrapper.findByTestId('shared-runners-toggle');
+ const findOverrideToggle = () => wrapper.findByTestId('override-runners-toggle');
const getSharedRunnersSetting = () => JSON.parse(mock.history.put[0].data).shared_runners_setting;
- const isLoadingIconVisible = () => findLoadingIcon().exists();
beforeEach(() => {
mock = new MockAxiosAdapter(axios);
-
- mock.onPut(provide.updatePath).reply(200);
+ mock.onPut(UPDATE_PATH).reply(200);
});
afterEach(() => {
@@ -51,102 +50,122 @@ describe('group_settings/components/shared_runners_form', () => {
mock.restore();
});
- describe('with default', () => {
+ describe('default state', () => {
beforeEach(() => {
createComponent();
});
- it('loading icon does not exist', () => {
- expect(isLoadingIconVisible()).toBe(false);
+ it('"Enable shared runners" toggle is enabled', () => {
+ expect(findSharedRunnersToggle().props()).toMatchObject({
+ isLoading: false,
+ disabled: false,
+ });
});
- it('enabled toggle exists', () => {
- expect(findEnabledToggle().exists()).toBe(true);
+ it('"Override the group setting" is disabled', () => {
+ expect(findOverrideToggle().props()).toMatchObject({
+ isLoading: false,
+ disabled: true,
+ });
});
+ });
- it('override toggle does not exist', () => {
- expect(findOverrideToggle().exists()).toBe(false);
+ describe('When group disabled shared runners', () => {
+ it(`toggles are not disabled with setting ${RUNNER_DISABLED_VALUE}`, () => {
+ createComponent({ sharedRunnersSetting: RUNNER_DISABLED_VALUE });
+
+ expect(findSharedRunnersToggle().props('disabled')).toBe(false);
+ expect(findOverrideToggle().props('disabled')).toBe(false);
});
});
- describe('loading icon', () => {
- it('shows and hides the loading icon on request', async () => {
- createComponent();
+ describe('When parent group disabled shared runners', () => {
+ it('toggles are disabled', () => {
+ createComponent({
+ sharedRunnersSetting: RUNNER_DISABLED_VALUE,
+ parentSharedRunnersSetting: RUNNER_DISABLED_VALUE,
+ });
+
+ expect(findSharedRunnersToggle().props('disabled')).toBe(true);
+ expect(findOverrideToggle().props('disabled')).toBe(true);
+ expect(findAlert('warning').exists()).toBe(true);
+ });
+ });
- expect(isLoadingIconVisible()).toBe(false);
+ describe('loading state', () => {
+ beforeEach(() => {
+ createComponent();
+ });
- findEnabledToggle().vm.$emit('change', true);
+ it('is not loading by default', () => {
+ expect(findSharedRunnersToggle().props('isLoading')).toBe(false);
+ expect(findOverrideToggle().props('isLoading')).toBe(false);
+ });
+ it('is loading immediately after request', async () => {
+ findSharedRunnersToggle().vm.$emit('change', true);
await nextTick();
- expect(isLoadingIconVisible()).toBe(true);
+ expect(findSharedRunnersToggle().props('isLoading')).toBe(true);
+ expect(findOverrideToggle().props('isLoading')).toBe(true);
+ });
+
+ it('does not update settings while loading', async () => {
+ findSharedRunnersToggle().vm.$emit('change', true);
+ findSharedRunnersToggle().vm.$emit('change', false);
+ await waitForPromises();
+ expect(mock.history.put.length).toBe(1);
+ });
+
+ it('is not loading state after completed request', async () => {
+ findSharedRunnersToggle().vm.$emit('change', true);
await waitForPromises();
- expect(isLoadingIconVisible()).toBe(false);
+ expect(findSharedRunnersToggle().props('isLoading')).toBe(false);
+ expect(findOverrideToggle().props('isLoading')).toBe(false);
});
});
- describe('enable toggle', () => {
+ describe('"Enable shared runners" toggle', () => {
beforeEach(() => {
createComponent();
});
- it('enabling the toggle sends correct payload', async () => {
- findEnabledToggle().vm.$emit('change', true);
-
+ it('sends correct payload when turned on', async () => {
+ findSharedRunnersToggle().vm.$emit('change', true);
await waitForPromises();
- expect(getSharedRunnersSetting()).toEqual(provide.runnerEnabled);
- expect(findOverrideToggle().exists()).toBe(false);
+ expect(getSharedRunnersSetting()).toEqual(RUNNER_ENABLED_VALUE);
+ expect(findOverrideToggle().props('disabled')).toBe(true);
});
- it('disabling the toggle sends correct payload', async () => {
- findEnabledToggle().vm.$emit('change', false);
-
+ it('sends correct payload when turned off', async () => {
+ findSharedRunnersToggle().vm.$emit('change', false);
await waitForPromises();
- expect(getSharedRunnersSetting()).toEqual(provide.runnerDisabled);
- expect(findOverrideToggle().exists()).toBe(true);
+ expect(getSharedRunnersSetting()).toEqual(RUNNER_DISABLED_VALUE);
+ expect(findOverrideToggle().props('disabled')).toBe(false);
});
});
- describe('override toggle', () => {
+ describe('"Override the group setting" toggle', () => {
beforeEach(() => {
- createComponent({ sharedRunnersAvailability: provide.runnerAllowOverride });
+ createComponent({ sharedRunnersSetting: RUNNER_ALLOW_OVERRIDE_VALUE });
});
it('enabling the override toggle sends correct payload', async () => {
findOverrideToggle().vm.$emit('change', true);
-
await waitForPromises();
- expect(getSharedRunnersSetting()).toEqual(provide.runnerAllowOverride);
+ expect(getSharedRunnersSetting()).toEqual(RUNNER_ALLOW_OVERRIDE_VALUE);
});
it('disabling the override toggle sends correct payload', async () => {
findOverrideToggle().vm.$emit('change', false);
-
await waitForPromises();
- expect(getSharedRunnersSetting()).toEqual(provide.runnerDisabled);
- });
- });
-
- describe('toggle disabled state', () => {
- it(`toggles are not disabled with setting ${provide.runnerDisabled}`, () => {
- createComponent({ sharedRunnersAvailability: provide.runnerDisabled });
- expect(findEnabledToggle().props('disabled')).toBe(false);
- expect(findOverrideToggle().props('disabled')).toBe(false);
- });
-
- it('toggles are disabled', () => {
- createComponent({
- sharedRunnersAvailability: provide.runnerDisabled,
- parentSharedRunnersAvailability: provide.runnerDisabled,
- });
- expect(findEnabledToggle().props('disabled')).toBe(true);
- expect(findOverrideToggle().props('disabled')).toBe(true);
+ expect(getSharedRunnersSetting()).toEqual(RUNNER_DISABLED_VALUE);
});
});
@@ -156,16 +175,16 @@ describe('group_settings/components/shared_runners_form', () => {
${{ error: 'Undefined error' }} | ${'Undefined error Refresh the page and try again.'}
`(`with error $errorObj`, ({ errorObj, message }) => {
beforeEach(async () => {
- mock.onPut(provide.updatePath).reply(500, errorObj);
+ mock.onPut(UPDATE_PATH).reply(500, errorObj);
createComponent();
- changeToggle(findEnabledToggle());
+ findSharedRunnersToggle().vm.$emit('change', false);
await waitForPromises();
});
it('error should be shown', () => {
- expect(findErrorAlert().text()).toBe(message);
+ expect(findAlert('danger').text()).toBe(message);
});
});
});
diff --git a/spec/frontend/groups/landing_spec.js b/spec/frontend/groups/landing_spec.js
index d60adea202b..2c2c19ee0c7 100644
--- a/spec/frontend/groups/landing_spec.js
+++ b/spec/frontend/groups/landing_spec.js
@@ -1,4 +1,4 @@
-import Cookies from 'js-cookie';
+import Cookies from '~/lib/utils/cookies';
import Landing from '~/groups/landing';
describe('Landing', () => {
diff --git a/spec/frontend/header_spec.js b/spec/frontend/header_spec.js
index 937bc9aa478..19849fba63c 100644
--- a/spec/frontend/header_spec.js
+++ b/spec/frontend/header_spec.js
@@ -1,6 +1,7 @@
import $ from 'jquery';
import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
import initTodoToggle, { initNavUserDropdownTracking } from '~/header';
+import { loadHTMLFixture, setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
describe('Header', () => {
describe('Todos notification', () => {
@@ -17,7 +18,11 @@ describe('Header', () => {
beforeEach(() => {
initTodoToggle();
- loadFixtures(fixtureTemplate);
+ loadHTMLFixture(fixtureTemplate);
+ });
+
+ afterEach(() => {
+ resetHTMLFixture();
});
it('should update todos-count after receiving the todo:toggle event', () => {
@@ -57,7 +62,7 @@ describe('Header', () => {
let trackingSpy;
beforeEach(() => {
- setFixtures(`
+ setHTMLFixture(`
<li class="js-nav-user-dropdown">
<a class="js-buy-pipeline-minutes-link" data-track-action="click_buy_ci_minutes" data-track-label="free" data-track-property="user_dropdown">Buy Pipeline minutes</a>
</li>`);
@@ -70,6 +75,7 @@ describe('Header', () => {
afterEach(() => {
unmockTracking();
+ resetHTMLFixture();
});
it('sends a tracking event when the dropdown is opened and contains Buy Pipeline minutes link', () => {
diff --git a/spec/frontend/helpers/startup_css_helper_spec.js b/spec/frontend/helpers/startup_css_helper_spec.js
index 703bdbd342f..2236b5aa261 100644
--- a/spec/frontend/helpers/startup_css_helper_spec.js
+++ b/spec/frontend/helpers/startup_css_helper_spec.js
@@ -1,3 +1,4 @@
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import { waitForCSSLoaded } from '~/helpers/startup_css_helper';
describe('waitForCSSLoaded', () => {
@@ -41,17 +42,19 @@ describe('waitForCSSLoaded', () => {
describe('with startup css enabled', () => {
it('should dispatch CSSLoaded when the assets are cached or already loaded', async () => {
- setFixtures(`
+ setHTMLFixture(`
<link href="one.css" data-startupcss="loaded">
<link href="two.css" data-startupcss="loaded">
`);
await waitForCSSLoaded(mockedCallback);
expect(mockedCallback).toHaveBeenCalledTimes(1);
+
+ resetHTMLFixture();
});
it('should wait to call CssLoaded until the assets are loaded', async () => {
- setFixtures(`
+ setHTMLFixture(`
<link href="one.css" data-startupcss="loading">
<link href="two.css" data-startupcss="loading">
`);
@@ -63,6 +66,8 @@ describe('waitForCSSLoaded', () => {
await events;
expect(mockedCallback).toHaveBeenCalledTimes(1);
+
+ resetHTMLFixture();
});
});
});
diff --git a/spec/frontend/ide/components/commit_sidebar/message_field_spec.js b/spec/frontend/ide/components/commit_sidebar/message_field_spec.js
index e66de6bb0b0..ace266aec5e 100644
--- a/spec/frontend/ide/components/commit_sidebar/message_field_spec.js
+++ b/spec/frontend/ide/components/commit_sidebar/message_field_spec.js
@@ -1,4 +1,5 @@
import Vue, { nextTick } from 'vue';
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import createComponent from 'helpers/vue_mount_component_helper';
import CommitMessageField from '~/ide/components/commit_sidebar/message_field.vue';
@@ -7,7 +8,7 @@ describe('IDE commit message field', () => {
let vm;
beforeEach(() => {
- setFixtures('<div id="app"></div>');
+ setHTMLFixture('<div id="app"></div>');
vm = createComponent(
Component,
@@ -21,6 +22,8 @@ describe('IDE commit message field', () => {
afterEach(() => {
vm.$destroy();
+
+ resetHTMLFixture();
});
it('adds is-focused class on focus', async () => {
diff --git a/spec/frontend/ide/components/new_dropdown/upload_spec.js b/spec/frontend/ide/components/new_dropdown/upload_spec.js
index 5a7419d6dce..3eafe9e7ccb 100644
--- a/spec/frontend/ide/components/new_dropdown/upload_spec.js
+++ b/spec/frontend/ide/components/new_dropdown/upload_spec.js
@@ -70,7 +70,9 @@ describe('new dropdown upload', () => {
});
it('calls readAsText and creates file in plain text (without encoding) if the file content is plain text', async () => {
- const waitForCreate = new Promise((resolve) => vm.$on('create', resolve));
+ const waitForCreate = new Promise((resolve) => {
+ vm.$on('create', resolve);
+ });
vm.createFile(textTarget, textFile);
diff --git a/spec/frontend/image_diff/image_diff_spec.js b/spec/frontend/image_diff/image_diff_spec.js
index 710aa7108a8..f8faa8d78c2 100644
--- a/spec/frontend/image_diff/image_diff_spec.js
+++ b/spec/frontend/image_diff/image_diff_spec.js
@@ -1,3 +1,4 @@
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import { TEST_HOST } from 'helpers/test_constants';
import imageDiffHelper from '~/image_diff/helpers/index';
import ImageDiff from '~/image_diff/image_diff';
@@ -9,7 +10,7 @@ describe('ImageDiff', () => {
let imageDiff;
beforeEach(() => {
- setFixtures(`
+ setHTMLFixture(`
<div id="element">
<div class="diff-file">
<div class="js-image-frame">
@@ -35,6 +36,10 @@ describe('ImageDiff', () => {
element = document.getElementById('element');
});
+ afterEach(() => {
+ resetHTMLFixture();
+ });
+
describe('constructor', () => {
beforeEach(() => {
imageDiff = new ImageDiff(element, {
diff --git a/spec/frontend/image_diff/init_discussion_tab_spec.js b/spec/frontend/image_diff/init_discussion_tab_spec.js
index f6f05037c95..3b427f0d54d 100644
--- a/spec/frontend/image_diff/init_discussion_tab_spec.js
+++ b/spec/frontend/image_diff/init_discussion_tab_spec.js
@@ -1,9 +1,10 @@
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import initImageDiffHelper from '~/image_diff/helpers/init_image_diff';
import initDiscussionTab from '~/image_diff/init_discussion_tab';
describe('initDiscussionTab', () => {
beforeEach(() => {
- setFixtures(`
+ setHTMLFixture(`
<div class="timeline-content">
<div class="diff-file js-image-file"></div>
<div class="diff-file js-image-file"></div>
@@ -11,6 +12,10 @@ describe('initDiscussionTab', () => {
`);
});
+ afterEach(() => {
+ resetHTMLFixture();
+ });
+
it('should pass canCreateNote as false to initImageDiff', () => {
jest
.spyOn(initImageDiffHelper, 'initImageDiff')
diff --git a/spec/frontend/image_diff/replaced_image_diff_spec.js b/spec/frontend/image_diff/replaced_image_diff_spec.js
index 2b401fc46bf..d789e964e4c 100644
--- a/spec/frontend/image_diff/replaced_image_diff_spec.js
+++ b/spec/frontend/image_diff/replaced_image_diff_spec.js
@@ -1,3 +1,4 @@
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import { TEST_HOST } from 'helpers/test_constants';
import imageDiffHelper from '~/image_diff/helpers/index';
import ImageDiff from '~/image_diff/image_diff';
@@ -9,7 +10,7 @@ describe('ReplacedImageDiff', () => {
let replacedImageDiff;
beforeEach(() => {
- setFixtures(`
+ setHTMLFixture(`
<div id="element">
<div class="two-up">
<div class="js-image-frame">
@@ -36,6 +37,10 @@ describe('ReplacedImageDiff', () => {
element = document.getElementById('element');
});
+ afterEach(() => {
+ resetHTMLFixture();
+ });
+
function setupImageFrameEls() {
replacedImageDiff.imageFrameEls = [];
replacedImageDiff.imageFrameEls[viewTypes.TWO_UP] = element.querySelector(
diff --git a/spec/frontend/import_entities/import_groups/components/import_table_spec.js b/spec/frontend/import_entities/import_groups/components/import_table_spec.js
index b17ff2e0f52..1939e43e5dc 100644
--- a/spec/frontend/import_entities/import_groups/components/import_table_spec.js
+++ b/spec/frontend/import_entities/import_groups/components/import_table_spec.js
@@ -9,7 +9,7 @@ import createFlash from '~/flash';
import httpStatus from '~/lib/utils/http_status';
import axios from '~/lib/utils/axios_utils';
import { STATUSES } from '~/import_entities/constants';
-import { i18n } from '~/import_entities/import_groups/constants';
+import { i18n, ROOT_NAMESPACE } from '~/import_entities/import_groups/constants';
import ImportTable from '~/import_entities/import_groups/components/import_table.vue';
import importGroupsMutation from '~/import_entities/import_groups/graphql/mutations/import_groups.mutation.graphql';
import PaginationLinks from '~/vue_shared/components/pagination_links.vue';
@@ -45,6 +45,8 @@ describe('import table', () => {
const findImportButtons = () =>
wrapper.findAll('button').wrappers.filter((w) => w.text() === 'Import');
const findPaginationDropdown = () => wrapper.find('[data-testid="page-size"]');
+ const findTargetNamespaceDropdown = (rowWrapper) =>
+ rowWrapper.find('[data-testid="target-namespace-selector"]');
const findPaginationDropdownText = () => findPaginationDropdown().find('button').text();
const findSelectionCount = () => wrapper.find('[data-test-id="selection-count"]');
@@ -70,6 +72,7 @@ describe('import table', () => {
groupPathRegex: /.*/,
jobsPath: '/fake_job_path',
sourceUrl: SOURCE_URL,
+ historyPath: '/fake_history_path',
},
apolloProvider,
});
@@ -136,6 +139,32 @@ describe('import table', () => {
expect(wrapper.findAll('tbody tr')).toHaveLength(FAKE_GROUPS.length);
});
+ it('correctly maintains root namespace as last import target', async () => {
+ createComponent({
+ bulkImportSourceGroups: () => ({
+ nodes: [
+ {
+ ...generateFakeEntry({ id: 1, status: STATUSES.FINISHED }),
+ lastImportTarget: {
+ id: 1,
+ targetNamespace: ROOT_NAMESPACE.fullPath,
+ newName: 'does-not-matter',
+ },
+ },
+ ],
+ pageInfo: FAKE_PAGE_INFO,
+ versionValidation: FAKE_VERSION_VALIDATION,
+ }),
+ });
+
+ await waitForPromises();
+ const firstRow = wrapper.find('tbody tr');
+ const targetNamespaceDropdownButton = findTargetNamespaceDropdown(firstRow).find(
+ '[aria-haspopup]',
+ );
+ expect(targetNamespaceDropdownButton.text()).toBe('No parent');
+ });
+
it('does not render status string when result list is empty', async () => {
createComponent({
bulkImportSourceGroups: jest.fn().mockResolvedValue({
diff --git a/spec/frontend/import_entities/import_groups/utils_spec.js b/spec/frontend/import_entities/import_groups/utils_spec.js
new file mode 100644
index 00000000000..2892c5c217b
--- /dev/null
+++ b/spec/frontend/import_entities/import_groups/utils_spec.js
@@ -0,0 +1,56 @@
+import { STATUSES } from '~/import_entities/constants';
+import { isFinished, isAvailableForImport } from '~/import_entities/import_groups/utils';
+
+const FINISHED_STATUSES = [STATUSES.FINISHED, STATUSES.FAILED, STATUSES.TIMEOUT];
+const OTHER_STATUSES = Object.values(STATUSES).filter(
+ (status) => !FINISHED_STATUSES.includes(status),
+);
+describe('gitlab migration status utils', () => {
+ describe('isFinished', () => {
+ it.each(FINISHED_STATUSES.map((s) => [s]))(
+ 'reports group as finished when import status is %s',
+ (status) => {
+ expect(isFinished({ progress: { status } })).toBe(true);
+ },
+ );
+
+ it.each(OTHER_STATUSES.map((s) => [s]))(
+ 'does not report group as finished when import status is %s',
+ (status) => {
+ expect(isFinished({ progress: { status } })).toBe(false);
+ },
+ );
+
+ it('does not report group as finished when there is no progress', () => {
+ expect(isFinished({ progress: null })).toBe(false);
+ });
+
+ it('does not report group as finished when status is unknown', () => {
+ expect(isFinished({ progress: { status: 'weird' } })).toBe(false);
+ });
+ });
+
+ describe('isAvailableForImport', () => {
+ it.each(FINISHED_STATUSES.map((s) => [s]))(
+ 'reports group as available for import when status is %s',
+ (status) => {
+ expect(isAvailableForImport({ progress: { status } })).toBe(true);
+ },
+ );
+
+ it.each(OTHER_STATUSES.map((s) => [s]))(
+ 'does not report group as not available for import when status is %s',
+ (status) => {
+ expect(isAvailableForImport({ progress: { status } })).toBe(false);
+ },
+ );
+
+ it('reports group as available for import when there is no progress', () => {
+ expect(isAvailableForImport({ progress: null })).toBe(true);
+ });
+
+ it('reports group as finished when status is unknown', () => {
+ expect(isFinished({ progress: { status: 'weird' } })).toBe(false);
+ });
+ });
+});
diff --git a/spec/frontend/import_entities/import_projects/components/import_projects_table_spec.js b/spec/frontend/import_entities/import_projects/components/import_projects_table_spec.js
index 88fcedd31b2..140fec3863b 100644
--- a/spec/frontend/import_entities/import_projects/components/import_projects_table_spec.js
+++ b/spec/frontend/import_entities/import_projects/components/import_projects_table_spec.js
@@ -1,4 +1,4 @@
-import { GlLoadingIcon, GlButton, GlIntersectionObserver, GlFormInput } from '@gitlab/ui';
+import { GlLoadingIcon, GlButton, GlIntersectionObserver, GlSearchBoxByClick } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
@@ -15,7 +15,7 @@ describe('ImportProjectsTable', () => {
const findFilterField = () =>
wrapper
- .findAllComponents(GlFormInput)
+ .findAllComponents(GlSearchBoxByClick)
.wrappers.find((w) => w.attributes('placeholder') === 'Filter by name');
const providerTitle = 'THE PROVIDER';
diff --git a/spec/frontend/incidents_settings/components/__snapshots__/pagerduty_form_spec.js.snap b/spec/frontend/incidents_settings/components/__snapshots__/pagerduty_form_spec.js.snap
index feee14c9c40..7e24aa439d4 100644
--- a/spec/frontend/incidents_settings/components/__snapshots__/pagerduty_form_spec.js.snap
+++ b/spec/frontend/incidents_settings/components/__snapshots__/pagerduty_form_spec.js.snap
@@ -57,6 +57,7 @@ exports[`Alert integration settings form should match the default snapshot 1`] =
</gl-button-stub>
<gl-modal-stub
+ arialabel=""
dismisslabel="Close"
modalclass=""
modalid="resetWebhookModal"
diff --git a/spec/frontend/integrations/edit/components/integration_form_spec.js b/spec/frontend/integrations/edit/components/integration_form_spec.js
index ca481e009cf..a2bdece821f 100644
--- a/spec/frontend/integrations/edit/components/integration_form_spec.js
+++ b/spec/frontend/integrations/edit/components/integration_form_spec.js
@@ -1,4 +1,4 @@
-import { GlForm } from '@gitlab/ui';
+import { GlBadge, GlForm } from '@gitlab/ui';
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import * as Sentry from '@sentry/browser';
@@ -18,11 +18,18 @@ import {
integrationLevels,
I18N_SUCCESSFUL_CONNECTION_MESSAGE,
I18N_DEFAULT_ERROR_MESSAGE,
+ billingPlans,
+ billingPlanNames,
} from '~/integrations/constants';
import { createStore } from '~/integrations/edit/store';
import httpStatus from '~/lib/utils/http_status';
import { refreshCurrentPage } from '~/lib/utils/url_utility';
-import { mockIntegrationProps, mockField, mockSectionConnection } from '../mock_data';
+import {
+ mockIntegrationProps,
+ mockField,
+ mockSectionConnection,
+ mockSectionJiraIssues,
+} from '../mock_data';
jest.mock('@sentry/browser');
jest.mock('~/lib/utils/url_utility');
@@ -72,6 +79,7 @@ describe('IntegrationForm', () => {
const findInstanceOrGroupSaveButton = () => wrapper.findByTestId('save-button-instance-group');
const findTestButton = () => wrapper.findByTestId('test-button');
const findTriggerFields = () => wrapper.findComponent(TriggerFields);
+ const findGlBadge = () => wrapper.findComponent(GlBadge);
const findGlForm = () => wrapper.findComponent(GlForm);
const findRedirectToField = () => wrapper.findByTestId('redirect-to-field');
const findDynamicField = () => wrapper.findComponent(DynamicField);
@@ -327,9 +335,21 @@ describe('IntegrationForm', () => {
expect(connectionSection.find('h4').text()).toBe(mockSectionConnection.title);
expect(connectionSection.find('p').text()).toBe(mockSectionConnection.description);
+ expect(findGlBadge().exists()).toBe(false);
expect(findConnectionSectionComponent().exists()).toBe(true);
});
+ it('renders GlBadge when `plan` is present', () => {
+ createComponent({
+ customStateProps: {
+ sections: [mockSectionConnection, mockSectionJiraIssues],
+ },
+ });
+
+ expect(findGlBadge().exists()).toBe(true);
+ expect(findGlBadge().text()).toMatchInterpolatedText(billingPlanNames[billingPlans.PREMIUM]);
+ });
+
it('passes only fields with section type', () => {
const sectionFields = [
{ name: 'username', type: 'text', section: mockSectionConnection.type },
diff --git a/spec/frontend/integrations/edit/components/jira_issues_fields_spec.js b/spec/frontend/integrations/edit/components/jira_issues_fields_spec.js
index 94e370a485f..b4c5d4f9957 100644
--- a/spec/frontend/integrations/edit/components/jira_issues_fields_spec.js
+++ b/spec/frontend/integrations/edit/components/jira_issues_fields_spec.js
@@ -10,7 +10,6 @@ describe('JiraIssuesFields', () => {
let wrapper;
const defaultProps = {
- showJiraIssuesIntegration: true,
showJiraVulnerabilitiesIntegration: true,
upgradePlanPath: 'https://gitlab.com',
};
@@ -42,8 +41,6 @@ describe('JiraIssuesFields', () => {
findEnableCheckbox().find('[type=checkbox]').attributes('disabled');
const findProjectKey = () => wrapper.findComponent(GlFormInput);
const findProjectKeyFormGroup = () => wrapper.findByTestId('project-key-form-group');
- const findPremiumUpgradeCTA = () => wrapper.findByTestId('premium-upgrade-cta');
- const findUltimateUpgradeCTA = () => wrapper.findByTestId('ultimate-upgrade-cta');
const findJiraForVulnerabilities = () => wrapper.findByTestId('jira-for-vulnerabilities');
const setEnableCheckbox = async (isEnabled = true) =>
findEnableCheckbox().vm.$emit('input', isEnabled);
@@ -55,19 +52,16 @@ describe('JiraIssuesFields', () => {
describe('template', () => {
describe.each`
- showJiraIssuesIntegration | showJiraVulnerabilitiesIntegration
- ${false} | ${false}
- ${false} | ${true}
- ${true} | ${false}
- ${true} | ${true}
+ showJiraIssuesIntegration
+ ${false}
+ ${true}
`(
- 'when `showJiraIssuesIntegration` is $jiraIssues and `showJiraVulnerabilitiesIntegration` is $jiraVulnerabilities',
- ({ showJiraIssuesIntegration, showJiraVulnerabilitiesIntegration }) => {
+ 'when showJiraIssuesIntegration = $showJiraIssuesIntegration',
+ ({ showJiraIssuesIntegration }) => {
beforeEach(() => {
createComponent({
props: {
showJiraIssuesIntegration,
- showJiraVulnerabilitiesIntegration,
},
});
});
@@ -77,39 +71,12 @@ describe('JiraIssuesFields', () => {
expect(findEnableCheckbox().exists()).toBe(true);
expect(findEnableCheckboxDisabled()).toBeUndefined();
});
-
- it('does not render the Premium CTA', () => {
- expect(findPremiumUpgradeCTA().exists()).toBe(false);
- });
-
- if (!showJiraVulnerabilitiesIntegration) {
- it.each`
- scenario | enableJiraIssues
- ${'when "Enable Jira issues" is checked, renders Ultimate upgrade CTA'} | ${true}
- ${'when "Enable Jira issues" is unchecked, does not render Ultimate upgrade CTA'} | ${false}
- `('$scenario', async ({ enableJiraIssues }) => {
- if (enableJiraIssues) {
- await setEnableCheckbox();
- }
- expect(findUltimateUpgradeCTA().exists()).toBe(enableJiraIssues);
- });
- }
} else {
- it('does not render enable checkbox', () => {
- expect(findEnableCheckbox().exists()).toBe(false);
- });
-
- it('renders the Premium CTA', () => {
- const premiumUpgradeCTA = findPremiumUpgradeCTA();
-
- expect(premiumUpgradeCTA.exists()).toBe(true);
- expect(premiumUpgradeCTA.props('upgradePlanPath')).toBe(defaultProps.upgradePlanPath);
+ it('renders enable checkbox as disabled', () => {
+ expect(findEnableCheckbox().exists()).toBe(true);
+ expect(findEnableCheckboxDisabled()).toBe('disabled');
});
}
-
- it('does not render the Ultimate CTA', () => {
- expect(findUltimateUpgradeCTA().exists()).toBe(false);
- });
},
);
diff --git a/spec/frontend/integrations/edit/mock_data.js b/spec/frontend/integrations/edit/mock_data.js
index 36850a0a33a..ac0c7d244e3 100644
--- a/spec/frontend/integrations/edit/mock_data.js
+++ b/spec/frontend/integrations/edit/mock_data.js
@@ -37,3 +37,11 @@ export const mockSectionConnection = {
title: 'Connection details',
description: 'Learn more on how to configure this integration.',
};
+
+export const mockSectionJiraIssues = {
+ type: 'jira_issues',
+ title: 'Issues',
+ description:
+ 'Work on Jira issues without leaving GitLab. Add a Jira menu to access a read-only list of your Jira issues. Learn more.',
+ plan: 'premium',
+};
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 84317da39e6..13985ce7d74 100644
--- a/spec/frontend/invite_members/components/invite_members_modal_spec.js
+++ b/spec/frontend/invite_members/components/invite_members_modal_spec.js
@@ -1,4 +1,4 @@
-import { GlLink, GlModal, GlSprintf } from '@gitlab/ui';
+import { GlLink, GlModal, GlSprintf, GlFormGroup } from '@gitlab/ui';
import MockAdapter from 'axios-mock-adapter';
import { nextTick } from 'vue';
import { stubComponent } from 'helpers/stub_component';
@@ -15,6 +15,7 @@ import {
MEMBERS_MODAL_CELEBRATE_INTRO,
MEMBERS_MODAL_CELEBRATE_TITLE,
MEMBERS_PLACEHOLDER,
+ MEMBERS_PLACEHOLDER_DISABLED,
MEMBERS_TO_PROJECT_CELEBRATE_INTRO_TEXT,
LEARN_GITLAB,
} from '~/invite_members/constants';
@@ -28,6 +29,8 @@ import {
propsData,
inviteSource,
newProjectPath,
+ freeUsersLimit,
+ membersCount,
user1,
user2,
user3,
@@ -45,12 +48,13 @@ describe('InviteMembersModal', () => {
let wrapper;
let mock;
- const createComponent = (props = {}) => {
+ const createComponent = (props = {}, stubs = {}) => {
wrapper = shallowMountExtended(InviteMembersModal, {
provide: {
newProjectPath,
},
propsData: {
+ usersLimitDataset: {},
...propsData,
...props,
},
@@ -62,16 +66,17 @@ describe('InviteMembersModal', () => {
template: '<div><slot></slot><slot name="modal-footer"></slot></div>',
}),
GlEmoji,
+ ...stubs,
},
});
};
- const createInviteMembersToProjectWrapper = () => {
- createComponent({ isProject: true });
+ const createInviteMembersToProjectWrapper = (usersLimitDataset = {}, stubs = {}) => {
+ createComponent({ usersLimitDataset, isProject: true }, stubs);
};
- const createInviteMembersToGroupWrapper = () => {
- createComponent({ isProject: false });
+ const createInviteMembersToGroupWrapper = (usersLimitDataset = {}, stubs = {}) => {
+ createComponent({ usersLimitDataset, isProject: false }, stubs);
};
beforeEach(() => {
@@ -95,7 +100,7 @@ describe('InviteMembersModal', () => {
const findMembersFormGroup = () => wrapper.findByTestId('members-form-group');
const membersFormGroupInvalidFeedback = () =>
findMembersFormGroup().attributes('invalid-feedback');
- const membersFormGroupDescription = () => findMembersFormGroup().attributes('description');
+ const membersFormGroupText = () => findMembersFormGroup().text();
const findMembersSelect = () => wrapper.findComponent(MembersTokenSelect);
const findTasksToBeDone = () => wrapper.findByTestId('invite-members-modal-tasks-to-be-done');
const findTasks = () => wrapper.findByTestId('invite-members-modal-tasks');
@@ -259,16 +264,33 @@ describe('InviteMembersModal', () => {
expect(wrapper.findComponent(ModalConfetti).exists()).toBe(false);
});
- it('includes the correct invitee, type, and formatted name', () => {
+ it('includes the correct invitee', () => {
expect(findIntroText()).toBe("You're inviting members to the test name project.");
expect(findCelebrationEmoji().exists()).toBe(false);
- expect(membersFormGroupDescription()).toBe(MEMBERS_PLACEHOLDER);
+ });
+
+ describe('members form group description', () => {
+ it('renders correct description', () => {
+ createInviteMembersToProjectWrapper({ freeUsersLimit, membersCount }, { GlFormGroup });
+ expect(membersFormGroupText()).toContain(MEMBERS_PLACEHOLDER);
+ });
+
+ describe('when reached user limit', () => {
+ it('renders correct description', () => {
+ createInviteMembersToProjectWrapper(
+ { freeUsersLimit, membersCount: 5 },
+ { GlFormGroup },
+ );
+
+ expect(membersFormGroupText()).toContain(MEMBERS_PLACEHOLDER_DISABLED);
+ });
+ });
});
});
describe('when inviting members with celebration', () => {
beforeEach(async () => {
- createComponent({ isProject: true });
+ createInviteMembersToProjectWrapper();
await triggerOpenModal({ mode: 'celebrate' });
});
@@ -285,7 +307,28 @@ describe('InviteMembersModal', () => {
`${MEMBERS_TO_PROJECT_CELEBRATE_INTRO_TEXT} ${MEMBERS_MODAL_CELEBRATE_INTRO}`,
);
expect(findCelebrationEmoji().exists()).toBe(true);
- expect(membersFormGroupDescription()).toBe(MEMBERS_PLACEHOLDER);
+ });
+
+ describe('members form group description', () => {
+ it('renders correct description', async () => {
+ createInviteMembersToProjectWrapper({ freeUsersLimit, membersCount }, { GlFormGroup });
+ await triggerOpenModal({ mode: 'celebrate' });
+
+ expect(membersFormGroupText()).toContain(MEMBERS_PLACEHOLDER);
+ });
+
+ describe('when reached user limit', () => {
+ it('renders correct description', async () => {
+ createInviteMembersToProjectWrapper(
+ { freeUsersLimit, membersCount: 5 },
+ { GlFormGroup },
+ );
+
+ await triggerOpenModal({ mode: 'celebrate' });
+
+ expect(membersFormGroupText()).toContain(MEMBERS_PLACEHOLDER_DISABLED);
+ });
+ });
});
});
});
@@ -295,7 +338,20 @@ describe('InviteMembersModal', () => {
createInviteMembersToGroupWrapper();
expect(findIntroText()).toBe("You're inviting members to the test name group.");
- expect(membersFormGroupDescription()).toBe(MEMBERS_PLACEHOLDER);
+ });
+
+ describe('members form group description', () => {
+ it('renders correct description', () => {
+ createInviteMembersToGroupWrapper({ freeUsersLimit, membersCount }, { GlFormGroup });
+ expect(membersFormGroupText()).toContain(MEMBERS_PLACEHOLDER);
+ });
+
+ describe('when reached user limit', () => {
+ it('renders correct description', () => {
+ createInviteMembersToGroupWrapper({ freeUsersLimit, membersCount: 5 }, { GlFormGroup });
+ expect(membersFormGroupText()).toContain(MEMBERS_PLACEHOLDER_DISABLED);
+ });
+ });
});
});
});
diff --git a/spec/frontend/invite_members/components/invite_modal_base_spec.js b/spec/frontend/invite_members/components/invite_modal_base_spec.js
index 8355ae67f20..010f7b999fc 100644
--- a/spec/frontend/invite_members/components/invite_modal_base_spec.js
+++ b/spec/frontend/invite_members/components/invite_modal_base_spec.js
@@ -6,18 +6,30 @@ import {
GlSprintf,
GlLink,
GlModal,
+ GlIcon,
} from '@gitlab/ui';
import { stubComponent } from 'helpers/stub_component';
+import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import InviteModalBase from '~/invite_members/components/invite_modal_base.vue';
import ContentTransition from '~/vue_shared/components/content_transition.vue';
-import { CANCEL_BUTTON_TEXT, INVITE_BUTTON_TEXT } from '~/invite_members/constants';
-import { propsData } from '../mock_data/modal_base';
+
+import {
+ CANCEL_BUTTON_TEXT,
+ INVITE_BUTTON_TEXT_DISABLED,
+ INVITE_BUTTON_TEXT,
+ CANCEL_BUTTON_TEXT_DISABLED,
+ ON_SHOW_TRACK_LABEL,
+ ON_CLOSE_TRACK_LABEL,
+ ON_SUBMIT_TRACK_LABEL,
+} from '~/invite_members/constants';
+
+import { propsData, membersPath, purchasePath } from '../mock_data/modal_base';
describe('InviteModalBase', () => {
let wrapper;
- const createComponent = (props = {}) => {
+ const createComponent = (props = {}, stubs = {}) => {
wrapper = shallowMountExtended(InviteModalBase, {
propsData: {
...propsData,
@@ -33,8 +45,9 @@ describe('InviteModalBase', () => {
GlDropdownItem: true,
GlSprintf,
GlFormGroup: stubComponent(GlFormGroup, {
- props: ['state', 'invalidFeedback', 'description'],
+ props: ['state', 'invalidFeedback'],
}),
+ ...stubs,
},
});
};
@@ -48,8 +61,12 @@ describe('InviteModalBase', () => {
const findDropdownItems = () => findDropdown().findAllComponents(GlDropdownItem);
const findDatepicker = () => wrapper.findComponent(GlDatepicker);
const findLink = () => wrapper.findComponent(GlLink);
+ const findIcon = () => wrapper.findComponent(GlIcon);
const findIntroText = () => wrapper.findByTestId('modal-base-intro-text').text();
const findMembersFormGroup = () => wrapper.findByTestId('members-form-group');
+ const findDisabledInput = () => wrapper.findByTestId('disabled-input');
+ const findCancelButton = () => wrapper.find('.js-modal-action-cancel');
+ const findActionButton = () => wrapper.find('.js-modal-action-primary');
describe('rendering the modal', () => {
beforeEach(() => {
@@ -106,11 +123,103 @@ describe('InviteModalBase', () => {
it('renders the members form group', () => {
expect(findMembersFormGroup().props()).toEqual({
- description: propsData.formGroupDescription,
invalidFeedback: '',
state: null,
});
});
+
+ it('renders description', () => {
+ createComponent({}, { GlFormGroup });
+
+ expect(findMembersFormGroup().text()).toContain(propsData.formGroupDescription);
+ });
+
+ describe('when users limit is reached', () => {
+ let trackingSpy;
+
+ const expectTracking = (action, label) =>
+ expect(trackingSpy).toHaveBeenCalledWith('default', action, {
+ label,
+ category: 'default',
+ });
+
+ beforeEach(() => {
+ createComponent(
+ { usersLimitDataset: { membersPath, purchasePath }, reachedLimit: true },
+ { GlModal, GlFormGroup },
+ );
+ });
+
+ it('renders correct blocks', () => {
+ expect(findIcon().exists()).toBe(true);
+ expect(findDisabledInput().exists()).toBe(true);
+ expect(findDropdown().exists()).toBe(false);
+ expect(findDatepicker().exists()).toBe(false);
+ });
+
+ it('renders correct buttons', () => {
+ const cancelButton = findCancelButton();
+ const actionButton = findActionButton();
+
+ expect(cancelButton.attributes('href')).toBe(purchasePath);
+ expect(cancelButton.text()).toBe(CANCEL_BUTTON_TEXT_DISABLED);
+ expect(actionButton.attributes('href')).toBe(membersPath);
+ expect(actionButton.text()).toBe(INVITE_BUTTON_TEXT_DISABLED);
+ });
+
+ it('tracks actions', () => {
+ createComponent({ reachedLimit: true }, { GlFormGroup, GlModal });
+ trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
+
+ const modal = wrapper.findComponent(GlModal);
+
+ modal.vm.$emit('shown');
+ expectTracking('render', ON_SHOW_TRACK_LABEL);
+
+ modal.vm.$emit('cancel', { preventDefault: jest.fn() });
+ expectTracking('click_button', ON_CLOSE_TRACK_LABEL);
+
+ modal.vm.$emit('primary', { preventDefault: jest.fn() });
+ expectTracking('click_button', ON_SUBMIT_TRACK_LABEL);
+
+ unmockTracking();
+ });
+
+ describe('when free user namespace', () => {
+ it('hides cancel button', () => {
+ createComponent(
+ {
+ usersLimitDataset: { membersPath, purchasePath, userNamespace: true },
+ reachedLimit: true,
+ },
+ { GlModal, GlFormGroup },
+ );
+
+ expect(findCancelButton().exists()).toBe(false);
+ });
+ });
+ });
+
+ describe('when users limit is not reached', () => {
+ const textRegex = /Select a role.+Read more about role permissions Access expiration date \(optional\)/;
+
+ beforeEach(() => {
+ createComponent({ reachedLimit: false }, { GlModal, GlFormGroup });
+ });
+
+ it('renders correct blocks', () => {
+ expect(findIcon().exists()).toBe(false);
+ expect(findDisabledInput().exists()).toBe(false);
+ expect(findDropdown().exists()).toBe(true);
+ expect(findDatepicker().exists()).toBe(true);
+ expect(wrapper.findComponent(GlModal).text()).toMatch(textRegex);
+ });
+
+ it('renders correct buttons', () => {
+ expect(findCancelButton().text()).toBe(CANCEL_BUTTON_TEXT);
+ expect(findActionButton().text()).toBe(INVITE_BUTTON_TEXT);
+ });
+ });
});
it('with isLoading, shows loading for invite button', () => {
@@ -127,7 +236,6 @@ describe('InviteModalBase', () => {
});
expect(findMembersFormGroup().props()).toEqual({
- description: propsData.formGroupDescription,
invalidFeedback: 'invalid message!',
state: false,
});
diff --git a/spec/frontend/invite_members/components/user_limit_notification_spec.js b/spec/frontend/invite_members/components/user_limit_notification_spec.js
index c779cf2ee3f..4c9adbfcc44 100644
--- a/spec/frontend/invite_members/components/user_limit_notification_spec.js
+++ b/spec/frontend/invite_members/components/user_limit_notification_spec.js
@@ -2,21 +2,31 @@ import { GlAlert, GlSprintf } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import UserLimitNotification from '~/invite_members/components/user_limit_notification.vue';
+import {
+ REACHED_LIMIT_MESSAGE,
+ REACHED_LIMIT_UPGRADE_SUGGESTION_MESSAGE,
+} from '~/invite_members/constants';
+
+import { freeUsersLimit, membersCount } from '../mock_data/member_modal';
+
describe('UserLimitNotification', () => {
let wrapper;
const findAlert = () => wrapper.findComponent(GlAlert);
- const createComponent = (providers = {}) => {
+ const createComponent = (reachedLimit = false, usersLimitDataset = {}) => {
wrapper = shallowMountExtended(UserLimitNotification, {
- provide: {
- name: 'my group',
- newTrialRegistrationPath: 'newTrialRegistrationPath',
- purchasePath: 'purchasePath',
- freeUsersLimit: 5,
- membersCount: 1,
- ...providers,
+ propsData: {
+ reachedLimit,
+ usersLimitDataset: {
+ freeUsersLimit,
+ membersCount,
+ newTrialRegistrationPath: 'newTrialRegistrationPath',
+ purchasePath: 'purchasePath',
+ ...usersLimitDataset,
+ },
},
+ provide: { name: 'my group' },
stubs: { GlSprintf },
});
};
@@ -26,21 +36,17 @@ describe('UserLimitNotification', () => {
});
describe('when limit is not reached', () => {
- beforeEach(() => {
+ it('renders empty block', () => {
createComponent();
- });
- it('renders empty block', () => {
expect(findAlert().exists()).toBe(false);
});
});
describe('when close to limit', () => {
- beforeEach(() => {
- createComponent({ membersCount: 3 });
- });
-
it("renders user's limit notification", () => {
+ createComponent(false, { membersCount: 3 });
+
const alert = findAlert();
expect(alert.attributes('title')).toEqual(
@@ -54,18 +60,27 @@ describe('UserLimitNotification', () => {
});
describe('when limit is reached', () => {
- beforeEach(() => {
- createComponent({ membersCount: 5 });
- });
-
it("renders user's limit notification", () => {
+ createComponent(true);
+
const alert = findAlert();
expect(alert.attributes('title')).toEqual("You've reached your 5 members limit for my group");
+ expect(alert.text()).toEqual(REACHED_LIMIT_UPGRADE_SUGGESTION_MESSAGE);
+ });
- expect(alert.text()).toEqual(
- 'New members will be unable to participate. You can manage your members by removing ones you no longer need. To get more members an owner of this namespace can start a trial or upgrade to a paid tier.',
- );
+ describe('when free user namespace', () => {
+ it("renders user's limit notification", () => {
+ createComponent(true, { userNamespace: true });
+
+ const alert = findAlert();
+
+ expect(alert.attributes('title')).toEqual(
+ "You've reached your 5 members limit for my group",
+ );
+
+ expect(alert.text()).toEqual(REACHED_LIMIT_MESSAGE);
+ });
});
});
});
diff --git a/spec/frontend/invite_members/mock_data/member_modal.js b/spec/frontend/invite_members/mock_data/member_modal.js
index 1b0cc57fb5b..474234cfacb 100644
--- a/spec/frontend/invite_members/mock_data/member_modal.js
+++ b/spec/frontend/invite_members/mock_data/member_modal.js
@@ -18,6 +18,8 @@ export const propsData = {
export const inviteSource = 'unknown';
export const newProjectPath = 'projects/new';
+export const freeUsersLimit = 5;
+export const membersCount = 1;
export const user1 = { id: 1, name: 'Name One', username: 'one_1', avatar_url: '' };
export const user2 = { id: 2, name: 'Name Two', username: 'one_2', avatar_url: '' };
diff --git a/spec/frontend/invite_members/mock_data/modal_base.js b/spec/frontend/invite_members/mock_data/modal_base.js
index ea5a8d2b00d..565e8d4df1e 100644
--- a/spec/frontend/invite_members/mock_data/modal_base.js
+++ b/spec/frontend/invite_members/mock_data/modal_base.js
@@ -9,3 +9,6 @@ export const propsData = {
labelSearchField: '_label_search_field_',
formGroupDescription: '_form_group_description_',
};
+
+export const membersPath = '/members_path';
+export const purchasePath = '/purchase_path';
diff --git a/spec/frontend/issuable/components/issuable_header_warnings_spec.js b/spec/frontend/issuable/components/issuable_header_warnings_spec.js
index c8380e42787..e3a36dc8820 100644
--- a/spec/frontend/issuable/components/issuable_header_warnings_spec.js
+++ b/spec/frontend/issuable/components/issuable_header_warnings_spec.js
@@ -66,7 +66,15 @@ describe('IssuableHeaderWarnings', () => {
});
it(`${renderTestMessage(confidentialStatus)} the confidential icon`, () => {
- expect(findConfidentialIcon().exists()).toBe(confidentialStatus);
+ const confidentialEl = findConfidentialIcon();
+ expect(confidentialEl.exists()).toBe(confidentialStatus);
+
+ if (confidentialStatus && !hiddenStatus) {
+ expect(confidentialEl.props()).toMatchObject({
+ workspaceType: 'project',
+ issuableType: 'issue',
+ });
+ }
});
it(`${renderTestMessage(confidentialStatus)} the hidden icon`, () => {
diff --git a/spec/frontend/issuable/components/status_box_spec.js b/spec/frontend/issuable/components/status_box_spec.js
index 9cbf023dbd6..728b8958b9b 100644
--- a/spec/frontend/issuable/components/status_box_spec.js
+++ b/spec/frontend/issuable/components/status_box_spec.js
@@ -1,71 +1,53 @@
-import { GlIcon, GlSprintf } from '@gitlab/ui';
+import { GlBadge, GlIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import StatusBox from '~/issuable/components/status_box.vue';
let wrapper;
function factory(propsData) {
- wrapper = shallowMount(StatusBox, { propsData, stubs: { GlSprintf } });
+ wrapper = shallowMount(StatusBox, { propsData, stubs: { GlBadge } });
}
-const testCases = [
- {
- name: 'Open',
- state: 'opened',
- class: 'status-box-open',
- icon: 'issue-open-m',
- },
- {
- name: 'Open',
- state: 'locked',
- class: 'status-box-open',
- icon: 'issue-open-m',
- },
- {
- name: 'Closed',
- state: 'closed',
- class: 'status-box-mr-closed',
- icon: 'issue-close',
- },
- {
- name: 'Merged',
- state: 'merged',
- class: 'status-box-mr-merged',
- icon: 'git-merge',
- },
-];
-
describe('Merge request status box component', () => {
+ const findBadge = () => wrapper.findComponent(GlBadge);
+
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
- testCases.forEach((testCase) => {
- describe(`when merge request is ${testCase.name}`, () => {
- it('renders human readable test', () => {
+ describe.each`
+ issuableType | badgeText | initialState | badgeClass | badgeVariant | badgeIcon
+ ${'merge_request'} | ${'Open'} | ${'opened'} | ${'issuable-status-badge-open'} | ${'success'} | ${'merge-request-open'}
+ ${'merge_request'} | ${'Closed'} | ${'closed'} | ${'issuable-status-badge-closed'} | ${'danger'} | ${'merge-request-close'}
+ ${'merge_request'} | ${'Merged'} | ${'merged'} | ${'issuable-status-badge-merged'} | ${'info'} | ${'merge'}
+ ${'issue'} | ${'Open'} | ${'opened'} | ${'issuable-status-badge-open'} | ${'success'} | ${'issues'}
+ ${'issue'} | ${'Closed'} | ${'closed'} | ${'issuable-status-badge-closed'} | ${'info'} | ${'issue-closed'}
+ `(
+ 'with issuableType set to "$issuableType" and state set to "$initialState"',
+ ({ issuableType, badgeText, initialState, badgeClass, badgeVariant, badgeIcon }) => {
+ beforeEach(() => {
factory({
- initialState: testCase.state,
+ initialState,
+ issuableType,
});
-
- expect(wrapper.text()).toContain(testCase.name);
});
- it('sets css class', () => {
- factory({
- initialState: testCase.state,
- });
+ it(`renders badge with text '${badgeText}'`, () => {
+ expect(findBadge().text()).toBe(badgeText);
+ });
- expect(wrapper.classes()).toContain(testCase.class);
+ it(`sets badge css class as '${badgeClass}'`, () => {
+ expect(findBadge().classes()).toContain(badgeClass);
});
- it('renders icon', () => {
- factory({
- initialState: testCase.state,
- });
+ it(`sets badge variant as '${badgeVariant}`, () => {
+ expect(findBadge().props('variant')).toBe(badgeVariant);
+ });
- expect(wrapper.findComponent(GlIcon).props('name')).toBe(testCase.icon);
+ it(`sets badge icon as '${badgeIcon}'`, () => {
+ expect(findBadge().findComponent(GlIcon).props('name')).toBe(badgeIcon);
});
- });
- });
+ },
+ );
});
diff --git a/spec/frontend/issuable/issuable_form_spec.js b/spec/frontend/issuable/issuable_form_spec.js
index 99ed18cf5bd..a1583076b41 100644
--- a/spec/frontend/issuable/issuable_form_spec.js
+++ b/spec/frontend/issuable/issuable_form_spec.js
@@ -1,5 +1,5 @@
import $ from 'jquery';
-
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import IssuableForm from '~/issuable/issuable_form';
import setWindowLocation from 'helpers/set_window_location_helper';
@@ -11,7 +11,7 @@ describe('IssuableForm', () => {
};
beforeEach(() => {
- setFixtures(`
+ setHTMLFixture(`
<form>
<input name="[title]" />
</form>
@@ -19,6 +19,10 @@ describe('IssuableForm', () => {
createIssuable($('form'));
});
+ afterEach(() => {
+ resetHTMLFixture();
+ });
+
describe('initAutosave', () => {
it('creates autosave with the searchTerm included', () => {
setWindowLocation('https://gitlab.test/foo?bar=true');
@@ -28,7 +32,7 @@ describe('IssuableForm', () => {
});
it("creates autosave fields without the searchTerm if it's an issue new form", () => {
- setFixtures(`
+ setHTMLFixture(`
<form data-new-issue-path="/issues/new">
<input name="[title]" />
</form>
diff --git a/spec/frontend/issuable/related_issues/components/related_issues_root_spec.js b/spec/frontend/issuable/related_issues/components/related_issues_root_spec.js
index b59717a1f60..1a03ea58b60 100644
--- a/spec/frontend/issuable/related_issues/components/related_issues_root_spec.js
+++ b/spec/frontend/issuable/related_issues/components/related_issues_root_spec.js
@@ -300,16 +300,27 @@ describe('RelatedIssuesRoot', () => {
expect(wrapper.vm.state.pendingReferences[1]).toEqual('random');
});
- it('prepends # when user enters a numeric value [0-9]', async () => {
- const input = '23';
+ it.each`
+ pathIdSeparator
+ ${'#'}
+ ${'&'}
+ `(
+ 'prepends $pathIdSeparator when user enters a numeric value [0-9]',
+ async ({ pathIdSeparator }) => {
+ const input = '23';
+
+ await wrapper.setProps({
+ pathIdSeparator,
+ });
- wrapper.vm.onInput({
- untouchedRawReferences: input.trim().split(/\s/),
- touchedReference: input,
- });
+ wrapper.vm.onInput({
+ untouchedRawReferences: input.trim().split(/\s/),
+ touchedReference: input,
+ });
- expect(wrapper.vm.inputValue).toBe(`#${input}`);
- });
+ expect(wrapper.vm.inputValue).toBe(`${pathIdSeparator}${input}`);
+ },
+ );
it('prepends # when user enters a number', async () => {
const input = 23;
diff --git a/spec/frontend/issues/issue_spec.js b/spec/frontend/issues/issue_spec.js
index 8a089b372ff..089ea8dbbad 100644
--- a/spec/frontend/issues/issue_spec.js
+++ b/spec/frontend/issues/issue_spec.js
@@ -1,5 +1,6 @@
import { getByText } from '@testing-library/dom';
import MockAdapter from 'axios-mock-adapter';
+import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import { EVENT_ISSUABLE_VUE_APP_CHANGE } from '~/issuable/constants';
import Issue from '~/issues/issue';
import axios from '~/lib/utils/axios_utils';
@@ -24,11 +25,11 @@ describe('Issue', () => {
const getIssueCounter = () => document.querySelector('.issue_counter');
const getOpenStatusBox = () =>
getByText(document, (_, el) => el.textContent.match(/Open/), {
- selector: '.status-box-open',
+ selector: '.issuable-status-badge-open',
});
const getClosedStatusBox = () =>
getByText(document, (_, el) => el.textContent.match(/Closed/), {
- selector: '.status-box-issue-closed',
+ selector: '.issuable-status-badge-closed',
});
describe.each`
@@ -38,9 +39,9 @@ describe('Issue', () => {
`('$desc', ({ isIssueInitiallyOpen, expectedCounterText }) => {
beforeEach(() => {
if (isIssueInitiallyOpen) {
- loadFixtures('issues/open-issue.html');
+ loadHTMLFixture('issues/open-issue.html');
} else {
- loadFixtures('issues/closed-issue.html');
+ loadHTMLFixture('issues/closed-issue.html');
}
testContext.issueCounter = getIssueCounter();
@@ -50,6 +51,10 @@ describe('Issue', () => {
testContext.issueCounter.textContent = '1,001';
});
+ afterEach(() => {
+ resetHTMLFixture();
+ });
+
it(`has the proper visible status box when ${isIssueInitiallyOpen ? 'open' : 'closed'}`, () => {
if (isIssueInitiallyOpen) {
expect(testContext.statusBoxClosed).toHaveClass('hidden');
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 5a9bd1ff8e4..d92ba527b5c 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,11 @@ import AxiosMockAdapter from 'axios-mock-adapter';
import { cloneDeep } from 'lodash';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
+import VueRouter from 'vue-router';
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 getIssuesWithoutCrmQuery from 'ee_else_ce/issues/list/queries/get_issues_without_crm.query.graphql';
+import getIssuesCountsWithoutCrmQuery from 'ee_else_ce/issues/list/queries/get_issues_counts_without_crm.query.graphql';
import createMockApollo from 'helpers/mock_apollo_helper';
import setWindowLocation from 'helpers/set_window_location_helper';
import { TEST_HOST } from 'helpers/test_constants';
@@ -58,6 +61,7 @@ describe('CE IssuesListApp component', () => {
let wrapper;
Vue.use(VueApollo);
+ Vue.use(VueRouter);
const defaultProvide = {
autocompleteAwardEmojisPath: 'autocomplete/award/emojis/path',
@@ -78,6 +82,7 @@ describe('CE IssuesListApp component', () => {
isAnonymousSearchDisabled: false,
isIssueRepositioningDisabled: false,
isProject: true,
+ isPublicVisibilityRestricted: false,
isSignedIn: true,
jiraIntegrationPath: 'jira/integration/path',
newIssuePath: 'new/issue/path',
@@ -107,6 +112,7 @@ describe('CE IssuesListApp component', () => {
const mountComponent = ({
provide = {},
+ data = {},
issuesQueryResponse = jest.fn().mockResolvedValue(defaultQueryResponse),
issuesCountsQueryResponse = jest.fn().mockResolvedValue(getIssuesCountsQueryResponse),
sortPreferenceMutationResponse = jest.fn().mockResolvedValue(setSortPreferenceMutationResponse),
@@ -115,16 +121,21 @@ describe('CE IssuesListApp component', () => {
const requestHandlers = [
[getIssuesQuery, issuesQueryResponse],
[getIssuesCountsQuery, issuesCountsQueryResponse],
+ [getIssuesWithoutCrmQuery, issuesQueryResponse],
+ [getIssuesCountsWithoutCrmQuery, issuesCountsQueryResponse],
[setSortPreferenceMutation, sortPreferenceMutationResponse],
];
- const apolloProvider = createMockApollo(requestHandlers);
return mountFn(IssuesListApp, {
- apolloProvider,
+ apolloProvider: createMockApollo(requestHandlers),
+ router: new VueRouter({ mode: 'history' }),
provide: {
...defaultProvide,
...provide,
},
+ data() {
+ return data;
+ },
});
};
@@ -139,10 +150,10 @@ describe('CE IssuesListApp component', () => {
});
describe('IssuableList', () => {
- beforeEach(async () => {
+ beforeEach(() => {
wrapper = mountComponent();
jest.runOnlyPendingTimers();
- await waitForPromises();
+ return waitForPromises();
});
it('renders', () => {
@@ -167,10 +178,6 @@ describe('CE IssuesListApp component', () => {
useKeysetPagination: true,
hasPreviousPage: getIssuesQueryResponse.data.project.issues.pageInfo.hasPreviousPage,
hasNextPage: getIssuesQueryResponse.data.project.issues.pageInfo.hasNextPage,
- urlParams: {
- sort: urlSortParams[CREATED_DESC],
- state: IssuableStates.Opened,
- },
});
});
});
@@ -200,7 +207,7 @@ describe('CE IssuesListApp component', () => {
describe('csv import/export component', () => {
describe('when user is signed in', () => {
- beforeEach(async () => {
+ beforeEach(() => {
setWindowLocation('?search=refactor&state=opened');
wrapper = mountComponent({
@@ -209,12 +216,12 @@ describe('CE IssuesListApp component', () => {
});
jest.runOnlyPendingTimers();
- await waitForPromises();
+ return waitForPromises();
});
it('renders', () => {
expect(findCsvImportExportButtons().props()).toMatchObject({
- exportCsvPath: `${defaultProvide.exportCsvPath}?search=refactor&sort=created_date&state=opened`,
+ exportCsvPath: `${defaultProvide.exportCsvPath}?search=refactor&state=opened`,
issuableCount: 1,
});
});
@@ -252,11 +259,9 @@ describe('CE IssuesListApp component', () => {
it('emits "issuables:enableBulkEdit" event to legacy bulk edit class', async () => {
wrapper = mountComponent({ provide: { canBulkUpdate: true }, mountFn: mount });
-
jest.spyOn(eventHub, '$emit');
findGlButtonAt(2).vm.$emit('click');
-
await waitForPromises();
expect(eventHub.$emit).toHaveBeenCalledWith('issuables:enableBulkEdit');
@@ -297,32 +302,25 @@ describe('CE IssuesListApp component', () => {
describe('page', () => {
it('page_after is set from the url params', () => {
setWindowLocation('?page_after=randomCursorString');
-
wrapper = mountComponent();
- expect(findIssuableList().props('urlParams')).toMatchObject({
- page_after: 'randomCursorString',
- });
+ expect(wrapper.vm.$route.query).toMatchObject({ page_after: 'randomCursorString' });
});
it('page_before is set from the url params', () => {
setWindowLocation('?page_before=anotherRandomCursorString');
-
wrapper = mountComponent();
- expect(findIssuableList().props('urlParams')).toMatchObject({
- page_before: 'anotherRandomCursorString',
- });
+ expect(wrapper.vm.$route.query).toMatchObject({ page_before: 'anotherRandomCursorString' });
});
});
describe('search', () => {
it('is set from the url params', () => {
setWindowLocation(locationSearch);
-
wrapper = mountComponent();
- expect(findIssuableList().props('urlParams')).toMatchObject({ search: 'find issues' });
+ expect(wrapper.vm.$route.query).toMatchObject({ search: 'find issues' });
});
});
@@ -333,10 +331,7 @@ describe('CE IssuesListApp component', () => {
it.each(oldEnumSortValues)('initial sort is set with value %s', (sort) => {
wrapper = mountComponent({ provide: { initialSort: sort } });
- expect(findIssuableList().props()).toMatchObject({
- initialSortBy: getSortKey(sort),
- urlParams: { sort },
- });
+ expect(findIssuableList().props('initialSortBy')).toBe(getSortKey(sort));
});
});
@@ -346,10 +341,7 @@ describe('CE IssuesListApp component', () => {
it.each(graphQLEnumSortValues)('initial sort is set with value %s', (sort) => {
wrapper = mountComponent({ provide: { initialSort: sort.toLowerCase() } });
- expect(findIssuableList().props()).toMatchObject({
- initialSortBy: sort,
- urlParams: { sort: urlSortParams[sort] },
- });
+ expect(findIssuableList().props('initialSortBy')).toBe(sort);
});
});
@@ -359,10 +351,7 @@ describe('CE IssuesListApp component', () => {
(sort) => {
wrapper = mountComponent({ provide: { initialSort: sort } });
- expect(findIssuableList().props()).toMatchObject({
- initialSortBy: CREATED_DESC,
- urlParams: { sort: urlSortParams[CREATED_DESC] },
- });
+ expect(findIssuableList().props('initialSortBy')).toBe(CREATED_DESC);
},
);
});
@@ -375,10 +364,7 @@ describe('CE IssuesListApp component', () => {
});
it('changes the sort to the default of created descending', () => {
- expect(findIssuableList().props()).toMatchObject({
- initialSortBy: CREATED_DESC,
- urlParams: { sort: urlSortParams[CREATED_DESC] },
- });
+ expect(findIssuableList().props('initialSortBy')).toBe(CREATED_DESC);
});
it('shows an alert to tell the user that manual reordering is disabled', () => {
@@ -393,9 +379,7 @@ describe('CE IssuesListApp component', () => {
describe('state', () => {
it('is set from the url params', () => {
const initialState = IssuableStates.All;
-
setWindowLocation(`?state=${initialState}`);
-
wrapper = mountComponent();
expect(findIssuableList().props('currentTab')).toBe(initialState);
@@ -405,7 +389,6 @@ describe('CE IssuesListApp component', () => {
describe('filter tokens', () => {
it('is set from the url params', () => {
setWindowLocation(locationSearch);
-
wrapper = mountComponent();
expect(findIssuableList().props('initialFilterValue')).toEqual(filteredTokens);
@@ -414,7 +397,6 @@ describe('CE IssuesListApp component', () => {
describe('when anonymous searching is performed', () => {
beforeEach(() => {
setWindowLocation(locationSearch);
-
wrapper = mountComponent({
provide: { isAnonymousSearchDisabled: true, isSignedIn: false },
});
@@ -649,12 +631,12 @@ describe('CE IssuesListApp component', () => {
${'fetching issues'} | ${'issuesQueryResponse'} | ${IssuesListApp.i18n.errorFetchingIssues}
${'fetching issue counts'} | ${'issuesCountsQueryResponse'} | ${IssuesListApp.i18n.errorFetchingCounts}
`('when there is an error $error', ({ mountOption, message }) => {
- beforeEach(async () => {
+ beforeEach(() => {
wrapper = mountComponent({
[mountOption]: jest.fn().mockRejectedValue(new Error('ERROR')),
});
jest.runOnlyPendingTimers();
- await waitForPromises();
+ return waitForPromises();
});
it('shows an error message', () => {
@@ -676,29 +658,51 @@ describe('CE IssuesListApp component', () => {
describe('when "click-tab" event is emitted by IssuableList', () => {
beforeEach(() => {
wrapper = mountComponent();
+ jest.spyOn(wrapper.vm.$router, 'push');
findIssuableList().vm.$emit('click-tab', IssuableStates.Closed);
});
- it('updates to the new tab', () => {
+ it('updates ui to the new tab', () => {
expect(findIssuableList().props('currentTab')).toBe(IssuableStates.Closed);
});
- });
- describe.each(['next-page', 'previous-page'])(
- 'when "%s" event is emitted by IssuableList',
- (event) => {
- beforeEach(() => {
- wrapper = mountComponent();
+ it('updates url to the new tab', () => {
+ expect(wrapper.vm.$router.push).toHaveBeenCalledWith({
+ query: expect.objectContaining({ state: IssuableStates.Closed }),
+ });
+ });
+ });
- findIssuableList().vm.$emit(event);
+ describe.each`
+ event | paramName | paramValue
+ ${'next-page'} | ${'page_after'} | ${'endCursor'}
+ ${'previous-page'} | ${'page_before'} | ${'startCursor'}
+ `('when "$event" event is emitted by IssuableList', ({ event, paramName, paramValue }) => {
+ beforeEach(() => {
+ wrapper = mountComponent({
+ data: {
+ pageInfo: {
+ endCursor: 'endCursor',
+ startCursor: 'startCursor',
+ },
+ },
});
+ jest.spyOn(wrapper.vm.$router, 'push');
+
+ findIssuableList().vm.$emit(event);
+ });
+
+ it('scrolls to the top', () => {
+ expect(scrollUp).toHaveBeenCalled();
+ });
- it('scrolls to the top', () => {
- expect(scrollUp).toHaveBeenCalled();
+ it(`updates url with "${paramName}" param`, () => {
+ expect(wrapper.vm.$router.push).toHaveBeenCalledWith({
+ query: expect.objectContaining({ [paramName]: paramValue }),
});
- },
- );
+ });
+ });
describe('when "reorder" event is emitted by IssuableList', () => {
const issueOne = {
@@ -752,18 +756,17 @@ describe('CE IssuesListApp component', () => {
`(
'when moving issue $description',
({ issueToMove, oldIndex, newIndex, moveBeforeId, moveAfterId }) => {
- beforeEach(async () => {
+ beforeEach(() => {
wrapper = mountComponent({
provide: { isProject },
issuesQueryResponse: jest.fn().mockResolvedValue(response(isProject)),
});
jest.runOnlyPendingTimers();
- await waitForPromises();
+ return waitForPromises();
});
it('makes API call to reorder the issue', async () => {
findIssuableList().vm.$emit('reorder', { oldIndex, newIndex });
-
await waitForPromises();
expect(axiosMock.history.put[0]).toMatchObject({
@@ -780,19 +783,18 @@ describe('CE IssuesListApp component', () => {
});
describe('when unsuccessful', () => {
- beforeEach(async () => {
+ beforeEach(() => {
wrapper = mountComponent({
issuesQueryResponse: jest.fn().mockResolvedValue(response()),
});
jest.runOnlyPendingTimers();
- await waitForPromises();
+ return waitForPromises();
});
it('displays an error message', async () => {
axiosMock.onPut(joinPaths(issueOne.webPath, 'reorder')).reply(500);
findIssuableList().vm.$emit('reorder', { oldIndex: 0, newIndex: 1 });
-
await waitForPromises();
expect(findIssuableList().props('error')).toBe(IssuesListApp.i18n.reorderError);
@@ -808,14 +810,14 @@ describe('CE IssuesListApp component', () => {
'updates to the new sort when payload is `%s`',
async (sortKey) => {
wrapper = mountComponent();
+ jest.spyOn(wrapper.vm.$router, 'push');
findIssuableList().vm.$emit('sort', sortKey);
-
jest.runOnlyPendingTimers();
await nextTick();
- expect(findIssuableList().props('urlParams')).toMatchObject({
- sort: urlSortParams[sortKey],
+ expect(wrapper.vm.$router.push).toHaveBeenCalledWith({
+ query: expect.objectContaining({ sort: urlSortParams[sortKey] }),
});
},
);
@@ -827,14 +829,13 @@ describe('CE IssuesListApp component', () => {
wrapper = mountComponent({
provide: { initialSort, isIssueRepositioningDisabled: true },
});
+ jest.spyOn(wrapper.vm.$router, 'push');
findIssuableList().vm.$emit('sort', RELATIVE_POSITION_ASC);
});
it('does not update the sort to manual', () => {
- expect(findIssuableList().props('urlParams')).toMatchObject({
- sort: urlSortParams[initialSort],
- });
+ expect(wrapper.vm.$router.push).not.toHaveBeenCalled();
});
it('shows an alert to tell the user that manual reordering is disabled', () => {
@@ -899,11 +900,14 @@ describe('CE IssuesListApp component', () => {
describe('when "filter" event is emitted by IssuableList', () => {
it('updates IssuableList with url params', async () => {
wrapper = mountComponent();
+ jest.spyOn(wrapper.vm.$router, 'push');
findIssuableList().vm.$emit('filter', filteredTokens);
await nextTick();
- expect(findIssuableList().props('urlParams')).toMatchObject(urlParams);
+ expect(wrapper.vm.$router.push).toHaveBeenCalledWith({
+ query: expect.objectContaining(urlParams),
+ });
});
describe('when anonymous searching is performed', () => {
@@ -911,19 +915,13 @@ describe('CE IssuesListApp component', () => {
wrapper = mountComponent({
provide: { isAnonymousSearchDisabled: true, isSignedIn: false },
});
+ jest.spyOn(wrapper.vm.$router, 'push');
findIssuableList().vm.$emit('filter', filteredTokens);
});
- it('does not update IssuableList with url params ', async () => {
- const defaultParams = {
- page_after: null,
- page_before: null,
- sort: 'created_date',
- state: 'opened',
- };
-
- expect(findIssuableList().props('urlParams')).toEqual(defaultParams);
+ it('does not update url params', () => {
+ expect(wrapper.vm.$router.push).not.toHaveBeenCalled();
});
it('shows an alert to tell the user they must be signed in to search', () => {
@@ -935,4 +933,23 @@ describe('CE IssuesListApp component', () => {
});
});
});
+
+ describe('public visibility', () => {
+ it.each`
+ description | isPublicVisibilityRestricted | isSignedIn | hideUsers
+ ${'shows users when public visibility is not restricted and is not signed in'} | ${false} | ${false} | ${false}
+ ${'shows users when public visibility is not restricted and is signed in'} | ${false} | ${true} | ${false}
+ ${'hides users when public visibility is restricted and is not signed in'} | ${true} | ${false} | ${true}
+ ${'shows users when public visibility is restricted and is signed in'} | ${true} | ${true} | ${false}
+ `('$description', ({ isPublicVisibilityRestricted, isSignedIn, hideUsers }) => {
+ const mockQuery = jest.fn().mockResolvedValue(defaultQueryResponse);
+ wrapper = mountComponent({
+ provide: { isPublicVisibilityRestricted, isSignedIn },
+ issuesQueryResponse: mockQuery,
+ });
+ jest.runOnlyPendingTimers();
+
+ expect(mockQuery).toHaveBeenCalledWith(expect.objectContaining({ hideUsers }));
+ });
+ });
});
diff --git a/spec/frontend/issues/list/mock_data.js b/spec/frontend/issues/list/mock_data.js
index b1a135ceb18..42f2d08082e 100644
--- a/spec/frontend/issues/list/mock_data.js
+++ b/spec/frontend/issues/list/mock_data.js
@@ -117,6 +117,7 @@ export const locationSearch = [
'not[author_username]=marge',
'assignee_username[]=bart',
'assignee_username[]=lisa',
+ 'assignee_username[]=5',
'not[assignee_username][]=patty',
'not[assignee_username][]=selma',
'milestone_title=season+3',
@@ -146,6 +147,8 @@ export const locationSearch = [
'not[epic_id]=34',
'weight=1',
'not[weight]=3',
+ 'crm_contact_id=123',
+ 'crm_organization_id=456',
].join('&');
export const locationSearchWithSpecialValues = [
@@ -165,6 +168,7 @@ export const filteredTokens = [
{ type: 'author_username', value: { data: 'marge', operator: OPERATOR_IS_NOT } },
{ type: 'assignee_username', value: { data: 'bart', operator: OPERATOR_IS } },
{ type: 'assignee_username', value: { data: 'lisa', operator: OPERATOR_IS } },
+ { type: 'assignee_username', value: { data: '5', operator: OPERATOR_IS } },
{ type: 'assignee_username', value: { data: 'patty', operator: OPERATOR_IS_NOT } },
{ type: 'assignee_username', value: { data: 'selma', operator: OPERATOR_IS_NOT } },
{ type: 'milestone', value: { data: 'season 3', operator: OPERATOR_IS } },
@@ -194,6 +198,8 @@ export const filteredTokens = [
{ type: 'epic_id', value: { data: '34', operator: OPERATOR_IS_NOT } },
{ type: 'weight', value: { data: '1', operator: OPERATOR_IS } },
{ type: 'weight', value: { data: '3', operator: OPERATOR_IS_NOT } },
+ { type: 'crm_contact', value: { data: '123', operator: OPERATOR_IS } },
+ { type: 'crm_organization', value: { data: '456', operator: OPERATOR_IS } },
{ type: 'filtered-search-term', value: { data: 'find' } },
{ type: 'filtered-search-term', value: { data: 'issues' } },
];
@@ -212,7 +218,7 @@ export const filteredTokensWithSpecialValues = [
export const apiParams = {
authorUsername: 'homer',
- assigneeUsernames: ['bart', 'lisa'],
+ assigneeUsernames: ['bart', 'lisa', '5'],
milestoneTitle: ['season 3', 'season 4'],
labelName: ['cartoon', 'tv'],
releaseTag: ['v3', 'v4'],
@@ -222,6 +228,8 @@ export const apiParams = {
iterationId: ['4', '12'],
epicId: '12',
weight: '1',
+ crmContactId: '123',
+ crmOrganizationId: '456',
not: {
authorUsername: 'marge',
assigneeUsernames: ['patty', 'selma'],
@@ -251,7 +259,7 @@ export const apiParamsWithSpecialValues = {
export const urlParams = {
author_username: 'homer',
'not[author_username]': 'marge',
- 'assignee_username[]': ['bart', 'lisa'],
+ 'assignee_username[]': ['bart', 'lisa', '5'],
'not[assignee_username][]': ['patty', 'selma'],
milestone_title: ['season 3', 'season 4'],
'not[milestone_title]': ['season 20', 'season 30'],
@@ -270,6 +278,8 @@ export const urlParams = {
'not[epic_id]': '34',
weight: '1',
'not[weight]': '3',
+ crm_contact_id: '123',
+ crm_organization_id: '456',
};
export const urlParamsWithSpecialValues = {
diff --git a/spec/frontend/issues/list/utils_spec.js b/spec/frontend/issues/list/utils_spec.js
index a60350d91c5..ce0477883d7 100644
--- a/spec/frontend/issues/list/utils_spec.js
+++ b/spec/frontend/issues/list/utils_spec.js
@@ -1,3 +1,5 @@
+import setWindowLocation from 'helpers/set_window_location_helper';
+import { TEST_HOST } from 'helpers/test_constants';
import {
apiParams,
apiParamsWithSpecialValues,
@@ -24,6 +26,7 @@ import {
getSortOptions,
isSortKey,
} from '~/issues/list/utils';
+import { FILTERED_SEARCH_TERM } from '~/vue_shared/components/filtered_search_bar/constants';
describe('getInitialPageParams', () => {
it.each(Object.keys(urlSortParams))(
@@ -124,24 +127,50 @@ describe('getFilterTokens', () => {
filteredTokensWithSpecialValues,
);
});
+
+ it.each`
+ description | argument
+ ${'an undefined value'} | ${undefined}
+ ${'an irrelevant value'} | ${'?unrecognised=parameter'}
+ `('returns an empty filtered search term given $description', ({ argument }) => {
+ expect(getFilterTokens(argument)).toEqual([
+ {
+ id: expect.any(String),
+ type: FILTERED_SEARCH_TERM,
+ value: { data: '' },
+ },
+ ]);
+ });
});
describe('convertToApiParams', () => {
+ beforeEach(() => {
+ setWindowLocation(TEST_HOST);
+ });
+
it('returns api params given filtered tokens', () => {
expect(convertToApiParams(filteredTokens)).toEqual(apiParams);
});
it('returns api params given filtered tokens with special values', () => {
+ setWindowLocation('?assignee_id=123');
+
expect(convertToApiParams(filteredTokensWithSpecialValues)).toEqual(apiParamsWithSpecialValues);
});
});
describe('convertToUrlParams', () => {
+ beforeEach(() => {
+ setWindowLocation(TEST_HOST);
+ });
+
it('returns url params given filtered tokens', () => {
expect(convertToUrlParams(filteredTokens)).toEqual(urlParams);
});
it('returns url params given filtered tokens with special values', () => {
+ setWindowLocation('?assignee_id=123');
+
expect(convertToUrlParams(filteredTokensWithSpecialValues)).toEqual(urlParamsWithSpecialValues);
});
});
diff --git a/spec/frontend/issues/show/components/app_spec.js b/spec/frontend/issues/show/components/app_spec.js
index 5ab64d8e9ca..27604b8ccf3 100644
--- a/spec/frontend/issues/show/components/app_spec.js
+++ b/spec/frontend/issues/show/components/app_spec.js
@@ -1,10 +1,12 @@
-import { GlIntersectionObserver } from '@gitlab/ui';
+import { GlIcon, GlIntersectionObserver } from '@gitlab/ui';
import MockAdapter from 'axios-mock-adapter';
import { nextTick } from 'vue';
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import waitForPromises from 'helpers/wait_for_promises';
import '~/behaviors/markdown/render_gfm';
-import { IssuableStatus, IssuableStatusText } from '~/issues/constants';
+import { IssuableStatus, IssuableStatusText, IssuableType } from '~/issues/constants';
import IssuableApp from '~/issues/show/components/app.vue';
import DescriptionComponent from '~/issues/show/components/description.vue';
import EditedComponent from '~/issues/show/components/edited.vue';
@@ -70,7 +72,7 @@ describe('Issuable output', () => {
};
beforeEach(() => {
- setFixtures(`
+ setHTMLFixture(`
<div>
<title>Title</title>
<div class="detail-page-description content-block">
@@ -105,6 +107,7 @@ describe('Issuable output', () => {
realtimeRequestCount = 0;
wrapper.vm.poll.stop();
wrapper.destroy();
+ resetHTMLFixture();
});
it('should render a title/description/edited and update title/description/edited on update', () => {
@@ -465,6 +468,31 @@ describe('Issuable output', () => {
expect(findStickyHeader().text()).toContain('Sticky header title');
});
+ it('shows with title for an epic', async () => {
+ wrapper.setProps({ issuableType: 'epic' });
+
+ await nextTick();
+
+ expect(findStickyHeader().text()).toContain('Sticky header title');
+ });
+
+ it.each`
+ issuableType | issuableStatus | statusIcon
+ ${IssuableType.Issue} | ${IssuableStatus.Open} | ${'issues'}
+ ${IssuableType.Issue} | ${IssuableStatus.Closed} | ${'issue-closed'}
+ ${IssuableType.Epic} | ${IssuableStatus.Open} | ${'epic'}
+ ${IssuableType.Epic} | ${IssuableStatus.Closed} | ${'epic-closed'}
+ `(
+ 'shows with state icon "$statusIcon" for $issuableType when status is $issuableStatus',
+ async ({ issuableType, issuableStatus, statusIcon }) => {
+ wrapper.setProps({ issuableType, issuableStatus });
+
+ await nextTick();
+
+ expect(findStickyHeader().findComponent(GlIcon).props('name')).toBe(statusIcon);
+ },
+ );
+
it.each`
title | state
${'shows with Open when status is opened'} | ${IssuableStatus.Open}
@@ -487,7 +515,14 @@ describe('Issuable output', () => {
await nextTick();
- expect(findConfidentialBadge().exists()).toBe(isConfidential);
+ const confidentialEl = findConfidentialBadge();
+ expect(confidentialEl.exists()).toBe(isConfidential);
+ if (isConfidential) {
+ expect(confidentialEl.props()).toMatchObject({
+ workspaceType: 'project',
+ issuableType: 'issue',
+ });
+ }
});
it.each`
@@ -613,4 +648,14 @@ describe('Issuable output', () => {
expect(wrapper.vm.updateStoreState).toHaveBeenCalled();
});
});
+
+ describe('listItemReorder event', () => {
+ it('makes request to update issue', async () => {
+ const description = 'I have been updated!';
+ findDescription().vm.$emit('listItemReorder', description);
+ await waitForPromises();
+
+ expect(mock.history.put[0].data).toContain(description);
+ });
+ });
});
diff --git a/spec/frontend/issues/show/components/description_spec.js b/spec/frontend/issues/show/components/description_spec.js
index 0b3daadae1d..1ae04531a6b 100644
--- a/spec/frontend/issues/show/components/description_spec.js
+++ b/spec/frontend/issues/show/components/description_spec.js
@@ -1,14 +1,20 @@
import $ from 'jquery';
-import { nextTick } from 'vue';
+import Vue, { nextTick } from 'vue';
+import VueApollo from 'vue-apollo';
import '~/behaviors/markdown/render_gfm';
import { GlTooltip, GlModal } from '@gitlab/ui';
+
import setWindowLocation from 'helpers/set_window_location_helper';
import { stubComponent } from 'helpers/stub_component';
import { TEST_HOST } from 'helpers/test_constants';
import { mockTracking } from 'helpers/tracking_helper';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+
import Description from '~/issues/show/components/description.vue';
import { updateHistory } from '~/lib/utils/url_utility';
+import workItemQuery from '~/work_items/graphql/work_item.query.graphql';
import TaskList from '~/task_list';
import WorkItemDetailModal from '~/work_items/components/work_item_detail_modal.vue';
import CreateWorkItem from '~/work_items/pages/create_work_item.vue';
@@ -27,17 +33,29 @@ jest.mock('~/task_list');
const showModal = jest.fn();
const hideModal = jest.fn();
+const showDetailsModal = jest.fn();
const $toast = {
show: jest.fn(),
};
+const workItemQueryResponse = {
+ data: {
+ workItem: null,
+ },
+};
+
+const queryHandler = jest.fn().mockResolvedValue(workItemQueryResponse);
+
describe('Description component', () => {
let wrapper;
+ Vue.use(VueApollo);
+
const findGfmContent = () => wrapper.find('[data-testid="gfm-content"]');
const findTextarea = () => wrapper.find('[data-testid="textarea"]');
const findTaskActionButtons = () => wrapper.findAll('.js-add-task');
const findConvertToTaskButton = () => wrapper.find('.js-add-task');
+ const findTaskLink = () => wrapper.find('a.gfm-issue');
const findTooltips = () => wrapper.findAllComponents(GlTooltip);
const findModal = () => wrapper.findComponent(GlModal);
@@ -52,6 +70,7 @@ describe('Description component', () => {
...props,
},
provide,
+ apolloProvider: createMockApollo([[workItemQuery, queryHandler]]),
mocks: {
$toast,
},
@@ -62,6 +81,11 @@ describe('Description component', () => {
hide: hideModal,
},
}),
+ WorkItemDetailModal: stubComponent(WorkItemDetailModal, {
+ methods: {
+ show: showDetailsModal,
+ },
+ }),
},
});
}
@@ -296,15 +320,15 @@ describe('Description component', () => {
});
it('shows toast after delete success', async () => {
- findWorkItemDetailModal().vm.$emit('workItemDeleted');
+ const newDesc = 'description';
+ findWorkItemDetailModal().vm.$emit('workItemDeleted', newDesc);
+ expect(wrapper.emitted('updateDescription')).toEqual([[newDesc]]);
expect($toast.show).toHaveBeenCalledWith('Work item deleted');
});
});
describe('work items detail', () => {
- const findTaskLink = () => wrapper.find('a.gfm-issue');
-
describe('when opening and closing', () => {
beforeEach(() => {
createComponent({
@@ -319,11 +343,9 @@ describe('Description component', () => {
});
it('opens when task button is clicked', async () => {
- expect(findWorkItemDetailModal().props('visible')).toBe(false);
-
await findTaskLink().trigger('click');
- expect(findWorkItemDetailModal().props('visible')).toBe(true);
+ expect(showDetailsModal).toHaveBeenCalled();
expect(updateHistory).toHaveBeenCalledWith({
url: `${TEST_HOST}/?work_item_id=2`,
replace: true,
@@ -333,12 +355,9 @@ describe('Description component', () => {
it('closes from an open state', async () => {
await findTaskLink().trigger('click');
- expect(findWorkItemDetailModal().props('visible')).toBe(true);
-
findWorkItemDetailModal().vm.$emit('close');
await nextTick();
- expect(findWorkItemDetailModal().props('visible')).toBe(false);
expect(updateHistory).toHaveBeenLastCalledWith({
url: `${TEST_HOST}/`,
replace: true,
@@ -364,16 +383,17 @@ describe('Description component', () => {
describe('when url query `work_item_id` exists', () => {
it.each`
- behavior | workItemId | visible
- ${'opens'} | ${'123'} | ${true}
- ${'does not open'} | ${'123e'} | ${false}
- ${'does not open'} | ${'12e3'} | ${false}
- ${'does not open'} | ${'1e23'} | ${false}
- ${'does not open'} | ${'x'} | ${false}
- ${'does not open'} | ${'undefined'} | ${false}
+ behavior | workItemId | modalOpened
+ ${'opens'} | ${'2'} | ${1}
+ ${'does not open'} | ${'123'} | ${0}
+ ${'does not open'} | ${'123e'} | ${0}
+ ${'does not open'} | ${'12e3'} | ${0}
+ ${'does not open'} | ${'1e23'} | ${0}
+ ${'does not open'} | ${'x'} | ${0}
+ ${'does not open'} | ${'undefined'} | ${0}
`(
'$behavior when url contains `work_item_id=$workItemId`',
- async ({ workItemId, visible }) => {
+ async ({ workItemId, modalOpened }) => {
setWindowLocation(`?work_item_id=${workItemId}`);
createComponent({
@@ -381,10 +401,43 @@ describe('Description component', () => {
provide: { glFeatures: { workItems: true } },
});
- expect(findWorkItemDetailModal().props('visible')).toBe(visible);
+ expect(showDetailsModal).toHaveBeenCalledTimes(modalOpened);
},
);
});
});
+
+ describe('when hovering task links', () => {
+ beforeEach(() => {
+ createComponent({
+ props: {
+ descriptionHtml: descriptionHtmlWithTask,
+ },
+ provide: {
+ glFeatures: { workItems: true },
+ },
+ });
+ return nextTick();
+ });
+
+ it('prefetches work item detail after work item link is hovered for 150ms', async () => {
+ await findTaskLink().trigger('mouseover');
+ jest.advanceTimersByTime(150);
+ await waitForPromises();
+
+ expect(queryHandler).toHaveBeenCalledWith({
+ id: 'gid://gitlab/WorkItem/2',
+ });
+ });
+
+ it('does not work item detail after work item link is hovered for less than 150ms', async () => {
+ await findTaskLink().trigger('mouseover');
+ await findTaskLink().trigger('mouseout');
+ jest.advanceTimersByTime(150);
+ await waitForPromises();
+
+ expect(queryHandler).not.toHaveBeenCalled();
+ });
+ });
});
});
diff --git a/spec/frontend/issues/show/components/fields/description_spec.js b/spec/frontend/issues/show/components/fields/description_spec.js
index 0dcd70ac19b..d0e33f0b980 100644
--- a/spec/frontend/issues/show/components/fields/description_spec.js
+++ b/spec/frontend/issues/show/components/fields/description_spec.js
@@ -24,7 +24,6 @@ describe('Description field component', () => {
beforeEach(() => {
jest.spyOn(eventHub, '$emit');
- gon.features = { markdownContinueLists: true };
});
afterEach(() => {
diff --git a/spec/frontend/issues/show/components/title_spec.js b/spec/frontend/issues/show/components/title_spec.js
index 29b5353ef1c..7560b733ae6 100644
--- a/spec/frontend/issues/show/components/title_spec.js
+++ b/spec/frontend/issues/show/components/title_spec.js
@@ -1,4 +1,5 @@
import Vue, { nextTick } from 'vue';
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import titleComponent from '~/issues/show/components/title.vue';
import eventHub from '~/issues/show/event_hub';
import Store from '~/issues/show/stores';
@@ -6,7 +7,7 @@ import Store from '~/issues/show/stores';
describe('Title component', () => {
let vm;
beforeEach(() => {
- setFixtures(`<title />`);
+ setHTMLFixture(`<title />`);
const Component = Vue.extend(titleComponent);
const store = new Store({
@@ -25,6 +26,10 @@ describe('Title component', () => {
}).$mount();
});
+ afterEach(() => {
+ resetHTMLFixture();
+ });
+
it('renders title HTML', () => {
expect(vm.$el.querySelector('.title').innerHTML.trim()).toBe('Testing <img>');
});
diff --git a/spec/frontend/issues/show/mock_data/mock_data.js b/spec/frontend/issues/show/mock_data/mock_data.js
index 7b0b8ca686a..909789b7a0f 100644
--- a/spec/frontend/issues/show/mock_data/mock_data.js
+++ b/spec/frontend/issues/show/mock_data/mock_data.js
@@ -77,7 +77,22 @@ export const descriptionHtmlWithTask = `
<ul data-sourcepos="1:1-3:7" class="task-list" dir="auto">
<li data-sourcepos="1:1-1:10" class="task-list-item">
<input type="checkbox" class="task-list-item-checkbox" disabled>
- <a href="/gitlab-org/gitlab-test/-/issues/48" data-original="#48+" data-link="false" data-link-reference="false" data-project="1" data-issue="2" data-reference-format="+" data-reference-type="task" data-container="body" data-placement="top" title="1" class="gfm gfm-issue has-tooltip">1 (#48)</a>
+ <a href="/gitlab-org/gitlab-test/-/issues/48" data-original="#48+" data-link="false" data-link-reference="false" data-project="1" data-issue="2" data-reference-format="+" data-reference-type="task" data-container="body" data-placement="top" title="1" class="gfm gfm-issue has-tooltip" data-issue-type="task">1 (#48)</a>
+ </li>
+ <li data-sourcepos="2:1-2:7" class="task-list-item">
+ <input type="checkbox" class="task-list-item-checkbox" disabled> 2
+ </li>
+ <li data-sourcepos="3:1-3:7" class="task-list-item">
+ <input type="checkbox" class="task-list-item-checkbox" disabled> 3
+ </li>
+ </ul>
+`;
+
+export const descriptionHtmlWithIssue = `
+ <ul data-sourcepos="1:1-3:7" class="task-list" dir="auto">
+ <li data-sourcepos="1:1-1:10" class="task-list-item">
+ <input type="checkbox" class="task-list-item-checkbox" disabled>
+ <a href="/gitlab-org/gitlab-test/-/issues/48" data-original="#48+" data-link="false" data-link-reference="false" data-project="1" data-issue="2" data-reference-format="+" data-reference-type="task" data-container="body" data-placement="top" title="1" class="gfm gfm-issue has-tooltip" data-issue-type="issue">1 (#48)</a>
</li>
<li data-sourcepos="2:1-2:7" class="task-list-item">
<input type="checkbox" class="task-list-item-checkbox" disabled> 2
diff --git a/spec/frontend/issues/show/utils_spec.js b/spec/frontend/issues/show/utils_spec.js
new file mode 100644
index 00000000000..e5f14cfc01a
--- /dev/null
+++ b/spec/frontend/issues/show/utils_spec.js
@@ -0,0 +1,40 @@
+import { convertDescriptionWithNewSort } from '~/issues/show/utils';
+
+describe('app/assets/javascripts/issues/show/utils.js', () => {
+ describe('convertDescriptionWithNewSort', () => {
+ it('converts markdown description with new list sort order', () => {
+ const description = `I am text
+
+- Item 1
+- Item 2
+ - Item 3
+ - Item 4
+- Item 5`;
+
+ // Drag Item 2 + children to Item 1's position
+ const html = `<ul data-sourcepos="3:1-8:0">
+ <li data-sourcepos="4:1-4:8">
+ Item 2
+ <ul data-sourcepos="5:1-6:10">
+ <li data-sourcepos="5:1-5:10">Item 3</li>
+ <li data-sourcepos="6:1-6:10">Item 4</li>
+ </ul>
+ </li>
+ <li data-sourcepos="3:1-3:8">Item 1</li>
+ <li data-sourcepos="7:1-8:0">Item 5</li>
+ <ul>`;
+ const list = document.createElement('div');
+ list.innerHTML = html;
+
+ const expected = `I am text
+
+- Item 2
+ - Item 3
+ - Item 4
+- Item 1
+- Item 5`;
+
+ expect(convertDescriptionWithNewSort(description, list.firstChild)).toBe(expected);
+ });
+ });
+});
diff --git a/spec/frontend/jira_connect/subscriptions/components/add_namespace_modal/groups_list_item_spec.js b/spec/frontend/jira_connect/subscriptions/components/add_namespace_modal/groups_list_item_spec.js
index 3d7bf7acb41..5df54abfc05 100644
--- a/spec/frontend/jira_connect/subscriptions/components/add_namespace_modal/groups_list_item_spec.js
+++ b/spec/frontend/jira_connect/subscriptions/components/add_namespace_modal/groups_list_item_spec.js
@@ -7,21 +7,35 @@ import * as JiraConnectApi from '~/jira_connect/subscriptions/api';
import GroupItemName from '~/jira_connect/subscriptions/components/group_item_name.vue';
import GroupsListItem from '~/jira_connect/subscriptions/components/add_namespace_modal/groups_list_item.vue';
import { persistAlert, reloadPage } from '~/jira_connect/subscriptions/utils';
+import {
+ I18N_ADD_SUBSCRIPTION_SUCCESS_ALERT_TITLE,
+ I18N_ADD_SUBSCRIPTION_SUCCESS_ALERT_MESSAGE,
+ INTEGRATIONS_DOC_LINK,
+} from '~/jira_connect/subscriptions/constants';
+import createStore from '~/jira_connect/subscriptions/store';
import { mockGroup1 } from '../../mock_data';
jest.mock('~/jira_connect/subscriptions/utils');
describe('GroupsListItem', () => {
let wrapper;
- const mockSubscriptionPath = 'subscriptionPath';
+ let store;
+
+ const mockAddSubscriptionsPath = '/addSubscriptionsPath';
+
+ const createComponent = ({ mountFn = shallowMount, provide } = {}) => {
+ store = createStore();
+
+ jest.spyOn(store, 'dispatch').mockImplementation();
- const createComponent = ({ mountFn = shallowMount } = {}) => {
wrapper = mountFn(GroupsListItem, {
+ store,
propsData: {
group: mockGroup1,
},
provide: {
- subscriptionsPath: mockSubscriptionPath,
+ addSubscriptionsPath: mockAddSubscriptionsPath,
+ ...provide,
},
});
};
@@ -51,62 +65,88 @@ describe('GroupsListItem', () => {
});
describe('on Link button click', () => {
- let addSubscriptionSpy;
+ describe('when jiraConnectOauth feature flag is disabled', () => {
+ let addSubscriptionSpy;
- beforeEach(() => {
- createComponent({ mountFn: mount });
+ beforeEach(() => {
+ createComponent({ mountFn: mount });
- addSubscriptionSpy = jest.spyOn(JiraConnectApi, 'addSubscription').mockResolvedValue();
- });
+ addSubscriptionSpy = jest.spyOn(JiraConnectApi, 'addSubscription').mockResolvedValue();
+ });
- it('sets button to loading and sends request', async () => {
- expect(findLinkButton().props('loading')).toBe(false);
+ it('sets button to loading and sends request', async () => {
+ expect(findLinkButton().props('loading')).toBe(false);
+
+ clickLinkButton();
+ await nextTick();
- clickLinkButton();
+ expect(findLinkButton().props('loading')).toBe(true);
+ await waitForPromises();
- await nextTick();
+ expect(addSubscriptionSpy).toHaveBeenCalledWith(
+ mockAddSubscriptionsPath,
+ mockGroup1.full_path,
+ );
+ expect(persistAlert).toHaveBeenCalledWith({
+ linkUrl: INTEGRATIONS_DOC_LINK,
+ message: I18N_ADD_SUBSCRIPTION_SUCCESS_ALERT_MESSAGE,
+ title: I18N_ADD_SUBSCRIPTION_SUCCESS_ALERT_TITLE,
+ variant: 'success',
+ });
+ });
- expect(findLinkButton().props('loading')).toBe(true);
+ describe('when request is successful', () => {
+ it('reloads the page', async () => {
+ clickLinkButton();
- await waitForPromises();
+ await waitForPromises();
- expect(addSubscriptionSpy).toHaveBeenCalledWith(mockSubscriptionPath, mockGroup1.full_path);
- expect(persistAlert).toHaveBeenCalledWith({
- linkUrl: '/help/integration/jira_development_panel.html#use-the-integration',
- message:
- 'You should now see GitLab.com activity inside your Jira Cloud issues. %{linkStart}Learn more%{linkEnd}',
- title: 'Namespace successfully linked',
- variant: 'success',
+ expect(reloadPage).toHaveBeenCalled();
+ });
});
- });
- describe('when request is successful', () => {
- it('reloads the page', async () => {
- clickLinkButton();
+ describe('when request has errors', () => {
+ const mockErrorMessage = 'error message';
+ const mockError = { response: { data: { error: mockErrorMessage } } };
- await waitForPromises();
+ beforeEach(() => {
+ addSubscriptionSpy = jest
+ .spyOn(JiraConnectApi, 'addSubscription')
+ .mockRejectedValue(mockError);
+ });
- expect(reloadPage).toHaveBeenCalled();
+ it('emits `error` event', async () => {
+ clickLinkButton();
+
+ await waitForPromises();
+
+ expect(reloadPage).not.toHaveBeenCalled();
+ expect(wrapper.emitted('error')[0][0]).toBe(mockErrorMessage);
+ });
});
});
- describe('when request has errors', () => {
- const mockErrorMessage = 'error message';
- const mockError = { response: { data: { error: mockErrorMessage } } };
+ describe('when jiraConnectOauth feature flag is enabled', () => {
+ const mockSubscriptionsPath = '/subscriptions';
beforeEach(() => {
- addSubscriptionSpy = jest
- .spyOn(JiraConnectApi, 'addSubscription')
- .mockRejectedValue(mockError);
+ createComponent({
+ mountFn: mount,
+ provide: {
+ subscriptionsPath: mockSubscriptionsPath,
+ glFeatures: { jiraConnectOauth: true },
+ },
+ });
});
- it('emits `error` event', async () => {
+ it('dispatches `addSubscription` action', async () => {
clickLinkButton();
+ await nextTick();
- await waitForPromises();
-
- expect(reloadPage).not.toHaveBeenCalled();
- expect(wrapper.emitted('error')[0][0]).toBe(mockErrorMessage);
+ expect(store.dispatch).toHaveBeenCalledWith('addSubscription', {
+ namespacePath: mockGroup1.full_path,
+ subscriptionsPath: mockSubscriptionsPath,
+ });
});
});
});
diff --git a/spec/frontend/jira_connect/subscriptions/components/app_spec.js b/spec/frontend/jira_connect/subscriptions/components/app_spec.js
index ce02144f22f..9894141be5a 100644
--- a/spec/frontend/jira_connect/subscriptions/components/app_spec.js
+++ b/spec/frontend/jira_connect/subscriptions/components/app_spec.js
@@ -3,8 +3,8 @@ import { nextTick } from 'vue';
import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
import JiraConnectApp from '~/jira_connect/subscriptions/components/app.vue';
-import SignInPage from '~/jira_connect/subscriptions/pages/sign_in.vue';
-import SubscriptionsPage from '~/jira_connect/subscriptions/pages/subscriptions.vue';
+import SignInPage from '~/jira_connect/subscriptions/pages/sign_in/sign_in_page.vue';
+import SubscriptionsPage from '~/jira_connect/subscriptions/pages/subscriptions_page.vue';
import UserLink from '~/jira_connect/subscriptions/components/user_link.vue';
import BrowserSupportAlert from '~/jira_connect/subscriptions/components/browser_support_alert.vue';
import createStore from '~/jira_connect/subscriptions/store';
@@ -12,6 +12,7 @@ import { SET_ALERT } from '~/jira_connect/subscriptions/store/mutation_types';
import { I18N_DEFAULT_SIGN_IN_ERROR_MESSAGE } from '~/jira_connect/subscriptions/constants';
import { __ } from '~/locale';
import AccessorUtilities from '~/lib/utils/accessor';
+import * as api from '~/jira_connect/subscriptions/api';
import { mockSubscription } from '../mock_data';
jest.mock('~/jira_connect/subscriptions/utils', () => ({
@@ -31,7 +32,8 @@ describe('JiraConnectApp', () => {
const findBrowserSupportAlert = () => wrapper.findComponent(BrowserSupportAlert);
const createComponent = ({ provide, mountFn = shallowMountExtended } = {}) => {
- store = createStore();
+ store = createStore({ subscriptions: [mockSubscription] });
+ jest.spyOn(store, 'dispatch').mockImplementation();
wrapper = mountFn(JiraConnectApp, {
store,
@@ -53,7 +55,6 @@ describe('JiraConnectApp', () => {
createComponent({
provide: {
usersPath,
- subscriptions: [mockSubscription],
},
});
});
@@ -79,14 +80,13 @@ describe('JiraConnectApp', () => {
createComponent({
provide: {
usersPath: '/user',
- subscriptions: [],
},
});
const userLink = findUserLink();
expect(userLink.exists()).toBe(true);
expect(userLink.props()).toEqual({
- hasSubscriptions: false,
+ hasSubscriptions: true,
user: null,
userSignedIn: false,
});
@@ -161,39 +161,11 @@ describe('JiraConnectApp', () => {
});
describe('when user signed out', () => {
- describe('when sign in page emits `sign-in-oauth` event', () => {
- const mockUser = { name: 'test' };
- beforeEach(async () => {
- createComponent({
- provide: {
- usersPath: '/mock',
- subscriptions: [],
- },
- });
- findSignInPage().vm.$emit('sign-in-oauth', mockUser);
-
- await nextTick();
- });
-
- it('hides sign in page and renders subscriptions page', () => {
- expect(findSignInPage().exists()).toBe(false);
- expect(findSubscriptionsPage().exists()).toBe(true);
- });
-
- it('sets correct UserLink props', () => {
- expect(findUserLink().props()).toMatchObject({
- user: mockUser,
- userSignedIn: true,
- });
- });
- });
-
describe('when sign in page emits `error` event', () => {
beforeEach(async () => {
createComponent({
provide: {
usersPath: '/mock',
- subscriptions: [],
},
});
findSignInPage().vm.$emit('error');
@@ -235,4 +207,31 @@ describe('JiraConnectApp', () => {
});
},
);
+
+ describe('when `jiraConnectOauth` feature flag is enabled', () => {
+ const mockSubscriptionsPath = '/mockSubscriptionsPath';
+
+ beforeEach(() => {
+ jest.spyOn(api, 'fetchSubscriptions').mockResolvedValue({ data: { subscriptions: [] } });
+
+ createComponent({
+ provide: {
+ glFeatures: { jiraConnectOauth: true },
+ subscriptionsPath: mockSubscriptionsPath,
+ },
+ });
+ });
+
+ describe('when component mounts', () => {
+ it('dispatches `fetchSubscriptions` action', async () => {
+ expect(store.dispatch).toHaveBeenCalledWith('fetchSubscriptions', mockSubscriptionsPath);
+ });
+ });
+
+ describe('when oauth button emits `sign-in-oauth` event', () => {
+ it('dispatches `fetchSubscriptions` action', () => {
+ expect(store.dispatch).toHaveBeenCalledWith('fetchSubscriptions', mockSubscriptionsPath);
+ });
+ });
+ });
});
diff --git a/spec/frontend/jira_connect/subscriptions/components/sign_in_oauth_button_spec.js b/spec/frontend/jira_connect/subscriptions/components/sign_in_oauth_button_spec.js
index 18274cd4362..8730e124ae7 100644
--- a/spec/frontend/jira_connect/subscriptions/components/sign_in_oauth_button_spec.js
+++ b/spec/frontend/jira_connect/subscriptions/components/sign_in_oauth_button_spec.js
@@ -11,9 +11,14 @@ import axios from '~/lib/utils/axios_utils';
import waitForPromises from 'helpers/wait_for_promises';
import httpStatus from '~/lib/utils/http_status';
import AccessorUtilities from '~/lib/utils/accessor';
+import { getCurrentUser } from '~/rest_api';
+import createStore from '~/jira_connect/subscriptions/store';
+import { SET_ACCESS_TOKEN } from '~/jira_connect/subscriptions/store/mutation_types';
jest.mock('~/lib/utils/accessor');
jest.mock('~/jira_connect/subscriptions/utils');
+jest.mock('~/jira_connect/subscriptions/api');
+jest.mock('~/rest_api');
jest.mock('~/jira_connect/subscriptions/pkce', () => ({
createCodeVerifier: jest.fn().mockReturnValue('mock-verifier'),
createCodeChallenge: jest.fn().mockResolvedValue('mock-challenge'),
@@ -28,9 +33,15 @@ const mockOauthMetadata = {
describe('SignInOauthButton', () => {
let wrapper;
let mockAxios;
+ let store;
const createComponent = ({ slots } = {}) => {
+ store = createStore();
+ jest.spyOn(store, 'dispatch').mockImplementation();
+ jest.spyOn(store, 'commit').mockImplementation();
+
wrapper = shallowMount(SignInOauthButton, {
+ store,
slots,
provide: {
oauthMetadata: mockOauthMetadata,
@@ -114,10 +125,6 @@ describe('SignInOauthButton', () => {
await waitForPromises();
});
- it('emits `error` event', () => {
- expect(wrapper.emitted('error')).toBeTruthy();
- });
-
it('does not emit `sign-in` event', () => {
expect(wrapper.emitted('sign-in')).toBeFalsy();
});
@@ -147,7 +154,7 @@ describe('SignInOauthButton', () => {
mockAxios
.onPost(mockOauthMetadata.oauth_token_url)
.replyOnce(httpStatus.OK, { access_token: mockAccessToken });
- mockAxios.onGet('/api/v4/user').replyOnce(httpStatus.OK, mockUser);
+ getCurrentUser.mockResolvedValue({ data: mockUser });
window.dispatchEvent(new MessageEvent('message', mockEvent));
@@ -161,25 +168,25 @@ describe('SignInOauthButton', () => {
});
});
- it('executes GET request to fetch user data', () => {
- expect(axios.get).toHaveBeenCalledWith('/api/v4/user', {
- headers: { Authorization: `Bearer ${mockAccessToken}` },
- });
+ it('dispatches loadCurrentUser action', () => {
+ expect(store.dispatch).toHaveBeenCalledWith('loadCurrentUser', mockAccessToken);
+ });
+
+ it('commits SET_ACCESS_TOKEN mutation with correct access token', () => {
+ expect(store.commit).toHaveBeenCalledWith(SET_ACCESS_TOKEN, mockAccessToken);
});
it('emits `sign-in` event with user data', () => {
- expect(wrapper.emitted('sign-in')[0]).toEqual([mockUser]);
+ expect(wrapper.emitted('sign-in')[0]).toBeTruthy();
});
});
describe('when API requests fail', () => {
beforeEach(async () => {
jest.spyOn(axios, 'post');
- jest.spyOn(axios, 'get');
mockAxios
.onPost(mockOauthMetadata.oauth_token_url)
- .replyOnce(httpStatus.INTERNAL_SERVER_ERROR, { access_token: mockAccessToken });
- mockAxios.onGet('/api/v4/user').replyOnce(httpStatus.INTERNAL_SERVER_ERROR, mockUser);
+ .replyOnce(httpStatus.INTERNAL_SERVER_ERROR);
window.dispatchEvent(new MessageEvent('message', mockEvent));
@@ -187,7 +194,7 @@ describe('SignInOauthButton', () => {
});
it('emits `error` event', () => {
- expect(wrapper.emitted('error')).toBeTruthy();
+ expect(wrapper.emitted('error')[0]).toEqual([]);
});
it('does not emit `sign-in` event', () => {
diff --git a/spec/frontend/jira_connect/subscriptions/components/subscriptions_list_spec.js b/spec/frontend/jira_connect/subscriptions/components/subscriptions_list_spec.js
index 2aad533f677..2d7c58fc278 100644
--- a/spec/frontend/jira_connect/subscriptions/components/subscriptions_list_spec.js
+++ b/spec/frontend/jira_connect/subscriptions/components/subscriptions_list_spec.js
@@ -20,12 +20,11 @@ describe('SubscriptionsList', () => {
let store;
const createComponent = () => {
- store = createStore();
+ store = createStore({
+ subscriptions: [mockSubscription],
+ });
wrapper = mount(SubscriptionsList, {
- provide: {
- subscriptions: [mockSubscription],
- },
store,
});
};
diff --git a/spec/frontend/jira_connect/subscriptions/pages/sign_in_spec.js b/spec/frontend/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_com_spec.js
index 97d1b077164..1649920b48b 100644
--- a/spec/frontend/jira_connect/subscriptions/pages/sign_in_spec.js
+++ b/spec/frontend/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_com_spec.js
@@ -1,6 +1,6 @@
import { shallowMount } from '@vue/test-utils';
-import SignInPage from '~/jira_connect/subscriptions/pages/sign_in.vue';
+import SignInGitlabCom from '~/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_com.vue';
import SignInLegacyButton from '~/jira_connect/subscriptions/components/sign_in_legacy_button.vue';
import SignInOauthButton from '~/jira_connect/subscriptions/components/sign_in_oauth_button.vue';
import SubscriptionsList from '~/jira_connect/subscriptions/components/subscriptions_list.vue';
@@ -15,7 +15,7 @@ const defaultProvide = {
usersPath: mockUsersPath,
};
-describe('SignInPage', () => {
+describe('SignInGitlabCom', () => {
let wrapper;
let store;
@@ -26,7 +26,7 @@ describe('SignInPage', () => {
const createComponent = ({ props, jiraConnectOauthEnabled } = {}) => {
store = createStore();
- wrapper = shallowMount(SignInPage, {
+ wrapper = shallowMount(SignInGitlabCom, {
store,
provide: {
...defaultProvide,
@@ -49,7 +49,7 @@ describe('SignInPage', () => {
describe('template', () => {
describe.each`
scenario | hasSubscriptions | signInButtonText
- ${'with subscriptions'} | ${true} | ${SignInPage.i18n.signInButtonTextWithSubscriptions}
+ ${'with subscriptions'} | ${true} | ${SignInGitlabCom.i18n.signInButtonTextWithSubscriptions}
${'without subscriptions'} | ${false} | ${I18N_DEFAULT_SIGN_IN_BUTTON_TEXT}
`('$scenario', ({ hasSubscriptions, signInButtonText }) => {
describe('when `jiraConnectOauthEnabled` feature flag is disabled', () => {
diff --git a/spec/frontend/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/index_spec.js b/spec/frontend/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/index_spec.js
new file mode 100644
index 00000000000..f4be8bf121d
--- /dev/null
+++ b/spec/frontend/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/index_spec.js
@@ -0,0 +1,83 @@
+import { nextTick } from 'vue';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+
+import SignInGitlabMultiversion from '~/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/index.vue';
+import VersionSelectForm from '~/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/version_select_form.vue';
+import SignInOauthButton from '~/jira_connect/subscriptions/components/sign_in_oauth_button.vue';
+
+describe('SignInGitlabMultiversion', () => {
+ let wrapper;
+
+ const findVersionSelectForm = () => wrapper.findComponent(VersionSelectForm);
+ const findSignInOauthButton = () => wrapper.findComponent(SignInOauthButton);
+ const findSubtitle = () => wrapper.findByTestId('subtitle');
+
+ const createComponent = () => {
+ wrapper = shallowMountExtended(SignInGitlabMultiversion);
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('when version is not selected', () => {
+ describe('VersionSelectForm', () => {
+ it('renders version select form', () => {
+ createComponent();
+
+ expect(findVersionSelectForm().exists()).toBe(true);
+ });
+
+ describe('when form emits "submit" event', () => {
+ it('hides the version select form and shows the sign in button', async () => {
+ createComponent();
+
+ findVersionSelectForm().vm.$emit('submit', 'gitlab.mycompany.com');
+ await nextTick();
+
+ expect(findVersionSelectForm().exists()).toBe(false);
+ expect(findSignInOauthButton().exists()).toBe(true);
+ });
+ });
+ });
+ });
+
+ describe('when version is selected', () => {
+ beforeEach(async () => {
+ createComponent();
+
+ findVersionSelectForm().vm.$emit('submit', 'gitlab.mycompany.com');
+ await nextTick();
+ });
+
+ describe('sign in button', () => {
+ it('renders sign in button', () => {
+ expect(findSignInOauthButton().exists()).toBe(true);
+ });
+
+ describe('when button emits `sign-in` event', () => {
+ it('emits `sign-in-oauth` event', () => {
+ const button = findSignInOauthButton();
+
+ const mockUser = { name: 'test' };
+ button.vm.$emit('sign-in', mockUser);
+
+ expect(wrapper.emitted('sign-in-oauth')[0]).toEqual([mockUser]);
+ });
+ });
+
+ describe('when button emits `error` event', () => {
+ it('emits `error` event', () => {
+ const button = findSignInOauthButton();
+ button.vm.$emit('error');
+
+ expect(wrapper.emitted('error')).toBeTruthy();
+ });
+ });
+ });
+
+ it('renders correct subtitle', () => {
+ expect(findSubtitle().text()).toBe(SignInGitlabMultiversion.i18n.signInSubtitle);
+ });
+ });
+});
diff --git a/spec/frontend/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/version_select_form_spec.js b/spec/frontend/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/version_select_form_spec.js
new file mode 100644
index 00000000000..29e7fe7a5b2
--- /dev/null
+++ b/spec/frontend/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/version_select_form_spec.js
@@ -0,0 +1,69 @@
+import { GlFormInput, GlFormRadioGroup, GlForm } from '@gitlab/ui';
+import { nextTick } from 'vue';
+
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import VersionSelectForm from '~/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/version_select_form.vue';
+
+describe('VersionSelectForm', () => {
+ let wrapper;
+
+ const findFormRadioGroup = () => wrapper.findComponent(GlFormRadioGroup);
+ const findForm = () => wrapper.findComponent(GlForm);
+ const findInput = () => wrapper.findComponent(GlFormInput);
+
+ const submitForm = () => findForm().vm.$emit('submit', new Event('submit'));
+
+ const createComponent = () => {
+ wrapper = shallowMountExtended(VersionSelectForm);
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('default state', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('selects saas radio option by default', () => {
+ expect(findFormRadioGroup().vm.$attrs.checked).toBe(VersionSelectForm.radioOptions.saas);
+ });
+
+ it('does not render instance input', () => {
+ expect(findInput().exists()).toBe(false);
+ });
+
+ describe('when form is submitted', () => {
+ it('emits "submit" event with gitlab.com as the payload', () => {
+ submitForm();
+
+ expect(wrapper.emitted('submit')[0][0]).toBe('https://gitlab.com');
+ });
+ });
+ });
+
+ describe('when "self-managed" radio option is selected', () => {
+ beforeEach(async () => {
+ createComponent();
+
+ findFormRadioGroup().vm.$emit('input', VersionSelectForm.radioOptions.selfManaged);
+ await nextTick();
+ });
+
+ it('reveals the self-managed input field', () => {
+ expect(findInput().exists()).toBe(true);
+ });
+
+ describe('when form is submitted', () => {
+ it('emits "submit" event with the input field value as the payload', () => {
+ const mockInstanceUrl = 'https://gitlab.example.com';
+
+ findInput().vm.$emit('input', mockInstanceUrl);
+ submitForm();
+
+ expect(wrapper.emitted('submit')[0][0]).toBe(mockInstanceUrl);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/jira_connect/subscriptions/pages/sign_in/sign_in_page_spec.js b/spec/frontend/jira_connect/subscriptions/pages/sign_in/sign_in_page_spec.js
new file mode 100644
index 00000000000..65b08fba592
--- /dev/null
+++ b/spec/frontend/jira_connect/subscriptions/pages/sign_in/sign_in_page_spec.js
@@ -0,0 +1,82 @@
+import { shallowMount } from '@vue/test-utils';
+
+import SignInPage from '~/jira_connect/subscriptions/pages/sign_in/sign_in_page.vue';
+import SignInGitlabCom from '~/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_com.vue';
+import SignInGitlabMultiversion from '~/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/index.vue';
+import createStore from '~/jira_connect/subscriptions/store';
+
+describe('SignInPage', () => {
+ let wrapper;
+ let store;
+
+ const findSignInGitlabCom = () => wrapper.findComponent(SignInGitlabCom);
+ const findSignInGitabMultiversion = () => wrapper.findComponent(SignInGitlabMultiversion);
+
+ const createComponent = ({
+ props = {},
+ jiraConnectOauthEnabled,
+ jiraConnectOauthSelfManagedEnabled,
+ } = {}) => {
+ store = createStore();
+
+ wrapper = shallowMount(SignInPage, {
+ store,
+ provide: {
+ glFeatures: {
+ jiraConnectOauth: jiraConnectOauthEnabled,
+ jiraConnectOauthSelfManaged: jiraConnectOauthSelfManagedEnabled,
+ },
+ },
+ propsData: { hasSubscriptions: false, ...props },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it.each`
+ jiraConnectOauthEnabled | jiraConnectOauthSelfManagedEnabled | shouldRenderDotCom | shouldRenderMultiversion
+ ${false} | ${false} | ${true} | ${false}
+ ${false} | ${true} | ${true} | ${false}
+ ${true} | ${false} | ${true} | ${false}
+ ${true} | ${true} | ${false} | ${true}
+ `(
+ 'renders correct component when jiraConnectOauth is $jiraConnectOauthEnabled and jiraConnectOauthSelfManaged is $jiraConnectOauthSelfManagedEnabled',
+ ({
+ jiraConnectOauthEnabled,
+ jiraConnectOauthSelfManagedEnabled,
+ shouldRenderDotCom,
+ shouldRenderMultiversion,
+ }) => {
+ createComponent({ jiraConnectOauthEnabled, jiraConnectOauthSelfManagedEnabled });
+
+ expect(findSignInGitlabCom().exists()).toBe(shouldRenderDotCom);
+ expect(findSignInGitabMultiversion().exists()).toBe(shouldRenderMultiversion);
+ },
+ );
+
+ describe('when jiraConnectOauthSelfManaged is false', () => {
+ beforeEach(() => {
+ createComponent({ jiraConnectOauthSelfManaged: false, props: { hasSubscriptions: true } });
+ });
+
+ it('renders SignInGitlabCom with correct props', () => {
+ expect(findSignInGitlabCom().props()).toEqual({ hasSubscriptions: true });
+ });
+
+ describe('when error event is emitted', () => {
+ it('emits another error event', () => {
+ findSignInGitlabCom().vm.$emit('error');
+ expect(wrapper.emitted('error')[0]).toBeTruthy();
+ });
+ });
+
+ describe('when sign-in-oauth event is emitted', () => {
+ it('emits another sign-in-oauth event', () => {
+ findSignInGitlabCom().vm.$emit('sign-in-oauth');
+ expect(wrapper.emitted('sign-in-oauth')[0]).toEqual([]);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/jira_connect/subscriptions/pages/subscriptions_page_spec.js b/spec/frontend/jira_connect/subscriptions/pages/subscriptions_page_spec.js
new file mode 100644
index 00000000000..4956af76ead
--- /dev/null
+++ b/spec/frontend/jira_connect/subscriptions/pages/subscriptions_page_spec.js
@@ -0,0 +1,71 @@
+import { GlEmptyState, GlLoadingIcon } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import SubscriptionsPage from '~/jira_connect/subscriptions/pages/subscriptions_page.vue';
+import AddNamespaceButton from '~/jira_connect/subscriptions/components/add_namespace_button.vue';
+import SubscriptionsList from '~/jira_connect/subscriptions/components/subscriptions_list.vue';
+import createStore from '~/jira_connect/subscriptions/store';
+
+describe('SubscriptionsPage', () => {
+ let wrapper;
+ let store;
+
+ const findAddNamespaceButton = () => wrapper.findComponent(AddNamespaceButton);
+ const findGlLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
+ const findSubscriptionsList = () => wrapper.findComponent(SubscriptionsList);
+ const findEmptyState = () => wrapper.findComponent(GlEmptyState);
+
+ const createComponent = ({ props, initialState } = {}) => {
+ store = createStore(initialState);
+
+ wrapper = shallowMount(SubscriptionsPage, {
+ store,
+ propsData: { hasSubscriptions: false, ...props },
+ stubs: {
+ GlEmptyState,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('template', () => {
+ describe.each`
+ scenario | subscriptionsLoading | hasSubscriptions | expectSubscriptionsList | expectEmptyState
+ ${'with subscriptions loading'} | ${true} | ${false} | ${false} | ${false}
+ ${'with subscriptions'} | ${false} | ${true} | ${true} | ${false}
+ ${'without subscriptions'} | ${false} | ${false} | ${false} | ${true}
+ `(
+ '$scenario',
+ ({ subscriptionsLoading, hasSubscriptions, expectEmptyState, expectSubscriptionsList }) => {
+ beforeEach(() => {
+ createComponent({
+ initialState: { subscriptionsLoading },
+ props: {
+ hasSubscriptions,
+ },
+ });
+ });
+
+ it(`${
+ subscriptionsLoading ? 'does not render' : 'renders'
+ } button to add namespace`, () => {
+ expect(findAddNamespaceButton().exists()).toBe(!subscriptionsLoading);
+ });
+
+ it(`${subscriptionsLoading ? 'renders' : 'does not render'} GlLoadingIcon`, () => {
+ expect(findGlLoadingIcon().exists()).toBe(subscriptionsLoading);
+ });
+
+ it(`${expectEmptyState ? 'renders' : 'does not render'} empty state`, () => {
+ expect(findEmptyState().exists()).toBe(expectEmptyState);
+ });
+
+ it(`${expectSubscriptionsList ? 'renders' : 'does not render'} subscriptions list`, () => {
+ expect(findSubscriptionsList().exists()).toBe(expectSubscriptionsList);
+ });
+ },
+ );
+ });
+});
diff --git a/spec/frontend/jira_connect/subscriptions/pages/subscriptions_spec.js b/spec/frontend/jira_connect/subscriptions/pages/subscriptions_spec.js
deleted file mode 100644
index 198278efc1f..00000000000
--- a/spec/frontend/jira_connect/subscriptions/pages/subscriptions_spec.js
+++ /dev/null
@@ -1,56 +0,0 @@
-import { GlEmptyState } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
-import SubscriptionsPage from '~/jira_connect/subscriptions/pages/subscriptions.vue';
-import AddNamespaceButton from '~/jira_connect/subscriptions/components/add_namespace_button.vue';
-import SubscriptionsList from '~/jira_connect/subscriptions/components/subscriptions_list.vue';
-import createStore from '~/jira_connect/subscriptions/store';
-
-describe('SubscriptionsPage', () => {
- let wrapper;
- let store;
-
- const findAddNamespaceButton = () => wrapper.findComponent(AddNamespaceButton);
- const findSubscriptionsList = () => wrapper.findComponent(SubscriptionsList);
- const findEmptyState = () => wrapper.findComponent(GlEmptyState);
-
- const createComponent = ({ props } = {}) => {
- store = createStore();
-
- wrapper = shallowMount(SubscriptionsPage, {
- store,
- propsData: props,
- });
- };
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- describe('template', () => {
- describe.each`
- scenario | expectSubscriptionsList | expectEmptyState
- ${'with subscriptions'} | ${true} | ${false}
- ${'without subscriptions'} | ${false} | ${true}
- `('$scenario', ({ expectEmptyState, expectSubscriptionsList }) => {
- beforeEach(() => {
- createComponent({
- props: {
- hasSubscriptions: expectSubscriptionsList,
- },
- });
- });
-
- it('renders button to add namespace', () => {
- expect(findAddNamespaceButton().exists()).toBe(true);
- });
-
- it(`${expectEmptyState ? 'renders' : 'does not render'} empty state`, () => {
- expect(findEmptyState().exists()).toBe(expectEmptyState);
- });
-
- it(`${expectSubscriptionsList ? 'renders' : 'does not render'} subscriptions list`, () => {
- expect(findSubscriptionsList().exists()).toBe(expectSubscriptionsList);
- });
- });
- });
-});
diff --git a/spec/frontend/jira_connect/subscriptions/store/actions_spec.js b/spec/frontend/jira_connect/subscriptions/store/actions_spec.js
new file mode 100644
index 00000000000..53b5d8e70af
--- /dev/null
+++ b/spec/frontend/jira_connect/subscriptions/store/actions_spec.js
@@ -0,0 +1,172 @@
+import testAction from 'helpers/vuex_action_helper';
+
+import * as types from '~/jira_connect/subscriptions/store/mutation_types';
+import {
+ fetchSubscriptions,
+ loadCurrentUser,
+ addSubscription,
+} from '~/jira_connect/subscriptions/store/actions';
+import state from '~/jira_connect/subscriptions/store/state';
+import * as api from '~/jira_connect/subscriptions/api';
+import * as userApi from '~/api/user_api';
+import * as integrationsApi from '~/api/integrations_api';
+import {
+ I18N_DEFAULT_SUBSCRIPTIONS_ERROR_MESSAGE,
+ I18N_ADD_SUBSCRIPTION_SUCCESS_ALERT_TITLE,
+ I18N_ADD_SUBSCRIPTION_SUCCESS_ALERT_MESSAGE,
+ INTEGRATIONS_DOC_LINK,
+} from '~/jira_connect/subscriptions/constants';
+import * as utils from '~/jira_connect/subscriptions/utils';
+
+describe('JiraConnect actions', () => {
+ let mockedState;
+
+ beforeEach(() => {
+ mockedState = state();
+ });
+
+ describe('fetchSubscriptions', () => {
+ const mockUrl = '/mock-url';
+
+ describe('when API request is successful', () => {
+ it('should commit SET_SUBSCRIPTIONS_LOADING and SET_SUBSCRIPTIONS mutations', async () => {
+ jest.spyOn(api, 'fetchSubscriptions').mockResolvedValue({ data: { subscriptions: [] } });
+
+ await testAction(
+ fetchSubscriptions,
+ mockUrl,
+ mockedState,
+ [
+ { type: types.SET_SUBSCRIPTIONS_LOADING, payload: true },
+ { type: types.SET_SUBSCRIPTIONS, payload: [] },
+ { type: types.SET_SUBSCRIPTIONS_LOADING, payload: false },
+ ],
+ [],
+ );
+
+ expect(api.fetchSubscriptions).toHaveBeenCalledWith(mockUrl);
+ });
+ });
+
+ describe('when API request fails', () => {
+ it('should commit SET_SUBSCRIPTIONS_LOADING, SET_SUBSCRIPTIONS_ERROR and SET_ALERT mutations', async () => {
+ jest.spyOn(api, 'fetchSubscriptions').mockRejectedValue();
+
+ await testAction(
+ fetchSubscriptions,
+ mockUrl,
+ mockedState,
+ [
+ { type: types.SET_SUBSCRIPTIONS_LOADING, payload: true },
+ { type: types.SET_SUBSCRIPTIONS_ERROR, payload: true },
+ {
+ type: types.SET_ALERT,
+ payload: { message: I18N_DEFAULT_SUBSCRIPTIONS_ERROR_MESSAGE, variant: 'danger' },
+ },
+ { type: types.SET_SUBSCRIPTIONS_LOADING, payload: false },
+ ],
+ [],
+ );
+
+ expect(api.fetchSubscriptions).toHaveBeenCalledWith(mockUrl);
+ });
+ });
+ });
+
+ describe('loadCurrentUser', () => {
+ const mockAccessToken = 'abcd1234';
+
+ describe('when API request succeeds', () => {
+ it('commits the SET_ACCESS_TOKEN and SET_CURRENT_USER mutations', async () => {
+ const mockUser = { name: 'root' };
+ jest.spyOn(userApi, 'getCurrentUser').mockResolvedValue({ data: mockUser });
+
+ await testAction(
+ loadCurrentUser,
+ mockAccessToken,
+ mockedState,
+ [{ type: types.SET_CURRENT_USER, payload: mockUser }],
+ [],
+ );
+
+ expect(userApi.getCurrentUser).toHaveBeenCalledWith({
+ headers: { Authorization: `Bearer ${mockAccessToken}` },
+ });
+ });
+ });
+
+ describe('when API request fails', () => {
+ it('commits the SET_CURRENT_USER_ERROR mutation', async () => {
+ jest.spyOn(userApi, 'getCurrentUser').mockRejectedValue();
+
+ await testAction(
+ loadCurrentUser,
+ mockAccessToken,
+ mockedState,
+ [{ type: types.SET_CURRENT_USER_ERROR }],
+ [],
+ );
+ });
+ });
+ });
+
+ describe('addSubscription', () => {
+ const mockNamespace = 'gitlab-org/gitlab';
+ const mockSubscriptionsPath = '/subscriptions';
+
+ beforeEach(() => {
+ jest.spyOn(utils, 'getJwt').mockReturnValue('1234');
+ });
+
+ describe('when API request succeeds', () => {
+ it('commits the SET_ACCESS_TOKEN and SET_CURRENT_USER mutations', async () => {
+ jest
+ .spyOn(integrationsApi, 'addJiraConnectSubscription')
+ .mockResolvedValue({ success: true });
+
+ await testAction(
+ addSubscription,
+ { namespacePath: mockNamespace, subscriptionsPath: mockSubscriptionsPath },
+ mockedState,
+ [
+ { type: types.ADD_SUBSCRIPTION_LOADING, payload: true },
+ {
+ type: types.SET_ALERT,
+ payload: {
+ title: I18N_ADD_SUBSCRIPTION_SUCCESS_ALERT_TITLE,
+ message: I18N_ADD_SUBSCRIPTION_SUCCESS_ALERT_MESSAGE,
+ linkUrl: INTEGRATIONS_DOC_LINK,
+ variant: 'success',
+ },
+ },
+ { type: types.ADD_SUBSCRIPTION_LOADING, payload: false },
+ ],
+ [{ type: 'fetchSubscriptions', payload: mockSubscriptionsPath }],
+ );
+
+ expect(integrationsApi.addJiraConnectSubscription).toHaveBeenCalledWith(mockNamespace, {
+ accessToken: null,
+ jwt: '1234',
+ });
+ });
+ });
+
+ describe('when API request fails', () => {
+ it('commits the SET_CURRENT_USER_ERROR mutation', async () => {
+ jest.spyOn(integrationsApi, 'addJiraConnectSubscription').mockRejectedValue();
+
+ await testAction(
+ addSubscription,
+ mockNamespace,
+ mockedState,
+ [
+ { type: types.ADD_SUBSCRIPTION_LOADING, payload: true },
+ { type: types.ADD_SUBSCRIPTION_ERROR },
+ { type: types.ADD_SUBSCRIPTION_LOADING, payload: false },
+ ],
+ [],
+ );
+ });
+ });
+ });
+});
diff --git a/spec/frontend/jira_connect/subscriptions/store/mutations_spec.js b/spec/frontend/jira_connect/subscriptions/store/mutations_spec.js
index 84a33dbf0b5..aeb136a76b9 100644
--- a/spec/frontend/jira_connect/subscriptions/store/mutations_spec.js
+++ b/spec/frontend/jira_connect/subscriptions/store/mutations_spec.js
@@ -25,4 +25,71 @@ describe('JiraConnect store mutations', () => {
});
});
});
+
+ describe('SET_SUBSCRIPTIONS', () => {
+ it('sets subscriptions loading flag', () => {
+ const mockSubscriptions = [{ name: 'test' }];
+ mutations.SET_SUBSCRIPTIONS(localState, mockSubscriptions);
+
+ expect(localState.subscriptions).toBe(mockSubscriptions);
+ });
+ });
+
+ describe('SET_SUBSCRIPTIONS_LOADING', () => {
+ it('sets subscriptions loading flag', () => {
+ mutations.SET_SUBSCRIPTIONS_LOADING(localState, true);
+
+ expect(localState.subscriptionsLoading).toBe(true);
+ });
+ });
+
+ describe('SET_SUBSCRIPTIONS_ERROR', () => {
+ it('sets subscriptions error', () => {
+ mutations.SET_SUBSCRIPTIONS_ERROR(localState, true);
+
+ expect(localState.subscriptionsError).toBe(true);
+ });
+ });
+
+ describe('ADD_SUBSCRIPTION_LOADING', () => {
+ it('sets addSubscriptionLoading', () => {
+ mutations.ADD_SUBSCRIPTION_LOADING(localState, true);
+
+ expect(localState.addSubscriptionLoading).toBe(true);
+ });
+ });
+
+ describe('ADD_SUBSCRIPTION_ERROR', () => {
+ it('sets addSubscriptionError', () => {
+ mutations.ADD_SUBSCRIPTION_ERROR(localState, true);
+
+ expect(localState.addSubscriptionError).toBe(true);
+ });
+ });
+
+ describe('SET_CURRENT_USER', () => {
+ it('sets currentUser', () => {
+ const mockUser = { name: 'root' };
+ mutations.SET_CURRENT_USER(localState, mockUser);
+
+ expect(localState.currentUser).toBe(mockUser);
+ });
+ });
+
+ describe('SET_CURRENT_USER_ERROR', () => {
+ it('sets currentUserError', () => {
+ mutations.SET_CURRENT_USER_ERROR(localState, true);
+
+ expect(localState.currentUserError).toBe(true);
+ });
+ });
+
+ describe('SET_ACCESS_TOKEN', () => {
+ it('sets accessToken', () => {
+ const mockAccessToken = 'asdf1234';
+ mutations.SET_ACCESS_TOKEN(localState, mockAccessToken);
+
+ expect(localState.accessToken).toBe(mockAccessToken);
+ });
+ });
});
diff --git a/spec/frontend/jobs/components/filtered_search/tokens/job_status_token_spec.js b/spec/frontend/jobs/components/filtered_search/tokens/job_status_token_spec.js
index ce8e482cc16..92ce3925a90 100644
--- a/spec/frontend/jobs/components/filtered_search/tokens/job_status_token_spec.js
+++ b/spec/frontend/jobs/components/filtered_search/tokens/job_status_token_spec.js
@@ -21,6 +21,7 @@ describe('Job Status Token', () => {
value: {
data: '',
},
+ cursorPosition: 'start',
};
const createComponent = () => {
diff --git a/spec/frontend/jobs/components/job_app_spec.js b/spec/frontend/jobs/components/job_app_spec.js
index 9abe66b4696..fc308766ab9 100644
--- a/spec/frontend/jobs/components/job_app_spec.js
+++ b/spec/frontend/jobs/components/job_app_spec.js
@@ -129,7 +129,9 @@ describe('Job App', () => {
const aYearAgo = new Date();
aYearAgo.setFullYear(aYearAgo.getFullYear() - 1);
- return setupAndMount({ jobData: { started: aYearAgo.toISOString() } });
+ return setupAndMount({
+ jobData: { started: aYearAgo.toISOString(), started_at: aYearAgo.toISOString() },
+ });
});
it('should render provided job information', () => {
diff --git a/spec/frontend/jobs/components/stuck_block_spec.js b/spec/frontend/jobs/components/stuck_block_spec.js
index 4db73eaaaec..1580ed45e46 100644
--- a/spec/frontend/jobs/components/stuck_block_spec.js
+++ b/spec/frontend/jobs/components/stuck_block_spec.js
@@ -32,7 +32,7 @@ describe('Stuck Block Job component', () => {
describe('with no runners for project', () => {
beforeEach(() => {
createWrapper({
- hasNoRunnersForProject: true,
+ hasOfflineRunnersForProject: true,
runnersPath: '/root/project/runners#js-runners-settings',
});
});
@@ -53,7 +53,7 @@ describe('Stuck Block Job component', () => {
describe('with tags', () => {
beforeEach(() => {
createWrapper({
- hasNoRunnersForProject: false,
+ hasOfflineRunnersForProject: false,
tags,
runnersPath: '/root/project/runners#js-runners-settings',
});
@@ -81,7 +81,7 @@ describe('Stuck Block Job component', () => {
describe('without active runners', () => {
beforeEach(() => {
createWrapper({
- hasNoRunnersForProject: false,
+ hasOfflineRunnersForProject: false,
runnersPath: '/root/project/runners#js-runners-settings',
});
});
diff --git a/spec/frontend/jobs/components/table/cells/actions_cell_spec.js b/spec/frontend/jobs/components/table/cells/actions_cell_spec.js
index 263698e94e1..976b128532d 100644
--- a/spec/frontend/jobs/components/table/cells/actions_cell_spec.js
+++ b/spec/frontend/jobs/components/table/cells/actions_cell_spec.js
@@ -1,8 +1,12 @@
import { GlModal } from '@gitlab/ui';
-import { nextTick } from 'vue';
+import Vue, { nextTick } from 'vue';
+import VueApollo from 'vue-apollo';
+import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { redirectTo } from '~/lib/utils/url_utility';
import ActionsCell from '~/jobs/components/table/cells/actions_cell.vue';
+import eventHub from '~/jobs/components/table/event_hub';
import JobPlayMutation from '~/jobs/components/table/graphql/mutations/job_play.mutation.graphql';
import JobRetryMutation from '~/jobs/components/table/graphql/mutations/job_retry.mutation.graphql';
import JobUnscheduleMutation from '~/jobs/components/table/graphql/mutations/job_unschedule.mutation.graphql';
@@ -15,11 +19,18 @@ import {
cannotRetryJob,
cannotPlayJob,
cannotPlayScheduledJob,
+ retryMutationResponse,
+ playMutationResponse,
+ cancelMutationResponse,
+ unscheduleMutationResponse,
} from '../../../mock_data';
+jest.mock('~/lib/utils/url_utility');
+
+Vue.use(VueApollo);
+
describe('Job actions cell', () => {
let wrapper;
- let mutate;
const findRetryButton = () => wrapper.findByTestId('retry');
const findPlayButton = () => wrapper.findByTestId('play');
@@ -31,29 +42,27 @@ describe('Job actions cell', () => {
const findModal = () => wrapper.findComponent(GlModal);
- const MUTATION_SUCCESS = { data: { JobRetryMutation: { jobId: retryableJob.id } } };
- const MUTATION_SUCCESS_UNSCHEDULE = {
- data: { JobUnscheduleMutation: { jobId: scheduledJob.id } },
- };
- const MUTATION_SUCCESS_PLAY = { data: { JobPlayMutation: { jobId: playableJob.id } } };
- const MUTATION_SUCCESS_CANCEL = { data: { JobCancelMutation: { jobId: cancelableJob.id } } };
+ const playMutationHandler = jest.fn().mockResolvedValue(playMutationResponse);
+ const retryMutationHandler = jest.fn().mockResolvedValue(retryMutationResponse);
+ const unscheduleMutationHandler = jest.fn().mockResolvedValue(unscheduleMutationResponse);
+ const cancelMutationHandler = jest.fn().mockResolvedValue(cancelMutationResponse);
const $toast = {
show: jest.fn(),
};
- const createComponent = (jobType, mutationType = MUTATION_SUCCESS, props = {}) => {
- mutate = jest.fn().mockResolvedValue(mutationType);
+ const createMockApolloProvider = (requestHandlers) => {
+ return createMockApollo(requestHandlers);
+ };
+ const createComponent = (jobType, requestHandlers, props = {}) => {
wrapper = shallowMountExtended(ActionsCell, {
propsData: {
job: jobType,
...props,
},
+ apolloProvider: createMockApolloProvider(requestHandlers),
mocks: {
- $apollo: {
- mutate,
- },
$toast,
},
});
@@ -101,24 +110,59 @@ describe('Job actions cell', () => {
});
it.each`
- button | mutationResult | action | jobType | mutationFile
- ${findPlayButton} | ${MUTATION_SUCCESS_PLAY} | ${'play'} | ${playableJob} | ${JobPlayMutation}
- ${findRetryButton} | ${MUTATION_SUCCESS} | ${'retry'} | ${retryableJob} | ${JobRetryMutation}
- ${findCancelButton} | ${MUTATION_SUCCESS_CANCEL} | ${'cancel'} | ${cancelableJob} | ${JobCancelMutation}
- `('performs the $action mutation', ({ button, mutationResult, jobType, mutationFile }) => {
- createComponent(jobType, mutationResult);
+ button | action | jobType | mutationFile | handler | jobId
+ ${findPlayButton} | ${'play'} | ${playableJob} | ${JobPlayMutation} | ${playMutationHandler} | ${playableJob.id}
+ ${findRetryButton} | ${'retry'} | ${retryableJob} | ${JobRetryMutation} | ${retryMutationHandler} | ${retryableJob.id}
+ ${findCancelButton} | ${'cancel'} | ${cancelableJob} | ${JobCancelMutation} | ${cancelMutationHandler} | ${cancelableJob.id}
+ `('performs the $action mutation', async ({ button, jobType, mutationFile, handler, jobId }) => {
+ createComponent(jobType, [[mutationFile, handler]]);
button().vm.$emit('click');
- expect(mutate).toHaveBeenCalledWith({
- mutation: mutationFile,
- variables: {
- id: jobType.id,
- },
- });
+ expect(handler).toHaveBeenCalledWith({ id: jobId });
});
it.each`
+ button | action | jobType | mutationFile | handler
+ ${findUnscheduleButton} | ${'unschedule'} | ${scheduledJob} | ${JobUnscheduleMutation} | ${unscheduleMutationHandler}
+ ${findCancelButton} | ${'cancel'} | ${cancelableJob} | ${JobCancelMutation} | ${cancelMutationHandler}
+ `(
+ 'the mutation action $action emits the jobActionPerformed event',
+ async ({ button, jobType, mutationFile, handler }) => {
+ jest.spyOn(eventHub, '$emit').mockImplementation(() => {});
+
+ createComponent(jobType, [[mutationFile, handler]]);
+
+ button().vm.$emit('click');
+
+ await waitForPromises();
+
+ expect(eventHub.$emit).toHaveBeenCalledWith('jobActionPerformed');
+ expect(redirectTo).not.toHaveBeenCalled();
+ },
+ );
+
+ it.each`
+ button | action | jobType | mutationFile | handler | redirectLink
+ ${findPlayButton} | ${'play'} | ${playableJob} | ${JobPlayMutation} | ${playMutationHandler} | ${'/root/project/-/jobs/1986'}
+ ${findRetryButton} | ${'retry'} | ${retryableJob} | ${JobRetryMutation} | ${retryMutationHandler} | ${'/root/project/-/jobs/1985'}
+ `(
+ 'the mutation action $action redirects to the job',
+ async ({ button, jobType, mutationFile, handler, redirectLink }) => {
+ jest.spyOn(eventHub, '$emit').mockImplementation(() => {});
+
+ createComponent(jobType, [[mutationFile, handler]]);
+
+ button().vm.$emit('click');
+
+ await waitForPromises();
+
+ expect(redirectTo).toHaveBeenCalledWith(redirectLink);
+ expect(eventHub.$emit).not.toHaveBeenCalled();
+ },
+ );
+
+ it.each`
button | action | jobType
${findPlayButton} | ${'play'} | ${playableJob}
${findRetryButton} | ${'retry'} | ${retryableJob}
@@ -152,20 +196,17 @@ describe('Job actions cell', () => {
});
it('unschedules a job', () => {
- createComponent(scheduledJob, MUTATION_SUCCESS_UNSCHEDULE);
+ createComponent(scheduledJob, [[JobUnscheduleMutation, unscheduleMutationHandler]]);
findUnscheduleButton().vm.$emit('click');
- expect(mutate).toHaveBeenCalledWith({
- mutation: JobUnscheduleMutation,
- variables: {
- id: scheduledJob.id,
- },
+ expect(unscheduleMutationHandler).toHaveBeenCalledWith({
+ id: scheduledJob.id,
});
});
it('shows the play job confirmation modal', async () => {
- createComponent(scheduledJob, MUTATION_SUCCESS);
+ createComponent(scheduledJob);
findPlayScheduledJobButton().vm.$emit('click');
diff --git a/spec/frontend/jobs/mock_data.js b/spec/frontend/jobs/mock_data.js
index 27b6c04eded..4676635cce0 100644
--- a/spec/frontend/jobs/mock_data.js
+++ b/spec/frontend/jobs/mock_data.js
@@ -1928,3 +1928,75 @@ export const CIJobConnectionExistingCache = {
};
export const mockFailedSearchToken = { type: 'status', value: { data: 'FAILED', operator: '=' } };
+
+export const retryMutationResponse = {
+ data: {
+ jobRetry: {
+ job: {
+ __typename: 'CiJob',
+ id: '"gid://gitlab/Ci::Build/1985"',
+ detailedStatus: {
+ detailsPath: '/root/project/-/jobs/1985',
+ id: 'pending-1985-1985',
+ __typename: 'DetailedStatus',
+ },
+ },
+ errors: [],
+ __typename: 'JobRetryPayload',
+ },
+ },
+};
+
+export const playMutationResponse = {
+ data: {
+ jobPlay: {
+ job: {
+ __typename: 'CiJob',
+ id: '"gid://gitlab/Ci::Build/1986"',
+ detailedStatus: {
+ detailsPath: '/root/project/-/jobs/1986',
+ id: 'pending-1986-1986',
+ __typename: 'DetailedStatus',
+ },
+ },
+ errors: [],
+ __typename: 'JobRetryPayload',
+ },
+ },
+};
+
+export const cancelMutationResponse = {
+ data: {
+ jobCancel: {
+ job: {
+ __typename: 'CiJob',
+ id: '"gid://gitlab/Ci::Build/1987"',
+ detailedStatus: {
+ detailsPath: '/root/project/-/jobs/1987',
+ id: 'pending-1987-1987',
+ __typename: 'DetailedStatus',
+ },
+ },
+ errors: [],
+ __typename: 'JobRetryPayload',
+ },
+ },
+};
+
+export const unscheduleMutationResponse = {
+ data: {
+ jobUnschedule: {
+ job: {
+ __typename: 'CiJob',
+ id: '"gid://gitlab/Ci::Build/1988"',
+ detailedStatus: {
+ detailsPath: '/root/project/-/jobs/1988',
+ id: 'pending-1988-1988',
+ __typename: 'DetailedStatus',
+ },
+ },
+ errors: [],
+ __typename: 'JobRetryPayload',
+ },
+ },
+};
diff --git a/spec/frontend/jobs/store/getters_spec.js b/spec/frontend/jobs/store/getters_spec.js
index f26c0cf00fd..c13b051c672 100644
--- a/spec/frontend/jobs/store/getters_spec.js
+++ b/spec/frontend/jobs/store/getters_spec.js
@@ -10,16 +10,18 @@ describe('Job Store Getters', () => {
describe('headerTime', () => {
describe('when the job has started key', () => {
- it('returns started key', () => {
+ it('returns started_at value', () => {
const started = '2018-08-31T16:20:49.023Z';
+ const startedAt = '2018-08-31T16:20:49.023Z';
+ localState.job.started_at = startedAt;
localState.job.started = started;
- expect(getters.headerTime(localState)).toEqual(started);
+ expect(getters.headerTime(localState)).toEqual(startedAt);
});
});
describe('when the job does not have started key', () => {
- it('returns created_at key', () => {
+ it('returns created_at value', () => {
const created = '2018-08-31T16:20:49.023Z';
localState.job.created_at = created;
@@ -58,7 +60,7 @@ describe('Job Store Getters', () => {
describe('shouldRenderTriggeredLabel', () => {
describe('when started equals null', () => {
it('returns false', () => {
- localState.job.started = null;
+ localState.job.started_at = null;
expect(getters.shouldRenderTriggeredLabel(localState)).toEqual(false);
});
@@ -66,7 +68,7 @@ describe('Job Store Getters', () => {
describe('when started equals string', () => {
it('returns true', () => {
- localState.job.started = '2018-08-31T16:20:49.023Z';
+ localState.job.started_at = '2018-08-31T16:20:49.023Z';
expect(getters.shouldRenderTriggeredLabel(localState)).toEqual(true);
});
@@ -206,7 +208,7 @@ describe('Job Store Getters', () => {
});
});
- describe('hasRunnersForProject', () => {
+ describe('hasOfflineRunnersForProject', () => {
describe('with available and offline runners', () => {
it('returns true', () => {
localState.job.runners = {
@@ -214,7 +216,7 @@ describe('Job Store Getters', () => {
online: false,
};
- expect(getters.hasRunnersForProject(localState)).toEqual(true);
+ expect(getters.hasOfflineRunnersForProject(localState)).toEqual(true);
});
});
@@ -225,7 +227,7 @@ describe('Job Store Getters', () => {
online: false,
};
- expect(getters.hasRunnersForProject(localState)).toEqual(false);
+ expect(getters.hasOfflineRunnersForProject(localState)).toEqual(false);
});
});
@@ -236,7 +238,7 @@ describe('Job Store Getters', () => {
online: true,
};
- expect(getters.hasRunnersForProject(localState)).toEqual(false);
+ expect(getters.hasOfflineRunnersForProject(localState)).toEqual(false);
});
});
});
diff --git a/spec/frontend/lib/dompurify_spec.js b/spec/frontend/lib/dompurify_spec.js
index 47a94a4dcde..34325dad6a1 100644
--- a/spec/frontend/lib/dompurify_spec.js
+++ b/spec/frontend/lib/dompurify_spec.js
@@ -73,6 +73,16 @@ describe('~/lib/dompurify', () => {
expect(sanitize('<p><gl-emoji>💯</gl-emoji></p>')).toBe('<p><gl-emoji>💯</gl-emoji></p>');
});
+ it("doesn't allow style tags", () => {
+ // removes style tags
+ expect(sanitize('<style>p {width:50%;}</style>')).toBe('');
+ expect(sanitize('<style type="text/css">p {width:50%;}</style>')).toBe('');
+ // removes mstyle tag (this can removed later by disallowing math tags)
+ expect(sanitize('<math><mstyle displaystyle="true"></mstyle></math>')).toBe('<math></math>');
+ // removes link tag (this is DOMPurify's default behavior)
+ expect(sanitize('<link rel="stylesheet" href="styles.css">')).toBe('');
+ });
+
describe.each`
type | gon
${'root'} | ${rootGon}
diff --git a/spec/frontend/lib/gfm/index_spec.js b/spec/frontend/lib/gfm/index_spec.js
index 5c72b5a51a7..c9a480e9943 100644
--- a/spec/frontend/lib/gfm/index_spec.js
+++ b/spec/frontend/lib/gfm/index_spec.js
@@ -33,14 +33,16 @@ describe('gfm', () => {
});
it('returns the result of executing the renderer function', async () => {
+ const rendered = { value: 'rendered tree' };
+
const result = await render({
markdown: '<strong>This is bold text</strong>',
renderer: () => {
- return 'rendered tree';
+ return rendered;
},
});
- expect(result).toBe('rendered tree');
+ expect(result).toEqual(rendered);
});
});
});
diff --git a/spec/frontend/lib/utils/common_utils_spec.js b/spec/frontend/lib/utils/common_utils_spec.js
index 763a9bd30fe..8e499844406 100644
--- a/spec/frontend/lib/utils/common_utils_spec.js
+++ b/spec/frontend/lib/utils/common_utils_spec.js
@@ -283,6 +283,75 @@ describe('common_utils', () => {
});
});
+ describe('insertText', () => {
+ let textArea;
+
+ beforeAll(() => {
+ textArea = document.createElement('textarea');
+ document.querySelector('body').appendChild(textArea);
+ textArea.value = 'two';
+ textArea.setSelectionRange(0, 0);
+ textArea.focus();
+ });
+
+ afterAll(() => {
+ textArea.parentNode.removeChild(textArea);
+ });
+
+ describe('using execCommand', () => {
+ beforeAll(() => {
+ document.execCommand = jest.fn(() => true);
+ });
+
+ it('inserts the text', () => {
+ commonUtils.insertText(textArea, 'one');
+
+ expect(document.execCommand).toHaveBeenCalledWith('insertText', false, 'one');
+ });
+
+ it('removes selected text', () => {
+ textArea.setSelectionRange(0, textArea.value.length);
+
+ commonUtils.insertText(textArea, '');
+
+ expect(document.execCommand).toHaveBeenCalledWith('delete');
+ });
+ });
+
+ describe('using fallback', () => {
+ beforeEach(() => {
+ document.execCommand = jest.fn(() => false);
+ jest.spyOn(textArea, 'dispatchEvent');
+ textArea.value = 'two';
+ textArea.setSelectionRange(0, 0);
+ });
+
+ it('inserts the text', () => {
+ commonUtils.insertText(textArea, 'one');
+
+ expect(textArea.value).toBe('onetwo');
+ expect(textArea.dispatchEvent).toHaveBeenCalled();
+ });
+
+ it('replaces the selection', () => {
+ textArea.setSelectionRange(0, textArea.value.length);
+
+ commonUtils.insertText(textArea, 'one');
+
+ expect(textArea.value).toBe('one');
+ expect(textArea.selectionStart).toBe(textArea.value.length);
+ });
+
+ it('removes selected text', () => {
+ textArea.setSelectionRange(0, textArea.value.length);
+
+ commonUtils.insertText(textArea, '');
+
+ expect(textArea.value).toBe('');
+ });
+ });
+ });
+
describe('normalizedHeaders', () => {
it('should upperCase all the header keys to keep them consistent', () => {
const apiHeaders = {
diff --git a/spec/frontend/lib/utils/datetime_utility_spec.js b/spec/frontend/lib/utils/datetime_utility_spec.js
index 7a64b654baa..8d989350173 100644
--- a/spec/frontend/lib/utils/datetime_utility_spec.js
+++ b/spec/frontend/lib/utils/datetime_utility_spec.js
@@ -308,7 +308,9 @@ describe('datefix', () => {
});
describe('parsePikadayDate', () => {
- // removed because of https://gitlab.com/gitlab-org/gitlab-foss/issues/39834
+ it('should return a UTC date', () => {
+ expect(datetimeUtility.parsePikadayDate('2020-01-29')).toEqual(new Date(2020, 0, 29));
+ });
});
describe('pikadayToString', () => {
diff --git a/spec/frontend/lib/utils/dom_utils_spec.js b/spec/frontend/lib/utils/dom_utils_spec.js
index 2f240f25d2a..88dac449527 100644
--- a/spec/frontend/lib/utils/dom_utils_spec.js
+++ b/spec/frontend/lib/utils/dom_utils_spec.js
@@ -1,3 +1,4 @@
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import {
addClassIfElementExists,
canScrollUp,
@@ -6,6 +7,7 @@ import {
isElementVisible,
isElementHidden,
getParents,
+ getParentByTagName,
setAttributes,
} from '~/lib/utils/dom_utils';
@@ -23,10 +25,14 @@ describe('DOM Utils', () => {
let parentElement;
beforeEach(() => {
- setFixtures(fixture);
+ setHTMLFixture(fixture);
parentElement = document.querySelector('.parent');
});
+ afterEach(() => {
+ resetHTMLFixture();
+ });
+
it('adds class if element exists', () => {
const childElement = parentElement.querySelector('.child');
@@ -126,10 +132,14 @@ describe('DOM Utils', () => {
let element;
beforeEach(() => {
- setFixtures('<div data-foo-bar data-baz data-qux="">');
+ setHTMLFixture('<div data-foo-bar data-baz data-qux="">');
element = document.querySelector('[data-foo-bar]');
});
+ afterEach(() => {
+ resetHTMLFixture();
+ });
+
it('throws if not given an element', () => {
expect(() => parseBooleanDataAttributes(null, ['baz'])).toThrow();
});
@@ -210,6 +220,21 @@ describe('DOM Utils', () => {
});
});
+ describe('getParentByTagName', () => {
+ const el = document.createElement('div');
+ el.innerHTML = '<p><span><strong><mark>hello world';
+
+ it.each`
+ tagName | parent
+ ${'strong'} | ${el.querySelector('strong')}
+ ${'span'} | ${el.querySelector('span')}
+ ${'p'} | ${el.querySelector('p')}
+ ${'pre'} | ${undefined}
+ `('gets a parent by tag name', ({ tagName, parent }) => {
+ expect(getParentByTagName(el.querySelector('mark'), tagName)).toBe(parent);
+ });
+ });
+
describe('setAttributes', () => {
it('sets multiple attribues on element', () => {
const div = document.createElement('div');
diff --git a/spec/frontend/lib/utils/file_upload_spec.js b/spec/frontend/lib/utils/file_upload_spec.js
index ff11107ea60..f63af2fe0a4 100644
--- a/spec/frontend/lib/utils/file_upload_spec.js
+++ b/spec/frontend/lib/utils/file_upload_spec.js
@@ -1,8 +1,9 @@
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import fileUpload, { getFilename, validateImageName } from '~/lib/utils/file_upload';
describe('File upload', () => {
beforeEach(() => {
- setFixtures(`
+ setHTMLFixture(`
<form>
<button class="js-button" type="button">Click me!</button>
<input type="text" class="js-input" />
@@ -11,6 +12,10 @@ describe('File upload', () => {
`);
});
+ afterEach(() => {
+ resetHTMLFixture();
+ });
+
describe('when there is a matching button and input', () => {
beforeEach(() => {
fileUpload('.js-button', '.js-input');
diff --git a/spec/frontend/lib/utils/mock_data.js b/spec/frontend/lib/utils/mock_data.js
index df1f79529e7..49a2af8b307 100644
--- a/spec/frontend/lib/utils/mock_data.js
+++ b/spec/frontend/lib/utils/mock_data.js
@@ -3,3 +3,45 @@ export const faviconDataUrl =
export const overlayDataUrl =
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAA85JREFUWAntVllIVGEUPv/9b46O41KplYN7PeRkti8TjQlhCUGh3MmeQugpIsGKAi2soIcIooiohxYKK2daqDAlIpIiWwxtQaJcaHE0d5tMrbn37z9XRqfR0TvVW56Hudf//uec72zfEWBCJjIwkYGJDPzvGSD/KgExN3Oi2Q+2DJgSDYQEMwItVGH1iZGmJw/Si1y+/PwVAMYYib22MYc/8hVQFgKDEfYoId0KYzagAQebsos/ewMZoeB9wdffcTYpQSaCTWHKoqSQaDk7zkIt0+aCUR8BelEHrf3dUNv9AcqbnsHtT5UKB/hTASh0SLYjnjb/CIDRJi0XiFAaJOpCD8zLpdb4NB66b1OfelthX815dtdRRfiti2aAXLvVLiMQ6olGyztGDkSo4JGGXk8/QFdGpYzpHG2GBQTDhtgVhPEaVbbVpvI6GJz22rv4TcAfrYI1x7Rj5MWWAppomKFVVb2302SFzUkZHAbkG+0b1+Gh77yNYjrmqnWTrLBLRxdvBWv8qlFujH/kYjJYyvLkj71t78zAUvzMAMnHhpN4zf9UREJhd8omyssxu1IgazQDwDnHUcNuH6vhPIE1fmuBzHt74Hn7W89jWGtcAjoaIDOFrdcMYJBkgOCoaRF0Lj0oglddDbCj6tRvKjphEpgjkzEQs2YAKsNxMzjn3nKurhzK+Ly7xe28ua8TwgMMcHJZnvvT0BPtEEKM4tDJ+C8GvIIk4ylINIXVZ0EUKJxYuh3mhCeokbudl6TtVc88dfBdLwbyaWB6zQCYQJpBYSrDGQxBQ/ZWRM2B+VNmQnVnHWx7elyNuL2/R336co7KyJR8CL9oLgEuFlREevWUkEl6uGwpVEG4FBm0OEf9N10NMgPlvWYAuNVwsWDKvcUNYsHUWTCZ13ysyFEXe6TO6aC8CUr9IiK+A05TQrc8yjwmxARHeeMAPlfQJw+AQRwu0YhL/GDXi9NwufG+S8dYkuYMqIb4SsWthotlNMOUCOM6r+G9cqXxPmd1dqrBav/o1zJy2l5/NUjJA/VORwYuFnOUaTQcPs9wMqwV++Xv8oADxKAcZ8nLPr8AoGW+xR6HSqYk3GodAz2QNj0V+Gr26dT9ASNH5239Pf0gktVNWZca8ZvfAFBprWS6hSu1pqt++Y0PD+WIwDAhIWQGtzvSHDbcodfFUFB9hg1Gjs5LXqIdFL+acFBl+FddqYwdxsWC3I70OvgfUaA65zhq2O2c8VxYcyIGFTVlXegYtvCXANCQZJMobjVcLMjtSK/IcEgyOOe8Ve5w7ryKDefp2P3+C/5ohv8HZmVLAAAAAElFTkSuQmCC';
+
+const absoluteUrls = [
+ 'http://example.org',
+ 'http://example.org:8080',
+ 'https://example.org',
+ 'https://example.org:8080',
+ 'https://192.168.1.1',
+];
+
+const rootRelativeUrls = ['/relative/link'];
+
+const relativeUrls = ['./relative/link', '../relative/link'];
+
+const urlsWithoutHost = ['http://', 'https://', 'https:https:https:'];
+
+/* eslint-disable no-script-url */
+const nonHttpUrls = [
+ 'javascript:',
+ 'javascript:alert("XSS")',
+ 'jav\tascript:alert("XSS");',
+ ' &#14; javascript:alert("XSS");',
+ 'ftp://192.168.1.1',
+ 'file:///',
+ 'file:///etc/hosts',
+];
+/* eslint-enable no-script-url */
+
+// javascript:alert('XSS')
+const encodedJavaScriptUrls = [
+ '&#0000106&#0000097&#0000118&#0000097&#0000115&#0000099&#0000114&#0000105&#0000112&#0000116&#0000058&#0000097&#0000108&#0000101&#0000114&#0000116&#0000040&#0000039&#0000088&#0000083&#0000083&#0000039&#0000041',
+ '&#106;&#97;&#118;&#97;&#115;&#99;&#114;&#105;&#112;&#116;&#58;&#97;&#108;&#101;&#114;&#116;&#40;&#39;&#88;&#83;&#83;&#39;&#41;',
+ '&#x6A&#x61&#x76&#x61&#x73&#x63&#x72&#x69&#x70&#x74&#x3A&#x61&#x6C&#x65&#x72&#x74&#x28&#x27&#x58&#x53&#x53&#x27&#x29',
+ '\\u006A\\u0061\\u0076\\u0061\\u0073\\u0063\\u0072\\u0069\\u0070\\u0074\\u003A\\u0061\\u006C\\u0065\\u0072\\u0074\\u0028\\u0027\\u0058\\u0053\\u0053\\u0027\\u0029',
+];
+
+export const safeUrls = [...absoluteUrls, ...rootRelativeUrls];
+export const unsafeUrls = [
+ ...relativeUrls,
+ ...urlsWithoutHost,
+ ...nonHttpUrls,
+ ...encodedJavaScriptUrls,
+];
diff --git a/spec/frontend/lib/utils/navigation_utility_spec.js b/spec/frontend/lib/utils/navigation_utility_spec.js
index 6a880a0f354..632a8904578 100644
--- a/spec/frontend/lib/utils/navigation_utility_spec.js
+++ b/spec/frontend/lib/utils/navigation_utility_spec.js
@@ -1,3 +1,4 @@
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import findAndFollowLink from '~/lib/utils/navigation_utility';
import * as navigationUtils from '~/lib/utils/navigation_utility';
import { visitUrl } from '~/lib/utils/url_utility';
@@ -8,11 +9,13 @@ describe('findAndFollowLink', () => {
it('visits a link when the selector exists', () => {
const href = '/some/path';
- setFixtures(`<a class="my-shortcut" href="${href}">link</a>`);
+ setHTMLFixture(`<a class="my-shortcut" href="${href}">link</a>`);
findAndFollowLink('.my-shortcut');
expect(visitUrl).toHaveBeenCalledWith(href);
+
+ resetHTMLFixture();
});
it('does not throw an exception when the selector does not exist', () => {
diff --git a/spec/frontend/lib/utils/resize_observer_spec.js b/spec/frontend/lib/utils/resize_observer_spec.js
index 6560562f204..c88ba73ebc6 100644
--- a/spec/frontend/lib/utils/resize_observer_spec.js
+++ b/spec/frontend/lib/utils/resize_observer_spec.js
@@ -1,3 +1,4 @@
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import { contentTop } from '~/lib/utils/common_utils';
import { scrollToTargetOnResize } from '~/lib/utils/resize_observer';
@@ -19,7 +20,7 @@ describe('ResizeObserver Utility', () => {
jest.spyOn(document.documentElement, 'scrollTo');
- setFixtures(`<div id="content-body"><div id="note_1234">note to scroll to</div></div>`);
+ setHTMLFixture(`<div id="content-body"><div id="note_1234">note to scroll to</div></div>`);
const target = document.querySelector('#note_1234');
@@ -28,6 +29,7 @@ describe('ResizeObserver Utility', () => {
afterEach(() => {
contentTop.mockReset();
+ resetHTMLFixture();
});
describe('Observer behavior', () => {
diff --git a/spec/frontend/lib/utils/text_markdown_spec.js b/spec/frontend/lib/utils/text_markdown_spec.js
index 103305f0797..d1bca3c73b6 100644
--- a/spec/frontend/lib/utils/text_markdown_spec.js
+++ b/spec/frontend/lib/utils/text_markdown_spec.js
@@ -1,5 +1,10 @@
import $ from 'jquery';
-import { insertMarkdownText, keypressNoteText } from '~/lib/utils/text_markdown';
+import {
+ insertMarkdownText,
+ keypressNoteText,
+ compositionStartNoteText,
+ compositionEndNoteText,
+} from '~/lib/utils/text_markdown';
import '~/lib/utils/jquery_at_who';
describe('init markdown', () => {
@@ -9,6 +14,9 @@ describe('init markdown', () => {
textArea = document.createElement('textarea');
document.querySelector('body').appendChild(textArea);
textArea.focus();
+
+ // needed for the underlying insertText to work
+ document.execCommand = jest.fn(() => false);
});
afterAll(() => {
@@ -172,7 +180,9 @@ describe('init markdown', () => {
const enterEvent = new KeyboardEvent('keydown', { key: 'Enter' });
beforeEach(() => {
- gon.features = { markdownContinueLists: true };
+ textArea.addEventListener('keydown', keypressNoteText);
+ textArea.addEventListener('compositionstart', compositionStartNoteText);
+ textArea.addEventListener('compositionend', compositionEndNoteText);
});
it.each`
@@ -203,7 +213,6 @@ describe('init markdown', () => {
textArea.value = text;
textArea.setSelectionRange(text.length, text.length);
- textArea.addEventListener('keydown', keypressNoteText);
textArea.dispatchEvent(enterEvent);
expect(textArea.value).toEqual(expected);
@@ -231,7 +240,6 @@ describe('init markdown', () => {
textArea.value = text;
textArea.setSelectionRange(text.length, text.length);
- textArea.addEventListener('keydown', keypressNoteText);
textArea.dispatchEvent(enterEvent);
expect(textArea.value.substr(0, textArea.selectionStart)).toEqual(expected);
@@ -251,7 +259,6 @@ describe('init markdown', () => {
textArea.value = text;
textArea.setSelectionRange(text.length, text.length);
- textArea.addEventListener('keydown', keypressNoteText);
textArea.dispatchEvent(enterEvent);
expect(textArea.value).toEqual(expected);
@@ -267,23 +274,25 @@ describe('init markdown', () => {
textArea.value = text;
textArea.setSelectionRange(add_at, add_at);
- textArea.addEventListener('keydown', keypressNoteText);
textArea.dispatchEvent(enterEvent);
expect(textArea.value).toEqual(expected);
},
);
- it('does nothing if feature flag disabled', () => {
- gon.features = { markdownContinueLists: false };
-
- const text = '- item';
- const expected = '- item';
+ it('does not duplicate a line item for IME characters', () => {
+ const text = '- 日本語';
+ const expected = '- 日本語\n- ';
+ textArea.dispatchEvent(new CompositionEvent('compositionstart'));
textArea.value = text;
+
+ // Press enter to end composition
+ textArea.dispatchEvent(enterEvent);
+ textArea.dispatchEvent(new CompositionEvent('compositionend'));
textArea.setSelectionRange(text.length, text.length);
- textArea.addEventListener('keydown', keypressNoteText);
+ // Press enter to make new line
textArea.dispatchEvent(enterEvent);
expect(textArea.value).toEqual(expected);
diff --git a/spec/frontend/lib/utils/url_utility_spec.js b/spec/frontend/lib/utils/url_utility_spec.js
index 7608cff4c9e..81cf4bd293b 100644
--- a/spec/frontend/lib/utils/url_utility_spec.js
+++ b/spec/frontend/lib/utils/url_utility_spec.js
@@ -1,6 +1,7 @@
import setWindowLocation from 'helpers/set_window_location_helper';
import { TEST_HOST } from 'helpers/test_constants';
import * as urlUtils from '~/lib/utils/url_utility';
+import { safeUrls, unsafeUrls } from './mock_data';
const shas = {
valid: [
@@ -575,48 +576,6 @@ describe('URL utility', () => {
});
describe('isSafeUrl', () => {
- const absoluteUrls = [
- 'http://example.org',
- 'http://example.org:8080',
- 'https://example.org',
- 'https://example.org:8080',
- 'https://192.168.1.1',
- ];
-
- const rootRelativeUrls = ['/relative/link'];
-
- const relativeUrls = ['./relative/link', '../relative/link'];
-
- const urlsWithoutHost = ['http://', 'https://', 'https:https:https:'];
-
- /* eslint-disable no-script-url */
- const nonHttpUrls = [
- 'javascript:',
- 'javascript:alert("XSS")',
- 'jav\tascript:alert("XSS");',
- ' &#14; javascript:alert("XSS");',
- 'ftp://192.168.1.1',
- 'file:///',
- 'file:///etc/hosts',
- ];
- /* eslint-enable no-script-url */
-
- // javascript:alert('XSS')
- const encodedJavaScriptUrls = [
- '&#0000106&#0000097&#0000118&#0000097&#0000115&#0000099&#0000114&#0000105&#0000112&#0000116&#0000058&#0000097&#0000108&#0000101&#0000114&#0000116&#0000040&#0000039&#0000088&#0000083&#0000083&#0000039&#0000041',
- '&#106;&#97;&#118;&#97;&#115;&#99;&#114;&#105;&#112;&#116;&#58;&#97;&#108;&#101;&#114;&#116;&#40;&#39;&#88;&#83;&#83;&#39;&#41;',
- '&#x6A&#x61&#x76&#x61&#x73&#x63&#x72&#x69&#x70&#x74&#x3A&#x61&#x6C&#x65&#x72&#x74&#x28&#x27&#x58&#x53&#x53&#x27&#x29',
- '\\u006A\\u0061\\u0076\\u0061\\u0073\\u0063\\u0072\\u0069\\u0070\\u0074\\u003A\\u0061\\u006C\\u0065\\u0072\\u0074\\u0028\\u0027\\u0058\\u0053\\u0053\\u0027\\u0029',
- ];
-
- const safeUrls = [...absoluteUrls, ...rootRelativeUrls];
- const unsafeUrls = [
- ...relativeUrls,
- ...urlsWithoutHost,
- ...nonHttpUrls,
- ...encodedJavaScriptUrls,
- ];
-
describe('with URL constructor support', () => {
it.each(safeUrls)('returns true for %s', (url) => {
expect(urlUtils.isSafeURL(url)).toBe(true);
@@ -628,6 +587,16 @@ describe('URL utility', () => {
});
});
+ describe('sanitizeUrl', () => {
+ it.each(safeUrls)('returns the url for %s', (url) => {
+ expect(urlUtils.sanitizeUrl(url)).toBe(url);
+ });
+
+ it.each(unsafeUrls)('returns `about:blank` for %s', (url) => {
+ expect(urlUtils.sanitizeUrl(url)).toBe('about:blank');
+ });
+ });
+
describe('getNormalizedURL', () => {
it.each`
url | base | result
diff --git a/spec/frontend/lib/utils/users_cache_spec.js b/spec/frontend/lib/utils/users_cache_spec.js
index 30bdddd8e73..d35ba20f570 100644
--- a/spec/frontend/lib/utils/users_cache_spec.js
+++ b/spec/frontend/lib/utils/users_cache_spec.js
@@ -228,4 +228,29 @@ describe('UsersCache', () => {
expect(userStatus).toBe(dummyUserStatus);
});
});
+
+ describe('updateById', () => {
+ describe('when the user is not cached', () => {
+ it('does nothing and returns undefined', () => {
+ expect(UsersCache.updateById(dummyUserId, { name: 'root' })).toBe(undefined);
+ expect(UsersCache.internalStorage).toStrictEqual({});
+ });
+ });
+
+ describe('when the user is cached', () => {
+ const updatedName = 'has two farms';
+ beforeEach(() => {
+ UsersCache.internalStorage[dummyUserId] = dummyUser;
+ });
+
+ it('updates the user only with the new data', async () => {
+ UsersCache.updateById(dummyUserId, { name: updatedName });
+
+ expect(await UsersCache.retrieveById(dummyUserId)).toStrictEqual({
+ username: dummyUser.username,
+ name: updatedName,
+ });
+ });
+ });
+ });
});
diff --git a/spec/frontend/listbox/index_spec.js b/spec/frontend/listbox/index_spec.js
index 45659a0e523..07c6cca535a 100644
--- a/spec/frontend/listbox/index_spec.js
+++ b/spec/frontend/listbox/index_spec.js
@@ -3,7 +3,7 @@ import { getAllByRole, getByRole } from '@testing-library/dom';
import { GlDropdown } from '@gitlab/ui';
import { createWrapper } from '@vue/test-utils';
import { initListbox, parseAttributes } from '~/listbox';
-import { getFixture, setHTMLFixture } from 'helpers/fixtures';
+import { getFixture, setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
jest.mock('~/lib/utils/url_utility');
@@ -63,6 +63,10 @@ describe('initListbox', () => {
await nextTick();
});
+ afterEach(() => {
+ resetHTMLFixture();
+ });
+
it('returns an instance', () => {
expect(instance).not.toBe(null);
});
diff --git a/spec/frontend/logs/components/tokens/token_with_loading_state_spec.js b/spec/frontend/logs/components/tokens/token_with_loading_state_spec.js
index d98d7d05c92..f667a590a36 100644
--- a/spec/frontend/logs/components/tokens/token_with_loading_state_spec.js
+++ b/spec/frontend/logs/components/tokens/token_with_loading_state_spec.js
@@ -11,7 +11,10 @@ describe('TokenWithLoadingState', () => {
const initWrapper = (props = {}, options) => {
wrapper = shallowMount(TokenWithLoadingState, {
- propsData: props,
+ propsData: {
+ cursorPosition: 'start',
+ ...props,
+ },
...options,
});
};
diff --git a/spec/frontend/members/components/table/role_dropdown_spec.js b/spec/frontend/members/components/table/role_dropdown_spec.js
index d4d950e99ba..2f1626a7044 100644
--- a/spec/frontend/members/components/table/role_dropdown_spec.js
+++ b/spec/frontend/members/components/table/role_dropdown_spec.js
@@ -4,8 +4,6 @@ import { within } from '@testing-library/dom';
import { mount, createWrapper } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
-import waitForPromises from 'helpers/wait_for_promises';
-import { BV_DROPDOWN_SHOW } from '~/lib/utils/constants';
import RoleDropdown from '~/members/components/table/role_dropdown.vue';
import { MEMBER_TYPES } from '~/members/constants';
import { member } from '../../mock_data';
@@ -70,13 +68,10 @@ describe('RoleDropdown', () => {
});
describe('when dropdown is open', () => {
- beforeEach((done) => {
+ beforeEach(() => {
createComponent();
- findDropdownToggle().trigger('click');
- wrapper.vm.$root.$on(BV_DROPDOWN_SHOW, () => {
- done();
- });
+ return findDropdownToggle().trigger('click');
});
it('renders all valid roles', () => {
@@ -95,14 +90,14 @@ describe('RoleDropdown', () => {
});
describe('when dropdown item is selected', () => {
- it('does nothing if the item selected was already selected', () => {
- getDropdownItemByText('Owner').trigger('click');
+ it('does nothing if the item selected was already selected', async () => {
+ await getDropdownItemByText('Owner').trigger('click');
expect(actions.updateMemberRole).not.toHaveBeenCalled();
});
- it('calls `updateMemberRole` Vuex action', () => {
- getDropdownItemByText('Developer').trigger('click');
+ it('calls `updateMemberRole` Vuex action', async () => {
+ await getDropdownItemByText('Developer').trigger('click');
expect(actions.updateMemberRole).toHaveBeenCalledWith(expect.any(Object), {
memberId: member.id,
@@ -111,21 +106,19 @@ describe('RoleDropdown', () => {
});
it('displays toast when successful', async () => {
- getDropdownItemByText('Developer').trigger('click');
+ await getDropdownItemByText('Developer').trigger('click');
- await waitForPromises();
+ await nextTick();
expect($toast.show).toHaveBeenCalledWith('Role updated successfully.');
});
it('disables dropdown while waiting for `updateMemberRole` to resolve', async () => {
- getDropdownItemByText('Developer').trigger('click');
-
- await nextTick();
+ await getDropdownItemByText('Developer').trigger('click');
expect(findDropdown().props('disabled')).toBe(true);
- await waitForPromises();
+ await nextTick();
expect(findDropdown().props('disabled')).toBe(false);
});
diff --git a/spec/frontend/merge_conflicts/store/actions_spec.js b/spec/frontend/merge_conflicts/store/actions_spec.js
index 1b6a0f9e977..7cee6576b53 100644
--- a/spec/frontend/merge_conflicts/store/actions_spec.js
+++ b/spec/frontend/merge_conflicts/store/actions_spec.js
@@ -1,6 +1,6 @@
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
-import Cookies from 'js-cookie';
+import Cookies from '~/lib/utils/cookies';
import { useMockLocationHelper } from 'helpers/mock_window_location_helper';
import testAction from 'helpers/vuex_action_helper';
import createFlash from '~/flash';
@@ -11,7 +11,7 @@ import { restoreFileLinesState, markLine, decorateFiles } from '~/merge_conflict
jest.mock('~/flash.js');
jest.mock('~/merge_conflicts/utils');
-jest.mock('js-cookie');
+jest.mock('~/lib/utils/cookies');
describe('merge conflicts actions', () => {
let mock;
diff --git a/spec/frontend/merge_request_spec.js b/spec/frontend/merge_request_spec.js
index 9229b353685..bcf64204c7a 100644
--- a/spec/frontend/merge_request_spec.js
+++ b/spec/frontend/merge_request_spec.js
@@ -1,5 +1,6 @@
import MockAdapter from 'axios-mock-adapter';
import $ from 'jquery';
+import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import { TEST_HOST } from 'spec/test_constants';
import waitForPromises from 'helpers/wait_for_promises';
import axios from '~/lib/utils/axios_utils';
@@ -11,7 +12,7 @@ describe('MergeRequest', () => {
let mock;
beforeEach(() => {
- loadFixtures('merge_requests/merge_request_with_task_list.html');
+ loadHTMLFixture('merge_requests/merge_request_with_task_list.html');
jest.spyOn(axios, 'patch');
mock = new MockAdapter(axios);
@@ -26,6 +27,7 @@ describe('MergeRequest', () => {
afterEach(() => {
mock.restore();
+ resetHTMLFixture();
});
it('modifies the Markdown field', async () => {
@@ -103,7 +105,7 @@ describe('MergeRequest', () => {
describe('hideCloseButton', () => {
describe('merge request of current_user', () => {
beforeEach(() => {
- loadFixtures('merge_requests/merge_request_of_current_user.html');
+ loadHTMLFixture('merge_requests/merge_request_of_current_user.html');
test.el = document.querySelector('.js-issuable-actions');
MergeRequest.hideCloseButton();
});
diff --git a/spec/frontend/merge_request_tabs_spec.js b/spec/frontend/merge_request_tabs_spec.js
index 5c24a070342..ccbc61ea658 100644
--- a/spec/frontend/merge_request_tabs_spec.js
+++ b/spec/frontend/merge_request_tabs_spec.js
@@ -1,5 +1,6 @@
import MockAdapter from 'axios-mock-adapter';
import $ from 'jquery';
+import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import initMrPage from 'helpers/init_vue_mr_page_helper';
import axios from '~/lib/utils/axios_utils';
import MergeRequestTabs from '~/merge_request_tabs';
@@ -79,7 +80,7 @@ describe('MergeRequestTabs', () => {
let tabUrl;
beforeEach(() => {
- loadFixtures('merge_requests/merge_request_with_task_list.html');
+ loadHTMLFixture('merge_requests/merge_request_with_task_list.html');
tabUrl = $('.commits-tab a').attr('href');
@@ -97,6 +98,10 @@ describe('MergeRequestTabs', () => {
};
});
+ afterEach(() => {
+ resetHTMLFixture();
+ });
+
describe('meta click', () => {
let metakeyEvent;
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 28039321428..a93035cc53a 100644
--- a/spec/frontend/monitoring/components/__snapshots__/dashboard_template_spec.js.snap
+++ b/spec/frontend/monitoring/components/__snapshots__/dashboard_template_spec.js.snap
@@ -17,6 +17,7 @@ exports[`Dashboard template matches the default snapshot 1`] = `
primarybuttontext=""
secondarybuttonlink=""
secondarybuttontext=""
+ showicon="true"
title="Feature deprecation"
variant="warning"
>
diff --git a/spec/frontend/monitoring/components/dashboard_panel_spec.js b/spec/frontend/monitoring/components/dashboard_panel_spec.js
index 7bd062b81f1..1f9eb03b5d4 100644
--- a/spec/frontend/monitoring/components/dashboard_panel_spec.js
+++ b/spec/frontend/monitoring/components/dashboard_panel_spec.js
@@ -65,6 +65,7 @@ describe('Dashboard Panel', () => {
},
store,
mocks,
+ provide: { glFeatures: { monitorLogging: true } },
...options,
});
};
@@ -379,6 +380,21 @@ describe('Dashboard Panel', () => {
expect(findViewLogsLink().attributes('href')).toMatch(mockLogsHref);
});
+ describe(':monitor_logging feature flag', () => {
+ it.each`
+ flagState | logsState | expected
+ ${true} | ${'shows'} | ${true}
+ ${false} | ${'hides'} | ${false}
+ `('$logsState logs when flag state is $flagState', async ({ flagState, expected }) => {
+ createWrapper({}, { provide: { glFeatures: { monitorLogging: flagState } } });
+ state.logsPath = mockLogsPath;
+ state.timeRange = mockTimeRange;
+ await nextTick();
+
+ expect(findViewLogsLink().exists()).toBe(expected);
+ });
+ });
+
it('it is overridden when a datazoom event is received', async () => {
state.logsPath = mockLogsPath;
state.timeRange = mockTimeRange;
@@ -488,15 +504,7 @@ describe('Dashboard Panel', () => {
store.registerModule(mockNamespace, monitoringDashboard);
store.state.embedGroup.modules.push(mockNamespace);
- wrapper = shallowMount(DashboardPanel, {
- propsData: {
- graphData,
- settingsPath: dashboardProps.settingsPath,
- namespace: mockNamespace,
- },
- store,
- mocks,
- });
+ createWrapper({ namespace: mockNamespace });
});
it('handles namespaced time range and logs path state', async () => {
diff --git a/spec/frontend/new_branch_spec.js b/spec/frontend/new_branch_spec.js
index 66b28a8c0dc..e4f4b3fa5b5 100644
--- a/spec/frontend/new_branch_spec.js
+++ b/spec/frontend/new_branch_spec.js
@@ -1,4 +1,5 @@
import $ from 'jquery';
+import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import NewBranchForm from '~/new_branch_form';
describe('Branch', () => {
@@ -18,11 +19,15 @@ describe('Branch', () => {
}
beforeEach(() => {
- loadFixtures('branches/new_branch.html');
+ loadHTMLFixture('branches/new_branch.html');
$('form').on('submit', (e) => e.preventDefault());
testContext.form = new NewBranchForm($('.js-create-branch-form'), []);
});
+ afterEach(() => {
+ resetHTMLFixture();
+ });
+
it("can't start with a dot", () => {
fillNameWith('.foo');
expectToHaveError("can't start with '.'");
diff --git a/spec/frontend/notes/components/comment_form_spec.js b/spec/frontend/notes/components/comment_form_spec.js
index a605edc4357..fb42e4d1d84 100644
--- a/spec/frontend/notes/components/comment_form_spec.js
+++ b/spec/frontend/notes/components/comment_form_spec.js
@@ -248,13 +248,21 @@ describe('issue_comment_form component', () => {
describe('textarea', () => {
describe('general', () => {
- it('should render textarea with placeholder', () => {
- mountComponent({ mountFunction: mount });
+ it.each`
+ noteType | confidential | placeholder
+ ${'comment'} | ${false} | ${'Write a comment or drag your files here…'}
+ ${'internal note'} | ${true} | ${'Write an internal note or drag your files here…'}
+ `(
+ 'should render textarea with placeholder for $noteType',
+ ({ confidential, placeholder }) => {
+ mountComponent({
+ mountFunction: mount,
+ initialData: { noteIsConfidential: confidential },
+ });
- expect(findTextArea().attributes('placeholder')).toBe(
- 'Write a comment or drag your files here…',
- );
- });
+ expect(findTextArea().attributes('placeholder')).toBe(placeholder);
+ },
+ );
it('should make textarea disabled while requesting', async () => {
mountComponent({ mountFunction: mount });
@@ -380,6 +388,20 @@ describe('issue_comment_form component', () => {
expect(findCloseReopenButton().text()).toBe('Close issue');
});
+ it.each`
+ confidential | buttonText
+ ${false} | ${'Comment'}
+ ${true} | ${'Add internal note'}
+ `('renders comment button with text "$buttonText"', ({ confidential, buttonText }) => {
+ mountComponent({
+ mountFunction: mount,
+ noteableData: createNotableDataMock({ confidential }),
+ initialData: { noteIsConfidential: confidential },
+ });
+
+ expect(findCommentButton().text()).toBe(buttonText);
+ });
+
it('should render comment button as disabled', () => {
mountComponent();
diff --git a/spec/frontend/notes/components/comment_type_dropdown_spec.js b/spec/frontend/notes/components/comment_type_dropdown_spec.js
index 8ac6144e5c8..cabf551deba 100644
--- a/spec/frontend/notes/components/comment_type_dropdown_spec.js
+++ b/spec/frontend/notes/components/comment_type_dropdown_spec.js
@@ -28,18 +28,42 @@ describe('CommentTypeDropdown component', () => {
wrapper.destroy();
});
- it('Should label action button "Comment" and correct dropdown item checked when selected', () => {
+ it.each`
+ isInternalNote | buttonText
+ ${false} | ${COMMENT_FORM.comment}
+ ${true} | ${COMMENT_FORM.internalComment}
+ `(
+ 'Should label action button as "$buttonText" for comment when `isInternalNote` is $isInternalNote',
+ ({ isInternalNote, buttonText }) => {
+ mountComponent({ props: { noteType: constants.COMMENT, isInternalNote } });
+
+ expect(findCommentGlDropdown().props()).toMatchObject({ text: buttonText });
+ },
+ );
+
+ it('Should set correct dropdown item checked when comment is selected', () => {
mountComponent({ props: { noteType: constants.COMMENT } });
- expect(findCommentGlDropdown().props()).toMatchObject({ text: COMMENT_FORM.comment });
expect(findCommentDropdownOption().props()).toMatchObject({ isChecked: true });
expect(findDiscussionDropdownOption().props()).toMatchObject({ isChecked: false });
});
- it('Should label action button "Start Thread" and correct dropdown item option checked when selected', () => {
+ it.each`
+ isInternalNote | buttonText
+ ${false} | ${COMMENT_FORM.startThread}
+ ${true} | ${COMMENT_FORM.startInternalThread}
+ `(
+ 'Should label action button as "$buttonText" for discussion when `isInternalNote` is $isInternalNote',
+ ({ isInternalNote, buttonText }) => {
+ mountComponent({ props: { noteType: constants.DISCUSSION, isInternalNote } });
+
+ expect(findCommentGlDropdown().props()).toMatchObject({ text: buttonText });
+ },
+ );
+
+ it('Should set correct dropdown item option checked when discussion is selected', () => {
mountComponent({ props: { noteType: constants.DISCUSSION } });
- expect(findCommentGlDropdown().props()).toMatchObject({ text: COMMENT_FORM.startThread });
expect(findCommentDropdownOption().props()).toMatchObject({ isChecked: false });
expect(findDiscussionDropdownOption().props()).toMatchObject({ isChecked: true });
});
diff --git a/spec/frontend/notes/components/discussion_counter_spec.js b/spec/frontend/notes/components/discussion_counter_spec.js
index a856d002d2e..f016cef18e6 100644
--- a/spec/frontend/notes/components/discussion_counter_spec.js
+++ b/spec/frontend/notes/components/discussion_counter_spec.js
@@ -45,7 +45,7 @@ describe('DiscussionCounter component', () => {
describe('has no discussions', () => {
it('does not render', () => {
- wrapper = shallowMount(DiscussionCounter, { store });
+ wrapper = shallowMount(DiscussionCounter, { store, propsData: { blocksMerge: true } });
expect(wrapper.find({ ref: 'discussionCounter' }).exists()).toBe(false);
});
@@ -55,7 +55,7 @@ describe('DiscussionCounter component', () => {
it('does not render', () => {
store.commit(types.ADD_OR_UPDATE_DISCUSSIONS, [{ ...discussionMock, resolvable: false }]);
store.dispatch('updateResolvableDiscussionsCounts');
- wrapper = shallowMount(DiscussionCounter, { store });
+ wrapper = shallowMount(DiscussionCounter, { store, propsData: { blocksMerge: true } });
expect(wrapper.find({ ref: 'discussionCounter' }).exists()).toBe(false);
});
@@ -75,20 +75,34 @@ describe('DiscussionCounter component', () => {
it('renders', () => {
updateStore();
- wrapper = shallowMount(DiscussionCounter, { store });
+ wrapper = shallowMount(DiscussionCounter, { store, propsData: { blocksMerge: true } });
expect(wrapper.find({ ref: 'discussionCounter' }).exists()).toBe(true);
});
it.each`
- title | resolved | isActive | groupLength
- ${'not allResolved'} | ${false} | ${false} | ${3}
- ${'allResolved'} | ${true} | ${true} | ${1}
- `('renders correctly if $title', ({ resolved, isActive, groupLength }) => {
+ blocksMerge | color
+ ${true} | ${'gl-bg-orange-50'}
+ ${false} | ${'gl-bg-gray-50'}
+ `(
+ 'changes background color to $color if blocksMerge is $blocksMerge',
+ ({ blocksMerge, color }) => {
+ updateStore();
+ store.state.unresolvedDiscussionsCount = 1;
+ wrapper = shallowMount(DiscussionCounter, { store, propsData: { blocksMerge } });
+
+ expect(wrapper.find('[data-testid="discussions-counter-text"]').classes()).toContain(color);
+ },
+ );
+
+ it.each`
+ title | resolved | groupLength
+ ${'not allResolved'} | ${false} | ${4}
+ ${'allResolved'} | ${true} | ${1}
+ `('renders correctly if $title', ({ resolved, groupLength }) => {
updateStore({ resolvable: true, resolved });
- wrapper = shallowMount(DiscussionCounter, { store });
+ wrapper = shallowMount(DiscussionCounter, { store, propsData: { blocksMerge: true } });
- expect(wrapper.find(`.is-active`).exists()).toBe(isActive);
expect(wrapper.findAll(GlButton)).toHaveLength(groupLength);
});
});
@@ -99,7 +113,7 @@ describe('DiscussionCounter component', () => {
const discussion = { ...discussionMock, expanded };
store.commit(types.ADD_OR_UPDATE_DISCUSSIONS, [discussion]);
store.dispatch('updateResolvableDiscussionsCounts');
- wrapper = shallowMount(DiscussionCounter, { store });
+ wrapper = shallowMount(DiscussionCounter, { store, propsData: { blocksMerge: true } });
toggleAllButton = wrapper.find('.toggle-all-discussions-btn');
};
@@ -117,26 +131,26 @@ describe('DiscussionCounter component', () => {
updateStoreWithExpanded(true);
expect(wrapper.vm.allExpanded).toBe(true);
- expect(toggleAllButton.props('icon')).toBe('angle-up');
+ expect(toggleAllButton.props('icon')).toBe('collapse');
toggleAllButton.vm.$emit('click');
await nextTick();
expect(wrapper.vm.allExpanded).toBe(false);
- expect(toggleAllButton.props('icon')).toBe('angle-down');
+ expect(toggleAllButton.props('icon')).toBe('expand');
});
it('expands all discussions if collapsed', async () => {
updateStoreWithExpanded(false);
expect(wrapper.vm.allExpanded).toBe(false);
- expect(toggleAllButton.props('icon')).toBe('angle-down');
+ expect(toggleAllButton.props('icon')).toBe('expand');
toggleAllButton.vm.$emit('click');
await nextTick();
expect(wrapper.vm.allExpanded).toBe(true);
- expect(toggleAllButton.props('icon')).toBe('angle-up');
+ expect(toggleAllButton.props('icon')).toBe('collapse');
});
});
});
diff --git a/spec/frontend/notes/components/note_body_spec.js b/spec/frontend/notes/components/note_body_spec.js
index 63f3cd865d5..378dcb97fab 100644
--- a/spec/frontend/notes/components/note_body_spec.js
+++ b/spec/frontend/notes/components/note_body_spec.js
@@ -1,9 +1,10 @@
import { shallowMount } from '@vue/test-utils';
-import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import { suggestionCommitMessage } from '~/diffs/store/getters';
-import noteBody from '~/notes/components/note_body.vue';
+import NoteBody from '~/notes/components/note_body.vue';
+import NoteAwardsList from '~/notes/components/note_awards_list.vue';
+import NoteForm from '~/notes/components/note_form.vue';
import createStore from '~/notes/stores';
import notes from '~/notes/stores/modules/index';
@@ -11,68 +12,89 @@ import Suggestions from '~/vue_shared/components/markdown/suggestions.vue';
import { noteableDataMock, notesDataMock, note } from '../mock_data';
+const createComponent = ({
+ props = {},
+ noteableData = noteableDataMock,
+ notesData = notesDataMock,
+ store = null,
+} = {}) => {
+ let mockStore;
+
+ if (!store) {
+ mockStore = createStore();
+
+ mockStore.dispatch('setNoteableData', noteableData);
+ mockStore.dispatch('setNotesData', notesData);
+ }
+
+ return shallowMount(NoteBody, {
+ store: mockStore || store,
+ propsData: {
+ note,
+ canEdit: true,
+ canAwardEmoji: true,
+ isEditing: false,
+ ...props,
+ },
+ });
+};
+
describe('issue_note_body component', () => {
- let store;
- let vm;
+ let wrapper;
beforeEach(() => {
- const Component = Vue.extend(noteBody);
-
- store = createStore();
- store.dispatch('setNoteableData', noteableDataMock);
- store.dispatch('setNotesData', notesDataMock);
-
- vm = new Component({
- store,
- propsData: {
- note,
- canEdit: true,
- canAwardEmoji: true,
- },
- }).$mount();
+ wrapper = createComponent();
});
afterEach(() => {
- vm.$destroy();
+ wrapper.destroy();
});
it('should render the note', () => {
- expect(vm.$el.querySelector('.note-text').innerHTML).toEqual(note.note_html);
+ expect(wrapper.find('.note-text').html()).toContain(note.note_html);
});
it('should render awards list', () => {
- expect(vm.$el.querySelector('.js-awards-block button [data-name="baseball"]')).not.toBeNull();
- expect(vm.$el.querySelector('.js-awards-block button [data-name="bath_tone3"]')).not.toBeNull();
+ expect(wrapper.findComponent(NoteAwardsList).exists()).toBe(true);
});
describe('isEditing', () => {
- beforeEach(async () => {
- vm.isEditing = true;
- await nextTick();
+ beforeEach(() => {
+ wrapper = createComponent({ props: { isEditing: true } });
});
it('renders edit form', () => {
- expect(vm.$el.querySelector('textarea.js-task-list-field')).not.toBeNull();
+ expect(wrapper.findComponent(NoteForm).exists()).toBe(true);
+ });
+
+ it.each`
+ confidential | buttonText
+ ${false} | ${'Save comment'}
+ ${true} | ${'Save internal note'}
+ `('renders save button with text "$buttonText"', ({ confidential, buttonText }) => {
+ wrapper = createComponent({ props: { note: { ...note, confidential }, isEditing: true } });
+
+ expect(wrapper.findComponent(NoteForm).props('saveButtonTitle')).toBe(buttonText);
});
it('adds autosave', () => {
const autosaveKey = `autosave/Note/${note.noteable_type}/${note.id}`;
- expect(vm.autosave.key).toEqual(autosaveKey);
+ // While we discourage testing wrapper props
+ // here we aren't testing a component prop
+ // but instead an instance object property
+ // which is defined in `app/assets/javascripts/notes/mixins/autosave.js`
+ expect(wrapper.vm.autosave.key).toEqual(autosaveKey);
});
});
describe('commitMessage', () => {
- let wrapper;
-
- Vue.use(Vuex);
-
beforeEach(() => {
const notesStore = notes();
notesStore.state.notes = {};
- store = new Vuex.Store({
+ const store = new Vuex.Store({
modules: {
notes: notesStore,
diffs: {
@@ -98,9 +120,9 @@ describe('issue_note_body component', () => {
},
});
- wrapper = shallowMount(noteBody, {
+ wrapper = createComponent({
store,
- propsData: {
+ props: {
note: { ...note, suggestions: [12345] },
canEdit: true,
file: { file_path: 'abc' },
diff --git a/spec/frontend/notes/components/note_form_spec.js b/spec/frontend/notes/components/note_form_spec.js
index b709141f4ac..252c24d1117 100644
--- a/spec/frontend/notes/components/note_form_spec.js
+++ b/spec/frontend/notes/components/note_form_spec.js
@@ -6,7 +6,7 @@ import { getDraft, updateDraft } from '~/lib/utils/autosave';
import NoteForm from '~/notes/components/note_form.vue';
import createStore from '~/notes/stores';
import MarkdownField from '~/vue_shared/components/markdown/field.vue';
-import { noteableDataMock, notesDataMock, discussionMock } from '../mock_data';
+import { noteableDataMock, notesDataMock, discussionMock, note } from '../mock_data';
jest.mock('~/lib/utils/autosave');
@@ -45,8 +45,6 @@ describe('issue_note_form component', () => {
noteBody: 'Magni suscipit eius consectetur enim et ex et commodi.',
noteId: '545',
};
-
- gon.features = { markdownContinueLists: true };
});
afterEach(() => {
@@ -116,6 +114,23 @@ describe('issue_note_form component', () => {
expect(textarea.attributes('data-supports-quick-actions')).toBe('true');
});
+ it.each`
+ confidential | placeholder
+ ${false} | ${'Write a comment or drag your files here…'}
+ ${true} | ${'Write an internal note or drag your files here…'}
+ `(
+ 'should set correct textarea placeholder text when discussion confidentiality is $confidential',
+ ({ confidential, placeholder }) => {
+ props.note = {
+ ...note,
+ confidential,
+ };
+ wrapper = createComponentWrapper();
+
+ expect(wrapper.find('textarea').attributes('placeholder')).toBe(placeholder);
+ },
+ );
+
it('should link to markdown docs', () => {
const { markdownDocsPath } = notesDataMock;
const markdownField = wrapper.find(MarkdownField);
diff --git a/spec/frontend/notes/components/note_header_spec.js b/spec/frontend/notes/components/note_header_spec.js
index 3513b562e0a..310a470aa18 100644
--- a/spec/frontend/notes/components/note_header_spec.js
+++ b/spec/frontend/notes/components/note_header_spec.js
@@ -21,7 +21,7 @@ describe('NoteHeader component', () => {
const findActionText = () => wrapper.find({ ref: 'actionText' });
const findTimestampLink = () => wrapper.find({ ref: 'noteTimestampLink' });
const findTimestamp = () => wrapper.find({ ref: 'noteTimestamp' });
- const findConfidentialIndicator = () => wrapper.findByTestId('confidentialIndicator');
+ const findConfidentialIndicator = () => wrapper.findByTestId('internalNoteIndicator');
const findSpinner = () => wrapper.find({ ref: 'spinner' });
const findAuthorStatus = () => wrapper.find({ ref: 'authorStatus' });
@@ -297,7 +297,7 @@ describe('NoteHeader component', () => {
createComponent({ isConfidential: true, noteableType: 'issue' });
expect(findConfidentialIndicator().attributes('title')).toBe(
- 'This comment is confidential and only visible to project members',
+ 'This internal note will always remain confidential',
);
});
});
diff --git a/spec/frontend/notes/components/notes_app_spec.js b/spec/frontend/notes/components/notes_app_spec.js
index e227af88d3f..413ee815906 100644
--- a/spec/frontend/notes/components/notes_app_spec.js
+++ b/spec/frontend/notes/components/notes_app_spec.js
@@ -2,6 +2,7 @@ import { mount, shallowMount } from '@vue/test-utils';
import AxiosMockAdapter from 'axios-mock-adapter';
import $ from 'jquery';
import { nextTick } from 'vue';
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import setWindowLocation from 'helpers/set_window_location_helper';
import { setTestTimeout } from 'helpers/timeout';
import waitForPromises from 'helpers/wait_for_promises';
@@ -92,13 +93,17 @@ describe('note_app', () => {
describe('set data', () => {
beforeEach(() => {
- setFixtures('<div class="js-discussions-count"></div>');
+ setHTMLFixture('<div class="js-discussions-count"></div>');
axiosMock.onAny().reply(200, []);
wrapper = mountComponent();
return waitForDiscussionsRequest();
});
+ afterEach(() => {
+ resetHTMLFixture();
+ });
+
it('should set notes data', () => {
expect(store.state.notesData).toEqual(mockData.notesDataMock);
});
@@ -122,13 +127,17 @@ describe('note_app', () => {
describe('render', () => {
beforeEach(() => {
- setFixtures('<div class="js-discussions-count"></div>');
+ setHTMLFixture('<div class="js-discussions-count"></div>');
axiosMock.onAny().reply(mockData.getIndividualNoteResponse);
wrapper = mountComponent();
return waitForDiscussionsRequest();
});
+ afterEach(() => {
+ resetHTMLFixture();
+ });
+
it('should render list of notes', () => {
const note =
mockData.INDIVIDUAL_NOTE_RESPONSE_MAP.GET[
@@ -160,7 +169,7 @@ describe('note_app', () => {
describe('render with comments disabled', () => {
beforeEach(() => {
- setFixtures('<div class="js-discussions-count"></div>');
+ setHTMLFixture('<div class="js-discussions-count"></div>');
axiosMock.onAny().reply(mockData.getIndividualNoteResponse);
store.state.commentsDisabled = true;
@@ -168,6 +177,10 @@ describe('note_app', () => {
return waitForDiscussionsRequest();
});
+ afterEach(() => {
+ resetHTMLFixture();
+ });
+
it('should not render form when commenting is disabled', () => {
expect(wrapper.find('.js-main-target-form').exists()).toBe(false);
});
@@ -179,7 +192,7 @@ describe('note_app', () => {
describe('timeline view', () => {
beforeEach(() => {
- setFixtures('<div class="js-discussions-count"></div>');
+ setHTMLFixture('<div class="js-discussions-count"></div>');
axiosMock.onAny().reply(mockData.getIndividualNoteResponse);
store.state.commentsDisabled = false;
@@ -189,6 +202,10 @@ describe('note_app', () => {
return waitForDiscussionsRequest();
});
+ afterEach(() => {
+ resetHTMLFixture();
+ });
+
it('should not render comments form', () => {
expect(wrapper.find('.js-main-target-form').exists()).toBe(false);
});
@@ -196,12 +213,15 @@ describe('note_app', () => {
describe('while fetching data', () => {
beforeEach(() => {
- setFixtures('<div class="js-discussions-count"></div>');
+ setHTMLFixture('<div class="js-discussions-count"></div>');
axiosMock.onAny().reply(200, []);
wrapper = mountComponent();
});
- afterEach(() => waitForDiscussionsRequest());
+ afterEach(() => {
+ waitForDiscussionsRequest();
+ resetHTMLFixture();
+ });
it('renders skeleton notes', () => {
expect(wrapper.find('.animation-container').exists()).toBe(true);
diff --git a/spec/frontend/notes/deprecated_notes_spec.js b/spec/frontend/notes/deprecated_notes_spec.js
index 7193475c96a..40b124b9029 100644
--- a/spec/frontend/notes/deprecated_notes_spec.js
+++ b/spec/frontend/notes/deprecated_notes_spec.js
@@ -3,6 +3,7 @@
import MockAdapter from 'axios-mock-adapter';
import $ from 'jquery';
import '~/behaviors/markdown/render_gfm';
+import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import { createSpyObj } from 'helpers/jest_helpers';
import { TEST_HOST } from 'helpers/test_constants';
import waitForPromises from 'helpers/wait_for_promises';
@@ -33,7 +34,7 @@ gl.utils.disableButtonIfEmptyField = () => {};
// eslint-disable-next-line jest/no-disabled-tests
describe.skip('Old Notes (~/deprecated_notes.js)', () => {
beforeEach(() => {
- loadFixtures(fixture);
+ loadHTMLFixture(fixture);
// Re-declare this here so that test_setup.js#beforeEach() doesn't
// overwrite it.
@@ -50,12 +51,14 @@ describe.skip('Old Notes (~/deprecated_notes.js)', () => {
setTestTimeoutOnce(4000);
});
- afterEach(() => {
+ afterEach(async () => {
// The Notes component sets a polling interval. Clear it after every run.
// Make sure to use jest.runOnlyPendingTimers() instead of runAllTimers().
jest.clearAllTimers();
- return axios.waitForAll().finally(() => mockAxios.restore());
+ await axios.waitForAll().finally(() => mockAxios.restore());
+
+ resetHTMLFixture();
});
it('loads the Notes class into the DOM', () => {
@@ -629,7 +632,7 @@ describe.skip('Old Notes (~/deprecated_notes.js)', () => {
let $notesContainer;
beforeEach(() => {
- loadFixtures('commit/show.html');
+ loadHTMLFixture('commit/show.html');
mockAxios.onPost(NOTES_POST_PATH).reply(200, note);
new Notes('', []);
diff --git a/spec/frontend/notes/mixins/discussion_navigation_spec.js b/spec/frontend/notes/mixins/discussion_navigation_spec.js
index aba80789a01..35b3dec6298 100644
--- a/spec/frontend/notes/mixins/discussion_navigation_spec.js
+++ b/spec/frontend/notes/mixins/discussion_navigation_spec.js
@@ -59,6 +59,7 @@ describe('Discussion navigation mixin', () => {
diffs: {
namespaced: true,
actions: { scrollToFile },
+ state: { diffFiles: [] },
},
},
});
diff --git a/spec/frontend/notes/mock_data.js b/spec/frontend/notes/mock_data.js
index a4aeeda48d8..c7a6ca5eae3 100644
--- a/spec/frontend/notes/mock_data.js
+++ b/spec/frontend/notes/mock_data.js
@@ -1171,7 +1171,7 @@ export const discussion1 = {
resolved: false,
active: true,
diff_file: {
- file_path: 'about.md',
+ file_identifier_hash: 'discfile1',
},
position: {
new_line: 50,
@@ -1189,7 +1189,7 @@ export const resolvedDiscussion1 = {
resolvable: true,
resolved: true,
diff_file: {
- file_path: 'about.md',
+ file_identifier_hash: 'discfile1',
},
position: {
new_line: 50,
@@ -1208,7 +1208,7 @@ export const discussion2 = {
resolved: false,
active: true,
diff_file: {
- file_path: 'README.md',
+ file_identifier_hash: 'discfile2',
},
position: {
new_line: null,
@@ -1227,7 +1227,7 @@ export const discussion3 = {
active: true,
resolved: false,
diff_file: {
- file_path: 'README.md',
+ file_identifier_hash: 'discfile3',
},
position: {
new_line: 21,
@@ -1240,6 +1240,12 @@ export const discussion3 = {
],
};
+export const authoritativeDiscussionFile = {
+ id: 'abc',
+ file_identifier_hash: 'discfile1',
+ order: 0,
+};
+
export const unresolvableDiscussion = {
resolvable: false,
};
diff --git a/spec/frontend/notes/stores/actions_spec.js b/spec/frontend/notes/stores/actions_spec.js
index 75e7756cd6b..ecb213590ad 100644
--- a/spec/frontend/notes/stores/actions_spec.js
+++ b/spec/frontend/notes/stores/actions_spec.js
@@ -1,4 +1,5 @@
import AxiosMockAdapter from 'axios-mock-adapter';
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import testAction from 'helpers/vuex_action_helper';
import { TEST_HOST } from 'spec/test_constants';
import Api from '~/api';
@@ -51,7 +52,7 @@ describe('Actions Notes Store', () => {
axiosMock = new AxiosMockAdapter(axios);
// This is necessary as we query Close issue button at the top of issue page when clicking bottom button
- setFixtures(
+ setHTMLFixture(
'<div class="detail-page-header-actions"><button class="btn-close btn-grouped"></button></div>',
);
});
@@ -59,6 +60,7 @@ describe('Actions Notes Store', () => {
afterEach(() => {
resetStore(store);
axiosMock.restore();
+ resetHTMLFixture();
});
describe('setNotesData', () => {
@@ -252,7 +254,9 @@ describe('Actions Notes Store', () => {
jest.advanceTimersByTime(time);
}
- return new Promise((resolve) => requestAnimationFrame(resolve));
+ return new Promise((resolve) => {
+ requestAnimationFrame(resolve);
+ });
};
const advanceXMoreIntervals = async (number) => {
const timeoutLength = pollInterval * number;
diff --git a/spec/frontend/notes/stores/getters_spec.js b/spec/frontend/notes/stores/getters_spec.js
index 9a11fdba508..6d078dcefcf 100644
--- a/spec/frontend/notes/stores/getters_spec.js
+++ b/spec/frontend/notes/stores/getters_spec.js
@@ -12,6 +12,7 @@ import {
discussion2,
discussion3,
resolvedDiscussion1,
+ authoritativeDiscussionFile,
unresolvableDiscussion,
draftComments,
draftReply,
@@ -26,6 +27,23 @@ const createDiscussionNeighborParams = (discussionId, diffOrder, step) => ({
});
const asDraftDiscussion = (x) => ({ ...x, individual_note: true });
+const createRootState = () => {
+ return {
+ diffs: {
+ diffFiles: [
+ { ...authoritativeDiscussionFile },
+ {
+ ...authoritativeDiscussionFile,
+ ...{ id: 'abc2', file_identifier_hash: 'discfile2', order: 1 },
+ },
+ {
+ ...authoritativeDiscussionFile,
+ ...{ id: 'abc3', file_identifier_hash: 'discfile3', order: 2 },
+ },
+ ],
+ },
+ };
+};
describe('Getters Notes Store', () => {
let state;
@@ -226,20 +244,84 @@ describe('Getters Notes Store', () => {
const localGetters = {
allResolvableDiscussions: [discussion3, discussion1, discussion2],
};
+ const rootState = createRootState();
- expect(getters.unresolvedDiscussionsIdsByDiff(state, localGetters)).toEqual([
+ expect(getters.unresolvedDiscussionsIdsByDiff(state, localGetters, rootState)).toEqual([
'abc1',
'abc2',
'abc3',
]);
});
+ // This is the same test as above, but it exercises the sorting algorithm
+ // for a "strange" Diff File ordering. The intent is to ensure that even if lots
+ // of shuffling has to occur, everything still works
+
+ it('should return all discussions IDs in unusual diff order', () => {
+ const localGetters = {
+ allResolvableDiscussions: [discussion3, discussion1, discussion2],
+ };
+ const rootState = {
+ diffs: {
+ diffFiles: [
+ // 2 is first, but should sort 2nd
+ {
+ ...authoritativeDiscussionFile,
+ ...{ id: 'abc2', file_identifier_hash: 'discfile2', order: 1 },
+ },
+ // 1 is second, but should sort 3rd
+ { ...authoritativeDiscussionFile, ...{ order: 2 } },
+ // 3 is third, but should sort 1st
+ {
+ ...authoritativeDiscussionFile,
+ ...{ id: 'abc3', file_identifier_hash: 'discfile3', order: 0 },
+ },
+ ],
+ },
+ };
+
+ expect(getters.unresolvedDiscussionsIdsByDiff(state, localGetters, rootState)).toEqual([
+ 'abc3',
+ 'abc2',
+ 'abc1',
+ ]);
+ });
+
+ it("should use the discussions array order if the files don't have explicit order values", () => {
+ const localGetters = {
+ allResolvableDiscussions: [discussion3, discussion1, discussion2], // This order is used!
+ };
+ const auth1 = { ...authoritativeDiscussionFile };
+ const auth2 = {
+ ...authoritativeDiscussionFile,
+ ...{ id: 'abc2', file_identifier_hash: 'discfile2' },
+ };
+ const auth3 = {
+ ...authoritativeDiscussionFile,
+ ...{ id: 'abc3', file_identifier_hash: 'discfile3' },
+ };
+ const rootState = {
+ diffs: { diffFiles: [auth2, auth1, auth3] }, // This order is not used!
+ };
+
+ delete auth1.order;
+ delete auth2.order;
+ delete auth3.order;
+
+ expect(getters.unresolvedDiscussionsIdsByDiff(state, localGetters, rootState)).toEqual([
+ 'abc3',
+ 'abc1',
+ 'abc2',
+ ]);
+ });
+
it('should return empty array if all discussions have been resolved', () => {
const localGetters = {
allResolvableDiscussions: [resolvedDiscussion1],
};
+ const rootState = createRootState();
- expect(getters.unresolvedDiscussionsIdsByDiff(state, localGetters)).toEqual([]);
+ expect(getters.unresolvedDiscussionsIdsByDiff(state, localGetters, rootState)).toEqual([]);
});
});
diff --git a/spec/frontend/oauth_remember_me_spec.js b/spec/frontend/oauth_remember_me_spec.js
index 3187cbf6547..1fa0e0aa8f6 100644
--- a/spec/frontend/oauth_remember_me_spec.js
+++ b/spec/frontend/oauth_remember_me_spec.js
@@ -1,4 +1,5 @@
import $ from 'jquery';
+import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import OAuthRememberMe from '~/pages/sessions/new/oauth_remember_me';
describe('OAuthRememberMe', () => {
@@ -7,11 +8,15 @@ describe('OAuthRememberMe', () => {
};
beforeEach(() => {
- loadFixtures('static/oauth_remember_me.html');
+ loadHTMLFixture('static/oauth_remember_me.html');
new OAuthRememberMe({ container: $('#oauth-container') }).bindEvents();
});
+ afterEach(() => {
+ resetHTMLFixture();
+ });
+
it('adds the "remember_me" query parameter to all OAuth login buttons', () => {
$('#oauth-container #remember_me').click();
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 a8d0d15007c..ca666e38291 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,7 @@
import { GlDropdownItem, GlIcon, GlDropdown } from '@gitlab/ui';
-import { shallowMount, createLocalVue } from '@vue/test-utils';
+import { shallowMount } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
-import { nextTick } from 'vue';
+import Vue, { nextTick } from 'vue';
import { numberToHumanSize } from '~/lib/utils/number_utils';
import { useFakeDate } from 'helpers/fake_date';
import createMockApollo from 'helpers/mock_apollo_helper';
@@ -28,7 +28,6 @@ import { imageTagsCountMock } from '../../mock_data';
describe('Details Header', () => {
let wrapper;
let apolloProvider;
- let localVue;
const defaultImage = {
name: 'foo',
@@ -64,28 +63,18 @@ describe('Details Header', () => {
const mountComponent = ({
propsData = { image: defaultImage },
resolver = jest.fn().mockResolvedValue(imageTagsCountMock()),
- $apollo = undefined,
} = {}) => {
- const mocks = {};
+ Vue.use(VueApollo);
- if ($apollo) {
- mocks.$apollo = $apollo;
- } else {
- localVue = createLocalVue();
- localVue.use(VueApollo);
-
- const requestHandlers = [[getContainerRepositoryMetadata, resolver]];
- apolloProvider = createMockApollo(requestHandlers);
- }
+ const requestHandlers = [[getContainerRepositoryMetadata, resolver]];
+ apolloProvider = createMockApollo(requestHandlers);
wrapper = shallowMount(component, {
- localVue,
apolloProvider,
propsData,
directives: {
GlTooltip: createMockDirective(),
},
- mocks,
stubs: {
TitleArea,
GlDropdown,
@@ -98,7 +87,6 @@ describe('Details Header', () => {
// if we want to mix createMockApollo and manual mocks we need to reset everything
wrapper.destroy();
apolloProvider = undefined;
- localVue = undefined;
wrapper = null;
});
@@ -194,10 +182,7 @@ describe('Details Header', () => {
describe('metadata items', () => {
describe('tags count', () => {
it('displays "-- tags" while loading', async () => {
- // here we are forced to mock apollo because `waitForMetadataItems` waits
- // for two ticks, de facto allowing the promise to resolve, so there is
- // no way to catch the component as both rendered and in loading state
- mountComponent({ $apollo: { queries: { containerRepository: { loading: true } } } });
+ mountComponent();
await waitForMetadataItems();
diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/cleanup_status_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/cleanup_status_spec.js
index e8ddad2d8ca..af5723267f4 100644
--- a/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/cleanup_status_spec.js
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/cleanup_status_spec.js
@@ -1,8 +1,8 @@
-import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
+import { GlLink, GlPopover, GlSprintf } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { helpPagePath } from '~/helpers/help_page_helper';
import CleanupStatus from '~/packages_and_registries/container_registry/explorer/components/list_page/cleanup_status.vue';
import {
- CLEANUP_TIMED_OUT_ERROR_MESSAGE,
CLEANUP_STATUS_SCHEDULED,
CLEANUP_STATUS_ONGOING,
CLEANUP_STATUS_UNFINISHED,
@@ -17,12 +17,20 @@ describe('cleanup_status', () => {
const findMainIcon = () => wrapper.findByTestId('main-icon');
const findExtraInfoIcon = () => wrapper.findByTestId('extra-info');
+ const findPopover = () => wrapper.findComponent(GlPopover);
+
+ const cleanupPolicyHelpPage = helpPagePath(
+ 'user/packages/container_registry/reduce_container_registry_storage.html',
+ { anchor: 'how-the-cleanup-policy-works' },
+ );
const mountComponent = (propsData = { status: SCHEDULED_STATUS }) => {
wrapper = shallowMountExtended(CleanupStatus, {
propsData,
- directives: {
- GlTooltip: createMockDirective(),
+ stubs: {
+ GlLink,
+ GlPopover,
+ GlSprintf,
},
});
};
@@ -43,7 +51,7 @@ describe('cleanup_status', () => {
mountComponent({ status });
expect(findMainIcon().exists()).toBe(visible);
- expect(wrapper.text()).toBe(text);
+ expect(wrapper.text()).toContain(text);
},
);
@@ -53,12 +61,6 @@ describe('cleanup_status', () => {
expect(findMainIcon().exists()).toBe(true);
});
-
- it(`has the orange class when the status is ${UNFINISHED_STATUS}`, () => {
- mountComponent({ status: UNFINISHED_STATUS });
-
- expect(findMainIcon().classes('gl-text-orange-500')).toBe(true);
- });
});
describe('extra info icon', () => {
@@ -76,12 +78,18 @@ describe('cleanup_status', () => {
},
);
- it(`has a tooltip`, () => {
- mountComponent({ status: UNFINISHED_STATUS });
+ it(`has a popover with a learn more link and a time frame for the next run`, () => {
+ jest.spyOn(Date, 'now').mockImplementation(() => new Date('2063-04-04T00:42:00Z').getTime());
- const tooltip = getBinding(findExtraInfoIcon().element, 'gl-tooltip');
+ mountComponent({
+ status: UNFINISHED_STATUS,
+ expirationPolicy: { next_run: '2063-04-08T01:44:03Z' },
+ });
- expect(tooltip.value.title).toBe(CLEANUP_TIMED_OUT_ERROR_MESSAGE);
+ expect(findPopover().exists()).toBe(true);
+ expect(findPopover().text()).toContain('The cleanup will continue within 4 days. Learn more');
+ expect(findPopover().findComponent(GlLink).exists()).toBe(true);
+ expect(findPopover().findComponent(GlLink).attributes('href')).toBe(cleanupPolicyHelpPage);
});
});
});
diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/registry_header_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/registry_header_spec.js
index 7d09c09d03b..f811468550d 100644
--- a/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/registry_header_spec.js
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/registry_header_spec.js
@@ -4,7 +4,6 @@ import { nextTick } from 'vue';
import Component from '~/packages_and_registries/container_registry/explorer/components/list_page/registry_header.vue';
import {
CONTAINER_REGISTRY_TITLE,
- LIST_INTRO_TEXT,
EXPIRATION_POLICY_DISABLED_TEXT,
SET_UP_CLEANUP,
} from '~/packages_and_registries/container_registry/explorer/constants';
@@ -135,9 +134,7 @@ describe('registry_header', () => {
it('is correctly bound to title_area props', () => {
mountComponent({ helpPagePath: 'foo' });
- expect(findTitleArea().props('infoMessages')).toEqual([
- { text: LIST_INTRO_TEXT, link: 'foo' },
- ]);
+ expect(findTitleArea().props('infoMessages')).toEqual([]);
});
});
});
diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/utils_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/utils_spec.js
new file mode 100644
index 00000000000..5063759a620
--- /dev/null
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/utils_spec.js
@@ -0,0 +1,21 @@
+import { timeTilRun } from '~/packages_and_registries/container_registry/explorer/utils';
+
+describe('Container registry utilities', () => {
+ describe('timeTilRun', () => {
+ beforeEach(() => {
+ jest.spyOn(Date, 'now').mockImplementation(() => new Date('2063-04-04T00:42:00Z').getTime());
+ });
+
+ it('should return a human readable time', () => {
+ const result = timeTilRun('2063-04-08T01:44:03Z');
+
+ expect(result).toBe('4 days');
+ });
+
+ it('should return an empty string with null times', () => {
+ const result = timeTilRun(null);
+
+ expect(result).toBe('');
+ });
+ });
+});
diff --git a/spec/frontend/packages_and_registries/dependency_proxy/app_spec.js b/spec/frontend/packages_and_registries/dependency_proxy/app_spec.js
index dbe9793fb8c..fe4a2c06f1c 100644
--- a/spec/frontend/packages_and_registries/dependency_proxy/app_spec.js
+++ b/spec/frontend/packages_and_registries/dependency_proxy/app_spec.js
@@ -9,7 +9,7 @@ import {
GlSprintf,
GlEmptyState,
} from '@gitlab/ui';
-import Vue from 'vue';
+import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import MockAdapter from 'axios-mock-adapter';
import createMockApollo from 'helpers/mock_apollo_helper';
@@ -47,7 +47,6 @@ describe('DependencyProxyApp', () => {
const provideDefaults = {
groupPath: 'gitlab-org',
groupId: dummyGrouptId,
- dependencyProxyAvailable: true,
noManifestsIllustration: 'noManifestsIllustration',
};
@@ -74,7 +73,6 @@ describe('DependencyProxyApp', () => {
});
}
- const findProxyNotAvailableAlert = () => wrapper.findByTestId('proxy-not-available');
const findClipBoardButton = () => wrapper.findComponent(ClipboardButton);
const findFormGroup = () => wrapper.findComponent(GlFormGroup);
const findFormInputGroup = () => wrapper.findComponent(GlFormInputGroup);
@@ -103,59 +101,22 @@ describe('DependencyProxyApp', () => {
mock.restore();
});
- describe('when the dependency proxy is not available', () => {
- const createComponentArguments = {
- provide: { ...provideDefaults, dependencyProxyAvailable: false },
- };
-
- it('renders an info alert', () => {
- createComponent(createComponentArguments);
-
- expect(findProxyNotAvailableAlert().text()).toBe(
- DependencyProxyApp.i18n.proxyNotAvailableText,
- );
- });
-
- it('does not render the main area', () => {
- createComponent(createComponentArguments);
-
- expect(findMainArea().exists()).toBe(false);
- });
-
- it('does not call the graphql endpoint', async () => {
- resolver = jest.fn().mockResolvedValue(proxyDetailsQuery());
- createComponent({ ...createComponentArguments });
-
- await waitForPromises();
-
- expect(resolver).not.toHaveBeenCalled();
- });
-
- it('hides the clear cache dropdown list', () => {
- createComponent(createComponentArguments);
-
- expect(findClearCacheDropdownList().exists()).toBe(false);
- });
- });
-
describe('when the dependency proxy is available', () => {
describe('when is loading', () => {
- it('renders the skeleton loader', () => {
+ beforeEach(() => {
createComponent();
+ });
+ it('renders the skeleton loader', () => {
expect(findSkeletonLoader().exists()).toBe(true);
});
- it('does not show the main section', () => {
- createComponent();
-
- expect(findMainArea().exists()).toBe(false);
+ it('does not render a form group with label', () => {
+ expect(findFormGroup().exists()).toBe(false);
});
- it('does not render the info alert', () => {
- createComponent();
-
- expect(findProxyNotAvailableAlert().exists()).toBe(false);
+ it('does not show the main section', () => {
+ expect(findMainArea().exists()).toBe(false);
});
});
@@ -166,10 +127,6 @@ describe('DependencyProxyApp', () => {
return waitForPromises();
});
- it('does not render the info alert', () => {
- expect(findProxyNotAvailableAlert().exists()).toBe(false);
- });
-
it('renders the main area', () => {
expect(findMainArea().exists()).toBe(true);
});
@@ -193,7 +150,7 @@ describe('DependencyProxyApp', () => {
});
});
- it('from group has a description with proxy count', () => {
+ it('form group has a description with proxy count', () => {
expect(findProxyCountText().text()).toBe('Contains 2 blobs of images (1024 Bytes)');
});
@@ -257,6 +214,28 @@ describe('DependencyProxyApp', () => {
});
});
+ describe('triggering page event on list', () => {
+ beforeEach(async () => {
+ findManifestList().vm.$emit('next-page');
+
+ await nextTick();
+ });
+
+ it('re-renders the skeleton loader', () => {
+ expect(findSkeletonLoader().exists()).toBe(true);
+ });
+
+ it('renders form group with label', () => {
+ expect(findFormGroup().attributes('label')).toEqual(
+ expect.stringMatching(DependencyProxyApp.i18n.proxyImagePrefix),
+ );
+ });
+
+ it('does not show the main section', () => {
+ expect(findMainArea().exists()).toBe(false);
+ });
+ });
+
it('shows the clear cache dropdown list', () => {
expect(findClearCacheDropdownList().exists()).toBe(true);
diff --git a/spec/frontend/packages_and_registries/dependency_proxy/components/manifest_row_spec.js b/spec/frontend/packages_and_registries/dependency_proxy/components/manifest_row_spec.js
index b7cbd875497..be3236d1f9c 100644
--- a/spec/frontend/packages_and_registries/dependency_proxy/components/manifest_row_spec.js
+++ b/spec/frontend/packages_and_registries/dependency_proxy/components/manifest_row_spec.js
@@ -3,6 +3,7 @@ import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import ListItem from '~/vue_shared/components/registry/list_item.vue';
import TimeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import Component from '~/packages_and_registries/dependency_proxy/components/manifest_row.vue';
+import { MANIFEST_PENDING_DESTRUCTION_STATUS } from '~/packages_and_registries/dependency_proxy/constants';
import { proxyManifests } from 'jest/packages_and_registries/dependency_proxy/mock_data';
describe('Manifest Row', () => {
@@ -26,34 +27,63 @@ describe('Manifest Row', () => {
const findListItem = () => wrapper.findComponent(ListItem);
const findCachedMessages = () => wrapper.findByTestId('cached-message');
const findTimeAgoTooltip = () => wrapper.findComponent(TimeagoTooltip);
-
- beforeEach(() => {
- createComponent();
- });
+ const findStatus = () => wrapper.findByTestId('status');
afterEach(() => {
wrapper.destroy();
});
- it('has a list item', () => {
- expect(findListItem().exists()).toBe(true);
- });
+ describe('With a manifest on the DEFAULT status', () => {
+ beforeEach(() => {
+ createComponent();
+ });
- it('displays the name', () => {
- expect(wrapper.text()).toContain('alpine');
- });
+ it('has a list item', () => {
+ expect(findListItem().exists()).toBe(true);
+ });
- it('displays the version', () => {
- expect(wrapper.text()).toContain('latest');
- });
+ it('displays the name', () => {
+ expect(wrapper.text()).toContain('alpine');
+ });
- it('displays the cached time', () => {
- expect(findCachedMessages().text()).toContain('Cached');
+ it('displays the version', () => {
+ expect(wrapper.text()).toContain('latest');
+ });
+
+ it('displays the cached time', () => {
+ expect(findCachedMessages().text()).toContain('Cached');
+ });
+
+ it('has a time ago tooltip component', () => {
+ expect(findTimeAgoTooltip().props()).toMatchObject({
+ time: defaultProps.manifest.createdAt,
+ });
+ });
+
+ it('does not have a status element displayed', () => {
+ expect(findStatus().exists()).toBe(false);
+ });
});
- it('has a time ago tooltip component', () => {
- expect(findTimeAgoTooltip().props()).toMatchObject({
- time: defaultProps.manifest.createdAt,
+ describe('With a manifest on the PENDING_DESTRUCTION_STATUS', () => {
+ const pendingDestructionManifest = {
+ manifest: {
+ ...defaultProps.manifest,
+ status: MANIFEST_PENDING_DESTRUCTION_STATUS,
+ },
+ };
+
+ beforeEach(() => {
+ createComponent(pendingDestructionManifest);
+ });
+
+ it('has a list item', () => {
+ expect(findListItem().exists()).toBe(true);
+ });
+
+ it('has a status element displayed', () => {
+ expect(findStatus().exists()).toBe(true);
+ expect(findStatus().text()).toBe('Scheduled for deletion');
});
});
});
diff --git a/spec/frontend/packages_and_registries/dependency_proxy/mock_data.js b/spec/frontend/packages_and_registries/dependency_proxy/mock_data.js
index 2aa427bc6af..37c8eb669ba 100644
--- a/spec/frontend/packages_and_registries/dependency_proxy/mock_data.js
+++ b/spec/frontend/packages_and_registries/dependency_proxy/mock_data.js
@@ -8,8 +8,18 @@ export const proxyData = () => ({
export const proxySettings = (extend = {}) => ({ enabled: true, ...extend });
export const proxyManifests = () => [
- { id: 'proxy-1', createdAt: '2021-09-22T09:45:28Z', imageName: 'alpine:latest' },
- { id: 'proxy-2', createdAt: '2021-09-21T09:45:28Z', imageName: 'alpine:stable' },
+ {
+ id: 'proxy-1',
+ createdAt: '2021-09-22T09:45:28Z',
+ imageName: 'alpine:latest',
+ status: 'DEFAULT',
+ },
+ {
+ id: 'proxy-2',
+ createdAt: '2021-09-21T09:45:28Z',
+ imageName: 'alpine:stable',
+ status: 'DEFAULT',
+ },
];
export const pagination = (extend) => ({
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/package_title_spec.js.snap b/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/package_title_spec.js.snap
index 519014bb9cf..fdddc131412 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/package_title_spec.js.snap
+++ b/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/package_title_spec.js.snap
@@ -29,12 +29,6 @@ exports[`PackageTitle renders with tags 1`] = `
<div
class="gl-display-flex gl-align-items-center gl-text-gray-500 gl-mt-3"
>
- <gl-icon-stub
- class="gl-mr-3"
- name="eye"
- size="16"
- />
-
<span
data-testid="sub-header"
>
@@ -127,12 +121,6 @@ exports[`PackageTitle renders without tags 1`] = `
<div
class="gl-display-flex gl-align-items-center gl-text-gray-500 gl-mt-3"
>
- <gl-icon-stub
- class="gl-mr-3"
- name="eye"
- size="16"
- />
-
<span
data-testid="sub-header"
>
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/package_title_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/package_title_spec.js
index 5da9cfffaae..d306f7834f0 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/details/package_title_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/details/package_title_spec.js
@@ -1,4 +1,4 @@
-import { GlIcon, GlSprintf } from '@gitlab/ui';
+import { GlSprintf } from '@gitlab/ui';
import { GlBreakpointInstance } from '@gitlab/ui/dist/utils';
import { nextTick } from 'vue';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
@@ -46,7 +46,6 @@ describe('PackageTitle', () => {
const findPackageRef = () => wrapper.findByTestId('package-ref');
const findPackageTags = () => wrapper.findComponent(PackageTags);
const findPackageBadges = () => wrapper.findAllByTestId('tag-badge');
- const findSubHeaderIcon = () => wrapper.findComponent(GlIcon);
const findSubHeaderText = () => wrapper.findByTestId('sub-header');
const findSubHeaderTimeAgo = () => wrapper.findComponent(TimeAgoTooltip);
@@ -120,12 +119,6 @@ describe('PackageTitle', () => {
});
describe('sub-header', () => {
- it('has the eye icon', async () => {
- await createComponent();
-
- expect(findSubHeaderIcon().props('name')).toBe('eye');
- });
-
it('has a text showing version', async () => {
await createComponent();
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 18a99f70756..031afa62890 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
@@ -38,8 +38,6 @@ exports[`packages_list_row renders 1`] = `
</router-link-stub>
<!---->
-
- <!---->
</div>
<!---->
@@ -98,16 +96,35 @@ exports[`packages_list_row renders 1`] = `
<div
class="gl-w-9 gl-display-flex gl-justify-content-end gl-pr-1"
>
- <gl-button-stub
- aria-label="Remove package"
- buttontextclasses=""
- category="secondary"
- data-testid="action-delete"
- icon="remove"
+ <gl-dropdown-stub
+ category="tertiary"
+ clearalltext="Clear all"
+ clearalltextclass="gl-px-5"
+ data-testid="delete-dropdown"
+ headertext=""
+ hideheaderborder="true"
+ highlighteditemstitle="Selected"
+ highlighteditemstitleclass="gl-px-5"
+ icon="ellipsis_v"
+ no-caret=""
size="medium"
- title="Remove package"
- variant="danger"
- />
+ text="More actions"
+ textsronly="true"
+ variant="default"
+ >
+ <gl-dropdown-item-stub
+ avatarurl=""
+ data-testid="action-delete"
+ iconcolor=""
+ iconname=""
+ iconrightarialabel=""
+ iconrightname=""
+ secondarytext=""
+ variant="danger"
+ >
+ Delete package
+ </gl-dropdown-item-stub>
+ </gl-dropdown-stub>
</div>
</div>
diff --git a/spec/frontend/packages_and_registries/package_registry/components/list/package_list_row_spec.js b/spec/frontend/packages_and_registries/package_registry/components/list/package_list_row_spec.js
index 12a3eaa3873..c16c09b5326 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
@@ -28,12 +28,12 @@ describe('packages_list_row', () => {
const packageWithoutTags = { ...packageData(), project: packageProject() };
const packageWithTags = { ...packageWithoutTags, tags: { nodes: packageTags() } };
+ const packageCannotDestroy = { ...packageData(), canDestroy: false };
const findPackageTags = () => wrapper.find(PackageTags);
const findPackagePath = () => wrapper.find(PackagePath);
- const findDeleteButton = () => wrapper.findByTestId('action-delete');
+ const findDeleteDropdown = () => wrapper.findByTestId('action-delete');
const findPackageIconAndName = () => wrapper.find(PackageIconAndName);
- const findListItem = () => wrapper.findComponent(ListItem);
const findPackageLink = () => wrapper.findByTestId('details-link');
const findWarningIcon = () => wrapper.findByTestId('warning-icon');
const findLeftSecondaryInfos = () => wrapper.findByTestId('left-secondary-infos');
@@ -102,22 +102,25 @@ describe('packages_list_row', () => {
});
describe('delete button', () => {
+ it('does not exist when package cannot be destroyed', () => {
+ mountComponent({ packageEntity: packageCannotDestroy });
+
+ expect(findDeleteDropdown().exists()).toBe(false);
+ });
+
it('exists and has the correct props', () => {
mountComponent({ packageEntity: packageWithoutTags });
- expect(findDeleteButton().exists()).toBe(true);
- expect(findDeleteButton().attributes()).toMatchObject({
- icon: 'remove',
- category: 'secondary',
+ expect(findDeleteDropdown().exists()).toBe(true);
+ expect(findDeleteDropdown().attributes()).toMatchObject({
variant: 'danger',
- title: 'Remove package',
});
});
it('emits the packageToDelete event when the delete button is clicked', async () => {
mountComponent({ packageEntity: packageWithoutTags });
- findDeleteButton().vm.$emit('click');
+ findDeleteDropdown().vm.$emit('click');
await nextTick();
expect(wrapper.emitted('packageToDelete')).toBeTruthy();
@@ -130,10 +133,6 @@ describe('packages_list_row', () => {
mountComponent({ packageEntity: { ...packageWithoutTags, status: PACKAGE_ERROR_STATUS } });
});
- it('list item has a disabled prop', () => {
- expect(findListItem().props('disabled')).toBe(true);
- });
-
it('details link is disabled', () => {
expect(findPackageLink().props('event')).toBe('');
});
@@ -141,14 +140,14 @@ describe('packages_list_row', () => {
it('has a warning icon', () => {
const icon = findWarningIcon();
const tooltip = getBinding(icon.element, 'gl-tooltip');
- expect(icon.props('icon')).toBe('warning');
+ expect(icon.props('name')).toBe('warning');
expect(tooltip.value).toMatchObject({
title: 'Invalid Package: failed metadata extraction',
});
});
- it('delete button does not exist', () => {
- expect(findDeleteButton().exists()).toBe(false);
+ it('has a delete dropdown', () => {
+ expect(findDeleteDropdown().exists()).toBe(true);
});
});
diff --git a/spec/frontend/packages_and_registries/package_registry/components/list/packages_list_spec.js b/spec/frontend/packages_and_registries/package_registry/components/list/packages_list_spec.js
index 97978dee909..660f00a2b31 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/list/packages_list_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/list/packages_list_spec.js
@@ -1,4 +1,4 @@
-import { GlKeysetPagination, GlModal, GlSprintf } from '@gitlab/ui';
+import { GlAlert, GlKeysetPagination, GlModal, GlSprintf } from '@gitlab/ui';
import { nextTick } from 'vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import PackagesListRow from '~/packages_and_registries/package_registry/components/list/package_list_row.vue';
@@ -21,6 +21,12 @@ describe('packages_list', () => {
id: 'gid://gitlab/Packages::Package/112',
name: 'second-package',
};
+ const errorPackage = {
+ ...packageData(),
+ id: 'gid://gitlab/Packages::Package/121',
+ status: 'ERROR',
+ name: 'error package',
+ };
const defaultProps = {
list: [firstPackage, secondPackage],
@@ -40,6 +46,7 @@ describe('packages_list', () => {
const findPackageListDeleteModal = () => wrapper.findComponent(GlModalStub);
const findEmptySlot = () => wrapper.findComponent(EmptySlotStub);
const findPackagesListRow = () => wrapper.findComponent(PackagesListRow);
+ const findErrorPackageAlert = () => wrapper.findComponent(GlAlert);
const mountComponent = (props) => {
wrapper = shallowMountExtended(PackagesList, {
@@ -109,6 +116,12 @@ describe('packages_list', () => {
expect(findPackageListDeleteModal().exists()).toBe(true);
});
+
+ it('does not have an error alert displayed', () => {
+ mountComponent();
+
+ expect(findErrorPackageAlert().exists()).toBe(false);
+ });
});
describe('when the user can destroy the package', () => {
@@ -140,6 +153,32 @@ describe('packages_list', () => {
});
});
+ describe('when an error package is present', () => {
+ beforeEach(() => {
+ mountComponent({ list: [firstPackage, errorPackage] });
+
+ return nextTick();
+ });
+
+ it('should display an alert message', () => {
+ expect(findErrorPackageAlert().exists()).toBe(true);
+ expect(findErrorPackageAlert().props('title')).toBe(
+ 'There was an error publishing a error package package',
+ );
+ expect(findErrorPackageAlert().text()).toBe(
+ 'There was a timeout and the package was not published. Delete this package and try again.',
+ );
+ });
+
+ it('should display the deletion modal when clicked on the confirm button', async () => {
+ findErrorPackageAlert().vm.$emit('primaryAction');
+
+ await nextTick();
+
+ expect(findPackageListDeleteModal().text()).toContain(errorPackage.name);
+ });
+ });
+
describe('when the list is empty', () => {
beforeEach(() => {
mountComponent({ list: [] });
diff --git a/spec/frontend/packages_and_registries/package_registry/components/list/packages_title_spec.js b/spec/frontend/packages_and_registries/package_registry/components/list/packages_title_spec.js
index e992ba12faa..23e5c7330d5 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/list/packages_title_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/list/packages_title_spec.js
@@ -37,7 +37,7 @@ describe('PackageTitle', () => {
expect(findTitleArea().props()).toMatchObject({
title: PackageTitle.i18n.LIST_TITLE_TEXT,
- infoMessages: [{ text: PackageTitle.i18n.LIST_INTRO_TEXT, link: 'foo' }],
+ infoMessages: [],
});
});
});
diff --git a/spec/frontend/packages_and_registries/package_registry/components/list/tokens/package_type_token_spec.js b/spec/frontend/packages_and_registries/package_registry/components/list/tokens/package_type_token_spec.js
index 26b2f3b359f..d0c111bae2d 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/list/tokens/package_type_token_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/list/tokens/package_type_token_spec.js
@@ -11,7 +11,10 @@ describe('packages_filter', () => {
const mountComponent = ({ attrs, listeners } = {}) => {
wrapper = shallowMount(component, {
- attrs,
+ attrs: {
+ cursorPosition: 'start',
+ ...attrs,
+ },
listeners,
});
};
diff --git a/spec/frontend/packages_and_registries/settings/group/components/dependency_proxy_settings_spec.js b/spec/frontend/packages_and_registries/settings/group/components/dependency_proxy_settings_spec.js
index 94f56e5c979..22754d31f93 100644
--- a/spec/frontend/packages_and_registries/settings/group/components/dependency_proxy_settings_spec.js
+++ b/spec/frontend/packages_and_registries/settings/group/components/dependency_proxy_settings_spec.js
@@ -6,11 +6,7 @@ import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import component from '~/packages_and_registries/settings/group/components/dependency_proxy_settings.vue';
-import {
- DEPENDENCY_PROXY_HEADER,
- DEPENDENCY_PROXY_SETTINGS_DESCRIPTION,
- DEPENDENCY_PROXY_DOCS_PATH,
-} from '~/packages_and_registries/settings/group/constants';
+import { DEPENDENCY_PROXY_HEADER } from '~/packages_and_registries/settings/group/constants';
import updateDependencyProxySettings from '~/packages_and_registries/settings/group/graphql/mutations/update_dependency_proxy_settings.mutation.graphql';
import updateDependencyProxyImageTtlGroupPolicy from '~/packages_and_registries/settings/group/graphql/mutations/update_dependency_proxy_image_ttl_group_policy.mutation.graphql';
@@ -91,8 +87,6 @@ describe('DependencyProxySettings', () => {
const findSettingsBlock = () => wrapper.findComponent(SettingsBlock);
const findSettingsTitles = () => wrapper.findComponent(SettingsTitles);
- const findDescription = () => wrapper.findByTestId('description');
- const findDescriptionLink = () => wrapper.findByTestId('description-link');
const findEnableProxyToggle = () => wrapper.findByTestId('dependency-proxy-setting-toggle');
const findEnableTtlPoliciesToggle = () =>
wrapper.findByTestId('dependency-proxy-ttl-policies-toggle');
@@ -126,21 +120,6 @@ describe('DependencyProxySettings', () => {
expect(wrapper.text()).toContain(DEPENDENCY_PROXY_HEADER);
});
- it('has the correct description text', () => {
- mountComponent();
-
- expect(findDescription().text()).toMatchInterpolatedText(DEPENDENCY_PROXY_SETTINGS_DESCRIPTION);
- });
-
- it('has the correct link', () => {
- mountComponent();
-
- expect(findDescriptionLink().attributes()).toMatchObject({
- href: DEPENDENCY_PROXY_DOCS_PATH,
- });
- expect(findDescriptionLink().text()).toBe('Learn more');
- });
-
describe('enable toggle', () => {
it('exists', () => {
mountComponent();
diff --git a/spec/frontend/packages_and_registries/settings/group/components/group_settings_app_spec.js b/spec/frontend/packages_and_registries/settings/group/components/group_settings_app_spec.js
index 5c30074a6af..635195ff0a4 100644
--- a/spec/frontend/packages_and_registries/settings/group/components/group_settings_app_spec.js
+++ b/spec/frontend/packages_and_registries/settings/group/components/group_settings_app_spec.js
@@ -28,7 +28,6 @@ describe('Group Settings App', () => {
const defaultProvide = {
defaultExpanded: false,
groupPath: 'foo_group_path',
- dependencyProxyAvailable: true,
};
const mountComponent = ({
@@ -140,15 +139,4 @@ describe('Group Settings App', () => {
});
});
});
-
- describe('when the dependency proxy is not available', () => {
- beforeEach(() => {
- mountComponent({ provide: { ...defaultProvide, dependencyProxyAvailable: false } });
- return waitForApolloQueryAndRender();
- });
-
- it('the setting block is hidden', () => {
- expect(findDependencyProxySettings().exists()).toBe(false);
- });
- });
});
diff --git a/spec/frontend/packages_and_registries/settings/project/settings/components/settings_form_spec.js b/spec/frontend/packages_and_registries/settings/project/settings/components/settings_form_spec.js
index 266f953c3e0..465e6dc73e2 100644
--- a/spec/frontend/packages_and_registries/settings/project/settings/components/settings_form_spec.js
+++ b/spec/frontend/packages_and_registries/settings/project/settings/components/settings_form_spec.js
@@ -1,6 +1,6 @@
-import { shallowMount, createLocalVue } from '@vue/test-utils';
+import { shallowMount } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
-import { nextTick } from 'vue';
+import Vue, { nextTick } from 'vue';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { GlCard, GlLoadingIcon } from 'jest/packages_and_registries/shared/stubs';
@@ -14,8 +14,6 @@ import expirationPolicyQuery from '~/packages_and_registries/settings/project/gr
import Tracking from '~/tracking';
import { expirationPolicyPayload, expirationPolicyMutationPayload } from '../mock_data';
-const localVue = createLocalVue();
-
describe('Settings Form', () => {
let wrapper;
let fakeApollo;
@@ -59,7 +57,6 @@ describe('Settings Form', () => {
data,
config,
provide = defaultProvidedValues,
- mocks,
} = {}) => {
wrapper = shallowMount(component, {
stubs: {
@@ -77,7 +74,6 @@ describe('Settings Form', () => {
$toast: {
show: jest.fn(),
},
- ...mocks,
},
...config,
});
@@ -88,7 +84,7 @@ describe('Settings Form', () => {
mutationResolver,
queryPayload = expirationPolicyPayload(),
} = {}) => {
- localVue.use(VueApollo);
+ Vue.use(VueApollo);
const requestHandlers = [
[updateContainerExpirationPolicyMutation, mutationResolver],
@@ -120,7 +116,6 @@ describe('Settings Form', () => {
value,
},
config: {
- localVue,
apolloProvider: fakeApollo,
},
});
@@ -356,8 +351,8 @@ describe('Settings Form', () => {
});
it('parses the error messages', async () => {
- const mutate = jest.fn().mockRejectedValue({
- graphQLErrors: [
+ const mutate = jest.fn().mockResolvedValue({
+ errors: [
{
extensions: {
problems: [{ path: ['nameRegexKeep'], message: 'baz' }],
@@ -365,7 +360,9 @@ describe('Settings Form', () => {
},
],
});
- mountComponent({ mocks: { $apollo: { mutate } } });
+ mountComponentWithApollo({
+ mutationResolver: mutate,
+ });
await submitForm();
diff --git a/spec/frontend/pager_spec.js b/spec/frontend/pager_spec.js
index 9df69124d66..dfb3e87a342 100644
--- a/spec/frontend/pager_spec.js
+++ b/spec/frontend/pager_spec.js
@@ -1,5 +1,6 @@
import MockAdapter from 'axios-mock-adapter';
import $ from 'jquery';
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import { TEST_HOST } from 'helpers/test_constants';
import waitForPromises from 'helpers/wait_for_promises';
import axios from '~/lib/utils/axios_utils';
@@ -26,12 +27,14 @@ describe('pager', () => {
const originalHref = window.location.href;
beforeEach(() => {
- setFixtures('<div class="content_list"></div><div class="loading"></div>');
+ setHTMLFixture('<div class="content_list"></div><div class="loading"></div>');
jest.spyOn($.fn, 'endlessScroll').mockImplementation();
});
afterEach(() => {
window.history.replaceState({}, null, originalHref);
+
+ resetHTMLFixture();
});
it('should get initial offset from query parameter', () => {
@@ -57,7 +60,7 @@ describe('pager', () => {
}
beforeEach(() => {
- setFixtures(
+ setHTMLFixture(
'<div class="content_list" data-href="/some_list"></div><div class="loading"></div>',
);
jest.spyOn(axios, 'get');
@@ -65,6 +68,10 @@ describe('pager', () => {
Pager.init();
});
+ afterEach(() => {
+ resetHTMLFixture();
+ });
+
it('shows loader while loading next page', async () => {
mockSuccess();
@@ -135,7 +142,11 @@ describe('pager', () => {
const href = `${TEST_HOST}/some_list.json`;
beforeEach(() => {
- setFixtures(`<div class="content_list" data-href="${href}"></div>`);
+ setHTMLFixture(`<div class="content_list" data-href="${href}"></div>`);
+ });
+
+ afterEach(() => {
+ resetHTMLFixture();
});
it('should use data-href attribute', () => {
@@ -154,7 +165,11 @@ describe('pager', () => {
describe('no data-href attribute attribute provided from list element', () => {
beforeEach(() => {
- setFixtures(`<div class="content_list"></div>`);
+ setHTMLFixture(`<div class="content_list"></div>`);
+ });
+
+ afterEach(() => {
+ resetHTMLFixture();
});
it('should use current url', () => {
@@ -190,7 +205,7 @@ describe('pager', () => {
describe('when `container` is visible', () => {
it('makes API request', () => {
- setFixtures(
+ setHTMLFixture(
`<div id="js-pager"><div class="content_list" data-href="${href}"></div></div>`,
);
@@ -199,12 +214,14 @@ describe('pager', () => {
endlessScrollCallback();
expect(axios.get).toHaveBeenCalledWith(href, expect.any(Object));
+
+ resetHTMLFixture();
});
});
describe('when `container` is not visible', () => {
it('does not make API request', () => {
- setFixtures(
+ setHTMLFixture(
`<div id="js-pager" style="display: none;"><div class="content_list" data-href="${href}"></div></div>`,
);
@@ -213,6 +230,8 @@ describe('pager', () => {
endlessScrollCallback();
expect(axios.get).not.toHaveBeenCalled();
+
+ resetHTMLFixture();
});
});
});
diff --git a/spec/frontend/pages/admin/abuse_reports/abuse_reports_spec.js b/spec/frontend/pages/admin/abuse_reports/abuse_reports_spec.js
index 71c9da238b4..6edfe9641b9 100644
--- a/spec/frontend/pages/admin/abuse_reports/abuse_reports_spec.js
+++ b/spec/frontend/pages/admin/abuse_reports/abuse_reports_spec.js
@@ -1,5 +1,5 @@
import $ from 'jquery';
-import '~/lib/utils/text_utility';
+import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import AbuseReports from '~/pages/admin/abuse_reports/abuse_reports';
describe('Abuse Reports', () => {
@@ -15,11 +15,15 @@ describe('Abuse Reports', () => {
$messages.filter((index, element) => element.innerText.indexOf(searchText) > -1).first();
beforeEach(() => {
- loadFixtures(FIXTURE);
+ loadHTMLFixture(FIXTURE);
new AbuseReports(); // eslint-disable-line no-new
$messages = $('.abuse-reports .message');
});
+ afterEach(() => {
+ resetHTMLFixture();
+ });
+
it('should truncate long messages', () => {
const $longMessage = findMessage('LONG MESSAGE');
diff --git a/spec/frontend/pages/admin/application_settings/account_and_limits_spec.js b/spec/frontend/pages/admin/application_settings/account_and_limits_spec.js
index 3a4f93d4464..542eb2f3ab8 100644
--- a/spec/frontend/pages/admin/application_settings/account_and_limits_spec.js
+++ b/spec/frontend/pages/admin/application_settings/account_and_limits_spec.js
@@ -1,4 +1,5 @@
import $ from 'jquery';
+import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import initUserInternalRegexPlaceholder, {
PLACEHOLDER_USER_EXTERNAL_DEFAULT_FALSE,
PLACEHOLDER_USER_EXTERNAL_DEFAULT_TRUE,
@@ -10,12 +11,16 @@ describe('AccountAndLimits', () => {
let $userInternalRegex;
beforeEach(() => {
- loadFixtures(FIXTURE);
+ loadHTMLFixture(FIXTURE);
initUserInternalRegexPlaceholder();
$userDefaultExternal = $('#application_setting_user_default_external');
$userInternalRegex = document.querySelector('#application_setting_user_default_internal_regex');
});
+ afterEach(() => {
+ resetHTMLFixture();
+ });
+
describe('Changing of userInternalRegex when userDefaultExternal', () => {
it('is unchecked', () => {
expect($userDefaultExternal.prop('checked')).toBeFalsy();
diff --git a/spec/frontend/pages/admin/application_settings/metrics_and_profiling/usage_statistics_spec.js b/spec/frontend/pages/admin/application_settings/metrics_and_profiling/usage_statistics_spec.js
index 4140b985682..3a52c243867 100644
--- a/spec/frontend/pages/admin/application_settings/metrics_and_profiling/usage_statistics_spec.js
+++ b/spec/frontend/pages/admin/application_settings/metrics_and_profiling/usage_statistics_spec.js
@@ -2,6 +2,7 @@ import initSetHelperText, {
HELPER_TEXT_SERVICE_PING_DISABLED,
HELPER_TEXT_SERVICE_PING_ENABLED,
} from '~/pages/admin/application_settings/metrics_and_profiling/usage_statistics';
+import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
describe('UsageStatistics', () => {
const FIXTURE = 'application_settings/usage.html';
@@ -11,7 +12,7 @@ describe('UsageStatistics', () => {
let servicePingFeaturesHelperText;
beforeEach(() => {
- loadFixtures(FIXTURE);
+ loadHTMLFixture(FIXTURE);
initSetHelperText();
servicePingCheckBox = document.getElementById('application_setting_usage_ping_enabled');
servicePingFeaturesCheckBox = document.getElementById(
@@ -21,6 +22,10 @@ describe('UsageStatistics', () => {
servicePingFeaturesHelperText = document.getElementById('service_ping_features_helper_text');
});
+ afterEach(() => {
+ resetHTMLFixture();
+ });
+
const expectEnabledservicePingFeaturesCheckBox = () => {
expect(servicePingFeaturesCheckBox.classList.contains('gl-cursor-not-allowed')).toBe(false);
expect(servicePingFeaturesHelperText.textContent).toEqual(HELPER_TEXT_SERVICE_PING_ENABLED);
diff --git a/spec/frontend/pages/admin/projects/components/namespace_select_spec.js b/spec/frontend/pages/admin/projects/components/namespace_select_spec.js
index f10b202f4d7..909349569a8 100644
--- a/spec/frontend/pages/admin/projects/components/namespace_select_spec.js
+++ b/spec/frontend/pages/admin/projects/components/namespace_select_spec.js
@@ -1,5 +1,6 @@
import { mount } from '@vue/test-utils';
import { nextTick } from 'vue';
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import Api from '~/api';
import NamespaceSelect from '~/pages/admin/projects/components/namespace_select.vue';
@@ -26,7 +27,7 @@ describe('Dropdown select component', () => {
};
beforeEach(() => {
- setFixtures('<div class="test-container"></div>');
+ setHTMLFixture('<div class="test-container"></div>');
jest.spyOn(Api, 'namespaces').mockImplementation((_, callback) =>
callback([
@@ -36,6 +37,10 @@ describe('Dropdown select component', () => {
);
});
+ afterEach(() => {
+ resetHTMLFixture();
+ });
+
it('creates a hidden input if fieldName is provided', () => {
mountDropdown({ fieldName: 'namespace-input' });
diff --git a/spec/frontend/pages/dashboard/todos/index/todos_spec.js b/spec/frontend/pages/dashboard/todos/index/todos_spec.js
index ae53afa7fba..3a9b59f291c 100644
--- a/spec/frontend/pages/dashboard/todos/index/todos_spec.js
+++ b/spec/frontend/pages/dashboard/todos/index/todos_spec.js
@@ -1,5 +1,6 @@
import MockAdapter from 'axios-mock-adapter';
import $ from 'jquery';
+import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import waitForPromises from 'helpers/wait_for_promises';
import '~/lib/utils/common_utils';
import axios from '~/lib/utils/axios_utils';
@@ -19,7 +20,7 @@ describe('Todos', () => {
let mock;
beforeEach(() => {
- loadFixtures('todos/todos.html');
+ loadHTMLFixture('todos/todos.html');
todoItem = document.querySelector('.todos-list .todo');
mock = new MockAdapter(axios);
@@ -27,6 +28,10 @@ describe('Todos', () => {
});
afterEach(() => {
+ resetHTMLFixture();
+ });
+
+ afterEach(() => {
mock.restore();
});
diff --git a/spec/frontend/pages/import/bulk_imports/history/components/bulk_imports_history_app_spec.js b/spec/frontend/pages/import/bulk_imports/history/components/bulk_imports_history_app_spec.js
index 43c48617800..a850b1655f7 100644
--- a/spec/frontend/pages/import/bulk_imports/history/components/bulk_imports_history_app_spec.js
+++ b/spec/frontend/pages/import/bulk_imports/history/components/bulk_imports_history_app_spec.js
@@ -3,6 +3,7 @@ import { mount, shallowMount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import PaginationBar from '~/vue_shared/components/pagination_bar/pagination_bar.vue';
+import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
import BulkImportsHistoryApp from '~/pages/import/bulk_imports/history/components/bulk_imports_history_app.vue';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
@@ -60,6 +61,8 @@ describe('BulkImportsHistoryApp', () => {
wrapper = mountFn(BulkImportsHistoryApp);
}
+ const findLocalStorageSync = () => wrapper.findComponent(LocalStorageSync);
+
const originalApiVersion = gon.api_version;
beforeAll(() => {
gon.api_version = 'v4';
@@ -137,6 +140,20 @@ describe('BulkImportsHistoryApp', () => {
);
});
+ it('sets up the local storage sync correctly', async () => {
+ const NEW_PAGE_SIZE = 4;
+
+ mock.onGet(API_URL).reply(200, DUMMY_RESPONSE, DEFAULT_HEADERS);
+ createComponent();
+ await axios.waitForAll();
+ mock.resetHistory();
+
+ wrapper.findComponent(PaginationBar).vm.$emit('set-page-size', NEW_PAGE_SIZE);
+ await axios.waitForAll();
+
+ expect(findLocalStorageSync().props('value')).toBe(NEW_PAGE_SIZE);
+ });
+
it('renders correct url for destination group when relative_url is empty', async () => {
mock.onGet(API_URL).reply(200, DUMMY_RESPONSE, DEFAULT_HEADERS);
createComponent({ shallow: false });
diff --git a/spec/frontend/pages/projects/learn_gitlab/components/__snapshots__/learn_gitlab_spec.js.snap b/spec/frontend/pages/projects/learn_gitlab/components/__snapshots__/learn_gitlab_spec.js.snap
index 269c7467c8b..005b8968383 100644
--- a/spec/frontend/pages/projects/learn_gitlab/components/__snapshots__/learn_gitlab_spec.js.snap
+++ b/spec/frontend/pages/projects/learn_gitlab/components/__snapshots__/learn_gitlab_spec.js.snap
@@ -178,7 +178,8 @@ exports[`Learn GitLab renders correctly 1`] = `
data-track-action="click_link"
data-track-label="Start a free Ultimate trial"
href="http://example.com/"
- target="_self"
+ rel="noopener noreferrer"
+ target="_blank"
>
Start a free Ultimate trial
@@ -209,7 +210,8 @@ exports[`Learn GitLab renders correctly 1`] = `
data-track-action="click_link"
data-track-label="Add code owners"
href="http://example.com/"
- target="_self"
+ rel="noopener noreferrer"
+ target="_blank"
>
Add code owners
@@ -240,7 +242,8 @@ exports[`Learn GitLab renders correctly 1`] = `
data-track-action="click_link"
data-track-label="Add merge request approval"
href="http://example.com/"
- target="_self"
+ rel="noopener noreferrer"
+ target="_blank"
>
Add merge request approval
diff --git a/spec/frontend/pages/projects/learn_gitlab/components/learn_gitlab_section_link_spec.js b/spec/frontend/pages/projects/learn_gitlab/components/learn_gitlab_section_link_spec.js
index b8ebf2a1430..d9aff37f703 100644
--- a/spec/frontend/pages/projects/learn_gitlab/components/learn_gitlab_section_link_spec.js
+++ b/spec/frontend/pages/projects/learn_gitlab/components/learn_gitlab_section_link_spec.js
@@ -1,8 +1,11 @@
+import { GlPopover, GlLink } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
+import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import { stubExperiments } from 'helpers/experimentation_helper';
import { mockTracking, triggerEvent, unmockTracking } from 'helpers/tracking_helper';
import eventHub from '~/invite_members/event_hub';
import LearnGitlabSectionLink from '~/pages/projects/learn_gitlab/components/learn_gitlab_section_link.vue';
+import { ACTION_LABELS } from '~/pages/projects/learn_gitlab/constants';
const defaultAction = 'gitWrite';
const defaultProps = {
@@ -10,6 +13,7 @@ const defaultProps = {
description: 'Some description',
url: 'https://example.com',
completed: false,
+ enabled: true,
};
const openInNewTabProps = {
@@ -26,16 +30,21 @@ describe('Learn GitLab Section Link', () => {
});
const createWrapper = (action = defaultAction, props = {}) => {
- wrapper = mount(LearnGitlabSectionLink, {
- propsData: { action, value: { ...defaultProps, ...props } },
- });
+ wrapper = extendedWrapper(
+ mount(LearnGitlabSectionLink, {
+ propsData: { action, value: { ...defaultProps, ...props } },
+ }),
+ );
};
const openInviteMembesrModalLink = () =>
wrapper.find('[data-testid="invite-for-help-continuous-onboarding-experiment-link"]');
const findUncompletedLink = () => wrapper.find('[data-testid="uncompleted-learn-gitlab-link"]');
-
+ const findDisabledLink = () => wrapper.findByTestId('disabled-learn-gitlab-link');
+ const findPopoverTrigger = () => wrapper.findByTestId('contact-admin-popover-trigger');
+ const findPopover = () => wrapper.findComponent(GlPopover);
+ const findPopoverLink = () => findPopover().findComponent(GlLink);
const videoTutorialLink = () => wrapper.find('[data-testid="video-tutorial-link"]');
it('renders no icon when not completed', () => {
@@ -62,6 +71,36 @@ describe('Learn GitLab Section Link', () => {
expect(wrapper.find('[data-testid="trial-only"]').exists()).toBe(true);
});
+ describe('disabled links', () => {
+ beforeEach(() => {
+ createWrapper('trialStarted', { enabled: false });
+ });
+
+ it('renders text without a link', () => {
+ expect(findDisabledLink().exists()).toBe(true);
+ expect(findDisabledLink().text()).toBe(ACTION_LABELS.trialStarted.title);
+ expect(findDisabledLink().attributes('href')).toBeUndefined();
+ });
+
+ it('renders a popover trigger with question icon', () => {
+ expect(findPopoverTrigger().exists()).toBe(true);
+ expect(findPopoverTrigger().props('icon')).toBe('question-o');
+ });
+
+ it('renders a popover', () => {
+ expect(findPopoverTrigger().attributes('id')).toBe(findPopover().props('target'));
+ expect(findPopover().props()).toMatchObject({
+ placement: 'top',
+ triggers: 'hover focus',
+ });
+ });
+
+ it('renders a link inside the popover', () => {
+ expect(findPopoverLink().exists()).toBe(true);
+ expect(findPopoverLink().attributes('href')).toBe(defaultProps.url);
+ });
+ });
+
describe('links marked with openInNewTab', () => {
beforeEach(() => {
createWrapper('securityScanEnabled', openInNewTabProps);
diff --git a/spec/frontend/pages/projects/learn_gitlab/components/learn_gitlab_spec.js b/spec/frontend/pages/projects/learn_gitlab/components/learn_gitlab_spec.js
index 5f1aff99578..0f63c243342 100644
--- a/spec/frontend/pages/projects/learn_gitlab/components/learn_gitlab_spec.js
+++ b/spec/frontend/pages/projects/learn_gitlab/components/learn_gitlab_spec.js
@@ -1,6 +1,6 @@
import { GlProgressBar, GlAlert } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
-import Cookies from 'js-cookie';
+import Cookies from '~/lib/utils/cookies';
import LearnGitlab from '~/pages/projects/learn_gitlab/components/learn_gitlab.vue';
import eventHub from '~/invite_members/event_hub';
import { INVITE_MODAL_OPEN_COOKIE } from '~/pages/projects/learn_gitlab/constants';
diff --git a/spec/frontend/pages/projects/learn_gitlab/components/mock_data.js b/spec/frontend/pages/projects/learn_gitlab/components/mock_data.js
index 5dc64097d81..1c29c68d2a9 100644
--- a/spec/frontend/pages/projects/learn_gitlab/components/mock_data.js
+++ b/spec/frontend/pages/projects/learn_gitlab/components/mock_data.js
@@ -3,47 +3,56 @@ export const testActions = {
url: 'http://example.com/',
completed: true,
svg: 'http://example.com/images/illustration.svg',
+ enabled: true,
},
userAdded: {
url: 'http://example.com/',
completed: true,
svg: 'http://example.com/images/illustration.svg',
+ enabled: true,
},
pipelineCreated: {
url: 'http://example.com/',
completed: false,
svg: 'http://example.com/images/illustration.svg',
+ enabled: true,
},
trialStarted: {
url: 'http://example.com/',
completed: false,
svg: 'http://example.com/images/illustration.svg',
+ enabled: true,
},
codeOwnersEnabled: {
url: 'http://example.com/',
completed: false,
svg: 'http://example.com/images/illustration.svg',
+ enabled: true,
},
requiredMrApprovalsEnabled: {
url: 'http://example.com/',
completed: false,
svg: 'http://example.com/images/illustration.svg',
+ enabled: true,
},
mergeRequestCreated: {
url: 'http://example.com/',
completed: false,
svg: 'http://example.com/images/illustration.svg',
+ enabled: true,
},
securityScanEnabled: {
url: 'https://docs.gitlab.com/ee/foobar/',
completed: false,
svg: 'http://example.com/images/illustration.svg',
+ enabled: true,
openInNewTab: true,
},
issueCreated: {
url: 'http://example.com/',
completed: false,
svg: 'http://example.com/images/illustration.svg',
+ enabled: true,
},
};
diff --git a/spec/frontend/pages/projects/merge_requests/edit/check_form_state_spec.js b/spec/frontend/pages/projects/merge_requests/edit/check_form_state_spec.js
index ea49111760b..5c186441817 100644
--- a/spec/frontend/pages/projects/merge_requests/edit/check_form_state_spec.js
+++ b/spec/frontend/pages/projects/merge_requests/edit/check_form_state_spec.js
@@ -1,3 +1,4 @@
+import { setHTMLFixture, resetHTMLFixture } from 'jest/__helpers__/fixtures';
import initCheckFormState from '~/pages/projects/merge_requests/edit/check_form_state';
describe('Check form state', () => {
@@ -7,7 +8,7 @@ describe('Check form state', () => {
let setDialogContent;
beforeEach(() => {
- setFixtures(`
+ setHTMLFixture(`
<form class="merge-request-form">
<input type="text" name="test" id="form-input"/>
</form>`);
@@ -22,6 +23,8 @@ describe('Check form state', () => {
afterEach(() => {
beforeUnloadEvent.preventDefault.mockRestore();
setDialogContent.mockRestore();
+
+ resetHTMLFixture();
});
it('shows confirmation dialog when there are unsaved changes', () => {
diff --git a/spec/frontend/pages/projects/pages_domains/form_spec.js b/spec/frontend/pages/projects/pages_domains/form_spec.js
index 55336596f30..e437121acd2 100644
--- a/spec/frontend/pages/projects/pages_domains/form_spec.js
+++ b/spec/frontend/pages/projects/pages_domains/form_spec.js
@@ -1,3 +1,4 @@
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import initForm from '~/pages/projects/pages_domains/form';
const ENABLED_UNLESS_AUTO_SSL_CLASS = 'js-enabled-unless-auto-ssl';
@@ -17,7 +18,7 @@ describe('Page domains form', () => {
const findUnlessAutoSsl = () => document.querySelector(`.${SHOW_UNLESS_AUTO_SSL_CLASS}`);
const create = () => {
- setFixtures(`
+ setHTMLFixture(`
<form>
<span
class="${SSL_TOGGLE_CLASS}"
@@ -31,6 +32,10 @@ describe('Page domains form', () => {
`);
};
+ afterEach(() => {
+ resetHTMLFixture();
+ });
+
it('instantiates the toggle', () => {
create();
initForm();
diff --git a/spec/frontend/pages/projects/pipeline_schedules/shared/components/pipeline_schedule_callout_spec.js b/spec/frontend/pages/projects/pipeline_schedules/shared/components/pipeline_schedule_callout_spec.js
index c28a03b35d7..ca7f70f4434 100644
--- a/spec/frontend/pages/projects/pipeline_schedules/shared/components/pipeline_schedule_callout_spec.js
+++ b/spec/frontend/pages/projects/pipeline_schedules/shared/components/pipeline_schedule_callout_spec.js
@@ -1,7 +1,7 @@
import { GlButton } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import Cookies from 'js-cookie';
import { nextTick } from 'vue';
+import Cookies from '~/lib/utils/cookies';
import PipelineSchedulesCallout from '~/pages/projects/pipeline_schedules/shared/components/pipeline_schedules_callout.vue';
const cookieKey = 'pipeline_schedules_callout_dismissed';
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 b700c255e8c..42eeff89bf4 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
@@ -1,4 +1,5 @@
import $ from 'jquery';
+import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import TimezoneDropdown, {
formatUtcOffset,
formatTimezone,
@@ -25,13 +26,17 @@ describe('Timezone Dropdown', () => {
describe('Initialize', () => {
describe('with dropdown already loaded', () => {
beforeEach(() => {
- loadFixtures('pipeline_schedules/edit.html');
+ loadHTMLFixture('pipeline_schedules/edit.html');
$wrapper = $('.dropdown');
$inputEl = $('#schedule_cron_timezone');
$inputEl.val('');
$dropdownEl = $('.js-timezone-dropdown');
});
+ afterEach(() => {
+ resetHTMLFixture();
+ });
+
it('can take an $inputEl in the constructor', () => {
initTimezoneDropdown();
@@ -86,7 +91,7 @@ describe('Timezone Dropdown', () => {
describe('without dropdown loaded', () => {
beforeEach(() => {
- loadFixtures('pipeline_schedules/edit.html');
+ loadHTMLFixture('pipeline_schedules/edit.html');
$wrapper = $('.dropdown');
$inputEl = $('#schedule_cron_timezone');
$dropdownEl = $('.js-timezone-dropdown');
diff --git a/spec/frontend/pages/search/show/refresh_counts_spec.js b/spec/frontend/pages/search/show/refresh_counts_spec.js
index 81c9bf74308..6f14f0c70bd 100644
--- a/spec/frontend/pages/search/show/refresh_counts_spec.js
+++ b/spec/frontend/pages/search/show/refresh_counts_spec.js
@@ -1,4 +1,5 @@
import MockAdapter from 'axios-mock-adapter';
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import { TEST_HOST } from 'helpers/test_constants';
import axios from '~/lib/utils/axios_utils';
import refreshCounts from '~/pages/search/show/refresh_counts';
@@ -18,7 +19,11 @@ describe('pages/search/show/refresh_counts', () => {
beforeEach(() => {
mock = new MockAdapter(axios);
- setFixtures(fixture);
+ setHTMLFixture(fixture);
+ });
+
+ afterEach(() => {
+ resetHTMLFixture();
});
afterEach(() => {
diff --git a/spec/frontend/pages/sessions/new/preserve_url_fragment_spec.js b/spec/frontend/pages/sessions/new/preserve_url_fragment_spec.js
index a29db961452..4c4a0fbea11 100644
--- a/spec/frontend/pages/sessions/new/preserve_url_fragment_spec.js
+++ b/spec/frontend/pages/sessions/new/preserve_url_fragment_spec.js
@@ -1,4 +1,5 @@
import $ from 'jquery';
+import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import preserveUrlFragment from '~/pages/sessions/new/preserve_url_fragment';
describe('preserve_url_fragment', () => {
@@ -7,7 +8,11 @@ describe('preserve_url_fragment', () => {
};
beforeEach(() => {
- loadFixtures('sessions/new.html');
+ loadHTMLFixture('sessions/new.html');
+ });
+
+ afterEach(() => {
+ resetHTMLFixture();
});
it('adds the url fragment to the login form actions', () => {
diff --git a/spec/frontend/pages/sessions/new/signin_tabs_memoizer_spec.js b/spec/frontend/pages/sessions/new/signin_tabs_memoizer_spec.js
index 601fcfedbe0..f736ce46f9b 100644
--- a/spec/frontend/pages/sessions/new/signin_tabs_memoizer_spec.js
+++ b/spec/frontend/pages/sessions/new/signin_tabs_memoizer_spec.js
@@ -1,3 +1,4 @@
+import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import { useLocalStorageSpy } from 'helpers/local_storage_helper';
import AccessorUtilities from '~/lib/utils/accessor';
import SigninTabsMemoizer from '~/pages/sessions/new/signin_tabs_memoizer';
@@ -19,11 +20,15 @@ describe('SigninTabsMemoizer', () => {
}
beforeEach(() => {
- loadFixtures(fixtureTemplate);
+ loadHTMLFixture(fixtureTemplate);
jest.spyOn(AccessorUtilities, 'canUseLocalStorage').mockReturnValue(true);
});
+ afterEach(() => {
+ resetHTMLFixture();
+ });
+
it('does nothing if no tab was previously selected', () => {
createMemoizer();
diff --git a/spec/frontend/pdf/index_spec.js b/spec/frontend/pdf/index_spec.js
index 1ae77a62675..2b0932493bb 100644
--- a/spec/frontend/pdf/index_spec.js
+++ b/spec/frontend/pdf/index_spec.js
@@ -14,18 +14,8 @@ const Component = Vue.extend(PDFLab);
describe('PDF component', () => {
let vm;
- const checkLoaded = (done) => {
- if (vm.loading) {
- setTimeout(() => {
- checkLoaded(done);
- }, 100);
- } else {
- done();
- }
- };
-
describe('without PDF data', () => {
- beforeEach((done) => {
+ beforeEach(() => {
vm = new Component({
propsData: {
pdf: '',
@@ -33,8 +23,6 @@ describe('PDF component', () => {
});
vm.$mount();
-
- checkLoaded(done);
});
it('does not render', () => {
@@ -43,7 +31,7 @@ describe('PDF component', () => {
});
describe('with PDF data', () => {
- beforeEach((done) => {
+ beforeEach(() => {
vm = new Component({
propsData: {
pdf,
@@ -51,8 +39,6 @@ describe('PDF component', () => {
});
vm.$mount();
-
- checkLoaded(done);
});
it('renders pdf component', () => {
diff --git a/spec/frontend/performance_bar/index_spec.js b/spec/frontend/performance_bar/index_spec.js
index 91cb46002be..6c1cbfa70a1 100644
--- a/spec/frontend/performance_bar/index_spec.js
+++ b/spec/frontend/performance_bar/index_spec.js
@@ -1,4 +1,5 @@
import MockAdapter from 'axios-mock-adapter';
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import axios from '~/lib/utils/axios_utils';
import '~/performance_bar/components/performance_bar_app.vue';
import performanceBar from '~/performance_bar';
@@ -11,7 +12,7 @@ describe('performance bar wrapper', () => {
let vm;
beforeEach(() => {
- setFixtures('<div id="js-peek"></div>');
+ setHTMLFixture('<div id="js-peek"></div>');
const peekWrapper = document.getElementById('js-peek');
performance.getEntriesByType = jest.fn().mockReturnValue([]);
@@ -49,6 +50,7 @@ describe('performance bar wrapper', () => {
vm.$destroy();
document.getElementById('js-peek').remove();
mock.restore();
+ resetHTMLFixture();
});
describe('addRequest', () => {
diff --git a/spec/frontend/pipeline_editor/components/commit/commit_form_spec.js b/spec/frontend/pipeline_editor/components/commit/commit_form_spec.js
index 59bd71b0e60..bec6c2a8d0c 100644
--- a/spec/frontend/pipeline_editor/components/commit/commit_form_spec.js
+++ b/spec/frontend/pipeline_editor/components/commit/commit_form_spec.js
@@ -71,7 +71,7 @@ describe('Pipeline Editor | Commit Form', () => {
expect(wrapper.emitted('submit')[0]).toEqual([
{
message: mockCommitMessage,
- targetBranch: mockDefaultBranch,
+ sourceBranch: mockDefaultBranch,
openMergeRequest: false,
},
]);
@@ -127,7 +127,7 @@ describe('Pipeline Editor | Commit Form', () => {
expect(wrapper.emitted('submit')[0]).toEqual([
{
message: anotherMessage,
- targetBranch: anotherBranch,
+ sourceBranch: anotherBranch,
openMergeRequest: true,
},
]);
diff --git a/spec/frontend/pipeline_editor/components/file-nav/pipeline_editor_file_nav_spec.js b/spec/frontend/pipeline_editor/components/file-nav/pipeline_editor_file_nav_spec.js
index e24de832d6d..a61796dbed2 100644
--- a/spec/frontend/pipeline_editor/components/file-nav/pipeline_editor_file_nav_spec.js
+++ b/spec/frontend/pipeline_editor/components/file-nav/pipeline_editor_file_nav_spec.js
@@ -1,19 +1,58 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
import { shallowMount } from '@vue/test-utils';
+import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import createMockApollo from 'helpers/mock_apollo_helper';
import BranchSwitcher from '~/pipeline_editor/components/file_nav/branch_switcher.vue';
import PipelineEditorFileNav from '~/pipeline_editor/components/file_nav/pipeline_editor_file_nav.vue';
+import FileTreePopover from '~/pipeline_editor/components/popovers/file_tree_popover.vue';
+import getAppStatus from '~/pipeline_editor/graphql/queries/client/app_status.query.graphql';
+import {
+ EDITOR_APP_STATUS_EMPTY,
+ EDITOR_APP_STATUS_LOADING,
+ EDITOR_APP_STATUS_VALID,
+} from '~/pipeline_editor/constants';
+
+Vue.use(VueApollo);
describe('Pipeline editor file nav', () => {
let wrapper;
- const createComponent = ({ provide = {} } = {}) => {
- wrapper = shallowMount(PipelineEditorFileNav, {
- provide: {
- ...provide,
+ const mockApollo = createMockApollo();
+
+ const createComponent = ({
+ appStatus = EDITOR_APP_STATUS_VALID,
+ isNewCiConfigFile = false,
+ pipelineEditorFileTree = false,
+ } = {}) => {
+ mockApollo.clients.defaultClient.cache.writeQuery({
+ query: getAppStatus,
+ data: {
+ app: {
+ __typename: 'PipelineEditorApp',
+ status: appStatus,
+ },
},
});
+
+ wrapper = extendedWrapper(
+ shallowMount(PipelineEditorFileNav, {
+ apolloProvider: mockApollo,
+ provide: {
+ glFeatures: {
+ pipelineEditorFileTree,
+ },
+ },
+ propsData: {
+ isNewCiConfigFile,
+ },
+ }),
+ );
};
const findBranchSwitcher = () => wrapper.findComponent(BranchSwitcher);
+ const findFileTreeBtn = () => wrapper.findByTestId('file-tree-toggle');
+ const findPopoverContainer = () => wrapper.findComponent(FileTreePopover);
afterEach(() => {
wrapper.destroy();
@@ -27,5 +66,91 @@ describe('Pipeline editor file nav', () => {
it('renders the branch switcher', () => {
expect(findBranchSwitcher().exists()).toBe(true);
});
+
+ it('does not render the file tree button', () => {
+ expect(findFileTreeBtn().exists()).toBe(false);
+ });
+
+ it('does not render the file tree popover', () => {
+ expect(findPopoverContainer().exists()).toBe(false);
+ });
+ });
+
+ describe('with pipelineEditorFileTree feature flag ON', () => {
+ describe('when editor is in the empty state', () => {
+ beforeEach(() => {
+ createComponent({
+ appStatus: EDITOR_APP_STATUS_EMPTY,
+ isNewCiConfigFile: false,
+ pipelineEditorFileTree: true,
+ });
+ });
+
+ it('does not render the file tree button', () => {
+ expect(findFileTreeBtn().exists()).toBe(false);
+ });
+
+ it('does not render the file tree popover', () => {
+ expect(findPopoverContainer().exists()).toBe(false);
+ });
+ });
+
+ describe('when user is about to create their config file for the first time', () => {
+ beforeEach(() => {
+ createComponent({
+ appStatus: EDITOR_APP_STATUS_VALID,
+ isNewCiConfigFile: true,
+ pipelineEditorFileTree: true,
+ });
+ });
+
+ it('does not render the file tree button', () => {
+ expect(findFileTreeBtn().exists()).toBe(false);
+ });
+
+ it('does not render the file tree popover', () => {
+ expect(findPopoverContainer().exists()).toBe(false);
+ });
+ });
+
+ describe('when app is in a global loading state', () => {
+ it('renders the file tree button with a loading icon', () => {
+ createComponent({
+ appStatus: EDITOR_APP_STATUS_LOADING,
+ isNewCiConfigFile: false,
+ pipelineEditorFileTree: true,
+ });
+
+ expect(findFileTreeBtn().exists()).toBe(true);
+ expect(findFileTreeBtn().attributes('loading')).toBe('true');
+ });
+ });
+
+ describe('when editor has a non-empty config file open', () => {
+ beforeEach(() => {
+ createComponent({
+ appStatus: EDITOR_APP_STATUS_VALID,
+ isNewCiConfigFile: false,
+ pipelineEditorFileTree: true,
+ });
+ });
+
+ it('renders the file tree button', () => {
+ expect(findFileTreeBtn().exists()).toBe(true);
+ expect(findFileTreeBtn().props('icon')).toBe('file-tree');
+ });
+
+ it('renders the file tree popover', () => {
+ expect(findPopoverContainer().exists()).toBe(true);
+ });
+
+ it('file tree button emits toggle-file-tree event', () => {
+ expect(wrapper.emitted('toggle-file-tree')).toBe(undefined);
+
+ findFileTreeBtn().vm.$emit('click');
+
+ expect(wrapper.emitted('toggle-file-tree')).toHaveLength(1);
+ });
+ });
});
});
diff --git a/spec/frontend/pipeline_editor/components/file-tree/container_spec.js b/spec/frontend/pipeline_editor/components/file-tree/container_spec.js
new file mode 100644
index 00000000000..04a93e8db25
--- /dev/null
+++ b/spec/frontend/pipeline_editor/components/file-tree/container_spec.js
@@ -0,0 +1,138 @@
+import { nextTick } from 'vue';
+import { shallowMount } from '@vue/test-utils';
+import { GlAlert } from '@gitlab/ui';
+import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import { createMockDirective } from 'helpers/vue_mock_directive';
+import PipelineEditorFileTreeContainer from '~/pipeline_editor/components/file_tree/container.vue';
+import PipelineEditorFileTreeItem from '~/pipeline_editor/components/file_tree/file_item.vue';
+import { FILE_TREE_TIP_DISMISSED_KEY } from '~/pipeline_editor/constants';
+import { mockCiConfigPath, mockIncludes, mockIncludesHelpPagePath } from '../../mock_data';
+
+describe('Pipeline editor file nav', () => {
+ let wrapper;
+
+ const createComponent = ({ includes = mockIncludes, stubs } = {}) => {
+ wrapper = extendedWrapper(
+ shallowMount(PipelineEditorFileTreeContainer, {
+ provide: {
+ ciConfigPath: mockCiConfigPath,
+ includesHelpPagePath: mockIncludesHelpPagePath,
+ },
+ propsData: {
+ includes,
+ },
+ directives: {
+ GlTooltip: createMockDirective(),
+ },
+ stubs,
+ }),
+ );
+ };
+
+ const findTip = () => wrapper.findComponent(GlAlert);
+ const findCurrentConfigFilename = () => wrapper.findByTestId('current-config-filename');
+ const fileTreeItems = () => wrapper.findAll(PipelineEditorFileTreeItem);
+
+ afterEach(() => {
+ localStorage.clear();
+ wrapper.destroy();
+ });
+
+ describe('template', () => {
+ beforeEach(() => {
+ createComponent({ stubs: { GlAlert } });
+ });
+
+ it('renders config file as a file item', () => {
+ expect(findCurrentConfigFilename().text()).toBe(mockCiConfigPath);
+ });
+ });
+
+ describe('when includes list is empty', () => {
+ describe('when dismiss state is not saved in local storage', () => {
+ beforeEach(() => {
+ createComponent({
+ includes: [],
+ stubs: { GlAlert },
+ });
+ });
+
+ it('does not render filenames', () => {
+ expect(fileTreeItems().exists()).toBe(false);
+ });
+
+ it('renders alert tip', async () => {
+ expect(findTip().exists()).toBe(true);
+ });
+
+ it('renders learn more link', async () => {
+ expect(findTip().props('secondaryButtonLink')).toBe(mockIncludesHelpPagePath);
+ });
+
+ it('can dismiss the tip', async () => {
+ expect(findTip().exists()).toBe(true);
+
+ findTip().vm.$emit('dismiss');
+ await nextTick();
+
+ expect(findTip().exists()).toBe(false);
+ });
+ });
+
+ describe('when dismiss state is saved in local storage', () => {
+ beforeEach(() => {
+ localStorage.setItem(FILE_TREE_TIP_DISMISSED_KEY, 'true');
+ createComponent({
+ includes: [],
+ stubs: { GlAlert },
+ });
+ });
+
+ it('does not render alert tip', async () => {
+ expect(findTip().exists()).toBe(false);
+ });
+ });
+
+ describe('when component receives new props with includes files', () => {
+ beforeEach(() => {
+ createComponent({ includes: [] });
+ });
+
+ it('hides tip and renders list of files', async () => {
+ expect(findTip().exists()).toBe(true);
+ expect(fileTreeItems()).toHaveLength(0);
+
+ await wrapper.setProps({ includes: mockIncludes });
+
+ expect(findTip().exists()).toBe(false);
+ expect(fileTreeItems()).toHaveLength(mockIncludes.length);
+ });
+ });
+ });
+
+ describe('when there are includes files', () => {
+ beforeEach(() => {
+ createComponent({ stubs: { GlAlert } });
+ });
+
+ it('does not render alert tip', () => {
+ expect(findTip().exists()).toBe(false);
+ });
+
+ it('renders the list of files', () => {
+ expect(fileTreeItems()).toHaveLength(mockIncludes.length);
+ });
+
+ describe('when component receives new props with empty includes', () => {
+ it('shows tip and does not render list of files', async () => {
+ expect(findTip().exists()).toBe(false);
+ expect(fileTreeItems()).toHaveLength(mockIncludes.length);
+
+ await wrapper.setProps({ includes: [] });
+
+ expect(findTip().exists()).toBe(true);
+ expect(fileTreeItems()).toHaveLength(0);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/pipeline_editor/components/file-tree/file_item_spec.js b/spec/frontend/pipeline_editor/components/file-tree/file_item_spec.js
new file mode 100644
index 00000000000..f12ac14c6be
--- /dev/null
+++ b/spec/frontend/pipeline_editor/components/file-tree/file_item_spec.js
@@ -0,0 +1,52 @@
+import { GlLink } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import FileIcon from '~/vue_shared/components/file_icon.vue';
+import PipelineEditorFileTreeItem from '~/pipeline_editor/components/file_tree/file_item.vue';
+import { mockIncludesWithBlob, mockDefaultIncludes } from '../../mock_data';
+
+describe('Pipeline editor file nav', () => {
+ let wrapper;
+
+ const createComponent = ({ file = mockDefaultIncludes } = {}) => {
+ wrapper = shallowMount(PipelineEditorFileTreeItem, {
+ propsData: {
+ file,
+ },
+ });
+ };
+
+ const fileIcon = () => wrapper.findComponent(FileIcon);
+ const link = () => wrapper.findComponent(GlLink);
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('template', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders file icon', () => {
+ expect(fileIcon().exists()).toBe(true);
+ });
+
+ it('renders file name', () => {
+ expect(wrapper.text()).toBe(mockDefaultIncludes.location);
+ });
+
+ it('links to raw path by default', () => {
+ expect(link().attributes('href')).toBe(mockDefaultIncludes.raw);
+ });
+ });
+
+ describe('when file has blob link', () => {
+ beforeEach(() => {
+ createComponent({ file: mockIncludesWithBlob });
+ });
+
+ it('links to blob path', () => {
+ expect(link().attributes('href')).toBe(mockIncludesWithBlob.blob);
+ });
+ });
+});
diff --git a/spec/frontend/pipeline_editor/components/pipeline_editor_tabs_spec.js b/spec/frontend/pipeline_editor/components/pipeline_editor_tabs_spec.js
index 6dffb7e5470..d159a20a8d6 100644
--- a/spec/frontend/pipeline_editor/components/pipeline_editor_tabs_spec.js
+++ b/spec/frontend/pipeline_editor/components/pipeline_editor_tabs_spec.js
@@ -3,7 +3,7 @@ import { shallowMount, mount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import setWindowLocation from 'helpers/set_window_location_helper';
import CiConfigMergedPreview from '~/pipeline_editor/components/editor/ci_config_merged_preview.vue';
-import WalkthroughPopover from '~/pipeline_editor/components/walkthrough_popover.vue';
+import WalkthroughPopover from '~/pipeline_editor/components/popovers/walkthrough_popover.vue';
import CiLint from '~/pipeline_editor/components/lint/ci_lint.vue';
import PipelineEditorTabs from '~/pipeline_editor/components/pipeline_editor_tabs.vue';
import EditorTab from '~/pipeline_editor/components/ui/editor_tab.vue';
diff --git a/spec/frontend/pipeline_editor/components/popovers/file_tree_popover_spec.js b/spec/frontend/pipeline_editor/components/popovers/file_tree_popover_spec.js
new file mode 100644
index 00000000000..98ce3f6ea40
--- /dev/null
+++ b/spec/frontend/pipeline_editor/components/popovers/file_tree_popover_spec.js
@@ -0,0 +1,56 @@
+import { nextTick } from 'vue';
+import { GlLink, GlPopover, GlSprintf } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import FileTreePopover from '~/pipeline_editor/components/popovers/file_tree_popover.vue';
+import { FILE_TREE_POPOVER_DISMISSED_KEY } from '~/pipeline_editor/constants';
+import { mockIncludesHelpPagePath } from '../../mock_data';
+
+describe('FileTreePopover component', () => {
+ let wrapper;
+
+ const findPopover = () => wrapper.findComponent(GlPopover);
+ const findLink = () => findPopover().findComponent(GlLink);
+
+ const createComponent = ({ stubs } = {}) => {
+ wrapper = shallowMount(FileTreePopover, {
+ provide: {
+ includesHelpPagePath: mockIncludesHelpPagePath,
+ },
+ stubs,
+ });
+ };
+
+ afterEach(() => {
+ localStorage.clear();
+ wrapper.destroy();
+ });
+
+ describe('default', () => {
+ beforeEach(async () => {
+ createComponent({ stubs: { GlSprintf } });
+ });
+
+ it('renders dismissable popover', async () => {
+ expect(findPopover().exists()).toBe(true);
+
+ findPopover().vm.$emit('close-button-clicked');
+ await nextTick();
+
+ expect(findPopover().exists()).toBe(false);
+ });
+
+ it('renders learn more link', () => {
+ expect(findLink().exists()).toBe(true);
+ expect(findLink().attributes('href')).toBe(mockIncludesHelpPagePath);
+ });
+ });
+
+ describe('when popover has already been dismissed before', () => {
+ it('does not render popover', async () => {
+ localStorage.setItem(FILE_TREE_POPOVER_DISMISSED_KEY, 'true');
+ createComponent();
+
+ expect(findPopover().exists()).toBe(false);
+ });
+ });
+});
diff --git a/spec/frontend/pipeline_editor/components/walkthrough_popover_spec.js b/spec/frontend/pipeline_editor/components/popovers/walkthrough_popover_spec.js
index a9ce89ff521..8d172a8462a 100644
--- a/spec/frontend/pipeline_editor/components/walkthrough_popover_spec.js
+++ b/spec/frontend/pipeline_editor/components/popovers/walkthrough_popover_spec.js
@@ -1,6 +1,6 @@
import { mount, shallowMount } from '@vue/test-utils';
import Vue from 'vue';
-import WalkthroughPopover from '~/pipeline_editor/components/walkthrough_popover.vue';
+import WalkthroughPopover from '~/pipeline_editor/components/popovers/walkthrough_popover.vue';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
Vue.config.ignoredElements = ['gl-emoji'];
diff --git a/spec/frontend/pipeline_editor/mock_data.js b/spec/frontend/pipeline_editor/mock_data.js
index f02f6870653..560b2820fae 100644
--- a/spec/frontend/pipeline_editor/mock_data.js
+++ b/spec/frontend/pipeline_editor/mock_data.js
@@ -9,6 +9,7 @@ export const mockNewBranch = 'new-branch';
export const mockNewMergeRequestPath = '/-/merge_requests/new';
export const mockCommitSha = 'aabbccdd';
export const mockCommitNextSha = 'eeffgghh';
+export const mockIncludesHelpPagePath = '/-/includes/help';
export const mockLintHelpPagePath = '/-/lint-help';
export const mockLintUnavailableHelpPagePath = '/-/pipeline-editor/troubleshoot';
export const mockYmlHelpPagePath = '/-/yml-help';
@@ -82,12 +83,46 @@ const mockJobFields = {
__typename: 'CiConfigJob',
};
+export const mockIncludesWithBlob = {
+ location: 'test-include.yml',
+ type: 'local',
+ blob:
+ 'http://gdk.test:3000/root/upstream/-/blob/dd54f00bb3645f8ddce7665d2ffb3864540399cb/test-include.yml',
+ raw:
+ 'http://gdk.test:3000/root/upstream/-/raw/dd54f00bb3645f8ddce7665d2ffb3864540399cb/test-include.yml',
+ __typename: 'CiConfigInclude',
+};
+
+export const mockDefaultIncludes = {
+ location: 'npm.gitlab-ci.yml',
+ type: 'template',
+ blob: null,
+ raw:
+ 'https://gitlab.com/gitlab-org/gitlab/-/raw/master/lib/gitlab/ci/templates/npm.gitlab-ci.yml',
+ __typename: 'CiConfigInclude',
+};
+
+export const mockIncludes = [
+ mockDefaultIncludes,
+ mockIncludesWithBlob,
+ {
+ location: 'a_really_really_long_name_for_includes_file.yml',
+ type: 'local',
+ blob:
+ 'http://gdk.test:3000/root/upstream/-/blob/dd54f00bb3645f8ddce7665d2ffb3864540399cb/a_really_really_long_name_for_includes_file.yml',
+ raw:
+ 'http://gdk.test:3000/root/upstream/-/raw/dd54f00bb3645f8ddce7665d2ffb3864540399cb/a_really_really_long_name_for_includes_file.yml',
+ __typename: 'CiConfigInclude',
+ },
+];
+
// Mock result of the graphql query at:
// app/assets/javascripts/pipeline_editor/graphql/queries/ci_config.graphql
export const mockCiConfigQueryResponse = {
data: {
ciConfig: {
errors: [],
+ includes: mockIncludes,
mergedYaml: mockCiYml,
status: CI_CONFIG_STATUS_VALID,
stages: {
diff --git a/spec/frontend/pipeline_editor/pipeline_editor_home_spec.js b/spec/frontend/pipeline_editor/pipeline_editor_home_spec.js
index 98e2c17967c..bf0f7fd8c9f 100644
--- a/spec/frontend/pipeline_editor/pipeline_editor_home_spec.js
+++ b/spec/frontend/pipeline_editor/pipeline_editor_home_spec.js
@@ -6,10 +6,17 @@ import CiEditorHeader from '~/pipeline_editor/components/editor/ci_editor_header
import CommitSection from '~/pipeline_editor/components/commit/commit_section.vue';
import PipelineEditorDrawer from '~/pipeline_editor/components/drawer/pipeline_editor_drawer.vue';
import PipelineEditorFileNav from '~/pipeline_editor/components/file_nav/pipeline_editor_file_nav.vue';
+import PipelineEditorFileTree from '~/pipeline_editor/components/file_tree/container.vue';
import BranchSwitcher from '~/pipeline_editor/components/file_nav/branch_switcher.vue';
import PipelineEditorHeader from '~/pipeline_editor/components/header/pipeline_editor_header.vue';
import PipelineEditorTabs from '~/pipeline_editor/components/pipeline_editor_tabs.vue';
-import { MERGED_TAB, VISUALIZE_TAB, CREATE_TAB, LINT_TAB } from '~/pipeline_editor/constants';
+import {
+ MERGED_TAB,
+ VISUALIZE_TAB,
+ CREATE_TAB,
+ LINT_TAB,
+ FILE_TREE_DISPLAY_KEY,
+} from '~/pipeline_editor/constants';
import PipelineEditorHome from '~/pipeline_editor/pipeline_editor_home.vue';
import { mockLintResponse, mockCiYml } from './mock_data';
@@ -47,11 +54,14 @@ describe('Pipeline editor home wrapper', () => {
const findFileNav = () => wrapper.findComponent(PipelineEditorFileNav);
const findModal = () => wrapper.findComponent(GlModal);
const findPipelineEditorDrawer = () => wrapper.findComponent(PipelineEditorDrawer);
+ const findPipelineEditorFileTree = () => wrapper.findComponent(PipelineEditorFileTree);
const findPipelineEditorHeader = () => wrapper.findComponent(PipelineEditorHeader);
const findPipelineEditorTabs = () => wrapper.findComponent(PipelineEditorTabs);
+ const findFileTreeBtn = () => wrapper.findByTestId('file-tree-toggle');
const findHelpBtn = () => wrapper.findByTestId('drawer-toggle');
afterEach(() => {
+ localStorage.clear();
wrapper.destroy();
});
@@ -230,4 +240,89 @@ describe('Pipeline editor home wrapper', () => {
expect(findPipelineEditorDrawer().props('isVisible')).toBe(false);
});
});
+
+ describe('file tree', () => {
+ const toggleFileTree = async () => {
+ findFileTreeBtn().vm.$emit('click');
+ await nextTick();
+ };
+
+ describe('with pipelineEditorFileTree feature flag OFF', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('hides the file tree', () => {
+ expect(findFileTreeBtn().exists()).toBe(false);
+ expect(findPipelineEditorFileTree().exists()).toBe(false);
+ });
+ });
+
+ describe('with pipelineEditorFileTree feature flag ON', () => {
+ describe('button toggle', () => {
+ beforeEach(() => {
+ createComponent({
+ glFeatures: {
+ pipelineEditorFileTree: true,
+ },
+ stubs: {
+ GlButton,
+ PipelineEditorFileNav,
+ },
+ });
+ });
+
+ it('shows button toggle', () => {
+ expect(findFileTreeBtn().exists()).toBe(true);
+ });
+
+ it('toggles the drawer on button click', async () => {
+ await toggleFileTree();
+
+ expect(findPipelineEditorFileTree().exists()).toBe(true);
+
+ await toggleFileTree();
+
+ expect(findPipelineEditorFileTree().exists()).toBe(false);
+ });
+
+ it('sets the display state in local storage', async () => {
+ await toggleFileTree();
+
+ expect(localStorage.getItem(FILE_TREE_DISPLAY_KEY)).toBe('true');
+
+ await toggleFileTree();
+
+ expect(localStorage.getItem(FILE_TREE_DISPLAY_KEY)).toBe('false');
+ });
+ });
+
+ describe('when file tree display state is saved in local storage', () => {
+ beforeEach(() => {
+ localStorage.setItem(FILE_TREE_DISPLAY_KEY, 'true');
+ createComponent({
+ glFeatures: { pipelineEditorFileTree: true },
+ stubs: { PipelineEditorFileNav },
+ });
+ });
+
+ it('shows the file tree by default', () => {
+ expect(findPipelineEditorFileTree().exists()).toBe(true);
+ });
+ });
+
+ describe('when file tree display state is not saved in local storage', () => {
+ beforeEach(() => {
+ createComponent({
+ glFeatures: { pipelineEditorFileTree: true },
+ stubs: { PipelineEditorFileNav },
+ });
+ });
+
+ it('hides the file tree by default', () => {
+ expect(findPipelineEditorFileTree().exists()).toBe(false);
+ });
+ });
+ });
+ });
});
diff --git a/spec/frontend/pipeline_wizard/components/commit_spec.js b/spec/frontend/pipeline_wizard/components/commit_spec.js
index 6496850b028..c987accbb0d 100644
--- a/spec/frontend/pipeline_wizard/components/commit_spec.js
+++ b/spec/frontend/pipeline_wizard/components/commit_spec.js
@@ -8,7 +8,7 @@ import createMockApollo from 'helpers/mock_apollo_helper';
import createCommitMutation from '~/pipeline_wizard/queries/create_commit.graphql';
import getFileMetadataQuery from '~/pipeline_wizard/queries/get_file_meta.graphql';
import RefSelector from '~/ref/components/ref_selector.vue';
-import flushPromises from 'helpers/flush_promises';
+import waitForPromises from 'helpers/wait_for_promises';
import {
createCommitMutationErrorResult,
createCommitMutationResult,
@@ -107,7 +107,7 @@ describe('Pipeline Wizard - Commit Page', () => {
it('does not show a load error if call is successful', async () => {
createComponent({ projectPath, filename });
- await flushPromises();
+ await waitForPromises();
expect(wrapper.findByTestId('load-error').exists()).not.toBe(true);
});
@@ -117,7 +117,7 @@ describe('Pipeline Wizard - Commit Page', () => {
{ defaultBranch: branch, projectPath, filename },
createMockApollo([[getFileMetadataQuery, () => fileQueryErrorResult]]),
);
- await flushPromises();
+ await waitForPromises();
expect(wrapper.findByTestId('load-error').exists()).toBe(true);
expect(wrapper.findByTestId('load-error').text()).toBe(i18n.errors.loadError);
});
@@ -131,9 +131,9 @@ describe('Pipeline Wizard - Commit Page', () => {
describe('successful commit', () => {
beforeEach(async () => {
createComponent();
- await flushPromises();
+ await waitForPromises();
await getButtonWithLabel(__('Commit')).trigger('click');
- await flushPromises();
+ await waitForPromises();
});
it('will not show an error', async () => {
@@ -159,9 +159,9 @@ describe('Pipeline Wizard - Commit Page', () => {
describe('failed commit', () => {
beforeEach(async () => {
createComponent({}, getMockApollo({ commitHasError: true }));
- await flushPromises();
+ await waitForPromises();
await getButtonWithLabel(__('Commit')).trigger('click');
- await flushPromises();
+ await waitForPromises();
});
it('will show an error', async () => {
@@ -229,7 +229,7 @@ describe('Pipeline Wizard - Commit Page', () => {
}),
);
- await flushPromises();
+ await waitForPromises();
consoleSpy = jest.spyOn(console, 'error');
@@ -243,7 +243,7 @@ describe('Pipeline Wizard - Commit Page', () => {
}
await Vue.nextTick();
- await flushPromises();
+ await waitForPromises();
});
afterAll(() => {
diff --git a/spec/frontend/pipelines/__snapshots__/utils_spec.js.snap b/spec/frontend/pipelines/__snapshots__/utils_spec.js.snap
index 2d2e5db598a..724ec7366d3 100644
--- a/spec/frontend/pipelines/__snapshots__/utils_spec.js.snap
+++ b/spec/frontend/pipelines/__snapshots__/utils_spec.js.snap
@@ -11,6 +11,7 @@ Array [
Object {
"__typename": "CiJob",
"id": "6",
+ "kind": "BUILD",
"name": "build_a_nlfjkdnlvskfnksvjknlfdjvlvnjdkjdf_nvjkenjkrlngjeknjkl",
"needs": Array [],
"previousStageJobsOrNeeds": Array [],
@@ -53,6 +54,7 @@ Array [
Object {
"__typename": "CiJob",
"id": "11",
+ "kind": "BUILD",
"name": "build_b",
"needs": Array [],
"previousStageJobsOrNeeds": Array [],
@@ -95,6 +97,7 @@ Array [
Object {
"__typename": "CiJob",
"id": "16",
+ "kind": "BUILD",
"name": "build_c",
"needs": Array [],
"previousStageJobsOrNeeds": Array [],
@@ -137,6 +140,7 @@ Array [
Object {
"__typename": "CiJob",
"id": "21",
+ "kind": "BUILD",
"name": "build_d 1/3",
"needs": Array [],
"previousStageJobsOrNeeds": Array [],
@@ -163,6 +167,7 @@ Array [
Object {
"__typename": "CiJob",
"id": "24",
+ "kind": "BUILD",
"name": "build_d 2/3",
"needs": Array [],
"previousStageJobsOrNeeds": Array [],
@@ -189,6 +194,7 @@ Array [
Object {
"__typename": "CiJob",
"id": "27",
+ "kind": "BUILD",
"name": "build_d 3/3",
"needs": Array [],
"previousStageJobsOrNeeds": Array [],
@@ -231,6 +237,7 @@ Array [
Object {
"__typename": "CiJob",
"id": "59",
+ "kind": "BUILD",
"name": "test_c",
"needs": Array [],
"previousStageJobsOrNeeds": Array [],
@@ -275,6 +282,7 @@ Array [
Object {
"__typename": "CiJob",
"id": "34",
+ "kind": "BUILD",
"name": "test_a",
"needs": Array [
"build_c",
@@ -325,6 +333,7 @@ Array [
Object {
"__typename": "CiJob",
"id": "42",
+ "kind": "BUILD",
"name": "test_b 1/2",
"needs": Array [
"build_d 3/3",
@@ -363,6 +372,7 @@ Array [
Object {
"__typename": "CiJob",
"id": "67",
+ "kind": "BUILD",
"name": "test_b 2/2",
"needs": Array [
"build_d 3/3",
@@ -417,6 +427,7 @@ Array [
Object {
"__typename": "CiJob",
"id": "53",
+ "kind": "BUILD",
"name": "test_d",
"needs": Array [
"build_b",
diff --git a/spec/frontend/pipelines/components/jobs/failed_jobs_app_spec.js b/spec/frontend/pipelines/components/jobs/failed_jobs_app_spec.js
new file mode 100644
index 00000000000..3b5632a8a4e
--- /dev/null
+++ b/spec/frontend/pipelines/components/jobs/failed_jobs_app_spec.js
@@ -0,0 +1,87 @@
+import { GlLoadingIcon } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import createFlash from '~/flash';
+import FailedJobsApp from '~/pipelines/components/jobs/failed_jobs_app.vue';
+import FailedJobsTable from '~/pipelines/components/jobs/failed_jobs_table.vue';
+import GetFailedJobsQuery from '~/pipelines/graphql/queries/get_failed_jobs.query.graphql';
+import { mockFailedJobsQueryResponse, mockFailedJobsSummaryData } from '../../mock_data';
+
+Vue.use(VueApollo);
+
+jest.mock('~/flash');
+
+describe('Failed Jobs App', () => {
+ let wrapper;
+ let resolverSpy;
+
+ const findLoadingSpinner = () => wrapper.findComponent(GlLoadingIcon);
+ const findJobsTable = () => wrapper.findComponent(FailedJobsTable);
+
+ const createMockApolloProvider = (resolver) => {
+ const requestHandlers = [[GetFailedJobsQuery, resolver]];
+
+ return createMockApollo(requestHandlers);
+ };
+
+ const createComponent = (resolver, failedJobsSummaryData = mockFailedJobsSummaryData) => {
+ wrapper = shallowMount(FailedJobsApp, {
+ provide: {
+ fullPath: 'root/ci-project',
+ pipelineIid: 1,
+ },
+ propsData: {
+ failedJobsSummary: failedJobsSummaryData,
+ },
+ apolloProvider: createMockApolloProvider(resolver),
+ });
+ };
+
+ beforeEach(() => {
+ resolverSpy = jest.fn().mockResolvedValue(mockFailedJobsQueryResponse);
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('loading spinner', () => {
+ beforeEach(() => {
+ createComponent(resolverSpy);
+ });
+
+ it('displays loading spinner when fetching failed jobs', () => {
+ expect(findLoadingSpinner().exists()).toBe(true);
+ });
+
+ it('hides loading spinner after the failed jobs have been fetched', async () => {
+ await waitForPromises();
+
+ expect(findLoadingSpinner().exists()).toBe(false);
+ });
+ });
+
+ it('displays the failed jobs table', async () => {
+ createComponent(resolverSpy);
+
+ await waitForPromises();
+
+ expect(findJobsTable().exists()).toBe(true);
+ expect(createFlash).not.toHaveBeenCalled();
+ });
+
+ it('handles query fetch error correctly', async () => {
+ resolverSpy = jest.fn().mockRejectedValue(new Error('GraphQL error'));
+
+ createComponent(resolverSpy);
+
+ await waitForPromises();
+
+ expect(createFlash).toHaveBeenCalledWith({
+ message: 'There was a problem fetching the failed jobs.',
+ });
+ });
+});
diff --git a/spec/frontend/pipelines/components/jobs/failed_jobs_table_spec.js b/spec/frontend/pipelines/components/jobs/failed_jobs_table_spec.js
new file mode 100644
index 00000000000..b597a3bf4b0
--- /dev/null
+++ b/spec/frontend/pipelines/components/jobs/failed_jobs_table_spec.js
@@ -0,0 +1,117 @@
+import { GlButton, GlLink, GlTableLite } from '@gitlab/ui';
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import createFlash from '~/flash';
+import { redirectTo } from '~/lib/utils/url_utility';
+import FailedJobsTable from '~/pipelines/components/jobs/failed_jobs_table.vue';
+import RetryFailedJobMutation from '~/pipelines/graphql/mutations/retry_failed_job.mutation.graphql';
+import {
+ successRetryMutationResponse,
+ failedRetryMutationResponse,
+ mockPreparedFailedJobsData,
+ mockPreparedFailedJobsDataNoPermission,
+} from '../../mock_data';
+
+jest.mock('~/flash');
+jest.mock('~/lib/utils/url_utility');
+
+Vue.use(VueApollo);
+
+describe('Failed Jobs Table', () => {
+ let wrapper;
+
+ const successRetryMutationHandler = jest.fn().mockResolvedValue(successRetryMutationResponse);
+ const failedRetryMutationHandler = jest.fn().mockResolvedValue(failedRetryMutationResponse);
+
+ const findJobsTable = () => wrapper.findComponent(GlTableLite);
+ const findRetryButton = () => wrapper.findComponent(GlButton);
+ const findJobLink = () => wrapper.findComponent(GlLink);
+ const findJobLog = () => wrapper.findByTestId('job-log');
+
+ const createMockApolloProvider = (resolver) => {
+ const requestHandlers = [[RetryFailedJobMutation, resolver]];
+ return createMockApollo(requestHandlers);
+ };
+
+ const createComponent = (resolver, failedJobsData = mockPreparedFailedJobsData) => {
+ wrapper = mountExtended(FailedJobsTable, {
+ propsData: {
+ failedJobs: failedJobsData,
+ },
+ apolloProvider: createMockApolloProvider(resolver),
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('displays the failed jobs table', () => {
+ createComponent();
+
+ expect(findJobsTable().exists()).toBe(true);
+ });
+
+ it('calls the retry failed job mutation correctly', () => {
+ createComponent(successRetryMutationHandler);
+
+ findRetryButton().trigger('click');
+
+ expect(successRetryMutationHandler).toHaveBeenCalledWith({
+ id: mockPreparedFailedJobsData[0].id,
+ });
+ });
+
+ it('redirects to the new job after the mutation', async () => {
+ const {
+ data: {
+ jobRetry: { job },
+ },
+ } = successRetryMutationResponse;
+
+ createComponent(successRetryMutationHandler);
+
+ findRetryButton().trigger('click');
+
+ await waitForPromises();
+
+ expect(redirectTo).toHaveBeenCalledWith(job.detailedStatus.detailsPath);
+ });
+
+ it('shows error message if the retry failed job mutation fails', async () => {
+ createComponent(failedRetryMutationHandler);
+
+ findRetryButton().trigger('click');
+
+ await waitForPromises();
+
+ expect(createFlash).toHaveBeenCalledWith({
+ message: 'There was a problem retrying the failed job.',
+ });
+ });
+
+ it('hides the job log and retry button if a user does not have permission', () => {
+ createComponent([[]], mockPreparedFailedJobsDataNoPermission);
+
+ expect(findJobLog().exists()).toBe(false);
+ expect(findRetryButton().exists()).toBe(false);
+ });
+
+ it('displays the job log and retry button if a user has permission', () => {
+ createComponent();
+
+ expect(findJobLog().exists()).toBe(true);
+ expect(findRetryButton().exists()).toBe(true);
+ });
+
+ it('job name links to the correct job', () => {
+ createComponent();
+
+ expect(findJobLink().attributes('href')).toBe(
+ mockPreparedFailedJobsData[0].detailedStatus.detailsPath,
+ );
+ });
+});
diff --git a/spec/frontend/pipelines/components/jobs/utils_spec.js b/spec/frontend/pipelines/components/jobs/utils_spec.js
new file mode 100644
index 00000000000..720446cfda3
--- /dev/null
+++ b/spec/frontend/pipelines/components/jobs/utils_spec.js
@@ -0,0 +1,14 @@
+import { prepareFailedJobs } from '~/pipelines/components/jobs/utils';
+import {
+ mockFailedJobsData,
+ mockFailedJobsSummaryData,
+ mockPreparedFailedJobsData,
+} from '../../mock_data';
+
+describe('Utils', () => {
+ it('prepares failed jobs data correctly', () => {
+ expect(prepareFailedJobs(mockFailedJobsData, mockFailedJobsSummaryData)).toEqual(
+ mockPreparedFailedJobsData,
+ );
+ });
+});
diff --git a/spec/frontend/pipelines/components/pipeline_tabs_spec.js b/spec/frontend/pipelines/components/pipeline_tabs_spec.js
index e18c3edbad9..89002ee47a8 100644
--- a/spec/frontend/pipelines/components/pipeline_tabs_spec.js
+++ b/spec/frontend/pipelines/components/pipeline_tabs_spec.js
@@ -21,14 +21,19 @@ describe('The Pipeline Tabs', () => {
const findPipelineApp = () => wrapper.findComponent(PipelineGraphWrapper);
const findTestsApp = () => wrapper.findComponent(TestReports);
+ const defaultProvide = {
+ defaultTabValue: '',
+ };
+
const createComponent = (propsData = {}) => {
wrapper = extendedWrapper(
shallowMount(PipelineTabs, {
propsData,
+ provide: {
+ ...defaultProvide,
+ },
stubs: {
- Dag: { template: '<div id="dag"/>' },
JobsApp: { template: '<div class="jobs" />' },
- PipelineGraph: { template: '<div id="graph" />' },
TestReports: { template: '<div id="tests" />' },
},
}),
diff --git a/spec/frontend/pipelines/empty_state/ci_templates_spec.js b/spec/frontend/pipelines/components/pipelines_list/empty_state/ci_templates_spec.js
index 606fdc9cac1..6531a15ab8e 100644
--- a/spec/frontend/pipelines/empty_state/ci_templates_spec.js
+++ b/spec/frontend/pipelines/components/pipelines_list/empty_state/ci_templates_spec.js
@@ -13,34 +13,35 @@ describe('CI Templates', () => {
let wrapper;
let trackingSpy;
- const createWrapper = () => {
- return shallowMountExtended(CiTemplates, {
+ const createWrapper = (propsData = {}) => {
+ wrapper = shallowMountExtended(CiTemplates, {
provide: {
pipelineEditorPath,
suggestedCiTemplates,
},
+ propsData,
});
};
const findTemplateDescription = () => wrapper.findByTestId('template-description');
const findTemplateLink = () => wrapper.findByTestId('template-link');
+ const findTemplateNames = () => wrapper.findAllByTestId('template-name');
const findTemplateName = () => wrapper.findByTestId('template-name');
const findTemplateLogo = () => wrapper.findByTestId('template-logo');
- beforeEach(() => {
- wrapper = createWrapper();
- });
-
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
describe('renders template list', () => {
- it('renders all suggested templates', () => {
- const content = wrapper.text();
+ beforeEach(() => {
+ createWrapper();
+ });
- expect(content).toContain('Android', 'Bash', 'C++');
+ it('renders all suggested templates', () => {
+ expect(findTemplateNames().length).toBe(3);
+ expect(wrapper.text()).toContain('Android', 'Bash', 'C++');
});
it('has the correct template name', () => {
@@ -53,9 +54,13 @@ describe('CI Templates', () => {
);
});
+ it('has the link button enabled', () => {
+ expect(findTemplateLink().props('disabled')).toBe(false);
+ });
+
it('has the description of the template', () => {
expect(findTemplateDescription().text()).toBe(
- 'CI/CD template to test and deploy your Android project.',
+ 'Continuous integration and deployment template to test and deploy your Android project.',
);
});
@@ -64,8 +69,30 @@ describe('CI Templates', () => {
});
});
+ describe('filtering the templates', () => {
+ beforeEach(() => {
+ createWrapper({ filterTemplates: ['Bash'] });
+ });
+
+ it('renders only the filtered templates', () => {
+ expect(findTemplateNames()).toHaveLength(1);
+ expect(findTemplateName().text()).toBe('Bash');
+ });
+ });
+
+ describe('disabling the templates', () => {
+ beforeEach(() => {
+ createWrapper({ disabled: true });
+ });
+
+ it('has the link button disabled', () => {
+ expect(findTemplateLink().props('disabled')).toBe(true);
+ });
+ });
+
describe('tracking', () => {
beforeEach(() => {
+ createWrapper();
trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
});
diff --git a/spec/frontend/pipelines/components/pipelines_list/empty_state/ios_templates_spec.js b/spec/frontend/pipelines/components/pipelines_list/empty_state/ios_templates_spec.js
new file mode 100644
index 00000000000..0c2938921d6
--- /dev/null
+++ b/spec/frontend/pipelines/components/pipelines_list/empty_state/ios_templates_spec.js
@@ -0,0 +1,138 @@
+import '~/commons';
+import { nextTick } from 'vue';
+import { GlPopover, GlButton } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import RunnerInstructionsModal from '~/vue_shared/components/runner_instructions/runner_instructions_modal.vue';
+import IosTemplates from '~/pipelines/components/pipelines_list/empty_state/ios_templates.vue';
+import CiTemplates from '~/pipelines/components/pipelines_list/empty_state/ci_templates.vue';
+
+const pipelineEditorPath = '/-/ci/editor';
+const registrationToken = 'SECRET_TOKEN';
+const iOSTemplateName = 'iOS-Fastlane';
+
+describe('iOS Templates', () => {
+ let wrapper;
+
+ const createWrapper = (providedPropsData = {}) => {
+ return shallowMountExtended(IosTemplates, {
+ provide: {
+ pipelineEditorPath,
+ iosRunnersAvailable: true,
+ ...providedPropsData,
+ },
+ propsData: {
+ registrationToken,
+ },
+ stubs: {
+ GlButton,
+ },
+ });
+ };
+
+ const findIosTemplate = () => wrapper.findComponent(CiTemplates);
+ const findRunnerInstructionsModal = () => wrapper.findComponent(RunnerInstructionsModal);
+ const findRunnerInstructionsPopover = () => wrapper.findComponent(GlPopover);
+ const findRunnerSetupTodoEmoji = () => wrapper.findByTestId('runner-setup-marked-todo');
+ const findRunnerSetupCompletedEmoji = () => wrapper.findByTestId('runner-setup-marked-completed');
+ const findSetupRunnerLink = () => wrapper.findByText('Set up a runner');
+ const configurePipelineLink = () => wrapper.findByTestId('configure-pipeline-link');
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ describe('when ios runners are not available', () => {
+ beforeEach(() => {
+ wrapper = createWrapper({ iosRunnersAvailable: false });
+ });
+
+ describe('the runner setup section', () => {
+ it('marks the section as todo', () => {
+ expect(findRunnerSetupTodoEmoji().isVisible()).toBe(true);
+ expect(findRunnerSetupCompletedEmoji().isVisible()).toBe(false);
+ });
+
+ it('renders the setup runner link', () => {
+ expect(findSetupRunnerLink().exists()).toBe(true);
+ });
+
+ it('renders the runner instructions modal with a popover once clicked', async () => {
+ findSetupRunnerLink().element.parentElement.click();
+
+ await nextTick();
+
+ expect(findRunnerInstructionsModal().exists()).toBe(true);
+ expect(findRunnerInstructionsModal().props('registrationToken')).toBe(registrationToken);
+ expect(findRunnerInstructionsModal().props('defaultPlatformName')).toBe('osx');
+
+ findRunnerInstructionsModal().vm.$emit('shown');
+
+ await nextTick();
+
+ expect(findRunnerInstructionsPopover().exists()).toBe(true);
+ });
+ });
+
+ describe('the configure pipeline section', () => {
+ it('has a disabled link button', () => {
+ expect(configurePipelineLink().props('disabled')).toBe(true);
+ });
+ });
+
+ describe('the ios-Fastlane template', () => {
+ it('renders the template', () => {
+ expect(findIosTemplate().props('filterTemplates')).toStrictEqual([iOSTemplateName]);
+ });
+
+ it('has a disabled link button', () => {
+ expect(findIosTemplate().props('disabled')).toBe(true);
+ });
+ });
+ });
+
+ describe('when ios runners are available', () => {
+ beforeEach(() => {
+ wrapper = createWrapper();
+ });
+
+ describe('the runner setup section', () => {
+ it('marks the section as completed', () => {
+ expect(findRunnerSetupTodoEmoji().isVisible()).toBe(false);
+ expect(findRunnerSetupCompletedEmoji().isVisible()).toBe(true);
+ });
+
+ it('does not render the setup runner link', () => {
+ expect(findSetupRunnerLink().exists()).toBe(false);
+ });
+ });
+
+ describe('the configure pipeline section', () => {
+ it('has an enabled link button', () => {
+ expect(configurePipelineLink().props('disabled')).toBe(false);
+ });
+
+ it('links to the pipeline editor with the right template', () => {
+ expect(configurePipelineLink().attributes('href')).toBe(
+ `${pipelineEditorPath}?template=${iOSTemplateName}`,
+ );
+ });
+ });
+
+ describe('the ios-Fastlane template', () => {
+ it('renders the template', () => {
+ expect(findIosTemplate().props('filterTemplates')).toStrictEqual([iOSTemplateName]);
+ });
+
+ it('has an enabled link button', () => {
+ expect(findIosTemplate().props('disabled')).toBe(false);
+ });
+
+ it('links to the pipeline editor with the right template', () => {
+ expect(configurePipelineLink().attributes('href')).toBe(
+ `${pipelineEditorPath}?template=${iOSTemplateName}`,
+ );
+ });
+ });
+ });
+});
diff --git a/spec/frontend/pipelines/empty_state/pipelines_ci_templates_spec.js b/spec/frontend/pipelines/components/pipelines_list/empty_state/pipelines_ci_templates_spec.js
index 14860f20317..b537c81da3f 100644
--- a/spec/frontend/pipelines/empty_state/pipelines_ci_templates_spec.js
+++ b/spec/frontend/pipelines/components/pipelines_list/empty_state/pipelines_ci_templates_spec.js
@@ -16,6 +16,7 @@ import {
} from '~/pipeline_editor/constants';
const pipelineEditorPath = '/-/ci/editor';
+const ciRunnerSettingsPath = '/-/settings/ci_cd';
jest.mock('~/experimentation/experiment_tracking');
@@ -27,8 +28,10 @@ describe('Pipelines CI Templates', () => {
return shallowMountExtended(PipelinesCiTemplates, {
provide: {
pipelineEditorPath,
+ ciRunnerSettingsPath,
+ anyRunnersAvailable: true,
+ ...propsData,
},
- propsData,
stubs,
});
};
diff --git a/spec/frontend/pipelines/components/pipelines_list/pipeline_stage_spec.js b/spec/frontend/pipelines/components/pipelines_list/pipeline_stage_spec.js
index 93bc8faa51b..6d0e99ff63e 100644
--- a/spec/frontend/pipelines/components/pipelines_list/pipeline_stage_spec.js
+++ b/spec/frontend/pipelines/components/pipelines_list/pipeline_stage_spec.js
@@ -1,6 +1,7 @@
import { GlDropdown } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
+import CiIcon from '~/vue_shared/components/ci_icon.vue';
import axios from '~/lib/utils/axios_utils';
import PipelineStage from '~/pipelines/components/pipelines_list/pipeline_stage.vue';
import eventHub from '~/pipelines/event_hub';
@@ -48,11 +49,12 @@ describe('Pipelines stage component', () => {
mock.restore();
});
+ const findCiActionBtn = () => wrapper.find('.js-ci-action');
+ const findCiIcon = () => wrapper.findComponent(CiIcon);
const findDropdown = () => wrapper.findComponent(GlDropdown);
const findDropdownToggle = () => wrapper.find('button.dropdown-toggle');
const findDropdownMenu = () =>
wrapper.find('[data-testid="mini-pipeline-graph-dropdown-menu-list"]');
- const findCiActionBtn = () => wrapper.find('.js-ci-action');
const findMergeTrainWarning = () => wrapper.find('[data-testid="warning-message-merge-trains"]');
const openStageDropdown = () => {
@@ -74,7 +76,7 @@ describe('Pipelines stage component', () => {
it('should render a dropdown with the status icon', () => {
expect(findDropdown().exists()).toBe(true);
expect(findDropdownToggle().exists()).toBe(true);
- expect(wrapper.find('[data-testid="status_success_borderless-icon"]').exists()).toBe(true);
+ expect(findCiIcon().exists()).toBe(true);
});
});
diff --git a/spec/frontend/pipelines/empty_state_spec.js b/spec/frontend/pipelines/empty_state_spec.js
index 46dad4a035c..0abf7f59717 100644
--- a/spec/frontend/pipelines/empty_state_spec.js
+++ b/spec/frontend/pipelines/empty_state_spec.js
@@ -1,7 +1,11 @@
import '~/commons';
-import { mount } from '@vue/test-utils';
+import { shallowMount } from '@vue/test-utils';
+import { GlEmptyState } from '@gitlab/ui';
+import { stubExperiments } from 'helpers/experimentation_helper';
import EmptyState from '~/pipelines/components/pipelines_list/empty_state.vue';
+import GitlabExperiment from '~/experimentation/components/gitlab_experiment.vue';
import PipelinesCiTemplates from '~/pipelines/components/pipelines_list/empty_state/pipelines_ci_templates.vue';
+import IosTemplates from '~/pipelines/components/pipelines_list/empty_state/ios_templates.vue';
describe('Pipelines Empty State', () => {
let wrapper;
@@ -9,44 +13,68 @@ describe('Pipelines Empty State', () => {
const findIllustration = () => wrapper.find('img');
const findButton = () => wrapper.find('a');
const pipelinesCiTemplates = () => wrapper.findComponent(PipelinesCiTemplates);
+ const iosTemplates = () => wrapper.findComponent(IosTemplates);
const createWrapper = (props = {}) => {
- wrapper = mount(EmptyState, {
+ wrapper = shallowMount(EmptyState, {
provide: {
pipelineEditorPath: '',
suggestedCiTemplates: [],
+ anyRunnersAvailable: true,
+ ciRunnerSettingsPath: '',
},
propsData: {
emptyStateSvgPath: 'foo.svg',
canSetCi: true,
...props,
},
+ stubs: {
+ GlEmptyState,
+ GitlabExperiment,
+ },
});
};
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
describe('when user can configure CI', () => {
- beforeEach(() => {
- createWrapper({}, mount);
- });
+ describe('when the ios_specific_templates experiment is active', () => {
+ beforeEach(() => {
+ stubExperiments({ ios_specific_templates: 'candidate' });
+ createWrapper();
+ });
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
+ it('should render the iOS templates', () => {
+ expect(iosTemplates().exists()).toBe(true);
+ });
+
+ it('should not render the CI/CD templates', () => {
+ expect(pipelinesCiTemplates().exists()).toBe(false);
+ });
});
- it('should render the CI/CD templates', () => {
- expect(pipelinesCiTemplates().exists()).toBe(true);
+ describe('when the ios_specific_templates experiment is inactive', () => {
+ beforeEach(() => {
+ stubExperiments({ ios_specific_templates: 'control' });
+ createWrapper();
+ });
+
+ it('should render the CI/CD templates', () => {
+ expect(pipelinesCiTemplates().exists()).toBe(true);
+ });
+
+ it('should not render the iOS templates', () => {
+ expect(iosTemplates().exists()).toBe(false);
+ });
});
});
describe('when user cannot configure CI', () => {
beforeEach(() => {
- createWrapper({ canSetCi: false }, mount);
- });
-
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
+ createWrapper({ canSetCi: false });
});
it('should render empty state SVG', () => {
diff --git a/spec/frontend/pipelines/graph/graph_component_wrapper_spec.js b/spec/frontend/pipelines/graph/graph_component_wrapper_spec.js
index cb7073fb5f5..49d64c6eac0 100644
--- a/spec/frontend/pipelines/graph/graph_component_wrapper_spec.js
+++ b/spec/frontend/pipelines/graph/graph_component_wrapper_spec.js
@@ -16,7 +16,7 @@ import {
} from '~/performance/constants';
import * as perfUtils from '~/performance/utils';
import {
- IID_FAILURE,
+ ACTION_FAILURE,
LAYER_VIEW,
STAGE_VIEW,
VIEW_TYPE_KEY,
@@ -188,7 +188,9 @@ describe('Pipeline graph wrapper', () => {
it('displays the no iid alert', () => {
expect(getAlert().exists()).toBe(true);
- expect(getAlert().text()).toBe(wrapper.vm.$options.errorTexts[IID_FAILURE]);
+ expect(getAlert().text()).toBe(
+ 'The data in this pipeline is too old to be rendered as a graph. Please check the Jobs tab to access historical data.',
+ );
});
it('does not display the graph', () => {
@@ -196,6 +198,27 @@ describe('Pipeline graph wrapper', () => {
});
});
+ describe('when there is an error with an action in the graph', () => {
+ beforeEach(async () => {
+ createComponentWithApollo();
+ await waitForPromises();
+ await getGraph().vm.$emit('error', { type: ACTION_FAILURE });
+ });
+
+ it('does not display the loading icon', () => {
+ expect(getLoadingIcon().exists()).toBe(false);
+ });
+
+ it('displays the action error alert', () => {
+ expect(getAlert().exists()).toBe(true);
+ expect(getAlert().text()).toBe('An error occurred while performing this action.');
+ });
+
+ it('displays the graph', () => {
+ expect(getGraph().exists()).toBe(true);
+ });
+ });
+
describe('when refresh action is emitted', () => {
beforeEach(async () => {
createComponentWithApollo();
diff --git a/spec/frontend/pipelines/graph/job_item_spec.js b/spec/frontend/pipelines/graph/job_item_spec.js
index 23e7ed7ebb4..4f0da09fec6 100644
--- a/spec/frontend/pipelines/graph/job_item_spec.js
+++ b/spec/frontend/pipelines/graph/job_item_spec.js
@@ -1,89 +1,34 @@
import { mount } from '@vue/test-utils';
import { nextTick } from 'vue';
+import { GlBadge } from '@gitlab/ui';
import JobItem from '~/pipelines/components/graph/job_item.vue';
+import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import {
+ delayedJob,
+ mockJob,
+ mockJobWithoutDetails,
+ mockJobWithUnauthorizedAction,
+ triggerJob,
+} from './mock_data';
describe('pipeline graph job item', () => {
let wrapper;
- const findJobWithoutLink = () => wrapper.find('[data-testid="job-without-link"]');
- const findJobWithLink = () => wrapper.find('[data-testid="job-with-link"]');
- const findActionComponent = () => wrapper.find('[data-testid="ci-action-component"]');
+ const findJobWithoutLink = () => wrapper.findByTestId('job-without-link');
+ const findJobWithLink = () => wrapper.findByTestId('job-with-link');
+ const findActionComponent = () => wrapper.findByTestId('ci-action-component');
+ const findBadge = () => wrapper.findComponent(GlBadge);
const createWrapper = (propsData) => {
- wrapper = mount(JobItem, {
- propsData,
- });
+ wrapper = extendedWrapper(
+ mount(JobItem, {
+ propsData,
+ }),
+ );
};
const triggerActiveClass = 'gl-shadow-x0-y0-b3-s1-blue-500';
- const delayedJob = {
- __typename: 'CiJob',
- name: 'delayed job',
- scheduledAt: '2015-07-03T10:01:00.000Z',
- needs: [],
- status: {
- __typename: 'DetailedStatus',
- icon: 'status_scheduled',
- tooltip: 'delayed manual action (%{remainingTime})',
- hasDetails: true,
- detailsPath: '/root/kinder-pipe/-/jobs/5339',
- group: 'scheduled',
- action: {
- __typename: 'StatusAction',
- icon: 'time-out',
- title: 'Unschedule',
- path: '/frontend-fixtures/builds-project/-/jobs/142/unschedule',
- buttonTitle: 'Unschedule job',
- },
- },
- };
-
- const mockJob = {
- id: 4256,
- name: 'test',
- status: {
- icon: 'status_success',
- text: 'passed',
- label: 'passed',
- tooltip: 'passed',
- group: 'success',
- detailsPath: '/root/ci-mock/builds/4256',
- hasDetails: true,
- action: {
- icon: 'retry',
- title: 'Retry',
- path: '/root/ci-mock/builds/4256/retry',
- method: 'post',
- },
- },
- };
- const mockJobWithoutDetails = {
- id: 4257,
- name: 'job_without_details',
- status: {
- icon: 'status_success',
- text: 'passed',
- label: 'passed',
- group: 'success',
- detailsPath: '/root/ci-mock/builds/4257',
- hasDetails: false,
- },
- };
- const mockJobWithUnauthorizedAction = {
- id: 4258,
- name: 'stop-environment',
- status: {
- icon: 'status_manual',
- label: 'manual stop action (not allowed)',
- tooltip: 'manual action',
- group: 'manual',
- detailsPath: '/root/ci-mock/builds/4258',
- hasDetails: true,
- action: null,
- },
- };
-
afterEach(() => {
wrapper.destroy();
});
@@ -148,13 +93,25 @@ describe('pipeline graph job item', () => {
});
});
- it('should render provided class name', () => {
- createWrapper({
- job: mockJob,
- cssClassJobName: 'css-class-job-name',
+ describe('job style', () => {
+ beforeEach(() => {
+ createWrapper({
+ job: mockJob,
+ cssClassJobName: 'css-class-job-name',
+ });
+ });
+
+ it('should render provided class name', () => {
+ expect(wrapper.find('a').classes()).toContain('css-class-job-name');
+ });
+
+ it('does not show a badge on the job item', () => {
+ expect(findBadge().exists()).toBe(false);
});
- expect(wrapper.find('a').classes()).toContain('css-class-job-name');
+ it('does not apply the trigger job class', () => {
+ expect(findJobWithLink().classes()).not.toContain('gl-rounded-lg');
+ });
});
describe('status label', () => {
@@ -201,34 +158,51 @@ describe('pipeline graph job item', () => {
});
});
- describe('trigger job highlighting', () => {
- it.each`
- job | jobName | expanded | link
- ${mockJob} | ${mockJob.name} | ${true} | ${true}
- ${mockJobWithoutDetails} | ${mockJobWithoutDetails.name} | ${true} | ${false}
- `(
- `trigger job should stay highlighted when downstream is expanded`,
- ({ job, jobName, expanded, link }) => {
- createWrapper({ job, pipelineExpanded: { jobName, expanded } });
- const findJobEl = link ? findJobWithLink : findJobWithoutLink;
-
- expect(findJobEl().classes()).toContain(triggerActiveClass);
- },
- );
+ describe('trigger job', () => {
+ describe('card', () => {
+ beforeEach(() => {
+ createWrapper({ job: triggerJob });
+ });
- it.each`
- job | jobName | expanded | link
- ${mockJob} | ${mockJob.name} | ${false} | ${true}
- ${mockJobWithoutDetails} | ${mockJobWithoutDetails.name} | ${false} | ${false}
- `(
- `trigger job should not be highlighted when downstream is not expanded`,
- ({ job, jobName, expanded, link }) => {
- createWrapper({ job, pipelineExpanded: { jobName, expanded } });
- const findJobEl = link ? findJobWithLink : findJobWithoutLink;
-
- expect(findJobEl().classes()).not.toContain(triggerActiveClass);
- },
- );
+ it('shows a badge on the job item', () => {
+ expect(findBadge().exists()).toBe(true);
+ expect(findBadge().text()).toBe('Trigger job');
+ });
+
+ it('applies a rounded corner style instead of the usual pill shape', () => {
+ expect(findJobWithoutLink().classes()).toContain('gl-rounded-lg');
+ });
+ });
+
+ describe('highlighting', () => {
+ it.each`
+ job | jobName | expanded | link
+ ${mockJob} | ${mockJob.name} | ${true} | ${true}
+ ${mockJobWithoutDetails} | ${mockJobWithoutDetails.name} | ${true} | ${false}
+ `(
+ `trigger job should stay highlighted when downstream is expanded`,
+ ({ job, jobName, expanded, link }) => {
+ createWrapper({ job, pipelineExpanded: { jobName, expanded } });
+ const findJobEl = link ? findJobWithLink : findJobWithoutLink;
+
+ expect(findJobEl().classes()).toContain(triggerActiveClass);
+ },
+ );
+
+ it.each`
+ job | jobName | expanded | link
+ ${mockJob} | ${mockJob.name} | ${false} | ${true}
+ ${mockJobWithoutDetails} | ${mockJobWithoutDetails.name} | ${false} | ${false}
+ `(
+ `trigger job should not be highlighted when downstream is not expanded`,
+ ({ job, jobName, expanded, link }) => {
+ createWrapper({ job, pipelineExpanded: { jobName, expanded } });
+ const findJobEl = link ? findJobWithLink : findJobWithoutLink;
+
+ expect(findJobEl().classes()).not.toContain(triggerActiveClass);
+ },
+ );
+ });
});
describe('job classes', () => {
diff --git a/spec/frontend/pipelines/graph/linked_pipeline_spec.js b/spec/frontend/pipelines/graph/linked_pipeline_spec.js
index d800a8c341e..06fd970778c 100644
--- a/spec/frontend/pipelines/graph/linked_pipeline_spec.js
+++ b/spec/frontend/pipelines/graph/linked_pipeline_spec.js
@@ -1,11 +1,21 @@
-import { GlButton, GlLoadingIcon } from '@gitlab/ui';
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import { GlButton, GlLoadingIcon, GlTooltip } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import { convertToGraphQLId } from '~/graphql_shared/utils';
import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants';
-import { UPSTREAM, DOWNSTREAM } from '~/pipelines/components/graph/constants';
+import { ACTION_FAILURE, UPSTREAM, DOWNSTREAM } from '~/pipelines/components/graph/constants';
import LinkedPipelineComponent from '~/pipelines/components/graph/linked_pipeline.vue';
+import { PIPELINE_GRAPHQL_TYPE } from '~/pipelines/constants';
+import CancelPipelineMutation from '~/pipelines/graphql/mutations/cancel_pipeline.mutation.graphql';
+import RetryPipelineMutation from '~/pipelines/graphql/mutations/retry_pipeline.mutation.graphql';
import CiStatus from '~/vue_shared/components/ci_icon.vue';
import mockPipeline from './linked_pipelines_mock_data';
+Vue.use(VueApollo);
+
describe('Linked pipeline', () => {
let wrapper;
@@ -27,22 +37,30 @@ describe('Linked pipeline', () => {
};
const findButton = () => wrapper.find(GlButton);
- const findDownstreamPipelineTitle = () => wrapper.find('[data-testid="downstream-title"]');
- const findPipelineLabel = () => wrapper.find('[data-testid="downstream-pipeline-label"]');
+ const findCancelButton = () => wrapper.findByLabelText('Cancel downstream pipeline');
+ const findCardTooltip = () => wrapper.findComponent(GlTooltip);
+ const findDownstreamPipelineTitle = () => wrapper.findByTestId('downstream-title');
+ const findExpandButton = () => wrapper.findByTestId('expand-pipeline-button');
const findLinkedPipeline = () => wrapper.find({ ref: 'linkedPipeline' });
const findLoadingIcon = () => wrapper.find(GlLoadingIcon);
- const findPipelineLink = () => wrapper.find('[data-testid="pipelineLink"]');
- const findExpandButton = () => wrapper.find('[data-testid="expand-pipeline-button"]');
-
- const createWrapper = (propsData, data = []) => {
- wrapper = mount(LinkedPipelineComponent, {
- propsData,
- data() {
- return {
- ...data,
- };
- },
- });
+ const findPipelineLabel = () => wrapper.findByTestId('downstream-pipeline-label');
+ const findPipelineLink = () => wrapper.findByTestId('pipelineLink');
+ const findRetryButton = () => wrapper.findByLabelText('Retry downstream pipeline');
+
+ const createWrapper = ({ propsData, downstreamRetryAction = false }) => {
+ const mockApollo = createMockApollo();
+
+ wrapper = extendedWrapper(
+ mount(LinkedPipelineComponent, {
+ propsData,
+ provide: {
+ glFeatures: {
+ downstreamRetryAction,
+ },
+ },
+ apolloProvider: mockApollo,
+ }),
+ );
};
afterEach(() => {
@@ -59,7 +77,7 @@ describe('Linked pipeline', () => {
};
beforeEach(() => {
- createWrapper(props);
+ createWrapper({ propsData: props });
});
it('should render the project name', () => {
@@ -84,18 +102,13 @@ describe('Linked pipeline', () => {
expect(wrapper.text()).toContain(`#${props.pipeline.id}`);
});
- it('should correctly compute the tooltip text', () => {
- expect(wrapper.vm.tooltipText).toContain(mockPipeline.project.name);
- expect(wrapper.vm.tooltipText).toContain(mockPipeline.status.label);
- expect(wrapper.vm.tooltipText).toContain(mockPipeline.sourceJob.name);
- expect(wrapper.vm.tooltipText).toContain(mockPipeline.id);
- });
+ it('adds the card tooltip text to the DOM', () => {
+ expect(findCardTooltip().exists()).toBe(true);
- it('should render the tooltip text as the title attribute', () => {
- const titleAttr = findLinkedPipeline().attributes('title');
-
- expect(titleAttr).toContain(mockPipeline.project.name);
- expect(titleAttr).toContain(mockPipeline.status.label);
+ expect(findCardTooltip().text()).toContain(mockPipeline.project.name);
+ expect(findCardTooltip().text()).toContain(mockPipeline.status.label);
+ expect(findCardTooltip().text()).toContain(mockPipeline.sourceJob.name);
+ expect(findCardTooltip().text()).toContain(mockPipeline.id);
});
it('should display multi-project label when pipeline project id is not the same as triggered pipeline project id', () => {
@@ -105,7 +118,7 @@ describe('Linked pipeline', () => {
describe('upstream pipelines', () => {
beforeEach(() => {
- createWrapper(upstreamProps);
+ createWrapper({ propsData: upstreamProps });
});
it('should display parent label when pipeline project id is the same as triggered_by pipeline project id', () => {
@@ -123,45 +136,246 @@ describe('Linked pipeline', () => {
});
describe('downstream pipelines', () => {
- beforeEach(() => {
- createWrapper(downstreamProps);
- });
-
- it('parent/child label container should exist', () => {
- expect(findPipelineLabel().exists()).toBe(true);
- });
-
- it('should display child label when pipeline project id is the same as triggered pipeline project id', () => {
- expect(findPipelineLabel().exists()).toBe(true);
- });
-
- it('should have the name of the trigger job on the card when it is a child pipeline', () => {
- expect(findDownstreamPipelineTitle().text()).toBe(mockPipeline.sourceJob.name);
- });
-
- it('downstream pipeline should contain the correct link', () => {
- expect(findPipelineLink().attributes('href')).toBe(downstreamProps.pipeline.path);
+ describe('styling', () => {
+ beforeEach(() => {
+ createWrapper({ propsData: downstreamProps });
+ });
+
+ it('parent/child label container should exist', () => {
+ expect(findPipelineLabel().exists()).toBe(true);
+ });
+
+ it('should display child label when pipeline project id is the same as triggered pipeline project id', () => {
+ expect(findPipelineLabel().exists()).toBe(true);
+ });
+
+ it('should have the name of the trigger job on the card when it is a child pipeline', () => {
+ expect(findDownstreamPipelineTitle().text()).toBe(mockPipeline.sourceJob.name);
+ });
+
+ it('downstream pipeline should contain the correct link', () => {
+ expect(findPipelineLink().attributes('href')).toBe(downstreamProps.pipeline.path);
+ });
+
+ it('applies the flex-row css class to the card', () => {
+ expect(findLinkedPipeline().classes()).toContain('gl-flex-direction-row');
+ expect(findLinkedPipeline().classes()).not.toContain('gl-flex-direction-row-reverse');
+ });
});
- it('applies the flex-row css class to the card', () => {
- expect(findLinkedPipeline().classes()).toContain('gl-flex-direction-row');
- expect(findLinkedPipeline().classes()).not.toContain('gl-flex-direction-row-reverse');
+ describe('action button', () => {
+ describe('with the `downstream_retry_action` flag on', () => {
+ describe('with permissions', () => {
+ describe('on an upstream', () => {
+ describe('when retryable', () => {
+ beforeEach(() => {
+ const retryablePipeline = {
+ ...upstreamProps,
+ pipeline: { ...mockPipeline, retryable: true },
+ };
+
+ createWrapper({ propsData: retryablePipeline, downstreamRetryAction: true });
+ });
+
+ it('does not show the retry or cancel button', () => {
+ expect(findCancelButton().exists()).toBe(false);
+ expect(findRetryButton().exists()).toBe(false);
+ });
+ });
+ });
+
+ describe('on a downstream', () => {
+ describe('when retryable', () => {
+ beforeEach(() => {
+ const retryablePipeline = {
+ ...downstreamProps,
+ pipeline: { ...mockPipeline, retryable: true },
+ };
+
+ createWrapper({ propsData: retryablePipeline, downstreamRetryAction: true });
+ });
+
+ it('shows only the retry button', () => {
+ expect(findCancelButton().exists()).toBe(false);
+ expect(findRetryButton().exists()).toBe(true);
+ });
+
+ it('hides the card tooltip when the action button tooltip is hovered', async () => {
+ expect(findCardTooltip().exists()).toBe(true);
+
+ await findRetryButton().trigger('mouseover');
+
+ expect(findCardTooltip().exists()).toBe(false);
+ });
+
+ describe('and the retry button is clicked', () => {
+ describe('on success', () => {
+ beforeEach(async () => {
+ jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue();
+ jest.spyOn(wrapper.vm, '$emit');
+ await findRetryButton().trigger('click');
+ });
+
+ it('calls the retry mutation ', () => {
+ expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledTimes(1);
+ expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({
+ mutation: RetryPipelineMutation,
+ variables: {
+ id: convertToGraphQLId(PIPELINE_GRAPHQL_TYPE, mockPipeline.id),
+ },
+ });
+ });
+
+ it('emits the refreshPipelineGraph event', () => {
+ expect(wrapper.vm.$emit).toHaveBeenCalledWith('refreshPipelineGraph');
+ });
+ });
+
+ describe('on failure', () => {
+ beforeEach(async () => {
+ jest.spyOn(wrapper.vm.$apollo, 'mutate').mockRejectedValue({ errors: [] });
+ jest.spyOn(wrapper.vm, '$emit');
+ await findRetryButton().trigger('click');
+ });
+
+ it('emits an error event', () => {
+ expect(wrapper.vm.$emit).toHaveBeenCalledWith('error', {
+ type: ACTION_FAILURE,
+ });
+ });
+ });
+ });
+ });
+
+ describe('when cancelable', () => {
+ beforeEach(() => {
+ const cancelablePipeline = {
+ ...downstreamProps,
+ pipeline: { ...mockPipeline, cancelable: true },
+ };
+
+ createWrapper({ propsData: cancelablePipeline, downstreamRetryAction: true });
+ });
+
+ it('shows only the cancel button ', () => {
+ expect(findCancelButton().exists()).toBe(true);
+ expect(findRetryButton().exists()).toBe(false);
+ });
+
+ it('hides the card tooltip when the action button tooltip is hovered', async () => {
+ expect(findCardTooltip().exists()).toBe(true);
+
+ await findCancelButton().trigger('mouseover');
+
+ expect(findCardTooltip().exists()).toBe(false);
+ });
+
+ describe('and the cancel button is clicked', () => {
+ describe('on success', () => {
+ beforeEach(async () => {
+ jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue();
+ jest.spyOn(wrapper.vm, '$emit');
+ await findCancelButton().trigger('click');
+ });
+
+ it('calls the cancel mutation', () => {
+ expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledTimes(1);
+ expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({
+ mutation: CancelPipelineMutation,
+ variables: {
+ id: convertToGraphQLId(PIPELINE_GRAPHQL_TYPE, mockPipeline.id),
+ },
+ });
+ });
+ it('emits the refreshPipelineGraph event', () => {
+ expect(wrapper.vm.$emit).toHaveBeenCalledWith('refreshPipelineGraph');
+ });
+ });
+ describe('on failure', () => {
+ beforeEach(async () => {
+ jest.spyOn(wrapper.vm.$apollo, 'mutate').mockRejectedValue({ errors: [] });
+ jest.spyOn(wrapper.vm, '$emit');
+ await findCancelButton().trigger('click');
+ });
+ it('emits an error event', () => {
+ expect(wrapper.vm.$emit).toHaveBeenCalledWith('error', {
+ type: ACTION_FAILURE,
+ });
+ });
+ });
+ });
+ });
+
+ describe('when both cancellable and retryable', () => {
+ beforeEach(() => {
+ const pipelineWithTwoActions = {
+ ...downstreamProps,
+ pipeline: { ...mockPipeline, cancelable: true, retryable: true },
+ };
+
+ createWrapper({ propsData: pipelineWithTwoActions, downstreamRetryAction: true });
+ });
+
+ it('only shows the cancel button', () => {
+ expect(findRetryButton().exists()).toBe(false);
+ expect(findCancelButton().exists()).toBe(true);
+ });
+ });
+ });
+ });
+
+ describe('without permissions', () => {
+ beforeEach(() => {
+ const pipelineWithTwoActions = {
+ ...downstreamProps,
+ pipeline: {
+ ...mockPipeline,
+ cancelable: true,
+ retryable: true,
+ userPermissions: { updatePipeline: false },
+ },
+ };
+
+ createWrapper({ propsData: pipelineWithTwoActions });
+ });
+
+ it('does not show any action button', () => {
+ expect(findRetryButton().exists()).toBe(false);
+ expect(findCancelButton().exists()).toBe(false);
+ });
+ });
+ });
+
+ describe('with the `downstream_retry_action` flag off', () => {
+ beforeEach(() => {
+ const pipelineWithTwoActions = {
+ ...downstreamProps,
+ pipeline: { ...mockPipeline, cancelable: true, retryable: true },
+ };
+
+ createWrapper({ propsData: pipelineWithTwoActions });
+ });
+ it('does not show any action button', () => {
+ expect(findRetryButton().exists()).toBe(false);
+ expect(findCancelButton().exists()).toBe(false);
+ });
+ });
});
});
describe('expand button', () => {
it.each`
- pipelineType | anglePosition | borderClass | expanded
- ${downstreamProps} | ${'angle-right'} | ${'gl-border-l-1!'} | ${false}
- ${downstreamProps} | ${'angle-left'} | ${'gl-border-l-1!'} | ${true}
- ${upstreamProps} | ${'angle-left'} | ${'gl-border-r-1!'} | ${false}
- ${upstreamProps} | ${'angle-right'} | ${'gl-border-r-1!'} | ${true}
+ pipelineType | anglePosition | buttonBorderClasses | expanded
+ ${downstreamProps} | ${'angle-right'} | ${'gl-border-l-0!'} | ${false}
+ ${downstreamProps} | ${'angle-left'} | ${'gl-border-l-0!'} | ${true}
+ ${upstreamProps} | ${'angle-left'} | ${'gl-border-r-0!'} | ${false}
+ ${upstreamProps} | ${'angle-right'} | ${'gl-border-r-0!'} | ${true}
`(
- '$pipelineType.columnTitle pipeline button icon should be $anglePosition with $borderClass if expanded state is $expanded',
- ({ pipelineType, anglePosition, borderClass, expanded }) => {
- createWrapper({ ...pipelineType, expanded });
+ '$pipelineType.columnTitle pipeline button icon should be $anglePosition with $buttonBorderClasses if expanded state is $expanded',
+ ({ pipelineType, anglePosition, buttonBorderClasses, expanded }) => {
+ createWrapper({ propsData: { ...pipelineType, expanded } });
expect(findExpandButton().props('icon')).toBe(anglePosition);
- expect(findExpandButton().classes()).toContain(borderClass);
+ expect(findExpandButton().classes()).toContain(buttonBorderClasses);
},
);
});
@@ -176,7 +390,7 @@ describe('Linked pipeline', () => {
};
beforeEach(() => {
- createWrapper(props);
+ createWrapper({ propsData: props });
});
it('loading icon is visible', () => {
@@ -194,7 +408,7 @@ describe('Linked pipeline', () => {
};
beforeEach(() => {
- createWrapper(props);
+ createWrapper({ propsData: props });
});
it('emits `pipelineClicked` event', () => {
diff --git a/spec/frontend/pipelines/graph/linked_pipelines_column_spec.js b/spec/frontend/pipelines/graph/linked_pipelines_column_spec.js
index 1673065e09c..46000711110 100644
--- a/spec/frontend/pipelines/graph/linked_pipelines_column_spec.js
+++ b/spec/frontend/pipelines/graph/linked_pipelines_column_spec.js
@@ -67,7 +67,6 @@ describe('Linked Pipelines Column', () => {
afterEach(() => {
wrapper.destroy();
- wrapper = null;
});
describe('it renders correctly', () => {
diff --git a/spec/frontend/pipelines/graph/linked_pipelines_mock_data.js b/spec/frontend/pipelines/graph/linked_pipelines_mock_data.js
index 955b70cbd3b..f7f5738e46d 100644
--- a/spec/frontend/pipelines/graph/linked_pipelines_mock_data.js
+++ b/spec/frontend/pipelines/graph/linked_pipelines_mock_data.js
@@ -2,6 +2,11 @@ export default {
__typename: 'Pipeline',
id: 195,
iid: '5',
+ retryable: false,
+ cancelable: false,
+ userPermissions: {
+ updatePipeline: true,
+ },
path: '/root/elemenohpee/-/pipelines/195',
status: {
__typename: 'DetailedStatus',
diff --git a/spec/frontend/pipelines/graph/mock_data.js b/spec/frontend/pipelines/graph/mock_data.js
index 0cf7dc507f4..6124d67af09 100644
--- a/spec/frontend/pipelines/graph/mock_data.js
+++ b/spec/frontend/pipelines/graph/mock_data.js
@@ -1,4 +1,5 @@
import { unwrapPipelineData } from '~/pipelines/components/graph/utils';
+import { BUILD_KIND, BRIDGE_KIND } from '~/pipelines/components/graph/constants';
export const mockPipelineResponse = {
data: {
@@ -50,6 +51,7 @@ export const mockPipelineResponse = {
{
__typename: 'CiJob',
id: '6',
+ kind: BUILD_KIND,
name: 'build_a_nlfjkdnlvskfnksvjknlfdjvlvnjdkjdf_nvjkenjkrlngjeknjkl',
scheduledAt: null,
status: {
@@ -101,6 +103,7 @@ export const mockPipelineResponse = {
__typename: 'CiJob',
id: '11',
name: 'build_b',
+ kind: BUILD_KIND,
scheduledAt: null,
status: {
__typename: 'DetailedStatus',
@@ -151,6 +154,7 @@ export const mockPipelineResponse = {
__typename: 'CiJob',
id: '16',
name: 'build_c',
+ kind: BUILD_KIND,
scheduledAt: null,
status: {
__typename: 'DetailedStatus',
@@ -200,6 +204,7 @@ export const mockPipelineResponse = {
{
__typename: 'CiJob',
id: '21',
+ kind: BUILD_KIND,
name: 'build_d 1/3',
scheduledAt: null,
status: {
@@ -232,6 +237,7 @@ export const mockPipelineResponse = {
{
__typename: 'CiJob',
id: '24',
+ kind: BUILD_KIND,
name: 'build_d 2/3',
scheduledAt: null,
status: {
@@ -264,6 +270,7 @@ export const mockPipelineResponse = {
{
__typename: 'CiJob',
id: '27',
+ kind: BUILD_KIND,
name: 'build_d 3/3',
scheduledAt: null,
status: {
@@ -329,6 +336,7 @@ export const mockPipelineResponse = {
{
__typename: 'CiJob',
id: '34',
+ kind: BUILD_KIND,
name: 'test_a',
scheduledAt: null,
status: {
@@ -413,6 +421,7 @@ export const mockPipelineResponse = {
{
__typename: 'CiJob',
id: '42',
+ kind: BUILD_KIND,
name: 'test_b 1/2',
scheduledAt: null,
status: {
@@ -499,6 +508,7 @@ export const mockPipelineResponse = {
{
__typename: 'CiJob',
id: '67',
+ kind: BUILD_KIND,
name: 'test_b 2/2',
scheduledAt: null,
status: {
@@ -603,6 +613,7 @@ export const mockPipelineResponse = {
{
__typename: 'CiJob',
id: '59',
+ kind: BUILD_KIND,
name: 'test_c',
scheduledAt: null,
status: {
@@ -646,6 +657,7 @@ export const mockPipelineResponse = {
{
__typename: 'CiJob',
id: '53',
+ kind: BUILD_KIND,
name: 'test_d',
scheduledAt: null,
status: {
@@ -699,6 +711,11 @@ export const downstream = {
id: 175,
iid: '31',
path: '/root/elemenohpee/-/pipelines/175',
+ retryable: true,
+ cancelable: false,
+ userPermissions: {
+ updatePipeline: true,
+ },
status: {
id: '70',
group: 'success',
@@ -724,6 +741,11 @@ export const downstream = {
id: 181,
iid: '27',
path: '/root/abcd-dag/-/pipelines/181',
+ retryable: true,
+ cancelable: false,
+ userPermissions: {
+ updatePipeline: true,
+ },
status: {
id: '72',
group: 'success',
@@ -752,6 +774,11 @@ export const upstream = {
id: 161,
iid: '24',
path: '/root/abcd-dag/-/pipelines/161',
+ retryable: true,
+ cancelable: false,
+ userPermissions: {
+ updatePipeline: true,
+ },
status: {
id: '74',
group: 'success',
@@ -786,6 +813,11 @@ export const wrappedPipelineReturn = {
updatePipeline: true,
},
downstream: {
+ retryable: true,
+ cancelable: false,
+ userPermissions: {
+ updatePipeline: true,
+ },
__typename: 'PipelineConnection',
nodes: [],
},
@@ -793,6 +825,11 @@ export const wrappedPipelineReturn = {
id: 'gid://gitlab/Ci::Pipeline/174',
iid: '37',
path: '/root/elemenohpee/-/pipelines/174',
+ retryable: true,
+ cancelable: false,
+ userPermissions: {
+ updatePipeline: true,
+ },
__typename: 'Pipeline',
status: {
__typename: 'DetailedStatus',
@@ -846,6 +883,7 @@ export const wrappedPipelineReturn = {
{
__typename: 'CiJob',
id: '83',
+ kind: BUILD_KIND,
name: 'build_n',
scheduledAt: null,
needs: {
@@ -916,3 +954,87 @@ export const mockCalloutsResponse = (mappedCallouts) => ({
},
},
});
+
+export const delayedJob = {
+ __typename: 'CiJob',
+ kind: BUILD_KIND,
+ name: 'delayed job',
+ scheduledAt: '2015-07-03T10:01:00.000Z',
+ needs: [],
+ status: {
+ __typename: 'DetailedStatus',
+ icon: 'status_scheduled',
+ tooltip: 'delayed manual action (%{remainingTime})',
+ hasDetails: true,
+ detailsPath: '/root/kinder-pipe/-/jobs/5339',
+ group: 'scheduled',
+ action: {
+ __typename: 'StatusAction',
+ icon: 'time-out',
+ title: 'Unschedule',
+ path: '/frontend-fixtures/builds-project/-/jobs/142/unschedule',
+ buttonTitle: 'Unschedule job',
+ },
+ },
+};
+
+export const mockJob = {
+ id: 4256,
+ name: 'test',
+ kind: BUILD_KIND,
+ status: {
+ icon: 'status_success',
+ text: 'passed',
+ label: 'passed',
+ tooltip: 'passed',
+ group: 'success',
+ detailsPath: '/root/ci-mock/builds/4256',
+ hasDetails: true,
+ action: {
+ icon: 'retry',
+ title: 'Retry',
+ path: '/root/ci-mock/builds/4256/retry',
+ method: 'post',
+ },
+ },
+};
+
+export const mockJobWithoutDetails = {
+ id: 4257,
+ name: 'job_without_details',
+ status: {
+ icon: 'status_success',
+ text: 'passed',
+ label: 'passed',
+ group: 'success',
+ detailsPath: '/root/ci-mock/builds/4257',
+ hasDetails: false,
+ },
+};
+
+export const mockJobWithUnauthorizedAction = {
+ id: 4258,
+ name: 'stop-environment',
+ status: {
+ icon: 'status_manual',
+ label: 'manual stop action (not allowed)',
+ tooltip: 'manual action',
+ group: 'manual',
+ detailsPath: '/root/ci-mock/builds/4258',
+ hasDetails: true,
+ action: null,
+ },
+};
+
+export const triggerJob = {
+ id: 4259,
+ name: 'trigger',
+ kind: BRIDGE_KIND,
+ status: {
+ icon: 'status_success',
+ text: 'passed',
+ label: 'passed',
+ group: 'success',
+ action: null,
+ },
+};
diff --git a/spec/frontend/pipelines/graph_shared/links_inner_spec.js b/spec/frontend/pipelines/graph_shared/links_inner_spec.js
index be422fac92c..2c6d126e12c 100644
--- a/spec/frontend/pipelines/graph_shared/links_inner_spec.js
+++ b/spec/frontend/pipelines/graph_shared/links_inner_spec.js
@@ -1,5 +1,5 @@
import { shallowMount } from '@vue/test-utils';
-import { setHTMLFixture } from 'helpers/fixtures';
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import LinksInner from '~/pipelines/components/graph_shared/links_inner.vue';
import { parseData } from '~/pipelines/components/parsing_utils';
import { createJobsHash } from '~/pipelines/utils';
@@ -42,7 +42,7 @@ describe('Links Inner component', () => {
// We create fixture so that each job has an empty div that represent
// the JobPill in the DOM. Each `JobPill` would have different coordinates,
// so we increment their coordinates on each iteration to simulate different positions.
- const setFixtures = ({ stages }) => {
+ const setHTMLFixtureLocal = ({ stages }) => {
const jobs = createJobsHash(stages);
const arrayOfJobs = Object.keys(jobs);
@@ -82,6 +82,7 @@ describe('Links Inner component', () => {
afterEach(() => {
jest.restoreAllMocks();
wrapper.destroy();
+ resetHTMLFixture();
});
describe('basic SVG creation', () => {
@@ -124,7 +125,7 @@ describe('Links Inner component', () => {
describe('with one need', () => {
beforeEach(() => {
- setFixtures(pipelineData);
+ setHTMLFixtureLocal(pipelineData);
createComponent({ pipelineData: pipelineData.stages });
});
@@ -143,7 +144,7 @@ describe('Links Inner component', () => {
describe('with a parallel need', () => {
beforeEach(() => {
- setFixtures(parallelNeedData);
+ setHTMLFixtureLocal(parallelNeedData);
createComponent({ pipelineData: parallelNeedData.stages });
});
@@ -162,7 +163,7 @@ describe('Links Inner component', () => {
describe('with same stage needs', () => {
beforeEach(() => {
- setFixtures(sameStageNeeds);
+ setHTMLFixtureLocal(sameStageNeeds);
createComponent({ pipelineData: sameStageNeeds.stages });
});
@@ -181,7 +182,7 @@ describe('Links Inner component', () => {
describe('with a large number of needs', () => {
beforeEach(() => {
- setFixtures(largePipelineData);
+ setHTMLFixtureLocal(largePipelineData);
createComponent({ pipelineData: largePipelineData.stages });
});
@@ -200,7 +201,7 @@ describe('Links Inner component', () => {
describe('interactions', () => {
beforeEach(() => {
- setFixtures(largePipelineData);
+ setHTMLFixtureLocal(largePipelineData);
createComponent({ pipelineData: largePipelineData.stages });
});
diff --git a/spec/frontend/pipelines/header_component_spec.js b/spec/frontend/pipelines/header_component_spec.js
index c4639bd8e16..5cc11adf696 100644
--- a/spec/frontend/pipelines/header_component_spec.js
+++ b/spec/frontend/pipelines/header_component_spec.js
@@ -1,4 +1,4 @@
-import { GlModal, GlLoadingIcon } from '@gitlab/ui';
+import { GlAlert, GlModal, GlLoadingIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
import waitForPromises from 'helpers/wait_for_promises';
@@ -21,6 +21,7 @@ describe('Pipeline details header', () => {
let glModalDirective;
let mutate = jest.fn();
+ const findAlert = () => wrapper.find(GlAlert);
const findDeleteModal = () => wrapper.find(GlModal);
const findRetryButton = () => wrapper.find('[data-testid="retryPipeline"]');
const findCancelButton = () => wrapper.find('[data-testid="cancelPipeline"]');
@@ -121,6 +122,22 @@ describe('Pipeline details header', () => {
it('should render retry action tooltip', () => {
expect(findRetryButton().attributes('title')).toBe(BUTTON_TOOLTIP_RETRY);
});
+
+ it('should display error message on failure', async () => {
+ const failureMessage = 'failure message';
+ jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue({
+ data: {
+ pipelineRetry: {
+ errors: [failureMessage],
+ },
+ },
+ });
+
+ findRetryButton().vm.$emit('click');
+ await waitForPromises();
+
+ expect(findAlert().text()).toBe(failureMessage);
+ });
});
describe('Retry action failed', () => {
@@ -156,6 +173,22 @@ describe('Pipeline details header', () => {
variables: { id: mockRunningPipelineHeader.id },
});
});
+
+ it('should display error message on failure', async () => {
+ const failureMessage = 'failure message';
+ jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue({
+ data: {
+ pipelineCancel: {
+ errors: [failureMessage],
+ },
+ },
+ });
+
+ findCancelButton().vm.$emit('click');
+ await waitForPromises();
+
+ expect(findAlert().text()).toBe(failureMessage);
+ });
});
describe('Delete action', () => {
@@ -179,6 +212,22 @@ describe('Pipeline details header', () => {
variables: { id: mockFailedPipelineHeader.id },
});
});
+
+ it('should display error message on failure', async () => {
+ const failureMessage = 'failure message';
+ jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue({
+ data: {
+ pipelineDestroy: {
+ errors: [failureMessage],
+ },
+ },
+ });
+
+ findDeleteModal().vm.$emit('ok');
+ await waitForPromises();
+
+ expect(findAlert().text()).toBe(failureMessage);
+ });
});
describe('Permissions', () => {
diff --git a/spec/frontend/pipelines/mock_data.js b/spec/frontend/pipelines/mock_data.js
index 59d4e808b32..57d1511d859 100644
--- a/spec/frontend/pipelines/mock_data.js
+++ b/spec/frontend/pipelines/mock_data.js
@@ -1141,3 +1141,218 @@ export const mockPipelineBranch = () => {
viewType: 'root',
};
};
+
+export const mockFailedJobsQueryResponse = {
+ data: {
+ project: {
+ __typename: 'Project',
+ id: 'gid://gitlab/Project/20',
+ pipeline: {
+ __typename: 'Pipeline',
+ id: 'gid://gitlab/Ci::Pipeline/300',
+ jobs: {
+ __typename: 'CiJobConnection',
+ nodes: [
+ {
+ __typename: 'CiJob',
+ status: 'FAILED',
+ detailedStatus: {
+ __typename: 'DetailedStatus',
+ id: 'failed-1848-1848',
+ detailsPath: '/root/ci-project/-/jobs/1848',
+ group: 'failed',
+ icon: 'status_failed',
+ label: 'failed',
+ text: 'failed',
+ tooltip: 'failed - (script failure)',
+ action: {
+ __typename: 'StatusAction',
+ id: 'Ci::Build-failed-1848',
+ buttonTitle: 'Retry this job',
+ icon: 'retry',
+ method: 'post',
+ path: '/root/ci-project/-/jobs/1848/retry',
+ title: 'Retry',
+ },
+ },
+ id: 'gid://gitlab/Ci::Build/1848',
+ stage: {
+ __typename: 'CiStage',
+ id: 'gid://gitlab/Ci::Stage/358',
+ name: 'build',
+ },
+ name: 'wait_job',
+ retryable: true,
+ userPermissions: {
+ __typename: 'JobPermissions',
+ readBuild: true,
+ updateBuild: true,
+ },
+ },
+ {
+ __typename: 'CiJob',
+ status: 'FAILED',
+ detailedStatus: {
+ __typename: 'DetailedStatus',
+ id: 'failed-1710-1710',
+ detailsPath: '/root/ci-project/-/jobs/1710',
+ group: 'failed',
+ icon: 'status_failed',
+ label: 'failed',
+ text: 'failed',
+ tooltip: 'failed - (script failure) (retried)',
+ action: null,
+ },
+ id: 'gid://gitlab/Ci::Build/1710',
+ stage: {
+ __typename: 'CiStage',
+ id: 'gid://gitlab/Ci::Stage/358',
+ name: 'build',
+ },
+ name: 'wait_job',
+ retryable: false,
+ userPermissions: {
+ __typename: 'JobPermissions',
+ readBuild: true,
+ updateBuild: true,
+ },
+ },
+ ],
+ },
+ },
+ },
+ },
+};
+
+export const mockFailedJobsSummaryData = [
+ {
+ id: 1848,
+ failure: null,
+ failure_summary:
+ '<span>Pulling docker image node:latest ...<br/></span><span>Using docker image sha256:738d733448be00c72cb6618b7a06a1424806c6d239d8885e92f9b1e8727092b5 for node:latest with digest node@sha256:e5b7b349d517159246070bf14242027a9e220ffa8bd98a67ba1495d969c06c01 ...<br/></span><div class="section-start" data-timestamp="1651175313" data-section="prepare-script" role="button"></div><span class="term-fg-l-cyan term-bold section section-header js-s-prepare-script">Preparing environment</span><span class="section section-header js-s-prepare-script"><br/></span><span class="section line js-s-prepare-script">Running on runner-kvkqh24-project-20-concurrent-0 via 0706719b1b8d...<br/></span><div class="section-end" data-section="prepare-script"></div><div class="section-start" data-timestamp="1651175313" data-section="get-sources" role="button"></div><span class="term-fg-l-cyan term-bold section section-header js-s-get-sources">Getting source from Git repository</span><span class="section section-header js-s-get-sources"><br/></span><span class="term-fg-l-green term-bold section line js-s-get-sources">Fetching changes with git depth set to 50...</span><span class="section line js-s-get-sources"><br/>Reinitialized existing Git repository in /builds/root/ci-project/.git/<br/>fatal: couldn\'t find remote ref refs/heads/test<br/></span><div class="section-end" data-section="get-sources"></div><span class="term-fg-l-red term-bold">ERROR: Job failed: exit code 1<br/></span>',
+ },
+];
+
+export const mockFailedJobsData = [
+ {
+ normalizedId: 1848,
+ __typename: 'CiJob',
+ status: 'FAILED',
+ detailedStatus: {
+ __typename: 'DetailedStatus',
+ id: 'failed-1848-1848',
+ detailsPath: '/root/ci-project/-/jobs/1848',
+ group: 'failed',
+ icon: 'status_failed',
+ label: 'failed',
+ text: 'failed',
+ tooltip: 'failed - (script failure)',
+ action: {
+ __typename: 'StatusAction',
+ id: 'Ci::Build-failed-1848',
+ buttonTitle: 'Retry this job',
+ icon: 'retry',
+ method: 'post',
+ path: '/root/ci-project/-/jobs/1848/retry',
+ title: 'Retry',
+ },
+ },
+ id: 'gid://gitlab/Ci::Build/1848',
+ stage: { __typename: 'CiStage', id: 'gid://gitlab/Ci::Stage/358', name: 'build' },
+ name: 'wait_job',
+ retryable: true,
+ userPermissions: { __typename: 'JobPermissions', readBuild: true, updateBuild: true },
+ },
+ {
+ normalizedId: 1710,
+ __typename: 'CiJob',
+ status: 'FAILED',
+ detailedStatus: {
+ __typename: 'DetailedStatus',
+ id: 'failed-1710-1710',
+ detailsPath: '/root/ci-project/-/jobs/1710',
+ group: 'failed',
+ icon: 'status_failed',
+ label: 'failed',
+ text: 'failed',
+ tooltip: 'failed - (script failure) (retried)',
+ action: null,
+ },
+ id: 'gid://gitlab/Ci::Build/1710',
+ stage: { __typename: 'CiStage', id: 'gid://gitlab/Ci::Stage/358', name: 'build' },
+ name: 'wait_job',
+ retryable: false,
+ userPermissions: { __typename: 'JobPermissions', readBuild: true, updateBuild: true },
+ },
+];
+
+export const mockPreparedFailedJobsData = [
+ {
+ __typename: 'CiJob',
+ _showDetails: true,
+ detailedStatus: {
+ __typename: 'DetailedStatus',
+ action: {
+ __typename: 'StatusAction',
+ buttonTitle: 'Retry this job',
+ icon: 'retry',
+ id: 'Ci::Build-failed-1848',
+ method: 'post',
+ path: '/root/ci-project/-/jobs/1848/retry',
+ title: 'Retry',
+ },
+ detailsPath: '/root/ci-project/-/jobs/1848',
+ group: 'failed',
+ icon: 'status_failed',
+ id: 'failed-1848-1848',
+ label: 'failed',
+ text: 'failed',
+ tooltip: 'failed - (script failure)',
+ },
+ failure: null,
+ failureSummary:
+ '<span>Pulling docker image node:latest ...<br/></span><span>Using docker image sha256:738d733448be00c72cb6618b7a06a1424806c6d239d8885e92f9b1e8727092b5 for node:latest with digest node@sha256:e5b7b349d517159246070bf14242027a9e220ffa8bd98a67ba1495d969c06c01 ...<br/></span><div class="section-start" data-timestamp="1651175313" data-section="prepare-script" role="button"></div><span class="term-fg-l-cyan term-bold section section-header js-s-prepare-script">Preparing environment</span><span class="section section-header js-s-prepare-script"><br/></span><span class="section line js-s-prepare-script">Running on runner-kvkqh24-project-20-concurrent-0 via 0706719b1b8d...<br/></span><div class="section-end" data-section="prepare-script"></div><div class="section-start" data-timestamp="1651175313" data-section="get-sources" role="button"></div><span class="term-fg-l-cyan term-bold section section-header js-s-get-sources">Getting source from Git repository</span><span class="section section-header js-s-get-sources"><br/></span><span class="term-fg-l-green term-bold section line js-s-get-sources">Fetching changes with git depth set to 50...</span><span class="section line js-s-get-sources"><br/>Reinitialized existing Git repository in /builds/root/ci-project/.git/<br/>fatal: couldn\'t find remote ref refs/heads/test<br/></span><div class="section-end" data-section="get-sources"></div><span class="term-fg-l-red term-bold">ERROR: Job failed: exit code 1<br/></span>',
+ id: 'gid://gitlab/Ci::Build/1848',
+ name: 'wait_job',
+ normalizedId: 1848,
+ retryable: true,
+ stage: { __typename: 'CiStage', id: 'gid://gitlab/Ci::Stage/358', name: 'build' },
+ status: 'FAILED',
+ userPermissions: { __typename: 'JobPermissions', readBuild: true, updateBuild: true },
+ },
+];
+
+export const mockPreparedFailedJobsDataNoPermission = [
+ {
+ ...mockPreparedFailedJobsData[0],
+ userPermissions: { __typename: 'JobPermissions', readBuild: false, updateBuild: false },
+ },
+];
+
+export const successRetryMutationResponse = {
+ data: {
+ jobRetry: {
+ job: {
+ __typename: 'CiJob',
+ id: '"gid://gitlab/Ci::Build/1985"',
+ detailedStatus: {
+ detailsPath: '/root/project/-/jobs/1985',
+ id: 'pending-1985-1985',
+ __typename: 'DetailedStatus',
+ },
+ },
+ errors: [],
+ __typename: 'JobRetryPayload',
+ },
+ },
+};
+
+export const failedRetryMutationResponse = {
+ data: {
+ jobRetry: {
+ job: {},
+ errors: ['New Error'],
+ __typename: 'JobRetryPayload',
+ },
+ },
+};
diff --git a/spec/frontend/pipelines/pipeline_graph/utils_spec.js b/spec/frontend/pipelines/pipeline_graph/utils_spec.js
index 5816bc06fe3..d6b13da3c3a 100644
--- a/spec/frontend/pipelines/pipeline_graph/utils_spec.js
+++ b/spec/frontend/pipelines/pipeline_graph/utils_spec.js
@@ -1,4 +1,5 @@
-import { createJobsHash, generateJobNeedsDict } from '~/pipelines/utils';
+import { createJobsHash, generateJobNeedsDict, getPipelineDefaultTab } from '~/pipelines/utils';
+import { TAB_QUERY_PARAM, validPipelineTabNames } from '~/pipelines/constants';
describe('utils functions', () => {
const jobName1 = 'build_1';
@@ -169,4 +170,21 @@ describe('utils functions', () => {
});
});
});
+
+ describe('getPipelineDefaultTab', () => {
+ const baseUrl = 'http://gitlab.com/user/multi-projects-small/-/pipelines/332/';
+ it('returns null if there was no `tab` params', () => {
+ expect(getPipelineDefaultTab(baseUrl)).toBe(null);
+ });
+
+ it('returns null if there was no valid tab param', () => {
+ expect(getPipelineDefaultTab(`${baseUrl}?${TAB_QUERY_PARAM}=invalid`)).toBe(null);
+ });
+
+ it('returns the correct tab name if present', () => {
+ validPipelineTabNames.forEach((tabName) => {
+ expect(getPipelineDefaultTab(`${baseUrl}?${TAB_QUERY_PARAM}=${tabName}`)).toBe(tabName);
+ });
+ });
+ });
});
diff --git a/spec/frontend/pipelines/pipelines_spec.js b/spec/frontend/pipelines/pipelines_spec.js
index d2b30c93746..de9f394db43 100644
--- a/spec/frontend/pipelines/pipelines_spec.js
+++ b/spec/frontend/pipelines/pipelines_spec.js
@@ -82,6 +82,8 @@ describe('Pipelines', () => {
provide: {
pipelineEditorPath: '',
suggestedCiTemplates: [],
+ ciRunnerSettingsPath: paths.ciRunnerSettingsPath,
+ anyRunnersAvailable: true,
},
propsData: {
store: new Store(),
diff --git a/spec/frontend/pipelines/test_reports/stores/actions_spec.js b/spec/frontend/pipelines/test_reports/stores/actions_spec.js
index d5acb115bc1..74a9d8c354f 100644
--- a/spec/frontend/pipelines/test_reports/stores/actions_spec.js
+++ b/spec/frontend/pipelines/test_reports/stores/actions_spec.js
@@ -82,17 +82,16 @@ describe('Actions TestReports Store', () => {
);
});
- it('should create flash on API error', async () => {
+ it('should call SET_SUITE_ERROR on error', () => {
const index = 0;
- await testAction(
+ return testAction(
actions.fetchTestSuite,
index,
{ ...state, testReports, suiteEndpoint: null },
- [],
+ [{ type: types.SET_SUITE_ERROR, payload: expect.any(Error) }],
[{ type: 'toggleLoading' }, { type: 'toggleLoading' }],
);
- expect(createFlash).toHaveBeenCalled();
});
describe('when we already have the suite data', () => {
diff --git a/spec/frontend/pipelines/test_reports/stores/mutations_spec.js b/spec/frontend/pipelines/test_reports/stores/mutations_spec.js
index f2dbeec6a06..6ab479a257c 100644
--- a/spec/frontend/pipelines/test_reports/stores/mutations_spec.js
+++ b/spec/frontend/pipelines/test_reports/stores/mutations_spec.js
@@ -1,6 +1,9 @@
import testReports from 'test_fixtures/pipelines/test_report.json';
import * as types from '~/pipelines/stores/test_reports/mutation_types';
import mutations from '~/pipelines/stores/test_reports/mutations';
+import createFlash from '~/flash';
+
+jest.mock('~/flash.js');
describe('Mutations TestReports Store', () => {
let mockState;
@@ -44,6 +47,24 @@ describe('Mutations TestReports Store', () => {
});
});
+ describe('set suite error', () => {
+ it('should set the error message in state if provided', () => {
+ const message = 'Test report artifacts have expired';
+
+ mutations[types.SET_SUITE_ERROR](mockState, {
+ response: { data: { errors: message } },
+ });
+
+ expect(mockState.errorMessage).toBe(message);
+ });
+
+ it('should show a flash message otherwise', () => {
+ mutations[types.SET_SUITE_ERROR](mockState, {});
+
+ expect(createFlash).toHaveBeenCalled();
+ });
+ });
+
describe('set selected suite index', () => {
it('should set selectedSuiteIndex', () => {
const selectedSuiteIndex = 0;
diff --git a/spec/frontend/pipelines/test_reports/test_suite_table_spec.js b/spec/frontend/pipelines/test_reports/test_suite_table_spec.js
index 97241e14129..dc72fa31ace 100644
--- a/spec/frontend/pipelines/test_reports/test_suite_table_spec.js
+++ b/spec/frontend/pipelines/test_reports/test_suite_table_spec.js
@@ -1,12 +1,13 @@
import { GlButton, GlFriendlyWrap, GlLink, GlPagination } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import Vuex from 'vuex';
import testReports from 'test_fixtures/pipelines/test_report.json';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import SuiteTable from '~/pipelines/components/test_reports/test_suite_table.vue';
import { TestStatus } from '~/pipelines/constants';
import * as getters from '~/pipelines/stores/test_reports/getters';
import { formatFilePath } from '~/pipelines/stores/test_reports/utils';
+import { ARTIFACTS_EXPIRED_ERROR_MESSAGE } from '~/pipelines/stores/test_reports/constants';
import skippedTestCases from './mock_data';
Vue.use(Vuex);
@@ -23,13 +24,14 @@ describe('Test reports suite table', () => {
const testCases = testSuite.test_cases;
const blobPath = '/test/blob/path';
- const noCasesMessage = () => wrapper.find('.js-no-test-cases');
- const allCaseRows = () => wrapper.findAll('.js-case-row');
- const findCaseRowAtIndex = (index) => wrapper.findAll('.js-case-row').at(index);
+ const noCasesMessage = () => wrapper.findByTestId('no-test-cases');
+ const artifactsExpiredMessage = () => wrapper.findByTestId('artifacts-expired');
+ const allCaseRows = () => wrapper.findAllByTestId('test-case-row');
+ const findCaseRowAtIndex = (index) => wrapper.findAllByTestId('test-case-row').at(index);
const findLinkForRow = (row) => row.find(GlLink);
const findIconForRow = (row, status) => row.find(`.ci-status-icon-${status}`);
- const createComponent = (suite = testSuite, perPage = 20) => {
+ const createComponent = ({ suite = testSuite, perPage = 20, errorMessage } = {}) => {
store = new Vuex.Store({
state: {
blobPath,
@@ -41,11 +43,12 @@ describe('Test reports suite table', () => {
page: 1,
perPage,
},
+ errorMessage,
},
getters,
});
- wrapper = shallowMount(SuiteTable, {
+ wrapper = shallowMountExtended(SuiteTable, {
store,
stubs: { GlFriendlyWrap },
});
@@ -55,12 +58,18 @@ describe('Test reports suite table', () => {
wrapper.destroy();
});
- describe('should not render', () => {
- beforeEach(() => createComponent([]));
+ it('should render a message when there are no test cases', () => {
+ createComponent({ suite: [] });
- it('a table when there are no test cases', () => {
- expect(noCasesMessage().exists()).toBe(true);
- });
+ expect(noCasesMessage().exists()).toBe(true);
+ expect(artifactsExpiredMessage().exists()).toBe(false);
+ });
+
+ it('should render a message when artifacts have expired', () => {
+ createComponent({ suite: [], errorMessage: ARTIFACTS_EXPIRED_ERROR_MESSAGE });
+
+ expect(noCasesMessage().exists()).toBe(true);
+ expect(artifactsExpiredMessage().exists()).toBe(true);
});
describe('when a test suite is supplied', () => {
@@ -102,7 +111,7 @@ describe('Test reports suite table', () => {
const perPage = 2;
beforeEach(() => {
- createComponent(testSuite, perPage);
+ createComponent({ testSuite, perPage });
});
it('renders one page of test cases', () => {
@@ -117,11 +126,13 @@ describe('Test reports suite table', () => {
describe('when a test case classname property is null', () => {
it('still renders all test cases', () => {
createComponent({
- ...testSuite,
- test_cases: testSuite.test_cases.map((testCase) => ({
- ...testCase,
- classname: null,
- })),
+ testSuite: {
+ ...testSuite,
+ test_cases: testSuite.test_cases.map((testCase) => ({
+ ...testCase,
+ classname: null,
+ })),
+ },
});
expect(allCaseRows()).toHaveLength(testCases.length);
@@ -131,11 +142,13 @@ describe('Test reports suite table', () => {
describe('when a test case name property is null', () => {
it('still renders all test cases', () => {
createComponent({
- ...testSuite,
- test_cases: testSuite.test_cases.map((testCase) => ({
- ...testCase,
- name: null,
- })),
+ testSuite: {
+ ...testSuite,
+ test_cases: testSuite.test_cases.map((testCase) => ({
+ ...testCase,
+ name: null,
+ })),
+ },
});
expect(allCaseRows()).toHaveLength(testCases.length);
diff --git a/spec/frontend/pipelines/tokens/pipeline_branch_name_token_spec.js b/spec/frontend/pipelines/tokens/pipeline_branch_name_token_spec.js
index 42ae154fb5e..ba478363d04 100644
--- a/spec/frontend/pipelines/tokens/pipeline_branch_name_token_spec.js
+++ b/spec/frontend/pipelines/tokens/pipeline_branch_name_token_spec.js
@@ -34,6 +34,7 @@ describe('Pipeline Branch Name Token', () => {
value: {
data: '',
},
+ cursorPosition: 'start',
};
const optionsWithDefaultBranchName = (options) => {
diff --git a/spec/frontend/pipelines/tokens/pipeline_source_token_spec.js b/spec/frontend/pipelines/tokens/pipeline_source_token_spec.js
index 684d2d0664a..b8abf2c1727 100644
--- a/spec/frontend/pipelines/tokens/pipeline_source_token_spec.js
+++ b/spec/frontend/pipelines/tokens/pipeline_source_token_spec.js
@@ -20,6 +20,7 @@ describe('Pipeline Source Token', () => {
value: {
data: '',
},
+ cursorPosition: 'start',
};
const createComponent = () => {
diff --git a/spec/frontend/pipelines/tokens/pipeline_status_token_spec.js b/spec/frontend/pipelines/tokens/pipeline_status_token_spec.js
index 1db736ba01e..2c5fa8b00e2 100644
--- a/spec/frontend/pipelines/tokens/pipeline_status_token_spec.js
+++ b/spec/frontend/pipelines/tokens/pipeline_status_token_spec.js
@@ -20,6 +20,7 @@ describe('Pipeline Status Token', () => {
value: {
data: '',
},
+ cursorPosition: 'start',
};
const createComponent = () => {
diff --git a/spec/frontend/pipelines/tokens/pipeline_tag_name_token_spec.js b/spec/frontend/pipelines/tokens/pipeline_tag_name_token_spec.js
index b03dbb73b95..596a9218c39 100644
--- a/spec/frontend/pipelines/tokens/pipeline_tag_name_token_spec.js
+++ b/spec/frontend/pipelines/tokens/pipeline_tag_name_token_spec.js
@@ -29,6 +29,7 @@ describe('Pipeline Branch Name Token', () => {
value: {
data: '',
},
+ cursorPosition: 'start',
};
const createComponent = (options, data) => {
diff --git a/spec/frontend/pipelines/tokens/pipeline_trigger_author_token_spec.js b/spec/frontend/pipelines/tokens/pipeline_trigger_author_token_spec.js
index 7ddbbb3b005..397dbdf95a9 100644
--- a/spec/frontend/pipelines/tokens/pipeline_trigger_author_token_spec.js
+++ b/spec/frontend/pipelines/tokens/pipeline_trigger_author_token_spec.js
@@ -24,6 +24,7 @@ describe('Pipeline Trigger Author Token', () => {
value: {
data: '',
},
+ cursorPosition: 'start',
};
const createComponent = (data) => {
diff --git a/spec/frontend/project_select_combo_button_spec.js b/spec/frontend/project_select_combo_button_spec.js
index 40e7d27edc8..b8d5a1a61f3 100644
--- a/spec/frontend/project_select_combo_button_spec.js
+++ b/spec/frontend/project_select_combo_button_spec.js
@@ -1,4 +1,5 @@
import $ from 'jquery';
+import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import ProjectSelectComboButton from '~/project_select_combo_button';
const fixturePath = 'static/project_select_combo_button.html';
@@ -22,16 +23,25 @@ describe('Project Select Combo Button', () => {
name: 'My Other Cool Project',
url: 'http://myothercoolproject.com',
},
+ vulnerableProject: {
+ name: 'Self XSS',
+ // eslint-disable-next-line no-script-url
+ url: 'javascript:alert(1)',
+ },
localStorageKey: 'group-12345-new-issue-recent-project',
relativePath: 'issues/new',
};
- loadFixtures(fixturePath);
+ loadHTMLFixture(fixturePath);
testContext.newItemBtn = document.querySelector('.js-new-project-item-link');
testContext.projectSelectInput = document.querySelector('.project-item-select');
});
+ afterEach(() => {
+ resetHTMLFixture();
+ });
+
describe('on page load when localStorage is empty', () => {
beforeEach(() => {
testContext.comboButton = new ProjectSelectComboButton(testContext.projectSelectInput);
@@ -99,6 +109,25 @@ describe('Project Select Combo Button', () => {
});
});
+ describe('after selecting a vulnerable project', () => {
+ beforeEach(() => {
+ testContext.comboButton = new ProjectSelectComboButton(testContext.projectSelectInput);
+
+ // mock the effect of selecting an item from the projects dropdown (select2)
+ $('.project-item-select')
+ .val(JSON.stringify(testContext.defaults.vulnerableProject))
+ .trigger('change');
+ });
+
+ it('newItemBtn href is correctly sanitized', () => {
+ expect(testContext.newItemBtn.getAttribute('href')).toBe('about:blank');
+ });
+
+ afterEach(() => {
+ window.localStorage.clear();
+ });
+ });
+
describe('deriveTextVariants', () => {
beforeEach(() => {
testContext.mockExecutionContext = {
diff --git a/spec/frontend/projects/commits/components/author_select_spec.js b/spec/frontend/projects/commits/components/author_select_spec.js
index 4e567ab030e..d11090cba8a 100644
--- a/spec/frontend/projects/commits/components/author_select_spec.js
+++ b/spec/frontend/projects/commits/components/author_select_spec.js
@@ -2,6 +2,7 @@ import { GlDropdown, GlDropdownSectionHeader, GlSearchBoxByType, GlDropdownItem
import { shallowMount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import * as urlUtility from '~/lib/utils/url_utility';
import AuthorSelect from '~/projects/commits/components/author_select.vue';
import { createStore } from '~/projects/commits/store';
@@ -30,7 +31,7 @@ describe('Author Select', () => {
let wrapper;
const createComponent = () => {
- setFixtures(`
+ setHTMLFixture(`
<div class="js-project-commits-show">
<input id="commits-search" type="text" />
<div id="commits-list"></div>
@@ -54,6 +55,7 @@ describe('Author Select', () => {
afterEach(() => {
wrapper.destroy();
+ resetHTMLFixture();
});
const findDropdownContainer = () => wrapper.find({ ref: 'dropdownContainer' });
diff --git a/spec/frontend/projects/components/__snapshots__/project_delete_button_spec.js.snap b/spec/frontend/projects/components/__snapshots__/project_delete_button_spec.js.snap
index 26a3b27d958..736d149f06d 100644
--- a/spec/frontend/projects/components/__snapshots__/project_delete_button_spec.js.snap
+++ b/spec/frontend/projects/components/__snapshots__/project_delete_button_spec.js.snap
@@ -31,6 +31,7 @@ exports[`Project remove modal initialized matches the snapshot 1`] = `
<gl-modal-stub
actioncancel="[object Object]"
actionprimary="[object Object]"
+ arialabel=""
dismisslabel="Close"
footer-class="gl-bg-gray-10 gl-p-5"
modalclass=""
@@ -49,6 +50,7 @@ exports[`Project remove modal initialized matches the snapshot 1`] = `
primarybuttontext=""
secondarybuttonlink=""
secondarybuttontext=""
+ showicon="true"
title=""
variant="danger"
>
diff --git a/spec/frontend/projects/components/shared/__snapshots__/delete_button_spec.js.snap b/spec/frontend/projects/components/shared/__snapshots__/delete_button_spec.js.snap
index 2d1039a8743..26495fbcf83 100644
--- a/spec/frontend/projects/components/shared/__snapshots__/delete_button_spec.js.snap
+++ b/spec/frontend/projects/components/shared/__snapshots__/delete_button_spec.js.snap
@@ -43,6 +43,7 @@ exports[`Project remove modal intialized matches the snapshot 1`] = `
primarybuttontext=""
secondarybuttonlink=""
secondarybuttontext=""
+ showicon="true"
title=""
variant="danger"
>
diff --git a/spec/frontend/projects/new/components/deployment_target_select_spec.js b/spec/frontend/projects/new/components/deployment_target_select_spec.js
index 1c443879dc3..f3b22d4a1b9 100644
--- a/spec/frontend/projects/new/components/deployment_target_select_spec.js
+++ b/spec/frontend/projects/new/components/deployment_target_select_spec.js
@@ -1,6 +1,7 @@
import { GlFormGroup, GlFormSelect, GlFormText, GlLink, GlSprintf } from '@gitlab/ui';
import { nextTick } from 'vue';
import { shallowMount } from '@vue/test-utils';
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import { mockTracking } from 'helpers/tracking_helper';
import DeploymentTargetSelect from '~/projects/new/components/deployment_target_select.vue';
import {
@@ -32,7 +33,7 @@ describe('Deployment target select', () => {
};
const createForm = () => {
- setFixtures(`
+ setHTMLFixture(`
<form id="${NEW_PROJECT_FORM}">
</form>
`);
@@ -47,6 +48,7 @@ describe('Deployment target select', () => {
afterEach(() => {
wrapper.destroy();
+ resetHTMLFixture();
});
it('renders the correct label', () => {
diff --git a/spec/frontend/projects/new/components/new_project_push_tip_popover_spec.js b/spec/frontend/projects/new/components/new_project_push_tip_popover_spec.js
index 31ddbc80ae4..42259a5c392 100644
--- a/spec/frontend/projects/new/components/new_project_push_tip_popover_spec.js
+++ b/spec/frontend/projects/new/components/new_project_push_tip_popover_spec.js
@@ -1,5 +1,6 @@
import { GlPopover, GlFormInputGroup } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import NewProjectPushTipPopover from '~/projects/new/components/new_project_push_tip_popover.vue';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
@@ -31,12 +32,13 @@ describe('New project push tip popover', () => {
};
beforeEach(() => {
- setFixtures(`<a id="${targetId}"></a>`);
+ setHTMLFixture(`<a id="${targetId}"></a>`);
buildWrapper();
});
afterEach(() => {
wrapper.destroy();
+ resetHTMLFixture();
});
it('renders popover that targets the specified target', () => {
diff --git a/spec/frontend/projects/pipelines/charts/components/ci_cd_analytics_charts_spec.js b/spec/frontend/projects/pipelines/charts/components/ci_cd_analytics_charts_spec.js
index cafb3f231bd..7bb289408b8 100644
--- a/spec/frontend/projects/pipelines/charts/components/ci_cd_analytics_charts_spec.js
+++ b/spec/frontend/projects/pipelines/charts/components/ci_cd_analytics_charts_spec.js
@@ -1,8 +1,8 @@
import { nextTick } from 'vue';
-import { GlSegmentedControl } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import CiCdAnalyticsAreaChart from '~/vue_shared/components/ci_cd_analytics/ci_cd_analytics_area_chart.vue';
import CiCdAnalyticsCharts from '~/vue_shared/components/ci_cd_analytics/ci_cd_analytics_charts.vue';
+import SegmentedControlButtonGroup from '~/vue_shared/components/segmented_control_button_group.vue';
import { transformedAreaChartData, chartOptions } from '../mock_data';
const DEFAULT_PROPS = {
@@ -48,7 +48,7 @@ describe('~/vue_shared/components/ci_cd_analytics/ci_cd_analytics_charts.vue', (
});
const findMetricsSlot = () => wrapper.findByTestId('metrics-slot');
- const findSegmentedControl = () => wrapper.findComponent(GlSegmentedControl);
+ const findSegmentedControl = () => wrapper.findComponent(SegmentedControlButtonGroup);
describe('segmented control', () => {
beforeEach(() => {
@@ -56,7 +56,7 @@ describe('~/vue_shared/components/ci_cd_analytics/ci_cd_analytics_charts.vue', (
});
it('should default to the first chart', () => {
- expect(findSegmentedControl().props('checked')).toBe(0);
+ expect(findSegmentedControl().props('value')).toBe(0);
});
it('should use the title and index as values', () => {
diff --git a/spec/frontend/projects/project_import_gitlab_project_spec.js b/spec/frontend/projects/project_import_gitlab_project_spec.js
index aaf8a81f626..76621ba9c06 100644
--- a/spec/frontend/projects/project_import_gitlab_project_spec.js
+++ b/spec/frontend/projects/project_import_gitlab_project_spec.js
@@ -1,3 +1,4 @@
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import projectImportGitlab from '~/projects/project_import_gitlab_project';
describe('Import Gitlab project', () => {
@@ -7,7 +8,7 @@ describe('Import Gitlab project', () => {
const setTestFixtures = (url) => {
window.history.pushState({}, null, url);
- setFixtures(`
+ setHTMLFixture(`
<input class="js-path-name" />
<input class="js-project-name" />
`);
@@ -21,6 +22,7 @@ describe('Import Gitlab project', () => {
afterEach(() => {
window.history.pushState({}, null, '');
+ resetHTMLFixture();
});
describe('project name', () => {
diff --git a/spec/frontend/projects/project_new_spec.js b/spec/frontend/projects/project_new_spec.js
index d2936cb9efe..fe325343da8 100644
--- a/spec/frontend/projects/project_new_spec.js
+++ b/spec/frontend/projects/project_new_spec.js
@@ -1,4 +1,5 @@
import $ from 'jquery';
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import { TEST_HOST } from 'helpers/test_constants';
import projectNew from '~/projects/project_new';
@@ -8,7 +9,7 @@ describe('New Project', () => {
let $projectName;
beforeEach(() => {
- setFixtures(`
+ setHTMLFixture(`
<div class='toggle-import-form'>
<div class='import-url-data'>
<div class="form-group">
@@ -33,6 +34,10 @@ describe('New Project', () => {
$projectName = $('#project_name');
});
+ afterEach(() => {
+ resetHTMLFixture();
+ });
+
describe('deriveProjectPathFromUrl', () => {
const dummyImportUrl = `${TEST_HOST}/dummy/import/url.git`;
diff --git a/spec/frontend/projects/projects_filterable_list_spec.js b/spec/frontend/projects/projects_filterable_list_spec.js
index a41e8b7bc09..f217efa411e 100644
--- a/spec/frontend/projects/projects_filterable_list_spec.js
+++ b/spec/frontend/projects/projects_filterable_list_spec.js
@@ -1,4 +1,4 @@
-import { setHTMLFixture } from 'helpers/fixtures';
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import ProjectsFilterableList from '~/projects/projects_filterable_list';
describe('ProjectsFilterableList', () => {
@@ -20,6 +20,10 @@ describe('ProjectsFilterableList', () => {
List = new ProjectsFilterableList(form, filter, holder);
});
+ afterEach(() => {
+ resetHTMLFixture();
+ });
+
describe('getFilterEndpoint', () => {
it('updates converts getPagePath for projects', () => {
jest.spyOn(List, 'getPagePath').mockReturnValue('blah/projects?');
diff --git a/spec/frontend/projects/settings/access_dropdown_spec.js b/spec/frontend/projects/settings/access_dropdown_spec.js
index 236968a3736..65b01172e7e 100644
--- a/spec/frontend/projects/settings/access_dropdown_spec.js
+++ b/spec/frontend/projects/settings/access_dropdown_spec.js
@@ -1,4 +1,5 @@
import $ from 'jquery';
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import AccessDropdown from '~/projects/settings/access_dropdown';
import { LEVEL_TYPES } from '~/projects/settings/constants';
@@ -7,7 +8,7 @@ describe('AccessDropdown', () => {
let dropdown;
beforeEach(() => {
- setFixtures(`
+ setHTMLFixture(`
<div id="dummy-dropdown">
<span class="dropdown-toggle-text"></span>
</div>
@@ -28,6 +29,10 @@ describe('AccessDropdown', () => {
dropdown = new AccessDropdown(options);
});
+ afterEach(() => {
+ resetHTMLFixture();
+ });
+
describe('toggleLabel', () => {
let $dropdownToggleText;
const dummyItems = [
diff --git a/spec/frontend/projects/settings/topics/components/topics_token_selector_spec.js b/spec/frontend/projects/settings/topics/components/topics_token_selector_spec.js
index dbea94cbd53..8b8e7d1454d 100644
--- a/spec/frontend/projects/settings/topics/components/topics_token_selector_spec.js
+++ b/spec/frontend/projects/settings/topics/components/topics_token_selector_spec.js
@@ -1,11 +1,11 @@
-import { GlTokenSelector, GlToken } from '@gitlab/ui';
+import { GlAvatarLabeled, GlTokenSelector, GlToken } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import { nextTick } from 'vue';
import TopicsTokenSelector from '~/projects/settings/topics/components/topics_token_selector.vue';
const mockTopics = [
- { id: 1, name: 'topic1', avatarUrl: 'avatar.com/topic1.png' },
- { id: 2, name: 'GitLab', avatarUrl: 'avatar.com/GitLab.png' },
+ { id: 1, name: 'topic1', title: 'Topic 1', avatarUrl: 'avatar.com/topic1.png' },
+ { id: 2, name: 'GitLab', title: 'GitLab', avatarUrl: 'avatar.com/GitLab.png' },
];
describe('TopicsTokenSelector', () => {
@@ -38,6 +38,8 @@ describe('TopicsTokenSelector', () => {
const findTokenSelectorInput = () => findTokenSelector().find('input[type="text"]');
+ const findAllAvatars = () => wrapper.findAllComponents(GlAvatarLabeled).wrappers;
+
const setTokenSelectorInputValue = (value) => {
const tokenSelectorInput = findTokenSelectorInput();
@@ -81,6 +83,13 @@ describe('TopicsTokenSelector', () => {
expect(tokenWrapper.text()).toBe(selected[index].name);
});
});
+
+ it('passes topic title to the avatar', async () => {
+ createComponent();
+ const avatars = findAllAvatars();
+
+ mockTopics.map((topic, index) => expect(avatars[index].text()).toBe(topic.title));
+ });
});
describe('when enter key is pressed', () => {
diff --git a/spec/frontend/projects/settings_service_desk/components/service_desk_setting_spec.js b/spec/frontend/projects/settings_service_desk/components/service_desk_setting_spec.js
index 57e515723e5..aac1a418142 100644
--- a/spec/frontend/projects/settings_service_desk/components/service_desk_setting_spec.js
+++ b/spec/frontend/projects/settings_service_desk/components/service_desk_setting_spec.js
@@ -165,8 +165,12 @@ describe('ServiceDeskSetting', () => {
describe('save button', () => {
it('renders a save button to save a template', () => {
wrapper = createComponent();
+ const saveButton = findButton();
- expect(findButton().text()).toContain('Save changes');
+ expect(saveButton.text()).toContain('Save changes');
+ expect(saveButton.props()).toMatchObject({
+ variant: 'confirm',
+ });
});
it('emits a save event with the chosen template when the save button is clicked', async () => {
diff --git a/spec/frontend/prometheus_alerts/components/reset_key_spec.js b/spec/frontend/prometheus_alerts/components/reset_key_spec.js
deleted file mode 100644
index dc5fdb1dffc..00000000000
--- a/spec/frontend/prometheus_alerts/components/reset_key_spec.js
+++ /dev/null
@@ -1,99 +0,0 @@
-import { GlModal } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
-import MockAdapter from 'axios-mock-adapter';
-import { nextTick } from 'vue';
-import waitForPromises from 'helpers/wait_for_promises';
-import axios from '~/lib/utils/axios_utils';
-import ResetKey from '~/prometheus_alerts/components/reset_key.vue';
-import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
-
-describe('ResetKey', () => {
- let mock;
- let vm;
-
- const propsData = {
- initialAuthorizationKey: 'abcd1234',
- changeKeyUrl: '/updateKeyUrl',
- notifyUrl: '/root/autodevops-deploy/prometheus/alerts/notify.json',
- learnMoreUrl: '/learnMore',
- };
-
- beforeEach(() => {
- mock = new MockAdapter(axios);
- setFixtures('<div class="flash-container"></div><div id="reset-key"></div>');
- });
-
- afterEach(() => {
- mock.restore();
- vm.destroy();
- });
-
- describe('authorization key exists', () => {
- beforeEach(() => {
- propsData.initialAuthorizationKey = 'abcd1234';
- vm = shallowMount(ResetKey, {
- propsData,
- });
- });
-
- it('shows fields and buttons', () => {
- expect(vm.find('#notify-url').attributes('value')).toEqual(propsData.notifyUrl);
- expect(vm.find('#authorization-key').attributes('value')).toEqual(
- propsData.initialAuthorizationKey,
- );
-
- expect(vm.findAll(ClipboardButton).length).toBe(2);
- expect(vm.find('.js-reset-auth-key').text()).toEqual('Reset key');
- });
-
- it('reset updates key', async () => {
- mock.onPost(propsData.changeKeyUrl).replyOnce(200, { token: 'newToken' });
-
- vm.find(GlModal).vm.$emit('ok');
-
- await nextTick();
- await waitForPromises();
- expect(vm.vm.authorizationKey).toEqual('newToken');
- expect(vm.find('#authorization-key').attributes('value')).toEqual('newToken');
- });
-
- it('reset key failure shows error', async () => {
- mock.onPost(propsData.changeKeyUrl).replyOnce(500);
-
- vm.find(GlModal).vm.$emit('ok');
-
- await nextTick();
- await waitForPromises();
- expect(vm.find('#authorization-key').attributes('value')).toEqual(
- propsData.initialAuthorizationKey,
- );
-
- expect(document.querySelector('.flash-container').innerText.trim()).toEqual(
- 'Failed to reset key. Please try again.',
- );
- });
- });
-
- describe('authorization key has not been set', () => {
- beforeEach(() => {
- propsData.initialAuthorizationKey = '';
- vm = shallowMount(ResetKey, {
- propsData,
- });
- });
-
- it('shows Generate Key button', () => {
- expect(vm.find('.js-reset-auth-key').text()).toEqual('Generate key');
- expect(vm.find('#authorization-key').attributes('value')).toEqual('');
- });
-
- it('Generate key button triggers key change', async () => {
- mock.onPost(propsData.changeKeyUrl).replyOnce(200, { token: 'newToken' });
-
- vm.find('.js-reset-auth-key').vm.$emit('click');
-
- await waitForPromises();
- expect(vm.find('#authorization-key').attributes('value')).toEqual('newToken');
- });
- });
-});
diff --git a/spec/frontend/prometheus_metrics/custom_metrics_spec.js b/spec/frontend/prometheus_metrics/custom_metrics_spec.js
index 20593351ee5..473327bf5e1 100644
--- a/spec/frontend/prometheus_metrics/custom_metrics_spec.js
+++ b/spec/frontend/prometheus_metrics/custom_metrics_spec.js
@@ -1,4 +1,5 @@
import MockAdapter from 'axios-mock-adapter';
+import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import axios from '~/lib/utils/axios_utils';
import PANEL_STATE from '~/prometheus_metrics/constants';
import CustomMetrics from '~/prometheus_metrics/custom_metrics';
@@ -15,11 +16,12 @@ describe('PrometheusMetrics', () => {
mock.onGet(customMetricsEndpoint).reply(200, {
metrics,
});
- loadFixtures(FIXTURE);
+ loadHTMLFixture(FIXTURE);
});
afterEach(() => {
mock.restore();
+ resetHTMLFixture();
});
describe('Custom Metrics', () => {
diff --git a/spec/frontend/prometheus_metrics/prometheus_metrics_spec.js b/spec/frontend/prometheus_metrics/prometheus_metrics_spec.js
index ee74e28ba23..1151c0b3769 100644
--- a/spec/frontend/prometheus_metrics/prometheus_metrics_spec.js
+++ b/spec/frontend/prometheus_metrics/prometheus_metrics_spec.js
@@ -1,4 +1,5 @@
import MockAdapter from 'axios-mock-adapter';
+import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import waitForPromises from 'helpers/wait_for_promises';
import axios from '~/lib/utils/axios_utils';
import PANEL_STATE from '~/prometheus_metrics/constants';
@@ -9,7 +10,7 @@ describe('PrometheusMetrics', () => {
const FIXTURE = 'services/prometheus/prometheus_service.html';
beforeEach(() => {
- loadFixtures(FIXTURE);
+ loadHTMLFixture(FIXTURE);
});
describe('constructor', () => {
@@ -19,6 +20,10 @@ describe('PrometheusMetrics', () => {
prometheusMetrics = new PrometheusMetrics('.js-prometheus-metrics-monitoring');
});
+ afterEach(() => {
+ resetHTMLFixture();
+ });
+
it('should initialize wrapper element refs on class object', () => {
expect(prometheusMetrics.$wrapper).toBeDefined();
expect(prometheusMetrics.$monitoredMetricsPanel).toBeDefined();
diff --git a/spec/frontend/protected_branches/protected_branch_create_spec.js b/spec/frontend/protected_branches/protected_branch_create_spec.js
index b3de2d5e031..4b634c52b01 100644
--- a/spec/frontend/protected_branches/protected_branch_create_spec.js
+++ b/spec/frontend/protected_branches/protected_branch_create_spec.js
@@ -1,3 +1,4 @@
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import ProtectedBranchCreate from '~/protected_branches/protected_branch_create';
const FORCE_PUSH_TOGGLE_TESTID = 'force-push-toggle';
@@ -21,7 +22,7 @@ describe('ProtectedBranchCreate', () => {
codeOwnerToggleChecked = false,
hasLicense = true,
} = {}) => {
- setFixtures(`
+ setHTMLFixture(`
<form class="js-new-protected-branch">
<span
class="js-force-push-toggle"
@@ -40,6 +41,10 @@ describe('ProtectedBranchCreate', () => {
return new ProtectedBranchCreate({ hasLicense });
};
+ afterEach(() => {
+ resetHTMLFixture();
+ });
+
describe('when license supports code owner approvals', () => {
it('instantiates the code owner toggle', () => {
create();
diff --git a/spec/frontend/protected_branches/protected_branch_edit_spec.js b/spec/frontend/protected_branches/protected_branch_edit_spec.js
index 959ca6ecde2..d842e00d850 100644
--- a/spec/frontend/protected_branches/protected_branch_edit_spec.js
+++ b/spec/frontend/protected_branches/protected_branch_edit_spec.js
@@ -1,5 +1,6 @@
import MockAdapter from 'axios-mock-adapter';
import $ from 'jquery';
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import { TEST_HOST } from 'helpers/test_constants';
import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
@@ -33,7 +34,7 @@ describe('ProtectedBranchEdit', () => {
codeOwnerToggleChecked = false,
hasLicense = true,
} = {}) => {
- setFixtures(`<div id="wrap" data-url="${TEST_URL}">
+ setHTMLFixture(`<div id="wrap" data-url="${TEST_URL}">
<span
class="js-force-push-toggle"
data-label="Toggle allowed to force push"
@@ -51,6 +52,7 @@ describe('ProtectedBranchEdit', () => {
afterEach(() => {
mock.restore();
+ resetHTMLFixture();
});
describe('when license supports code owner approvals', () => {
@@ -76,7 +78,11 @@ describe('ProtectedBranchEdit', () => {
describe('when toggles are not available in the DOM on page load', () => {
beforeEach(() => {
create({ hasLicense: true });
- setFixtures('');
+ setHTMLFixture('');
+ });
+
+ afterEach(() => {
+ resetHTMLFixture();
});
it('does not instantiate the force push toggle', () => {
diff --git a/spec/frontend/read_more_spec.js b/spec/frontend/read_more_spec.js
index 16f0d7fb075..80d7c941660 100644
--- a/spec/frontend/read_more_spec.js
+++ b/spec/frontend/read_more_spec.js
@@ -1,10 +1,15 @@
+import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import initReadMore from '~/read_more';
describe('Read more click-to-expand functionality', () => {
const fixtureName = 'projects/overview.html';
beforeEach(() => {
- loadFixtures(fixtureName);
+ loadHTMLFixture(fixtureName);
+ });
+
+ afterEach(() => {
+ resetHTMLFixture();
});
describe('expands target element', () => {
diff --git a/spec/frontend/releases/components/app_edit_new_spec.js b/spec/frontend/releases/components/app_edit_new_spec.js
index 0a0a683b56d..80be27c92ff 100644
--- a/spec/frontend/releases/components/app_edit_new_spec.js
+++ b/spec/frontend/releases/components/app_edit_new_spec.js
@@ -4,6 +4,7 @@ import MockAdapter from 'axios-mock-adapter';
import { merge } from 'lodash';
import Vuex from 'vuex';
import { nextTick } from 'vue';
+import { GlFormCheckbox } from '@gitlab/ui';
import originalRelease from 'test_fixtures/api/releases/release.json';
import setWindowLocation from 'helpers/set_window_location_helper';
import { TEST_HOST } from 'helpers/test_constants';
@@ -11,6 +12,7 @@ import * as commonUtils from '~/lib/utils/common_utils';
import ReleaseEditNewApp from '~/releases/components/app_edit_new.vue';
import AssetLinksForm from '~/releases/components/asset_links_form.vue';
import { BACK_URL_PARAM } from '~/releases/constants';
+import MarkdownField from '~/vue_shared/components/markdown/field.vue';
const originalMilestones = originalRelease.milestones;
const releasesPagePath = 'path/to/releases/page';
@@ -47,6 +49,7 @@ describe('Release edit/new component', () => {
links: [],
},
}),
+ formattedReleaseNotes: () => 'these notes are formatted',
};
const store = new Vuex.Store(
@@ -129,6 +132,11 @@ describe('Release edit/new component', () => {
expect(wrapper.find('#release-notes').element.value).toBe(release.description);
});
+ it('sets the preview text to be the formatted release notes', () => {
+ const notes = getters.formattedReleaseNotes();
+ expect(wrapper.findComponent(MarkdownField).props('textareaValue')).toBe(notes);
+ });
+
it('renders the "Save changes" button as type="submit"', () => {
expect(findSubmitButton().attributes('type')).toBe('submit');
});
@@ -195,6 +203,10 @@ describe('Release edit/new component', () => {
it('renders the submit button with the text "Create release"', () => {
expect(findSubmitButton().text()).toBe('Create release');
});
+
+ it('renders a checkbox to include release notes', () => {
+ expect(wrapper.find(GlFormCheckbox).exists()).toBe(true);
+ });
});
describe('when editing an existing release', () => {
diff --git a/spec/frontend/releases/components/tag_field_new_spec.js b/spec/frontend/releases/components/tag_field_new_spec.js
index c13b513f87e..9f500c318ea 100644
--- a/spec/frontend/releases/components/tag_field_new_spec.js
+++ b/spec/frontend/releases/components/tag_field_new_spec.js
@@ -1,5 +1,7 @@
import { GlDropdownItem } from '@gitlab/ui';
import { mount, shallowMount } from '@vue/test-utils';
+import axios from 'axios';
+import MockAdapter from 'axios-mock-adapter';
import Vue, { nextTick } from 'vue';
import { __ } from '~/locale';
import TagFieldNew from '~/releases/components/tag_field_new.vue';
@@ -14,6 +16,7 @@ const NONEXISTENT_TAG_NAME = 'nonexistent-tag';
describe('releases/components/tag_field_new', () => {
let store;
let wrapper;
+ let mock;
let RefSelectorStub;
const createComponent = (
@@ -65,11 +68,14 @@ describe('releases/components/tag_field_new', () => {
links: [],
},
};
+
+ mock = new MockAdapter(axios);
+ gon.api_version = 'v4';
});
afterEach(() => {
wrapper.destroy();
- wrapper = null;
+ mock.restore();
});
const findTagNameFormGroup = () => wrapper.find('[data-testid="tag-name-field"]');
@@ -114,9 +120,14 @@ describe('releases/components/tag_field_new', () => {
expect(store.state.editNew.release.tagName).toBe(updatedTagName);
});
- it('shows the "Create from" field', () => {
+ it('hides the "Create from" field', () => {
expect(findCreateFromFormGroup().exists()).toBe(false);
});
+
+ it('fetches the release notes for the tag', () => {
+ const expectedUrl = `/api/v4/projects/1234/repository/tags/${updatedTagName}`;
+ expect(mock.history.get).toContainEqual(expect.objectContaining({ url: expectedUrl }));
+ });
});
});
@@ -177,6 +188,18 @@ describe('releases/components/tag_field_new', () => {
await expectValidationMessageToBe('hidden');
});
+
+ it('displays a validation error if the tag has an associated release', async () => {
+ findTagNameDropdown().vm.$emit('input', 'vTest');
+ findTagNameDropdown().vm.$emit('hide');
+
+ store.state.editNew.existingRelease = {};
+
+ await expectValidationMessageToBe('shown');
+ expect(findTagNameFormGroup().text()).toContain(
+ __('Selected tag is already in use. Choose another option.'),
+ );
+ });
});
describe('when the user has interacted with the component and the value is empty', () => {
@@ -185,6 +208,7 @@ describe('releases/components/tag_field_new', () => {
findTagNameDropdown().vm.$emit('hide');
await expectValidationMessageToBe('shown');
+ expect(findTagNameFormGroup().text()).toContain(__('Tag name is required.'));
});
});
});
diff --git a/spec/frontend/releases/stores/modules/detail/actions_spec.js b/spec/frontend/releases/stores/modules/detail/actions_spec.js
index d8329fb82b1..41653f62ebf 100644
--- a/spec/frontend/releases/stores/modules/detail/actions_spec.js
+++ b/spec/frontend/releases/stores/modules/detail/actions_spec.js
@@ -1,8 +1,10 @@
import { cloneDeep } from 'lodash';
import originalOneReleaseForEditingQueryResponse from 'test_fixtures/graphql/releases/graphql/queries/one_release_for_editing.query.graphql.json';
import testAction from 'helpers/vuex_action_helper';
+import { getTag } from '~/api/tags_api';
import createFlash from '~/flash';
import { redirectTo } from '~/lib/utils/url_utility';
+import { s__ } from '~/locale';
import { ASSET_LINK_TYPE } from '~/releases/constants';
import createReleaseAssetLinkMutation from '~/releases/graphql/mutations/create_release_link.mutation.graphql';
import deleteReleaseAssetLinkMutation from '~/releases/graphql/mutations/delete_release_link.mutation.graphql';
@@ -12,6 +14,8 @@ import * as types from '~/releases/stores/modules/edit_new/mutation_types';
import createState from '~/releases/stores/modules/edit_new/state';
import { gqClient, convertOneReleaseGraphQLResponse } from '~/releases/util';
+jest.mock('~/api/tags_api');
+
jest.mock('~/flash');
jest.mock('~/lib/utils/url_utility', () => ({
@@ -567,4 +571,46 @@ describe('Release edit/new actions', () => {
});
});
});
+
+ describe('fetchTagNotes', () => {
+ const tagName = 'v8.0.0';
+
+ it('saves the tag notes on succes', async () => {
+ const tag = { message: 'this is a tag' };
+ getTag.mockResolvedValue({ data: tag });
+
+ await testAction(
+ actions.fetchTagNotes,
+ tagName,
+ state,
+ [
+ { type: types.REQUEST_TAG_NOTES },
+ { type: types.RECEIVE_TAG_NOTES_SUCCESS, payload: tag },
+ ],
+ [],
+ );
+
+ expect(getTag).toHaveBeenCalledWith(state.projectId, tagName);
+ });
+ it('creates a flash on error', async () => {
+ error = new Error();
+ getTag.mockRejectedValue(error);
+
+ await testAction(
+ actions.fetchTagNotes,
+ tagName,
+ state,
+ [
+ { type: types.REQUEST_TAG_NOTES },
+ { type: types.RECEIVE_TAG_NOTES_ERROR, payload: error },
+ ],
+ [],
+ );
+
+ expect(createFlash).toHaveBeenCalledWith({
+ message: s__('Release|Unable to fetch the tag notes.'),
+ });
+ expect(getTag).toHaveBeenCalledWith(state.projectId, tagName);
+ });
+ });
});
diff --git a/spec/frontend/releases/stores/modules/detail/getters_spec.js b/spec/frontend/releases/stores/modules/detail/getters_spec.js
index c32969c131e..c42c6c00f56 100644
--- a/spec/frontend/releases/stores/modules/detail/getters_spec.js
+++ b/spec/frontend/releases/stores/modules/detail/getters_spec.js
@@ -1,3 +1,4 @@
+import { s__ } from '~/locale';
import * as getters from '~/releases/stores/modules/edit_new/getters';
describe('Release edit/new getters', () => {
@@ -145,6 +146,8 @@ describe('Release edit/new getters', () => {
],
},
},
+ // tag has an existing release
+ existingRelease: {},
};
actualErrors = getters.validationErrors(state);
@@ -158,6 +161,14 @@ describe('Release edit/new getters', () => {
expect(actualErrors).toMatchObject(expectedErrors);
});
+ it('returns a validation error if the tag has an existing release', () => {
+ const expectedErrors = {
+ existingRelease: true,
+ };
+
+ expect(actualErrors).toMatchObject(expectedErrors);
+ });
+
it('returns a validation error if links share a URL', () => {
const expectedErrors = {
assets: {
@@ -369,4 +380,25 @@ describe('Release edit/new getters', () => {
expect(actualVariables).toEqual(expectedVariables);
});
});
+
+ describe('formattedReleaseNotes', () => {
+ it.each`
+ description | includeTagNotes | tagNotes | included
+ ${'release notes'} | ${true} | ${'tag notes'} | ${true}
+ ${'release notes'} | ${true} | ${''} | ${false}
+ ${'release notes'} | ${false} | ${'tag notes'} | ${false}
+ `(
+ 'should include tag notes=$included when includeTagNotes=$includeTagNotes and tagNotes=$tagNotes',
+ ({ description, includeTagNotes, tagNotes, included }) => {
+ const state = { release: { description }, includeTagNotes, tagNotes };
+
+ const text = `### ${s__('Releases|Tag message')}\n\n${tagNotes}\n`;
+ if (included) {
+ expect(getters.formattedReleaseNotes(state)).toContain(text);
+ } else {
+ expect(getters.formattedReleaseNotes(state)).not.toContain(text);
+ }
+ },
+ );
+ });
});
diff --git a/spec/frontend/releases/stores/modules/detail/mutations_spec.js b/spec/frontend/releases/stores/modules/detail/mutations_spec.js
index 24dcedb3580..85844831e0b 100644
--- a/spec/frontend/releases/stores/modules/detail/mutations_spec.js
+++ b/spec/frontend/releases/stores/modules/detail/mutations_spec.js
@@ -237,4 +237,41 @@ describe('Release edit/new mutations', () => {
expect(state.release.assets.links).not.toContainEqual(linkToRemove);
});
});
+ describe(`${types.REQUEST_TAG_NOTES}`, () => {
+ it('sets isFetchingTagNotes to true', () => {
+ state.isFetchingTagNotes = false;
+ mutations[types.REQUEST_TAG_NOTES](state);
+ expect(state.isFetchingTagNotes).toBe(true);
+ });
+ });
+ describe(`${types.RECEIVE_TAG_NOTES_SUCCESS}`, () => {
+ it('sets the tag notes in the state', () => {
+ state.isFetchingTagNotes = true;
+ const message = 'tag notes';
+
+ mutations[types.RECEIVE_TAG_NOTES_SUCCESS](state, { message, release });
+ expect(state.tagNotes).toBe(message);
+ expect(state.isFetchingTagNotes).toBe(false);
+ expect(state.existingRelease).toBe(release);
+ });
+ });
+ describe(`${types.RECEIVE_TAG_NOTES_ERROR}`, () => {
+ it('sets tag notes to empty', () => {
+ const message = 'there was an error';
+ state.isFetchingTagNotes = true;
+ state.tagNotes = 'tag notes';
+
+ mutations[types.RECEIVE_TAG_NOTES_ERROR](state, { message });
+ expect(state.tagNotes).toBe('');
+ expect(state.isFetchingTagNotes).toBe(false);
+ });
+ });
+ describe(`${types.UPDATE_INCLUDE_TAG_NOTES}`, () => {
+ it('sets whether or not to include the tag notes', () => {
+ state.includeTagNotes = false;
+
+ mutations[types.UPDATE_INCLUDE_TAG_NOTES](state, true);
+ expect(state.includeTagNotes).toBe(true);
+ });
+ });
});
diff --git a/spec/frontend/reports/codequality_report/store/getters_spec.js b/spec/frontend/reports/codequality_report/store/getters_spec.js
index b5f6edf85eb..646903390ff 100644
--- a/spec/frontend/reports/codequality_report/store/getters_spec.js
+++ b/spec/frontend/reports/codequality_report/store/getters_spec.js
@@ -61,8 +61,8 @@ describe('Codequality reports store getters', () => {
it.each`
resolvedIssues | newIssues | expectedText
${0} | ${0} | ${'No changes to code quality'}
- ${0} | ${1} | ${'Code quality degraded'}
- ${2} | ${0} | ${'Code quality improved'}
+ ${0} | ${1} | ${'Code quality degraded due to 1 new issue'}
+ ${2} | ${0} | ${'Code quality improved due to 2 resolved issues'}
${1} | ${2} | ${'Code quality scanning detected 3 changes in merged results'}
`(
'returns a summary containing $resolvedIssues resolved issues and $newIssues new issues',
diff --git a/spec/frontend/reports/components/report_link_spec.js b/spec/frontend/reports/components/report_link_spec.js
index fc21515ded6..2ed0617a598 100644
--- a/spec/frontend/reports/components/report_link_spec.js
+++ b/spec/frontend/reports/components/report_link_spec.js
@@ -1,69 +1,56 @@
-import Vue from 'vue';
-import mountComponent from 'helpers/vue_mount_component_helper';
-import component from '~/reports/components/report_link.vue';
+import { shallowMount } from '@vue/test-utils';
+import ReportLink from '~/reports/components/report_link.vue';
-describe('report link', () => {
- let vm;
-
- const Component = Vue.extend(component);
+describe('app/assets/javascripts/reports/components/report_link.vue', () => {
+ let wrapper;
afterEach(() => {
- vm.$destroy();
+ wrapper.destroy();
});
- describe('With url', () => {
- it('renders link', () => {
- vm = mountComponent(Component, {
- issue: {
- path: 'Gemfile.lock',
- urlPath: '/Gemfile.lock',
- },
- });
+ const defaultProps = {
+ issue: {},
+ };
+
+ const createComponent = (props = {}) => {
+ wrapper = shallowMount(ReportLink, {
+ propsData: { ...defaultProps, ...props },
+ });
+ };
+
+ describe('When an issue prop has a $urlPath property', () => {
+ it('render a link that will take the user to the $urlPath', () => {
+ createComponent({ issue: { path: 'Gemfile.lock', urlPath: '/Gemfile.lock' } });
- expect(vm.$el.textContent.trim()).toContain('in');
- expect(vm.$el.querySelector('a').getAttribute('href')).toEqual('/Gemfile.lock');
- expect(vm.$el.querySelector('a').textContent.trim()).toEqual('Gemfile.lock');
+ expect(wrapper.text()).toContain('in');
+ expect(wrapper.find('a').attributes('href')).toBe('/Gemfile.lock');
+ expect(wrapper.find('a').text()).toContain('Gemfile.lock');
});
});
- describe('Without url', () => {
+ describe('When an issue prop has no $urlPath property', () => {
it('does not render link', () => {
- vm = mountComponent(Component, {
- issue: {
- path: 'Gemfile.lock',
- },
- });
+ createComponent({ issue: { path: 'Gemfile.lock' } });
- expect(vm.$el.querySelector('a')).toBeNull();
- expect(vm.$el.textContent.trim()).toContain('in');
- expect(vm.$el.textContent.trim()).toContain('Gemfile.lock');
+ expect(wrapper.find('a').exists()).toBe(false);
+ expect(wrapper.text()).toContain('in');
+ expect(wrapper.text()).toContain('Gemfile.lock');
});
});
- describe('with line', () => {
- it('renders line number', () => {
- vm = mountComponent(Component, {
- issue: {
- path: 'Gemfile.lock',
- urlPath: 'https://groups.google.com/forum/#!topic/rubyonrails-security/335P1DcLG00',
- line: 22,
- },
- });
+ describe('When an issue prop has a $line property', () => {
+ it('render a line number', () => {
+ createComponent({ issue: { path: 'Gemfile.lock', urlPath: '/Gemfile.lock', line: 22 } });
- expect(vm.$el.querySelector('a').textContent.trim()).toContain('Gemfile.lock:22');
+ expect(wrapper.find('a').text()).toContain('Gemfile.lock:22');
});
});
- describe('without line', () => {
- it('does not render line number', () => {
- vm = mountComponent(Component, {
- issue: {
- path: 'Gemfile.lock',
- urlPath: 'https://groups.google.com/forum/#!topic/rubyonrails-security/335P1DcLG00',
- },
- });
+ describe('When an issue prop does not have a $line property', () => {
+ it('does not render a line number', () => {
+ createComponent({ issue: { urlPath: '/Gemfile.lock' } });
- expect(vm.$el.querySelector('a').textContent.trim()).not.toContain(':22');
+ expect(wrapper.find('a').text()).not.toContain(':22');
});
});
});
diff --git a/spec/frontend/repository/components/__snapshots__/last_commit_spec.js.snap b/spec/frontend/repository/components/__snapshots__/last_commit_spec.js.snap
index fea937b905f..4732d68c8c6 100644
--- a/spec/frontend/repository/components/__snapshots__/last_commit_spec.js.snap
+++ b/spec/frontend/repository/components/__snapshots__/last_commit_spec.js.snap
@@ -2,13 +2,13 @@
exports[`Repository last commit component renders commit widget 1`] = `
<div
- class="well-segment commit gl-p-5 gl-w-full"
+ class="well-segment commit gl-p-5 gl-w-full gl-display-flex"
>
<user-avatar-link-stub
- class="avatar-cell"
+ class="gl-my-2 gl-mr-4"
imgalt=""
- imgcssclasses=""
- imgsize="40"
+ imgcssclasses="gl-mr-0!"
+ imgsize="32"
imgsrc="https://test.com"
linkhref="/test"
tooltipplacement="top"
@@ -55,7 +55,11 @@ exports[`Repository last commit component renders commit widget 1`] = `
</div>
<div
- class="commit-actions flex-row"
+ class="gl-flex-grow-1"
+ />
+
+ <div
+ class="commit-actions gl-display-flex gl-flex-align gl-align-items-center gl-flex-direction-row"
>
<!---->
diff --git a/spec/frontend/repository/components/blob_content_viewer_spec.js b/spec/frontend/repository/components/blob_content_viewer_spec.js
index 2f6de03b73d..2ab4afbffbe 100644
--- a/spec/frontend/repository/components/blob_content_viewer_spec.js
+++ b/spec/frontend/repository/components/blob_content_viewer_spec.js
@@ -26,6 +26,7 @@ import { isLoggedIn } from '~/lib/utils/common_utils';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import httpStatusCodes from '~/lib/utils/http_status';
import LineHighlighter from '~/blob/line_highlighter';
+import { LEGACY_FILE_TYPES } from '~/repository/constants';
import {
simpleViewerMock,
richViewerMock,
@@ -195,6 +196,14 @@ describe('Blob content viewer component', () => {
expect(mockAxios.history.get[0].url).toBe(legacyViewerUrl);
});
+ it.each(LEGACY_FILE_TYPES)(
+ 'loads the legacy viewer when a file type is identified as legacy',
+ async (type) => {
+ await createComponent({ blob: { ...simpleViewerMock, fileType: type, webPath: type } });
+ expect(mockAxios.history.get[0].url).toBe(`${type}?format=json&viewer=simple`);
+ },
+ );
+
it('loads the LineHighlighter', async () => {
mockAxios.onGet(legacyViewerUrl).replyOnce(httpStatusCodes.OK, 'test');
await createComponent({ blob: { ...simpleViewerMock, fileType, highlightJs } });
diff --git a/spec/frontend/repository/components/breadcrumbs_spec.js b/spec/frontend/repository/components/breadcrumbs_spec.js
index eef66045573..40b32904589 100644
--- a/spec/frontend/repository/components/breadcrumbs_spec.js
+++ b/spec/frontend/repository/components/breadcrumbs_spec.js
@@ -147,7 +147,7 @@ describe('Repository breadcrumbs component', () => {
describe('renders the new directory modal', () => {
beforeEach(() => {
- factory('/', { canEditTree: true });
+ factory('some_dir', { canEditTree: true, newDirPath: 'root/master' });
});
it('does not render the modal while loading', () => {
expect(findNewDirectoryModal().exists()).toBe(false);
@@ -161,6 +161,7 @@ describe('Repository breadcrumbs component', () => {
await nextTick();
expect(findNewDirectoryModal().exists()).toBe(true);
+ expect(findNewDirectoryModal().props('path')).toBe('root/master/some_dir');
});
});
});
diff --git a/spec/frontend/right_sidebar_spec.js b/spec/frontend/right_sidebar_spec.js
index d1f861669a0..5847842f5a6 100644
--- a/spec/frontend/right_sidebar_spec.js
+++ b/spec/frontend/right_sidebar_spec.js
@@ -1,6 +1,6 @@
import MockAdapter from 'axios-mock-adapter';
import $ from 'jquery';
-import '~/commons/bootstrap';
+import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import axios from '~/lib/utils/axios_utils';
import Sidebar from '~/right_sidebar';
@@ -30,7 +30,7 @@ describe('RightSidebar', () => {
let mock;
beforeEach(() => {
- loadFixtures(fixtureName);
+ loadHTMLFixture(fixtureName);
mock = new MockAdapter(axios);
new Sidebar(); // eslint-disable-line no-new
$aside = $('.right-sidebar');
@@ -44,6 +44,8 @@ describe('RightSidebar', () => {
afterEach(() => {
mock.restore();
+
+ resetHTMLFixture();
});
it('should expand/collapse the sidebar when arrow is clicked', () => {
diff --git a/spec/frontend/runner/admin_runner_edit/admin_runner_edit_app_spec.js b/spec/frontend/runner/admin_runner_edit/admin_runner_edit_app_spec.js
index d121c6be218..8a34cb14d8b 100644
--- a/spec/frontend/runner/admin_runner_edit/admin_runner_edit_app_spec.js
+++ b/spec/frontend/runner/admin_runner_edit/admin_runner_edit_app_spec.js
@@ -7,17 +7,20 @@ import { createAlert } from '~/flash';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import RunnerHeader from '~/runner/components/runner_header.vue';
-import runnerQuery from '~/runner/graphql/details/runner.query.graphql';
+import RunnerUpdateForm from '~/runner/components/runner_update_form.vue';
+import runnerFormQuery from '~/runner/graphql/edit/runner_form.query.graphql';
import AdminRunnerEditApp from '~//runner/admin_runner_edit/admin_runner_edit_app.vue';
import { captureException } from '~/runner/sentry_utils';
-import { runnerData } from '../mock_data';
+import { runnerFormData } from '../mock_data';
jest.mock('~/flash');
jest.mock('~/runner/sentry_utils');
-const mockRunnerGraphqlId = runnerData.data.runner.id;
+const mockRunner = runnerFormData.data.runner;
+const mockRunnerGraphqlId = mockRunner.id;
const mockRunnerId = `${getIdFromGraphQLId(mockRunnerGraphqlId)}`;
+const mockRunnerPath = `/admin/runners/${mockRunnerId}`;
Vue.use(VueApollo);
@@ -26,12 +29,14 @@ describe('AdminRunnerEditApp', () => {
let mockRunnerQuery;
const findRunnerHeader = () => wrapper.findComponent(RunnerHeader);
+ const findRunnerUpdateForm = () => wrapper.findComponent(RunnerUpdateForm);
const createComponentWithApollo = ({ props = {}, mountFn = shallowMount } = {}) => {
wrapper = mountFn(AdminRunnerEditApp, {
- apolloProvider: createMockApollo([[runnerQuery, mockRunnerQuery]]),
+ apolloProvider: createMockApollo([[runnerFormQuery, mockRunnerQuery]]),
propsData: {
runnerId: mockRunnerId,
+ runnerPath: mockRunnerPath,
...props,
},
});
@@ -40,7 +45,7 @@ describe('AdminRunnerEditApp', () => {
};
beforeEach(() => {
- mockRunnerQuery = jest.fn().mockResolvedValue(runnerData);
+ mockRunnerQuery = jest.fn().mockResolvedValue(runnerFormData);
});
afterEach(() => {
@@ -68,6 +73,26 @@ describe('AdminRunnerEditApp', () => {
expect(findRunnerHeader().text()).toContain(`shared`);
});
+ it('displays a loading runner form', () => {
+ createComponentWithApollo();
+
+ expect(findRunnerUpdateForm().props()).toMatchObject({
+ runner: null,
+ loading: true,
+ runnerPath: mockRunnerPath,
+ });
+ });
+
+ it('displays the runner form', async () => {
+ await createComponentWithApollo();
+
+ expect(findRunnerUpdateForm().props()).toMatchObject({
+ runner: mockRunner,
+ loading: false,
+ runnerPath: mockRunnerPath,
+ });
+ });
+
describe('When there is an error', () => {
beforeEach(async () => {
mockRunnerQuery = jest.fn().mockRejectedValueOnce(new Error('Error!'));
diff --git a/spec/frontend/runner/admin_runner_show/admin_runner_show_app_spec.js b/spec/frontend/runner/admin_runner_show/admin_runner_show_app_spec.js
index f994ff24c21..07259ec3538 100644
--- a/spec/frontend/runner/admin_runner_show/admin_runner_show_app_spec.js
+++ b/spec/frontend/runner/admin_runner_show/admin_runner_show_app_spec.js
@@ -3,24 +3,30 @@ import { 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 { createAlert } from '~/flash';
+import { createAlert, VARIANT_SUCCESS } from '~/flash';
+import { redirectTo } from '~/lib/utils/url_utility';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import RunnerHeader from '~/runner/components/runner_header.vue';
import RunnerPauseButton from '~/runner/components/runner_pause_button.vue';
+import RunnerDeleteButton from '~/runner/components/runner_delete_button.vue';
import RunnerEditButton from '~/runner/components/runner_edit_button.vue';
-import runnerQuery from '~/runner/graphql/details/runner.query.graphql';
+import runnerQuery from '~/runner/graphql/show/runner.query.graphql';
import AdminRunnerShowApp from '~/runner/admin_runner_show/admin_runner_show_app.vue';
import { captureException } from '~/runner/sentry_utils';
+import { saveAlertToLocalStorage } from '~/runner/local_storage_alert/save_alert_to_local_storage';
import { runnerData } from '../mock_data';
+jest.mock('~/runner/local_storage_alert/save_alert_to_local_storage');
jest.mock('~/flash');
jest.mock('~/runner/sentry_utils');
+jest.mock('~/lib/utils/url_utility');
const mockRunner = runnerData.data.runner;
const mockRunnerGraphqlId = mockRunner.id;
const mockRunnerId = `${getIdFromGraphQLId(mockRunnerGraphqlId)}`;
+const mockRunnersPath = '/admin/runners';
Vue.use(VueApollo);
@@ -29,6 +35,7 @@ describe('AdminRunnerShowApp', () => {
let mockRunnerQuery;
const findRunnerHeader = () => wrapper.findComponent(RunnerHeader);
+ const findRunnerDeleteButton = () => wrapper.findComponent(RunnerDeleteButton);
const findRunnerEditButton = () => wrapper.findComponent(RunnerEditButton);
const findRunnerPauseButton = () => wrapper.findComponent(RunnerPauseButton);
@@ -45,6 +52,7 @@ describe('AdminRunnerShowApp', () => {
apolloProvider: createMockApollo([[runnerQuery, mockRunnerQuery]]),
propsData: {
runnerId: mockRunnerId,
+ runnersPath: mockRunnersPath,
...props,
},
});
@@ -75,6 +83,7 @@ describe('AdminRunnerShowApp', () => {
it('displays the runner edit and pause buttons', async () => {
expect(findRunnerEditButton().exists()).toBe(true);
expect(findRunnerPauseButton().exists()).toBe(true);
+ expect(findRunnerDeleteButton().exists()).toBe(true);
});
it('shows basic runner details', async () => {
@@ -82,6 +91,9 @@ describe('AdminRunnerShowApp', () => {
Last contact Never contacted
Version 1.0.0
IP Address 127.0.0.1
+ Executor None
+ Architecture None
+ Platform darwin
Configuration Runs untagged jobs
Maximum job timeout None
Tags None`.replace(/\s+/g, ' ');
@@ -108,6 +120,42 @@ describe('AdminRunnerShowApp', () => {
});
});
+ describe('when runner cannot be deleted', () => {
+ beforeEach(async () => {
+ mockRunnerQueryResult({
+ userPermissions: {
+ deleteRunner: false,
+ },
+ });
+
+ await createComponent({
+ mountFn: mount,
+ });
+ });
+
+ it('does not display the runner edit and pause buttons', () => {
+ expect(findRunnerDeleteButton().exists()).toBe(false);
+ });
+ });
+
+ describe('when runner is deleted', () => {
+ beforeEach(async () => {
+ await createComponent({
+ mountFn: mount,
+ });
+ });
+
+ it('redirects to the runner list page', () => {
+ findRunnerDeleteButton().vm.$emit('deleted', { message: 'Runner deleted' });
+
+ expect(saveAlertToLocalStorage).toHaveBeenCalledWith({
+ message: 'Runner deleted',
+ variant: VARIANT_SUCCESS,
+ });
+ expect(redirectTo).toHaveBeenCalledWith(mockRunnersPath);
+ });
+ });
+
describe('when runner does not have an edit url ', () => {
beforeEach(async () => {
mockRunnerQueryResult({
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 2ef856c90ab..405813be4e3 100644
--- a/spec/frontend/runner/admin_runners/admin_runners_app_spec.js
+++ b/spec/frontend/runner/admin_runners/admin_runners_app_spec.js
@@ -35,6 +35,8 @@ import {
PARAM_KEY_STATUS,
PARAM_KEY_TAG,
STATUS_ONLINE,
+ STATUS_OFFLINE,
+ STATUS_STALE,
RUNNER_PAGE_SIZE,
} from '~/runner/constants';
import adminRunnersQuery from '~/runner/graphql/list/admin_runners.query.graphql';
@@ -52,6 +54,7 @@ import {
const mockRegistrationToken = 'MOCK_REGISTRATION_TOKEN';
const mockRunners = runnersData.data.runners.nodes;
+const mockRunnersCount = runnersCountData.data.runners.count;
jest.mock('~/flash');
jest.mock('~/runner/sentry_utils');
@@ -124,18 +127,6 @@ describe('AdminRunnersApp', () => {
wrapper.destroy();
});
- it('shows total runner counts', async () => {
- createComponent({ mountFn: mountExtended });
-
- 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;
@@ -197,6 +188,24 @@ describe('AdminRunnersApp', () => {
expect(findRegistrationDropdown().props('type')).toBe(INSTANCE_TYPE);
});
+ it('shows total runner counts', async () => {
+ expect(mockRunnersCountQuery).toHaveBeenCalledWith({
+ status: STATUS_ONLINE,
+ });
+ expect(mockRunnersCountQuery).toHaveBeenCalledWith({
+ status: STATUS_OFFLINE,
+ });
+ expect(mockRunnersCountQuery).toHaveBeenCalledWith({
+ status: STATUS_STALE,
+ });
+
+ expect(findRunnerStats().props()).toMatchObject({
+ onlineRunnersCount: mockRunnersCount,
+ offlineRunnersCount: mockRunnersCount,
+ staleRunnersCount: mockRunnersCount,
+ });
+ });
+
it('shows the runners list', () => {
expect(findRunnerList().props('runners')).toEqual(mockRunners);
});
@@ -329,13 +338,30 @@ describe('AdminRunnersApp', () => {
first: RUNNER_PAGE_SIZE,
});
});
+
+ it('fetches count results for requested status', () => {
+ expect(mockRunnersCountQuery).toHaveBeenCalledWith({
+ type: INSTANCE_TYPE,
+ status: STATUS_ONLINE,
+ tagList: ['tag1'],
+ });
+
+ expect(findRunnerStats().props()).toMatchObject({
+ onlineRunnersCount: mockRunnersCount,
+ });
+ });
});
describe('when a filter is selected by the user', () => {
beforeEach(() => {
+ mockRunnersCountQuery.mockClear();
+
findRunnerFilteredSearchBar().vm.$emit('input', {
runnerType: null,
- filters: [{ type: PARAM_KEY_STATUS, value: { data: STATUS_ONLINE, operator: '=' } }],
+ filters: [
+ { type: PARAM_KEY_STATUS, value: { data: STATUS_ONLINE, operator: '=' } },
+ { type: PARAM_KEY_TAG, value: { data: 'tag1', operator: '=' } },
+ ],
sort: CREATED_ASC,
});
});
@@ -343,17 +369,45 @@ describe('AdminRunnersApp', () => {
it('updates the browser url', () => {
expect(updateHistory).toHaveBeenLastCalledWith({
title: expect.any(String),
- url: 'http://test.host/admin/runners?status[]=ONLINE&sort=CREATED_ASC',
+ url: 'http://test.host/admin/runners?status[]=ONLINE&tag[]=tag1&sort=CREATED_ASC',
});
});
it('requests the runners with filters', () => {
expect(mockRunnersQuery).toHaveBeenLastCalledWith({
status: STATUS_ONLINE,
+ tagList: ['tag1'],
sort: CREATED_ASC,
first: RUNNER_PAGE_SIZE,
});
});
+
+ it('fetches count results for requested status', () => {
+ expect(mockRunnersCountQuery).toHaveBeenCalledWith({
+ tagList: ['tag1'],
+ status: STATUS_ONLINE,
+ });
+
+ expect(findRunnerStats().props()).toMatchObject({
+ onlineRunnersCount: mockRunnersCount,
+ });
+ });
+
+ it('skips fetching count results for status that were not in filter', () => {
+ expect(mockRunnersCountQuery).not.toHaveBeenCalledWith({
+ tagList: ['tag1'],
+ status: STATUS_OFFLINE,
+ });
+ expect(mockRunnersCountQuery).not.toHaveBeenCalledWith({
+ tagList: ['tag1'],
+ status: STATUS_STALE,
+ });
+
+ expect(findRunnerStats().props()).toMatchObject({
+ offlineRunnersCount: null,
+ staleRunnersCount: null,
+ });
+ });
});
it('when runners have not loaded, shows a loading state', () => {
diff --git a/spec/frontend/runner/components/registration/registration_dropdown_spec.js b/spec/frontend/runner/components/registration/registration_dropdown_spec.js
index 5cd93df9967..81c2788f084 100644
--- a/spec/frontend/runner/components/registration/registration_dropdown_spec.js
+++ b/spec/frontend/runner/components/registration/registration_dropdown_spec.js
@@ -35,6 +35,16 @@ describe('RegistrationDropdown', () => {
const findRegistrationTokenInput = () => wrapper.findByTestId('token-value').find('input');
const findTokenResetDropdownItem = () =>
wrapper.findComponent(RegistrationTokenResetDropdownItem);
+ const findModalContent = () =>
+ createWrapper(document.body)
+ .find('[data-testid="runner-instructions-modal"]')
+ .text()
+ .replace(/[\n\t\s]+/g, ' ');
+
+ const openModal = async () => {
+ await findRegistrationInstructionsDropdownItem().trigger('click');
+ await waitForPromises();
+ };
const createComponent = ({ props = {}, ...options } = {}, mountFn = shallowMount) => {
wrapper = extendedWrapper(
@@ -49,6 +59,25 @@ describe('RegistrationDropdown', () => {
);
};
+ const createComponentWithModal = () => {
+ Vue.use(VueApollo);
+
+ const requestHandlers = [
+ [getRunnerPlatformsQuery, jest.fn().mockResolvedValue(mockGraphqlRunnerPlatforms)],
+ [getRunnerSetupInstructionsQuery, jest.fn().mockResolvedValue(mockGraphqlInstructions)],
+ ];
+
+ createComponent(
+ {
+ // Mock load modal contents from API
+ apolloProvider: createMockApollo(requestHandlers),
+ // Use `attachTo` to find the modal
+ attachTo: document.body,
+ },
+ mount,
+ );
+ };
+
it.each`
type | text
${INSTANCE_TYPE} | ${'Register an instance runner'}
@@ -76,29 +105,10 @@ describe('RegistrationDropdown', () => {
});
describe('When the dropdown item is clicked', () => {
- Vue.use(VueApollo);
-
- const requestHandlers = [
- [getRunnerPlatformsQuery, jest.fn().mockResolvedValue(mockGraphqlRunnerPlatforms)],
- [getRunnerSetupInstructionsQuery, jest.fn().mockResolvedValue(mockGraphqlInstructions)],
- ];
-
- const findModalInBody = () =>
- createWrapper(document.body).find('[data-testid="runner-instructions-modal"]');
-
beforeEach(async () => {
- createComponent(
- {
- // Mock load modal contents from API
- apolloProvider: createMockApollo(requestHandlers),
- // Use `attachTo` to find the modal
- attachTo: document.body,
- },
- mount,
- );
-
- await findRegistrationInstructionsDropdownItem().trigger('click');
- await waitForPromises();
+ createComponentWithModal({}, mount);
+
+ await openModal();
});
afterEach(() => {
@@ -106,9 +116,7 @@ describe('RegistrationDropdown', () => {
});
it('opens the modal with contents', () => {
- const modalText = findModalInBody()
- .text()
- .replace(/[\n\t\s]+/g, ' ');
+ const modalText = findModalContent();
expect(modalText).toContain('Install a runner');
@@ -153,15 +161,34 @@ describe('RegistrationDropdown', () => {
});
});
- it('Updates the token when it gets reset', async () => {
+ describe('When token is reset', () => {
const newToken = 'mock1';
- createComponent({}, mount);
- expect(findRegistrationTokenInput().props('value')).not.toBe(newToken);
+ const resetToken = async () => {
+ findTokenResetDropdownItem().vm.$emit('tokenReset', newToken);
+ await nextTick();
+ };
+
+ it('Updates token in input', async () => {
+ createComponent({}, mount);
+
+ expect(findRegistrationTokenInput().props('value')).not.toBe(newToken);
+
+ await resetToken();
+
+ expect(findRegistrationToken().props('value')).toBe(newToken);
+ });
- findTokenResetDropdownItem().vm.$emit('tokenReset', newToken);
- await nextTick();
+ it('Updates token in modal', async () => {
+ createComponentWithModal({}, mount);
- expect(findRegistrationToken().props('value')).toBe(newToken);
+ await openModal();
+
+ expect(findModalContent()).toContain(mockToken);
+
+ await resetToken();
+
+ expect(findModalContent()).toContain(newToken);
+ });
});
});
diff --git a/spec/frontend/runner/components/runner_delete_button_spec.js b/spec/frontend/runner/components/runner_delete_button_spec.js
index 3eb257607b4..b11c749d0a7 100644
--- a/spec/frontend/runner/components/runner_delete_button_spec.js
+++ b/spec/frontend/runner/components/runner_delete_button_spec.js
@@ -118,6 +118,12 @@ describe('RunnerDeleteButton', () => {
expect(findBtn().attributes('aria-label')).toBe(undefined);
});
+ it('Passes other attributes to the button', () => {
+ createComponent({ props: { category: 'secondary' } });
+
+ expect(findBtn().props('category')).toBe('secondary');
+ });
+
describe(`Before the delete button is clicked`, () => {
it('The mutation has not been called', () => {
expect(runnerDeleteHandler).toHaveBeenCalledTimes(0);
diff --git a/spec/frontend/runner/components/runner_details_spec.js b/spec/frontend/runner/components/runner_details_spec.js
index 6bf4a52a799..162d21febfd 100644
--- a/spec/frontend/runner/components/runner_details_spec.js
+++ b/spec/frontend/runner/components/runner_details_spec.js
@@ -77,6 +77,9 @@ describe('RunnerDetails', () => {
${'Last contact'} | ${{ contactedAt: null }} | ${'Never contacted'}
${'Version'} | ${{ version: '12.3' }} | ${'12.3'}
${'Version'} | ${{ version: null }} | ${'None'}
+ ${'Executor'} | ${{ executorName: 'shell' }} | ${'shell'}
+ ${'Architecture'} | ${{ architectureName: 'amd64' }} | ${'amd64'}
+ ${'Platform'} | ${{ platformName: 'darwin' }} | ${'darwin'}
${'IP Address'} | ${{ ipAddress: '127.0.0.1' }} | ${'127.0.0.1'}
${'IP Address'} | ${{ ipAddress: null }} | ${'None'}
${'Configuration'} | ${{ accessLevel: ACCESS_LEVEL_REF_PROTECTED, runUntagged: true }} | ${'Protected, Runs untagged jobs'}
diff --git a/spec/frontend/runner/components/runner_jobs_spec.js b/spec/frontend/runner/components/runner_jobs_spec.js
index 9e40e911448..8ac5685a0dd 100644
--- a/spec/frontend/runner/components/runner_jobs_spec.js
+++ b/spec/frontend/runner/components/runner_jobs_spec.js
@@ -11,7 +11,7 @@ import RunnerPagination from '~/runner/components/runner_pagination.vue';
import { captureException } from '~/runner/sentry_utils';
import { I18N_NO_JOBS_FOUND, RUNNER_DETAILS_JOBS_PAGE_SIZE } from '~/runner/constants';
-import runnerJobsQuery from '~/runner/graphql/details/runner_jobs.query.graphql';
+import runnerJobsQuery from '~/runner/graphql/show/runner_jobs.query.graphql';
import { runnerData, runnerJobsData } from '../mock_data';
diff --git a/spec/frontend/runner/components/runner_projects_spec.js b/spec/frontend/runner/components/runner_projects_spec.js
index 62ebc6539e2..04627e2307b 100644
--- a/spec/frontend/runner/components/runner_projects_spec.js
+++ b/spec/frontend/runner/components/runner_projects_spec.js
@@ -16,7 +16,7 @@ import RunnerAssignedItem from '~/runner/components/runner_assigned_item.vue';
import RunnerPagination from '~/runner/components/runner_pagination.vue';
import { captureException } from '~/runner/sentry_utils';
-import runnerProjectsQuery from '~/runner/graphql/details/runner_projects.query.graphql';
+import runnerProjectsQuery from '~/runner/graphql/show/runner_projects.query.graphql';
import { runnerData, runnerProjectsData } from '../mock_data';
diff --git a/spec/frontend/runner/components/runner_update_form_spec.js b/spec/frontend/runner/components/runner_update_form_spec.js
index b071791e39f..3037364d941 100644
--- a/spec/frontend/runner/components/runner_update_form_spec.js
+++ b/spec/frontend/runner/components/runner_update_form_spec.js
@@ -1,10 +1,11 @@
import Vue, { nextTick } from 'vue';
-import { GlForm } from '@gitlab/ui';
+import { GlForm, GlSkeletonLoader } from '@gitlab/ui';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { createAlert, VARIANT_SUCCESS } from '~/flash';
+import { redirectTo } from '~/lib/utils/url_utility';
import RunnerUpdateForm from '~/runner/components/runner_update_form.vue';
import {
INSTANCE_TYPE,
@@ -13,14 +14,18 @@ import {
ACCESS_LEVEL_REF_PROTECTED,
ACCESS_LEVEL_NOT_PROTECTED,
} from '~/runner/constants';
-import runnerUpdateMutation from '~/runner/graphql/details/runner_update.mutation.graphql';
+import runnerUpdateMutation from '~/runner/graphql/edit/runner_update.mutation.graphql';
import { captureException } from '~/runner/sentry_utils';
-import { runnerData } from '../mock_data';
+import { saveAlertToLocalStorage } from '~/runner/local_storage_alert/save_alert_to_local_storage';
+import { runnerFormData } from '../mock_data';
+jest.mock('~/runner/local_storage_alert/save_alert_to_local_storage');
jest.mock('~/flash');
jest.mock('~/runner/sentry_utils');
+jest.mock('~/lib/utils/url_utility');
-const mockRunner = runnerData.data.runner;
+const mockRunner = runnerFormData.data.runner;
+const mockRunnerPath = '/admin/runners/1';
Vue.use(VueApollo);
@@ -33,8 +38,7 @@ describe('RunnerUpdateForm', () => {
const findProtectedCheckbox = () => wrapper.findByTestId('runner-field-protected');
const findRunUntaggedCheckbox = () => wrapper.findByTestId('runner-field-run-untagged');
const findLockedCheckbox = () => wrapper.findByTestId('runner-field-locked');
-
- const findIpInput = () => wrapper.findByTestId('runner-field-ip-address').find('input');
+ const findFields = () => wrapper.findAll('[data-testid^="runner-field"');
const findDescriptionInput = () => wrapper.findByTestId('runner-field-description').find('input');
const findMaxJobTimeoutInput = () =>
@@ -53,7 +57,6 @@ describe('RunnerUpdateForm', () => {
: ACCESS_LEVEL_NOT_PROTECTED,
runUntagged: findRunUntaggedCheckbox().element.checked,
locked: findLockedCheckbox().element?.checked || false,
- ipAddress: findIpInput().element.value,
maximumTimeout: findMaxJobTimeoutInput().element.value || null,
tagList: findTagsInput().element.value.split(',').filter(Boolean),
});
@@ -62,6 +65,7 @@ describe('RunnerUpdateForm', () => {
wrapper = mountExtended(RunnerUpdateForm, {
propsData: {
runner: mockRunner,
+ runnerPath: mockRunnerPath,
...props,
},
apolloProvider: createMockApollo([[runnerUpdateMutation, runnerUpdateHandler]]),
@@ -74,12 +78,13 @@ describe('RunnerUpdateForm', () => {
input: expect.objectContaining(submittedRunner),
});
- expect(createAlert).toHaveBeenLastCalledWith({
- message: expect.stringContaining('saved'),
- variant: VARIANT_SUCCESS,
- });
-
- expect(findSubmitDisabledAttr()).toBeUndefined();
+ expect(saveAlertToLocalStorage).toHaveBeenCalledWith(
+ expect.objectContaining({
+ message: expect.any(String),
+ variant: VARIANT_SUCCESS,
+ }),
+ );
+ expect(redirectTo).toHaveBeenCalledWith(mockRunnerPath);
};
beforeEach(() => {
@@ -122,27 +127,19 @@ describe('RunnerUpdateForm', () => {
await submitFormAndWait();
// Some read-only fields are not submitted
- const {
- __typename,
- ipAddress,
- runnerType,
- createdAt,
- status,
- editAdminUrl,
- contactedAt,
- userPermissions,
- version,
- groups,
- jobCount,
- ...submitted
- } = mockRunner;
+ const { __typename, shortSha, runnerType, createdAt, status, ...submitted } = mockRunner;
expectToHaveSubmittedRunnerContaining(submitted);
});
describe('When data is being loaded', () => {
beforeEach(() => {
- createComponent({ props: { runner: null } });
+ createComponent({ props: { loading: true } });
+ });
+
+ it('Form skeleton is shown', () => {
+ expect(wrapper.find(GlSkeletonLoader).exists()).toBe(true);
+ expect(findFields()).toHaveLength(0);
});
it('Form cannot be submitted', () => {
@@ -151,11 +148,12 @@ describe('RunnerUpdateForm', () => {
it('Form is updated when data loads', async () => {
wrapper.setProps({
- runner: mockRunner,
+ loading: false,
});
await nextTick();
+ expect(findFields()).not.toHaveLength(0);
expect(mockRunner).toMatchObject(getFieldsModel());
});
});
@@ -273,8 +271,11 @@ describe('RunnerUpdateForm', () => {
expect(createAlert).toHaveBeenLastCalledWith({
message: mockErrorMsg,
});
- expect(captureException).not.toHaveBeenCalled();
expect(findSubmitDisabledAttr()).toBeUndefined();
+
+ expect(captureException).not.toHaveBeenCalled();
+ expect(saveAlertToLocalStorage).not.toHaveBeenCalled();
+ expect(redirectTo).not.toHaveBeenCalled();
});
});
});
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 02348bf737a..52bd51a974b 100644
--- a/spec/frontend/runner/group_runners/group_runners_app_spec.js
+++ b/spec/frontend/runner/group_runners/group_runners_app_spec.js
@@ -30,7 +30,10 @@ import {
PROJECT_TYPE,
PARAM_KEY_PAUSED,
PARAM_KEY_STATUS,
+ PARAM_KEY_TAG,
STATUS_ONLINE,
+ STATUS_OFFLINE,
+ STATUS_STALE,
RUNNER_PAGE_SIZE,
I18N_EDIT,
} from '~/runner/constants';
@@ -53,7 +56,7 @@ Vue.use(GlToast);
const mockGroupFullPath = 'group1';
const mockRegistrationToken = 'AABBCC';
const mockGroupRunnersEdges = groupRunnersData.data.group.runners.edges;
-const mockGroupRunnersLimitedCount = mockGroupRunnersEdges.length;
+const mockGroupRunnersCount = mockGroupRunnersEdges.length;
jest.mock('~/flash');
jest.mock('~/runner/sentry_utils');
@@ -94,7 +97,7 @@ describe('GroupRunnersApp', () => {
propsData: {
registrationToken: mockRegistrationToken,
groupFullPath: mockGroupFullPath,
- groupRunnersLimitedCount: mockGroupRunnersLimitedCount,
+ groupRunnersLimitedCount: mockGroupRunnersCount,
...props,
},
provide: {
@@ -115,15 +118,24 @@ describe('GroupRunnersApp', () => {
});
it('shows total runner counts', async () => {
- createComponent({ mountFn: mountExtended });
-
- await waitForPromises();
-
- const stats = findRunnerStats().text();
+ expect(mockGroupRunnersCountQuery).toHaveBeenCalledWith({
+ groupFullPath: mockGroupFullPath,
+ status: STATUS_ONLINE,
+ });
+ expect(mockGroupRunnersCountQuery).toHaveBeenCalledWith({
+ groupFullPath: mockGroupFullPath,
+ status: STATUS_OFFLINE,
+ });
+ expect(mockGroupRunnersCountQuery).toHaveBeenCalledWith({
+ groupFullPath: mockGroupFullPath,
+ status: STATUS_STALE,
+ });
- expect(stats).toMatch('Online runners 2');
- expect(stats).toMatch('Offline runners 2');
- expect(stats).toMatch('Stale runners 2');
+ expect(findRunnerStats().props()).toMatchObject({
+ onlineRunnersCount: mockGroupRunnersCount,
+ offlineRunnersCount: mockGroupRunnersCount,
+ staleRunnersCount: mockGroupRunnersCount,
+ });
});
it('shows the runner tabs with a runner count for each type', async () => {
@@ -281,13 +293,28 @@ describe('GroupRunnersApp', () => {
first: RUNNER_PAGE_SIZE,
});
});
+
+ it('fetches count results for requested status', () => {
+ expect(mockGroupRunnersCountQuery).toHaveBeenCalledWith({
+ groupFullPath: mockGroupFullPath,
+ type: INSTANCE_TYPE,
+ status: STATUS_ONLINE,
+ });
+
+ expect(findRunnerStats().props()).toMatchObject({
+ onlineRunnersCount: mockGroupRunnersCount,
+ });
+ });
});
describe('when a filter is selected by the user', () => {
beforeEach(async () => {
findRunnerFilteredSearchBar().vm.$emit('input', {
runnerType: null,
- filters: [{ type: PARAM_KEY_STATUS, value: { data: STATUS_ONLINE, operator: '=' } }],
+ filters: [
+ { type: PARAM_KEY_STATUS, value: { data: STATUS_ONLINE, operator: '=' } },
+ { type: PARAM_KEY_TAG, value: { data: 'tag1', operator: '=' } },
+ ],
sort: CREATED_ASC,
});
@@ -297,7 +324,7 @@ describe('GroupRunnersApp', () => {
it('updates the browser url', () => {
expect(updateHistory).toHaveBeenLastCalledWith({
title: expect.any(String),
- url: 'http://test.host/groups/group1/-/runners?status[]=ONLINE&sort=CREATED_ASC',
+ url: 'http://test.host/groups/group1/-/runners?status[]=ONLINE&tag[]=tag1&sort=CREATED_ASC',
});
});
@@ -305,10 +332,41 @@ describe('GroupRunnersApp', () => {
expect(mockGroupRunnersQuery).toHaveBeenLastCalledWith({
groupFullPath: mockGroupFullPath,
status: STATUS_ONLINE,
+ tagList: ['tag1'],
sort: CREATED_ASC,
first: RUNNER_PAGE_SIZE,
});
});
+
+ it('fetches count results for requested status', () => {
+ expect(mockGroupRunnersCountQuery).toHaveBeenCalledWith({
+ groupFullPath: mockGroupFullPath,
+ tagList: ['tag1'],
+ status: STATUS_ONLINE,
+ });
+
+ expect(findRunnerStats().props()).toMatchObject({
+ onlineRunnersCount: mockGroupRunnersCount,
+ });
+ });
+
+ it('skips fetching count results for status that were not in filter', () => {
+ expect(mockGroupRunnersCountQuery).not.toHaveBeenCalledWith({
+ groupFullPath: mockGroupFullPath,
+ tagList: ['tag1'],
+ status: STATUS_OFFLINE,
+ });
+ expect(mockGroupRunnersCountQuery).not.toHaveBeenCalledWith({
+ groupFullPath: mockGroupFullPath,
+ tagList: ['tag1'],
+ status: STATUS_STALE,
+ });
+
+ expect(findRunnerStats().props()).toMatchObject({
+ offlineRunnersCount: null,
+ staleRunnersCount: null,
+ });
+ });
});
it('when runners have not loaded, shows a loading state', () => {
diff --git a/spec/frontend/runner/local_storage_alert/save_alert_to_local_storage_spec.js b/spec/frontend/runner/local_storage_alert/save_alert_to_local_storage_spec.js
new file mode 100644
index 00000000000..69cda6d6022
--- /dev/null
+++ b/spec/frontend/runner/local_storage_alert/save_alert_to_local_storage_spec.js
@@ -0,0 +1,24 @@
+import AccessorUtilities from '~/lib/utils/accessor';
+import { saveAlertToLocalStorage } from '~/runner/local_storage_alert/save_alert_to_local_storage';
+import { LOCAL_STORAGE_ALERT_KEY } from '~/runner/local_storage_alert/constants';
+import { useLocalStorageSpy } from 'helpers/local_storage_helper';
+
+const mockAlert = { message: 'Message!' };
+
+describe('saveAlertToLocalStorage', () => {
+ useLocalStorageSpy();
+
+ beforeEach(() => {
+ jest.spyOn(AccessorUtilities, 'canUseLocalStorage').mockReturnValue(true);
+ });
+
+ it('saves message to local storage', () => {
+ saveAlertToLocalStorage(mockAlert);
+
+ expect(localStorage.setItem).toHaveBeenCalledTimes(1);
+ expect(localStorage.setItem).toHaveBeenCalledWith(
+ LOCAL_STORAGE_ALERT_KEY,
+ JSON.stringify(mockAlert),
+ );
+ });
+});
diff --git a/spec/frontend/runner/local_storage_alert/show_alert_from_local_storage_spec.js b/spec/frontend/runner/local_storage_alert/show_alert_from_local_storage_spec.js
new file mode 100644
index 00000000000..cabbe642dac
--- /dev/null
+++ b/spec/frontend/runner/local_storage_alert/show_alert_from_local_storage_spec.js
@@ -0,0 +1,40 @@
+import AccessorUtilities from '~/lib/utils/accessor';
+import { showAlertFromLocalStorage } from '~/runner/local_storage_alert/show_alert_from_local_storage';
+import { LOCAL_STORAGE_ALERT_KEY } from '~/runner/local_storage_alert/constants';
+import { useLocalStorageSpy } from 'helpers/local_storage_helper';
+import { createAlert } from '~/flash';
+
+jest.mock('~/flash');
+
+describe('showAlertFromLocalStorage', () => {
+ useLocalStorageSpy();
+
+ beforeEach(() => {
+ jest.spyOn(AccessorUtilities, 'canUseLocalStorage').mockReturnValue(true);
+ });
+
+ it('retrieves message from local storage and displays it', async () => {
+ const mockAlert = { message: 'Message!' };
+
+ localStorage.getItem.mockReturnValueOnce(JSON.stringify(mockAlert));
+
+ await showAlertFromLocalStorage();
+
+ expect(createAlert).toHaveBeenCalledTimes(1);
+ expect(createAlert).toHaveBeenCalledWith(mockAlert);
+
+ expect(localStorage.removeItem).toHaveBeenCalledTimes(1);
+ expect(localStorage.removeItem).toHaveBeenCalledWith(LOCAL_STORAGE_ALERT_KEY);
+ });
+
+ it.each(['not a json string', null])('does not fail when stored message is %o', async (item) => {
+ localStorage.getItem.mockReturnValueOnce(item);
+
+ await showAlertFromLocalStorage();
+
+ expect(createAlert).not.toHaveBeenCalled();
+
+ expect(localStorage.removeItem).toHaveBeenCalledTimes(1);
+ expect(localStorage.removeItem).toHaveBeenCalledWith(LOCAL_STORAGE_ALERT_KEY);
+ });
+});
diff --git a/spec/frontend/runner/mock_data.js b/spec/frontend/runner/mock_data.js
index fbe8926124c..1c2333b552c 100644
--- a/spec/frontend/runner/mock_data.js
+++ b/spec/frontend/runner/mock_data.js
@@ -1,5 +1,14 @@
// Fixtures generated by: spec/frontend/fixtures/runner.rb
+// Show runner queries
+import runnerData from 'test_fixtures/graphql/runner/show/runner.query.graphql.json';
+import runnerWithGroupData from 'test_fixtures/graphql/runner/show/runner.query.graphql.with_group.json';
+import runnerProjectsData from 'test_fixtures/graphql/runner/show/runner_projects.query.graphql.json';
+import runnerJobsData from 'test_fixtures/graphql/runner/show/runner_jobs.query.graphql.json';
+
+// Edit runner queries
+import runnerFormData from 'test_fixtures/graphql/runner/edit/runner_form.query.graphql.json';
+
// List queries
import runnersData from 'test_fixtures/graphql/runner/list/admin_runners.query.graphql.json';
import runnersDataPaginated from 'test_fixtures/graphql/runner/list/admin_runners.query.graphql.paginated.json';
@@ -8,25 +17,20 @@ import groupRunnersData from 'test_fixtures/graphql/runner/list/group_runners.qu
import groupRunnersDataPaginated from 'test_fixtures/graphql/runner/list/group_runners.query.graphql.paginated.json';
import groupRunnersCountData from 'test_fixtures/graphql/runner/list/group_runners_count.query.graphql.json';
-// Details queries
-import runnerData from 'test_fixtures/graphql/runner/details/runner.query.graphql.json';
-import runnerWithGroupData from 'test_fixtures/graphql/runner/details/runner.query.graphql.with_group.json';
-import runnerProjectsData from 'test_fixtures/graphql/runner/details/runner_projects.query.graphql.json';
-import runnerJobsData from 'test_fixtures/graphql/runner/details/runner_jobs.query.graphql.json';
-
// Other mock data
export const onlineContactTimeoutSecs = 2 * 60 * 60;
export const staleTimeoutSecs = 5259492; // Ruby's `2.months`
export {
runnersData,
- runnersCountData,
runnersDataPaginated,
+ runnersCountData,
+ groupRunnersData,
+ groupRunnersDataPaginated,
+ groupRunnersCountData,
runnerData,
runnerWithGroupData,
runnerProjectsData,
runnerJobsData,
- groupRunnersData,
- groupRunnersCountData,
- groupRunnersDataPaginated,
+ runnerFormData,
};
diff --git a/spec/frontend/runner/runner_search_utils_spec.js b/spec/frontend/runner/runner_search_utils_spec.js
index 7834e76fe48..a3c1458ed26 100644
--- a/spec/frontend/runner/runner_search_utils_spec.js
+++ b/spec/frontend/runner/runner_search_utils_spec.js
@@ -220,13 +220,11 @@ describe('search_params.js', () => {
});
it.each`
- query | updatedQuery
- ${'status[]=NOT_CONNECTED'} | ${'status[]=NEVER_CONTACTED'}
- ${'status[]=NOT_CONNECTED&a=b'} | ${'status[]=NEVER_CONTACTED&a=b'}
- ${'status[]=ACTIVE'} | ${'paused[]=false'}
- ${'status[]=ACTIVE&a=b'} | ${'a=b&paused[]=false'}
- ${'status[]=ACTIVE'} | ${'paused[]=false'}
- ${'status[]=PAUSED'} | ${'paused[]=true'}
+ query | updatedQuery
+ ${'status[]=ACTIVE'} | ${'paused[]=false'}
+ ${'status[]=ACTIVE&a=b'} | ${'a=b&paused[]=false'}
+ ${'status[]=ACTIVE'} | ${'paused[]=false'}
+ ${'status[]=PAUSED'} | ${'paused[]=true'}
`('updates "$query" to "$updatedQuery"', ({ query, updatedQuery }) => {
const mockUrl = 'http://test.host/admin/runners?';
diff --git a/spec/frontend/search/highlight_blob_search_result_spec.js b/spec/frontend/search/highlight_blob_search_result_spec.js
index 9fa3bfc1f9a..15cff436076 100644
--- a/spec/frontend/search/highlight_blob_search_result_spec.js
+++ b/spec/frontend/search/highlight_blob_search_result_spec.js
@@ -1,10 +1,15 @@
import setHighlightClass from '~/search/highlight_blob_search_result';
+import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
const fixture = 'search/blob_search_result.html';
const searchKeyword = 'Send'; // spec/frontend/fixtures/search.rb#79
describe('search/highlight_blob_search_result', () => {
- beforeEach(() => loadFixtures(fixture));
+ beforeEach(() => loadHTMLFixture(fixture));
+
+ afterEach(() => {
+ resetHTMLFixture();
+ });
it('highlights lines with search term occurrence', () => {
setHighlightClass(searchKeyword);
diff --git a/spec/frontend/search_autocomplete_spec.js b/spec/frontend/search_autocomplete_spec.js
index 190f2803324..4639552b4d3 100644
--- a/spec/frontend/search_autocomplete_spec.js
+++ b/spec/frontend/search_autocomplete_spec.js
@@ -1,5 +1,6 @@
import AxiosMockAdapter from 'axios-mock-adapter';
import $ from 'jquery';
+import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
import axios from '~/lib/utils/axios_utils';
import initSearchAutocomplete from '~/search_autocomplete';
@@ -104,7 +105,7 @@ describe('Search autocomplete dropdown', () => {
};
beforeEach(() => {
- loadFixtures('static/search_autocomplete.html');
+ loadHTMLFixture('static/search_autocomplete.html');
window.gon = {};
window.gon.current_user_id = userId;
@@ -118,6 +119,8 @@ describe('Search autocomplete dropdown', () => {
// Undo what we did to the shared <body>
removeBodyAttributes();
window.gon = {};
+
+ resetHTMLFixture();
});
it('should show Dashboard specific dropdown menu', () => {
diff --git a/spec/frontend/security_configuration/components/app_spec.js b/spec/frontend/security_configuration/components/app_spec.js
index 9a18cb636b2..d7d46d0d415 100644
--- a/spec/frontend/security_configuration/components/app_spec.js
+++ b/spec/frontend/security_configuration/components/app_spec.js
@@ -1,6 +1,8 @@
+import Vue, { nextTick } from 'vue';
+import VueApollo from 'vue-apollo';
import { GlTab, GlTabs, GlLink } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
-import { nextTick } from 'vue';
+
import { useLocalStorageSpy } from 'helpers/local_storage_helper';
import { makeMockUserCalloutDismisser } from 'helpers/mock_user_callout_dismisser';
import stubChildren from 'helpers/stub_children';
@@ -18,15 +20,22 @@ import {
LICENSE_COMPLIANCE_DESCRIPTION,
LICENSE_COMPLIANCE_HELP_PATH,
AUTO_DEVOPS_ENABLED_ALERT_DISMISSED_STORAGE_KEY,
+ LICENSE_ULTIMATE,
+ LICENSE_PREMIUM,
+ LICENSE_FREE,
} from '~/security_configuration/components/constants';
import FeatureCard from '~/security_configuration/components/feature_card.vue';
import TrainingProviderList from '~/security_configuration/components/training_provider_list.vue';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import currentLicenseQuery from '~/security_configuration/graphql/current_license.query.graphql';
+import waitForPromises from 'helpers/wait_for_promises';
import UpgradeBanner from '~/security_configuration/components/upgrade_banner.vue';
import {
REPORT_TYPE_LICENSE_COMPLIANCE,
REPORT_TYPE_SAST,
} from '~/vue_shared/security_reports/constants';
+import { getCurrentLicensePlanResponse } from '../mock_data';
const upgradePath = '/upgrade';
const autoDevopsHelpPagePath = '/autoDevopsHelpPagePath';
@@ -36,18 +45,33 @@ const projectFullPath = 'namespace/project';
const vulnerabilityTrainingDocsPath = 'user/application_security/vulnerabilities/index';
useLocalStorageSpy();
+Vue.use(VueApollo);
describe('App component', () => {
let wrapper;
let userCalloutDismissSpy;
+ let mockApollo;
const createComponent = ({
shouldShowCallout = true,
- secureVulnerabilityTraining = true,
+ licenseQueryResponse = LICENSE_ULTIMATE,
...propsData
}) => {
userCalloutDismissSpy = jest.fn();
+ mockApollo = createMockApollo([
+ [
+ currentLicenseQuery,
+ jest
+ .fn()
+ .mockResolvedValue(
+ licenseQueryResponse instanceof Error
+ ? licenseQueryResponse
+ : getCurrentLicensePlanResponse(licenseQueryResponse),
+ ),
+ ],
+ ]);
+
wrapper = extendedWrapper(
mount(SecurityConfigurationApp, {
propsData,
@@ -57,10 +81,8 @@ describe('App component', () => {
autoDevopsPath,
projectFullPath,
vulnerabilityTrainingDocsPath,
- glFeatures: {
- secureVulnerabilityTraining,
- },
},
+ apolloProvider: mockApollo,
stubs: {
...stubChildren(SecurityConfigurationApp),
GlLink: false,
@@ -135,14 +157,16 @@ describe('App component', () => {
afterEach(() => {
wrapper.destroy();
+ mockApollo = null;
});
describe('basic structure', () => {
- beforeEach(() => {
+ beforeEach(async () => {
createComponent({
augmentedSecurityFeatures: securityFeaturesMock,
augmentedComplianceFeatures: complianceFeaturesMock,
});
+ await waitForPromises();
});
it('renders main-heading with correct text', () => {
@@ -445,11 +469,12 @@ describe('App component', () => {
});
describe('Vulnerability management', () => {
- beforeEach(() => {
+ beforeEach(async () => {
createComponent({
augmentedSecurityFeatures: securityFeaturesMock,
augmentedComplianceFeatures: complianceFeaturesMock,
});
+ await waitForPromises();
});
it('renders TrainingProviderList component', () => {
@@ -466,23 +491,25 @@ describe('App component', () => {
expect(trainingLink.text()).toBe('Learn more about vulnerability training');
expect(trainingLink.attributes('href')).toBe(vulnerabilityTrainingDocsPath);
});
- });
-
- describe('when secureVulnerabilityTraining feature flag is disabled', () => {
- beforeEach(() => {
- createComponent({
- augmentedSecurityFeatures: securityFeaturesMock,
- augmentedComplianceFeatures: complianceFeaturesMock,
- secureVulnerabilityTraining: false,
- });
- });
- it('renders correct amount of tabs', () => {
- expect(findTabs()).toHaveLength(2);
- });
-
- it('does not render the vulnerability-management tab', () => {
- expect(wrapper.findByTestId('vulnerability-management-tab').exists()).toBe(false);
- });
+ it.each`
+ licenseQueryResponse | display
+ ${LICENSE_ULTIMATE} | ${true}
+ ${LICENSE_PREMIUM} | ${false}
+ ${LICENSE_FREE} | ${false}
+ ${null} | ${true}
+ ${new Error()} | ${true}
+ `(
+ 'displays $display for license $licenseQueryResponse',
+ async ({ licenseQueryResponse, display }) => {
+ createComponent({
+ licenseQueryResponse,
+ augmentedSecurityFeatures: securityFeaturesMock,
+ augmentedComplianceFeatures: complianceFeaturesMock,
+ });
+ await waitForPromises();
+ expect(findVulnerabilityManagementTab().exists()).toBe(display);
+ },
+ );
});
});
diff --git a/spec/frontend/security_configuration/mock_data.js b/spec/frontend/security_configuration/mock_data.js
index 18a480bf082..94a36472a1d 100644
--- a/spec/frontend/security_configuration/mock_data.js
+++ b/spec/frontend/security_configuration/mock_data.js
@@ -111,3 +111,12 @@ export const tempProviderLogos = {
svg: `<svg>${[testProviderName[1]]}</svg>`,
},
};
+
+export const getCurrentLicensePlanResponse = (plan) => ({
+ data: {
+ currentLicense: {
+ id: 'gid://gitlab/License/1',
+ plan,
+ },
+ },
+});
diff --git a/spec/frontend/self_monitor/components/__snapshots__/self_monitor_form_spec.js.snap b/spec/frontend/self_monitor/components/__snapshots__/self_monitor_form_spec.js.snap
index c968c28c811..62a9ff98243 100644
--- a/spec/frontend/self_monitor/components/__snapshots__/self_monitor_form_spec.js.snap
+++ b/spec/frontend/self_monitor/components/__snapshots__/self_monitor_form_spec.js.snap
@@ -63,6 +63,7 @@ exports[`self monitor component When the self monitor project has not been creat
</div>
<gl-modal-stub
+ arialabel=""
cancel-title="Cancel"
category="primary"
dismisslabel="Close"
diff --git a/spec/frontend/serverless/components/__snapshots__/empty_state_spec.js.snap b/spec/frontend/serverless/components/__snapshots__/empty_state_spec.js.snap
deleted file mode 100644
index 0f4dfdf8a75..00000000000
--- a/spec/frontend/serverless/components/__snapshots__/empty_state_spec.js.snap
+++ /dev/null
@@ -1,22 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`EmptyStateComponent should render content 1`] = `
-"<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 gl-dark-invert-keep-hue\\"></div>
- </div>
- <div class=\\"gl-max-w-full gl-m-auto\\">
- <div class=\\"gl-mx-auto gl-my-0 gl-p-5\\">
- <h1 class=\\"gl-font-size-h-display gl-line-height-36 h4\\">
- Getting started with serverless
- </h1>
- <p class=\\"gl-mt-3\\">Serverless was <gl-link-stub target=\\"_blank\\" href=\\"https://about.gitlab.com/releases/2021/09/22/gitlab-14-3-released/#gitlab-serverless\\">deprecated</gl-link-stub>. But if you opt to use it, you must install Knative in your Kubernetes cluster first. <gl-link-stub href=\\"/help\\">Learn more.</gl-link-stub>
- </p>
- <div class=\\"gl-display-flex gl-flex-wrap gl-justify-content-center\\">
- <!---->
- <!---->
- </div>
- </div>
- </div>
-</section>"
-`;
diff --git a/spec/frontend/serverless/components/area_spec.js b/spec/frontend/serverless/components/area_spec.js
deleted file mode 100644
index 05c9ee44307..00000000000
--- a/spec/frontend/serverless/components/area_spec.js
+++ /dev/null
@@ -1,121 +0,0 @@
-import { shallowMount } from '@vue/test-utils';
-import Area from '~/serverless/components/area.vue';
-import { mockNormalizedMetrics } from '../mock_data';
-
-describe('Area component', () => {
- const mockWidgets = 'mockWidgets';
- const mockGraphData = mockNormalizedMetrics;
- let areaChart;
-
- beforeEach(() => {
- areaChart = shallowMount(Area, {
- propsData: {
- graphData: mockGraphData,
- containerWidth: 0,
- },
- slots: {
- default: mockWidgets,
- },
- });
- });
-
- afterEach(() => {
- areaChart.destroy();
- });
-
- it('renders chart title', () => {
- expect(areaChart.find({ ref: 'graphTitle' }).text()).toBe(mockGraphData.title);
- });
-
- it('contains graph widgets from slot', () => {
- expect(areaChart.find({ ref: 'graphWidgets' }).text()).toBe(mockWidgets);
- });
-
- describe('methods', () => {
- describe('formatTooltipText', () => {
- const mockDate = mockNormalizedMetrics.queries[0].result[0].values[0].time;
- const generateSeriesData = (type) => ({
- seriesData: [
- {
- componentSubType: type,
- value: [mockDate, 4],
- },
- ],
- value: mockDate,
- });
-
- describe('series is of line type', () => {
- beforeEach(() => {
- areaChart.vm.formatTooltipText(generateSeriesData('line'));
- });
-
- it('formats tooltip title', () => {
- expect(areaChart.vm.tooltipPopoverTitle).toBe('28 Feb 2019, 11:11AM');
- });
-
- it('formats tooltip content', () => {
- expect(areaChart.vm.tooltipPopoverContent).toBe('Invocations (requests): 4');
- });
- });
-
- it('verify default interval value of 1', () => {
- expect(areaChart.vm.getInterval).toBe(1);
- });
- });
-
- describe('onResize', () => {
- const mockWidth = 233;
-
- beforeEach(() => {
- jest.spyOn(Element.prototype, 'getBoundingClientRect').mockImplementation(() => ({
- width: mockWidth,
- }));
- areaChart.vm.onResize();
- });
-
- it('sets area chart width', () => {
- expect(areaChart.vm.width).toBe(mockWidth);
- });
- });
- });
-
- describe('computed', () => {
- describe('chartData', () => {
- it('utilizes all data points', () => {
- expect(Object.keys(areaChart.vm.chartData)).toEqual(['requests']);
- expect(areaChart.vm.chartData.requests.length).toBe(2);
- });
-
- it('creates valid data', () => {
- const data = areaChart.vm.chartData.requests;
-
- expect(
- data.filter(
- (datum) => new Date(datum.time).getTime() > 0 && typeof datum.value === 'number',
- ).length,
- ).toBe(data.length);
- });
- });
-
- describe('generateSeries', () => {
- it('utilizes correct time data', () => {
- expect(areaChart.vm.generateSeries.data).toEqual([
- ['2019-02-28T11:11:38.756Z', 0],
- ['2019-02-28T11:12:38.756Z', 0],
- ]);
- });
- });
-
- describe('xAxisLabel', () => {
- it('constructs a label for the chart x-axis', () => {
- expect(areaChart.vm.xAxisLabel).toBe('invocations / minute');
- });
- });
-
- describe('yAxisLabel', () => {
- it('constructs a label for the chart y-axis', () => {
- expect(areaChart.vm.yAxisLabel).toBe('Invocations (requests)');
- });
- });
- });
-});
diff --git a/spec/frontend/serverless/components/empty_state_spec.js b/spec/frontend/serverless/components/empty_state_spec.js
deleted file mode 100644
index d63882c2a6d..00000000000
--- a/spec/frontend/serverless/components/empty_state_spec.js
+++ /dev/null
@@ -1,25 +0,0 @@
-import { GlEmptyState, GlSprintf } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
-import EmptyStateComponent from '~/serverless/components/empty_state.vue';
-import { createStore } from '~/serverless/store';
-
-describe('EmptyStateComponent', () => {
- let wrapper;
-
- beforeEach(() => {
- const store = createStore({
- clustersPath: '/clusters',
- helpPath: '/help',
- emptyImagePath: '/image.svg',
- });
- wrapper = shallowMount(EmptyStateComponent, { store, stubs: { GlEmptyState, GlSprintf } });
- });
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- it('should render content', () => {
- expect(wrapper.html()).toMatchSnapshot();
- });
-});
diff --git a/spec/frontend/serverless/components/environment_row_spec.js b/spec/frontend/serverless/components/environment_row_spec.js
deleted file mode 100644
index 944283136d0..00000000000
--- a/spec/frontend/serverless/components/environment_row_spec.js
+++ /dev/null
@@ -1,68 +0,0 @@
-import { shallowMount } from '@vue/test-utils';
-import environmentRowComponent from '~/serverless/components/environment_row.vue';
-
-import { translate } from '~/serverless/utils';
-import { mockServerlessFunctions, mockServerlessFunctionsDiffEnv } from '../mock_data';
-
-const createComponent = (env, envName) =>
- shallowMount(environmentRowComponent, {
- propsData: { env, envName },
- }).vm;
-
-describe('environment row component', () => {
- describe('default global cluster case', () => {
- let vm;
-
- beforeEach(() => {
- vm = createComponent(translate(mockServerlessFunctions.functions)['*'], '*');
- });
-
- afterEach(() => vm.$destroy());
-
- it('has the correct envId', () => {
- expect(vm.envId).toEqual('env-global');
- });
-
- it('is open by default', () => {
- expect(vm.isOpenClass).toEqual({ 'is-open': true });
- });
-
- it('generates correct output', () => {
- expect(vm.$el.id).toEqual('env-global');
- expect(vm.$el.classList.contains('is-open')).toBe(true);
- expect(vm.$el.querySelector('div.title').innerHTML.trim()).toEqual('*');
- });
-
- it('opens and closes correctly', () => {
- expect(vm.isOpen).toBe(true);
-
- vm.toggleOpen();
-
- expect(vm.isOpen).toBe(false);
- });
- });
-
- describe('default named cluster case', () => {
- let vm;
-
- beforeEach(() => {
- vm = createComponent(translate(mockServerlessFunctionsDiffEnv.functions).test, 'test');
- });
-
- afterEach(() => vm.$destroy());
-
- it('has the correct envId', () => {
- expect(vm.envId).toEqual('env-test');
- });
-
- it('is open by default', () => {
- expect(vm.isOpenClass).toEqual({ 'is-open': true });
- });
-
- it('generates correct output', () => {
- expect(vm.$el.id).toEqual('env-test');
- expect(vm.$el.classList.contains('is-open')).toBe(true);
- expect(vm.$el.querySelector('div.title').innerHTML.trim()).toEqual('test');
- });
- });
-});
diff --git a/spec/frontend/serverless/components/function_details_spec.js b/spec/frontend/serverless/components/function_details_spec.js
deleted file mode 100644
index 0c9b2498589..00000000000
--- a/spec/frontend/serverless/components/function_details_spec.js
+++ /dev/null
@@ -1,100 +0,0 @@
-import { shallowMount } from '@vue/test-utils';
-import Vue from 'vue';
-import Vuex from 'vuex';
-
-import functionDetailsComponent from '~/serverless/components/function_details.vue';
-import { createStore } from '~/serverless/store';
-
-describe('functionDetailsComponent', () => {
- let component;
- let store;
-
- beforeEach(() => {
- Vue.use(Vuex);
-
- store = createStore({ clustersPath: '/clusters', helpPath: '/help' });
- });
-
- afterEach(() => {
- component.vm.$destroy();
- });
-
- describe('Verify base functionality', () => {
- const serviceStub = {
- name: 'test',
- description: 'a description',
- environment: '*',
- url: 'http://service.com/test',
- namespace: 'test-ns',
- podcount: 0,
- metricsUrl: '/metrics',
- };
-
- it('has a name, description, URL, and no pods loaded', () => {
- component = shallowMount(functionDetailsComponent, {
- store,
- propsData: {
- func: serviceStub,
- hasPrometheus: false,
- },
- });
-
- expect(
- component.vm.$el.querySelector('.serverless-function-name').innerHTML.trim(),
- ).toContain('test');
-
- expect(
- component.vm.$el.querySelector('.serverless-function-description').innerHTML.trim(),
- ).toContain('a description');
-
- expect(component.vm.$el.querySelector('p').innerHTML.trim()).toContain(
- 'No pods loaded at this time.',
- );
- });
-
- it('has a pods loaded', () => {
- serviceStub.podcount = 1;
-
- component = shallowMount(functionDetailsComponent, {
- store,
- propsData: {
- func: serviceStub,
- hasPrometheus: false,
- },
- });
-
- expect(component.vm.$el.querySelector('p').innerHTML.trim()).toContain('1 pod in use');
- });
-
- it('has multiple pods loaded', () => {
- serviceStub.podcount = 3;
-
- component = shallowMount(functionDetailsComponent, {
- store,
- propsData: {
- func: serviceStub,
- hasPrometheus: false,
- },
- });
-
- expect(component.vm.$el.querySelector('p').innerHTML.trim()).toContain('3 pods in use');
- });
-
- it('can support a missing description', () => {
- serviceStub.description = null;
-
- component = shallowMount(functionDetailsComponent, {
- store,
- propsData: {
- func: serviceStub,
- hasPrometheus: false,
- },
- });
-
- expect(
- component.vm.$el.querySelector('.serverless-function-description').querySelector('div')
- .innerHTML.length,
- ).toEqual(0);
- });
- });
-});
diff --git a/spec/frontend/serverless/components/function_row_spec.js b/spec/frontend/serverless/components/function_row_spec.js
deleted file mode 100644
index 081edd33b3b..00000000000
--- a/spec/frontend/serverless/components/function_row_spec.js
+++ /dev/null
@@ -1,34 +0,0 @@
-import { shallowMount } from '@vue/test-utils';
-import functionRowComponent from '~/serverless/components/function_row.vue';
-import Timeago from '~/vue_shared/components/time_ago_tooltip.vue';
-
-import { mockServerlessFunction } from '../mock_data';
-
-describe('functionRowComponent', () => {
- let wrapper;
-
- const createComponent = (func) => {
- wrapper = shallowMount(functionRowComponent, {
- propsData: { func },
- });
- };
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- it('Parses the function details correctly', () => {
- createComponent(mockServerlessFunction);
-
- expect(wrapper.find('b').text()).toBe(mockServerlessFunction.name);
- expect(wrapper.find('span').text()).toBe(mockServerlessFunction.image);
- expect(wrapper.find(Timeago).attributes('time')).not.toBe(null);
- });
-
- it('handles clicks correctly', () => {
- createComponent(mockServerlessFunction);
- const { vm } = wrapper;
-
- expect(vm.checkClass(vm.$el.querySelector('p'))).toBe(true); // check somewhere inside the row
- });
-});
diff --git a/spec/frontend/serverless/components/functions_spec.js b/spec/frontend/serverless/components/functions_spec.js
deleted file mode 100644
index 846fd63e918..00000000000
--- a/spec/frontend/serverless/components/functions_spec.js
+++ /dev/null
@@ -1,86 +0,0 @@
-import { GlLoadingIcon, GlAlert, GlSprintf } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
-import Vue, { nextTick } from 'vue';
-import AxiosMockAdapter from 'axios-mock-adapter';
-import Vuex from 'vuex';
-import { TEST_HOST } from 'helpers/test_constants';
-import axios from '~/lib/utils/axios_utils';
-import EmptyState from '~/serverless/components/empty_state.vue';
-import EnvironmentRow from '~/serverless/components/environment_row.vue';
-import functionsComponent from '~/serverless/components/functions.vue';
-import { createStore } from '~/serverless/store';
-import { mockServerlessFunctions } from '../mock_data';
-
-describe('functionsComponent', () => {
- const statusPath = `${TEST_HOST}/statusPath`;
-
- let component;
- let store;
- let axiosMock;
-
- beforeEach(() => {
- axiosMock = new AxiosMockAdapter(axios);
- axiosMock.onGet(statusPath).reply(200);
-
- Vue.use(Vuex);
-
- store = createStore({});
- component = shallowMount(functionsComponent, { store, stubs: { GlSprintf } });
- });
-
- afterEach(() => {
- component.destroy();
- axiosMock.restore();
- });
-
- it('should render deprecation notice', () => {
- expect(component.findComponent(GlAlert).text()).toBe(
- 'Serverless was deprecated in GitLab 14.3.',
- );
- });
-
- it('should render empty state when Knative is not installed', async () => {
- await store.dispatch('receiveFunctionsSuccess', { knative_installed: false });
-
- expect(component.findComponent(EmptyState).exists()).toBe(true);
- });
-
- it('should render a loading component', async () => {
- await store.dispatch('requestFunctionsLoading');
-
- expect(component.findComponent(GlLoadingIcon).exists()).toBe(true);
- });
-
- it('should render empty state when there is no function data', async () => {
- await store.dispatch('receiveFunctionsNoDataSuccess', { knative_installed: true });
-
- expect(
- component.vm.$el
- .querySelector('.empty-state, .js-empty-state')
- .classList.contains('js-empty-state'),
- ).toBe(true);
-
- expect(component.vm.$el.querySelector('.state-title, .text-center').innerHTML.trim()).toEqual(
- 'No functions available',
- );
- });
-
- it('should render functions and a loader when functions are partially fetched', async () => {
- await store.dispatch('receiveFunctionsPartial', {
- ...mockServerlessFunctions,
- knative_installed: 'checking',
- });
-
- expect(component.find('.js-functions-wrapper').exists()).toBe(true);
- expect(component.find('.js-functions-loader').exists()).toBe(true);
- });
-
- it('should render the functions list', async () => {
- store = createStore({ clustersPath: 'clustersPath', helpPath: 'helpPath', statusPath });
-
- await component.vm.$store.dispatch('receiveFunctionsSuccess', mockServerlessFunctions);
-
- await nextTick();
- expect(component.findComponent(EnvironmentRow).exists()).toBe(true);
- });
-});
diff --git a/spec/frontend/serverless/components/missing_prometheus_spec.js b/spec/frontend/serverless/components/missing_prometheus_spec.js
deleted file mode 100644
index 1b93fd784e1..00000000000
--- a/spec/frontend/serverless/components/missing_prometheus_spec.js
+++ /dev/null
@@ -1,38 +0,0 @@
-import { GlButton } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
-import missingPrometheusComponent from '~/serverless/components/missing_prometheus.vue';
-import { createStore } from '~/serverless/store';
-
-describe('missingPrometheusComponent', () => {
- let wrapper;
-
- const createComponent = (missingData) => {
- const store = createStore({ clustersPath: '/clusters', helpPath: '/help' });
-
- wrapper = shallowMount(missingPrometheusComponent, { store, propsData: { missingData } });
- };
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- it('should render missing prometheus message', () => {
- createComponent(false);
- const { vm } = wrapper;
-
- expect(vm.$el.querySelector('.state-description').innerHTML.trim()).toContain(
- 'Function invocation metrics require the Prometheus cluster integration.',
- );
-
- expect(wrapper.find(GlButton).attributes('variant')).toBe('success');
- });
-
- it('should render no prometheus data message', () => {
- createComponent(true);
- const { vm } = wrapper;
-
- expect(vm.$el.querySelector('.state-description').innerHTML.trim()).toContain(
- 'Invocation metrics loading or not available at this time.',
- );
- });
-});
diff --git a/spec/frontend/serverless/components/pod_box_spec.js b/spec/frontend/serverless/components/pod_box_spec.js
deleted file mode 100644
index cf0c14a2cac..00000000000
--- a/spec/frontend/serverless/components/pod_box_spec.js
+++ /dev/null
@@ -1,22 +0,0 @@
-import { shallowMount } from '@vue/test-utils';
-import podBoxComponent from '~/serverless/components/pod_box.vue';
-
-const createComponent = (count) =>
- shallowMount(podBoxComponent, {
- propsData: {
- count,
- },
- }).vm;
-
-describe('podBoxComponent', () => {
- it('should render three boxes', () => {
- const count = 3;
- const vm = createComponent(count);
- const rects = vm.$el.querySelectorAll('rect');
-
- expect(rects.length).toEqual(3);
- expect(parseInt(rects[2].getAttribute('x'), 10)).toEqual(40);
-
- vm.$destroy();
- });
-});
diff --git a/spec/frontend/serverless/components/url_spec.js b/spec/frontend/serverless/components/url_spec.js
deleted file mode 100644
index 8c839577aa0..00000000000
--- a/spec/frontend/serverless/components/url_spec.js
+++ /dev/null
@@ -1,26 +0,0 @@
-import { shallowMount } from '@vue/test-utils';
-import Vue from 'vue';
-import urlComponent from '~/serverless/components/url.vue';
-import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
-
-const createComponent = (uri) =>
- shallowMount(Vue.extend(urlComponent), {
- propsData: {
- uri,
- },
- });
-
-describe('urlComponent', () => {
- it('should render correctly', () => {
- const uri = 'http://testfunc.apps.example.com';
- const wrapper = createComponent(uri);
- const { vm } = wrapper;
-
- expect(vm.$el.classList.contains('clipboard-group')).toBe(true);
- expect(wrapper.find(ClipboardButton).attributes('text')).toEqual(uri);
-
- expect(vm.$el.querySelector('[data-testid="url-text-field"]').innerHTML).toContain(uri);
-
- vm.$destroy();
- });
-});
diff --git a/spec/frontend/serverless/mock_data.js b/spec/frontend/serverless/mock_data.js
deleted file mode 100644
index 1816ad62a04..00000000000
--- a/spec/frontend/serverless/mock_data.js
+++ /dev/null
@@ -1,145 +0,0 @@
-export const mockServerlessFunctions = {
- knative_installed: true,
- functions: [
- {
- name: 'testfunc1',
- namespace: 'tm-example',
- environment_scope: '*',
- cluster_id: 46,
- detail_url: '/testuser/testproj/serverless/functions/*/testfunc1',
- podcount: null,
- created_at: '2019-02-05T01:01:23Z',
- url: 'http://testfunc1.tm-example.apps.example.com',
- description: 'A test service',
- image: 'knative-test-container-buildtemplate',
- },
- {
- name: 'testfunc2',
- namespace: 'tm-example',
- environment_scope: '*',
- cluster_id: 46,
- detail_url: '/testuser/testproj/serverless/functions/*/testfunc2',
- podcount: null,
- created_at: '2019-02-05T01:01:23Z',
- url: 'http://testfunc2.tm-example.apps.example.com',
- description: 'A second test service\nThis one with additional descriptions',
- image: 'knative-test-echo-buildtemplate',
- },
- ],
-};
-
-export const mockServerlessFunctionsDiffEnv = {
- knative_installed: true,
- functions: [
- {
- name: 'testfunc1',
- namespace: 'tm-example',
- environment_scope: '*',
- cluster_id: 46,
- detail_url: '/testuser/testproj/serverless/functions/*/testfunc1',
- podcount: null,
- created_at: '2019-02-05T01:01:23Z',
- url: 'http://testfunc1.tm-example.apps.example.com',
- description: 'A test service',
- image: 'knative-test-container-buildtemplate',
- },
- {
- name: 'testfunc2',
- namespace: 'tm-example',
- environment_scope: 'test',
- cluster_id: 46,
- detail_url: '/testuser/testproj/serverless/functions/*/testfunc2',
- podcount: null,
- created_at: '2019-02-05T01:01:23Z',
- url: 'http://testfunc2.tm-example.apps.example.com',
- description: 'A second test service\nThis one with additional descriptions',
- image: 'knative-test-echo-buildtemplate',
- },
- ],
-};
-
-export const mockServerlessFunction = {
- name: 'testfunc1',
- namespace: 'tm-example',
- environment_scope: '*',
- cluster_id: 46,
- detail_url: '/testuser/testproj/serverless/functions/*/testfunc1',
- podcount: '3',
- created_at: '2019-02-05T01:01:23Z',
- url: 'http://testfunc1.tm-example.apps.example.com',
- description: 'A test service',
- image: 'knative-test-container-buildtemplate',
-};
-
-export const mockMultilineServerlessFunction = {
- name: 'testfunc1',
- namespace: 'tm-example',
- environment_scope: '*',
- cluster_id: 46,
- detail_url: '/testuser/testproj/serverless/functions/*/testfunc1',
- podcount: '3',
- created_at: '2019-02-05T01:01:23Z',
- url: 'http://testfunc1.tm-example.apps.example.com',
- description: 'testfunc1\nA test service line\\nWith additional services',
- image: 'knative-test-container-buildtemplate',
-};
-
-export const mockMetrics = {
- success: true,
- last_update: '2019-02-28T19:11:38.926Z',
- metrics: {
- id: 22,
- title: 'Knative function invocations',
- required_metrics: ['container_memory_usage_bytes', 'container_cpu_usage_seconds_total'],
- weight: 0,
- y_label: 'Invocations',
- queries: [
- {
- query_range:
- 'floor(sum(rate(istio_revision_request_count{destination_configuration="%{function_name}", destination_namespace="%{kube_namespace}"}[1m])*30))',
- unit: 'requests',
- label: 'invocations / minute',
- result: [
- {
- metric: {},
- values: [
- [1551352298.756, '0'],
- [1551352358.756, '0'],
- ],
- },
- ],
- },
- ],
- },
-};
-
-export const mockNormalizedMetrics = {
- id: 22,
- title: 'Knative function invocations',
- required_metrics: ['container_memory_usage_bytes', 'container_cpu_usage_seconds_total'],
- weight: 0,
- y_label: 'Invocations',
- queries: [
- {
- query_range:
- 'floor(sum(rate(istio_revision_request_count{destination_configuration="%{function_name}", destination_namespace="%{kube_namespace}"}[1m])*30))',
- unit: 'requests',
- label: 'invocations / minute',
- result: [
- {
- metric: {},
- values: [
- {
- time: '2019-02-28T11:11:38.756Z',
- value: 0,
- },
- {
- time: '2019-02-28T11:12:38.756Z',
- value: 0,
- },
- ],
- },
- ],
- },
- ],
-};
diff --git a/spec/frontend/serverless/store/actions_spec.js b/spec/frontend/serverless/store/actions_spec.js
deleted file mode 100644
index 5fbecf081a6..00000000000
--- a/spec/frontend/serverless/store/actions_spec.js
+++ /dev/null
@@ -1,80 +0,0 @@
-import MockAdapter from 'axios-mock-adapter';
-import testAction from 'helpers/vuex_action_helper';
-import axios from '~/lib/utils/axios_utils';
-import statusCodes from '~/lib/utils/http_status';
-import { fetchFunctions, fetchMetrics } from '~/serverless/store/actions';
-import { mockServerlessFunctions, mockMetrics } from '../mock_data';
-import { adjustMetricQuery } from '../utils';
-
-describe('ServerlessActions', () => {
- let mock;
-
- beforeEach(() => {
- mock = new MockAdapter(axios);
- });
-
- afterEach(() => {
- mock.restore();
- });
-
- describe('fetchFunctions', () => {
- it('should successfully fetch functions', () => {
- const endpoint = '/functions';
- mock.onGet(endpoint).reply(statusCodes.OK, JSON.stringify(mockServerlessFunctions));
-
- return testAction(
- fetchFunctions,
- { functionsPath: endpoint },
- {},
- [],
- [
- { type: 'requestFunctionsLoading' },
- { type: 'receiveFunctionsSuccess', payload: mockServerlessFunctions },
- ],
- );
- });
-
- it('should successfully retry', () => {
- const endpoint = '/functions';
- mock
- .onGet(endpoint)
- .reply(() => new Promise((resolve) => setTimeout(() => resolve(200), Infinity)));
-
- return testAction(
- fetchFunctions,
- { functionsPath: endpoint },
- {},
- [],
- [{ type: 'requestFunctionsLoading' }],
- );
- });
- });
-
- describe('fetchMetrics', () => {
- it('should return no prometheus', () => {
- const endpoint = '/metrics';
- mock.onGet(endpoint).reply(statusCodes.NO_CONTENT);
-
- return testAction(
- fetchMetrics,
- { metricsPath: endpoint, hasPrometheus: false },
- {},
- [],
- [{ type: 'receiveMetricsNoPrometheus' }],
- );
- });
-
- it('should successfully fetch metrics', () => {
- const endpoint = '/metrics';
- mock.onGet(endpoint).reply(statusCodes.OK, JSON.stringify(mockMetrics));
-
- return testAction(
- fetchMetrics,
- { metricsPath: endpoint, hasPrometheus: true },
- {},
- [],
- [{ type: 'receiveMetricsSuccess', payload: adjustMetricQuery(mockMetrics) }],
- );
- });
- });
-});
diff --git a/spec/frontend/serverless/store/getters_spec.js b/spec/frontend/serverless/store/getters_spec.js
deleted file mode 100644
index e1942bd2759..00000000000
--- a/spec/frontend/serverless/store/getters_spec.js
+++ /dev/null
@@ -1,43 +0,0 @@
-import * as getters from '~/serverless/store/getters';
-import serverlessState from '~/serverless/store/state';
-import { mockServerlessFunctions } from '../mock_data';
-
-describe('Serverless Store Getters', () => {
- let state;
-
- beforeEach(() => {
- state = serverlessState;
- });
-
- describe('hasPrometheusMissingData', () => {
- it('should return false if Prometheus is not installed', () => {
- state.hasPrometheus = false;
-
- expect(getters.hasPrometheusMissingData(state)).toEqual(false);
- });
-
- it('should return false if Prometheus is installed and there is data', () => {
- state.hasPrometheusData = true;
-
- expect(getters.hasPrometheusMissingData(state)).toEqual(false);
- });
-
- it('should return true if Prometheus is installed and there is no data', () => {
- state.hasPrometheus = true;
- state.hasPrometheusData = false;
-
- expect(getters.hasPrometheusMissingData(state)).toEqual(true);
- });
- });
-
- describe('getFunctions', () => {
- it('should translate the raw function array to group the functions per environment scope', () => {
- state.functions = mockServerlessFunctions.functions;
-
- const funcs = getters.getFunctions(state);
-
- expect(Object.keys(funcs)).toContain('*');
- expect(funcs['*'].length).toEqual(2);
- });
- });
-});
diff --git a/spec/frontend/serverless/store/mutations_spec.js b/spec/frontend/serverless/store/mutations_spec.js
deleted file mode 100644
index a1a8f9a2ca7..00000000000
--- a/spec/frontend/serverless/store/mutations_spec.js
+++ /dev/null
@@ -1,86 +0,0 @@
-import * as types from '~/serverless/store/mutation_types';
-import mutations from '~/serverless/store/mutations';
-import { mockServerlessFunctions, mockMetrics } from '../mock_data';
-
-describe('ServerlessMutations', () => {
- describe('Functions List Mutations', () => {
- it('should ensure loading is true', () => {
- const state = {};
-
- mutations[types.REQUEST_FUNCTIONS_LOADING](state);
-
- expect(state.isLoading).toEqual(true);
- });
-
- it('should set proper state once functions are loaded', () => {
- const state = {};
-
- mutations[types.RECEIVE_FUNCTIONS_SUCCESS](state, mockServerlessFunctions);
-
- expect(state.isLoading).toEqual(false);
- expect(state.hasFunctionData).toEqual(true);
- expect(state.functions).toEqual(mockServerlessFunctions.functions);
- });
-
- it('should ensure loading has stopped and hasFunctionData is false when there are no functions available', () => {
- const state = {};
-
- mutations[types.RECEIVE_FUNCTIONS_NODATA_SUCCESS](state, { knative_installed: true });
-
- expect(state.isLoading).toEqual(false);
- expect(state.hasFunctionData).toEqual(false);
- expect(state.functions).toBe(undefined);
- });
-
- it('should ensure loading has stopped, and an error is raised', () => {
- const state = {};
-
- mutations[types.RECEIVE_FUNCTIONS_ERROR](state, 'sample error');
-
- expect(state.isLoading).toEqual(false);
- expect(state.hasFunctionData).toEqual(false);
- expect(state.functions).toBe(undefined);
- expect(state.error).not.toBe(undefined);
- });
- });
-
- describe('Function Details Metrics Mutations', () => {
- it('should ensure isLoading and hasPrometheus data flags indicate data is loaded', () => {
- const state = {};
-
- mutations[types.RECEIVE_METRICS_SUCCESS](state, mockMetrics);
-
- expect(state.isLoading).toEqual(false);
- expect(state.hasPrometheusData).toEqual(true);
- expect(state.graphData).toEqual(mockMetrics);
- });
-
- it('should ensure isLoading and hasPrometheus data flags are cleared indicating no functions available', () => {
- const state = {};
-
- mutations[types.RECEIVE_METRICS_NODATA_SUCCESS](state);
-
- expect(state.isLoading).toEqual(false);
- expect(state.hasPrometheusData).toEqual(false);
- expect(state.graphData).toBe(undefined);
- });
-
- it('should properly indicate an error', () => {
- const state = {};
-
- mutations[types.RECEIVE_METRICS_ERROR](state, 'sample error');
-
- expect(state.hasPrometheusData).toEqual(false);
- expect(state.error).not.toBe(undefined);
- });
-
- it('should properly indicate when prometheus is installed', () => {
- const state = {};
-
- mutations[types.RECEIVE_METRICS_NO_PROMETHEUS](state);
-
- expect(state.hasPrometheus).toEqual(false);
- expect(state.hasPrometheusData).toEqual(false);
- });
- });
-});
diff --git a/spec/frontend/serverless/utils.js b/spec/frontend/serverless/utils.js
deleted file mode 100644
index 7caf7da231e..00000000000
--- a/spec/frontend/serverless/utils.js
+++ /dev/null
@@ -1,17 +0,0 @@
-export const adjustMetricQuery = (data) => {
- const updatedMetric = data.metrics;
-
- const queries = data.metrics.queries.map((query) => ({
- ...query,
- result: query.result.map((result) => ({
- ...result,
- values: result.values.map(([timestamp, value]) => ({
- time: new Date(timestamp * 1000).toISOString(),
- value: Number(value),
- })),
- })),
- }));
-
- updatedMetric.queries = queries;
- return updatedMetric;
-};
diff --git a/spec/frontend/settings_panels_spec.js b/spec/frontend/settings_panels_spec.js
index 3a62cd703ab..d59e1a20b27 100644
--- a/spec/frontend/settings_panels_spec.js
+++ b/spec/frontend/settings_panels_spec.js
@@ -1,9 +1,14 @@
import $ from 'jquery';
+import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import initSettingsPanels, { isExpanded } from '~/settings_panels';
describe('Settings Panels', () => {
beforeEach(() => {
- loadFixtures('groups/edit.html');
+ loadHTMLFixture('groups/edit.html');
+ });
+
+ afterEach(() => {
+ resetHTMLFixture();
});
describe('initSettingsPane', () => {
diff --git a/spec/frontend/shortcuts_spec.js b/spec/frontend/shortcuts_spec.js
index 8b9a11056f2..e859d435f48 100644
--- a/spec/frontend/shortcuts_spec.js
+++ b/spec/frontend/shortcuts_spec.js
@@ -1,5 +1,6 @@
import $ from 'jquery';
import { flatten } from 'lodash';
+import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import Shortcuts from '~/behaviors/shortcuts/shortcuts';
const mockMousetrap = {
@@ -21,7 +22,7 @@ describe('Shortcuts', () => {
});
beforeEach(() => {
- loadFixtures(fixtureName);
+ loadHTMLFixture(fixtureName);
jest.spyOn(document.querySelector('.js-new-note-form .js-md-preview-button'), 'focus');
jest.spyOn(document.querySelector('.edit-note .js-md-preview-button'), 'focus');
@@ -30,6 +31,10 @@ describe('Shortcuts', () => {
new Shortcuts(); // eslint-disable-line no-new
});
+ afterEach(() => {
+ resetHTMLFixture();
+ });
+
describe('toggleMarkdownPreview', () => {
it('focuses preview button in form', () => {
Shortcuts.toggleMarkdownPreview(
diff --git a/spec/frontend/sidebar/components/assignees/uncollapsed_assignee_list_spec.js b/spec/frontend/sidebar/components/assignees/uncollapsed_assignee_list_spec.js
index 90aae85e1ca..f7437386814 100644
--- a/spec/frontend/sidebar/components/assignees/uncollapsed_assignee_list_spec.js
+++ b/spec/frontend/sidebar/components/assignees/uncollapsed_assignee_list_spec.js
@@ -47,12 +47,10 @@ describe('UncollapsedAssigneeList component', () => {
it('calls the AssigneeAvatarLink with the proper props', () => {
expect(wrapper.find(AssigneeAvatarLink).exists()).toBe(true);
- expect(wrapper.find(AssigneeAvatarLink).props().tooltipPlacement).toEqual('left');
});
it('Shows one user with avatar, username and author name', () => {
expect(wrapper.text()).toContain(user.name);
- expect(wrapper.text()).toContain(`@${user.username}`);
});
});
diff --git a/spec/frontend/sidebar/components/attention_requested_toggle_spec.js b/spec/frontend/sidebar/components/attention_requested_toggle_spec.js
index a9ae23c1624..959fa799eb7 100644
--- a/spec/frontend/sidebar/components/attention_requested_toggle_spec.js
+++ b/spec/frontend/sidebar/components/attention_requested_toggle_spec.js
@@ -68,6 +68,7 @@ describe('Attention require toggle', () => {
{
user: { attention_requested: true, can_update_merge_request: true },
callback: expect.anything(),
+ direction: 'remove',
},
]);
});
@@ -96,9 +97,9 @@ describe('Attention require toggle', () => {
it.each`
type | attentionRequested | tooltip | canUpdateMergeRequest
- ${'reviewer'} | ${true} | ${AttentionRequestedToggle.i18n.removeAttentionRequested} | ${true}
- ${'reviewer'} | ${false} | ${AttentionRequestedToggle.i18n.attentionRequestedReviewer} | ${true}
- ${'assignee'} | ${false} | ${AttentionRequestedToggle.i18n.attentionRequestedAssignee} | ${true}
+ ${'reviewer'} | ${true} | ${AttentionRequestedToggle.i18n.removeAttentionRequest} | ${true}
+ ${'reviewer'} | ${false} | ${AttentionRequestedToggle.i18n.addAttentionRequest} | ${true}
+ ${'assignee'} | ${false} | ${AttentionRequestedToggle.i18n.addAttentionRequest} | ${true}
${'reviewer'} | ${true} | ${AttentionRequestedToggle.i18n.attentionRequestedNoPermission} | ${false}
${'reviewer'} | ${false} | ${AttentionRequestedToggle.i18n.noAttentionRequestedNoPermission} | ${false}
${'assignee'} | ${true} | ${AttentionRequestedToggle.i18n.attentionRequestedNoPermission} | ${false}
diff --git a/spec/frontend/sidebar/components/confidential/sidebar_confidentiality_content_spec.js b/spec/frontend/sidebar/components/confidential/sidebar_confidentiality_content_spec.js
index 8844e1626cd..ab45fdf03bc 100644
--- a/spec/frontend/sidebar/components/confidential/sidebar_confidentiality_content_spec.js
+++ b/spec/frontend/sidebar/components/confidential/sidebar_confidentiality_content_spec.js
@@ -1,4 +1,4 @@
-import { GlIcon } from '@gitlab/ui';
+import { GlIcon, GlAlert } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import SidebarConfidentialityContent from '~/sidebar/components/confidential/sidebar_confidentiality_content.vue';
@@ -60,12 +60,24 @@ describe('Sidebar Confidentiality Content', () => {
it('displays a correct confidential text for issue', () => {
createComponent({ confidential: true });
- expect(findText().text()).toBe('This issue is confidential');
+
+ const alertEl = findText().findComponent(GlAlert);
+
+ expect(alertEl.props()).toMatchObject({
+ showIcon: false,
+ dismissible: false,
+ variant: 'warning',
+ });
+ expect(alertEl.text()).toBe(
+ 'Only project members with at least Reporter role can view or be notified about this issue.',
+ );
});
it('displays a correct confidential text for epic', () => {
createComponent({ confidential: true, issuableType: 'epic' });
- expect(findText().text()).toBe('This epic is confidential');
+ expect(findText().findComponent(GlAlert).text()).toBe(
+ 'Only group members with at least Reporter role can view or be notified about this epic.',
+ );
});
});
});
diff --git a/spec/frontend/sidebar/components/confidential/sidebar_confidentiality_form_spec.js b/spec/frontend/sidebar/components/confidential/sidebar_confidentiality_form_spec.js
index 28a19fb9df6..85d6bc7b782 100644
--- a/spec/frontend/sidebar/components/confidential/sidebar_confidentiality_form_spec.js
+++ b/spec/frontend/sidebar/components/confidential/sidebar_confidentiality_form_spec.js
@@ -89,7 +89,7 @@ describe('Sidebar Confidentiality Form', () => {
it('renders a message about making an issue confidential', () => {
expect(findWarningMessage().text()).toBe(
- 'You are going to turn on confidentiality. Only team members with at least Reporter access will be able to see and leave comments on the issue.',
+ 'You are going to turn on confidentiality. Only project members with at least Reporter role can view or be notified about this issue.',
);
});
diff --git a/spec/frontend/sidebar/components/crm_contacts_spec.js b/spec/frontend/sidebar/components/crm_contacts_spec.js
index 758cff30e2d..6456829258f 100644
--- a/spec/frontend/sidebar/components/crm_contacts_spec.js
+++ b/spec/frontend/sidebar/components/crm_contacts_spec.js
@@ -33,7 +33,7 @@ describe('Issue crm contacts component', () => {
[issueCrmContactsSubscription, subscriptionHandler],
]);
wrapper = shallowMountExtended(CrmContacts, {
- propsData: { issueId: '123' },
+ propsData: { issueId: '123', groupIssuesPath: '/groups/flightjs/-/issues' },
apolloProvider: fakeApollo,
});
};
@@ -71,8 +71,14 @@ describe('Issue crm contacts component', () => {
await waitForPromises();
expect(wrapper.find('#contact_0').text()).toContain('Someone Important');
+ expect(wrapper.find('#contact_0').attributes('href')).toBe(
+ '/groups/flightjs/-/issues?crm_contact_id=1',
+ );
expect(wrapper.find('#contact_container_0').text()).toContain('si@gitlab.com');
expect(wrapper.find('#contact_1').text()).toContain('Marty McFly');
+ expect(wrapper.find('#contact_1').attributes('href')).toBe(
+ '/groups/flightjs/-/issues?crm_contact_id=5',
+ );
});
it('renders correct results after subscription update', async () => {
@@ -83,5 +89,8 @@ describe('Issue crm contacts component', () => {
contact.forEach((property) => {
expect(wrapper.find('#contact_container_0').text()).toContain(property);
});
+ expect(wrapper.find('#contact_0').attributes('href')).toBe(
+ '/groups/flightjs/-/issues?crm_contact_id=13',
+ );
});
});
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 d0792fa7b73..8999f120a0f 100644
--- a/spec/frontend/sidebar/components/reviewers/uncollapsed_reviewer_list_spec.js
+++ b/spec/frontend/sidebar/components/reviewers/uncollapsed_reviewer_list_spec.js
@@ -42,9 +42,8 @@ describe('UncollapsedReviewerList component', () => {
expect(wrapper.findAll(ReviewerAvatarLink).length).toBe(1);
});
- it('shows one user with avatar, username and author name', () => {
+ it('shows one user with avatar, and author name', () => {
expect(wrapper.text()).toContain(user.name);
- expect(wrapper.text()).toContain(`@root`);
});
it('renders re-request loading icon', async () => {
@@ -84,11 +83,9 @@ describe('UncollapsedReviewerList component', () => {
expect(wrapper.findAll(ReviewerAvatarLink).length).toBe(2);
});
- it('shows both users with avatar, username and author name', () => {
+ it('shows both users with avatar, and author name', () => {
expect(wrapper.text()).toContain(user.name);
- expect(wrapper.text()).toContain(`@root`);
expect(wrapper.text()).toContain(user2.name);
- expect(wrapper.text()).toContain(`@hello-world`);
});
it('renders approval icon', () => {
diff --git a/spec/frontend/sidebar/components/time_tracking/mock_data.js b/spec/frontend/sidebar/components/time_tracking/mock_data.js
index 3f1b3fa8ec1..ba2781118d9 100644
--- a/spec/frontend/sidebar/components/time_tracking/mock_data.js
+++ b/spec/frontend/sidebar/components/time_tracking/mock_data.js
@@ -9,6 +9,7 @@ export const getIssueTimelogsQueryResponse = {
nodes: [
{
__typename: 'Timelog',
+ id: 'gid://gitlab/Timelog/18',
timeSpent: 14400,
user: {
id: 'user-1',
@@ -25,6 +26,7 @@ export const getIssueTimelogsQueryResponse = {
},
{
__typename: 'Timelog',
+ id: 'gid://gitlab/Timelog/20',
timeSpent: 1800,
user: {
id: 'user-2',
@@ -37,6 +39,7 @@ export const getIssueTimelogsQueryResponse = {
},
{
__typename: 'Timelog',
+ id: 'gid://gitlab/Timelog/25',
timeSpent: 14400,
user: {
id: 'user-2',
@@ -68,6 +71,7 @@ export const getMrTimelogsQueryResponse = {
nodes: [
{
__typename: 'Timelog',
+ id: 'gid://gitlab/Timelog/13',
timeSpent: 1800,
user: {
id: 'user-1',
@@ -84,6 +88,7 @@ export const getMrTimelogsQueryResponse = {
},
{
__typename: 'Timelog',
+ id: 'gid://gitlab/Timelog/22',
timeSpent: 3600,
user: {
id: 'user-1',
@@ -96,6 +101,7 @@ export const getMrTimelogsQueryResponse = {
},
{
__typename: 'Timelog',
+ id: 'gid://gitlab/Timelog/64',
timeSpent: 300,
user: {
id: 'user-1',
diff --git a/spec/frontend/sidebar/lock/issuable_lock_form_spec.js b/spec/frontend/sidebar/lock/issuable_lock_form_spec.js
index 7bf7e563a01..8478d3d674d 100644
--- a/spec/frontend/sidebar/lock/issuable_lock_form_spec.js
+++ b/spec/frontend/sidebar/lock/issuable_lock_form_spec.js
@@ -37,6 +37,9 @@ describe('IssuableLockForm', () => {
const createComponent = ({ props = {} }) => {
wrapper = shallowMount(IssuableLockForm, {
store,
+ provide: {
+ fullPath: '',
+ },
propsData: {
isEditable: true,
...props,
diff --git a/spec/frontend/sidebar/reviewers_spec.js b/spec/frontend/sidebar/reviewers_spec.js
index fc24b51287f..351dfc9a6ed 100644
--- a/spec/frontend/sidebar/reviewers_spec.js
+++ b/spec/frontend/sidebar/reviewers_spec.js
@@ -146,7 +146,6 @@ describe('Reviewer component', () => {
const userItems = wrapper.findAll('[data-testid="reviewer"]');
expect(userItems.length).toBe(3);
- expect(userItems.at(0).find('a').attributes('title')).toBe(users[2].name);
});
it('passes the sorted reviewers to the collapsed-reviewer-list', () => {
diff --git a/spec/frontend/sidebar/sidebar_mediator_spec.js b/spec/frontend/sidebar/sidebar_mediator_spec.js
index c472a98bf0b..82fb10ab1d2 100644
--- a/spec/frontend/sidebar/sidebar_mediator_spec.js
+++ b/spec/frontend/sidebar/sidebar_mediator_spec.js
@@ -1,4 +1,5 @@
import MockAdapter from 'axios-mock-adapter';
+import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import * as urlUtility from '~/lib/utils/url_utility';
import SidebarService, { gqClient } from '~/sidebar/services/sidebar_service';
@@ -8,6 +9,7 @@ import toast from '~/vue_shared/plugins/global_toast';
import { refreshUserMergeRequestCounts } from '~/commons/nav/user_merge_requests';
import Mock from './mock_data';
+jest.mock('~/flash');
jest.mock('~/vue_shared/plugins/global_toast');
jest.mock('~/commons/nav/user_merge_requests');
@@ -122,25 +124,39 @@ describe('Sidebar mediator', () => {
});
describe('toggleAttentionRequested', () => {
- let attentionRequiredService;
+ let requestAttentionMock;
+ let removeAttentionRequestMock;
beforeEach(() => {
- attentionRequiredService = jest
- .spyOn(mediator.service, 'toggleAttentionRequested')
+ requestAttentionMock = jest.spyOn(mediator.service, 'requestAttention').mockResolvedValue();
+ removeAttentionRequestMock = jest
+ .spyOn(mediator.service, 'removeAttentionRequest')
.mockResolvedValue();
});
- it('calls attentionRequired service method', async () => {
- mediator.store.reviewers = [{ id: 1, attention_requested: false, username: 'root' }];
+ it.each`
+ attentionIsCurrentlyRequested | serviceMethod
+ ${true} | ${'remove'}
+ ${false} | ${'add'}
+ `(
+ "calls the $serviceMethod service method when the user's attention request is set to $attentionIsCurrentlyRequested",
+ async ({ serviceMethod }) => {
+ const methods = {
+ add: requestAttentionMock,
+ remove: removeAttentionRequestMock,
+ };
+ mediator.store.reviewers = [{ id: 1, attention_requested: false, username: 'root' }];
- await mediator.toggleAttentionRequested('reviewer', {
- user: { id: 1, username: 'root' },
- callback: jest.fn(),
- });
+ await mediator.toggleAttentionRequested('reviewer', {
+ user: { id: 1, username: 'root' },
+ callback: jest.fn(),
+ direction: serviceMethod,
+ });
- expect(attentionRequiredService).toHaveBeenCalledWith(1);
- expect(refreshUserMergeRequestCounts).toHaveBeenCalled();
- });
+ expect(methods[serviceMethod]).toHaveBeenCalledWith(1);
+ expect(refreshUserMergeRequestCounts).toHaveBeenCalled();
+ },
+ );
it.each`
type | method
@@ -172,5 +188,27 @@ describe('Sidebar mediator', () => {
expect(toast).toHaveBeenCalledWith(toastMessage);
},
);
+
+ describe('errors', () => {
+ beforeEach(() => {
+ jest
+ .spyOn(mediator.service, 'removeAttentionRequest')
+ .mockRejectedValueOnce(new Error('Something went wrong'));
+ });
+
+ it('shows an error message', async () => {
+ await mediator.toggleAttentionRequested('reviewer', {
+ user: { id: 1, username: 'root' },
+ callback: jest.fn(),
+ direction: 'remove',
+ });
+
+ expect(createFlash).toHaveBeenCalledWith(
+ expect.objectContaining({
+ message: 'Updating the attention request for root failed.',
+ }),
+ );
+ });
+ });
});
});
diff --git a/spec/frontend/single_file_diff_spec.js b/spec/frontend/single_file_diff_spec.js
index 8718152655f..6f42ec47458 100644
--- a/spec/frontend/single_file_diff_spec.js
+++ b/spec/frontend/single_file_diff_spec.js
@@ -1,6 +1,6 @@
import MockAdapter from 'axios-mock-adapter';
import $ from 'jquery';
-import { setHTMLFixture } from 'helpers/fixtures';
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import axios from '~/lib/utils/axios_utils';
import SingleFileDiff from '~/single_file_diff';
@@ -15,6 +15,7 @@ describe('SingleFileDiff', () => {
afterEach(() => {
mock.restore();
+ resetHTMLFixture();
});
it('loads diff via axios exactly once for collapsed diffs', async () => {
diff --git a/spec/frontend/smart_interval_spec.js b/spec/frontend/smart_interval_spec.js
index 1a2fd7ff8f1..5dda097ae6a 100644
--- a/spec/frontend/smart_interval_spec.js
+++ b/spec/frontend/smart_interval_spec.js
@@ -1,5 +1,6 @@
import $ from 'jquery';
import { assignIn } from 'lodash';
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import waitForPromises from 'helpers/wait_for_promises';
import SmartInterval from '~/smart_interval';
@@ -116,11 +117,15 @@ describe('SmartInterval', () => {
describe('DOM Events', () => {
beforeEach(() => {
// This ensures DOM and DOM events are initialized for these specs.
- setFixtures('<div></div>');
+ setHTMLFixture('<div></div>');
interval = createDefaultSmartInterval();
});
+ afterEach(() => {
+ resetHTMLFixture();
+ });
+
it('should pause when page is not visible', () => {
jest.runOnlyPendingTimers();
diff --git a/spec/frontend/snippet/collapsible_input_spec.js b/spec/frontend/snippet/collapsible_input_spec.js
index 3f14a9cd1a1..56e64d136c2 100644
--- a/spec/frontend/snippet/collapsible_input_spec.js
+++ b/spec/frontend/snippet/collapsible_input_spec.js
@@ -1,4 +1,4 @@
-import { setHTMLFixture } from 'helpers/fixtures';
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import setupCollapsibleInputs from '~/snippet/collapsible_input';
describe('~/snippet/collapsible_input', () => {
@@ -38,6 +38,10 @@ describe('~/snippet/collapsible_input', () => {
setupCollapsibleInputs();
});
+ afterEach(() => {
+ resetHTMLFixture();
+ });
+
const findInput = (el) => el.querySelector('textarea,input');
const findCollapsed = (el) => el.querySelector('.js-collapsed');
const findExpanded = (el) => el.querySelector('.js-expanded');
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 2b26c306c68..fec300ddd7e 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
@@ -28,9 +28,9 @@ exports[`Snippet Description Edit component rendering matches the snapshot 1`] =
data-uploads-path=""
>
<markdown-header-stub
- data-testid="markdownHeader"
enablepreview="true"
linecontent=""
+ restrictedtoolbaritems=""
suggestionstartindex="0"
/>
@@ -81,6 +81,7 @@ exports[`Snippet Description Edit component rendering matches the snapshot 1`] =
canattachfile="true"
markdowndocspath="help/"
quickactionsdocspath=""
+ showcommenttoolbar="true"
/>
</div>
</div>
diff --git a/spec/frontend/snippets/components/edit_spec.js b/spec/frontend/snippets/components/edit_spec.js
index 9cfe136129a..8a767765149 100644
--- a/spec/frontend/snippets/components/edit_spec.js
+++ b/spec/frontend/snippets/components/edit_spec.js
@@ -1,5 +1,4 @@
-import { GlLoadingIcon } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
+import { GlFormGroup, GlLoadingIcon } from '@gitlab/ui';
import Vue, { nextTick } from 'vue';
import { merge } from 'lodash';
@@ -7,6 +6,7 @@ import VueApollo, { ApolloMutation } from 'vue-apollo';
import { useFakeDate } from 'helpers/fake_date';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import GetSnippetQuery from 'shared_queries/snippet/snippet.query.graphql';
import createFlash from '~/flash';
import * as urlUtils from '~/lib/utils/url_utility';
@@ -22,7 +22,6 @@ import {
import CreateSnippetMutation from '~/snippets/mutations/create_snippet.mutation.graphql';
import UpdateSnippetMutation from '~/snippets/mutations/update_snippet.mutation.graphql';
import FormFooterActions from '~/vue_shared/components/form/form_footer_actions.vue';
-import TitleField from '~/vue_shared/components/form/title.vue';
import { testEntries, createGQLSnippetsQueryResponse, createGQLSnippet } from '../test_utils';
jest.mock('~/flash');
@@ -112,19 +111,19 @@ describe('Snippet Edit app', () => {
gon.relative_url_root = originalRelativeUrlRoot;
});
- const findBlobActions = () => wrapper.find(SnippetBlobActionsEdit);
- const findSubmitButton = () => wrapper.find('[data-testid="snippet-submit-btn"]');
- const findCancelButton = () => wrapper.find('[data-testid="snippet-cancel-btn"]');
- const hasDisabledSubmit = () => Boolean(findSubmitButton().attributes('disabled'));
- const clickSubmitBtn = () => wrapper.find('[data-testid="snippet-edit-form"]').trigger('submit');
+ const findBlobActions = () => wrapper.findComponent(SnippetBlobActionsEdit);
+ const findCancelButton = () => wrapper.findByTestId('snippet-cancel-btn');
+ const clickSubmitBtn = () => wrapper.findByTestId('snippet-edit-form').trigger('submit');
+
const triggerBlobActions = (actions) => findBlobActions().vm.$emit('actions', actions);
const setUploadFilesHtml = (paths) => {
wrapper.vm.$el.innerHTML = paths
.map((path) => `<input name="files[]" value="${path}">`)
.join('');
};
- const setTitle = (val) => wrapper.find(TitleField).vm.$emit('input', val);
- const setDescription = (val) => wrapper.find(SnippetDescriptionEdit).vm.$emit('input', val);
+ const setTitle = (val) => wrapper.findByTestId('snippet-title-input').vm.$emit('input', val);
+ const setDescription = (val) =>
+ wrapper.findComponent(SnippetDescriptionEdit).vm.$emit('input', val);
const createComponent = ({ props = {}, selectedLevel = SNIPPET_VISIBILITY_PRIVATE } = {}) => {
if (wrapper) {
@@ -139,7 +138,7 @@ describe('Snippet Edit app', () => {
];
const apolloProvider = createMockApollo(requestHandlers);
- wrapper = shallowMount(SnippetEditApp, {
+ wrapper = shallowMountExtended(SnippetEditApp, {
apolloProvider,
stubs: {
ApolloMutation,
@@ -177,7 +176,7 @@ describe('Snippet Edit app', () => {
it('renders loader', () => {
createComponent();
- expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
+ expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
});
});
@@ -193,10 +192,10 @@ describe('Snippet Edit app', () => {
});
it('should render components', () => {
- expect(wrapper.find(TitleField).exists()).toBe(true);
- expect(wrapper.find(SnippetDescriptionEdit).exists()).toBe(true);
- expect(wrapper.find(SnippetVisibilityEdit).exists()).toBe(true);
- expect(wrapper.find(FormFooterActions).exists()).toBe(true);
+ expect(wrapper.findComponent(GlFormGroup).attributes('label')).toEqual('Title');
+ expect(wrapper.findComponent(SnippetDescriptionEdit).exists()).toBe(true);
+ expect(wrapper.findComponent(SnippetVisibilityEdit).exists()).toBe(true);
+ expect(wrapper.findComponent(FormFooterActions).exists()).toBe(true);
expect(findBlobActions().exists()).toBe(true);
});
@@ -207,25 +206,34 @@ describe('Snippet Edit app', () => {
describe('default', () => {
it.each`
- title | actions | shouldDisable
- ${''} | ${[]} | ${true}
- ${''} | ${[TEST_ACTIONS.VALID]} | ${true}
- ${'foo'} | ${[]} | ${false}
- ${'foo'} | ${[TEST_ACTIONS.VALID]} | ${false}
- ${'foo'} | ${[TEST_ACTIONS.VALID, TEST_ACTIONS.NO_CONTENT]} | ${true}
- ${'foo'} | ${[TEST_ACTIONS.VALID, TEST_ACTIONS.NO_PATH]} | ${false}
+ title | actions | titleHasErrors | blobActionsHasErrors
+ ${''} | ${[]} | ${true} | ${false}
+ ${''} | ${[TEST_ACTIONS.VALID]} | ${true} | ${false}
+ ${'foo'} | ${[]} | ${false} | ${false}
+ ${'foo'} | ${[TEST_ACTIONS.VALID]} | ${false} | ${false}
+ ${'foo'} | ${[TEST_ACTIONS.VALID, TEST_ACTIONS.NO_CONTENT]} | ${false} | ${true}
+ ${'foo'} | ${[TEST_ACTIONS.VALID, TEST_ACTIONS.NO_PATH]} | ${false} | ${false}
`(
- 'should handle submit disable (title="$title", actions="$actions", shouldDisable="$shouldDisable")',
- async ({ title, actions, shouldDisable }) => {
+ 'validates correctly (title="$title", actions="$actions", titleHasErrors="$titleHasErrors", blobActionsHasErrors="$blobActionsHasErrors")',
+ async ({ title, actions, titleHasErrors, blobActionsHasErrors }) => {
getSpy.mockResolvedValue(createQueryResponse({ title }));
await createComponentAndLoad();
triggerBlobActions(actions);
+ clickSubmitBtn();
+
await nextTick();
- expect(hasDisabledSubmit()).toBe(shouldDisable);
+ expect(wrapper.findComponent(GlFormGroup).exists()).toBe(true);
+ expect(Boolean(wrapper.findComponent(GlFormGroup).attributes('state'))).toEqual(
+ !titleHasErrors,
+ );
+
+ expect(wrapper.find(SnippetBlobActionsEdit).props('isValid')).toEqual(
+ !blobActionsHasErrors,
+ );
},
);
@@ -262,35 +270,64 @@ describe('Snippet Edit app', () => {
);
describe('form submission handling', () => {
- it.each`
- snippetGid | projectPath | uploadedFiles | input | mutationType
- ${''} | ${'project/path'} | ${[]} | ${{ ...getApiData(), projectPath: 'project/path', uploadedFiles: [] }} | ${'createSnippet'}
- ${''} | ${''} | ${[]} | ${{ ...getApiData(), projectPath: '', uploadedFiles: [] }} | ${'createSnippet'}
- ${''} | ${''} | ${TEST_UPLOADED_FILES} | ${{ ...getApiData(), projectPath: '', uploadedFiles: TEST_UPLOADED_FILES }} | ${'createSnippet'}
- ${TEST_SNIPPET_GID} | ${'project/path'} | ${[]} | ${getApiData(createSnippet())} | ${'updateSnippet'}
- ${TEST_SNIPPET_GID} | ${''} | ${[]} | ${getApiData(createSnippet())} | ${'updateSnippet'}
- `(
- 'should submit mutation $mutationType (snippetGid=$snippetGid, projectPath=$projectPath, uploadedFiles=$uploadedFiles)',
- async ({ snippetGid, projectPath, uploadedFiles, mutationType, input }) => {
- await createComponentAndLoad({
- props: {
- snippetGid,
- projectPath,
- },
- });
-
- setUploadFilesHtml(uploadedFiles);
-
- await nextTick();
-
- clickSubmitBtn();
+ describe('when creating a new snippet', () => {
+ it.each`
+ projectPath | uploadedFiles | input
+ ${''} | ${TEST_UPLOADED_FILES} | ${{ ...getApiData({ title: 'Title' }), projectPath: '', uploadedFiles: TEST_UPLOADED_FILES }}
+ ${'project/path'} | ${TEST_UPLOADED_FILES} | ${{ ...getApiData({ title: 'Title' }), projectPath: 'project/path', uploadedFiles: TEST_UPLOADED_FILES }}
+ `(
+ 'should submit a createSnippet mutation (projectPath=$projectPath, uploadedFiles=$uploadedFiles)',
+ async ({ projectPath, uploadedFiles, input }) => {
+ await createComponentAndLoad({
+ props: {
+ snippetGid: '',
+ projectPath,
+ },
+ });
+
+ setTitle(input.title);
+ setUploadFilesHtml(uploadedFiles);
+
+ await nextTick();
+
+ clickSubmitBtn();
+
+ expect(mutateSpy).toHaveBeenCalledTimes(1);
+ expect(mutateSpy).toHaveBeenCalledWith('createSnippet', {
+ input,
+ });
+ },
+ );
+ });
- expect(mutateSpy).toHaveBeenCalledTimes(1);
- expect(mutateSpy).toHaveBeenCalledWith(mutationType, {
- input,
- });
- },
- );
+ describe('when updating a snippet', () => {
+ it.each`
+ projectPath | uploadedFiles | input
+ ${''} | ${[]} | ${getApiData(createSnippet())}
+ ${'project/path'} | ${[]} | ${getApiData(createSnippet())}
+ `(
+ 'should submit an updateSnippet mutation (projectPath=$projectPath, uploadedFiles=$uploadedFiles)',
+ async ({ projectPath, uploadedFiles, input }) => {
+ await createComponentAndLoad({
+ props: {
+ snippetGid: TEST_SNIPPET_GID,
+ projectPath,
+ },
+ });
+
+ setUploadFilesHtml(uploadedFiles);
+
+ await nextTick();
+
+ clickSubmitBtn();
+
+ expect(mutateSpy).toHaveBeenCalledTimes(1);
+ expect(mutateSpy).toHaveBeenCalledWith('updateSnippet', {
+ input,
+ });
+ },
+ );
+ });
it('should redirect to snippet view on successful mutation', async () => {
await createComponentAndSubmit();
@@ -298,30 +335,55 @@ describe('Snippet Edit app', () => {
expect(urlUtils.redirectTo).toHaveBeenCalledWith(TEST_WEB_URL);
});
- it.each`
- snippetGid | projectPath | mutationRes | expectMessage
- ${''} | ${'project/path'} | ${createMutationResponseWithErrors('createSnippet')} | ${`Can't create snippet: ${TEST_MUTATION_ERROR}`}
- ${''} | ${''} | ${createMutationResponseWithErrors('createSnippet')} | ${`Can't create snippet: ${TEST_MUTATION_ERROR}`}
- ${TEST_SNIPPET_GID} | ${'project/path'} | ${createMutationResponseWithErrors('updateSnippet')} | ${`Can't update snippet: ${TEST_MUTATION_ERROR}`}
- ${TEST_SNIPPET_GID} | ${''} | ${createMutationResponseWithErrors('updateSnippet')} | ${`Can't update snippet: ${TEST_MUTATION_ERROR}`}
- `(
- 'should flash error with (snippet=$snippetGid, projectPath=$projectPath)',
- async ({ snippetGid, projectPath, mutationRes, expectMessage }) => {
- mutateSpy.mockResolvedValue(mutationRes);
-
- await createComponentAndSubmit({
- props: {
- projectPath,
- snippetGid,
- },
+ describe('when there are errors after creating a new snippet', () => {
+ it.each`
+ projectPath
+ ${'project/path'}
+ ${''}
+ `('should flash error (projectPath=$projectPath)', async ({ projectPath }) => {
+ mutateSpy.mockResolvedValue(createMutationResponseWithErrors('createSnippet'));
+
+ await createComponentAndLoad({
+ props: { projectPath, snippetGid: '' },
});
+ setTitle('Title');
+
+ clickSubmitBtn();
+
+ await waitForPromises();
+
expect(urlUtils.redirectTo).not.toHaveBeenCalled();
expect(createFlash).toHaveBeenCalledWith({
- message: expectMessage,
+ message: `Can't create snippet: ${TEST_MUTATION_ERROR}`,
});
- },
- );
+ });
+ });
+
+ describe('when there are errors after updating a snippet', () => {
+ it.each`
+ projectPath
+ ${'project/path'}
+ ${''}
+ `(
+ 'should flash error with (snippet=$snippetGid, projectPath=$projectPath)',
+ async ({ projectPath }) => {
+ mutateSpy.mockResolvedValue(createMutationResponseWithErrors('updateSnippet'));
+
+ await createComponentAndSubmit({
+ props: {
+ projectPath,
+ snippetGid: TEST_SNIPPET_GID,
+ },
+ });
+
+ expect(urlUtils.redirectTo).not.toHaveBeenCalled();
+ expect(createFlash).toHaveBeenCalledWith({
+ message: `Can't update snippet: ${TEST_MUTATION_ERROR}`,
+ });
+ },
+ );
+ });
describe('with apollo network error', () => {
beforeEach(async () => {
@@ -382,6 +444,7 @@ describe('Snippet Edit app', () => {
false,
() => {
triggerBlobActions([testEntries.updated.diff]);
+ setTitle('test');
clickSubmitBtn();
},
],
diff --git a/spec/frontend/snippets/components/snippet_blob_actions_edit_spec.js b/spec/frontend/snippets/components/snippet_blob_actions_edit_spec.js
index 8174ba5c693..df98312b498 100644
--- a/spec/frontend/snippets/components/snippet_blob_actions_edit_spec.js
+++ b/spec/frontend/snippets/components/snippet_blob_actions_edit_spec.js
@@ -1,6 +1,7 @@
-import { shallowMount } from '@vue/test-utils';
import { times } from 'lodash';
import { nextTick } from 'vue';
+import { GlFormGroup } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
import SnippetBlobActionsEdit from '~/snippets/components/snippet_blob_actions_edit.vue';
import SnippetBlobEdit from '~/snippets/components/snippet_blob_edit.vue';
import {
@@ -8,6 +9,7 @@ import {
SNIPPET_BLOB_ACTION_CREATE,
SNIPPET_BLOB_ACTION_MOVE,
} from '~/snippets/constants';
+import { s__ } from '~/locale';
import { testEntries, createBlobFromTestEntry } from '../test_utils';
const TEST_BLOBS = [
@@ -29,7 +31,7 @@ describe('snippets/components/snippet_blob_actions_edit', () => {
});
};
- const findLabel = () => wrapper.find('label');
+ const findLabel = () => wrapper.findComponent(GlFormGroup);
const findBlobEdits = () => wrapper.findAll(SnippetBlobEdit);
const findBlobsData = () =>
findBlobEdits().wrappers.map((x) => ({
@@ -65,7 +67,7 @@ describe('snippets/components/snippet_blob_actions_edit', () => {
});
it('renders label', () => {
- expect(findLabel().text()).toBe('Files');
+ expect(findLabel().attributes('label')).toBe('Files');
});
it(`renders delete button (show=true)`, () => {
@@ -280,4 +282,32 @@ describe('snippets/components/snippet_blob_actions_edit', () => {
expect(findAddButton().props('disabled')).toBe(true);
});
});
+
+ describe('isValid prop', () => {
+ const validationMessage = s__(
+ "Snippets|Snippets can't contain empty files. Ensure all files have content, or delete them.",
+ );
+
+ describe('when not present', () => {
+ it('sets the label validation state to true', () => {
+ createComponent();
+
+ const label = findLabel();
+
+ expect(Boolean(label.attributes('state'))).toEqual(true);
+ expect(label.attributes('invalid-feedback')).toEqual(validationMessage);
+ });
+ });
+
+ describe('when present', () => {
+ it('sets the label validation state to the value', () => {
+ createComponent({ isValid: false });
+
+ const label = findLabel();
+
+ expect(Boolean(label.attributes('state'))).toEqual(false);
+ expect(label.attributes('invalid-feedback')).toEqual(validationMessage);
+ });
+ });
+ });
});
diff --git a/spec/frontend/syntax_highlight_spec.js b/spec/frontend/syntax_highlight_spec.js
index 8ad4f8d5c70..1be6c213350 100644
--- a/spec/frontend/syntax_highlight_spec.js
+++ b/spec/frontend/syntax_highlight_spec.js
@@ -1,6 +1,6 @@
/* eslint-disable no-return-assign */
-
import $ from 'jquery';
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import syntaxHighlight from '~/syntax_highlight';
describe('Syntax Highlighter', () => {
@@ -20,7 +20,11 @@ describe('Syntax Highlighter', () => {
`('highlight using $desc syntax', ({ fn }) => {
describe('on a js-syntax-highlight element', () => {
beforeEach(() => {
- setFixtures('<div class="js-syntax-highlight"></div>');
+ setHTMLFixture('<div class="js-syntax-highlight"></div>');
+ });
+
+ afterEach(() => {
+ resetHTMLFixture();
});
it('applies syntax highlighting', () => {
@@ -33,11 +37,15 @@ describe('Syntax Highlighter', () => {
describe('on a parent element', () => {
beforeEach(() => {
- setFixtures(
+ setHTMLFixture(
'<div class="parent">\n <div class="js-syntax-highlight"></div>\n <div class="foo"></div>\n <div class="js-syntax-highlight"></div>\n</div>',
);
});
+ afterEach(() => {
+ resetHTMLFixture();
+ });
+
it('applies highlighting to all applicable children', () => {
stubUserColorScheme('monokai');
syntaxHighlight(fn('.parent'));
@@ -49,7 +57,7 @@ describe('Syntax Highlighter', () => {
});
it('prevents an infinite loop when no matches exist', () => {
- setFixtures('<div></div>');
+ setHTMLFixture('<div></div>');
const highlight = () => syntaxHighlight(fn('div'));
expect(highlight).not.toThrow();
diff --git a/spec/frontend/tabs/index_spec.js b/spec/frontend/tabs/index_spec.js
index 98617b404ff..67e3d707adb 100644
--- a/spec/frontend/tabs/index_spec.js
+++ b/spec/frontend/tabs/index_spec.js
@@ -1,6 +1,6 @@
import { GlTabsBehavior, TAB_SHOWN_EVENT } from '~/tabs';
import { ACTIVE_PANEL_CLASS, ACTIVE_TAB_CLASSES } from '~/tabs/constants';
-import { getFixture, setHTMLFixture } from 'helpers/fixtures';
+import { getFixture, setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
const tabsFixture = getFixture('tabs/tabs.html');
@@ -93,6 +93,8 @@ describe('GlTabsBehavior', () => {
describe('when given an element', () => {
afterEach(() => {
glTabs.destroy();
+
+ resetHTMLFixture();
});
beforeEach(() => {
@@ -250,6 +252,10 @@ describe('GlTabsBehavior', () => {
glTabs = new GlTabsBehavior(tabsEl);
});
+ afterEach(() => {
+ resetHTMLFixture();
+ });
+
it('connects the panels to their tabs correctly', () => {
findTab('bar').click();
diff --git a/spec/frontend/task_list_spec.js b/spec/frontend/task_list_spec.js
index fbdb73ae6de..e79c516a694 100644
--- a/spec/frontend/task_list_spec.js
+++ b/spec/frontend/task_list_spec.js
@@ -1,4 +1,5 @@
import $ from 'jquery';
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import axios from '~/lib/utils/axios_utils';
import TaskList from '~/task_list';
@@ -14,7 +15,7 @@ describe('TaskList', () => {
const createTaskList = () => new TaskList(taskListOptions);
beforeEach(() => {
- setFixtures(`
+ setHTMLFixture(`
<div class="task-list">
<div class="js-task-list-container">
<ul data-sourcepos="5:1-5:11" class="task-list" dir="auto">
@@ -37,6 +38,10 @@ describe('TaskList', () => {
taskList = createTaskList();
});
+ afterEach(() => {
+ resetHTMLFixture();
+ });
+
it('should call init when the class constructed', () => {
jest.spyOn(TaskList.prototype, 'init');
jest.spyOn(TaskList.prototype, 'disable').mockImplementation(() => {});
diff --git a/spec/frontend/tracking/tracking_spec.js b/spec/frontend/tracking/tracking_spec.js
index 665bf44fc77..08da3a9a465 100644
--- a/spec/frontend/tracking/tracking_spec.js
+++ b/spec/frontend/tracking/tracking_spec.js
@@ -76,6 +76,18 @@ describe('Tracking', () => {
);
});
+ it('returns `true` if the Snowplow library was called without issues', () => {
+ expect(Tracking.event(TEST_CATEGORY, TEST_ACTION)).toBe(true);
+ });
+
+ it('returns `false` if the Snowplow library throws an error', () => {
+ snowplowSpy.mockImplementation(() => {
+ throw new Error();
+ });
+
+ expect(Tracking.event(TEST_CATEGORY, TEST_ACTION)).toBe(false);
+ });
+
it('allows adding extra data to the default context', () => {
const extra = { foo: 'bar' };
diff --git a/spec/frontend/user_lists/components/user_lists_table_spec.js b/spec/frontend/user_lists/components/user_lists_table_spec.js
index 08eb8ae0843..fb5093eb065 100644
--- a/spec/frontend/user_lists/components/user_lists_table_spec.js
+++ b/spec/frontend/user_lists/components/user_lists_table_spec.js
@@ -2,6 +2,7 @@ import { GlModal } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import * as timeago from 'timeago.js';
import { nextTick } from 'vue';
+import { timeagoLanguageCode } from '~/lib/utils/datetime/timeago_utility';
import UserListsTable from '~/user_lists/components/user_lists_table.vue';
import { userList } from 'jest/feature_flags/mock_data';
@@ -31,7 +32,7 @@ describe('User Lists Table', () => {
userList.user_xids.replace(/,/g, ', '),
);
expect(wrapper.find('[data-testid="ffUserListTimestamp"]').text()).toBe('created 2 weeks ago');
- expect(timeago.format).toHaveBeenCalledWith(userList.created_at);
+ expect(timeago.format).toHaveBeenCalledWith(userList.created_at, timeagoLanguageCode);
});
it('should set the title for a tooltip on the created stamp', () => {
diff --git a/spec/frontend/user_popovers_spec.js b/spec/frontend/user_popovers_spec.js
index 745b66fd700..fa598716645 100644
--- a/spec/frontend/user_popovers_spec.js
+++ b/spec/frontend/user_popovers_spec.js
@@ -1,5 +1,13 @@
+import { within } from '@testing-library/dom';
+import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import UsersCache from '~/lib/utils/users_cache';
import initUserPopovers from '~/user_popovers';
+import waitForPromises from 'helpers/wait_for_promises';
+
+jest.mock('~/api/user_api', () => ({
+ followUser: jest.fn().mockResolvedValue({}),
+ unfollowUser: jest.fn().mockResolvedValue({}),
+}));
describe('User Popovers', () => {
const fixtureTemplate = 'merge_requests/merge_request_with_mentions.html';
@@ -19,7 +27,7 @@ describe('User Popovers', () => {
return link;
};
- const dummyUser = { name: 'root' };
+ const dummyUser = { name: 'root', username: 'root', is_followed: false };
const dummyUserStatus = { message: 'active' };
let popovers;
@@ -35,7 +43,7 @@ describe('User Popovers', () => {
};
beforeEach(() => {
- loadFixtures(fixtureTemplate);
+ loadHTMLFixture(fixtureTemplate);
const usersCacheSpy = () => Promise.resolve(dummyUser);
jest.spyOn(UsersCache, 'retrieveById').mockImplementation((userId) => usersCacheSpy(userId));
@@ -44,10 +52,15 @@ describe('User Popovers', () => {
jest
.spyOn(UsersCache, 'retrieveStatusById')
.mockImplementation((userId) => userStatusCacheSpy(userId));
+ jest.spyOn(UsersCache, 'updateById');
popovers = initUserPopovers(document.querySelectorAll(selector));
});
+ afterEach(() => {
+ resetHTMLFixture();
+ });
+
it('initializes a popover for each user link with a user id', () => {
const linksWithUsers = findFixtureLinks();
@@ -115,4 +128,32 @@ describe('User Popovers', () => {
expect(userLink.getAttribute('aria-describedby')).toBe(null);
});
+
+ it('updates toggle follow button and `UsersCache` when toggle follow button is clicked', async () => {
+ const [firstPopover] = popovers;
+ const withinFirstPopover = within(firstPopover.$el);
+ const findFollowButton = () => withinFirstPopover.queryByRole('button', { name: 'Follow' });
+ const findUnfollowButton = () => withinFirstPopover.queryByRole('button', { name: 'Unfollow' });
+
+ const userLink = document.querySelector(selector);
+ triggerEvent('mouseenter', userLink);
+
+ await waitForPromises();
+
+ const { userId } = document.querySelector(selector).dataset;
+
+ triggerEvent('click', findFollowButton());
+
+ await waitForPromises();
+
+ expect(findUnfollowButton()).not.toBe(null);
+ expect(UsersCache.updateById).toHaveBeenCalledWith(userId, { is_followed: true });
+
+ triggerEvent('click', findUnfollowButton());
+
+ await waitForPromises();
+
+ expect(findFollowButton()).not.toBe(null);
+ expect(UsersCache.updateById).toHaveBeenCalledWith(userId, { is_followed: false });
+ });
});
diff --git a/spec/frontend/vue_alerts_spec.js b/spec/frontend/vue_alerts_spec.js
index 1952eea4a01..de2faa09438 100644
--- a/spec/frontend/vue_alerts_spec.js
+++ b/spec/frontend/vue_alerts_spec.js
@@ -1,5 +1,5 @@
import { nextTick } from 'vue';
-import { setHTMLFixture } from 'helpers/fixtures';
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import { TEST_HOST } from 'helpers/test_constants';
import initVueAlerts from '~/vue_alerts';
@@ -40,6 +40,10 @@ describe('VueAlerts', () => {
);
});
+ afterEach(() => {
+ resetHTMLFixture();
+ });
+
const findJsHooks = () => document.querySelectorAll('.js-vue-alert');
const findAlerts = () => document.querySelectorAll('.gl-alert');
const findAlertDismiss = (alert) => alert.querySelector('.gl-dismiss-btn');
diff --git a/spec/frontend/vue_mr_widget/components/added_commit_message_spec.js b/spec/frontend/vue_mr_widget/components/added_commit_message_spec.js
new file mode 100644
index 00000000000..150680caa7e
--- /dev/null
+++ b/spec/frontend/vue_mr_widget/components/added_commit_message_spec.js
@@ -0,0 +1,31 @@
+import { shallowMount } from '@vue/test-utils';
+import AddedCommentMessage from '~/vue_merge_request_widget/components/added_commit_message.vue';
+
+let wrapper;
+
+function factory(propsData) {
+ wrapper = shallowMount(AddedCommentMessage, {
+ propsData: {
+ isFastForwardEnabled: false,
+ targetBranch: 'main',
+ ...propsData,
+ },
+ provide: {
+ glFeatures: {
+ restructuredMrWidget: true.valueOf,
+ },
+ },
+ });
+}
+
+describe('Widget added commit message', () => {
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('displays changes where not merged when state is closed', () => {
+ factory({ state: 'closed' });
+
+ expect(wrapper.element.outerHTML).toContain('The changes were not merged');
+ });
+});
diff --git a/spec/frontend/vue_mr_widget/components/extensions/utils_spec.js b/spec/frontend/vue_mr_widget/components/extensions/utils_spec.js
index 98cfc04eb25..5799799ad5e 100644
--- a/spec/frontend/vue_mr_widget/components/extensions/utils_spec.js
+++ b/spec/frontend/vue_mr_widget/components/extensions/utils_spec.js
@@ -2,16 +2,18 @@ import { generateText } from '~/vue_merge_request_widget/components/extensions/u
describe('generateText', () => {
it.each`
- text | expectedText
- ${'%{strong_start}Hello world%{strong_end}'} | ${'<span class="gl-font-weight-bold">Hello world</span>'}
- ${'%{success_start}Hello world%{success_end}'} | ${'<span class="gl-font-weight-bold gl-text-green-500">Hello world</span>'}
- ${'%{danger_start}Hello world%{danger_end}'} | ${'<span class="gl-font-weight-bold gl-text-red-500">Hello world</span>'}
- ${'%{critical_start}Hello world%{critical_end}'} | ${'<span class="gl-font-weight-bold gl-text-red-800">Hello world</span>'}
- ${'%{same_start}Hello world%{same_end}'} | ${'<span class="gl-font-weight-bold gl-text-gray-700">Hello world</span>'}
- ${'%{small_start}Hello world%{small_end}'} | ${'<span class="gl-font-sm gl-text-gray-700">Hello world</span>'}
- ${'%{strong_start}%{danger_start}Hello world%{danger_end}%{strong_end}'} | ${'<span class="gl-font-weight-bold"><span class="gl-font-weight-bold gl-text-red-500">Hello world</span></span>'}
- ${'%{no_exist_start}Hello world%{no_exist_end}'} | ${'Hello world'}
- ${['array']} | ${null}
+ text | expectedText
+ ${'%{strong_start}Hello world%{strong_end}'} | ${'<span class="gl-font-weight-bold">Hello world</span>'}
+ ${'%{success_start}Hello world%{success_end}'} | ${'<span class="gl-font-weight-bold gl-text-green-500">Hello world</span>'}
+ ${'%{danger_start}Hello world%{danger_end}'} | ${'<span class="gl-font-weight-bold gl-text-red-500">Hello world</span>'}
+ ${'%{critical_start}Hello world%{critical_end}'} | ${'<span class="gl-font-weight-bold gl-text-red-800">Hello world</span>'}
+ ${'%{same_start}Hello world%{same_end}'} | ${'<span class="gl-font-weight-bold gl-text-gray-700">Hello world</span>'}
+ ${'%{small_start}Hello world%{small_end}'} | ${'<span class="gl-font-sm gl-text-gray-700">Hello world</span>'}
+ ${'%{strong_start}%{danger_start}Hello world%{danger_end}%{strong_end}'} | ${'<span class="gl-font-weight-bold"><span class="gl-font-weight-bold gl-text-red-500">Hello world</span></span>'}
+ ${'%{no_exist_start}Hello world%{no_exist_end}'} | ${'Hello world'}
+ ${{ text: 'Hello world', href: 'http://www.example.com' }} | ${'<a class="gl-text-decoration-underline" href="http://www.example.com">Hello world</a>'}
+ ${{ prependText: 'Hello', text: 'world', href: 'http://www.example.com' }} | ${'Hello <a class="gl-text-decoration-underline" href="http://www.example.com">world</a>'}
+ ${['array']} | ${null}
`('generates $expectedText from $text', ({ text, expectedText }) => {
expect(generateText(text)).toBe(expectedText);
});
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 5a1f17573d4..ed6dc598845 100644
--- a/spec/frontend/vue_mr_widget/components/mr_widget_header_spec.js
+++ b/spec/frontend/vue_mr_widget/components/mr_widget_header_spec.js
@@ -1,7 +1,5 @@
import { shallowMount, mount } from '@vue/test-utils';
-import { nextTick } from 'vue';
import Header from '~/vue_merge_request_widget/components/mr_widget_header.vue';
-import WebIdeLink from '~/vue_shared/components/web_ide_link.vue';
describe('MRWidgetHeader', () => {
let wrapper;
@@ -17,16 +15,6 @@ describe('MRWidgetHeader', () => {
gon.relative_url_root = '';
});
- const expectDownloadDropdownItems = () => {
- const downloadEmailPatchesEl = wrapper.find('.js-download-email-patches');
- const downloadPlainDiffEl = wrapper.find('.js-download-plain-diff');
-
- expect(downloadEmailPatchesEl.text().trim()).toBe('Email patches');
- expect(downloadEmailPatchesEl.attributes('href')).toBe('/mr/email-patches');
- expect(downloadPlainDiffEl.text().trim()).toBe('Plain diff');
- expect(downloadPlainDiffEl.attributes('href')).toBe('/mr/plainDiffPath');
- };
-
const commonMrProps = {
divergedCommitsCount: 1,
sourceBranch: 'mr-widget-refactor',
@@ -36,8 +24,6 @@ describe('MRWidgetHeader', () => {
statusPath: 'abc',
};
- const findWebIdeButton = () => wrapper.findComponent(WebIdeLink);
-
describe('computed', () => {
describe('shouldShowCommitsBehindText', () => {
it('return true when there are divergedCommitsCount', () => {
@@ -133,136 +119,6 @@ describe('MRWidgetHeader', () => {
});
});
- describe('with an open merge request', () => {
- const mrDefaultOptions = {
- iid: 1,
- divergedCommitsCount: 12,
- sourceBranch: 'mr-widget-refactor',
- sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">mr-widget-refactor</a>',
- sourceBranchRemoved: false,
- targetBranchPath: 'foo/bar/commits-path',
- targetBranchTreePath: 'foo/bar/tree/path',
- targetBranch: 'main',
- isOpen: true,
- canPushToSourceBranch: true,
- emailPatchesPath: '/mr/email-patches',
- plainDiffPath: '/mr/plainDiffPath',
- statusPath: 'abc',
- sourceProjectFullPath: 'root/gitlab-ce',
- targetProjectFullPath: 'gitlab-org/gitlab-ce',
- 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', () => {
- createComponent({
- mr: { ...mrDefaultOptions },
- });
-
- const button = wrapper.find('.js-check-out-branch');
-
- expect(button.text().trim()).toBe('Check out branch');
- });
-
- it.each([
- [
- 'renders web ide button',
- {
- mrProps: {},
- relativeUrl: '',
- webIdeUrl:
- '/-/ide/project/root/gitlab-ce/merge_requests/1?target_project=gitlab-org%2Fgitlab-ce',
- },
- ],
- [
- 'renders web ide button with blank target_project, when mr has same target project',
- {
- mrProps: { targetProjectFullPath: 'root/gitlab-ce' },
- relativeUrl: '',
- webIdeUrl: '/-/ide/project/root/gitlab-ce/merge_requests/1?target_project=',
- },
- ],
- [
- 'renders web ide button with relative url',
- {
- mrProps: { iid: 2 },
- relativeUrl: '/gitlab',
- webIdeUrl:
- '/gitlab/-/ide/project/root/gitlab-ce/merge_requests/2?target_project=gitlab-org%2Fgitlab-ce',
- },
- ],
- ])('%s', async (_, { mrProps, relativeUrl, webIdeUrl }) => {
- gon.relative_url_root = relativeUrl;
- createComponent({
- mr: { ...mrDefaultOptions, ...mrProps },
- });
-
- await nextTick();
-
- expect(findWebIdeButton().props()).toMatchObject({
- showEditButton: false,
- showWebIdeButton: true,
- webIdeText: 'Open in Web IDE',
- gitpodText: 'Open in Gitpod',
- gitpodEnabled: true,
- showGitpodButton: true,
- gitpodUrl: 'http://gitpod.localhost',
- userPreferencesGitpodPath: mrDefaultOptions.userPreferencesGitpodPath,
- userProfileEnableGitpodPath: mrDefaultOptions.userProfileEnableGitpodPath,
- webIdeUrl,
- });
- });
-
- it('does not render web ide button if source branch is removed', async () => {
- createComponent({ mr: { ...mrDefaultOptions, sourceBranchRemoved: true } });
-
- await nextTick();
-
- expect(findWebIdeButton().exists()).toBe(false);
- });
-
- it('renders download dropdown with links', () => {
- createComponent({
- mr: { ...mrDefaultOptions },
- });
-
- expectDownloadDropdownItems();
- });
- });
-
- describe('with a closed merge request', () => {
- beforeEach(() => {
- createComponent({
- mr: {
- divergedCommitsCount: 12,
- sourceBranch: 'mr-widget-refactor',
- sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">mr-widget-refactor</a>',
- sourceBranchRemoved: false,
- targetBranchPath: 'foo/bar/commits-path',
- targetBranchTreePath: 'foo/bar/tree/path',
- targetBranch: 'main',
- isOpen: false,
- emailPatchesPath: '/mr/email-patches',
- plainDiffPath: '/mr/plainDiffPath',
- statusPath: 'abc',
- },
- });
- });
-
- it('does not render checkout branch button with modal trigger', () => {
- const button = wrapper.find('.js-check-out-branch');
-
- expect(button.exists()).toBe(false);
- });
-
- it('renders download dropdown with links', () => {
- expectDownloadDropdownItems();
- });
- });
-
describe('without diverged commits', () => {
beforeEach(() => {
createComponent({
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 0e364eb6800..da3a323e8ea 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
@@ -59,6 +59,7 @@ const createTestMr = (customConfig) => {
mergeImmediatelyDocsPath: 'path/to/merge/immediately/docs',
transitionStateMachine: (transition) => eventHub.$emit('StateMachineValueChanged', transition),
translateStateToMachine: () => this.transitionStateMachine(),
+ state: 'open',
};
Object.assign(mr, customConfig.mr);
@@ -321,7 +322,6 @@ describe('ReadyToMerge', () => {
await waitForPromises();
- expect(wrapper.vm.isMakingRequest).toBeTruthy();
expect(eventHub.$emit).toHaveBeenCalledWith('MRWidgetUpdateRequested');
expect(eventHub.$emit).toHaveBeenCalledWith('StateMachineValueChanged', {
transition: 'start-auto-merge',
@@ -348,7 +348,6 @@ describe('ReadyToMerge', () => {
await waitForPromises();
- expect(wrapper.vm.isMakingRequest).toBeTruthy();
expect(eventHub.$emit).toHaveBeenCalledWith('FailedToMerge', undefined);
const params = wrapper.vm.service.merge.mock.calls[0][0];
@@ -371,7 +370,6 @@ describe('ReadyToMerge', () => {
await waitForPromises();
- expect(wrapper.vm.isMakingRequest).toBeTruthy();
expect(wrapper.vm.mr.transitionStateMachine).toHaveBeenCalledWith({
transition: 'start-merge',
});
diff --git a/spec/frontend/vue_mr_widget/extensions/test_report/index_spec.js b/spec/frontend/vue_mr_widget/extensions/test_report/index_spec.js
index 88b8e32bd5d..2bc6860743a 100644
--- a/spec/frontend/vue_mr_widget/extensions/test_report/index_spec.js
+++ b/spec/frontend/vue_mr_widget/extensions/test_report/index_spec.js
@@ -16,6 +16,7 @@ import newErrorsTestReports from 'jest/reports/mock_data/new_errors_report.json'
import newFailedTestReports from 'jest/reports/mock_data/new_failures_report.json';
import successTestReports from 'jest/reports/mock_data/no_failures_report.json';
import resolvedFailures from 'jest/reports/mock_data/resolved_failures.json';
+import recentFailures from 'jest/reports/mock_data/recent_failures_report.json';
const reportWithParsingErrors = failedReport;
reportWithParsingErrors.suites[0].suite_errors = {
@@ -101,6 +102,17 @@ describe('Test report extension', () => {
expect(wrapper.text()).toContain(expectedResult);
});
+ it('displays report level recently failed count', async () => {
+ mockApi(httpStatusCodes.OK, recentFailures);
+ createComponent();
+
+ await waitForPromises();
+
+ expect(wrapper.text()).toContain(
+ '2 out of 3 failed tests have failed more than once in the last 14 days',
+ );
+ });
+
it('displays a link to the full report', async () => {
mockApi(httpStatusCodes.OK);
createComponent();
@@ -125,10 +137,10 @@ describe('Test report extension', () => {
it('displays summary for each suite', async () => {
await createExpandedWidgetWithData();
- expect(trimText(findAllExtensionListItems().at(0).text())).toBe(
+ expect(trimText(findAllExtensionListItems().at(0).text())).toContain(
'rspec:pg: 1 failed and 2 fixed test results, 8 total tests',
);
- expect(trimText(findAllExtensionListItems().at(1).text())).toBe(
+ expect(trimText(findAllExtensionListItems().at(1).text())).toContain(
'java ant: 1 failed, 3 total tests',
);
});
@@ -145,5 +157,37 @@ describe('Test report extension', () => {
'Base report parsing error: JUnit data parsing failed: string not matched',
);
});
+
+ it('displays suite level recently failed count', async () => {
+ await createExpandedWidgetWithData(recentFailures);
+
+ expect(trimText(findAllExtensionListItems().at(0).text())).toContain(
+ '1 out of 2 failed tests has failed more than once in the last 14 days',
+ );
+ expect(trimText(findAllExtensionListItems().at(1).text())).toContain(
+ '1 out of 1 failed test has failed more than once in the last 14 days',
+ );
+ });
+
+ it('displays the list of failed and fixed tests', async () => {
+ await createExpandedWidgetWithData();
+
+ const firstSuite = trimText(findAllExtensionListItems().at(0).text());
+ const secondSuite = trimText(findAllExtensionListItems().at(1).text());
+
+ expect(firstSuite).toContain('Test#subtract when a is 2 and b is 1 returns correct result');
+ expect(firstSuite).toContain('Test#sum when a is 1 and b is 2 returns summary');
+ expect(firstSuite).toContain('Test#sum when a is 100 and b is 200 returns summary');
+
+ expect(secondSuite).toContain('sumTest');
+ });
+
+ it('displays the test level recently failed count', async () => {
+ await createExpandedWidgetWithData(recentFailures);
+
+ expect(trimText(findAllExtensionListItems().at(0).text())).toContain(
+ 'Failed 8 times in main in the last 14 days',
+ );
+ });
});
});
diff --git a/spec/frontend/vue_mr_widget/extentions/accessibility/index_spec.js b/spec/frontend/vue_mr_widget/extentions/accessibility/index_spec.js
index ea422a57956..6d1b3bb34a5 100644
--- a/spec/frontend/vue_mr_widget/extentions/accessibility/index_spec.js
+++ b/spec/frontend/vue_mr_widget/extentions/accessibility/index_spec.js
@@ -107,7 +107,7 @@ describe('Accessibility extension', () => {
it('displays report list item formatted', () => {
const text = {
newError: trimText(findAllExtensionListItems().at(0).text()),
- resolvedError: findAllExtensionListItems().at(3).text(),
+ resolvedError: trimText(findAllExtensionListItems().at(3).text()),
existingError: trimText(findAllExtensionListItems().at(6).text()),
};
diff --git a/spec/frontend/vue_shared/components/color_picker/color_picker_spec.js b/spec/frontend/vue_shared/components/color_picker/color_picker_spec.js
index 28b3bf5287a..8cbe0630426 100644
--- a/spec/frontend/vue_shared/components/color_picker/color_picker_spec.js
+++ b/spec/frontend/vue_shared/components/color_picker/color_picker_spec.js
@@ -3,6 +3,8 @@ import { mount, shallowMount } from '@vue/test-utils';
import ColorPicker from '~/vue_shared/components/color_picker/color_picker.vue';
+jest.mock('lodash/uniqueId', () => (prefix) => (prefix ? `${prefix}1` : 1));
+
describe('ColorPicker', () => {
let wrapper;
@@ -14,10 +16,11 @@ describe('ColorPicker', () => {
const setColor = '#000000';
const invalidText = 'Please enter a valid hex (#RRGGBB or #RGB) color value';
- const label = () => wrapper.find(GlFormGroup).attributes('label');
+ const findGlFormGroup = () => wrapper.find(GlFormGroup);
const colorPreview = () => wrapper.find('[data-testid="color-preview"]');
const colorPicker = () => wrapper.find(GlFormInput);
- const colorInput = () => wrapper.find(GlFormInputGroup).find('input[type="text"]');
+ const colorInput = () => wrapper.find('input[type="color"]');
+ const colorTextInput = () => wrapper.find(GlFormInputGroup).find('input[type="text"]');
const invalidFeedback = () => wrapper.find('.invalid-feedback');
const description = () => wrapper.find(GlFormGroup).attributes('description');
const presetColors = () => wrapper.findAll(GlLink);
@@ -39,13 +42,29 @@ describe('ColorPicker', () => {
it('hides the label if the label is not passed', () => {
createComponent(shallowMount);
- expect(label()).toBe('');
+ expect(findGlFormGroup().attributes('label')).toBe('');
});
it('shows the label if the label is passed', () => {
createComponent(shallowMount, { label: 'test' });
- expect(label()).toBe('test');
+ expect(findGlFormGroup().attributes('label')).toBe('test');
+ });
+
+ describe.each`
+ desc | id
+ ${'with prop id'} | ${'test-id'}
+ ${'without prop id'} | ${undefined}
+ `('$desc', ({ id }) => {
+ beforeEach(() => {
+ createComponent(mount, { id, label: 'test' });
+ });
+
+ it('renders the same `ID` for input and `for` for label', () => {
+ expect(findGlFormGroup().find('label').attributes('for')).toBe(
+ colorInput().attributes('id'),
+ );
+ });
});
});
@@ -55,30 +74,30 @@ describe('ColorPicker', () => {
expect(colorPreview().attributes('style')).toBe(undefined);
expect(colorPicker().attributes('value')).toBe(undefined);
- expect(colorInput().props('value')).toBe('');
+ expect(colorTextInput().props('value')).toBe('');
expect(colorPreview().attributes('class')).toContain('gl-inset-border-1-gray-400');
});
it('has a color set on initialization', () => {
createComponent(mount, { value: setColor });
- expect(colorInput().props('value')).toBe(setColor);
+ expect(colorTextInput().props('value')).toBe(setColor);
});
it('emits input event from component when a color is selected', async () => {
createComponent();
- await colorInput().setValue(setColor);
+ await colorTextInput().setValue(setColor);
expect(wrapper.emitted().input[0]).toStrictEqual([setColor]);
});
it('trims spaces from submitted colors', async () => {
createComponent();
- await colorInput().setValue(` ${setColor} `);
+ await colorTextInput().setValue(` ${setColor} `);
expect(wrapper.emitted().input[0]).toStrictEqual([setColor]);
expect(colorPreview().attributes('class')).toContain('gl-inset-border-1-gray-400');
- expect(colorInput().attributes('class')).not.toContain('is-invalid');
+ expect(colorTextInput().attributes('class')).not.toContain('is-invalid');
});
it('shows invalid feedback when the state is marked as invalid', async () => {
@@ -86,14 +105,14 @@ describe('ColorPicker', () => {
expect(invalidFeedback().text()).toBe(invalidText);
expect(colorPreview().attributes('class')).toContain('gl-inset-border-1-red-500');
- expect(colorInput().attributes('class')).toContain('is-invalid');
+ expect(colorTextInput().attributes('class')).toContain('is-invalid');
});
});
describe('inputs', () => {
it('has color input value entered', async () => {
createComponent();
- await colorInput().setValue(setColor);
+ await colorTextInput().setValue(setColor);
expect(wrapper.emitted().input[0]).toStrictEqual([setColor]);
});
diff --git a/spec/frontend/vue_shared/components/confidentiality_badge_spec.js b/spec/frontend/vue_shared/components/confidentiality_badge_spec.js
new file mode 100644
index 00000000000..9d11fbbaf55
--- /dev/null
+++ b/spec/frontend/vue_shared/components/confidentiality_badge_spec.js
@@ -0,0 +1,52 @@
+import { GlBadge } from '@gitlab/ui';
+
+import { shallowMount } from '@vue/test-utils';
+import { WorkspaceType, IssuableType } from '~/issues/constants';
+
+import ConfidentialityBadge from '~/vue_shared/components/confidentiality_badge.vue';
+
+const createComponent = ({
+ workspaceType = WorkspaceType.project,
+ issuableType = IssuableType.Issue,
+} = {}) =>
+ shallowMount(ConfidentialityBadge, {
+ propsData: {
+ workspaceType,
+ issuableType,
+ },
+ });
+
+describe('ConfidentialityBadge', () => {
+ let wrapper;
+
+ beforeEach(() => {
+ wrapper = createComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it.each`
+ workspaceType | issuableType | expectedTooltip
+ ${WorkspaceType.project} | ${IssuableType.Issue} | ${'Only project members with at least Reporter role can view or be notified about this issue.'}
+ ${WorkspaceType.group} | ${IssuableType.Epic} | ${'Only group members with at least Reporter role can view or be notified about this epic.'}
+ `(
+ 'should render gl-badge with correct tooltip when workspaceType is $workspaceType and issuableType is $issuableType',
+ ({ workspaceType, issuableType, expectedTooltip }) => {
+ wrapper = createComponent({
+ workspaceType,
+ issuableType,
+ });
+
+ const badgeEl = wrapper.findComponent(GlBadge);
+
+ expect(badgeEl.props()).toMatchObject({
+ icon: 'eye-slash',
+ variant: 'warning',
+ });
+ expect(badgeEl.attributes('title')).toBe(expectedTooltip);
+ expect(badgeEl.text()).toBe('Confidential');
+ },
+ );
+});
diff --git a/spec/frontend/vue_shared/components/confirm_danger/confirm_danger_modal_spec.js b/spec/frontend/vue_shared/components/confirm_danger/confirm_danger_modal_spec.js
index f75694bd504..a660643d74f 100644
--- a/spec/frontend/vue_shared/components/confirm_danger/confirm_danger_modal_spec.js
+++ b/spec/frontend/vue_shared/components/confirm_danger/confirm_danger_modal_spec.js
@@ -3,6 +3,7 @@ import {
CONFIRM_DANGER_WARNING,
CONFIRM_DANGER_MODAL_BUTTON,
CONFIRM_DANGER_MODAL_ID,
+ CONFIRM_DANGER_MODAL_CANCEL,
} from '~/vue_shared/components/confirm_danger/constants';
import ConfirmDangerModal from '~/vue_shared/components/confirm_danger/confirm_danger_modal.vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
@@ -10,6 +11,7 @@ import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
describe('Confirm Danger Modal', () => {
const confirmDangerMessage = 'This is a dangerous activity';
const confirmButtonText = 'Confirm button text';
+ const cancelButtonText = 'Cancel button text';
const phrase = 'You must construct additional pylons';
const modalId = CONFIRM_DANGER_MODAL_ID;
@@ -21,6 +23,7 @@ describe('Confirm Danger Modal', () => {
const findDefaultWarning = () => wrapper.findByTestId('confirm-danger-warning');
const findAdditionalMessage = () => wrapper.findByTestId('confirm-danger-message');
const findPrimaryAction = () => findModal().props('actionPrimary');
+ const findCancelAction = () => findModal().props('actionCancel');
const findPrimaryActionAttributes = (attr) => findPrimaryAction().attributes[0][attr];
const createComponent = ({ provide = {} } = {}) =>
@@ -34,7 +37,9 @@ describe('Confirm Danger Modal', () => {
});
beforeEach(() => {
- wrapper = createComponent({ provide: { confirmDangerMessage, confirmButtonText } });
+ wrapper = createComponent({
+ provide: { confirmDangerMessage, confirmButtonText, cancelButtonText },
+ });
});
afterEach(() => {
@@ -54,6 +59,10 @@ describe('Confirm Danger Modal', () => {
expect(findPrimaryActionAttributes('variant')).toBe('danger');
});
+ it('renders the cancel button', () => {
+ expect(findCancelAction().text).toBe(cancelButtonText);
+ });
+
it('renders the correct confirmation phrase', () => {
expect(findConfirmationPhrase().text()).toBe(
`Please type ${phrase} to proceed or close this modal to cancel.`,
@@ -72,6 +81,10 @@ describe('Confirm Danger Modal', () => {
it('renders the default confirm button', () => {
expect(findPrimaryAction().text).toBe(CONFIRM_DANGER_MODAL_BUTTON);
});
+
+ it('renders the default cancel button', () => {
+ expect(findCancelAction().text).toBe(CONFIRM_DANGER_MODAL_CANCEL);
+ });
});
describe('with a valid confirmation phrase', () => {
diff --git a/spec/frontend/vue_shared/components/date_time_picker/date_time_picker_spec.js b/spec/frontend/vue_shared/components/date_time_picker/date_time_picker_spec.js
index d4b6b987c69..aa41df438d2 100644
--- a/spec/frontend/vue_shared/components/date_time_picker/date_time_picker_spec.js
+++ b/spec/frontend/vue_shared/components/date_time_picker/date_time_picker_spec.js
@@ -15,7 +15,7 @@ describe('DateTimePicker', () => {
const dropdownToggle = () => wrapper.find('.dropdown-toggle');
const dropdownMenu = () => wrapper.find('.dropdown-menu');
const cancelButton = () => wrapper.find('[data-testid="cancelButton"]');
- const applyButtonElement = () => wrapper.find('button.btn-success').element;
+ const applyButtonElement = () => wrapper.find('button.btn-confirm').element;
const findQuickRangeItems = () => wrapper.findAll('.dropdown-item');
const createComponent = (props) => {
diff --git a/spec/frontend/vue_shared/components/deployment_instance/deployment_instance_spec.js b/spec/frontend/vue_shared/components/deployment_instance/deployment_instance_spec.js
index 59653a0ec13..e3d8bfd22ca 100644
--- a/spec/frontend/vue_shared/components/deployment_instance/deployment_instance_spec.js
+++ b/spec/frontend/vue_shared/components/deployment_instance/deployment_instance_spec.js
@@ -6,12 +6,16 @@ import { folder } from './mock_data';
describe('Deploy Board Instance', () => {
let wrapper;
- const createComponent = (props = {}) =>
+ const createComponent = (props = {}, provide) =>
shallowMount(DeployBoardInstance, {
propsData: {
status: 'succeeded',
...props,
},
+ provide: {
+ glFeatures: { monitorLogging: true },
+ ...provide,
+ },
});
describe('as a non-canary deployment', () => {
@@ -95,4 +99,23 @@ describe('Deploy Board Instance', () => {
expect(wrapper.attributes('title')).toEqual('');
});
});
+
+ describe(':monitor_logging feature flag', () => {
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it.each`
+ flagState | logsState | expected
+ ${true} | ${'shows'} | ${'/root/review-app/-/logs?environment_name=foo&pod_name=tanuki-1'}
+ ${false} | ${'hides'} | ${undefined}
+ `('$logsState log link when flag state is $flagState', async ({ flagState, expected }) => {
+ wrapper = createComponent(
+ { logsPath: folder.logs_path, podName: 'tanuki-1' },
+ { glFeatures: { monitorLogging: flagState } },
+ );
+
+ expect(wrapper.attributes('href')).toEqual(expected);
+ });
+ });
});
diff --git a/spec/frontend/vue_shared/components/dropdown/dropdown_hidden_input_spec.js b/spec/frontend/vue_shared/components/dropdown/dropdown_hidden_input_spec.js
deleted file mode 100644
index 30b8e869aab..00000000000
--- a/spec/frontend/vue_shared/components/dropdown/dropdown_hidden_input_spec.js
+++ /dev/null
@@ -1,36 +0,0 @@
-import Vue from 'vue';
-
-import mountComponent from 'helpers/vue_mount_component_helper';
-import dropdownHiddenInputComponent from '~/vue_shared/components/dropdown/dropdown_hidden_input.vue';
-
-import { mockLabels } from './mock_data';
-
-const createComponent = (name = 'label_id[]', value = mockLabels[0].id) => {
- const Component = Vue.extend(dropdownHiddenInputComponent);
-
- return mountComponent(Component, {
- name,
- value,
- });
-};
-
-describe('DropdownHiddenInputComponent', () => {
- let vm;
-
- beforeEach(() => {
- vm = createComponent();
- });
-
- afterEach(() => {
- vm.$destroy();
- });
-
- describe('template', () => {
- it('renders input element of type `hidden`', () => {
- expect(vm.$el.nodeName).toBe('INPUT');
- expect(vm.$el.getAttribute('type')).toBe('hidden');
- expect(vm.$el.getAttribute('name')).toBe(vm.name);
- expect(vm.$el.getAttribute('value')).toBe(`${vm.value}`);
- });
- });
-});
diff --git a/spec/frontend/vue_shared/components/dropdown/dropdown_search_input_spec.js b/spec/frontend/vue_shared/components/dropdown/dropdown_search_input_spec.js
deleted file mode 100644
index b32dbeb8852..00000000000
--- a/spec/frontend/vue_shared/components/dropdown/dropdown_search_input_spec.js
+++ /dev/null
@@ -1,51 +0,0 @@
-import { mount } from '@vue/test-utils';
-import { nextTick } from 'vue';
-import DropdownSearchInputComponent from '~/vue_shared/components/dropdown/dropdown_search_input.vue';
-
-describe('DropdownSearchInputComponent', () => {
- let wrapper;
-
- const defaultProps = {
- placeholderText: 'Search something',
- };
- const buildVM = (propsData = defaultProps) => {
- wrapper = mount(DropdownSearchInputComponent, {
- propsData,
- });
- };
- const findInputEl = () => wrapper.find('.dropdown-input-field');
-
- beforeEach(() => {
- buildVM();
- });
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- describe('template', () => {
- it('renders input element with type `search`', () => {
- expect(findInputEl().exists()).toBe(true);
- expect(findInputEl().attributes('type')).toBe('search');
- });
-
- it('renders search icon element', () => {
- expect(wrapper.find('.dropdown-input-search[data-testid="search-icon"]').exists()).toBe(true);
- });
-
- it('displays custom placeholder text', () => {
- expect(findInputEl().attributes('placeholder')).toBe(defaultProps.placeholderText);
- });
-
- it('focuses input element when focused property equals true', async () => {
- const inputEl = findInputEl().element;
-
- jest.spyOn(inputEl, 'focus');
-
- wrapper.setProps({ focused: true });
-
- await nextTick();
- expect(inputEl.focus).toHaveBeenCalled();
- });
- });
-});
diff --git a/spec/frontend/vue_shared/components/file_finder/index_spec.js b/spec/frontend/vue_shared/components/file_finder/index_spec.js
index 921091c5b84..5cf891a2e52 100644
--- a/spec/frontend/vue_shared/components/file_finder/index_spec.js
+++ b/spec/frontend/vue_shared/components/file_finder/index_spec.js
@@ -1,5 +1,6 @@
import Mousetrap from 'mousetrap';
import Vue, { nextTick } from 'vue';
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import { file } from 'jest/ide/helpers';
import { UP_KEY_CODE, DOWN_KEY_CODE, ENTER_KEY_CODE, ESC_KEY_CODE } from '~/lib/utils/keycodes';
import FindFileComponent from '~/vue_shared/components/file_finder/index.vue';
@@ -22,7 +23,11 @@ describe('File finder item spec', () => {
}
beforeEach(() => {
- setFixtures('<div id="app"></div>');
+ setHTMLFixture('<div id="app"></div>');
+ });
+
+ afterEach(() => {
+ resetHTMLFixture();
});
afterEach(() => {
@@ -105,18 +110,6 @@ describe('File finder item spec', () => {
});
});
- describe('listHeight', () => {
- it('returns 55 when entries exist', () => {
- expect(vm.listHeight).toBe(55);
- });
-
- it('returns 33 when entries dont exist', () => {
- vm.searchText = 'testing 123';
-
- expect(vm.listHeight).toBe(33);
- });
- });
-
describe('filteredBlobsLength', () => {
it('returns length of filtered blobs', () => {
vm.searchText = 'index';
@@ -253,11 +246,9 @@ describe('File finder item spec', () => {
describe('without entries', () => {
it('renders loading text when loading', () => {
- createComponent({
- loading: true,
- });
+ createComponent({ loading: true });
- expect(vm.$el.textContent).toContain('Loading...');
+ expect(vm.$el.querySelector('.gl-spinner')).not.toBe(null);
});
it('renders no files text', () => {
@@ -307,7 +298,7 @@ describe('File finder item spec', () => {
});
it('stops callback in monaco editor', () => {
- setFixtures('<div class="inputarea"></div>');
+ setHTMLFixture('<div class="inputarea"></div>');
expect(
Mousetrap.prototype.stopCallback(null, document.querySelector('.inputarea'), 't'),
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 b6a181e6a0b..e44bc8771f5 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
@@ -11,7 +11,10 @@ import { shallowMount, mount } from '@vue/test-utils';
import { nextTick } from 'vue';
import RecentSearchesService from '~/filtered_search/services/recent_searches_service';
import RecentSearchesStore from '~/filtered_search/stores/recent_searches_store';
-import { SortDirection } from '~/vue_shared/components/filtered_search_bar/constants';
+import {
+ FILTERED_SEARCH_TERM,
+ SortDirection,
+} from '~/vue_shared/components/filtered_search_bar/constants';
import FilteredSearchBarRoot from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
import { uniqueTokens } from '~/vue_shared/components/filtered_search_bar/filtered_search_utils';
@@ -68,6 +71,10 @@ const createComponent = ({
describe('FilteredSearchBarRoot', () => {
let wrapper;
+ const findGlButton = () => wrapper.findComponent(GlButton);
+ const findGlDropdown = () => wrapper.findComponent(GlDropdown);
+ const findGlFilteredSearch = () => wrapper.findComponent(GlFilteredSearch);
+
beforeEach(() => {
wrapper = createComponent({ sortOptions: mockSortOptions });
});
@@ -79,7 +86,7 @@ describe('FilteredSearchBarRoot', () => {
describe('data', () => {
it('initializes `filterValue`, `selectedSortOption` and `selectedSortDirection` data props and displays the sort dropdown', () => {
expect(wrapper.vm.filterValue).toEqual([]);
- expect(wrapper.vm.selectedSortOption).toBe(mockSortOptions[0].sortDirection.descending);
+ expect(wrapper.vm.selectedSortOption).toBe(mockSortOptions[0]);
expect(wrapper.vm.selectedSortDirection).toBe(SortDirection.descending);
expect(wrapper.find(GlButtonGroup).exists()).toBe(true);
expect(wrapper.find(GlButton).exists()).toBe(true);
@@ -225,9 +232,7 @@ describe('FilteredSearchBarRoot', () => {
});
it('initializes `recentSearchesPromise` prop with a promise by using `recentSearchesService.fetch()`', () => {
- jest
- .spyOn(wrapper.vm.recentSearchesService, 'fetch')
- .mockReturnValue(new Promise(() => []));
+ jest.spyOn(wrapper.vm.recentSearchesService, 'fetch').mockResolvedValue([]);
wrapper.vm.setupRecentSearch();
@@ -489,4 +494,40 @@ describe('FilteredSearchBarRoot', () => {
expect(sortButtonEl.props('icon')).toBe('sort-highest');
});
});
+
+ describe('watchers', () => {
+ const tokenValue = {
+ id: 'id-1',
+ type: FILTERED_SEARCH_TERM,
+ value: { data: '' },
+ };
+
+ it('syncs filter value', async () => {
+ await wrapper.setProps({ initialFilterValue: [tokenValue], syncFilterAndSort: true });
+
+ expect(findGlFilteredSearch().props('value')).toEqual([tokenValue]);
+ });
+
+ it('does not sync filter value when syncFilterAndSort=false', async () => {
+ await wrapper.setProps({ initialFilterValue: [tokenValue], syncFilterAndSort: false });
+
+ expect(findGlFilteredSearch().props('value')).toEqual([]);
+ });
+
+ it('syncs sort values', async () => {
+ await wrapper.setProps({ initialSortBy: 'updated_asc', syncFilterAndSort: true });
+
+ expect(findGlDropdown().props('text')).toBe('Last updated');
+ expect(findGlButton().props('icon')).toBe('sort-lowest');
+ expect(findGlButton().attributes('aria-label')).toBe('Sort direction: Ascending');
+ });
+
+ it('does not sync sort values when syncFilterAndSort=false', async () => {
+ await wrapper.setProps({ initialSortBy: 'updated_asc', syncFilterAndSort: false });
+
+ expect(findGlDropdown().props('text')).toBe('Created date');
+ expect(findGlButton().props('icon')).toBe('sort-highest');
+ expect(findGlButton().attributes('aria-label')).toBe('Sort direction: 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 87066b70023..3f24d5df858 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
@@ -51,6 +51,7 @@ function createComponent(options = {}) {
config,
value,
active,
+ cursorPosition: 'start',
},
provide: {
portalName: 'fake target',
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/base_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/base_token_spec.js
index af8a2a496ea..ca8cd419d87 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/base_token_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/base_token_spec.js
@@ -78,6 +78,7 @@ const mockProps = {
suggestionsLoading: false,
defaultSuggestions: DEFAULT_NONE_ANY,
getActiveTokenValue: (labels, data) => labels.find((label) => label.title === data),
+ cursorPosition: 'start',
};
function createComponent({
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 7a7db434052..7b495ec9bee 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
@@ -39,6 +39,7 @@ function createComponent(options = {}) {
config,
value,
active,
+ cursorPosition: 'start',
},
provide: {
portalName: 'fake target',
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 b163563cea4..dcb0d095b1b 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
@@ -45,6 +45,7 @@ function createComponent(options = {}) {
config,
value,
active,
+ cursorPosition: 'start',
},
provide: {
portalName: 'fake target',
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 52df27c2d00..f03a2e7934f 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
@@ -45,6 +45,7 @@ function createComponent(options = {}) {
config,
value,
active,
+ cursorPosition: 'start',
},
provide: {
portalName: 'fake target',
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 de9ec863dd5..7c545f76c0b 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
@@ -42,6 +42,7 @@ function createComponent(options = {}) {
config,
value,
active,
+ cursorPosition: 'start',
},
provide: {
portalName: 'fake target',
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/release_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/release_token_spec.js
index 8be21b35414..4bbbaab9b7a 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/release_token_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/release_token_spec.js
@@ -18,6 +18,7 @@ describe('ReleaseToken', () => {
active: false,
config,
value,
+ cursorPosition: 'start',
},
provide: {
portalName: 'fake target',
diff --git a/spec/frontend/vue_shared/components/gitlab_version_check_spec.js b/spec/frontend/vue_shared/components/gitlab_version_check_spec.js
index b673e5407d4..b180e8c12dd 100644
--- a/spec/frontend/vue_shared/components/gitlab_version_check_spec.js
+++ b/spec/frontend/vue_shared/components/gitlab_version_check_spec.js
@@ -1,7 +1,7 @@
import { GlBadge } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
-import flushPromises from 'helpers/flush_promises';
+import waitForPromises from 'helpers/wait_for_promises';
import axios from '~/lib/utils/axios_utils';
import GitlabVersionCheck from '~/vue_shared/components/gitlab_version_check.vue';
@@ -43,7 +43,7 @@ describe('GitlabVersionCheck', () => {
describe(`is ${description}`, () => {
beforeEach(async () => {
createComponent(mockResponse);
- await flushPromises(); // Ensure we wrap up the axios call
+ await waitForPromises(); // Ensure we wrap up the axios call
});
it(`does${renders ? '' : ' not'} render GlBadge`, () => {
@@ -61,7 +61,7 @@ describe('GitlabVersionCheck', () => {
describe(`when response is ${mockResponse.res.severity}`, () => {
beforeEach(async () => {
createComponent(mockResponse);
- await flushPromises(); // Ensure we wrap up the axios call
+ await waitForPromises(); // Ensure we wrap up the axios call
});
it(`title is ${expectedUI.title}`, () => {
diff --git a/spec/frontend/vue_shared/components/markdown/field_spec.js b/spec/frontend/vue_shared/components/markdown/field_spec.js
index d1c4d777d44..b3376f26a25 100644
--- a/spec/frontend/vue_shared/components/markdown/field_spec.js
+++ b/spec/frontend/vue_shared/components/markdown/field_spec.js
@@ -5,12 +5,14 @@ import { TEST_HOST, FIXTURES_PATH } from 'spec/test_constants';
import axios from '~/lib/utils/axios_utils';
import MarkdownField from '~/vue_shared/components/markdown/field.vue';
import MarkdownFieldHeader from '~/vue_shared/components/markdown/header.vue';
+import MarkdownToolbar from '~/vue_shared/components/markdown/toolbar.vue';
import { mountExtended } from 'helpers/vue_test_utils_helper';
const markdownPreviewPath = `${TEST_HOST}/preview`;
const markdownDocsPath = `${TEST_HOST}/docs`;
const textareaValue = 'testing\n123';
const uploadsPath = 'test/uploads';
+const restrictedToolBarItems = ['quote'];
function assertMarkdownTabs(isWrite, writeLink, previewLink, wrapper) {
expect(writeLink.element.children[0].classList.contains('active')).toBe(isWrite);
@@ -63,6 +65,7 @@ describe('Markdown field component', () => {
textareaValue,
lines,
enablePreview,
+ restrictedToolBarItems,
},
provide: {
glFeatures: {
@@ -81,6 +84,8 @@ describe('Markdown field component', () => {
const getAttachButton = () => subject.find('.button-attach-file');
const clickAttachButton = () => getAttachButton().trigger('click');
const findDropzone = () => subject.find('.div-dropzone');
+ const findMarkdownHeader = () => subject.findComponent(MarkdownFieldHeader);
+ const findMarkdownToolbar = () => subject.findComponent(MarkdownToolbar);
describe('mounted', () => {
const previewHTML = `
@@ -184,9 +189,23 @@ describe('Markdown field component', () => {
assertMarkdownTabs(false, writeLink, previewLink, subject);
});
+
+ it('passes correct props to MarkdownToolbar', () => {
+ expect(findMarkdownToolbar().props()).toEqual({
+ canAttachFile: true,
+ markdownDocsPath,
+ quickActionsDocsPath: '',
+ showCommentToolBar: true,
+ });
+ });
});
describe('markdown buttons', () => {
+ beforeEach(() => {
+ // needed for the underlying insertText to work
+ document.execCommand = jest.fn(() => false);
+ });
+
it('converts single words', async () => {
const textarea = subject.find('textarea').element;
textarea.setSelectionRange(0, 7);
@@ -309,9 +328,7 @@ describe('Markdown field component', () => {
it('escapes new line characters', () => {
createSubject({ lines: [{ rich_text: 'hello world\\n' }] });
- expect(subject.find('[data-testid="markdownHeader"]').props('lineContent')).toBe(
- 'hello world%br',
- );
+ expect(findMarkdownHeader().props('lineContent')).toBe('hello world%br');
});
});
@@ -325,4 +342,12 @@ describe('Markdown field component', () => {
expect(subject.findComponent(MarkdownFieldHeader).props('enablePreview')).toBe(true);
});
+
+ it('passess restricted tool bar items', () => {
+ createSubject();
+
+ expect(subject.findComponent(MarkdownFieldHeader).props('restrictedToolBarItems')).toBe(
+ restrictedToolBarItems,
+ );
+ });
});
diff --git a/spec/frontend/vue_shared/components/markdown/header_spec.js b/spec/frontend/vue_shared/components/markdown/header_spec.js
index fa4ca63f910..67222cab247 100644
--- a/spec/frontend/vue_shared/components/markdown/header_spec.js
+++ b/spec/frontend/vue_shared/components/markdown/header_spec.js
@@ -166,4 +166,26 @@ describe('Markdown field header component', () => {
expect(wrapper.findByTestId('preview-tab').exists()).toBe(false);
});
+
+ describe('restricted tool bar items', () => {
+ let defaultCount;
+
+ beforeEach(() => {
+ defaultCount = findToolbarButtons().length;
+ });
+
+ it('restricts items as per input', () => {
+ createWrapper({
+ restrictedToolBarItems: ['quote'],
+ });
+
+ expect(findToolbarButtons().length).toBe(defaultCount - 1);
+ });
+
+ it('shows all items by default', () => {
+ createWrapper();
+
+ expect(findToolbarButtons().length).toBe(defaultCount);
+ });
+ });
});
diff --git a/spec/frontend/vue_shared/components/markdown/toolbar_spec.js b/spec/frontend/vue_shared/components/markdown/toolbar_spec.js
index 8bff85b0bda..f698794b951 100644
--- a/spec/frontend/vue_shared/components/markdown/toolbar_spec.js
+++ b/spec/frontend/vue_shared/components/markdown/toolbar_spec.js
@@ -33,4 +33,18 @@ describe('toolbar', () => {
expect(wrapper.vm.$el.querySelector('.uploading-container')).toBeNull();
});
});
+
+ describe('comment tool bar settings', () => {
+ it('does not show comment tool bar div', () => {
+ createMountedWrapper({ showCommentToolBar: false });
+
+ expect(wrapper.find('.comment-toolbar').exists()).toBe(false);
+ });
+
+ it('shows comment tool bar by default', () => {
+ createMountedWrapper();
+
+ expect(wrapper.find('.comment-toolbar').exists()).toBe(true);
+ });
+ });
});
diff --git a/spec/frontend/vue_shared/components/metric_images/__snapshots__/metric_images_table_spec.js.snap b/spec/frontend/vue_shared/components/metric_images/__snapshots__/metric_images_table_spec.js.snap
index 5dd12d9edf5..015049795a1 100644
--- a/spec/frontend/vue_shared/components/metric_images/__snapshots__/metric_images_table_spec.js.snap
+++ b/spec/frontend/vue_shared/components/metric_images/__snapshots__/metric_images_table_spec.js.snap
@@ -10,6 +10,7 @@ exports[`Metrics upload item render the metrics image component 1`] = `
<gl-modal-stub
actioncancel="[object Object]"
actionprimary="[object Object]"
+ arialabel=""
body-class="gl-pb-0! gl-min-h-6!"
dismisslabel="Close"
modalclass=""
@@ -26,6 +27,7 @@ exports[`Metrics upload item render the metrics image component 1`] = `
<gl-modal-stub
actioncancel="[object Object]"
actionprimary="[object Object]"
+ arialabel=""
data-testid="metric-image-edit-modal"
dismisslabel="Close"
modalclass=""
diff --git a/spec/frontend/vue_shared/components/registry/list_item_spec.js b/spec/frontend/vue_shared/components/registry/list_item_spec.js
index 1b93292e37b..6e9abb2bfb3 100644
--- a/spec/frontend/vue_shared/components/registry/list_item_spec.js
+++ b/spec/frontend/vue_shared/components/registry/list_item_spec.js
@@ -101,20 +101,6 @@ describe('list item', () => {
});
});
- describe('disabled prop', () => {
- it('when true applies gl-opacity-5 class', () => {
- mountComponent({ disabled: true });
-
- expect(wrapper.classes('gl-opacity-5')).toBe(true);
- });
-
- it('when false does not apply gl-opacity-5 class', () => {
- mountComponent({ disabled: false });
-
- expect(wrapper.classes('gl-opacity-5')).toBe(false);
- });
- });
-
describe('borders and selection', () => {
it.each`
first | selected | shouldHave | shouldNotHave
diff --git a/spec/frontend/vue_shared/components/runner_aws_deployments/__snapshots__/runner_aws_deployments_modal_spec.js.snap b/spec/frontend/vue_shared/components/runner_aws_deployments/__snapshots__/runner_aws_deployments_modal_spec.js.snap
index ac313e556fc..8ff49271eb5 100644
--- a/spec/frontend/vue_shared/components/runner_aws_deployments/__snapshots__/runner_aws_deployments_modal_spec.js.snap
+++ b/spec/frontend/vue_shared/components/runner_aws_deployments/__snapshots__/runner_aws_deployments_modal_spec.js.snap
@@ -4,6 +4,7 @@ exports[`RunnerAwsDeploymentsModal renders the modal 1`] = `
<gl-modal-stub
actionprimary="[object Object]"
actionsecondary="[object Object]"
+ arialabel=""
dismisslabel="Close"
modalclass=""
modalid="runner-aws-deployments-modal"
diff --git a/spec/frontend/vue_shared/components/runner_instructions/runner_instructions_modal_spec.js b/spec/frontend/vue_shared/components/runner_instructions/runner_instructions_modal_spec.js
index 0da9939e97f..001b6ee4a6f 100644
--- a/spec/frontend/vue_shared/components/runner_instructions/runner_instructions_modal_spec.js
+++ b/spec/frontend/vue_shared/components/runner_instructions/runner_instructions_modal_spec.js
@@ -45,8 +45,10 @@ describe('RunnerInstructionsModal component', () => {
const findSkeletonLoader = () => wrapper.findComponent(GlSkeletonLoader);
const findGlLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findAlert = () => wrapper.findComponent(GlAlert);
+ const findModal = () => wrapper.findComponent(GlModal);
const findPlatformButtonGroup = () => wrapper.findByTestId('platform-buttons');
const findPlatformButtons = () => findPlatformButtonGroup().findAllComponents(GlButton);
+ const findOsxPlatformButton = () => wrapper.find({ ref: 'osx' });
const findArchitectureDropdownItems = () => wrapper.findAllByTestId('architecture-dropdown-item');
const findBinaryInstructions = () => wrapper.findByTestId('binary-instructions');
const findRegisterCommand = () => wrapper.findByTestId('register-command');
@@ -140,6 +142,38 @@ describe('RunnerInstructionsModal component', () => {
expect(instructions).toBe(registerInstructions);
});
});
+
+ describe('when the modal is shown', () => {
+ it('sets the focus on the selected platform', () => {
+ findPlatformButtons().at(0).element.focus = jest.fn();
+
+ findModal().vm.$emit('shown');
+
+ expect(findPlatformButtons().at(0).element.focus).toHaveBeenCalled();
+ });
+ });
+
+ describe('when providing a defaultPlatformName', () => {
+ beforeEach(async () => {
+ createComponent({ props: { defaultPlatformName: 'osx' } });
+ await waitForPromises();
+ });
+
+ it('runner instructions for the default selected platform are requested', () => {
+ expect(runnerSetupInstructionsHandler).toHaveBeenCalledWith({
+ platform: 'osx',
+ architecture: 'amd64',
+ });
+ });
+
+ it('sets the focus on the default selected platform', () => {
+ findOsxPlatformButton().element.focus = jest.fn();
+
+ findModal().vm.$emit('shown');
+
+ expect(findOsxPlatformButton().element.focus).toHaveBeenCalled();
+ });
+ });
});
describe('after a platform and architecture are selected', () => {
diff --git a/spec/frontend/vue_shared/components/segmented_control_button_group_spec.js b/spec/frontend/vue_shared/components/segmented_control_button_group_spec.js
new file mode 100644
index 00000000000..88445b6684c
--- /dev/null
+++ b/spec/frontend/vue_shared/components/segmented_control_button_group_spec.js
@@ -0,0 +1,104 @@
+import { GlButtonGroup, GlButton } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import SegmentedControlButtonGroup from '~/vue_shared/components/segmented_control_button_group.vue';
+
+const DEFAULT_OPTIONS = [
+ { text: 'Lorem', value: 'abc' },
+ { text: 'Ipsum', value: 'def' },
+ { text: 'Foo', value: 'x', disabled: true },
+ { text: 'Dolar', value: 'ghi' },
+];
+
+describe('~/vue_shared/components/segmented_control_button_group.vue', () => {
+ let wrapper;
+
+ const createComponent = (props = {}, scopedSlots = {}) => {
+ wrapper = shallowMount(SegmentedControlButtonGroup, {
+ propsData: {
+ value: DEFAULT_OPTIONS[0].value,
+ options: DEFAULT_OPTIONS,
+ ...props,
+ },
+ scopedSlots,
+ });
+ };
+
+ const findButtonGroup = () => wrapper.findComponent(GlButtonGroup);
+ const findButtons = () => findButtonGroup().findAllComponents(GlButton);
+ const findButtonsData = () =>
+ findButtons().wrappers.map((x) => ({
+ selected: x.props('selected'),
+ text: x.text(),
+ disabled: x.props('disabled'),
+ }));
+ const findButtonWithText = (text) => findButtons().wrappers.find((x) => x.text() === text);
+
+ const optionsAsButtonData = (options) =>
+ options.map(({ text, disabled = false }) => ({
+ selected: false,
+ text,
+ disabled,
+ }));
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('default', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders button group', () => {
+ expect(findButtonGroup().exists()).toBe(true);
+ });
+
+ it('renders buttons', () => {
+ const expectation = optionsAsButtonData(DEFAULT_OPTIONS);
+ expectation[0].selected = true;
+
+ expect(findButtonsData()).toEqual(expectation);
+ });
+
+ describe.each(DEFAULT_OPTIONS.filter((x) => !x.disabled))(
+ 'when button clicked %p',
+ ({ text, value }) => {
+ it('emits input with value', () => {
+ expect(wrapper.emitted('input')).toBeUndefined();
+
+ findButtonWithText(text).vm.$emit('click');
+
+ expect(wrapper.emitted('input')).toEqual([[value]]);
+ });
+ },
+ );
+ });
+
+ const VALUE_TEST_CASES = [0, 1, 3].map((index) => [DEFAULT_OPTIONS[index].value, index]);
+
+ describe.each(VALUE_TEST_CASES)('with value=%s', (value, index) => {
+ it(`renders selected button at ${index}`, () => {
+ createComponent({ value });
+
+ const expectation = optionsAsButtonData(DEFAULT_OPTIONS);
+ expectation[index].selected = true;
+
+ expect(findButtonsData()).toEqual(expectation);
+ });
+ });
+
+ describe('with button-content slot', () => {
+ it('renders button content based on slot', () => {
+ createComponent(
+ {},
+ {
+ 'button-content': `<template #button-content="{ text }">In a slot - {{ text }}</template>`,
+ },
+ );
+
+ expect(findButtonsData().map((x) => x.text)).toEqual(
+ DEFAULT_OPTIONS.map((x) => `In a slot - ${x.text}`),
+ );
+ });
+ });
+});
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 3ceed670d77..9c29f304c71 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
@@ -153,7 +153,11 @@ describe('DropdownContentsCreateView', () => {
});
it('enables a Create button', () => {
- expect(findCreateButton().props('disabled')).toBe(false);
+ expect(findCreateButton().props()).toMatchObject({
+ disabled: false,
+ category: 'primary',
+ variant: 'confirm',
+ });
});
it('renders a loader spinner after Create button click', async () => {
diff --git a/spec/frontend/vue_shared/components/usage_quotas/usage_banner_spec.js b/spec/frontend/vue_shared/components/usage_quotas/usage_banner_spec.js
new file mode 100644
index 00000000000..662c09d02bf
--- /dev/null
+++ b/spec/frontend/vue_shared/components/usage_quotas/usage_banner_spec.js
@@ -0,0 +1,62 @@
+import { GlSkeletonLoader } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import component from '~/vue_shared/components/usage_quotas/usage_banner.vue';
+
+describe('usage banner', () => {
+ let wrapper;
+
+ const findLeftPrimaryTextSlot = () => wrapper.findByTestId('left-primary-text');
+ const findLeftSecondaryTextSlot = () => wrapper.findByTestId('left-secondary-text');
+ const findRightPrimaryTextSlot = () => wrapper.findByTestId('right-primary-text');
+ const findRightSecondaryTextSlot = () => wrapper.findByTestId('right-secondary-text');
+ const findSkeletonLoader = () => wrapper.findComponent(GlSkeletonLoader);
+
+ const mountComponent = (propsData, slots) => {
+ wrapper = shallowMountExtended(component, {
+ propsData,
+ slots: {
+ 'left-primary-text': '<div data-testid="left-primary-text" />',
+ 'left-secondary-text': '<div data-testid="left-secondary-text" />',
+ 'right-primary-text': '<div data-testid="right-primary-text" />',
+ 'right-secondary-text': '<div data-testid="right-secondary-text" />',
+ ...slots,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe.each`
+ slotName | finderFunction
+ ${'left-primary-text'} | ${findLeftPrimaryTextSlot}
+ ${'left-secondary-text'} | ${findLeftSecondaryTextSlot}
+ ${'right-primary-text'} | ${findRightPrimaryTextSlot}
+ ${'right-secondary-text'} | ${findRightSecondaryTextSlot}
+ `('$slotName slot', ({ finderFunction, slotName }) => {
+ it('exist when the slot is filled', () => {
+ mountComponent();
+
+ expect(finderFunction().exists()).toBe(true);
+ });
+
+ it('does not exist when the slot is empty', () => {
+ mountComponent({}, { [slotName]: '' });
+
+ expect(finderFunction().exists()).toBe(false);
+ });
+ });
+
+ it('should show a skeleton loader component', () => {
+ mountComponent({ loading: true });
+
+ expect(findSkeletonLoader().exists()).toBe(true);
+ });
+
+ it('should not show a skeleton loader component', () => {
+ mountComponent();
+
+ expect(findSkeletonLoader().exists()).toBe(false);
+ });
+});
diff --git a/spec/frontend/vue_shared/components/user_popover/user_popover_spec.js b/spec/frontend/vue_shared/components/user_popover/user_popover_spec.js
index 3329199a46b..a54f3450633 100644
--- a/spec/frontend/vue_shared/components/user_popover/user_popover_spec.js
+++ b/spec/frontend/vue_shared/components/user_popover/user_popover_spec.js
@@ -1,11 +1,22 @@
import { GlSkeletonLoader, GlIcon } from '@gitlab/ui';
+import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import { AVAILABILITY_STATUS } from '~/set_status_modal/utils';
import UserNameWithStatus from '~/sidebar/components/assignees/user_name_with_status.vue';
import UserPopover from '~/vue_shared/components/user_popover/user_popover.vue';
+import axios from '~/lib/utils/axios_utils';
+import createFlash from '~/flash';
+import { followUser, unfollowUser } from '~/api/user_api';
+
+jest.mock('~/flash');
+jest.mock('~/api/user_api', () => ({
+ followUser: jest.fn(),
+ unfollowUser: jest.fn(),
+}));
const DEFAULT_PROPS = {
user: {
+ id: 1,
username: 'root',
name: 'Administrator',
location: 'Vienna',
@@ -15,6 +26,7 @@ const DEFAULT_PROPS = {
workInformation: null,
status: null,
pronouns: 'they/them',
+ isFollowed: false,
loaded: true,
},
};
@@ -25,11 +37,13 @@ describe('User Popover Component', () => {
let wrapper;
beforeEach(() => {
- loadFixtures(fixtureTemplate);
+ loadHTMLFixture(fixtureTemplate);
+ gon.features = {};
});
afterEach(() => {
wrapper.destroy();
+ resetHTMLFixture();
});
const findUserStatus = () => wrapper.findByTestId('user-popover-status');
@@ -37,15 +51,15 @@ describe('User Popover Component', () => {
const findUserName = () => wrapper.find(UserNameWithStatus);
const findSecurityBotDocsLink = () => wrapper.findByTestId('user-popover-bot-docs-link');
const findUserLocalTime = () => wrapper.findByTestId('user-popover-local-time');
+ const findToggleFollowButton = () => wrapper.findByTestId('toggle-follow-button');
- const createWrapper = (props = {}, options = {}) => {
+ const createWrapper = (props = {}) => {
wrapper = mountExtended(UserPopover, {
propsData: {
...DEFAULT_PROPS,
target: findTarget(),
...props,
},
- ...options,
});
};
@@ -289,4 +303,124 @@ describe('User Popover Component', () => {
expect(findUserLocalTime().exists()).toBe(false);
});
});
+
+ describe("when current user doesn't follow the user", () => {
+ beforeEach(() => createWrapper());
+
+ it('renders the Follow button with the correct variant', () => {
+ expect(findToggleFollowButton().text()).toBe('Follow');
+ expect(findToggleFollowButton().props('variant')).toBe('confirm');
+ });
+
+ describe('when clicking', () => {
+ it('follows the user', async () => {
+ followUser.mockResolvedValue({});
+
+ await findToggleFollowButton().trigger('click');
+
+ expect(findToggleFollowButton().props('loading')).toBe(true);
+
+ await axios.waitForAll();
+
+ expect(wrapper.emitted().follow.length).toBe(1);
+ expect(wrapper.emitted().unfollow).toBeFalsy();
+ });
+
+ describe('when an error occurs', () => {
+ beforeEach(() => {
+ followUser.mockRejectedValue({});
+
+ findToggleFollowButton().trigger('click');
+ });
+
+ it('shows an error message', async () => {
+ await axios.waitForAll();
+
+ expect(createFlash).toHaveBeenCalledWith({
+ message: 'An error occurred while trying to follow this user, please try again.',
+ error: {},
+ captureError: true,
+ });
+ });
+
+ it('emits no events', async () => {
+ await axios.waitForAll();
+
+ expect(wrapper.emitted().follow).toBe(undefined);
+ expect(wrapper.emitted().unfollow).toBe(undefined);
+ });
+ });
+ });
+ });
+
+ describe('when current user follows the user', () => {
+ beforeEach(() => createWrapper({ user: { ...DEFAULT_PROPS.user, isFollowed: true } }));
+
+ it('renders the Unfollow button with the correct variant', () => {
+ expect(findToggleFollowButton().text()).toBe('Unfollow');
+ expect(findToggleFollowButton().props('variant')).toBe('default');
+ });
+
+ describe('when clicking', () => {
+ it('unfollows the user', async () => {
+ unfollowUser.mockResolvedValue({});
+
+ findToggleFollowButton().trigger('click');
+
+ await axios.waitForAll();
+
+ expect(wrapper.emitted().follow).toBe(undefined);
+ expect(wrapper.emitted().unfollow.length).toBe(1);
+ });
+
+ describe('when an error occurs', () => {
+ beforeEach(async () => {
+ unfollowUser.mockRejectedValue({});
+
+ findToggleFollowButton().trigger('click');
+
+ await axios.waitForAll();
+ });
+
+ it('shows an error message', () => {
+ expect(createFlash).toHaveBeenCalledWith({
+ message: 'An error occurred while trying to unfollow this user, please try again.',
+ error: {},
+ captureError: true,
+ });
+ });
+
+ it('emits no events', () => {
+ expect(wrapper.emitted().follow).toBe(undefined);
+ expect(wrapper.emitted().unfollow).toBe(undefined);
+ });
+ });
+ });
+ });
+
+ describe('when the current user is the user', () => {
+ beforeEach(() => {
+ gon.current_username = DEFAULT_PROPS.user.username;
+ createWrapper();
+ });
+
+ it("doesn't render the toggle follow button", () => {
+ expect(findToggleFollowButton().exists()).toBe(false);
+ });
+ });
+
+ describe('when API does not support `isFollowed`', () => {
+ beforeEach(() => {
+ const user = {
+ ...DEFAULT_PROPS.user,
+ isFollowed: undefined,
+ };
+
+ createWrapper({ user });
+ });
+
+ it('does not render the toggle follow button', () => {
+ expect(findToggleFollowButton().exists()).toBe(false);
+ });
+ });
});
diff --git a/spec/frontend/vue_shared/directives/autofocusonshow_spec.js b/spec/frontend/vue_shared/directives/autofocusonshow_spec.js
index 59ce9f086c3..d052c99ec0e 100644
--- a/spec/frontend/vue_shared/directives/autofocusonshow_spec.js
+++ b/spec/frontend/vue_shared/directives/autofocusonshow_spec.js
@@ -1,3 +1,4 @@
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import autofocusonshow from '~/vue_shared/directives/autofocusonshow';
/**
@@ -10,10 +11,14 @@ describe('AutofocusOnShow directive', () => {
let el;
beforeEach(() => {
- setFixtures('<div id="container" style="display: none;"><input id="inputel"/></div>');
+ setHTMLFixture('<div id="container" style="display: none;"><input id="inputel"/></div>');
el = document.querySelector('#inputel');
});
+ afterEach(() => {
+ resetHTMLFixture();
+ });
+
it('should bind IntersectionObserver on input element', () => {
jest.spyOn(el, 'focus').mockImplementation(() => {});
diff --git a/spec/frontend/vue_shared/issuable/list/components/issuable_bulk_edit_sidebar_spec.js b/spec/frontend/vue_shared/issuable/list/components/issuable_bulk_edit_sidebar_spec.js
index 7dfeced571a..a25f92c9cf2 100644
--- a/spec/frontend/vue_shared/issuable/list/components/issuable_bulk_edit_sidebar_spec.js
+++ b/spec/frontend/vue_shared/issuable/list/components/issuable_bulk_edit_sidebar_spec.js
@@ -1,6 +1,7 @@
import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import IssuableBulkEditSidebar from '~/vue_shared/issuable/list/components/issuable_bulk_edit_sidebar.vue';
const createComponent = ({ expanded = true } = {}) =>
@@ -22,12 +23,13 @@ describe('IssuableBulkEditSidebar', () => {
let wrapper;
beforeEach(() => {
- setFixtures('<div class="layout-page right-sidebar-collapsed"></div>');
+ setHTMLFixture('<div class="layout-page right-sidebar-collapsed"></div>');
wrapper = createComponent();
});
afterEach(() => {
wrapper.destroy();
+ resetHTMLFixture();
});
describe('watch', () => {
diff --git a/spec/frontend/vue_shared/issuable/show/components/issuable_edit_form_spec.js b/spec/frontend/vue_shared/issuable/show/components/issuable_edit_form_spec.js
index b79dc0bf976..d3e484cf913 100644
--- a/spec/frontend/vue_shared/issuable/show/components/issuable_edit_form_spec.js
+++ b/spec/frontend/vue_shared/issuable/show/components/issuable_edit_form_spec.js
@@ -36,7 +36,6 @@ describe('IssuableEditForm', () => {
beforeEach(() => {
wrapper = createComponent();
- gon.features = { markdownContinueLists: true };
});
afterEach(() => {
diff --git a/spec/frontend/vue_shared/issuable/show/components/issuable_header_spec.js b/spec/frontend/vue_shared/issuable/show/components/issuable_header_spec.js
index 1cdd709159f..544db891a13 100644
--- a/spec/frontend/vue_shared/issuable/show/components/issuable_header_spec.js
+++ b/spec/frontend/vue_shared/issuable/show/components/issuable_header_spec.js
@@ -1,8 +1,6 @@
import { GlIcon, GlAvatarLabeled } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
-import { nextTick } from 'vue';
-import { extendedWrapper } from 'helpers/vue_test_utils_helper';
-
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import IssuableHeader from '~/vue_shared/issuable/show/components/issuable_header.vue';
import { mockIssuableShowProps, mockIssuable } from '../mock_data';
@@ -12,10 +10,17 @@ const issuableHeaderProps = {
...mockIssuableShowProps,
};
-const createComponent = (propsData = issuableHeaderProps, { stubs } = {}) =>
- extendedWrapper(
- shallowMount(IssuableHeader, {
- propsData,
+describe('IssuableHeader', () => {
+ let wrapper;
+
+ const findTaskStatusEl = () => wrapper.findByTestId('task-status');
+
+ const createComponent = (props = {}, { stubs } = {}) => {
+ wrapper = shallowMountExtended(IssuableHeader, {
+ propsData: {
+ ...issuableHeaderProps,
+ ...props,
+ },
slots: {
'status-badge': 'Open',
'header-actions': `
@@ -24,23 +29,18 @@ const createComponent = (propsData = issuableHeaderProps, { stubs } = {}) =>
`,
},
stubs,
- }),
- );
-
-describe('IssuableHeader', () => {
- let wrapper;
-
- beforeEach(() => {
- wrapper = createComponent();
- });
+ });
+ };
afterEach(() => {
wrapper.destroy();
+ resetHTMLFixture();
});
describe('computed', () => {
describe('authorId', () => {
it('returns numeric ID from GraphQL ID of `author` prop', () => {
+ createComponent();
expect(wrapper.vm.authorId).toBe(1);
});
});
@@ -48,10 +48,11 @@ describe('IssuableHeader', () => {
describe('handleRightSidebarToggleClick', () => {
beforeEach(() => {
- setFixtures('<button class="js-toggle-right-sidebar-button">Collapse sidebar</button>');
+ setHTMLFixture('<button class="js-toggle-right-sidebar-button">Collapse sidebar</button>');
});
it('dispatches `click` event on sidebar toggle button', () => {
+ createComponent();
wrapper.vm.toggleSidebarButtonEl = document.querySelector('.js-toggle-right-sidebar-button');
jest.spyOn(wrapper.vm.toggleSidebarButtonEl, 'dispatchEvent').mockImplementation(jest.fn);
@@ -67,20 +68,21 @@ describe('IssuableHeader', () => {
describe('template', () => {
it('renders issuable status icon and text', () => {
+ createComponent();
const statusBoxEl = wrapper.findByTestId('status');
+ const statusIconEl = statusBoxEl.findComponent(GlIcon);
expect(statusBoxEl.exists()).toBe(true);
- expect(statusBoxEl.find(GlIcon).props('name')).toBe(mockIssuableShowProps.statusIcon);
+ expect(statusIconEl.props('name')).toBe(mockIssuableShowProps.statusIcon);
+ expect(statusIconEl.attributes('class')).toBe(mockIssuableShowProps.statusIconClass);
expect(statusBoxEl.text()).toContain('Open');
});
it('renders blocked icon when issuable is blocked', async () => {
- wrapper.setProps({
+ createComponent({
blocked: true,
});
- await nextTick();
-
const blockedEl = wrapper.findByTestId('blocked');
expect(blockedEl.exists()).toBe(true);
@@ -88,12 +90,10 @@ describe('IssuableHeader', () => {
});
it('renders confidential icon when issuable is confidential', async () => {
- wrapper.setProps({
+ createComponent({
confidential: true,
});
- await nextTick();
-
const confidentialEl = wrapper.findByTestId('confidential');
expect(confidentialEl.exists()).toBe(true);
@@ -101,6 +101,7 @@ describe('IssuableHeader', () => {
});
it('renders issuable author avatar', () => {
+ createComponent();
const { username, name, webUrl, avatarUrl } = mockIssuable.author;
const avatarElAttrs = {
'data-user-id': '1',
@@ -120,28 +121,26 @@ describe('IssuableHeader', () => {
expect(avatarEl.find(GlAvatarLabeled).find(GlIcon).exists()).toBe(false);
});
- it('renders tast status text when `taskCompletionStatus` prop is defined', () => {
- let taskStatusEl = wrapper.findByTestId('task-status');
+ it('renders task status text when `taskCompletionStatus` prop is defined', () => {
+ createComponent();
- expect(taskStatusEl.exists()).toBe(true);
- expect(taskStatusEl.text()).toContain('0 of 5 tasks completed');
+ expect(findTaskStatusEl().exists()).toBe(true);
+ expect(findTaskStatusEl().text()).toContain('0 of 5 tasks completed');
+ });
- const wrapperSingleTask = createComponent({
- ...issuableHeaderProps,
+ it('does not render task status text when tasks count is 0', () => {
+ createComponent({
taskCompletionStatus: {
+ count: 0,
completedCount: 0,
- count: 1,
},
});
- taskStatusEl = wrapperSingleTask.findByTestId('task-status');
-
- expect(taskStatusEl.text()).toContain('0 of 1 task completed');
-
- wrapperSingleTask.destroy();
+ expect(findTaskStatusEl().exists()).toBe(false);
});
it('renders sidebar toggle button', () => {
+ createComponent();
const toggleButtonEl = wrapper.findByTestId('sidebar-toggle');
expect(toggleButtonEl.exists()).toBe(true);
@@ -149,6 +148,7 @@ describe('IssuableHeader', () => {
});
it('renders header actions', () => {
+ createComponent();
const actionsEl = wrapper.findByTestId('header-actions');
expect(actionsEl.find('button.js-close').exists()).toBe(true);
@@ -157,9 +157,8 @@ describe('IssuableHeader', () => {
describe('when author exists outside of GitLab', () => {
it("renders 'external-link' icon in avatar label", () => {
- wrapper = createComponent(
+ createComponent(
{
- ...issuableHeaderProps,
author: {
...issuableHeaderProps.author,
webUrl: 'https://jira.com/test-user/author.jpg',
diff --git a/spec/frontend/vue_shared/issuable/show/components/issuable_show_root_spec.js b/spec/frontend/vue_shared/issuable/show/components/issuable_show_root_spec.js
index d1eb1366225..8b027f990a2 100644
--- a/spec/frontend/vue_shared/issuable/show/components/issuable_show_root_spec.js
+++ b/spec/frontend/vue_shared/issuable/show/components/issuable_show_root_spec.js
@@ -49,6 +49,7 @@ describe('IssuableShowRoot', () => {
const {
statusBadgeClass,
statusIcon,
+ statusIconClass,
enableEdit,
enableAutocomplete,
editFormVisible,
@@ -56,7 +57,7 @@ describe('IssuableShowRoot', () => {
descriptionHelpPath,
taskCompletionStatus,
} = mockIssuableShowProps;
- const { blocked, confidential, createdAt, author } = mockIssuable;
+ const { state, blocked, confidential, createdAt, author } = mockIssuable;
it('renders component container element with class `issuable-show-container`', () => {
expect(wrapper.classes()).toContain('issuable-show-container');
@@ -67,15 +68,17 @@ describe('IssuableShowRoot', () => {
expect(issuableHeader.exists()).toBe(true);
expect(issuableHeader.props()).toMatchObject({
+ issuableState: state,
statusBadgeClass,
statusIcon,
+ statusIconClass,
blocked,
confidential,
createdAt,
author,
taskCompletionStatus,
});
- expect(issuableHeader.find('.issuable-status-box').text()).toContain('Open');
+ expect(issuableHeader.find('.issuable-status-badge').text()).toContain('Open');
expect(issuableHeader.find('.detail-page-header-actions button.js-close').exists()).toBe(
true,
);
diff --git a/spec/frontend/vue_shared/issuable/show/mock_data.js b/spec/frontend/vue_shared/issuable/show/mock_data.js
index f5f3ed58655..32bb9edfe08 100644
--- a/spec/frontend/vue_shared/issuable/show/mock_data.js
+++ b/spec/frontend/vue_shared/issuable/show/mock_data.js
@@ -36,8 +36,9 @@ export const mockIssuableShowProps = {
enableTaskList: true,
enableEdit: true,
showFieldTitle: false,
- statusBadgeClass: 'status-box-open',
- statusIcon: 'issue-open-m',
+ statusBadgeClass: 'issuable-status-badge-open',
+ statusIcon: 'issues',
+ statusIconClass: 'gl-sm-display-none',
taskCompletionStatus: {
completedCount: 0,
count: 5,
diff --git a/spec/frontend/vue_shared/issuable/sidebar/components/issuable_sidebar_root_spec.js b/spec/frontend/vue_shared/issuable/sidebar/components/issuable_sidebar_root_spec.js
index 47bf3c8ed83..6c9e5f85fa0 100644
--- a/spec/frontend/vue_shared/issuable/sidebar/components/issuable_sidebar_root_spec.js
+++ b/spec/frontend/vue_shared/issuable/sidebar/components/issuable_sidebar_root_spec.js
@@ -1,6 +1,7 @@
import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
-import Cookies from 'js-cookie';
import { nextTick } from 'vue';
+import Cookies from '~/lib/utils/cookies';
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import IssuableSidebarRoot from '~/vue_shared/issuable/sidebar/components/issuable_sidebar_root.vue';
@@ -9,7 +10,7 @@ import { USER_COLLAPSED_GUTTER_COOKIE } from '~/vue_shared/issuable/sidebar/cons
const MOCK_LAYOUT_PAGE_CLASS = 'layout-page';
const createComponent = () => {
- setFixtures(`<div class="${MOCK_LAYOUT_PAGE_CLASS}"></div>`);
+ setHTMLFixture(`<div class="${MOCK_LAYOUT_PAGE_CLASS}"></div>`);
return shallowMountExtended(IssuableSidebarRoot, {
slots: {
@@ -38,6 +39,7 @@ describe('IssuableSidebarRoot', () => {
afterEach(() => {
wrapper.destroy();
+ resetHTMLFixture();
});
describe('when sidebar is expanded', () => {
diff --git a/spec/frontend/security_configuration/components/section_layout_spec.js b/spec/frontend/vue_shared/security_configuration/components/section_layout_spec.js
index 75da380bbb8..136fe74b0d6 100644
--- a/spec/frontend/security_configuration/components/section_layout_spec.js
+++ b/spec/frontend/vue_shared/security_configuration/components/section_layout_spec.js
@@ -1,6 +1,7 @@
import { mount } from '@vue/test-utils';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
-import SectionLayout from '~/security_configuration/components/section_layout.vue';
+import SectionLayout from '~/vue_shared/security_configuration/components/section_layout.vue';
+import SectionLoader from '~/vue_shared/security_configuration/components/section_loader.vue';
describe('Section Layout component', () => {
let wrapper;
@@ -18,6 +19,7 @@ describe('Section Layout component', () => {
};
const findHeading = () => wrapper.find('h2');
+ const findLoader = () => wrapper.findComponent(SectionLoader);
afterEach(() => {
wrapper.destroy();
@@ -46,4 +48,11 @@ describe('Section Layout component', () => {
});
});
});
+
+ describe('loading state', () => {
+ it('should show loaders when loading', () => {
+ createComponent({ heading: 'testheading', isLoading: true });
+ expect(findLoader().exists()).toBe(true);
+ });
+ });
});
diff --git a/spec/frontend/vue_shared/security_reports/mock_data.js b/spec/frontend/vue_shared/security_reports/mock_data.js
index dac9accbbf5..a9ad675e538 100644
--- a/spec/frontend/vue_shared/security_reports/mock_data.js
+++ b/spec/frontend/vue_shared/security_reports/mock_data.js
@@ -62,7 +62,7 @@ export const mockFindings = [
report_type: 'dependency_scanning',
name: '3rd party CORS request may execute in jquery',
severity: 'high',
- scanner: { external_id: 'retire.js', name: 'Retire.js' },
+ scanner: { external_id: 'gemnasium', name: 'gemnasium' },
identifiers: [
{
external_type: 'cve',
@@ -145,7 +145,7 @@ export const mockFindings = [
name:
'jQuery before 3.4.0, as used in Drupal, Backdrop CMS, and other products, mishandles jQuery.extend(true, {}, ...) because of Object.prototype pollution in jquery',
severity: 'low',
- scanner: { external_id: 'retire.js', name: 'Retire.js' },
+ scanner: { external_id: 'gemnasium', name: 'gemnasium' },
identifiers: [
{
external_type: 'cve',
@@ -227,7 +227,7 @@ export const mockFindings = [
name:
'jQuery before 3.4.0, as used in Drupal, Backdrop CMS, and other products, mishandles jQuery.extend(true, {}, ...) because of Object.prototype pollution in jquery',
severity: 'low',
- scanner: { external_id: 'retire.js', name: 'Retire.js' },
+ scanner: { external_id: 'gemnasium', name: 'gemnasium' },
identifiers: [
{
external_type: 'cve',
diff --git a/spec/frontend/whats_new/components/feature_spec.js b/spec/frontend/whats_new/components/feature_spec.js
index 8f4b4b08f50..b6627c257ff 100644
--- a/spec/frontend/whats_new/components/feature_spec.js
+++ b/spec/frontend/whats_new/components/feature_spec.js
@@ -21,6 +21,7 @@ describe("What's new single feature", () => {
const findReleaseDate = () => wrapper.find('[data-testid="release-date"]');
const findBodyAnchor = () => wrapper.find('[data-testid="body-content"] a');
+ const findImageLink = () => wrapper.find('[data-testid="whats-new-image-link"]');
const createWrapper = ({ feature } = {}) => {
wrapper = shallowMount(Feature, {
@@ -35,18 +36,38 @@ describe("What's new single feature", () => {
it('renders the date', () => {
createWrapper({ feature: exampleFeature });
+
expect(findReleaseDate().text()).toBe('April 22, 2021');
});
- describe('when the published_at is null', () => {
- it("doesn't render the date", () => {
+ it('renders image link', () => {
+ createWrapper({ feature: exampleFeature });
+
+ expect(findImageLink().exists()).toBe(true);
+ expect(findImageLink().find('div').attributes('style')).toBe(
+ `background-image: url(${exampleFeature.image_url});`,
+ );
+ });
+
+ describe('when published_at is null', () => {
+ it('does not render the date', () => {
createWrapper({ feature: { ...exampleFeature, published_at: null } });
+
expect(findReleaseDate().exists()).toBe(false);
});
});
+ describe('when image_url is null', () => {
+ it('does not render image link', () => {
+ createWrapper({ feature: { ...exampleFeature, image_url: null } });
+
+ expect(findImageLink().exists()).toBe(false);
+ });
+ });
+
it('safe-html config allows target attribute on elements', () => {
createWrapper({ feature: exampleFeature });
+
expect(findBodyAnchor().attributes()).toEqual({
href: expect.any(String),
rel: 'noopener noreferrer',
diff --git a/spec/frontend/whats_new/utils/notification_spec.js b/spec/frontend/whats_new/utils/notification_spec.js
index ef61462a3c5..dac02ee07bd 100644
--- a/spec/frontend/whats_new/utils/notification_spec.js
+++ b/spec/frontend/whats_new/utils/notification_spec.js
@@ -1,3 +1,4 @@
+import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import { useLocalStorageSpy } from 'helpers/local_storage_helper';
import { setNotification, getVersionDigest } from '~/whats_new/utils/notification';
@@ -11,12 +12,13 @@ describe('~/whats_new/utils/notification', () => {
const getAppEl = () => wrapper.querySelector('.app');
beforeEach(() => {
- loadFixtures('static/whats_new_notification.html');
+ loadHTMLFixture('static/whats_new_notification.html');
wrapper = document.querySelector('.whats-new-notification-fixture-root');
});
afterEach(() => {
wrapper.remove();
+ resetHTMLFixture();
});
describe('setNotification', () => {
diff --git a/spec/frontend/wikis_spec.js b/spec/frontend/wikis_spec.js
index c4e914bcf34..d8748c65da7 100644
--- a/spec/frontend/wikis_spec.js
+++ b/spec/frontend/wikis_spec.js
@@ -1,5 +1,5 @@
import { escape } from 'lodash';
-import { setHTMLFixture } from 'helpers/fixtures';
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import Wikis from '~/pages/shared/wikis/wikis';
import Tracking from '~/tracking';
@@ -21,6 +21,10 @@ describe('Wikis', () => {
Wikis.trackPageView();
});
+ afterEach(() => {
+ resetHTMLFixture();
+ });
+
it('sends the tracking event and context', () => {
expect(Tracking.event).toHaveBeenCalledWith(trackingPage, 'view_wiki_page', {
label: 'view_wiki_page',
diff --git a/spec/frontend/work_items/components/item_state_spec.js b/spec/frontend/work_items/components/item_state_spec.js
new file mode 100644
index 00000000000..79b76f3c061
--- /dev/null
+++ b/spec/frontend/work_items/components/item_state_spec.js
@@ -0,0 +1,54 @@
+import { mount } from '@vue/test-utils';
+import { STATE_OPEN, STATE_CLOSED } from '~/work_items/constants';
+import ItemState from '~/work_items/components/item_state.vue';
+
+describe('ItemState', () => {
+ let wrapper;
+
+ const findLabel = () => wrapper.find('label').text();
+ const selectedValue = () => wrapper.find('option:checked').element.value;
+
+ const clickOpen = () => wrapper.findAll('option').at(0).setSelected();
+
+ const createComponent = ({ state = STATE_OPEN, disabled = false } = {}) => {
+ wrapper = mount(ItemState, {
+ propsData: {
+ state,
+ disabled,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('renders label and dropdown', () => {
+ createComponent();
+
+ expect(findLabel()).toBe('Status');
+ expect(selectedValue()).toBe(STATE_OPEN);
+ });
+
+ it('renders dropdown for closed', () => {
+ createComponent({ state: STATE_CLOSED });
+
+ expect(selectedValue()).toBe(STATE_CLOSED);
+ });
+
+ it('emits changed event', async () => {
+ createComponent({ state: STATE_CLOSED });
+
+ await clickOpen();
+
+ expect(wrapper.emitted('changed')).toEqual([[STATE_OPEN]]);
+ });
+
+ it('does not emits changed event if clicking selected value', async () => {
+ createComponent({ state: STATE_OPEN });
+
+ await clickOpen();
+
+ expect(wrapper.emitted('changed')).toBeUndefined();
+ });
+});
diff --git a/spec/frontend/work_items/components/work_item_actions_spec.js b/spec/frontend/work_items/components/work_item_actions_spec.js
index d0e9cfee353..137a0a7326d 100644
--- a/spec/frontend/work_items/components/work_item_actions_spec.js
+++ b/spec/frontend/work_items/components/work_item_actions_spec.js
@@ -1,30 +1,18 @@
import { GlDropdownItem, GlModal } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import Vue from 'vue';
-import VueApollo from 'vue-apollo';
-import waitForPromises from 'helpers/wait_for_promises';
-import createMockApollo from 'helpers/mock_apollo_helper';
import WorkItemActions from '~/work_items/components/work_item_actions.vue';
-import deleteWorkItem from '~/work_items/graphql/delete_work_item.mutation.graphql';
-import { deleteWorkItemResponse, deleteWorkItemFailureResponse } from '../mock_data';
describe('WorkItemActions component', () => {
let wrapper;
let glModalDirective;
- Vue.use(VueApollo);
-
const findModal = () => wrapper.findComponent(GlModal);
const findDeleteButton = () => wrapper.findComponent(GlDropdownItem);
- const createComponent = ({
- canUpdate = true,
- deleteWorkItemHandler = jest.fn().mockResolvedValue(deleteWorkItemResponse),
- } = {}) => {
+ const createComponent = ({ canDelete = true } = {}) => {
glModalDirective = jest.fn();
wrapper = shallowMount(WorkItemActions, {
- apolloProvider: createMockApollo([[deleteWorkItem, deleteWorkItemHandler]]),
- propsData: { workItemId: '123', canUpdate },
+ propsData: { workItemId: '123', canDelete },
directives: {
glModal: {
bind(_, { value }) {
@@ -54,48 +42,17 @@ describe('WorkItemActions component', () => {
expect(glModalDirective).toHaveBeenCalled();
});
- it('calls delete mutation when clicking OK button', () => {
- const deleteWorkItemHandler = jest.fn().mockResolvedValue(deleteWorkItemResponse);
-
- createComponent({
- deleteWorkItemHandler,
- });
-
- findModal().vm.$emit('ok');
-
- expect(deleteWorkItemHandler).toHaveBeenCalled();
- expect(wrapper.emitted('error')).toBeUndefined();
- });
-
- it('emits event after delete success', async () => {
+ it('emits event when clicking OK button', () => {
createComponent();
findModal().vm.$emit('ok');
- await waitForPromises();
-
- expect(wrapper.emitted('workItemDeleted')).not.toBeUndefined();
- expect(wrapper.emitted('error')).toBeUndefined();
- });
-
- it('emits error event after delete failure', async () => {
- createComponent({
- deleteWorkItemHandler: jest.fn().mockResolvedValue(deleteWorkItemFailureResponse),
- });
-
- findModal().vm.$emit('ok');
-
- await waitForPromises();
-
- expect(wrapper.emitted('error')[0]).toEqual([
- "The resource that you are attempting to access does not exist or you don't have permission to perform this action",
- ]);
- expect(wrapper.emitted('workItemDeleted')).toBeUndefined();
+ expect(wrapper.emitted('deleteWorkItem')).toEqual([[]]);
});
- it('does not render when canUpdate is false', () => {
+ it('does not render when canDelete is false', () => {
createComponent({
- canUpdate: false,
+ canDelete: false,
});
expect(wrapper.html()).toBe('');
diff --git a/spec/frontend/work_items/components/work_item_detail_modal_spec.js b/spec/frontend/work_items/components/work_item_detail_modal_spec.js
index 9f35ccb853b..aaabdbc82d9 100644
--- a/spec/frontend/work_items/components/work_item_detail_modal_spec.js
+++ b/spec/frontend/work_items/components/work_item_detail_modal_spec.js
@@ -1,23 +1,57 @@
-import { GlModal } from '@gitlab/ui';
+import { GlAlert } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import Vue from 'vue';
+import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
+import waitForPromises from 'helpers/wait_for_promises';
+import createMockApollo from 'helpers/mock_apollo_helper';
import WorkItemDetail from '~/work_items/components/work_item_detail.vue';
import WorkItemDetailModal from '~/work_items/components/work_item_detail_modal.vue';
-import WorkItemActions from '~/work_items/components/work_item_actions.vue';
+import deleteWorkItemFromTaskMutation from '~/work_items/graphql/delete_task_from_work_item.mutation.graphql';
describe('WorkItemDetailModal component', () => {
let wrapper;
Vue.use(VueApollo);
+ const hideModal = jest.fn();
+ const GlModal = {
+ template: `
+ <div>
+ <slot></slot>
+ </div>
+ `,
+ methods: {
+ hide: hideModal,
+ },
+ };
+
const findModal = () => wrapper.findComponent(GlModal);
- const findWorkItemActions = () => wrapper.findComponent(WorkItemActions);
+ const findAlert = () => wrapper.findComponent(GlAlert);
const findWorkItemDetail = () => wrapper.findComponent(WorkItemDetail);
- const createComponent = ({ visible = true, workItemId = '1', canUpdate = false } = {}) => {
+ const createComponent = ({ workItemId = '1', error = false } = {}) => {
+ const apolloProvider = createMockApollo([
+ [
+ deleteWorkItemFromTaskMutation,
+ jest.fn().mockResolvedValue({
+ data: {
+ workItemDeleteTask: {
+ workItem: { id: 123, descriptionHtml: 'updated work item desc' },
+ errors: [],
+ },
+ },
+ }),
+ ],
+ ]);
+
wrapper = shallowMount(WorkItemDetailModal, {
- propsData: { visible, workItemId, canUpdate },
+ apolloProvider,
+ propsData: { workItemId },
+ data() {
+ return {
+ error,
+ };
+ },
stubs: {
GlModal,
},
@@ -28,31 +62,59 @@ describe('WorkItemDetailModal component', () => {
wrapper.destroy();
});
- describe.each([true, false])('when visible=%s', (visible) => {
- it(`${visible ? 'renders' : 'does not render'} modal`, () => {
- createComponent({ visible });
+ it('renders WorkItemDetail', () => {
+ createComponent();
- expect(findModal().props('visible')).toBe(visible);
+ expect(findWorkItemDetail().props()).toEqual({
+ workItemId: '1',
});
});
- it('renders heading', () => {
+ it('renders alert if there is an error', () => {
+ createComponent({ error: true });
+
+ expect(findAlert().exists()).toBe(true);
+ });
+
+ it('does not render alert if there is no error', () => {
createComponent();
- expect(wrapper.find('h2').text()).toBe('Work Item');
+ expect(findAlert().exists()).toBe(false);
});
- it('renders WorkItemDetail', () => {
+ it('dismisses the alert on `dismiss` emitted event', async () => {
+ createComponent({ error: true });
+ findAlert().vm.$emit('dismiss');
+ await nextTick();
+
+ expect(findAlert().exists()).toBe(false);
+ });
+
+ it('emits `close` event on hiding the modal', () => {
createComponent();
+ findModal().vm.$emit('hide');
- expect(findWorkItemDetail().props()).toEqual({ workItemId: '1' });
+ expect(wrapper.emitted('close')).toBeTruthy();
});
- it('shows work item actions', () => {
- createComponent({
- canUpdate: true,
- });
+ it('emits `workItemUpdated` event on updating work item', () => {
+ createComponent();
+ findWorkItemDetail().vm.$emit('workItemUpdated');
+
+ expect(wrapper.emitted('workItemUpdated')).toBeTruthy();
+ });
+
+ describe('delete work item', () => {
+ it('emits workItemDeleted and closes modal', async () => {
+ createComponent();
+ const newDesc = 'updated work item desc';
+
+ findWorkItemDetail().vm.$emit('deleteWorkItem');
- expect(findWorkItemActions().exists()).toBe(true);
+ await waitForPromises();
+
+ expect(wrapper.emitted('workItemDeleted')).toEqual([[newDesc]]);
+ expect(hideModal).toHaveBeenCalled();
+ });
});
});
diff --git a/spec/frontend/work_items/components/work_item_state_spec.js b/spec/frontend/work_items/components/work_item_state_spec.js
new file mode 100644
index 00000000000..9e48f56d9e9
--- /dev/null
+++ b/spec/frontend/work_items/components/work_item_state_spec.js
@@ -0,0 +1,117 @@
+import { shallowMount } from '@vue/test-utils';
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import { mockTracking } from 'helpers/tracking_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import ItemState from '~/work_items/components/item_state.vue';
+import WorkItemState from '~/work_items/components/work_item_state.vue';
+import {
+ i18n,
+ STATE_OPEN,
+ STATE_CLOSED,
+ STATE_EVENT_CLOSE,
+ STATE_EVENT_REOPEN,
+} from '~/work_items/constants';
+import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
+import { updateWorkItemMutationResponse, workItemQueryResponse } from '../mock_data';
+
+describe('WorkItemState component', () => {
+ let wrapper;
+
+ Vue.use(VueApollo);
+
+ const mutationSuccessHandler = jest.fn().mockResolvedValue(updateWorkItemMutationResponse);
+
+ const findItemState = () => wrapper.findComponent(ItemState);
+
+ const createComponent = ({
+ state = STATE_OPEN,
+ mutationHandler = mutationSuccessHandler,
+ } = {}) => {
+ const { id, workItemType } = workItemQueryResponse.data.workItem;
+ wrapper = shallowMount(WorkItemState, {
+ apolloProvider: createMockApollo([[updateWorkItemMutation, mutationHandler]]),
+ propsData: {
+ workItem: {
+ id,
+ state,
+ workItemType,
+ },
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('renders state', () => {
+ createComponent();
+
+ expect(findItemState().props('state')).toBe(workItemQueryResponse.data.workItem.state);
+ });
+
+ describe('when updating the state', () => {
+ it('calls a mutation', () => {
+ createComponent();
+
+ findItemState().vm.$emit('changed', STATE_CLOSED);
+
+ expect(mutationSuccessHandler).toHaveBeenCalledWith({
+ input: {
+ id: workItemQueryResponse.data.workItem.id,
+ stateEvent: STATE_EVENT_CLOSE,
+ },
+ });
+ });
+
+ it('calls a mutation with REOPEN', () => {
+ createComponent({
+ state: STATE_CLOSED,
+ });
+
+ findItemState().vm.$emit('changed', STATE_OPEN);
+
+ expect(mutationSuccessHandler).toHaveBeenCalledWith({
+ input: {
+ id: workItemQueryResponse.data.workItem.id,
+ stateEvent: STATE_EVENT_REOPEN,
+ },
+ });
+ });
+
+ it('emits updated event', async () => {
+ createComponent();
+
+ findItemState().vm.$emit('changed', STATE_CLOSED);
+ await waitForPromises();
+
+ expect(wrapper.emitted('updated')).toEqual([[]]);
+ });
+
+ it('emits an error message when the mutation was unsuccessful', async () => {
+ createComponent({ mutationHandler: jest.fn().mockRejectedValue('Error!') });
+
+ findItemState().vm.$emit('changed', STATE_CLOSED);
+ await waitForPromises();
+
+ expect(wrapper.emitted('error')).toEqual([[i18n.updateError]]);
+ });
+
+ it('tracks editing the state', async () => {
+ const trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
+
+ createComponent();
+
+ findItemState().vm.$emit('changed', STATE_CLOSED);
+ await waitForPromises();
+
+ expect(trackingSpy).toHaveBeenCalledWith('workItems:show', 'updated_state', {
+ category: 'workItems:show',
+ label: 'item_state',
+ property: 'type_Task',
+ });
+ });
+ });
+});
diff --git a/spec/frontend/work_items/components/work_item_title_spec.js b/spec/frontend/work_items/components/work_item_title_spec.js
index 9b1ef2d14e4..19b56362ac0 100644
--- a/spec/frontend/work_items/components/work_item_title_spec.js
+++ b/spec/frontend/work_items/components/work_item_title_spec.js
@@ -1,4 +1,3 @@
-import { GlLoadingIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
@@ -18,15 +17,13 @@ describe('WorkItemTitle component', () => {
const mutationSuccessHandler = jest.fn().mockResolvedValue(updateWorkItemMutationResponse);
- const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findItemTitle = () => wrapper.findComponent(ItemTitle);
- const createComponent = ({ loading = false, mutationHandler = mutationSuccessHandler } = {}) => {
+ const createComponent = ({ mutationHandler = mutationSuccessHandler } = {}) => {
const { id, title, workItemType } = workItemQueryResponse.data.workItem;
wrapper = shallowMount(WorkItemTitle, {
apolloProvider: createMockApollo([[updateWorkItemMutation, mutationHandler]]),
propsData: {
- loading,
workItemId: id,
workItemTitle: title,
workItemType: workItemType.name,
@@ -38,32 +35,10 @@ describe('WorkItemTitle component', () => {
wrapper.destroy();
});
- describe('when loading', () => {
- beforeEach(() => {
- createComponent({ loading: true });
- });
-
- it('renders loading spinner', () => {
- expect(findLoadingIcon().exists()).toBe(true);
- });
-
- it('does not render title', () => {
- expect(findItemTitle().exists()).toBe(false);
- });
- });
-
- describe('when loaded', () => {
- beforeEach(() => {
- createComponent({ loading: false });
- });
-
- it('does not render loading spinner', () => {
- expect(findLoadingIcon().exists()).toBe(false);
- });
+ it('renders title', () => {
+ createComponent();
- it('renders title', () => {
- expect(findItemTitle().props('title')).toBe(workItemQueryResponse.data.workItem.title);
- });
+ expect(findItemTitle().props('title')).toBe(workItemQueryResponse.data.workItem.title);
});
describe('when updating the title', () => {
@@ -82,6 +57,15 @@ describe('WorkItemTitle component', () => {
});
});
+ it('emits updated event', async () => {
+ createComponent();
+
+ findItemTitle().vm.$emit('title-changed', 'new title');
+ await waitForPromises();
+
+ expect(wrapper.emitted('updated')).toEqual([[]]);
+ });
+
it('does not call a mutation when the title has not changed', () => {
createComponent();
diff --git a/spec/frontend/work_items/mock_data.js b/spec/frontend/work_items/mock_data.js
index 722e1708c15..f3483550013 100644
--- a/spec/frontend/work_items/mock_data.js
+++ b/spec/frontend/work_items/mock_data.js
@@ -4,11 +4,17 @@ export const workItemQueryResponse = {
__typename: 'WorkItem',
id: 'gid://gitlab/WorkItem/1',
title: 'Test',
+ state: 'OPEN',
+ description: 'description',
workItemType: {
__typename: 'WorkItemType',
id: 'gid://gitlab/WorkItems::Type/5',
name: 'Task',
},
+ userPermissions: {
+ deleteWorkItem: false,
+ updateWorkItem: false,
+ },
},
},
};
@@ -21,11 +27,17 @@ export const updateWorkItemMutationResponse = {
__typename: 'WorkItem',
id: 'gid://gitlab/WorkItem/1',
title: 'Updated title',
+ state: 'OPEN',
+ description: 'description',
workItemType: {
__typename: 'WorkItemType',
id: 'gid://gitlab/WorkItems::Type/5',
name: 'Task',
},
+ userPermissions: {
+ deleteWorkItem: false,
+ updateWorkItem: false,
+ },
},
},
},
@@ -39,6 +51,7 @@ export const projectWorkItemTypesQueryResponse = {
nodes: [
{ id: 'gid://gitlab/WorkItems::Type/1', name: 'Issue' },
{ id: 'gid://gitlab/WorkItems::Type/2', name: 'Incident' },
+ { id: 'gid://gitlab/WorkItems::Type/3', name: 'Task' },
],
},
},
@@ -53,11 +66,17 @@ export const createWorkItemMutationResponse = {
__typename: 'WorkItem',
id: 'gid://gitlab/WorkItem/1',
title: 'Updated title',
+ state: 'OPEN',
+ description: 'description',
workItemType: {
__typename: 'WorkItemType',
id: 'gid://gitlab/WorkItems::Type/5',
name: 'Task',
},
+ userPermissions: {
+ deleteWorkItem: false,
+ updateWorkItem: false,
+ },
},
},
},
@@ -72,6 +91,10 @@ export const createWorkItemFromTaskMutationResponse = {
descriptionHtml: '<p>New description</p>',
id: 'gid://gitlab/WorkItem/13',
__typename: 'WorkItem',
+ userPermissions: {
+ deleteWorkItem: false,
+ updateWorkItem: false,
+ },
},
},
},
diff --git a/spec/frontend/work_items/pages/create_work_item_spec.js b/spec/frontend/work_items/pages/create_work_item_spec.js
index fb1f1d56356..e89477ed599 100644
--- a/spec/frontend/work_items/pages/create_work_item_spec.js
+++ b/spec/frontend/work_items/pages/create_work_item_spec.js
@@ -158,6 +158,11 @@ describe('Create work item component', () => {
it('adds padding for content', () => {
expect(findContent().classes('gl-px-5')).toBe(true);
});
+
+ it('defaults type to `Task`', async () => {
+ await waitForPromises();
+ expect(findSelect().attributes('value')).toBe('gid://gitlab/WorkItems::Type/3');
+ });
});
it('displays a loading icon inside dropdown when work items query is loading', () => {
@@ -181,7 +186,7 @@ describe('Create work item component', () => {
});
it('displays a list of work item types', () => {
- expect(findSelect().attributes('options').split(',')).toHaveLength(3);
+ expect(findSelect().attributes('options').split(',')).toHaveLength(4);
});
it('selects a work item type on click', async () => {
diff --git a/spec/frontend/work_items/pages/work_item_detail_spec.js b/spec/frontend/work_items/pages/work_item_detail_spec.js
index 1eb6c0145e7..9f87655175c 100644
--- a/spec/frontend/work_items/pages/work_item_detail_spec.js
+++ b/spec/frontend/work_items/pages/work_item_detail_spec.js
@@ -1,10 +1,11 @@
-import { GlAlert } from '@gitlab/ui';
+import { GlAlert, 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 waitForPromises from 'helpers/wait_for_promises';
import WorkItemDetail from '~/work_items/components/work_item_detail.vue';
+import WorkItemState from '~/work_items/components/work_item_state.vue';
import WorkItemTitle from '~/work_items/components/work_item_title.vue';
import { i18n } from '~/work_items/constants';
import workItemQuery from '~/work_items/graphql/work_item.query.graphql';
@@ -20,7 +21,9 @@ describe('WorkItemDetail component', () => {
const initialSubscriptionHandler = jest.fn().mockResolvedValue(workItemTitleSubscriptionResponse);
const findAlert = () => wrapper.findComponent(GlAlert);
+ const findSkeleton = () => wrapper.findComponent(GlSkeletonLoader);
const findWorkItemTitle = () => wrapper.findComponent(WorkItemTitle);
+ const findWorkItemState = () => wrapper.findComponent(WorkItemState);
const createComponent = ({
workItemId = workItemQueryResponse.data.workItem.id,
@@ -55,8 +58,10 @@ describe('WorkItemDetail component', () => {
createComponent();
});
- it('renders WorkItemTitle in loading state', () => {
- expect(findWorkItemTitle().props('loading')).toBe(true);
+ it('renders skeleton loader', () => {
+ expect(findSkeleton().exists()).toBe(true);
+ expect(findWorkItemState().exists()).toBe(false);
+ expect(findWorkItemTitle().exists()).toBe(false);
});
});
@@ -66,8 +71,10 @@ describe('WorkItemDetail component', () => {
return waitForPromises();
});
- it('does not render WorkItemTitle in loading state', () => {
- expect(findWorkItemTitle().props('loading')).toBe(false);
+ it('does not render skeleton', () => {
+ expect(findSkeleton().exists()).toBe(false);
+ expect(findWorkItemState().exists()).toBe(true);
+ expect(findWorkItemTitle().exists()).toBe(true);
});
});
@@ -82,6 +89,7 @@ describe('WorkItemDetail component', () => {
it('shows an error message when WorkItemTitle emits an `error` event', async () => {
createComponent();
+ await waitForPromises();
findWorkItemTitle().vm.$emit('error', i18n.updateError);
await waitForPromises();
@@ -96,4 +104,18 @@ describe('WorkItemDetail component', () => {
issuableId: workItemQueryResponse.data.workItem.id,
});
});
+
+ it('emits workItemUpdated event when fields updated', async () => {
+ createComponent();
+
+ await waitForPromises();
+
+ findWorkItemState().vm.$emit('updated');
+
+ expect(wrapper.emitted('workItemUpdated')).toEqual([[]]);
+
+ findWorkItemTitle().vm.$emit('updated');
+
+ expect(wrapper.emitted('workItemUpdated')).toEqual([[], []]);
+ });
});
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 2803724b9af..85096392e84 100644
--- a/spec/frontend/work_items/pages/work_item_root_spec.js
+++ b/spec/frontend/work_items/pages/work_item_root_spec.js
@@ -1,21 +1,45 @@
+import { GlAlert } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
+import waitForPromises from 'helpers/wait_for_promises';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import { visitUrl } from '~/lib/utils/url_utility';
import WorkItemDetail from '~/work_items/components/work_item_detail.vue';
import WorkItemsRoot from '~/work_items/pages/work_item_root.vue';
+import deleteWorkItem from '~/work_items/graphql/delete_work_item.mutation.graphql';
+import { deleteWorkItemResponse, deleteWorkItemFailureResponse } from '../mock_data';
+
+jest.mock('~/lib/utils/url_utility', () => ({
+ visitUrl: jest.fn(),
+}));
Vue.use(VueApollo);
describe('Work items root component', () => {
let wrapper;
+ const issuesListPath = '/-/issues';
+ const mockToastShow = jest.fn();
const findWorkItemDetail = () => wrapper.findComponent(WorkItemDetail);
+ const findAlert = () => wrapper.findComponent(GlAlert);
- const createComponent = () => {
+ const createComponent = ({
+ deleteWorkItemHandler = jest.fn().mockResolvedValue(deleteWorkItemResponse),
+ } = {}) => {
wrapper = shallowMount(WorkItemsRoot, {
+ apolloProvider: createMockApollo([[deleteWorkItem, deleteWorkItemHandler]]),
+ provide: {
+ issuesListPath,
+ },
propsData: {
id: '1',
},
+ mocks: {
+ $toast: {
+ show: mockToastShow,
+ },
+ },
});
};
@@ -26,6 +50,38 @@ describe('Work items root component', () => {
it('renders WorkItemDetail', () => {
createComponent();
- expect(findWorkItemDetail().props()).toEqual({ workItemId: 'gid://gitlab/WorkItem/1' });
+ expect(findWorkItemDetail().props()).toEqual({
+ workItemId: 'gid://gitlab/WorkItem/1',
+ });
+ });
+
+ it('deletes work item when deleteWorkItem event emitted', async () => {
+ const deleteWorkItemHandler = jest.fn().mockResolvedValue(deleteWorkItemResponse);
+
+ createComponent({
+ deleteWorkItemHandler,
+ });
+
+ findWorkItemDetail().vm.$emit('deleteWorkItem');
+
+ await waitForPromises();
+
+ expect(deleteWorkItemHandler).toHaveBeenCalled();
+ expect(mockToastShow).toHaveBeenCalled();
+ expect(visitUrl).toHaveBeenCalledWith(issuesListPath);
+ });
+
+ it('shows alert if delete fails', async () => {
+ const deleteWorkItemHandler = jest.fn().mockRejectedValue(deleteWorkItemFailureResponse);
+
+ createComponent({
+ deleteWorkItemHandler,
+ });
+
+ findWorkItemDetail().vm.$emit('deleteWorkItem');
+
+ await waitForPromises();
+
+ expect(findAlert().exists()).toBe(true);
});
});
diff --git a/spec/frontend/work_items/router_spec.js b/spec/frontend/work_items/router_spec.js
index 7e68c5e4f0e..99dcd886f7b 100644
--- a/spec/frontend/work_items/router_spec.js
+++ b/spec/frontend/work_items/router_spec.js
@@ -17,6 +17,7 @@ describe('Work items router', () => {
router,
provide: {
fullPath: 'full-path',
+ issuesListPath: 'full-path/-/issues',
},
mocks: {
$apollo: {
diff --git a/spec/frontend/zen_mode_spec.js b/spec/frontend/zen_mode_spec.js
index 44684619fae..a88910b2613 100644
--- a/spec/frontend/zen_mode_spec.js
+++ b/spec/frontend/zen_mode_spec.js
@@ -3,6 +3,7 @@ import MockAdapter from 'axios-mock-adapter';
import Dropzone from 'dropzone';
import $ from 'jquery';
import Mousetrap from 'mousetrap';
+import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import GLForm from '~/gl_form';
import * as utils from '~/lib/utils/common_utils';
import ZenMode from '~/zen_mode';
@@ -33,7 +34,7 @@ describe('ZenMode', () => {
mock = new MockAdapter(axios);
mock.onGet().reply(200);
- loadFixtures(fixtureName);
+ loadHTMLFixture(fixtureName);
const form = $('.js-new-note-form');
new GLForm(form); // eslint-disable-line no-new
@@ -45,8 +46,10 @@ describe('ZenMode', () => {
// Set this manually because we can't actually scroll the window
zen.scroll_position = 456;
+ });
- gon.features = { markdownContinueLists: true };
+ afterEach(() => {
+ resetHTMLFixture();
});
describe('enabling dropzone', () => {