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-04-20 13:00:54 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2022-04-20 13:00:54 +0300
commit3cccd102ba543e02725d247893729e5c73b38295 (patch)
treef36a04ec38517f5deaaacb5acc7d949688d1e187 /spec/frontend
parent205943281328046ef7b4528031b90fbda70c75ac (diff)
Add latest changes from gitlab-org/gitlab@14-10-stable-eev14.10.0-rc42
Diffstat (limited to 'spec/frontend')
-rw-r--r--spec/frontend/__helpers__/matchers/index.js1
-rw-r--r--spec/frontend/__helpers__/matchers/to_validate_json_schema.js34
-rw-r--r--spec/frontend/__helpers__/matchers/to_validate_json_schema_spec.js65
-rw-r--r--spec/frontend/__helpers__/mock_apollo_helper.js2
-rw-r--r--spec/frontend/__helpers__/mock_dom_observer.js4
-rw-r--r--spec/frontend/__helpers__/vuex_action_helper.js1
-rw-r--r--spec/frontend/__helpers__/yaml_transformer.js11
-rw-r--r--spec/frontend/access_tokens/components/__snapshots__/expires_at_field_spec.js.snap2
-rw-r--r--spec/frontend/add_context_commits_modal/store/actions_spec.js67
-rw-r--r--spec/frontend/admin/statistics_panel/store/actions_spec.js37
-rw-r--r--spec/frontend/admin/topics/components/remove_avatar_spec.js11
-rw-r--r--spec/frontend/admin/users/components/actions/actions_spec.js42
-rw-r--r--spec/frontend/admin/users/components/modals/__snapshots__/delete_user_modal_spec.js.snap174
-rw-r--r--spec/frontend/admin/users/components/modals/delete_user_modal_spec.js104
-rw-r--r--spec/frontend/admin/users/components/modals/user_modal_manager_spec.js126
-rw-r--r--spec/frontend/alerts_settings/components/alerts_settings_wrapper_spec.js3
-rw-r--r--spec/frontend/alerts_settings/components/mocks/apollo_mock.js2
-rw-r--r--spec/frontend/api/alert_management_alerts_api_spec.js140
-rw-r--r--spec/frontend/api_spec.js600
-rw-r--r--spec/frontend/authentication/u2f/authenticate_spec.js15
-rw-r--r--spec/frontend/authentication/u2f/register_spec.js4
-rw-r--r--spec/frontend/badges/components/badge_spec.js6
-rw-r--r--spec/frontend/badges/store/actions_spec.js260
-rw-r--r--spec/frontend/batch_comments/stores/modules/batch_comments/actions_spec.js98
-rw-r--r--spec/frontend/behaviors/gl_emoji_spec.js6
-rw-r--r--spec/frontend/blob/balsamiq/balsamiq_viewer_spec.js363
-rw-r--r--spec/frontend/boards/boards_util_spec.js30
-rw-r--r--spec/frontend/boards/components/board_filtered_search_spec.js4
-rw-r--r--spec/frontend/boards/components/board_form_spec.js2
-rw-r--r--spec/frontend/boards/components/board_top_bar_spec.js88
-rw-r--r--spec/frontend/boards/components/boards_selector_spec.js14
-rw-r--r--spec/frontend/boards/components/issuable_title_spec.js33
-rw-r--r--spec/frontend/boards/components/issue_board_filtered_search_spec.js3
-rw-r--r--spec/frontend/boards/components/issue_time_estimate_spec.js8
-rw-r--r--spec/frontend/boards/components/item_count_spec.js6
-rw-r--r--spec/frontend/boards/stores/actions_spec.js70
-rw-r--r--spec/frontend/captcha/apollo_captcha_link_spec.js94
-rw-r--r--spec/frontend/ci_variable_list/components/ci_variable_modal_spec.js2
-rw-r--r--spec/frontend/ci_variable_list/store/actions_spec.js92
-rw-r--r--spec/frontend/clusters_list/components/agent_empty_state_spec.js20
-rw-r--r--spec/frontend/clusters_list/components/agent_table_spec.js2
-rw-r--r--spec/frontend/clusters_list/components/agent_token_spec.js8
-rw-r--r--spec/frontend/clusters_list/components/agents_spec.js2
-rw-r--r--spec/frontend/clusters_list/components/available_agents_dropwdown_spec.js21
-rw-r--r--spec/frontend/clusters_list/components/clusters_actions_spec.js107
-rw-r--r--spec/frontend/clusters_list/components/clusters_empty_state_spec.js45
-rw-r--r--spec/frontend/clusters_list/components/clusters_view_all_spec.js93
-rw-r--r--spec/frontend/clusters_list/mocks/apollo.js1
-rw-r--r--spec/frontend/clusters_list/store/actions_spec.js101
-rw-r--r--spec/frontend/code_navigation/components/app_spec.js7
-rw-r--r--spec/frontend/code_navigation/store/actions_spec.js64
-rw-r--r--spec/frontend/code_navigation/store/mutations_spec.js2
-rw-r--r--spec/frontend/code_navigation/utils/index_spec.js30
-rw-r--r--spec/frontend/commit/commit_box_pipeline_mini_graph_spec.js57
-rw-r--r--spec/frontend/commit/components/commit_box_pipeline_status_spec.js150
-rw-r--r--spec/frontend/commit/mock_data.js46
-rw-r--r--spec/frontend/commit/pipelines/pipelines_table_spec.js16
-rw-r--r--spec/frontend/commit/pipelines/utils_spec.js59
-rw-r--r--spec/frontend/commits_spec.js28
-rw-r--r--spec/frontend/confidential_merge_request/components/__snapshots__/project_form_group_spec.js.snap8
-rw-r--r--spec/frontend/content_editor/components/code_block_bubble_menu_spec.js142
-rw-r--r--spec/frontend/content_editor/components/formatting_bubble_menu_spec.js2
-rw-r--r--spec/frontend/content_editor/components/wrappers/media_spec.js (renamed from spec/frontend/content_editor/components/wrappers/image_spec.js)21
-rw-r--r--spec/frontend/content_editor/extensions/attachment_spec.js79
-rw-r--r--spec/frontend/content_editor/extensions/code_block_highlight_spec.js74
-rw-r--r--spec/frontend/content_editor/extensions/frontmatter_spec.js2
-rw-r--r--spec/frontend/content_editor/services/code_block_language_loader_spec.js120
-rw-r--r--spec/frontend/content_editor/services/content_editor_spec.js24
-rw-r--r--spec/frontend/contributors/store/actions_spec.js26
-rw-r--r--spec/frontend/create_cluster/gke_cluster/stores/actions_spec.js25
-rw-r--r--spec/frontend/crm/contact_form_spec.js157
-rw-r--r--spec/frontend/crm/contact_form_wrapper_spec.js88
-rw-r--r--spec/frontend/crm/contacts_root_spec.js77
-rw-r--r--spec/frontend/crm/form_spec.js51
-rw-r--r--spec/frontend/crm/mock_data.js25
-rw-r--r--spec/frontend/crm/new_organization_form_spec.js109
-rw-r--r--spec/frontend/crm/organization_form_wrapper_spec.js88
-rw-r--r--spec/frontend/crm/organizations_root_spec.js51
-rw-r--r--spec/frontend/design_management/components/design_notes/__snapshots__/design_note_spec.js.snap28
-rw-r--r--spec/frontend/design_management/components/design_notes/design_discussion_spec.js7
-rw-r--r--spec/frontend/design_management/components/design_notes/design_note_spec.js37
-rw-r--r--spec/frontend/design_management/pages/index_spec.js2
-rw-r--r--spec/frontend/diffs/components/commit_item_spec.js9
-rw-r--r--spec/frontend/diffs/components/diff_line_note_form_spec.js19
-rw-r--r--spec/frontend/diffs/store/actions_spec.js439
-rw-r--r--spec/frontend/diffs/store/utils_spec.js2
-rw-r--r--spec/frontend/editor/components/helpers.js12
-rw-r--r--spec/frontend/editor/components/source_editor_toolbar_button_spec.js146
-rw-r--r--spec/frontend/editor/components/source_editor_toolbar_spec.js116
-rw-r--r--spec/frontend/editor/schema/ci/ci_schema_spec.js90
-rw-r--r--spec/frontend/editor/schema/ci/json_tests/negative_tests/default_no_additional_properties.json12
-rw-r--r--spec/frontend/editor/schema/ci/json_tests/negative_tests/inherit_default_no_additional_properties.json8
-rw-r--r--spec/frontend/editor/schema/ci/json_tests/negative_tests/job_variables_must_not_contain_objects.json12
-rw-r--r--spec/frontend/editor/schema/ci/json_tests/negative_tests/release_assets_links_empty.json13
-rw-r--r--spec/frontend/editor/schema/ci/json_tests/negative_tests/release_assets_links_invalid_link_type.json24
-rw-r--r--spec/frontend/editor/schema/ci/json_tests/negative_tests/release_assets_links_missing.json11
-rw-r--r--spec/frontend/editor/schema/ci/json_tests/negative_tests/retry_unknown_when.json9
-rw-r--r--spec/frontend/editor/schema/ci/json_tests/positive_tests/allow_failure.json19
-rw-r--r--spec/frontend/editor/schema/ci/json_tests/positive_tests/environment.json75
-rw-r--r--spec/frontend/editor/schema/ci/json_tests/positive_tests/gitlab-ci-dependencies.json68
-rw-r--r--spec/frontend/editor/schema/ci/json_tests/positive_tests/gitlab-ci.json350
-rw-r--r--spec/frontend/editor/schema/ci/json_tests/positive_tests/inherit.json54
-rw-r--r--spec/frontend/editor/schema/ci/json_tests/positive_tests/multiple-caches.json24
-rw-r--r--spec/frontend/editor/schema/ci/json_tests/positive_tests/retry.json60
-rw-r--r--spec/frontend/editor/schema/ci/json_tests/positive_tests/terraform_report.json50
-rw-r--r--spec/frontend/editor/schema/ci/json_tests/positive_tests/variables.json22
-rw-r--r--spec/frontend/editor/schema/ci/json_tests/positive_tests/variables_mix_string_and_user_input.json10
-rw-r--r--spec/frontend/editor/schema/ci/yaml_tests/negative_tests/cache.yml15
-rw-r--r--spec/frontend/editor/schema/ci/yaml_tests/negative_tests/include.yml17
-rw-r--r--spec/frontend/editor/schema/ci/yaml_tests/positive_tests/cache.yml25
-rw-r--r--spec/frontend/editor/schema/ci/yaml_tests/positive_tests/filter.yml18
-rw-r--r--spec/frontend/editor/schema/ci/yaml_tests/positive_tests/include.yml32
-rw-r--r--spec/frontend/editor/schema/ci/yaml_tests/positive_tests/rules.yml13
-rw-r--r--spec/frontend/environments/deploy_board_component_spec.js16
-rw-r--r--spec/frontend/environments/empty_state_spec.js53
-rw-r--r--spec/frontend/environments/emtpy_state_spec.js24
-rw-r--r--spec/frontend/environments/environment_item_spec.js53
-rw-r--r--spec/frontend/environments/environment_table_spec.js5
-rw-r--r--spec/frontend/environments/graphql/mock_data.js2
-rw-r--r--spec/frontend/environments/new_environment_item_spec.js28
-rw-r--r--spec/frontend/error_tracking/store/actions_spec.js19
-rw-r--r--spec/frontend/error_tracking/store/details/actions_spec.js26
-rw-r--r--spec/frontend/error_tracking/store/list/actions_spec.js16
-rw-r--r--spec/frontend/error_tracking_settings/store/actions_spec.js72
-rw-r--r--spec/frontend/feature_flags/store/edit/actions_spec.js55
-rw-r--r--spec/frontend/feature_flags/store/index/actions_spec.js86
-rw-r--r--spec/frontend/feature_flags/store/new/actions_spec.js25
-rw-r--r--spec/frontend/filtered_search/droplab/plugins/ajax_filter_spec.js22
-rw-r--r--spec/frontend/filtered_search/filtered_search_manager_spec.js18
-rw-r--r--spec/frontend/filtered_search/services/recent_searches_service_spec.js58
-rw-r--r--spec/frontend/filtered_search/visual_token_value_spec.js91
-rw-r--r--spec/frontend/fixtures/startup_css.rb8
-rw-r--r--spec/frontend/frequent_items/store/actions_spec.js70
-rw-r--r--spec/frontend/google_cloud/components/app_spec.js2
-rw-r--r--spec/frontend/gpg_badges_spec.js52
-rw-r--r--spec/frontend/groups/components/item_type_icon_spec.js25
-rw-r--r--spec/frontend/header_search/components/app_spec.js14
-rw-r--r--spec/frontend/header_search/components/header_search_autocomplete_items_spec.js98
-rw-r--r--spec/frontend/header_search/components/header_search_scoped_items_spec.js31
-rw-r--r--spec/frontend/header_search/mock_data.js131
-rw-r--r--spec/frontend/header_spec.js10
-rw-r--r--spec/frontend/ide/components/commit_sidebar/form_spec.js2
-rw-r--r--spec/frontend/ide/components/ide_side_bar_spec.js2
-rw-r--r--spec/frontend/ide/components/new_dropdown/upload_spec.js22
-rw-r--r--spec/frontend/ide/stores/actions/merge_request_spec.js387
-rw-r--r--spec/frontend/ide/stores/actions/project_spec.js172
-rw-r--r--spec/frontend/ide/stores/actions/tree_spec.js92
-rw-r--r--spec/frontend/ide/stores/actions_spec.js537
-rw-r--r--spec/frontend/ide/stores/modules/branches/actions_spec.js30
-rw-r--r--spec/frontend/ide/stores/modules/clientside/actions_spec.js8
-rw-r--r--spec/frontend/ide/stores/modules/commit/actions_spec.js384
-rw-r--r--spec/frontend/ide/stores/modules/file_templates/actions_spec.js62
-rw-r--r--spec/frontend/ide/stores/modules/merge_requests/actions_spec.js35
-rw-r--r--spec/frontend/ide/stores/modules/pane/actions_spec.js27
-rw-r--r--spec/frontend/ide/stores/modules/pipelines/actions_spec.js152
-rw-r--r--spec/frontend/ide/stores/modules/terminal_sync/actions_spec.js31
-rw-r--r--spec/frontend/ide/stores/plugins/terminal_spec.js16
-rw-r--r--spec/frontend/image_diff/init_discussion_tab_spec.js6
-rw-r--r--spec/frontend/image_diff/replaced_image_diff_spec.js64
-rw-r--r--spec/frontend/import_entities/components/import_status_spec.js145
-rw-r--r--spec/frontend/import_entities/import_projects/components/import_projects_table_spec.js2
-rw-r--r--spec/frontend/import_entities/import_projects/components/provider_repo_table_row_spec.js7
-rw-r--r--spec/frontend/import_entities/import_projects/store/mutations_spec.js29
-rw-r--r--spec/frontend/incidents/components/incidents_list_spec.js30
-rw-r--r--spec/frontend/integrations/edit/components/integration_form_spec.js63
-rw-r--r--spec/frontend/integrations/edit/components/jira_issues_fields_spec.js44
-rw-r--r--spec/frontend/invite_members/components/group_select_spec.js5
-rw-r--r--spec/frontend/invite_members/components/invite_groups_modal_spec.js13
-rw-r--r--spec/frontend/invite_members/components/invite_members_modal_spec.js131
-rw-r--r--spec/frontend/invite_members/components/invite_modal_base_spec.js24
-rw-r--r--spec/frontend/invite_members/components/members_token_select_spec.js4
-rw-r--r--spec/frontend/invite_members/components/user_limit_notification_spec.js71
-rw-r--r--spec/frontend/invite_members/mock_data/api_responses.js62
-rw-r--r--spec/frontend/invite_members/mock_data/group_modal.js1
-rw-r--r--spec/frontend/invite_members/mock_data/member_modal.js1
-rw-r--r--spec/frontend/invite_members/utils/response_message_parser_spec.js28
-rw-r--r--spec/frontend/issuable/issuable_form_spec.js44
-rw-r--r--spec/frontend/issues/create_merge_request_dropdown_spec.js19
-rw-r--r--spec/frontend/issues/list/components/issue_card_time_info_spec.js13
-rw-r--r--spec/frontend/issues/list/components/issues_list_app_spec.js27
-rw-r--r--spec/frontend/issues/list/mock_data.js2
-rw-r--r--spec/frontend/issues/related_merge_requests/store/actions_spec.js40
-rw-r--r--spec/frontend/issues/show/components/app_spec.js84
-rw-r--r--spec/frontend/issues/show/components/description_spec.js170
-rw-r--r--spec/frontend/issues/show/components/fields/description_spec.js5
-rw-r--r--spec/frontend/issues/show/components/fields/description_template_spec.js101
-rw-r--r--spec/frontend/issues/show/components/fields/title_spec.js4
-rw-r--r--spec/frontend/issues/show/components/incidents/incident_tabs_spec.js18
-rw-r--r--spec/frontend/issues/show/mock_data/mock_data.js15
-rw-r--r--spec/frontend/jira_connect/subscriptions/components/add_namespace_modal/groups_list_item_spec.js2
-rw-r--r--spec/frontend/jira_connect/subscriptions/components/app_spec.js28
-rw-r--r--spec/frontend/jira_connect/subscriptions/components/browser_support_alert_spec.js37
-rw-r--r--spec/frontend/jira_connect/subscriptions/components/compatibility_alert_spec.js2
-rw-r--r--spec/frontend/jira_connect/subscriptions/pages/sign_in_spec.js2
-rw-r--r--spec/frontend/jira_import/components/jira_import_form_spec.js21
-rw-r--r--spec/frontend/jobs/components/filtered_search/jobs_filtered_search_spec.js49
-rw-r--r--spec/frontend/jobs/components/filtered_search/tokens/job_status_token_spec.js57
-rw-r--r--spec/frontend/jobs/components/job_app_spec.js23
-rw-r--r--spec/frontend/jobs/components/table/graphql/cache_config_spec.js20
-rw-r--r--spec/frontend/jobs/components/table/job_table_app_spec.js105
-rw-r--r--spec/frontend/jobs/components/table/jobs_table_tabs_spec.js46
-rw-r--r--spec/frontend/jobs/components/trigger_block_spec.js7
-rw-r--r--spec/frontend/jobs/mock_data.js16
-rw-r--r--spec/frontend/jobs/store/actions_spec.js138
-rw-r--r--spec/frontend/labels/components/promote_label_modal_spec.js38
-rw-r--r--spec/frontend/lib/apollo/suppress_network_errors_during_navigation_link_spec.js39
-rw-r--r--spec/frontend/lib/gfm/index_spec.js46
-rw-r--r--spec/frontend/lib/utils/apollo_startup_js_link_spec.js51
-rw-r--r--spec/frontend/lib/utils/common_utils_spec.js69
-rw-r--r--spec/frontend/lib/utils/confirm_via_gl_modal/confirm_modal_spec.js26
-rw-r--r--spec/frontend/lib/utils/datetime/date_calculation_utility_spec.js17
-rw-r--r--spec/frontend/lib/utils/datetime/date_format_utility_spec.js12
-rw-r--r--spec/frontend/lib/utils/datetime/timeago_utility_spec.js50
-rw-r--r--spec/frontend/lib/utils/poll_spec.js60
-rw-r--r--spec/frontend/lib/utils/text_markdown_spec.js17
-rw-r--r--spec/frontend/lib/utils/unit_format/formatter_factory_spec.js50
-rw-r--r--spec/frontend/lib/utils/unit_format/index_spec.js15
-rw-r--r--spec/frontend/lib/utils/users_cache_spec.js108
-rw-r--r--spec/frontend/members/components/table/members_table_spec.js19
-rw-r--r--spec/frontend/members/mock_data.js3
-rw-r--r--spec/frontend/merge_conflicts/store/actions_spec.js85
-rw-r--r--spec/frontend/milestones/components/delete_milestone_modal_spec.js37
-rw-r--r--spec/frontend/milestones/components/milestone_combobox_spec.js22
-rw-r--r--spec/frontend/monitoring/components/__snapshots__/dashboard_template_spec.js.snap8
-rw-r--r--spec/frontend/monitoring/components/__snapshots__/empty_state_spec.js.snap3
-rw-r--r--spec/frontend/monitoring/components/__snapshots__/group_empty_state_spec.js.snap7
-rw-r--r--spec/frontend/monitoring/components/dashboard_actions_menu_spec.js4
-rw-r--r--spec/frontend/monitoring/components/dashboard_url_time_spec.js2
-rw-r--r--spec/frontend/monitoring/store/actions_spec.js335
-rw-r--r--spec/frontend/monitoring/store/utils_spec.js4
-rw-r--r--spec/frontend/mr_notes/stores/actions_spec.js88
-rw-r--r--spec/frontend/notes/components/diff_discussion_header_spec.js2
-rw-r--r--spec/frontend/notes/components/note_actions_spec.js3
-rw-r--r--spec/frontend/notes/components/note_form_spec.js9
-rw-r--r--spec/frontend/notes/components/note_header_spec.js28
-rw-r--r--spec/frontend/notes/components/noteable_discussion_spec.js1
-rw-r--r--spec/frontend/notes/components/noteable_note_spec.js2
-rw-r--r--spec/frontend/notes/components/notes_app_spec.js8
-rw-r--r--spec/frontend/notes/components/sort_discussion_spec.js4
-rw-r--r--spec/frontend/notes/deprecated_notes_spec.js11
-rw-r--r--spec/frontend/notes/stores/actions_spec.js407
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/components/delete_button_spec.js67
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/image_list_row_spec.js10
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/registry_header_spec.js10
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/mock_data.js2
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/pages/list_spec.js31
-rw-r--r--spec/frontend/packages_and_registries/dependency_proxy/app_spec.js70
-rw-r--r--spec/frontend/packages_and_registries/harbor_registry/components/list/harbor_list_header_spec.js88
-rw-r--r--spec/frontend/packages_and_registries/harbor_registry/components/list/harbor_list_row_spec.js99
-rw-r--r--spec/frontend/packages_and_registries/harbor_registry/components/list/harbor_list_spec.js39
-rw-r--r--spec/frontend/packages_and_registries/harbor_registry/mock_data.js175
-rw-r--r--spec/frontend/packages_and_registries/harbor_registry/pages/index_spec.js24
-rw-r--r--spec/frontend/packages_and_registries/harbor_registry/pages/list_spec.js140
-rw-r--r--spec/frontend/packages_and_registries/infrastructure_registry/components/details/store/actions_spec.js108
-rw-r--r--spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/__snapshots__/packages_list_app_spec.js.snap2
-rw-r--r--spec/frontend/packages_and_registries/infrastructure_registry/components/list/stores/actions_spec.js129
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/list/packages_search_spec.js1
-rw-r--r--spec/frontend/packages_and_registries/package_registry/pages/__snapshots__/list_spec.js.snap2
-rw-r--r--spec/frontend/packages_and_registries/settings/project/settings/components/registry_settings_app_spec.js24
-rw-r--r--spec/frontend/packages_and_registries/shared/components/__snapshots__/cleanup_policy_enabled_alert_spec.js.snap19
-rw-r--r--spec/frontend/packages_and_registries/shared/components/__snapshots__/registry_breadcrumb_spec.js.snap10
-rw-r--r--spec/frontend/packages_and_registries/shared/components/cleanup_policy_enabled_alert_spec.js49
-rw-r--r--spec/frontend/packages_and_registries/shared/components/registry_breadcrumb_spec.js8
-rw-r--r--spec/frontend/pager_spec.js57
-rw-r--r--spec/frontend/pages/admin/application_settings/account_and_limits_spec.js3
-rw-r--r--spec/frontend/pages/admin/jobs/index/components/stop_jobs_modal_spec.js22
-rw-r--r--spec/frontend/pages/dashboard/todos/index/todos_spec.js8
-rw-r--r--spec/frontend/pages/import/bulk_imports/history/components/bulk_imports_history_app_spec.js10
-rw-r--r--spec/frontend/pages/import/history/components/import_error_details_spec.js66
-rw-r--r--spec/frontend/pages/import/history/components/import_history_app_spec.js205
-rw-r--r--spec/frontend/pages/profiles/show/emoji_menu_spec.js16
-rw-r--r--spec/frontend/pages/projects/learn_gitlab/components/__snapshots__/learn_gitlab_section_card_spec.js.snap37
-rw-r--r--spec/frontend/pages/projects/learn_gitlab/components/__snapshots__/learn_gitlab_spec.js.snap452
-rw-r--r--spec/frontend/pages/projects/learn_gitlab/components/learn_gitlab_section_link_spec.js53
-rw-r--r--spec/frontend/pages/projects/shared/permissions/components/project_feature_settings_spec.js30
-rw-r--r--spec/frontend/pages/projects/shared/permissions/components/settings_panel_spec.js63
-rw-r--r--spec/frontend/pages/shared/wikis/components/wiki_content_spec.js97
-rw-r--r--spec/frontend/pages/shared/wikis/components/wiki_form_spec.js192
-rw-r--r--spec/frontend/pdf/page_spec.js16
-rw-r--r--spec/frontend/pipeline_editor/components/drawer/pipeline_editor_drawer_spec.js137
-rw-r--r--spec/frontend/pipeline_editor/components/editor/ci_editor_header_spec.js53
-rw-r--r--spec/frontend/pipeline_editor/components/pipeline_editor_tabs_spec.js1
-rw-r--r--spec/frontend/pipeline_editor/pipeline_editor_home_spec.js99
-rw-r--r--spec/frontend/pipeline_wizard/components/wrapper_spec.js84
-rw-r--r--spec/frontend/pipeline_wizard/mock/yaml.js11
-rw-r--r--spec/frontend/pipelines/components/pipeline_tabs_spec.js61
-rw-r--r--spec/frontend/pipelines/components/pipelines_filtered_search_spec.js2
-rw-r--r--spec/frontend/pipelines/empty_state/ci_templates_spec.js85
-rw-r--r--spec/frontend/pipelines/empty_state/pipelines_ci_templates_spec.js (renamed from spec/frontend/pipelines/pipelines_ci_templates_spec.js)72
-rw-r--r--spec/frontend/pipelines/empty_state_spec.js2
-rw-r--r--spec/frontend/pipelines/graph/action_component_spec.js11
-rw-r--r--spec/frontend/pipelines/graph/graph_component_wrapper_spec.js6
-rw-r--r--spec/frontend/pipelines/pipeline_triggerer_spec.js81
-rw-r--r--spec/frontend/pipelines/pipeline_url_spec.js25
-rw-r--r--spec/frontend/pipelines/pipelines_spec.js2
-rw-r--r--spec/frontend/pipelines/test_reports/stores/actions_spec.js51
-rw-r--r--spec/frontend/profile/add_ssh_key_validation_spec.js2
-rw-r--r--spec/frontend/profile/preferences/components/__snapshots__/diffs_colors_preview_spec.js.snap915
-rw-r--r--spec/frontend/profile/preferences/components/diffs_colors_preview_spec.js23
-rw-r--r--spec/frontend/profile/preferences/components/diffs_colors_spec.js153
-rw-r--r--spec/frontend/profile/preferences/components/integration_view_spec.js33
-rw-r--r--spec/frontend/projects/commit/store/actions_spec.js16
-rw-r--r--spec/frontend/projects/components/__snapshots__/project_delete_button_spec.js.snap2
-rw-r--r--spec/frontend/projects/new/components/deployment_target_select_spec.js39
-rw-r--r--spec/frontend/projects/new/components/new_project_url_select_spec.js37
-rw-r--r--spec/frontend/releases/components/app_index_apollo_client_spec.js398
-rw-r--r--spec/frontend/releases/components/app_index_spec.js483
-rw-r--r--spec/frontend/releases/components/app_show_spec.js6
-rw-r--r--spec/frontend/releases/components/releases_pagination_apollo_client_spec.js126
-rw-r--r--spec/frontend/releases/components/releases_pagination_spec.js180
-rw-r--r--spec/frontend/releases/components/releases_sort_apollo_client_spec.js103
-rw-r--r--spec/frontend/releases/components/releases_sort_spec.js122
-rw-r--r--spec/frontend/releases/stores/modules/list/actions_spec.js197
-rw-r--r--spec/frontend/releases/stores/modules/list/helpers.js5
-rw-r--r--spec/frontend/releases/stores/modules/list/mutations_spec.js81
-rw-r--r--spec/frontend/reports/accessibility_report/store/actions_spec.js30
-rw-r--r--spec/frontend/reports/codequality_report/components/codequality_issue_body_spec.js1
-rw-r--r--spec/frontend/reports/codequality_report/grouped_codequality_reports_app_spec.js4
-rw-r--r--spec/frontend/reports/codequality_report/store/actions_spec.js44
-rw-r--r--spec/frontend/reports/components/report_section_spec.js31
-rw-r--r--spec/frontend/reports/components/summary_row_spec.js34
-rw-r--r--spec/frontend/reports/grouped_test_report/store/actions_spec.js44
-rw-r--r--spec/frontend/repository/components/__snapshots__/last_commit_spec.js.snap111
-rw-r--r--spec/frontend/repository/components/blob_content_viewer_spec.js19
-rw-r--r--spec/frontend/repository/components/breadcrumbs_spec.js45
-rw-r--r--spec/frontend/repository/components/last_commit_spec.js23
-rw-r--r--spec/frontend/runner/admin_runners/admin_runners_app_spec.js111
-rw-r--r--spec/frontend/runner/components/__snapshots__/runner_status_popover_spec.js.snap3
-rw-r--r--spec/frontend/runner/components/cells/runner_actions_cell_spec.js22
-rw-r--r--spec/frontend/runner/components/cells/runner_summary_cell_spec.js6
-rw-r--r--spec/frontend/runner/components/registration/registration_dropdown_spec.js17
-rw-r--r--spec/frontend/runner/components/registration/registration_token_spec.js75
-rw-r--r--spec/frontend/runner/components/runner_assigned_item_spec.js3
-rw-r--r--spec/frontend/runner/components/runner_bulk_delete_spec.js103
-rw-r--r--spec/frontend/runner/components/runner_delete_button_spec.js64
-rw-r--r--spec/frontend/runner/components/runner_filtered_search_bar_spec.js6
-rw-r--r--spec/frontend/runner/components/runner_jobs_spec.js2
-rw-r--r--spec/frontend/runner/components/runner_list_spec.js65
-rw-r--r--spec/frontend/runner/components/runner_pause_button_spec.js4
-rw-r--r--spec/frontend/runner/components/runner_projects_spec.js2
-rw-r--r--spec/frontend/runner/components/runner_status_badge_spec.js25
-rw-r--r--spec/frontend/runner/components/runner_status_popover_spec.js36
-rw-r--r--spec/frontend/runner/graphql/local_state_spec.js72
-rw-r--r--spec/frontend/runner/group_runners/group_runners_app_spec.js56
-rw-r--r--spec/frontend/runner/mock_data.js4
-rw-r--r--spec/frontend/runner/runner_search_utils_spec.js40
-rw-r--r--spec/frontend/runner/utils_spec.js4
-rw-r--r--spec/frontend/search/store/actions_spec.js20
-rw-r--r--spec/frontend/search_autocomplete_spec.js32
-rw-r--r--spec/frontend/search_settings/components/search_settings_spec.js41
-rw-r--r--spec/frontend/security_configuration/components/app_spec.js14
-rw-r--r--spec/frontend/security_configuration/components/feature_card_badge_spec.js40
-rw-r--r--spec/frontend/security_configuration/components/feature_card_spec.js27
-rw-r--r--spec/frontend/security_configuration/components/training_provider_list_spec.js16
-rw-r--r--spec/frontend/self_monitor/store/actions_spec.js50
-rw-r--r--spec/frontend/serverless/components/__snapshots__/empty_state_spec.js.snap2
-rw-r--r--spec/frontend/serverless/store/actions_spec.js46
-rw-r--r--spec/frontend/set_status_modal/set_status_modal_wrapper_spec.js44
-rw-r--r--spec/frontend/shortcuts_spec.js2
-rw-r--r--spec/frontend/sidebar/assignees_realtime_spec.js23
-rw-r--r--spec/frontend/sidebar/components/incidents/escalation_status_spec.js34
-rw-r--r--spec/frontend/sidebar/mock_data.js22
-rw-r--r--spec/frontend/sidebar/participants_spec.js5
-rw-r--r--spec/frontend/snippets/components/edit_spec.js4
-rw-r--r--spec/frontend/snippets/components/snippet_header_spec.js2
-rw-r--r--spec/frontend/task_list_spec.js32
-rw-r--r--spec/frontend/terraform/components/empty_state_spec.js13
-rw-r--r--spec/frontend/terraform/components/mock_data.js35
-rw-r--r--spec/frontend/terraform/components/states_table_actions_spec.js3
-rw-r--r--spec/frontend/tracking/tracking_spec.js66
-rw-r--r--spec/frontend/user_lists/components/edit_user_list_spec.js2
-rw-r--r--spec/frontend/user_lists/components/new_user_list_spec.js2
-rw-r--r--spec/frontend/user_lists/components/user_list_form_spec.js2
-rw-r--r--spec/frontend/user_lists/components/user_list_spec.js2
-rw-r--r--spec/frontend/user_lists/components/user_lists_spec.js2
-rw-r--r--spec/frontend/user_lists/components/user_lists_table_spec.js2
-rw-r--r--spec/frontend/user_lists/store/edit/actions_spec.js2
-rw-r--r--spec/frontend/user_lists/store/edit/mutations_spec.js2
-rw-r--r--spec/frontend/user_lists/store/index/actions_spec.js51
-rw-r--r--spec/frontend/user_lists/store/index/mutations_spec.js2
-rw-r--r--spec/frontend/user_lists/store/new/actions_spec.js2
-rw-r--r--spec/frontend/vue_mr_widget/components/approvals/approvals_spec.js70
-rw-r--r--spec/frontend/vue_mr_widget/components/extensions/utils_spec.js2
-rw-r--r--spec/frontend/vue_mr_widget/components/mr_widget_memory_usage_spec.js44
-rw-r--r--spec/frontend/vue_mr_widget/components/states/mr_widget_merged_spec.js31
-rw-r--r--spec/frontend/vue_mr_widget/extensions/test_report/index_spec.js149
-rw-r--r--spec/frontend/vue_mr_widget/mr_widget_options_spec.js121
-rw-r--r--spec/frontend/vue_mr_widget/stores/artifacts_list/actions_spec.js35
-rw-r--r--spec/frontend/vue_mr_widget/test_extensions.js9
-rw-r--r--spec/frontend/vue_shared/alert_details/alert_details_spec.js30
-rw-r--r--spec/frontend/vue_shared/alert_details/service_spec.js44
-rw-r--r--spec/frontend/vue_shared/components/__snapshots__/awards_list_spec.js.snap127
-rw-r--r--spec/frontend/vue_shared/components/__snapshots__/identicon_spec.js.snap21
-rw-r--r--spec/frontend/vue_shared/components/awards_list_spec.js3
-rw-r--r--spec/frontend/vue_shared/components/blob_viewers/simple_viewer_spec.js17
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_bar_root_spec.js32
-rw-r--r--spec/frontend/vue_shared/components/form/input_copy_toggle_visibility_spec.js10
-rw-r--r--spec/frontend/vue_shared/components/help_popover_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/identicon_spec.js50
-rw-r--r--spec/frontend/vue_shared/components/line_numbers_spec.js37
-rw-r--r--spec/frontend/vue_shared/components/local_storage_sync_spec.js277
-rw-r--r--spec/frontend/vue_shared/components/markdown/apply_suggestion_spec.js23
-rw-r--r--spec/frontend/vue_shared/components/markdown/field_spec.js17
-rw-r--r--spec/frontend/vue_shared/components/markdown/header_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/metric_images/__snapshots__/metric_images_table_spec.js.snap73
-rw-r--r--spec/frontend/vue_shared/components/metric_images/metric_images_tab_spec.js174
-rw-r--r--spec/frontend/vue_shared/components/metric_images/metric_images_table_spec.js230
-rw-r--r--spec/frontend/vue_shared/components/metric_images/mock_data.js5
-rw-r--r--spec/frontend/vue_shared/components/metric_images/store/actions_spec.js158
-rw-r--r--spec/frontend/vue_shared/components/metric_images/store/mutations_spec.js147
-rw-r--r--spec/frontend/vue_shared/components/notes/placeholder_note_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/project_avatar/default_spec.js50
-rw-r--r--spec/frontend/vue_shared/components/project_selector/project_list_item_spec.js9
-rw-r--r--spec/frontend/vue_shared/components/registry/persisted_dropdown_selection_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/runner_aws_deployments/__snapshots__/runner_aws_deployments_modal_spec.js.snap8
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/actions_spec.js74
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_widget/labels_select_root_spec.js35
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_widget/mock_data.js28
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/components/chunk_line_spec.js69
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/components/chunk_spec.js82
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/source_viewer_spec.js107
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/utils_spec.js26
-rw-r--r--spec/frontend/vue_shared/components/user_avatar/user_avatar_image_new_spec.js32
-rw-r--r--spec/frontend/vue_shared/components/user_avatar/user_avatar_image_old_spec.js39
-rw-r--r--spec/frontend/vue_shared/components/user_avatar/user_avatar_list_spec.js25
-rw-r--r--spec/frontend/vue_shared/components/user_select_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/web_ide_link_spec.js5
-rw-r--r--spec/frontend/vue_shared/issuable/list/components/issuable_list_root_spec.js7
-rw-r--r--spec/frontend/vue_shared/security_reports/store/modules/sast/actions_spec.js39
-rw-r--r--spec/frontend/vue_shared/security_reports/store/modules/secret_detection/actions_spec.js40
-rw-r--r--spec/frontend/vuex_shared/modules/modal/actions_spec.js16
-rw-r--r--spec/frontend/work_items/components/item_title_spec.js4
-rw-r--r--spec/frontend/work_items/components/work_item_actions_spec.js103
-rw-r--r--spec/frontend/work_items/components/work_item_detail_modal_spec.js58
-rw-r--r--spec/frontend/work_items/components/work_item_detail_spec.js40
-rw-r--r--spec/frontend/work_items/components/work_item_title_spec.js117
-rw-r--r--spec/frontend/work_items/mock_data.js97
-rw-r--r--spec/frontend/work_items/pages/create_work_item_spec.js73
-rw-r--r--spec/frontend/work_items/pages/work_item_detail_spec.js99
-rw-r--r--spec/frontend/work_items/pages/work_item_root_spec.js91
-rw-r--r--spec/frontend/work_items/router_spec.js2
441 files changed, 14421 insertions, 9874 deletions
diff --git a/spec/frontend/__helpers__/matchers/index.js b/spec/frontend/__helpers__/matchers/index.js
index 76571bafb06..9b83ced10e1 100644
--- a/spec/frontend/__helpers__/matchers/index.js
+++ b/spec/frontend/__helpers__/matchers/index.js
@@ -1,3 +1,4 @@
export * from './to_have_sprite_icon';
export * from './to_have_tracking_attributes';
export * from './to_match_interpolated_text';
+export * from './to_validate_json_schema';
diff --git a/spec/frontend/__helpers__/matchers/to_validate_json_schema.js b/spec/frontend/__helpers__/matchers/to_validate_json_schema.js
new file mode 100644
index 00000000000..ff391f08c55
--- /dev/null
+++ b/spec/frontend/__helpers__/matchers/to_validate_json_schema.js
@@ -0,0 +1,34 @@
+// NOTE: Make sure to initialize ajv when using this helper
+
+const getAjvErrorMessage = ({ errors }) => {
+ return (errors || []).map((error) => {
+ return `Error with item ${error.instancePath}: ${error.message}`;
+ });
+};
+
+export function toValidateJsonSchema(testData, validator) {
+ if (!(validator instanceof Function && validator.schema)) {
+ return {
+ validator,
+ message: () =>
+ 'Validator must be a validating function with property "schema", created with `ajv.compile`. See https://ajv.js.org/api.html#ajv-compile-schema-object-data-any-boolean-promise-any.',
+ pass: false,
+ };
+ }
+
+ const isValid = validator(testData);
+
+ return {
+ actual: testData,
+ message: () => {
+ if (isValid) {
+ // We can match, but still fail because we're in a `expect...not.` context
+ return 'Expected the given data not to pass the schema validation, but found that it was considered valid.';
+ }
+
+ const errorMessages = getAjvErrorMessage(validator).join('\n');
+ return `Expected the given data to pass the schema validation, but found that it was considered invalid. Errors:\n${errorMessages}`;
+ },
+ pass: isValid,
+ };
+}
diff --git a/spec/frontend/__helpers__/matchers/to_validate_json_schema_spec.js b/spec/frontend/__helpers__/matchers/to_validate_json_schema_spec.js
new file mode 100644
index 00000000000..fd42c710c65
--- /dev/null
+++ b/spec/frontend/__helpers__/matchers/to_validate_json_schema_spec.js
@@ -0,0 +1,65 @@
+import Ajv from 'ajv';
+import AjvFormats from 'ajv-formats';
+
+const JSON_SCHEMA = {
+ type: 'object',
+ properties: {
+ fruit: {
+ type: 'string',
+ minLength: 3,
+ },
+ },
+};
+
+const ajv = new Ajv({
+ strictTypes: false,
+ strictTuples: false,
+ allowMatchingProperties: true,
+});
+
+AjvFormats(ajv);
+const schema = ajv.compile(JSON_SCHEMA);
+
+describe('custom matcher toValidateJsonSchema', () => {
+ it('throws error if validator is not compiled correctly', () => {
+ expect(() => {
+ expect({}).toValidateJsonSchema({});
+ }).toThrow(
+ 'Validator must be a validating function with property "schema", created with `ajv.compile`. See https://ajv.js.org/api.html#ajv-compile-schema-object-data-any-boolean-promise-any.',
+ );
+ });
+
+ describe('positive assertions', () => {
+ it.each`
+ description | input
+ ${'valid input'} | ${{ fruit: 'apple' }}
+ `('schema validation passes for $description', ({ input }) => {
+ expect(input).toValidateJsonSchema(schema);
+ });
+
+ it('throws if not matching', () => {
+ expect(() => expect(null).toValidateJsonSchema(schema)).toThrowError(
+ `Expected the given data to pass the schema validation, but found that it was considered invalid. Errors:
+Error with item : must be object`,
+ );
+ });
+ });
+
+ describe('negative assertions', () => {
+ it.each`
+ description | input
+ ${'no input'} | ${null}
+ ${'input with invalid type'} | ${'banana'}
+ ${'input with invalid length'} | ${{ fruit: 'aa' }}
+ ${'input with invalid type'} | ${{ fruit: 12345 }}
+ `('schema validation fails for $description', ({ input }) => {
+ expect(input).not.toValidateJsonSchema(schema);
+ });
+
+ it('throws if matching', () => {
+ expect(() => expect({ fruit: 'apple' }).not.toValidateJsonSchema(schema)).toThrowError(
+ 'Expected the given data not to pass the schema validation, but found that it was considered valid.',
+ );
+ });
+ });
+});
diff --git a/spec/frontend/__helpers__/mock_apollo_helper.js b/spec/frontend/__helpers__/mock_apollo_helper.js
index c07a6d8ef85..bae9f33be87 100644
--- a/spec/frontend/__helpers__/mock_apollo_helper.js
+++ b/spec/frontend/__helpers__/mock_apollo_helper.js
@@ -1,7 +1,7 @@
import { InMemoryCache } from '@apollo/client/core';
import { createMockClient as createMockApolloClient } from 'mock-apollo-client';
import VueApollo from 'vue-apollo';
-import possibleTypes from '~/graphql_shared/possibleTypes.json';
+import possibleTypes from '~/graphql_shared/possible_types.json';
import { typePolicies } from '~/lib/graphql';
export function createMockClient(handlers = [], resolvers = {}, cacheOptions = {}) {
diff --git a/spec/frontend/__helpers__/mock_dom_observer.js b/spec/frontend/__helpers__/mock_dom_observer.js
index dd26b594ad9..bc2646be4c2 100644
--- a/spec/frontend/__helpers__/mock_dom_observer.js
+++ b/spec/frontend/__helpers__/mock_dom_observer.js
@@ -22,14 +22,14 @@ class MockObserver {
takeRecords() {}
- // eslint-disable-next-line babel/camelcase
+ // eslint-disable-next-line camelcase
$_triggerObserve(node, { entry = {}, options = {} } = {}) {
if (this.$_hasObserver(node, options)) {
this.$_cb([{ target: node, ...entry }]);
}
}
- // eslint-disable-next-line babel/camelcase
+ // eslint-disable-next-line camelcase
$_hasObserver(node, options = {}) {
return this.$_observers.some(
([obvNode, obvOptions]) => node === obvNode && isMatch(options, obvOptions),
diff --git a/spec/frontend/__helpers__/vuex_action_helper.js b/spec/frontend/__helpers__/vuex_action_helper.js
index 68203b544ef..95a811d0385 100644
--- a/spec/frontend/__helpers__/vuex_action_helper.js
+++ b/spec/frontend/__helpers__/vuex_action_helper.js
@@ -49,6 +49,7 @@ const noop = () => {};
* expectedActions: [],
* })
*/
+
export default (
actionArg,
payloadArg,
diff --git a/spec/frontend/__helpers__/yaml_transformer.js b/spec/frontend/__helpers__/yaml_transformer.js
new file mode 100644
index 00000000000..a23f9b1f715
--- /dev/null
+++ b/spec/frontend/__helpers__/yaml_transformer.js
@@ -0,0 +1,11 @@
+/* eslint-disable import/no-commonjs */
+const JsYaml = require('js-yaml');
+
+// This will transform YAML files to JSON strings
+module.exports = {
+ process: (sourceContent) => {
+ const jsonContent = JsYaml.load(sourceContent);
+ const json = JSON.stringify(jsonContent);
+ return `module.exports = ${json}`;
+ },
+};
diff --git a/spec/frontend/access_tokens/components/__snapshots__/expires_at_field_spec.js.snap b/spec/frontend/access_tokens/components/__snapshots__/expires_at_field_spec.js.snap
index dd742419d32..36003154b58 100644
--- a/spec/frontend/access_tokens/components/__snapshots__/expires_at_field_spec.js.snap
+++ b/spec/frontend/access_tokens/components/__snapshots__/expires_at_field_spec.js.snap
@@ -8,7 +8,7 @@ exports[`~/access_tokens/components/expires_at_field should render datepicker wi
optionaltext="(optional)"
>
<gl-datepicker-stub
- ariallabel=""
+ arialabel=""
autocomplete=""
container=""
displayfield="true"
diff --git a/spec/frontend/add_context_commits_modal/store/actions_spec.js b/spec/frontend/add_context_commits_modal/store/actions_spec.js
index fa4d52cbfbb..4b58a69c2b8 100644
--- a/spec/frontend/add_context_commits_modal/store/actions_spec.js
+++ b/spec/frontend/add_context_commits_modal/store/actions_spec.js
@@ -42,9 +42,9 @@ describe('AddContextCommitsModalStoreActions', () => {
});
describe('setBaseConfig', () => {
- it('commits SET_BASE_CONFIG', (done) => {
+ it('commits SET_BASE_CONFIG', () => {
const options = { contextCommitsPath, mergeRequestIid, projectId };
- testAction(
+ return testAction(
setBaseConfig,
options,
{
@@ -59,62 +59,54 @@ describe('AddContextCommitsModalStoreActions', () => {
},
],
[],
- done,
);
});
});
describe('setTabIndex', () => {
- it('commits SET_TABINDEX', (done) => {
- testAction(
+ it('commits SET_TABINDEX', () => {
+ return testAction(
setTabIndex,
{ tabIndex: 1 },
{ tabIndex: 0 },
[{ type: types.SET_TABINDEX, payload: { tabIndex: 1 } }],
[],
- done,
);
});
});
describe('setCommits', () => {
- it('commits SET_COMMITS', (done) => {
- testAction(
+ it('commits SET_COMMITS', () => {
+ return testAction(
setCommits,
{ commits: [], silentAddition: false },
{ isLoadingCommits: false, commits: [] },
[{ type: types.SET_COMMITS, payload: [] }],
[],
- done,
);
});
- it('commits SET_COMMITS_SILENT', (done) => {
- testAction(
+ it('commits SET_COMMITS_SILENT', () => {
+ return testAction(
setCommits,
{ commits: [], silentAddition: true },
{ isLoadingCommits: true, commits: [] },
[{ type: types.SET_COMMITS_SILENT, payload: [] }],
[],
- done,
);
});
});
describe('createContextCommits', () => {
- it('calls API to create context commits', (done) => {
+ it('calls API to create context commits', async () => {
mock.onPost(contextCommitEndpoint).reply(200, {});
- testAction(createContextCommits, { commits: [] }, {}, [], [], done);
+ await testAction(createContextCommits, { commits: [] }, {}, [], []);
- createContextCommits(
+ await createContextCommits(
{ state: { projectId, mergeRequestIid }, commit: () => null },
{ commits: [] },
- )
- .then(() => {
- done();
- })
- .catch(done.fail);
+ );
});
});
@@ -126,9 +118,9 @@ describe('AddContextCommitsModalStoreActions', () => {
)
.reply(200, [dummyCommit]);
});
- it('commits FETCH_CONTEXT_COMMITS', (done) => {
+ it('commits FETCH_CONTEXT_COMMITS', () => {
const contextCommit = { ...dummyCommit, isSelected: true };
- testAction(
+ return testAction(
fetchContextCommits,
null,
{
@@ -144,20 +136,18 @@ describe('AddContextCommitsModalStoreActions', () => {
{ type: 'setCommits', payload: { commits: [contextCommit], silentAddition: true } },
{ type: 'setSelectedCommits', payload: [contextCommit] },
],
- done,
);
});
});
describe('setContextCommits', () => {
- it('commits SET_CONTEXT_COMMITS', (done) => {
- testAction(
+ it('commits SET_CONTEXT_COMMITS', () => {
+ return testAction(
setContextCommits,
{ data: [] },
{ contextCommits: [], isLoadingContextCommits: false },
[{ type: types.SET_CONTEXT_COMMITS, payload: { data: [] } }],
[],
- done,
);
});
});
@@ -168,71 +158,66 @@ describe('AddContextCommitsModalStoreActions', () => {
.onDelete('/api/v4/projects/gitlab-org%2Fgitlab/merge_requests/1/context_commits')
.reply(204);
});
- it('calls API to remove context commits', (done) => {
- testAction(
+ it('calls API to remove context commits', () => {
+ return testAction(
removeContextCommits,
{ forceReload: false },
{ mergeRequestIid, projectId, toRemoveCommits: [] },
[],
[],
- done,
);
});
});
describe('setSelectedCommits', () => {
- it('commits SET_SELECTED_COMMITS', (done) => {
- testAction(
+ it('commits SET_SELECTED_COMMITS', () => {
+ return testAction(
setSelectedCommits,
[dummyCommit],
{ selectedCommits: [] },
[{ type: types.SET_SELECTED_COMMITS, payload: [dummyCommit] }],
[],
- done,
);
});
});
describe('setSearchText', () => {
- it('commits SET_SEARCH_TEXT', (done) => {
+ it('commits SET_SEARCH_TEXT', () => {
const searchText = 'Dummy Text';
- testAction(
+ return testAction(
setSearchText,
searchText,
{ searchText: '' },
[{ type: types.SET_SEARCH_TEXT, payload: searchText }],
[],
- done,
);
});
});
describe('setToRemoveCommits', () => {
- it('commits SET_TO_REMOVE_COMMITS', (done) => {
+ it('commits SET_TO_REMOVE_COMMITS', () => {
const commitId = 'abcde';
- testAction(
+ return testAction(
setToRemoveCommits,
[commitId],
{ toRemoveCommits: [] },
[{ type: types.SET_TO_REMOVE_COMMITS, payload: [commitId] }],
[],
- done,
);
});
});
describe('resetModalState', () => {
- it('commits RESET_MODAL_STATE', (done) => {
+ it('commits RESET_MODAL_STATE', () => {
const commitId = 'abcde';
- testAction(
+ return testAction(
resetModalState,
null,
{ toRemoveCommits: [commitId] },
[{ type: types.RESET_MODAL_STATE }],
[],
- done,
);
});
});
diff --git a/spec/frontend/admin/statistics_panel/store/actions_spec.js b/spec/frontend/admin/statistics_panel/store/actions_spec.js
index c7481b664b3..e7cdb5feb6a 100644
--- a/spec/frontend/admin/statistics_panel/store/actions_spec.js
+++ b/spec/frontend/admin/statistics_panel/store/actions_spec.js
@@ -22,8 +22,8 @@ describe('Admin statistics panel actions', () => {
mock.onGet(/api\/(.*)\/application\/statistics/).replyOnce(200, mockStatistics);
});
- it('dispatches success with received data', (done) =>
- testAction(
+ it('dispatches success with received data', () => {
+ return testAction(
actions.fetchStatistics,
null,
state,
@@ -37,8 +37,8 @@ describe('Admin statistics panel actions', () => {
),
},
],
- done,
- ));
+ );
+ });
});
describe('error', () => {
@@ -46,8 +46,8 @@ describe('Admin statistics panel actions', () => {
mock.onGet(/api\/(.*)\/application\/statistics/).replyOnce(500);
});
- it('dispatches error', (done) =>
- testAction(
+ it('dispatches error', () => {
+ return testAction(
actions.fetchStatistics,
null,
state,
@@ -61,26 +61,26 @@ describe('Admin statistics panel actions', () => {
payload: new Error('Request failed with status code 500'),
},
],
- done,
- ));
+ );
+ });
});
});
describe('requestStatistic', () => {
- it('should commit the request mutation', (done) =>
- testAction(
+ it('should commit the request mutation', () => {
+ return testAction(
actions.requestStatistics,
null,
state,
[{ type: types.REQUEST_STATISTICS }],
[],
- done,
- ));
+ );
+ });
});
describe('receiveStatisticsSuccess', () => {
- it('should commit received data', (done) =>
- testAction(
+ it('should commit received data', () => {
+ return testAction(
actions.receiveStatisticsSuccess,
mockStatistics,
state,
@@ -91,13 +91,13 @@ describe('Admin statistics panel actions', () => {
},
],
[],
- done,
- ));
+ );
+ });
});
describe('receiveStatisticsError', () => {
- it('should commit error', (done) => {
- testAction(
+ it('should commit error', () => {
+ return testAction(
actions.receiveStatisticsError,
500,
state,
@@ -108,7 +108,6 @@ describe('Admin statistics panel actions', () => {
},
],
[],
- done,
);
});
});
diff --git a/spec/frontend/admin/topics/components/remove_avatar_spec.js b/spec/frontend/admin/topics/components/remove_avatar_spec.js
index d4656f0a199..97d257c682c 100644
--- a/spec/frontend/admin/topics/components/remove_avatar_spec.js
+++ b/spec/frontend/admin/topics/components/remove_avatar_spec.js
@@ -1,10 +1,11 @@
-import { GlButton, GlModal } from '@gitlab/ui';
+import { GlButton, GlModal, GlSprintf } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import RemoveAvatar from '~/admin/topics/components/remove_avatar.vue';
const modalID = 'fake-id';
const path = 'topic/path/1';
+const name = 'Topic 1';
jest.mock('lodash/uniqueId', () => () => 'fake-id');
jest.mock('~/lib/utils/csrf', () => ({ token: 'mock-csrf-token' }));
@@ -16,10 +17,14 @@ describe('RemoveAvatar', () => {
wrapper = shallowMount(RemoveAvatar, {
provide: {
path,
+ name,
},
directives: {
GlModal: createMockDirective(),
},
+ stubs: {
+ GlSprintf,
+ },
});
};
@@ -55,8 +60,8 @@ describe('RemoveAvatar', () => {
const modal = findModal();
expect(modal.exists()).toBe(true);
- expect(modal.props('title')).toBe('Confirm remove avatar');
- expect(modal.text()).toBe('Avatar will be removed. Are you sure?');
+ expect(modal.props('title')).toBe('Remove topic avatar');
+ expect(modal.text()).toBe(`Topic avatar for ${name} will be removed. This cannot be undone.`);
});
it('contains the correct modal ID', () => {
diff --git a/spec/frontend/admin/users/components/actions/actions_spec.js b/spec/frontend/admin/users/components/actions/actions_spec.js
index fa485e73999..b758c15a91a 100644
--- a/spec/frontend/admin/users/components/actions/actions_spec.js
+++ b/spec/frontend/admin/users/components/actions/actions_spec.js
@@ -1,9 +1,9 @@
import { GlDropdownItem } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import { nextTick } from 'vue';
-import { kebabCase } from 'lodash';
import Actions from '~/admin/users/components/actions';
-import SharedDeleteAction from '~/admin/users/components/actions/shared/shared_delete_action.vue';
+import eventHub, {
+ EVENT_OPEN_DELETE_USER_MODAL,
+} from '~/admin/users/components/modals/delete_user_modal_event_hub';
import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
import { OBSTACLE_TYPES } from '~/vue_shared/components/user_deletion_obstacles/constants';
import { CONFIRMATION_ACTIONS, DELETE_ACTIONS } from '../../constants';
@@ -14,12 +14,11 @@ describe('Action components', () => {
const findDropdownItem = () => wrapper.find(GlDropdownItem);
- const initComponent = ({ component, props, stubs = {} } = {}) => {
+ const initComponent = ({ component, props } = {}) => {
wrapper = shallowMount(component, {
propsData: {
...props,
},
- stubs,
});
};
@@ -29,7 +28,7 @@ describe('Action components', () => {
});
describe('CONFIRMATION_ACTIONS', () => {
- it.each(CONFIRMATION_ACTIONS)('renders a dropdown item for "%s"', async (action) => {
+ it.each(CONFIRMATION_ACTIONS)('renders a dropdown item for "%s"', (action) => {
initComponent({
component: Actions[capitalizeFirstCharacter(action)],
props: {
@@ -38,20 +37,23 @@ describe('Action components', () => {
},
});
- await nextTick();
expect(findDropdownItem().exists()).toBe(true);
});
});
describe('DELETE_ACTION_COMPONENTS', () => {
+ beforeEach(() => {
+ jest.spyOn(eventHub, '$emit').mockImplementation();
+ });
+
const userDeletionObstacles = [
{ name: 'schedule1', type: OBSTACLE_TYPES.oncallSchedules },
{ name: 'policy1', type: OBSTACLE_TYPES.escalationPolicies },
];
- it.each(DELETE_ACTIONS.map((action) => [action, paths[action]]))(
- 'renders a dropdown item for "%s"',
- async (action, expectedPath) => {
+ it.each(DELETE_ACTIONS)(
+ 'renders a dropdown item that opens the delete user modal when clicked for "%s"',
+ async (action) => {
initComponent({
component: Actions[capitalizeFirstCharacter(action)],
props: {
@@ -59,21 +61,19 @@ describe('Action components', () => {
paths,
userDeletionObstacles,
},
- stubs: { SharedDeleteAction },
});
- await nextTick();
- const sharedAction = wrapper.find(SharedDeleteAction);
+ await findDropdownItem().vm.$emit('click');
- expect(sharedAction.attributes('data-block-user-url')).toBe(paths.block);
- expect(sharedAction.attributes('data-delete-user-url')).toBe(expectedPath);
- expect(sharedAction.attributes('data-gl-modal-action')).toBe(kebabCase(action));
- expect(sharedAction.attributes('data-username')).toBe('John Doe');
- expect(sharedAction.attributes('data-user-deletion-obstacles')).toBe(
- JSON.stringify(userDeletionObstacles),
+ expect(eventHub.$emit).toHaveBeenCalledWith(
+ EVENT_OPEN_DELETE_USER_MODAL,
+ expect.objectContaining({
+ username: 'John Doe',
+ blockPath: paths.block,
+ deletePath: paths[action],
+ userDeletionObstacles,
+ }),
);
-
- expect(findDropdownItem().exists()).toBe(true);
},
);
});
diff --git a/spec/frontend/admin/users/components/modals/__snapshots__/delete_user_modal_spec.js.snap b/spec/frontend/admin/users/components/modals/__snapshots__/delete_user_modal_spec.js.snap
index 7a17ef2cc6c..265569ac0e3 100644
--- a/spec/frontend/admin/users/components/modals/__snapshots__/delete_user_modal_spec.js.snap
+++ b/spec/frontend/admin/users/components/modals/__snapshots__/delete_user_modal_spec.js.snap
@@ -1,160 +1,28 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`User Operation confirmation modal renders modal with form included 1`] = `
-<div>
- <p>
- <gl-sprintf-stub
- message="content"
- />
- </p>
-
- <user-deletion-obstacles-list-stub
- obstacles="schedule1,policy1"
- username="username"
+exports[`Delete user modal renders modal with form included 1`] = `
+<form
+ action=""
+ method="post"
+>
+ <input
+ name="_method"
+ type="hidden"
+ value="delete"
/>
- <p>
- <gl-sprintf-stub
- message="To confirm, type %{username}"
- />
- </p>
-
- <form
- action="delete-url"
- method="post"
- >
- <input
- name="_method"
- type="hidden"
- value="delete"
- />
-
- <input
- name="authenticity_token"
- type="hidden"
- value="csrf"
- />
-
- <gl-form-input-stub
- autocomplete="off"
- autofocus=""
- name="username"
- type="text"
- value=""
- />
- </form>
- <gl-button-stub
- buttontextclasses=""
- category="primary"
- icon=""
- size="medium"
- variant="default"
- >
- Cancel
- </gl-button-stub>
-
- <gl-button-stub
- buttontextclasses=""
- category="secondary"
- disabled="true"
- icon=""
- size="medium"
- variant="danger"
- >
-
- secondaryAction
-
- </gl-button-stub>
-
- <gl-button-stub
- buttontextclasses=""
- category="primary"
- disabled="true"
- icon=""
- size="medium"
- variant="danger"
- >
- action
- </gl-button-stub>
-</div>
-`;
-
-exports[`User Operation confirmation modal when user's name has leading and trailing whitespace displays user's name without whitespace 1`] = `
-<div>
- <p>
- content
- </p>
-
- <user-deletion-obstacles-list-stub
- obstacles="schedule1,policy1"
- username="John Smith"
+ <input
+ name="authenticity_token"
+ type="hidden"
+ value="csrf"
/>
- <p>
- To confirm, type
- <code
- class="gl-white-space-pre-wrap"
- >
- John Smith
- </code>
- </p>
-
- <form
- action="delete-url"
- method="post"
- >
- <input
- name="_method"
- type="hidden"
- value="delete"
- />
-
- <input
- name="authenticity_token"
- type="hidden"
- value="csrf"
- />
-
- <gl-form-input-stub
- autocomplete="off"
- autofocus=""
- name="username"
- type="text"
- value=""
- />
- </form>
- <gl-button-stub
- buttontextclasses=""
- category="primary"
- icon=""
- size="medium"
- variant="default"
- >
- Cancel
- </gl-button-stub>
-
- <gl-button-stub
- buttontextclasses=""
- category="secondary"
- disabled="true"
- icon=""
- size="medium"
- variant="danger"
- >
-
- secondaryAction
-
- </gl-button-stub>
-
- <gl-button-stub
- buttontextclasses=""
- category="primary"
- disabled="true"
- icon=""
- size="medium"
- variant="danger"
- >
- action
- </gl-button-stub>
-</div>
+ <gl-form-input-stub
+ autocomplete="off"
+ autofocus=""
+ name="username"
+ type="text"
+ value=""
+ />
+</form>
`;
diff --git a/spec/frontend/admin/users/components/modals/delete_user_modal_spec.js b/spec/frontend/admin/users/components/modals/delete_user_modal_spec.js
index f875cd24ee1..09a345ac826 100644
--- a/spec/frontend/admin/users/components/modals/delete_user_modal_spec.js
+++ b/spec/frontend/admin/users/components/modals/delete_user_modal_spec.js
@@ -1,6 +1,8 @@
import { GlButton, GlFormInput, GlSprintf } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
-import { nextTick } from 'vue';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import eventHub, {
+ EVENT_OPEN_DELETE_USER_MODAL,
+} from '~/admin/users/components/modals/delete_user_modal_event_hub';
import DeleteUserModal from '~/admin/users/components/modals/delete_user_modal.vue';
import UserDeletionObstaclesList from '~/vue_shared/components/user_deletion_obstacles/user_deletion_obstacles_list.vue';
import ModalStub from './stubs/modal_stub';
@@ -9,7 +11,7 @@ const TEST_DELETE_USER_URL = 'delete-url';
const TEST_BLOCK_USER_URL = 'block-url';
const TEST_CSRF = 'csrf';
-describe('User Operation confirmation modal', () => {
+describe('Delete user modal', () => {
let wrapper;
let formSubmitSpy;
@@ -27,28 +29,36 @@ describe('User Operation confirmation modal', () => {
const getMethodParam = () => new FormData(findForm().element).get('_method');
const getFormAction = () => findForm().attributes('action');
const findUserDeletionObstaclesList = () => wrapper.findComponent(UserDeletionObstaclesList);
+ const findMessageUsername = () => wrapper.findByTestId('message-username');
+ const findConfirmUsername = () => wrapper.findByTestId('confirm-username');
+ const emitOpenModalEvent = (modalData) => {
+ return eventHub.$emit(EVENT_OPEN_DELETE_USER_MODAL, modalData);
+ };
const setUsername = (username) => {
- findUsernameInput().vm.$emit('input', username);
+ return findUsernameInput().vm.$emit('input', username);
};
const username = 'username';
const badUsername = 'bad_username';
- const userDeletionObstacles = '["schedule1", "policy1"]';
+ const userDeletionObstacles = ['schedule1', 'policy1'];
+
+ const mockModalData = {
+ username,
+ blockPath: TEST_BLOCK_USER_URL,
+ deletePath: TEST_DELETE_USER_URL,
+ userDeletionObstacles,
+ i18n: {
+ title: 'Modal for %{username}',
+ primaryButtonLabel: 'Delete user',
+ messageBody: 'Delete %{username} or rather %{strongStart}block user%{strongEnd}?',
+ },
+ };
- const createComponent = (props = {}, stubs = {}) => {
- wrapper = shallowMount(DeleteUserModal, {
+ const createComponent = (stubs = {}) => {
+ wrapper = shallowMountExtended(DeleteUserModal, {
propsData: {
- username,
- title: 'title',
- content: 'content',
- action: 'action',
- secondaryAction: 'secondaryAction',
- deleteUserUrl: TEST_DELETE_USER_URL,
- blockUserUrl: TEST_BLOCK_USER_URL,
csrfToken: TEST_CSRF,
- userDeletionObstacles,
- ...props,
},
stubs: {
GlModal: ModalStub,
@@ -68,7 +78,7 @@ describe('User Operation confirmation modal', () => {
it('renders modal with form included', () => {
createComponent();
- expect(wrapper.element).toMatchSnapshot();
+ expect(findForm().element).toMatchSnapshot();
});
describe('on created', () => {
@@ -83,11 +93,11 @@ describe('User Operation confirmation modal', () => {
});
describe('with incorrect username', () => {
- beforeEach(async () => {
+ beforeEach(() => {
createComponent();
- setUsername(badUsername);
+ emitOpenModalEvent(mockModalData);
- await nextTick();
+ return setUsername(badUsername);
});
it('shows incorrect username', () => {
@@ -101,11 +111,11 @@ describe('User Operation confirmation modal', () => {
});
describe('with correct username', () => {
- beforeEach(async () => {
+ beforeEach(() => {
createComponent();
- setUsername(username);
+ emitOpenModalEvent(mockModalData);
- await nextTick();
+ return setUsername(username);
});
it('shows correct username', () => {
@@ -117,11 +127,9 @@ describe('User Operation confirmation modal', () => {
expect(findSecondaryButton().attributes('disabled')).toBeFalsy();
});
- describe('when primary action is submitted', () => {
- beforeEach(async () => {
- findPrimaryButton().vm.$emit('click');
-
- await nextTick();
+ describe('when primary action is clicked', () => {
+ beforeEach(() => {
+ return findPrimaryButton().vm.$emit('click');
});
it('clears the input', () => {
@@ -136,11 +144,9 @@ describe('User Operation confirmation modal', () => {
});
});
- describe('when secondary action is submitted', () => {
- beforeEach(async () => {
- findSecondaryButton().vm.$emit('click');
-
- await nextTick();
+ describe('when secondary action is clicked', () => {
+ beforeEach(() => {
+ return findSecondaryButton().vm.$emit('click');
});
it('has correct form attributes and calls submit', () => {
@@ -154,22 +160,23 @@ describe('User Operation confirmation modal', () => {
describe("when user's name has leading and trailing whitespace", () => {
beforeEach(() => {
- createComponent(
- {
- username: ' John Smith ',
- },
- { GlSprintf },
- );
+ createComponent({ GlSprintf });
+ return emitOpenModalEvent({ ...mockModalData, username: ' John Smith ' });
});
it("displays user's name without whitespace", () => {
- expect(wrapper.element).toMatchSnapshot();
+ expect(findMessageUsername().text()).toBe('John Smith');
+ expect(findConfirmUsername().text()).toBe('John Smith');
});
- it("shows enabled buttons when user's name is entered without whitespace", async () => {
- setUsername('John Smith');
+ it('passes user name without whitespace to the obstacles', () => {
+ expect(findUserDeletionObstaclesList().props()).toMatchObject({
+ userName: 'John Smith',
+ });
+ });
- await nextTick();
+ it("shows enabled buttons when user's name is entered without whitespace", async () => {
+ await setUsername('John Smith');
expect(findPrimaryButton().attributes('disabled')).toBeUndefined();
expect(findSecondaryButton().attributes('disabled')).toBeUndefined();
@@ -177,17 +184,20 @@ describe('User Operation confirmation modal', () => {
});
describe('Related user-deletion-obstacles list', () => {
- it('does NOT render the list when user has no related obstacles', () => {
- createComponent({ userDeletionObstacles: '[]' });
+ it('does NOT render the list when user has no related obstacles', async () => {
+ createComponent();
+ await emitOpenModalEvent({ ...mockModalData, userDeletionObstacles: [] });
+
expect(findUserDeletionObstaclesList().exists()).toBe(false);
});
- it('renders the list when user has related obstalces', () => {
+ it('renders the list when user has related obstalces', async () => {
createComponent();
+ await emitOpenModalEvent(mockModalData);
const obstacles = findUserDeletionObstaclesList();
expect(obstacles.exists()).toBe(true);
- expect(obstacles.props('obstacles')).toEqual(JSON.parse(userDeletionObstacles));
+ expect(obstacles.props('obstacles')).toEqual(userDeletionObstacles);
});
});
});
diff --git a/spec/frontend/admin/users/components/modals/user_modal_manager_spec.js b/spec/frontend/admin/users/components/modals/user_modal_manager_spec.js
deleted file mode 100644
index 4786357faa1..00000000000
--- a/spec/frontend/admin/users/components/modals/user_modal_manager_spec.js
+++ /dev/null
@@ -1,126 +0,0 @@
-import { mount } from '@vue/test-utils';
-import { nextTick } from 'vue';
-import UserModalManager from '~/admin/users/components/modals/user_modal_manager.vue';
-import ModalStub from './stubs/modal_stub';
-
-describe('Users admin page Modal Manager', () => {
- let wrapper;
-
- const modalConfiguration = {
- action1: {
- title: 'action1',
- content: 'Action Modal 1',
- },
- action2: {
- title: 'action2',
- content: 'Action Modal 2',
- },
- };
-
- const findModal = () => wrapper.find({ ref: 'modal' });
-
- const createComponent = (props = {}) => {
- wrapper = mount(UserModalManager, {
- propsData: {
- selector: '.js-delete-user-modal-button',
- modalConfiguration,
- csrfToken: 'dummyCSRF',
- ...props,
- },
- stubs: {
- DeleteUserModal: ModalStub,
- },
- });
- };
-
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
- describe('render behavior', () => {
- it('does not renders modal when initialized', () => {
- createComponent();
- expect(findModal().exists()).toBeFalsy();
- });
-
- it('throws if action has no proper configuration', () => {
- createComponent({
- modalConfiguration: {},
- });
- expect(() => wrapper.vm.show({ glModalAction: 'action1' })).toThrow();
- });
-
- it('renders modal with expected props when valid configuration is passed', async () => {
- createComponent();
- wrapper.vm.show({
- glModalAction: 'action1',
- extraProp: 'extraPropValue',
- });
-
- await nextTick();
- const modal = findModal();
- expect(modal.exists()).toBeTruthy();
- expect(modal.vm.$attrs.csrfToken).toEqual('dummyCSRF');
- expect(modal.vm.$attrs.extraProp).toEqual('extraPropValue');
- expect(modal.vm.showWasCalled).toBeTruthy();
- });
- });
-
- describe('click handling', () => {
- let button;
- let button2;
-
- const createButtons = () => {
- button = document.createElement('button');
- button2 = document.createElement('button');
- button.setAttribute('class', 'js-delete-user-modal-button');
- button.setAttribute('data-username', 'foo');
- button.setAttribute('data-gl-modal-action', 'action1');
- button.setAttribute('data-block-user-url', '/block');
- button.setAttribute('data-delete-user-url', '/delete');
- document.body.appendChild(button);
- document.body.appendChild(button2);
- };
- const removeButtons = () => {
- button.remove();
- button = null;
- button2.remove();
- button2 = null;
- };
-
- beforeEach(() => {
- createButtons();
- createComponent();
- });
-
- afterEach(() => {
- removeButtons();
- });
-
- it('renders the modal when the button is clicked', async () => {
- button.click();
-
- await nextTick();
-
- expect(findModal().exists()).toBe(true);
- });
-
- it('does not render the modal when a misconfigured button is clicked', async () => {
- button.removeAttribute('data-gl-modal-action');
- button.click();
-
- await nextTick();
-
- expect(findModal().exists()).toBe(false);
- });
-
- it('does not render the modal when a button without the selector class is clicked', async () => {
- button2.click();
-
- await nextTick();
-
- expect(findModal().exists()).toBe(false);
- });
- });
-});
diff --git a/spec/frontend/alerts_settings/components/alerts_settings_wrapper_spec.js b/spec/frontend/alerts_settings/components/alerts_settings_wrapper_spec.js
index 6193233881d..ed185c11732 100644
--- a/spec/frontend/alerts_settings/components/alerts_settings_wrapper_spec.js
+++ b/spec/frontend/alerts_settings/components/alerts_settings_wrapper_spec.js
@@ -476,9 +476,6 @@ describe('AlertsSettingsWrapper', () => {
destroyHttpIntegration(wrapper);
expect(destroyIntegrationHandler).toHaveBeenCalled();
- await waitForPromises();
-
- expect(findIntegrations()).toHaveLength(3);
});
it('displays flash if mutation had a recoverable error', async () => {
diff --git a/spec/frontend/alerts_settings/components/mocks/apollo_mock.js b/spec/frontend/alerts_settings/components/mocks/apollo_mock.js
index 694dff56632..170af1b5e0c 100644
--- a/spec/frontend/alerts_settings/components/mocks/apollo_mock.js
+++ b/spec/frontend/alerts_settings/components/mocks/apollo_mock.js
@@ -102,7 +102,7 @@ export const destroyIntegrationResponse = {
httpIntegrationDestroy: {
errors: [],
integration: {
- __typename: 'AlertManagementIntegration',
+ __typename: 'AlertManagementHttpIntegration',
id: '37',
type: 'HTTP',
active: true,
diff --git a/spec/frontend/api/alert_management_alerts_api_spec.js b/spec/frontend/api/alert_management_alerts_api_spec.js
new file mode 100644
index 00000000000..aac14e64286
--- /dev/null
+++ b/spec/frontend/api/alert_management_alerts_api_spec.js
@@ -0,0 +1,140 @@
+import MockAdapter from 'axios-mock-adapter';
+import * as alertManagementAlertsApi from '~/api/alert_management_alerts_api';
+import axios from '~/lib/utils/axios_utils';
+
+describe('~/api/alert_management_alerts_api.js', () => {
+ let mock;
+ let originalGon;
+
+ const projectId = 1;
+ const alertIid = 2;
+
+ const imageData = { filePath: 'test', filename: 'hello', id: 5, url: null };
+
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+
+ originalGon = window.gon;
+ window.gon = { api_version: 'v4' };
+ });
+
+ afterEach(() => {
+ mock.restore();
+ window.gon = originalGon;
+ });
+
+ describe('fetchAlertMetricImages', () => {
+ beforeEach(() => {
+ jest.spyOn(axios, 'get');
+ });
+
+ it('retrieves metric images from the correct URL and returns them in the response data', () => {
+ const expectedUrl = `/api/v4/projects/${projectId}/alert_management_alerts/${alertIid}/metric_images`;
+ const expectedData = [imageData];
+ const options = { alertIid, id: projectId };
+
+ mock.onGet(expectedUrl).reply(200, { data: expectedData });
+
+ return alertManagementAlertsApi.fetchAlertMetricImages(options).then(({ data }) => {
+ expect(axios.get).toHaveBeenCalledWith(expectedUrl);
+ expect(data.data).toEqual(expectedData);
+ });
+ });
+ });
+
+ describe('uploadAlertMetricImage', () => {
+ beforeEach(() => {
+ jest.spyOn(axios, 'post');
+ });
+
+ it('uploads a metric image to the correct URL and returns it in the response data', () => {
+ const expectedUrl = `/api/v4/projects/${projectId}/alert_management_alerts/${alertIid}/metric_images`;
+ const expectedData = [imageData];
+
+ const file = new File(['zip contents'], 'hello');
+ const url = 'https://www.example.com';
+ const urlText = 'Example website';
+
+ const expectedFormData = new FormData();
+ expectedFormData.append('file', file);
+ expectedFormData.append('url', url);
+ expectedFormData.append('url_text', urlText);
+
+ mock.onPost(expectedUrl).reply(201, { data: expectedData });
+
+ return alertManagementAlertsApi
+ .uploadAlertMetricImage({
+ alertIid,
+ id: projectId,
+ file,
+ url,
+ urlText,
+ })
+ .then(({ data }) => {
+ expect(data).toEqual({ data: expectedData });
+ expect(axios.post).toHaveBeenCalledWith(expectedUrl, expectedFormData, {
+ headers: { 'Content-Type': 'multipart/form-data' },
+ });
+ });
+ });
+ });
+
+ describe('updateAlertMetricImage', () => {
+ beforeEach(() => {
+ jest.spyOn(axios, 'put');
+ });
+
+ it('updates a metric image to the correct URL and returns it in the response data', () => {
+ const imageIid = 3;
+ const expectedUrl = `/api/v4/projects/${projectId}/alert_management_alerts/${alertIid}/metric_images/${imageIid}`;
+ const expectedData = [imageData];
+
+ const url = 'https://www.example.com';
+ const urlText = 'Example website';
+
+ const expectedFormData = new FormData();
+ expectedFormData.append('url', url);
+ expectedFormData.append('url_text', urlText);
+
+ mock.onPut(expectedUrl).reply(200, { data: expectedData });
+
+ return alertManagementAlertsApi
+ .updateAlertMetricImage({
+ alertIid,
+ id: projectId,
+ imageId: imageIid,
+ url,
+ urlText,
+ })
+ .then(({ data }) => {
+ expect(data).toEqual({ data: expectedData });
+ expect(axios.put).toHaveBeenCalledWith(expectedUrl, expectedFormData);
+ });
+ });
+ });
+
+ describe('deleteAlertMetricImage', () => {
+ beforeEach(() => {
+ jest.spyOn(axios, 'delete');
+ });
+
+ it('deletes a metric image to the correct URL and returns it in the response data', () => {
+ const imageIid = 3;
+ const expectedUrl = `/api/v4/projects/${projectId}/alert_management_alerts/${alertIid}/metric_images/${imageIid}`;
+ const expectedData = [imageData];
+
+ mock.onDelete(expectedUrl).reply(204, { data: expectedData });
+
+ return alertManagementAlertsApi
+ .deleteAlertMetricImage({
+ alertIid,
+ id: projectId,
+ imageId: imageIid,
+ })
+ .then(({ data }) => {
+ expect(data).toEqual({ data: expectedData });
+ expect(axios.delete).toHaveBeenCalledWith(expectedUrl);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/api_spec.js b/spec/frontend/api_spec.js
index bc3e12d3fc4..85332bf21d8 100644
--- a/spec/frontend/api_spec.js
+++ b/spec/frontend/api_spec.js
@@ -2,6 +2,9 @@ import MockAdapter from 'axios-mock-adapter';
import Api, { DEFAULT_PER_PAGE } from '~/api';
import axios from '~/lib/utils/axios_utils';
import httpStatus from '~/lib/utils/http_status';
+import createFlash from '~/flash';
+
+jest.mock('~/flash');
describe('Api', () => {
const dummyApiVersion = 'v3000';
@@ -155,66 +158,44 @@ describe('Api', () => {
});
describe('group', () => {
- it('fetches a group', (done) => {
+ it('fetches a group', () => {
const groupId = '123456';
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/groups/${groupId}`;
mock.onGet(expectedUrl).reply(httpStatus.OK, {
name: 'test',
});
- Api.group(groupId, (response) => {
- expect(response.name).toBe('test');
- done();
+ return new Promise((resolve) => {
+ Api.group(groupId, (response) => {
+ expect(response.name).toBe('test');
+ resolve();
+ });
});
});
});
describe('groupMembers', () => {
- it('fetches group members', (done) => {
+ it('fetches group members', () => {
const groupId = '54321';
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/groups/${groupId}/members`;
const expectedData = [{ id: 7 }];
mock.onGet(expectedUrl).reply(httpStatus.OK, expectedData);
- Api.groupMembers(groupId)
- .then(({ data }) => {
- expect(data).toEqual(expectedData);
- })
- .then(done)
- .catch(done.fail);
- });
- });
-
- describe('addGroupMembersByUserId', () => {
- it('adds an existing User as a new Group Member by User ID', () => {
- const groupId = 1;
- const expectedUserId = 2;
- const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/groups/1/members`;
- const params = {
- user_id: expectedUserId,
- access_level: 10,
- expires_at: undefined,
- };
-
- mock.onPost(expectedUrl).reply(200, {
- id: expectedUserId,
- state: 'active',
- });
-
- return Api.addGroupMembersByUserId(groupId, params).then(({ data }) => {
- expect(data.id).toBe(expectedUserId);
- expect(data.state).toBe('active');
+ return Api.groupMembers(groupId).then(({ data }) => {
+ expect(data).toEqual(expectedData);
});
});
});
- describe('inviteGroupMembersByEmail', () => {
+ describe('inviteGroupMembers', () => {
it('invites a new email address to create a new User and become a Group Member', () => {
const groupId = 1;
const email = 'email@example.com';
+ const userId = '1';
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/groups/1/invitations`;
const params = {
email,
+ userId,
access_level: 10,
expires_at: undefined,
};
@@ -223,14 +204,14 @@ describe('Api', () => {
status: 'success',
});
- return Api.inviteGroupMembersByEmail(groupId, params).then(({ data }) => {
+ return Api.inviteGroupMembers(groupId, params).then(({ data }) => {
expect(data.status).toBe('success');
});
});
});
describe('groupMilestones', () => {
- it('fetches group milestones', (done) => {
+ it('fetches group milestones', () => {
const groupId = '16';
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/groups/${groupId}/milestones`;
const expectedData = [
@@ -250,17 +231,14 @@ describe('Api', () => {
];
mock.onGet(expectedUrl).reply(httpStatus.OK, expectedData);
- Api.groupMilestones(groupId)
- .then(({ data }) => {
- expect(data).toEqual(expectedData);
- })
- .then(done)
- .catch(done.fail);
+ return Api.groupMilestones(groupId).then(({ data }) => {
+ expect(data).toEqual(expectedData);
+ });
});
});
describe('groups', () => {
- it('fetches groups', (done) => {
+ it('fetches groups', () => {
const query = 'dummy query';
const options = { unused: 'option' };
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/groups.json`;
@@ -270,16 +248,18 @@ describe('Api', () => {
},
]);
- Api.groups(query, options, (response) => {
- expect(response.length).toBe(1);
- expect(response[0].name).toBe('test');
- done();
+ return new Promise((resolve) => {
+ Api.groups(query, options, (response) => {
+ expect(response.length).toBe(1);
+ expect(response[0].name).toBe('test');
+ resolve();
+ });
});
});
});
describe('groupLabels', () => {
- it('fetches group labels', (done) => {
+ it('fetches group labels', () => {
const options = { params: { search: 'foo' } };
const expectedGroup = 'gitlab-org';
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/groups/${expectedGroup}/labels`;
@@ -290,18 +270,15 @@ describe('Api', () => {
},
]);
- Api.groupLabels(expectedGroup, options)
- .then((res) => {
- expect(res.length).toBe(1);
- expect(res[0].name).toBe('Foo Label');
- })
- .then(done)
- .catch(done.fail);
+ return Api.groupLabels(expectedGroup, options).then((res) => {
+ expect(res.length).toBe(1);
+ expect(res[0].name).toBe('Foo Label');
+ });
});
});
describe('namespaces', () => {
- it('fetches namespaces', (done) => {
+ it('fetches namespaces', () => {
const query = 'dummy query';
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/namespaces.json`;
mock.onGet(expectedUrl).reply(httpStatus.OK, [
@@ -310,16 +287,18 @@ describe('Api', () => {
},
]);
- Api.namespaces(query, (response) => {
- expect(response.length).toBe(1);
- expect(response[0].name).toBe('test');
- done();
+ return new Promise((resolve) => {
+ Api.namespaces(query, (response) => {
+ expect(response.length).toBe(1);
+ expect(response[0].name).toBe('test');
+ resolve();
+ });
});
});
});
describe('projects', () => {
- it('fetches projects with membership when logged in', (done) => {
+ it('fetches projects with membership when logged in', () => {
const query = 'dummy query';
const options = { unused: 'option' };
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects.json`;
@@ -330,14 +309,16 @@ describe('Api', () => {
},
]);
- Api.projects(query, options, (response) => {
- expect(response.length).toBe(1);
- expect(response[0].name).toBe('test');
- done();
+ return new Promise((resolve) => {
+ Api.projects(query, options, (response) => {
+ expect(response.length).toBe(1);
+ expect(response[0].name).toBe('test');
+ resolve();
+ });
});
});
- it('fetches projects without membership when not logged in', (done) => {
+ it('fetches projects without membership when not logged in', () => {
const query = 'dummy query';
const options = { unused: 'option' };
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects.json`;
@@ -347,31 +328,30 @@ describe('Api', () => {
},
]);
- Api.projects(query, options, (response) => {
- expect(response.length).toBe(1);
- expect(response[0].name).toBe('test');
- done();
+ return new Promise((resolve) => {
+ Api.projects(query, options, (response) => {
+ expect(response.length).toBe(1);
+ expect(response[0].name).toBe('test');
+ resolve();
+ });
});
});
});
describe('updateProject', () => {
- it('update a project with the given payload', (done) => {
+ it('update a project with the given payload', () => {
const projectPath = 'foo';
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects/${projectPath}`;
mock.onPut(expectedUrl).reply(httpStatus.OK, { foo: 'bar' });
- Api.updateProject(projectPath, { foo: 'bar' })
- .then(({ data }) => {
- expect(data.foo).toBe('bar');
- done();
- })
- .catch(done.fail);
+ return Api.updateProject(projectPath, { foo: 'bar' }).then(({ data }) => {
+ expect(data.foo).toBe('bar');
+ });
});
});
describe('projectUsers', () => {
- it('fetches all users of a particular project', (done) => {
+ it('fetches all users of a particular project', () => {
const query = 'dummy query';
const options = { unused: 'option' };
const projectPath = 'gitlab-org%2Fgitlab-ce';
@@ -382,13 +362,10 @@ describe('Api', () => {
},
]);
- Api.projectUsers('gitlab-org/gitlab-ce', query, options)
- .then((response) => {
- expect(response.length).toBe(1);
- expect(response[0].name).toBe('test');
- })
- .then(done)
- .catch(done.fail);
+ return Api.projectUsers('gitlab-org/gitlab-ce', query, options).then((response) => {
+ expect(response.length).toBe(1);
+ expect(response[0].name).toBe('test');
+ });
});
});
@@ -396,38 +373,32 @@ describe('Api', () => {
const projectPath = 'abc';
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects/${projectPath}/merge_requests`;
- it('fetches all merge requests for a project', (done) => {
+ it('fetches all merge requests for a project', () => {
const mockData = [{ source_branch: 'foo' }, { source_branch: 'bar' }];
mock.onGet(expectedUrl).reply(httpStatus.OK, mockData);
- Api.projectMergeRequests(projectPath)
- .then(({ data }) => {
- expect(data.length).toEqual(2);
- expect(data[0].source_branch).toBe('foo');
- expect(data[1].source_branch).toBe('bar');
- })
- .then(done)
- .catch(done.fail);
+ return Api.projectMergeRequests(projectPath).then(({ data }) => {
+ expect(data.length).toEqual(2);
+ expect(data[0].source_branch).toBe('foo');
+ expect(data[1].source_branch).toBe('bar');
+ });
});
- it('fetches merge requests filtered with passed params', (done) => {
+ it('fetches merge requests filtered with passed params', () => {
const params = {
source_branch: 'bar',
};
const mockData = [{ source_branch: 'bar' }];
mock.onGet(expectedUrl, { params }).reply(httpStatus.OK, mockData);
- Api.projectMergeRequests(projectPath, params)
- .then(({ data }) => {
- expect(data.length).toEqual(1);
- expect(data[0].source_branch).toBe('bar');
- })
- .then(done)
- .catch(done.fail);
+ return Api.projectMergeRequests(projectPath, params).then(({ data }) => {
+ expect(data.length).toEqual(1);
+ expect(data[0].source_branch).toBe('bar');
+ });
});
});
describe('projectMergeRequest', () => {
- it('fetches a merge request', (done) => {
+ it('fetches a merge request', () => {
const projectPath = 'abc';
const mergeRequestId = '123456';
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects/${projectPath}/merge_requests/${mergeRequestId}`;
@@ -435,17 +406,14 @@ describe('Api', () => {
title: 'test',
});
- Api.projectMergeRequest(projectPath, mergeRequestId)
- .then(({ data }) => {
- expect(data.title).toBe('test');
- })
- .then(done)
- .catch(done.fail);
+ return Api.projectMergeRequest(projectPath, mergeRequestId).then(({ data }) => {
+ expect(data.title).toBe('test');
+ });
});
});
describe('projectMergeRequestChanges', () => {
- it('fetches the changes of a merge request', (done) => {
+ it('fetches the changes of a merge request', () => {
const projectPath = 'abc';
const mergeRequestId = '123456';
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects/${projectPath}/merge_requests/${mergeRequestId}/changes`;
@@ -453,17 +421,14 @@ describe('Api', () => {
title: 'test',
});
- Api.projectMergeRequestChanges(projectPath, mergeRequestId)
- .then(({ data }) => {
- expect(data.title).toBe('test');
- })
- .then(done)
- .catch(done.fail);
+ return Api.projectMergeRequestChanges(projectPath, mergeRequestId).then(({ data }) => {
+ expect(data.title).toBe('test');
+ });
});
});
describe('projectMergeRequestVersions', () => {
- it('fetches the versions of a merge request', (done) => {
+ it('fetches the versions of a merge request', () => {
const projectPath = 'abc';
const mergeRequestId = '123456';
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects/${projectPath}/merge_requests/${mergeRequestId}/versions`;
@@ -473,30 +438,24 @@ describe('Api', () => {
},
]);
- Api.projectMergeRequestVersions(projectPath, mergeRequestId)
- .then(({ data }) => {
- expect(data.length).toBe(1);
- expect(data[0].id).toBe(123);
- })
- .then(done)
- .catch(done.fail);
+ return Api.projectMergeRequestVersions(projectPath, mergeRequestId).then(({ data }) => {
+ expect(data.length).toBe(1);
+ expect(data[0].id).toBe(123);
+ });
});
});
describe('projectRunners', () => {
- it('fetches the runners of a project', (done) => {
+ it('fetches the runners of a project', () => {
const projectPath = 7;
const params = { scope: 'active' };
const mockData = [{ id: 4 }];
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects/${projectPath}/runners`;
mock.onGet(expectedUrl, { params }).reply(httpStatus.OK, mockData);
- Api.projectRunners(projectPath, { params })
- .then(({ data }) => {
- expect(data).toEqual(mockData);
- })
- .then(done)
- .catch(done.fail);
+ return Api.projectRunners(projectPath, { params }).then(({ data }) => {
+ expect(data).toEqual(mockData);
+ });
});
});
@@ -525,7 +484,7 @@ describe('Api', () => {
});
describe('projectMilestones', () => {
- it('fetches project milestones', (done) => {
+ it('fetches project milestones', () => {
const projectId = 1;
const options = { state: 'active' };
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects/1/milestones`;
@@ -537,13 +496,10 @@ describe('Api', () => {
},
]);
- Api.projectMilestones(projectId, options)
- .then(({ data }) => {
- expect(data.length).toBe(1);
- expect(data[0].title).toBe('milestone1');
- })
- .then(done)
- .catch(done.fail);
+ return Api.projectMilestones(projectId, options).then(({ data }) => {
+ expect(data.length).toBe(1);
+ expect(data[0].title).toBe('milestone1');
+ });
});
});
@@ -566,36 +522,15 @@ describe('Api', () => {
});
});
- describe('addProjectMembersByUserId', () => {
- it('adds an existing User as a new Project Member by User ID', () => {
- const projectId = 1;
- const expectedUserId = 2;
- const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects/1/members`;
- const params = {
- user_id: expectedUserId,
- access_level: 10,
- expires_at: undefined,
- };
-
- mock.onPost(expectedUrl).reply(200, {
- id: expectedUserId,
- state: 'active',
- });
-
- return Api.addProjectMembersByUserId(projectId, params).then(({ data }) => {
- expect(data.id).toBe(expectedUserId);
- expect(data.state).toBe('active');
- });
- });
- });
-
- describe('inviteProjectMembersByEmail', () => {
+ describe('inviteProjectMembers', () => {
it('invites a new email address to create a new User and become a Project Member', () => {
const projectId = 1;
- const expectedEmail = 'email@example.com';
+ const email = 'email@example.com';
+ const userId = '1';
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects/1/invitations`;
const params = {
- email: expectedEmail,
+ email,
+ userId,
access_level: 10,
expires_at: undefined,
};
@@ -604,14 +539,14 @@ describe('Api', () => {
status: 'success',
});
- return Api.inviteProjectMembersByEmail(projectId, params).then(({ data }) => {
+ return Api.inviteProjectMembers(projectId, params).then(({ data }) => {
expect(data.status).toBe('success');
});
});
});
describe('newLabel', () => {
- it('creates a new project label', (done) => {
+ it('creates a new project label', () => {
const namespace = 'some namespace';
const project = 'some project';
const labelData = { some: 'data' };
@@ -630,13 +565,15 @@ describe('Api', () => {
];
});
- Api.newLabel(namespace, project, labelData, (response) => {
- expect(response.name).toBe('test');
- done();
+ return new Promise((resolve) => {
+ Api.newLabel(namespace, project, labelData, (response) => {
+ expect(response.name).toBe('test');
+ resolve();
+ });
});
});
- it('creates a new group label', (done) => {
+ it('creates a new group label', () => {
const namespace = 'group/subgroup';
const labelData = { name: 'Foo', color: '#000000' };
const expectedUrl = Api.buildUrl(Api.groupLabelsPath).replace(':namespace_path', namespace);
@@ -651,15 +588,17 @@ describe('Api', () => {
];
});
- Api.newLabel(namespace, undefined, labelData, (response) => {
- expect(response.name).toBe('Foo');
- done();
+ return new Promise((resolve) => {
+ Api.newLabel(namespace, undefined, labelData, (response) => {
+ expect(response.name).toBe('Foo');
+ resolve();
+ });
});
});
});
describe('groupProjects', () => {
- it('fetches group projects', (done) => {
+ it('fetches group projects', () => {
const groupId = '123456';
const query = 'dummy query';
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/groups/${groupId}/projects.json`;
@@ -669,11 +608,40 @@ describe('Api', () => {
},
]);
- Api.groupProjects(groupId, query, {}, (response) => {
- expect(response.length).toBe(1);
- expect(response[0].name).toBe('test');
- done();
+ return new Promise((resolve) => {
+ Api.groupProjects(groupId, query, {}, (response) => {
+ expect(response.length).toBe(1);
+ expect(response[0].name).toBe('test');
+ resolve();
+ });
+ });
+ });
+
+ it('uses flesh on error by default', async () => {
+ const groupId = '123456';
+ const query = 'dummy query';
+ const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/groups/${groupId}/projects.json`;
+ const flashCallback = (callCount) => {
+ expect(createFlash).toHaveBeenCalledTimes(callCount);
+ createFlash.mockClear();
+ };
+
+ mock.onGet(expectedUrl).reply(500, null);
+
+ const response = await Api.groupProjects(groupId, query, {}, () => {}).then(() => {
+ flashCallback(1);
});
+ expect(response).toBeUndefined();
+ });
+
+ it('NOT uses flesh on error with param useCustomErrorHandler', async () => {
+ const groupId = '123456';
+ const query = 'dummy query';
+ const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/groups/${groupId}/projects.json`;
+
+ mock.onGet(expectedUrl).reply(500, null);
+ const apiCall = Api.groupProjects(groupId, query, {}, () => {}, true);
+ await expect(apiCall).rejects.toThrow();
});
});
@@ -734,12 +702,14 @@ describe('Api', () => {
templateKey,
)}`;
- it('fetches an issue template', (done) => {
+ it('fetches an issue template', () => {
mock.onGet(expectedUrl).reply(httpStatus.OK, 'test');
- Api.issueTemplate(namespace, project, templateKey, templateType, (error, response) => {
- expect(response).toBe('test');
- done();
+ return new Promise((resolve) => {
+ Api.issueTemplate(namespace, project, templateKey, templateType, (_, response) => {
+ expect(response).toBe('test');
+ resolve();
+ });
});
});
@@ -747,8 +717,11 @@ describe('Api', () => {
it('rejects the Promise', () => {
mock.onGet(expectedUrl).replyOnce(httpStatus.INTERNAL_SERVER_ERROR);
- Api.issueTemplate(namespace, project, templateKey, templateType, () => {
- expect(mock.history.get).toHaveLength(1);
+ return new Promise((resolve) => {
+ Api.issueTemplate(namespace, project, templateKey, templateType, () => {
+ expect(mock.history.get).toHaveLength(1);
+ resolve();
+ });
});
});
});
@@ -760,19 +733,21 @@ describe('Api', () => {
const templateType = 'template type';
const expectedUrl = `${dummyUrlRoot}/${namespace}/${project}/templates/${templateType}`;
- it('fetches all templates by type', (done) => {
+ it('fetches all templates by type', () => {
const expectedData = [
{ key: 'Template1', name: 'Template 1', content: 'This is template 1!' },
];
mock.onGet(expectedUrl).reply(httpStatus.OK, expectedData);
- Api.issueTemplates(namespace, project, templateType, (error, response) => {
- expect(response.length).toBe(1);
- const { key, name, content } = response[0];
- expect(key).toBe('Template1');
- expect(name).toBe('Template 1');
- expect(content).toBe('This is template 1!');
- done();
+ return new Promise((resolve) => {
+ Api.issueTemplates(namespace, project, templateType, (_, response) => {
+ expect(response.length).toBe(1);
+ const { key, name, content } = response[0];
+ expect(key).toBe('Template1');
+ expect(name).toBe('Template 1');
+ expect(content).toBe('This is template 1!');
+ resolve();
+ });
});
});
@@ -788,34 +763,44 @@ describe('Api', () => {
});
describe('projectTemplates', () => {
- it('fetches a list of templates', (done) => {
+ it('fetches a list of templates', () => {
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects/gitlab-org%2Fgitlab-ce/templates/licenses`;
mock.onGet(expectedUrl).reply(httpStatus.OK, 'test');
- Api.projectTemplates('gitlab-org/gitlab-ce', 'licenses', {}, (response) => {
- expect(response).toBe('test');
- done();
+ return new Promise((resolve) => {
+ Api.projectTemplates('gitlab-org/gitlab-ce', 'licenses', {}, (response) => {
+ expect(response).toBe('test');
+ resolve();
+ });
});
});
});
describe('projectTemplate', () => {
- it('fetches a single template', (done) => {
+ it('fetches a single template', () => {
const data = { unused: 'option' };
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects/gitlab-org%2Fgitlab-ce/templates/licenses/test%20license`;
mock.onGet(expectedUrl).reply(httpStatus.OK, 'test');
- Api.projectTemplate('gitlab-org/gitlab-ce', 'licenses', 'test license', data, (response) => {
- expect(response).toBe('test');
- done();
+ return new Promise((resolve) => {
+ Api.projectTemplate(
+ 'gitlab-org/gitlab-ce',
+ 'licenses',
+ 'test license',
+ data,
+ (response) => {
+ expect(response).toBe('test');
+ resolve();
+ },
+ );
});
});
});
describe('users', () => {
- it('fetches users', (done) => {
+ it('fetches users', () => {
const query = 'dummy query';
const options = { unused: 'option' };
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/users.json`;
@@ -825,68 +810,56 @@ describe('Api', () => {
},
]);
- Api.users(query, options)
- .then(({ data }) => {
- expect(data.length).toBe(1);
- expect(data[0].name).toBe('test');
- })
- .then(done)
- .catch(done.fail);
+ return Api.users(query, options).then(({ data }) => {
+ expect(data.length).toBe(1);
+ expect(data[0].name).toBe('test');
+ });
});
});
describe('user', () => {
- it('fetches single user', (done) => {
+ it('fetches single user', () => {
const userId = '123456';
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/users/${userId}`;
mock.onGet(expectedUrl).reply(httpStatus.OK, {
name: 'testuser',
});
- Api.user(userId)
- .then(({ data }) => {
- expect(data.name).toBe('testuser');
- })
- .then(done)
- .catch(done.fail);
+ return Api.user(userId).then(({ data }) => {
+ expect(data.name).toBe('testuser');
+ });
});
});
describe('user counts', () => {
- it('fetches single user counts', (done) => {
+ it('fetches single user counts', () => {
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/user_counts`;
mock.onGet(expectedUrl).reply(httpStatus.OK, {
merge_requests: 4,
});
- Api.userCounts()
- .then(({ data }) => {
- expect(data.merge_requests).toBe(4);
- })
- .then(done)
- .catch(done.fail);
+ return Api.userCounts().then(({ data }) => {
+ expect(data.merge_requests).toBe(4);
+ });
});
});
describe('user status', () => {
- it('fetches single user status', (done) => {
+ it('fetches single user status', () => {
const userId = '123456';
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/users/${userId}/status`;
mock.onGet(expectedUrl).reply(httpStatus.OK, {
message: 'testmessage',
});
- Api.userStatus(userId)
- .then(({ data }) => {
- expect(data.message).toBe('testmessage');
- })
- .then(done)
- .catch(done.fail);
+ return Api.userStatus(userId).then(({ data }) => {
+ expect(data.message).toBe('testmessage');
+ });
});
});
describe('user projects', () => {
- it('fetches all projects that belong to a particular user', (done) => {
+ it('fetches all projects that belong to a particular user', () => {
const query = 'dummy query';
const options = { unused: 'option' };
const userId = '123456';
@@ -897,16 +870,18 @@ describe('Api', () => {
},
]);
- Api.userProjects(userId, query, options, (response) => {
- expect(response.length).toBe(1);
- expect(response[0].name).toBe('test');
- done();
+ return new Promise((resolve) => {
+ Api.userProjects(userId, query, options, (response) => {
+ expect(response.length).toBe(1);
+ expect(response[0].name).toBe('test');
+ resolve();
+ });
});
});
});
describe('commitPipelines', () => {
- it('fetches pipelines for a given commit', (done) => {
+ it('fetches pipelines for a given commit', () => {
const projectId = 'example/foobar';
const commitSha = 'abc123def';
const expectedUrl = `${dummyUrlRoot}/${projectId}/commit/${commitSha}/pipelines`;
@@ -916,13 +891,10 @@ describe('Api', () => {
},
]);
- Api.commitPipelines(projectId, commitSha)
- .then(({ data }) => {
- expect(data.length).toBe(1);
- expect(data[0].name).toBe('test');
- })
- .then(done)
- .catch(done.fail);
+ return Api.commitPipelines(projectId, commitSha).then(({ data }) => {
+ expect(data.length).toBe(1);
+ expect(data[0].name).toBe('test');
+ });
});
});
@@ -947,7 +919,7 @@ describe('Api', () => {
});
describe('createBranch', () => {
- it('creates new branch', (done) => {
+ it('creates new branch', () => {
const ref = 'main';
const branch = 'new-branch-name';
const dummyProjectPath = 'gitlab-org/gitlab-ce';
@@ -961,18 +933,15 @@ describe('Api', () => {
name: branch,
});
- Api.createBranch(dummyProjectPath, { ref, branch })
- .then(({ data }) => {
- expect(data.name).toBe(branch);
- expect(axios.post).toHaveBeenCalledWith(expectedUrl, { ref, branch });
- })
- .then(done)
- .catch(done.fail);
+ return Api.createBranch(dummyProjectPath, { ref, branch }).then(({ data }) => {
+ expect(data.name).toBe(branch);
+ expect(axios.post).toHaveBeenCalledWith(expectedUrl, { ref, branch });
+ });
});
});
describe('projectForks', () => {
- it('gets forked projects', (done) => {
+ it('gets forked projects', () => {
const dummyProjectPath = 'gitlab-org/gitlab-ce';
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects/${encodeURIComponent(
dummyProjectPath,
@@ -982,20 +951,17 @@ describe('Api', () => {
mock.onGet(expectedUrl).replyOnce(httpStatus.OK, ['fork']);
- Api.projectForks(dummyProjectPath, { visibility: 'private' })
- .then(({ data }) => {
- expect(data).toEqual(['fork']);
- expect(axios.get).toHaveBeenCalledWith(expectedUrl, {
- params: { visibility: 'private' },
- });
- })
- .then(done)
- .catch(done.fail);
+ return Api.projectForks(dummyProjectPath, { visibility: 'private' }).then(({ data }) => {
+ expect(data).toEqual(['fork']);
+ expect(axios.get).toHaveBeenCalledWith(expectedUrl, {
+ params: { visibility: 'private' },
+ });
+ });
});
});
describe('createContextCommits', () => {
- it('creates a new context commit', (done) => {
+ it('creates a new context commit', () => {
const projectPath = 'abc';
const mergeRequestId = '123456';
const commitsData = ['abcdefg'];
@@ -1014,17 +980,16 @@ describe('Api', () => {
},
]);
- Api.createContextCommits(projectPath, mergeRequestId, expectedData)
- .then(({ data }) => {
+ return Api.createContextCommits(projectPath, mergeRequestId, expectedData).then(
+ ({ data }) => {
expect(data[0].title).toBe('Dummy commit');
- })
- .then(done)
- .catch(done.fail);
+ },
+ );
});
});
describe('allContextCommits', () => {
- it('gets all context commits', (done) => {
+ it('gets all context commits', () => {
const projectPath = 'abc';
const mergeRequestId = '123456';
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects/${projectPath}/merge_requests/${mergeRequestId}/context_commits`;
@@ -1035,17 +1000,14 @@ describe('Api', () => {
.onGet(expectedUrl)
.replyOnce(200, [{ id: 'abcdef', short_id: 'abcdefghi', title: 'Dummy commit title' }]);
- Api.allContextCommits(projectPath, mergeRequestId)
- .then(({ data }) => {
- expect(data[0].title).toBe('Dummy commit title');
- })
- .then(done)
- .catch(done.fail);
+ return Api.allContextCommits(projectPath, mergeRequestId).then(({ data }) => {
+ expect(data[0].title).toBe('Dummy commit title');
+ });
});
});
describe('removeContextCommits', () => {
- it('removes context commits', (done) => {
+ it('removes context commits', () => {
const projectPath = 'abc';
const mergeRequestId = '123456';
const commitsData = ['abcdefg'];
@@ -1058,12 +1020,9 @@ describe('Api', () => {
mock.onDelete(expectedUrl).replyOnce(204);
- Api.removeContextCommits(projectPath, mergeRequestId, expectedData)
- .then(() => {
- expect(axios.delete).toHaveBeenCalledWith(expectedUrl, { data: expectedData });
- })
- .then(done)
- .catch(done.fail);
+ return Api.removeContextCommits(projectPath, mergeRequestId, expectedData).then(() => {
+ expect(axios.delete).toHaveBeenCalledWith(expectedUrl, { data: expectedData });
+ });
});
});
@@ -1306,41 +1265,37 @@ describe('Api', () => {
});
describe('updateIssue', () => {
- it('update an issue with the given payload', (done) => {
+ it('update an issue with the given payload', () => {
const projectId = 8;
const issue = 1;
const expectedArray = [1, 2, 3];
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects/${projectId}/issues/${issue}`;
mock.onPut(expectedUrl).reply(httpStatus.OK, { assigneeIds: expectedArray });
- Api.updateIssue(projectId, issue, { assigneeIds: expectedArray })
- .then(({ data }) => {
- expect(data.assigneeIds).toEqual(expectedArray);
- done();
- })
- .catch(done.fail);
+ return Api.updateIssue(projectId, issue, { assigneeIds: expectedArray }).then(({ data }) => {
+ expect(data.assigneeIds).toEqual(expectedArray);
+ });
});
});
describe('updateMergeRequest', () => {
- it('update an issue with the given payload', (done) => {
+ it('update an issue with the given payload', () => {
const projectId = 8;
const mergeRequest = 1;
const expectedArray = [1, 2, 3];
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects/${projectId}/merge_requests/${mergeRequest}`;
mock.onPut(expectedUrl).reply(httpStatus.OK, { assigneeIds: expectedArray });
- Api.updateMergeRequest(projectId, mergeRequest, { assigneeIds: expectedArray })
- .then(({ data }) => {
+ return Api.updateMergeRequest(projectId, mergeRequest, { assigneeIds: expectedArray }).then(
+ ({ data }) => {
expect(data.assigneeIds).toEqual(expectedArray);
- done();
- })
- .catch(done.fail);
+ },
+ );
});
});
describe('tags', () => {
- it('fetches all tags of a particular project', (done) => {
+ it('fetches all tags of a particular project', () => {
const query = 'dummy query';
const options = { unused: 'option' };
const projectId = 8;
@@ -1351,13 +1306,10 @@ describe('Api', () => {
},
]);
- Api.tags(projectId, query, options)
- .then(({ data }) => {
- expect(data.length).toBe(1);
- expect(data[0].name).toBe('test');
- })
- .then(done)
- .catch(done.fail);
+ return Api.tags(projectId, query, options).then(({ data }) => {
+ expect(data.length).toBe(1);
+ expect(data[0].name).toBe('test');
+ });
});
});
@@ -1641,6 +1593,18 @@ describe('Api', () => {
});
});
+ describe('dependency proxy cache', () => {
+ it('schedules the cache list for deletion', async () => {
+ const groupId = 1;
+ const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/groups/${groupId}/dependency_proxy/cache`;
+
+ mock.onDelete(expectedUrl).reply(httpStatus.ACCEPTED);
+ const { status } = await Api.deleteDependencyProxyCacheList(groupId, {});
+
+ expect(status).toBe(httpStatus.ACCEPTED);
+ });
+ });
+
describe('Feature Flag User List', () => {
let expectedUrl;
let projectId;
@@ -1727,4 +1691,36 @@ describe('Api', () => {
});
});
});
+
+ describe('projectProtectedBranch', () => {
+ const branchName = 'new-branch-name';
+ const dummyProjectId = 5;
+ const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects/${dummyProjectId}/protected_branches/${branchName}`;
+
+ it('returns 404 for non-existing branch', () => {
+ jest.spyOn(axios, 'get');
+
+ mock.onGet(expectedUrl).replyOnce(httpStatus.NOT_FOUND, {
+ message: '404 Not found',
+ });
+
+ return Api.projectProtectedBranch(dummyProjectId, branchName).catch((error) => {
+ expect(error.response.status).toBe(httpStatus.NOT_FOUND);
+ expect(axios.get).toHaveBeenCalledWith(expectedUrl);
+ });
+ });
+
+ it('returns 200 with branch information', () => {
+ const expectedObj = { name: branchName };
+
+ jest.spyOn(axios, 'get');
+
+ mock.onGet(expectedUrl).replyOnce(httpStatus.OK, expectedObj);
+
+ return Api.projectProtectedBranch(dummyProjectId, branchName).then((data) => {
+ expect(data).toEqual(expectedObj);
+ expect(axios.get).toHaveBeenCalledWith(expectedUrl);
+ });
+ });
+ });
});
diff --git a/spec/frontend/authentication/u2f/authenticate_spec.js b/spec/frontend/authentication/u2f/authenticate_spec.js
index 153d4be56af..31782899ce4 100644
--- a/spec/frontend/authentication/u2f/authenticate_spec.js
+++ b/spec/frontend/authentication/u2f/authenticate_spec.js
@@ -36,24 +36,19 @@ describe('U2FAuthenticate', () => {
window.u2f = oldu2f;
});
- it('falls back to normal 2fa', (done) => {
- component
- .start()
- .then(() => {
- expect(component.switchToFallbackUI).toHaveBeenCalled();
- done();
- })
- .catch(done.fail);
+ it('falls back to normal 2fa', async () => {
+ await component.start();
+ expect(component.switchToFallbackUI).toHaveBeenCalled();
});
});
describe('with u2f available', () => {
- beforeEach((done) => {
+ beforeEach(() => {
// bypass automatic form submission within renderAuthenticated
jest.spyOn(component, 'renderAuthenticated').mockReturnValue(true);
u2fDevice = new MockU2FDevice();
- component.start().then(done).catch(done.fail);
+ return component.start();
});
it('allows authenticating via a U2F device', () => {
diff --git a/spec/frontend/authentication/u2f/register_spec.js b/spec/frontend/authentication/u2f/register_spec.js
index a814144ac7a..810396aa9fd 100644
--- a/spec/frontend/authentication/u2f/register_spec.js
+++ b/spec/frontend/authentication/u2f/register_spec.js
@@ -8,12 +8,12 @@ describe('U2FRegister', () => {
let container;
let component;
- beforeEach((done) => {
+ beforeEach(() => {
loadFixtures('u2f/register.html');
u2fDevice = new MockU2FDevice();
container = $('#js-register-token-2fa');
component = new U2FRegister(container, {});
- component.start().then(done).catch(done.fail);
+ return component.start();
});
it('allows registering a U2F device', () => {
diff --git a/spec/frontend/badges/components/badge_spec.js b/spec/frontend/badges/components/badge_spec.js
index 2310fb8bd8e..fe4cf8ce8eb 100644
--- a/spec/frontend/badges/components/badge_spec.js
+++ b/spec/frontend/badges/components/badge_spec.js
@@ -89,11 +89,9 @@ describe('Badge component', () => {
});
describe('behavior', () => {
- beforeEach((done) => {
+ beforeEach(() => {
setFixtures('<div id="dummy-element"></div>');
- createComponent({ ...dummyProps }, '#dummy-element')
- .then(done)
- .catch(done.fail);
+ return createComponent({ ...dummyProps }, '#dummy-element');
});
it('shows a badge image after loading', () => {
diff --git a/spec/frontend/badges/store/actions_spec.js b/spec/frontend/badges/store/actions_spec.js
index 75699f24463..02e1b8e65e4 100644
--- a/spec/frontend/badges/store/actions_spec.js
+++ b/spec/frontend/badges/store/actions_spec.js
@@ -33,41 +33,38 @@ describe('Badges store actions', () => {
});
describe('requestNewBadge', () => {
- it('commits REQUEST_NEW_BADGE', (done) => {
- testAction(
+ it('commits REQUEST_NEW_BADGE', () => {
+ return testAction(
actions.requestNewBadge,
null,
state,
[{ type: mutationTypes.REQUEST_NEW_BADGE }],
[],
- done,
);
});
});
describe('receiveNewBadge', () => {
- it('commits RECEIVE_NEW_BADGE', (done) => {
+ it('commits RECEIVE_NEW_BADGE', () => {
const newBadge = createDummyBadge();
- testAction(
+ return testAction(
actions.receiveNewBadge,
newBadge,
state,
[{ type: mutationTypes.RECEIVE_NEW_BADGE, payload: newBadge }],
[],
- done,
);
});
});
describe('receiveNewBadgeError', () => {
- it('commits RECEIVE_NEW_BADGE_ERROR', (done) => {
- testAction(
+ it('commits RECEIVE_NEW_BADGE_ERROR', () => {
+ return testAction(
actions.receiveNewBadgeError,
null,
state,
[{ type: mutationTypes.RECEIVE_NEW_BADGE_ERROR }],
[],
- done,
);
});
});
@@ -87,7 +84,7 @@ describe('Badges store actions', () => {
};
});
- it('dispatches requestNewBadge and receiveNewBadge for successful response', (done) => {
+ it('dispatches requestNewBadge and receiveNewBadge for successful response', async () => {
const dummyResponse = createDummyBadgeResponse();
endpointMock.replyOnce((req) => {
@@ -105,16 +102,12 @@ describe('Badges store actions', () => {
});
const dummyBadge = transformBackendBadge(dummyResponse);
- actions
- .addBadge({ state, dispatch })
- .then(() => {
- expect(dispatch.mock.calls).toEqual([['receiveNewBadge', dummyBadge]]);
- })
- .then(done)
- .catch(done.fail);
+
+ await actions.addBadge({ state, dispatch });
+ expect(dispatch.mock.calls).toEqual([['receiveNewBadge', dummyBadge]]);
});
- it('dispatches requestNewBadge and receiveNewBadgeError for error response', (done) => {
+ it('dispatches requestNewBadge and receiveNewBadgeError for error response', async () => {
endpointMock.replyOnce((req) => {
expect(req.data).toBe(
JSON.stringify({
@@ -129,52 +122,43 @@ describe('Badges store actions', () => {
return [500, ''];
});
- actions
- .addBadge({ state, dispatch })
- .then(() => done.fail('Expected Ajax call to fail!'))
- .catch(() => {
- expect(dispatch.mock.calls).toEqual([['receiveNewBadgeError']]);
- })
- .then(done)
- .catch(done.fail);
+ await expect(actions.addBadge({ state, dispatch })).rejects.toThrow();
+ expect(dispatch.mock.calls).toEqual([['receiveNewBadgeError']]);
});
});
describe('requestDeleteBadge', () => {
- it('commits REQUEST_DELETE_BADGE', (done) => {
- testAction(
+ it('commits REQUEST_DELETE_BADGE', () => {
+ return testAction(
actions.requestDeleteBadge,
badgeId,
state,
[{ type: mutationTypes.REQUEST_DELETE_BADGE, payload: badgeId }],
[],
- done,
);
});
});
describe('receiveDeleteBadge', () => {
- it('commits RECEIVE_DELETE_BADGE', (done) => {
- testAction(
+ it('commits RECEIVE_DELETE_BADGE', () => {
+ return testAction(
actions.receiveDeleteBadge,
badgeId,
state,
[{ type: mutationTypes.RECEIVE_DELETE_BADGE, payload: badgeId }],
[],
- done,
);
});
});
describe('receiveDeleteBadgeError', () => {
- it('commits RECEIVE_DELETE_BADGE_ERROR', (done) => {
- testAction(
+ it('commits RECEIVE_DELETE_BADGE_ERROR', () => {
+ return testAction(
actions.receiveDeleteBadgeError,
badgeId,
state,
[{ type: mutationTypes.RECEIVE_DELETE_BADGE_ERROR, payload: badgeId }],
[],
- done,
);
});
});
@@ -188,91 +172,76 @@ describe('Badges store actions', () => {
dispatch = jest.fn();
});
- it('dispatches requestDeleteBadge and receiveDeleteBadge for successful response', (done) => {
+ it('dispatches requestDeleteBadge and receiveDeleteBadge for successful response', async () => {
endpointMock.replyOnce(() => {
expect(dispatch.mock.calls).toEqual([['requestDeleteBadge', badgeId]]);
dispatch.mockClear();
return [200, ''];
});
- actions
- .deleteBadge({ state, dispatch }, { id: badgeId })
- .then(() => {
- expect(dispatch.mock.calls).toEqual([['receiveDeleteBadge', badgeId]]);
- })
- .then(done)
- .catch(done.fail);
+ await actions.deleteBadge({ state, dispatch }, { id: badgeId });
+ expect(dispatch.mock.calls).toEqual([['receiveDeleteBadge', badgeId]]);
});
- it('dispatches requestDeleteBadge and receiveDeleteBadgeError for error response', (done) => {
+ it('dispatches requestDeleteBadge and receiveDeleteBadgeError for error response', async () => {
endpointMock.replyOnce(() => {
expect(dispatch.mock.calls).toEqual([['requestDeleteBadge', badgeId]]);
dispatch.mockClear();
return [500, ''];
});
- actions
- .deleteBadge({ state, dispatch }, { id: badgeId })
- .then(() => done.fail('Expected Ajax call to fail!'))
- .catch(() => {
- expect(dispatch.mock.calls).toEqual([['receiveDeleteBadgeError', badgeId]]);
- })
- .then(done)
- .catch(done.fail);
+ await expect(actions.deleteBadge({ state, dispatch }, { id: badgeId })).rejects.toThrow();
+ expect(dispatch.mock.calls).toEqual([['receiveDeleteBadgeError', badgeId]]);
});
});
describe('editBadge', () => {
- it('commits START_EDITING', (done) => {
+ it('commits START_EDITING', () => {
const dummyBadge = createDummyBadge();
- testAction(
+ return testAction(
actions.editBadge,
dummyBadge,
state,
[{ type: mutationTypes.START_EDITING, payload: dummyBadge }],
[],
- done,
);
});
});
describe('requestLoadBadges', () => {
- it('commits REQUEST_LOAD_BADGES', (done) => {
+ it('commits REQUEST_LOAD_BADGES', () => {
const dummyData = 'this is not real data';
- testAction(
+ return testAction(
actions.requestLoadBadges,
dummyData,
state,
[{ type: mutationTypes.REQUEST_LOAD_BADGES, payload: dummyData }],
[],
- done,
);
});
});
describe('receiveLoadBadges', () => {
- it('commits RECEIVE_LOAD_BADGES', (done) => {
+ it('commits RECEIVE_LOAD_BADGES', () => {
const badges = dummyBadges;
- testAction(
+ return testAction(
actions.receiveLoadBadges,
badges,
state,
[{ type: mutationTypes.RECEIVE_LOAD_BADGES, payload: badges }],
[],
- done,
);
});
});
describe('receiveLoadBadgesError', () => {
- it('commits RECEIVE_LOAD_BADGES_ERROR', (done) => {
- testAction(
+ it('commits RECEIVE_LOAD_BADGES_ERROR', () => {
+ return testAction(
actions.receiveLoadBadgesError,
null,
state,
[{ type: mutationTypes.RECEIVE_LOAD_BADGES_ERROR }],
[],
- done,
);
});
});
@@ -286,7 +255,7 @@ describe('Badges store actions', () => {
dispatch = jest.fn();
});
- it('dispatches requestLoadBadges and receiveLoadBadges for successful response', (done) => {
+ it('dispatches requestLoadBadges and receiveLoadBadges for successful response', async () => {
const dummyData = 'this is just some data';
const dummyReponse = [
createDummyBadgeResponse(),
@@ -299,18 +268,13 @@ describe('Badges store actions', () => {
return [200, dummyReponse];
});
- actions
- .loadBadges({ state, dispatch }, dummyData)
- .then(() => {
- const badges = dummyReponse.map(transformBackendBadge);
+ await actions.loadBadges({ state, dispatch }, dummyData);
+ const badges = dummyReponse.map(transformBackendBadge);
- expect(dispatch.mock.calls).toEqual([['receiveLoadBadges', badges]]);
- })
- .then(done)
- .catch(done.fail);
+ expect(dispatch.mock.calls).toEqual([['receiveLoadBadges', badges]]);
});
- it('dispatches requestLoadBadges and receiveLoadBadgesError for error response', (done) => {
+ it('dispatches requestLoadBadges and receiveLoadBadgesError for error response', async () => {
const dummyData = 'this is just some data';
endpointMock.replyOnce(() => {
expect(dispatch.mock.calls).toEqual([['requestLoadBadges', dummyData]]);
@@ -318,53 +282,44 @@ describe('Badges store actions', () => {
return [500, ''];
});
- actions
- .loadBadges({ state, dispatch }, dummyData)
- .then(() => done.fail('Expected Ajax call to fail!'))
- .catch(() => {
- expect(dispatch.mock.calls).toEqual([['receiveLoadBadgesError']]);
- })
- .then(done)
- .catch(done.fail);
+ await expect(actions.loadBadges({ state, dispatch }, dummyData)).rejects.toThrow();
+ expect(dispatch.mock.calls).toEqual([['receiveLoadBadgesError']]);
});
});
describe('requestRenderedBadge', () => {
- it('commits REQUEST_RENDERED_BADGE', (done) => {
- testAction(
+ it('commits REQUEST_RENDERED_BADGE', () => {
+ return testAction(
actions.requestRenderedBadge,
null,
state,
[{ type: mutationTypes.REQUEST_RENDERED_BADGE }],
[],
- done,
);
});
});
describe('receiveRenderedBadge', () => {
- it('commits RECEIVE_RENDERED_BADGE', (done) => {
+ it('commits RECEIVE_RENDERED_BADGE', () => {
const dummyBadge = createDummyBadge();
- testAction(
+ return testAction(
actions.receiveRenderedBadge,
dummyBadge,
state,
[{ type: mutationTypes.RECEIVE_RENDERED_BADGE, payload: dummyBadge }],
[],
- done,
);
});
});
describe('receiveRenderedBadgeError', () => {
- it('commits RECEIVE_RENDERED_BADGE_ERROR', (done) => {
- testAction(
+ it('commits RECEIVE_RENDERED_BADGE_ERROR', () => {
+ return testAction(
actions.receiveRenderedBadgeError,
null,
state,
[{ type: mutationTypes.RECEIVE_RENDERED_BADGE_ERROR }],
[],
- done,
);
});
});
@@ -388,56 +343,41 @@ describe('Badges store actions', () => {
dispatch = jest.fn();
});
- it('returns immediately if imageUrl is empty', (done) => {
+ it('returns immediately if imageUrl is empty', async () => {
jest.spyOn(axios, 'get').mockImplementation(() => {});
badgeInForm.imageUrl = '';
- actions
- .renderBadge({ state, dispatch })
- .then(() => {
- expect(axios.get).not.toHaveBeenCalled();
- })
- .then(done)
- .catch(done.fail);
+ await actions.renderBadge({ state, dispatch });
+ expect(axios.get).not.toHaveBeenCalled();
});
- it('returns immediately if linkUrl is empty', (done) => {
+ it('returns immediately if linkUrl is empty', async () => {
jest.spyOn(axios, 'get').mockImplementation(() => {});
badgeInForm.linkUrl = '';
- actions
- .renderBadge({ state, dispatch })
- .then(() => {
- expect(axios.get).not.toHaveBeenCalled();
- })
- .then(done)
- .catch(done.fail);
+ await actions.renderBadge({ state, dispatch });
+ expect(axios.get).not.toHaveBeenCalled();
});
- it('escapes user input', (done) => {
+ it('escapes user input', async () => {
jest
.spyOn(axios, 'get')
.mockImplementation(() => Promise.resolve({ data: createDummyBadgeResponse() }));
badgeInForm.imageUrl = '&make-sandwich=true';
badgeInForm.linkUrl = '<script>I am dangerous!</script>';
- actions
- .renderBadge({ state, dispatch })
- .then(() => {
- expect(axios.get.mock.calls.length).toBe(1);
- const url = axios.get.mock.calls[0][0];
+ await actions.renderBadge({ state, dispatch });
+ expect(axios.get.mock.calls.length).toBe(1);
+ 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$'));
- })
- .then(done)
- .catch(done.fail);
+ 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$'));
});
- it('dispatches requestRenderedBadge and receiveRenderedBadge for successful response', (done) => {
+ it('dispatches requestRenderedBadge and receiveRenderedBadge for successful response', async () => {
const dummyReponse = createDummyBadgeResponse();
endpointMock.replyOnce(() => {
expect(dispatch.mock.calls).toEqual([['requestRenderedBadge']]);
@@ -445,71 +385,57 @@ describe('Badges store actions', () => {
return [200, dummyReponse];
});
- actions
- .renderBadge({ state, dispatch })
- .then(() => {
- const renderedBadge = transformBackendBadge(dummyReponse);
+ await actions.renderBadge({ state, dispatch });
+ const renderedBadge = transformBackendBadge(dummyReponse);
- expect(dispatch.mock.calls).toEqual([['receiveRenderedBadge', renderedBadge]]);
- })
- .then(done)
- .catch(done.fail);
+ expect(dispatch.mock.calls).toEqual([['receiveRenderedBadge', renderedBadge]]);
});
- it('dispatches requestRenderedBadge and receiveRenderedBadgeError for error response', (done) => {
+ it('dispatches requestRenderedBadge and receiveRenderedBadgeError for error response', async () => {
endpointMock.replyOnce(() => {
expect(dispatch.mock.calls).toEqual([['requestRenderedBadge']]);
dispatch.mockClear();
return [500, ''];
});
- actions
- .renderBadge({ state, dispatch })
- .then(() => done.fail('Expected Ajax call to fail!'))
- .catch(() => {
- expect(dispatch.mock.calls).toEqual([['receiveRenderedBadgeError']]);
- })
- .then(done)
- .catch(done.fail);
+ await expect(actions.renderBadge({ state, dispatch })).rejects.toThrow();
+ expect(dispatch.mock.calls).toEqual([['receiveRenderedBadgeError']]);
});
});
describe('requestUpdatedBadge', () => {
- it('commits REQUEST_UPDATED_BADGE', (done) => {
- testAction(
+ it('commits REQUEST_UPDATED_BADGE', () => {
+ return testAction(
actions.requestUpdatedBadge,
null,
state,
[{ type: mutationTypes.REQUEST_UPDATED_BADGE }],
[],
- done,
);
});
});
describe('receiveUpdatedBadge', () => {
- it('commits RECEIVE_UPDATED_BADGE', (done) => {
+ it('commits RECEIVE_UPDATED_BADGE', () => {
const updatedBadge = createDummyBadge();
- testAction(
+ return testAction(
actions.receiveUpdatedBadge,
updatedBadge,
state,
[{ type: mutationTypes.RECEIVE_UPDATED_BADGE, payload: updatedBadge }],
[],
- done,
);
});
});
describe('receiveUpdatedBadgeError', () => {
- it('commits RECEIVE_UPDATED_BADGE_ERROR', (done) => {
- testAction(
+ it('commits RECEIVE_UPDATED_BADGE_ERROR', () => {
+ return testAction(
actions.receiveUpdatedBadgeError,
null,
state,
[{ type: mutationTypes.RECEIVE_UPDATED_BADGE_ERROR }],
[],
- done,
);
});
});
@@ -529,7 +455,7 @@ describe('Badges store actions', () => {
dispatch = jest.fn();
});
- it('dispatches requestUpdatedBadge and receiveUpdatedBadge for successful response', (done) => {
+ it('dispatches requestUpdatedBadge and receiveUpdatedBadge for successful response', async () => {
const dummyResponse = createDummyBadgeResponse();
endpointMock.replyOnce((req) => {
@@ -547,16 +473,11 @@ describe('Badges store actions', () => {
});
const updatedBadge = transformBackendBadge(dummyResponse);
- actions
- .saveBadge({ state, dispatch })
- .then(() => {
- expect(dispatch.mock.calls).toEqual([['receiveUpdatedBadge', updatedBadge]]);
- })
- .then(done)
- .catch(done.fail);
+ await actions.saveBadge({ state, dispatch });
+ expect(dispatch.mock.calls).toEqual([['receiveUpdatedBadge', updatedBadge]]);
});
- it('dispatches requestUpdatedBadge and receiveUpdatedBadgeError for error response', (done) => {
+ it('dispatches requestUpdatedBadge and receiveUpdatedBadgeError for error response', async () => {
endpointMock.replyOnce((req) => {
expect(req.data).toBe(
JSON.stringify({
@@ -571,53 +492,44 @@ describe('Badges store actions', () => {
return [500, ''];
});
- actions
- .saveBadge({ state, dispatch })
- .then(() => done.fail('Expected Ajax call to fail!'))
- .catch(() => {
- expect(dispatch.mock.calls).toEqual([['receiveUpdatedBadgeError']]);
- })
- .then(done)
- .catch(done.fail);
+ await expect(actions.saveBadge({ state, dispatch })).rejects.toThrow();
+ expect(dispatch.mock.calls).toEqual([['receiveUpdatedBadgeError']]);
});
});
describe('stopEditing', () => {
- it('commits STOP_EDITING', (done) => {
- testAction(
+ it('commits STOP_EDITING', () => {
+ return testAction(
actions.stopEditing,
null,
state,
[{ type: mutationTypes.STOP_EDITING }],
[],
- done,
);
});
});
describe('updateBadgeInForm', () => {
- it('commits UPDATE_BADGE_IN_FORM', (done) => {
+ it('commits UPDATE_BADGE_IN_FORM', () => {
const dummyBadge = createDummyBadge();
- testAction(
+ return testAction(
actions.updateBadgeInForm,
dummyBadge,
state,
[{ type: mutationTypes.UPDATE_BADGE_IN_FORM, payload: dummyBadge }],
[],
- done,
);
});
describe('updateBadgeInModal', () => {
- it('commits UPDATE_BADGE_IN_MODAL', (done) => {
+ it('commits UPDATE_BADGE_IN_MODAL', () => {
const dummyBadge = createDummyBadge();
- testAction(
+ return testAction(
actions.updateBadgeInModal,
dummyBadge,
state,
[{ type: mutationTypes.UPDATE_BADGE_IN_MODAL, payload: dummyBadge }],
[],
- done,
);
});
});
diff --git a/spec/frontend/batch_comments/stores/modules/batch_comments/actions_spec.js b/spec/frontend/batch_comments/stores/modules/batch_comments/actions_spec.js
index b0e9e5dd00b..e9535d8cc12 100644
--- a/spec/frontend/batch_comments/stores/modules/batch_comments/actions_spec.js
+++ b/spec/frontend/batch_comments/stores/modules/batch_comments/actions_spec.js
@@ -29,53 +29,56 @@ describe('Batch comments store actions', () => {
});
describe('addDraftToDiscussion', () => {
- it('commits ADD_NEW_DRAFT if no errors returned', (done) => {
+ it('commits ADD_NEW_DRAFT if no errors returned', () => {
res = { id: 1 };
mock.onAny().reply(200, res);
- testAction(
+ return testAction(
actions.addDraftToDiscussion,
{ endpoint: TEST_HOST, data: 'test' },
null,
[{ type: 'ADD_NEW_DRAFT', payload: res }],
[],
- done,
);
});
- it('does not commit ADD_NEW_DRAFT if errors returned', (done) => {
+ it('does not commit ADD_NEW_DRAFT if errors returned', () => {
mock.onAny().reply(500);
- testAction(
+ return testAction(
actions.addDraftToDiscussion,
{ endpoint: TEST_HOST, data: 'test' },
null,
[],
[],
- done,
);
});
});
describe('createNewDraft', () => {
- it('commits ADD_NEW_DRAFT if no errors returned', (done) => {
+ it('commits ADD_NEW_DRAFT if no errors returned', () => {
res = { id: 1 };
mock.onAny().reply(200, res);
- testAction(
+ return testAction(
actions.createNewDraft,
{ endpoint: TEST_HOST, data: 'test' },
null,
[{ type: 'ADD_NEW_DRAFT', payload: res }],
[],
- done,
);
});
- it('does not commit ADD_NEW_DRAFT if errors returned', (done) => {
+ it('does not commit ADD_NEW_DRAFT if errors returned', () => {
mock.onAny().reply(500);
- testAction(actions.createNewDraft, { endpoint: TEST_HOST, data: 'test' }, null, [], [], done);
+ return testAction(
+ actions.createNewDraft,
+ { endpoint: TEST_HOST, data: 'test' },
+ null,
+ [],
+ [],
+ );
});
});
@@ -90,7 +93,7 @@ describe('Batch comments store actions', () => {
};
});
- it('commits DELETE_DRAFT if no errors returned', (done) => {
+ it('commits DELETE_DRAFT if no errors returned', () => {
const commit = jest.fn();
const context = {
getters,
@@ -99,16 +102,12 @@ describe('Batch comments store actions', () => {
res = { id: 1 };
mock.onAny().reply(200);
- actions
- .deleteDraft(context, { id: 1 })
- .then(() => {
- expect(commit).toHaveBeenCalledWith('DELETE_DRAFT', 1);
- })
- .then(done)
- .catch(done.fail);
+ return actions.deleteDraft(context, { id: 1 }).then(() => {
+ expect(commit).toHaveBeenCalledWith('DELETE_DRAFT', 1);
+ });
});
- it('does not commit DELETE_DRAFT if errors returned', (done) => {
+ it('does not commit DELETE_DRAFT if errors returned', () => {
const commit = jest.fn();
const context = {
getters,
@@ -116,13 +115,9 @@ describe('Batch comments store actions', () => {
};
mock.onAny().reply(500);
- actions
- .deleteDraft(context, { id: 1 })
- .then(() => {
- expect(commit).not.toHaveBeenCalledWith('DELETE_DRAFT', 1);
- })
- .then(done)
- .catch(done.fail);
+ return actions.deleteDraft(context, { id: 1 }).then(() => {
+ expect(commit).not.toHaveBeenCalledWith('DELETE_DRAFT', 1);
+ });
});
});
@@ -137,7 +132,7 @@ describe('Batch comments store actions', () => {
};
});
- it('commits SET_BATCH_COMMENTS_DRAFTS with returned data', (done) => {
+ it('commits SET_BATCH_COMMENTS_DRAFTS with returned data', () => {
const commit = jest.fn();
const dispatch = jest.fn();
const context = {
@@ -151,14 +146,10 @@ describe('Batch comments store actions', () => {
res = { id: 1 };
mock.onAny().reply(200, res);
- actions
- .fetchDrafts(context)
- .then(() => {
- expect(commit).toHaveBeenCalledWith('SET_BATCH_COMMENTS_DRAFTS', { id: 1 });
- expect(dispatch).toHaveBeenCalledWith('convertToDiscussion', '1', { root: true });
- })
- .then(done)
- .catch(done.fail);
+ return actions.fetchDrafts(context).then(() => {
+ expect(commit).toHaveBeenCalledWith('SET_BATCH_COMMENTS_DRAFTS', { id: 1 });
+ expect(dispatch).toHaveBeenCalledWith('convertToDiscussion', '1', { root: true });
+ });
});
});
@@ -177,32 +168,24 @@ describe('Batch comments store actions', () => {
rootGetters = { discussionsStructuredByLineCode: 'discussions' };
});
- it('dispatches actions & commits', (done) => {
+ it('dispatches actions & commits', () => {
mock.onAny().reply(200);
- actions
- .publishReview({ dispatch, commit, getters, rootGetters })
- .then(() => {
- expect(commit.mock.calls[0]).toEqual(['REQUEST_PUBLISH_REVIEW']);
- expect(commit.mock.calls[1]).toEqual(['RECEIVE_PUBLISH_REVIEW_SUCCESS']);
+ return actions.publishReview({ dispatch, commit, getters, rootGetters }).then(() => {
+ expect(commit.mock.calls[0]).toEqual(['REQUEST_PUBLISH_REVIEW']);
+ expect(commit.mock.calls[1]).toEqual(['RECEIVE_PUBLISH_REVIEW_SUCCESS']);
- expect(dispatch.mock.calls[0]).toEqual(['updateDiscussionsAfterPublish']);
- })
- .then(done)
- .catch(done.fail);
+ expect(dispatch.mock.calls[0]).toEqual(['updateDiscussionsAfterPublish']);
+ });
});
- it('dispatches error commits', (done) => {
+ it('dispatches error commits', () => {
mock.onAny().reply(500);
- actions
- .publishReview({ dispatch, commit, getters, rootGetters })
- .then(() => {
- expect(commit.mock.calls[0]).toEqual(['REQUEST_PUBLISH_REVIEW']);
- expect(commit.mock.calls[1]).toEqual(['RECEIVE_PUBLISH_REVIEW_ERROR']);
- })
- .then(done)
- .catch(done.fail);
+ return actions.publishReview({ dispatch, commit, getters, rootGetters }).then(() => {
+ expect(commit.mock.calls[0]).toEqual(['REQUEST_PUBLISH_REVIEW']);
+ expect(commit.mock.calls[1]).toEqual(['RECEIVE_PUBLISH_REVIEW_ERROR']);
+ });
});
});
@@ -262,7 +245,7 @@ describe('Batch comments store actions', () => {
});
describe('expandAllDiscussions', () => {
- it('dispatches expandDiscussion for all drafts', (done) => {
+ it('dispatches expandDiscussion for all drafts', () => {
const state = {
drafts: [
{
@@ -271,7 +254,7 @@ describe('Batch comments store actions', () => {
],
};
- testAction(
+ return testAction(
actions.expandAllDiscussions,
null,
state,
@@ -282,7 +265,6 @@ describe('Batch comments store actions', () => {
payload: { discussionId: '1' },
},
],
- done,
);
});
});
diff --git a/spec/frontend/behaviors/gl_emoji_spec.js b/spec/frontend/behaviors/gl_emoji_spec.js
index cac1ea67cf5..8842ad636ec 100644
--- a/spec/frontend/behaviors/gl_emoji_spec.js
+++ b/spec/frontend/behaviors/gl_emoji_spec.js
@@ -77,6 +77,12 @@ describe('gl_emoji', () => {
'<gl-emoji data-name="grey_question" data-unicode-version="6.0" title="white question mark ornament">❔</gl-emoji>',
`<gl-emoji data-name="grey_question" data-unicode-version="6.0" title="white question mark ornament"><img class="emoji" title=":grey_question:" alt=":grey_question:" src="/-/emojis/${EMOJI_VERSION}/grey_question.png" width="20" height="20" align="absmiddle"></gl-emoji>`,
],
+ [
+ 'custom emoji with image fallback',
+ '<gl-emoji data-fallback-src="https://cultofthepartyparrot.com/parrots/hd/parrot.gif" data-name="party-parrot" data-unicode-version="custom"></gl-emoji>',
+ '<gl-emoji data-fallback-src="https://cultofthepartyparrot.com/parrots/hd/parrot.gif" data-name="party-parrot" data-unicode-version="custom"><img class="emoji" title=":party-parrot:" alt=":party-parrot:" src="https://cultofthepartyparrot.com/parrots/hd/parrot.gif" width="20" height="20" align="absmiddle"></gl-emoji>',
+ '<gl-emoji data-fallback-src="https://cultofthepartyparrot.com/parrots/hd/parrot.gif" data-name="party-parrot" data-unicode-version="custom"><img class="emoji" title=":party-parrot:" alt=":party-parrot:" src="https://cultofthepartyparrot.com/parrots/hd/parrot.gif" width="20" height="20" align="absmiddle"></gl-emoji>',
+ ],
])('%s', (name, markup, withEmojiSupport, withoutEmojiSupport) => {
it(`renders correctly with emoji support`, async () => {
jest.spyOn(EmojiUnicodeSupport, 'default').mockReturnValue(true);
diff --git a/spec/frontend/blob/balsamiq/balsamiq_viewer_spec.js b/spec/frontend/blob/balsamiq/balsamiq_viewer_spec.js
deleted file mode 100644
index d7531d15b9a..00000000000
--- a/spec/frontend/blob/balsamiq/balsamiq_viewer_spec.js
+++ /dev/null
@@ -1,363 +0,0 @@
-import sqljs from 'sql.js';
-import ClassSpecHelper from 'helpers/class_spec_helper';
-import BalsamiqViewer from '~/blob/balsamiq/balsamiq_viewer';
-import axios from '~/lib/utils/axios_utils';
-
-jest.mock('sql.js');
-
-describe('BalsamiqViewer', () => {
- const mockArrayBuffer = new ArrayBuffer(10);
- let balsamiqViewer;
- let viewer;
-
- describe('class constructor', () => {
- beforeEach(() => {
- viewer = {};
-
- balsamiqViewer = new BalsamiqViewer(viewer);
- });
-
- it('should set .viewer', () => {
- expect(balsamiqViewer.viewer).toBe(viewer);
- });
- });
-
- describe('loadFile', () => {
- let bv;
- const endpoint = 'endpoint';
- const requestSuccess = Promise.resolve({
- data: mockArrayBuffer,
- status: 200,
- });
-
- beforeEach(() => {
- viewer = {};
- bv = new BalsamiqViewer(viewer);
- });
-
- it('should call `axios.get` on `endpoint` param with responseType set to `arraybuffer', () => {
- jest.spyOn(axios, 'get').mockReturnValue(requestSuccess);
- jest.spyOn(bv, 'renderFile').mockReturnValue();
-
- bv.loadFile(endpoint);
-
- expect(axios.get).toHaveBeenCalledWith(
- endpoint,
- expect.objectContaining({
- responseType: 'arraybuffer',
- }),
- );
- });
-
- it('should call `renderFile` on request success', (done) => {
- jest.spyOn(axios, 'get').mockReturnValue(requestSuccess);
- jest.spyOn(bv, 'renderFile').mockImplementation(() => {});
-
- bv.loadFile(endpoint)
- .then(() => {
- expect(bv.renderFile).toHaveBeenCalledWith(mockArrayBuffer);
- })
- .then(done)
- .catch(done.fail);
- });
-
- it('should not call `renderFile` on request failure', (done) => {
- jest.spyOn(axios, 'get').mockReturnValue(Promise.reject());
- jest.spyOn(bv, 'renderFile').mockImplementation(() => {});
-
- bv.loadFile(endpoint)
- .then(() => {
- done.fail('Expected loadFile to throw error!');
- })
- .catch(() => {
- expect(bv.renderFile).not.toHaveBeenCalled();
- })
- .then(done)
- .catch(done.fail);
- });
- });
-
- describe('renderFile', () => {
- let container;
- let previews;
-
- beforeEach(() => {
- viewer = {
- appendChild: jest.fn(),
- };
- previews = [document.createElement('ul'), document.createElement('ul')];
-
- balsamiqViewer = {
- initDatabase: jest.fn(),
- getPreviews: jest.fn(),
- renderPreview: jest.fn(),
- };
- balsamiqViewer.viewer = viewer;
-
- balsamiqViewer.getPreviews.mockReturnValue(previews);
- balsamiqViewer.renderPreview.mockImplementation((preview) => preview);
- viewer.appendChild.mockImplementation((containerElement) => {
- container = containerElement;
- });
-
- BalsamiqViewer.prototype.renderFile.call(balsamiqViewer, mockArrayBuffer);
- });
-
- it('should call .initDatabase', () => {
- expect(balsamiqViewer.initDatabase).toHaveBeenCalledWith(mockArrayBuffer);
- });
-
- it('should call .getPreviews', () => {
- expect(balsamiqViewer.getPreviews).toHaveBeenCalled();
- });
-
- it('should call .renderPreview for each preview', () => {
- const allArgs = balsamiqViewer.renderPreview.mock.calls;
-
- expect(allArgs.length).toBe(2);
-
- previews.forEach((preview, i) => {
- expect(allArgs[i][0]).toBe(preview);
- });
- });
-
- it('should set the container HTML', () => {
- expect(container.innerHTML).toBe('<ul></ul><ul></ul>');
- });
-
- it('should add inline preview classes', () => {
- expect(container.classList[0]).toBe('list-inline');
- expect(container.classList[1]).toBe('previews');
- });
-
- it('should call viewer.appendChild', () => {
- expect(viewer.appendChild).toHaveBeenCalledWith(container);
- });
- });
-
- describe('initDatabase', () => {
- let uint8Array;
- let data;
-
- beforeEach(() => {
- uint8Array = {};
- data = 'data';
- balsamiqViewer = {};
- window.Uint8Array = jest.fn();
- window.Uint8Array.mockReturnValue(uint8Array);
-
- BalsamiqViewer.prototype.initDatabase.call(balsamiqViewer, data);
- });
-
- it('should instantiate Uint8Array', () => {
- expect(window.Uint8Array).toHaveBeenCalledWith(data);
- });
-
- it('should call sqljs.Database', () => {
- expect(sqljs.Database).toHaveBeenCalledWith(uint8Array);
- });
-
- it('should set .database', () => {
- expect(balsamiqViewer.database).not.toBe(null);
- });
- });
-
- describe('getPreviews', () => {
- let database;
- let thumbnails;
- let getPreviews;
-
- beforeEach(() => {
- database = {
- exec: jest.fn(),
- };
- thumbnails = [{ values: [0, 1, 2] }];
-
- balsamiqViewer = {
- database,
- };
-
- jest
- .spyOn(BalsamiqViewer, 'parsePreview')
- .mockImplementation((preview) => preview.toString());
- database.exec.mockReturnValue(thumbnails);
-
- getPreviews = BalsamiqViewer.prototype.getPreviews.call(balsamiqViewer);
- });
-
- it('should call database.exec', () => {
- expect(database.exec).toHaveBeenCalledWith('SELECT * FROM thumbnails');
- });
-
- it('should call .parsePreview for each value', () => {
- const allArgs = BalsamiqViewer.parsePreview.mock.calls;
-
- expect(allArgs.length).toBe(3);
-
- thumbnails[0].values.forEach((value, i) => {
- expect(allArgs[i][0]).toBe(value);
- });
- });
-
- it('should return an array of parsed values', () => {
- expect(getPreviews).toEqual(['0', '1', '2']);
- });
- });
-
- describe('getResource', () => {
- let database;
- let resourceID;
- let resource;
- let getResource;
-
- beforeEach(() => {
- database = {
- exec: jest.fn(),
- };
- resourceID = 4;
- resource = ['resource'];
-
- balsamiqViewer = {
- database,
- };
-
- database.exec.mockReturnValue(resource);
-
- getResource = BalsamiqViewer.prototype.getResource.call(balsamiqViewer, resourceID);
- });
-
- it('should call database.exec', () => {
- expect(database.exec).toHaveBeenCalledWith(
- `SELECT * FROM resources WHERE id = '${resourceID}'`,
- );
- });
-
- it('should return the selected resource', () => {
- expect(getResource).toBe(resource[0]);
- });
- });
-
- describe('renderPreview', () => {
- let previewElement;
- let innerHTML;
- let preview;
- let renderPreview;
-
- beforeEach(() => {
- innerHTML = '<a>innerHTML</a>';
- previewElement = {
- outerHTML: '<p>outerHTML</p>',
- classList: {
- add: jest.fn(),
- },
- };
- preview = {};
-
- balsamiqViewer = {
- renderTemplate: jest.fn(),
- };
-
- jest.spyOn(document, 'createElement').mockReturnValue(previewElement);
- balsamiqViewer.renderTemplate.mockReturnValue(innerHTML);
-
- renderPreview = BalsamiqViewer.prototype.renderPreview.call(balsamiqViewer, preview);
- });
-
- it('should call classList.add', () => {
- expect(previewElement.classList.add).toHaveBeenCalledWith('preview');
- });
-
- it('should call .renderTemplate', () => {
- expect(balsamiqViewer.renderTemplate).toHaveBeenCalledWith(preview);
- });
-
- it('should set .innerHTML', () => {
- expect(previewElement.innerHTML).toBe(innerHTML);
- });
-
- it('should return element', () => {
- expect(renderPreview).toBe(previewElement);
- });
- });
-
- describe('renderTemplate', () => {
- let preview;
- let name;
- let resource;
- let template;
- let renderTemplate;
-
- beforeEach(() => {
- preview = { resourceID: 1, image: 'image' };
- name = 'name';
- resource = 'resource';
- template = `
- <div class="card">
- <div class="card-header">name</div>
- <div class="card-body">
- <img class="img-thumbnail" src="data:image/png;base64,image"/>
- </div>
- </div>
- `;
-
- balsamiqViewer = {
- getResource: jest.fn(),
- };
-
- jest.spyOn(BalsamiqViewer, 'parseTitle').mockReturnValue(name);
- balsamiqViewer.getResource.mockReturnValue(resource);
-
- renderTemplate = BalsamiqViewer.prototype.renderTemplate.call(balsamiqViewer, preview);
- });
-
- it('should call .getResource', () => {
- expect(balsamiqViewer.getResource).toHaveBeenCalledWith(preview.resourceID);
- });
-
- it('should call .parseTitle', () => {
- expect(BalsamiqViewer.parseTitle).toHaveBeenCalledWith(resource);
- });
-
- it('should return the template string', () => {
- expect(renderTemplate.replace(/\s/g, '')).toEqual(template.replace(/\s/g, ''));
- });
- });
-
- describe('parsePreview', () => {
- let preview;
- let parsePreview;
-
- beforeEach(() => {
- preview = ['{}', '{ "id": 1 }'];
-
- jest.spyOn(JSON, 'parse');
-
- parsePreview = BalsamiqViewer.parsePreview(preview);
- });
-
- ClassSpecHelper.itShouldBeAStaticMethod(BalsamiqViewer, 'parsePreview');
-
- it('should return the parsed JSON', () => {
- expect(parsePreview).toEqual(JSON.parse('{ "id": 1 }'));
- });
- });
-
- describe('parseTitle', () => {
- let title;
- let parseTitle;
-
- beforeEach(() => {
- title = { values: [['{}', '{}', '{"name":"name"}']] };
-
- jest.spyOn(JSON, 'parse');
-
- parseTitle = BalsamiqViewer.parseTitle(title);
- });
-
- ClassSpecHelper.itShouldBeAStaticMethod(BalsamiqViewer, 'parsePreview');
-
- it('should return the name value', () => {
- expect(parseTitle).toBe('name');
- });
- });
-});
diff --git a/spec/frontend/boards/boards_util_spec.js b/spec/frontend/boards/boards_util_spec.js
index d45b6e35a45..ab3cf072357 100644
--- a/spec/frontend/boards/boards_util_spec.js
+++ b/spec/frontend/boards/boards_util_spec.js
@@ -1,6 +1,12 @@
import { formatIssueInput, filterVariables } from '~/boards/boards_util';
describe('formatIssueInput', () => {
+ const issueInput = {
+ labelIds: ['gid://gitlab/GroupLabel/5'],
+ projectPath: 'gitlab-org/gitlab-test',
+ id: 'gid://gitlab/Issue/11',
+ };
+
it('correctly merges boardConfig into the issue', () => {
const boardConfig = {
labels: [
@@ -14,12 +20,6 @@ describe('formatIssueInput', () => {
weight: 1,
};
- const issueInput = {
- labelIds: ['gid://gitlab/GroupLabel/5'],
- projectPath: 'gitlab-org/gitlab-test',
- id: 'gid://gitlab/Issue/11',
- };
-
const result = formatIssueInput(issueInput, boardConfig);
expect(result).toEqual({
projectPath: 'gitlab-org/gitlab-test',
@@ -27,8 +27,26 @@ describe('formatIssueInput', () => {
labelIds: ['gid://gitlab/GroupLabel/5', 'gid://gitlab/GroupLabel/44'],
assigneeIds: ['gid://gitlab/User/55'],
milestoneId: 'gid://gitlab/Milestone/66',
+ weight: 1,
});
});
+
+ it('does not add weight to input if weight is NONE', () => {
+ const boardConfig = {
+ weight: -2, // NO_WEIGHT
+ };
+
+ const result = formatIssueInput(issueInput, boardConfig);
+ const expected = {
+ projectPath: 'gitlab-org/gitlab-test',
+ id: 'gid://gitlab/Issue/11',
+ labelIds: ['gid://gitlab/GroupLabel/5'],
+ assigneeIds: [],
+ milestoneId: undefined,
+ };
+
+ expect(result).toEqual(expected);
+ });
});
describe('filterVariables', () => {
diff --git a/spec/frontend/boards/components/board_filtered_search_spec.js b/spec/frontend/boards/components/board_filtered_search_spec.js
index 85ba703a6ee..731578e15a3 100644
--- a/spec/frontend/boards/components/board_filtered_search_spec.js
+++ b/spec/frontend/boards/components/board_filtered_search_spec.js
@@ -124,7 +124,7 @@ describe('BoardFilteredSearch', () => {
{ type: 'milestone', value: { data: 'New Milestone', operator: '=' } },
{ type: 'type', value: { data: 'INCIDENT', operator: '=' } },
{ type: 'weight', value: { data: '2', operator: '=' } },
- { type: 'iteration', value: { data: '3341', operator: '=' } },
+ { type: 'iteration', value: { data: 'Any&3', operator: '=' } },
{ type: 'release', value: { data: 'v1.0.0', operator: '=' } },
];
jest.spyOn(urlUtility, 'updateHistory');
@@ -134,7 +134,7 @@ describe('BoardFilteredSearch', () => {
title: '',
replace: true,
url:
- 'http://test.host/?author_username=root&label_name[]=label&label_name[]=label%262&assignee_username=root&milestone_title=New%20Milestone&iteration_id=3341&types=INCIDENT&weight=2&release_tag=v1.0.0',
+ 'http://test.host/?author_username=root&label_name[]=label&label_name[]=label%262&assignee_username=root&milestone_title=New%20Milestone&iteration_id=Any&iteration_cadence_id=3&types=INCIDENT&weight=2&release_tag=v1.0.0',
});
});
diff --git a/spec/frontend/boards/components/board_form_spec.js b/spec/frontend/boards/components/board_form_spec.js
index c976ba7525b..6a659623b53 100644
--- a/spec/frontend/boards/components/board_form_spec.js
+++ b/spec/frontend/boards/components/board_form_spec.js
@@ -62,7 +62,7 @@ describe('BoardForm', () => {
};
},
provide: {
- rootPath: 'root',
+ boardBaseUrl: 'root',
},
mocks: {
$apollo: {
diff --git a/spec/frontend/boards/components/board_top_bar_spec.js b/spec/frontend/boards/components/board_top_bar_spec.js
new file mode 100644
index 00000000000..997768a0cc7
--- /dev/null
+++ b/spec/frontend/boards/components/board_top_bar_spec.js
@@ -0,0 +1,88 @@
+import { shallowMount } from '@vue/test-utils';
+import Vue from 'vue';
+import Vuex from 'vuex';
+
+import BoardTopBar from '~/boards/components/board_top_bar.vue';
+import BoardAddNewColumnTrigger from '~/boards/components/board_add_new_column_trigger.vue';
+import BoardsSelector from '~/boards/components/boards_selector.vue';
+import ConfigToggle from '~/boards/components/config_toggle.vue';
+import IssueBoardFilteredSearch from '~/boards/components/issue_board_filtered_search.vue';
+import NewBoardButton from '~/boards/components/new_board_button.vue';
+import ToggleFocus from '~/boards/components/toggle_focus.vue';
+
+describe('BoardTopBar', () => {
+ let wrapper;
+
+ Vue.use(Vuex);
+
+ const createStore = ({ mockGetters = {} } = {}) => {
+ return new Vuex.Store({
+ state: {},
+ getters: {
+ isEpicBoard: () => false,
+ ...mockGetters,
+ },
+ });
+ };
+
+ const createComponent = ({ provide = {}, mockGetters = {} } = {}) => {
+ const store = createStore({ mockGetters });
+ wrapper = shallowMount(BoardTopBar, {
+ store,
+ provide: {
+ swimlanesFeatureAvailable: false,
+ canAdminList: false,
+ isSignedIn: false,
+ fullPath: 'gitlab-org',
+ boardType: 'group',
+ releasesFetchPath: '/releases',
+ ...provide,
+ },
+ stubs: { IssueBoardFilteredSearch },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('base template', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders BoardsSelector component', () => {
+ expect(wrapper.findComponent(BoardsSelector).exists()).toBe(true);
+ });
+
+ it('renders IssueBoardFilteredSearch component', () => {
+ expect(wrapper.findComponent(IssueBoardFilteredSearch).exists()).toBe(true);
+ });
+
+ it('renders NewBoardButton component', () => {
+ expect(wrapper.findComponent(NewBoardButton).exists()).toBe(true);
+ });
+
+ it('renders ConfigToggle component', () => {
+ expect(wrapper.findComponent(ConfigToggle).exists()).toBe(true);
+ });
+
+ it('renders ToggleFocus component', () => {
+ expect(wrapper.findComponent(ToggleFocus).exists()).toBe(true);
+ });
+
+ it('does not render BoardAddNewColumnTrigger component', () => {
+ expect(wrapper.findComponent(BoardAddNewColumnTrigger).exists()).toBe(false);
+ });
+ });
+
+ describe('when user can admin list', () => {
+ beforeEach(() => {
+ createComponent({ provide: { canAdminList: true } });
+ });
+
+ it('renders BoardAddNewColumnTrigger component', () => {
+ expect(wrapper.findComponent(BoardAddNewColumnTrigger).exists()).toBe(true);
+ });
+ });
+});
diff --git a/spec/frontend/boards/components/boards_selector_spec.js b/spec/frontend/boards/components/boards_selector_spec.js
index 0c044deb78c..f60d04af4fc 100644
--- a/spec/frontend/boards/components/boards_selector_spec.js
+++ b/spec/frontend/boards/components/boards_selector_spec.js
@@ -1,5 +1,4 @@
import { GlDropdown, GlLoadingIcon, GlDropdownSectionHeader } from '@gitlab/ui';
-import { mount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import Vuex from 'vuex';
@@ -14,6 +13,7 @@ import groupRecentBoardsQuery from '~/boards/graphql/group_recent_boards.query.g
import projectRecentBoardsQuery from '~/boards/graphql/project_recent_boards.query.graphql';
import defaultStore from '~/boards/stores';
import createMockApollo from 'helpers/mock_apollo_helper';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
import {
mockGroupBoardResponse,
mockProjectBoardResponse,
@@ -60,7 +60,7 @@ describe('BoardsSelector', () => {
searchBoxInput.trigger('input');
};
- const getDropdownItems = () => wrapper.findAll('.js-dropdown-item');
+ const getDropdownItems = () => wrapper.findAllByTestId('dropdown-item');
const getDropdownHeaders = () => wrapper.findAllComponents(GlDropdownSectionHeader);
const getLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findDropdown = () => wrapper.findComponent(GlDropdown);
@@ -100,11 +100,15 @@ describe('BoardsSelector', () => {
[groupRecentBoardsQuery, groupRecentBoardsQueryHandlerSuccess],
]);
- wrapper = mount(BoardsSelector, {
+ wrapper = mountExtended(BoardsSelector, {
store,
apolloProvider: fakeApollo,
propsData: {
throttleDuration,
+ },
+ attachTo: document.body,
+ provide: {
+ fullPath: '',
boardBaseUrl: `${TEST_HOST}/board/base/url`,
hasMissingBoards: false,
canAdminBoard: true,
@@ -112,10 +116,6 @@ describe('BoardsSelector', () => {
scopedIssueBoardFeatureEnabled: true,
weights: [],
},
- attachTo: document.body,
- provide: {
- fullPath: '',
- },
});
};
diff --git a/spec/frontend/boards/components/issuable_title_spec.js b/spec/frontend/boards/components/issuable_title_spec.js
deleted file mode 100644
index 4b7f491b998..00000000000
--- a/spec/frontend/boards/components/issuable_title_spec.js
+++ /dev/null
@@ -1,33 +0,0 @@
-import { shallowMount } from '@vue/test-utils';
-import IssuableTitle from '~/boards/components/issuable_title.vue';
-
-describe('IssuableTitle', () => {
- let wrapper;
- const defaultProps = {
- title: 'One',
- refPath: 'path',
- };
- const createComponent = () => {
- wrapper = shallowMount(IssuableTitle, {
- propsData: { ...defaultProps },
- });
- };
- const findIssueContent = () => wrapper.find('[data-testid="issue-title"]');
-
- beforeEach(() => {
- createComponent();
- });
-
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
- it('renders a title of an issue in the sidebar', () => {
- expect(findIssueContent().text()).toContain('One');
- });
-
- it('renders a referencePath of an issue in the sidebar', () => {
- expect(findIssueContent().text()).toContain('path');
- });
-});
diff --git a/spec/frontend/boards/components/issue_board_filtered_search_spec.js b/spec/frontend/boards/components/issue_board_filtered_search_spec.js
index 76e8b84d8ef..e4a6a2b8b76 100644
--- a/spec/frontend/boards/components/issue_board_filtered_search_spec.js
+++ b/spec/frontend/boards/components/issue_board_filtered_search_spec.js
@@ -14,10 +14,11 @@ describe('IssueBoardFilter', () => {
const createComponent = ({ isSignedIn = false } = {}) => {
wrapper = shallowMount(IssueBoardFilteredSpec, {
- propsData: { fullPath: 'gitlab-org', boardType: 'group' },
provide: {
isSignedIn,
releasesFetchPath: '/releases',
+ fullPath: 'gitlab-org',
+ boardType: 'group',
},
});
};
diff --git a/spec/frontend/boards/components/issue_time_estimate_spec.js b/spec/frontend/boards/components/issue_time_estimate_spec.js
index 635964b6b4a..948a7a20f7f 100644
--- a/spec/frontend/boards/components/issue_time_estimate_spec.js
+++ b/spec/frontend/boards/components/issue_time_estimate_spec.js
@@ -5,6 +5,8 @@ import IssueTimeEstimate from '~/boards/components/issue_time_estimate.vue';
describe('Issue Time Estimate component', () => {
let wrapper;
+ const findIssueTimeEstimate = () => wrapper.find('[data-testid="issue-time-estimate"]');
+
afterEach(() => {
wrapper.destroy();
});
@@ -26,7 +28,7 @@ describe('Issue Time Estimate component', () => {
});
it('renders expanded time estimate in tooltip', () => {
- expect(wrapper.find('.js-issue-time-estimate').text()).toContain('2 weeks 3 days 1 minute');
+ expect(findIssueTimeEstimate().text()).toContain('2 weeks 3 days 1 minute');
});
it('prevents tooltip xss', async () => {
@@ -42,7 +44,7 @@ describe('Issue Time Estimate component', () => {
expect(alertSpy).not.toHaveBeenCalled();
expect(wrapper.find('time').text().trim()).toEqual('0m');
- expect(wrapper.find('.js-issue-time-estimate').text()).toContain('0m');
+ expect(findIssueTimeEstimate().text()).toContain('0m');
});
});
@@ -63,7 +65,7 @@ describe('Issue Time Estimate component', () => {
});
it('renders expanded time estimate in tooltip', () => {
- expect(wrapper.find('.js-issue-time-estimate').text()).toContain('104 hours 1 minute');
+ expect(findIssueTimeEstimate().text()).toContain('104 hours 1 minute');
});
});
});
diff --git a/spec/frontend/boards/components/item_count_spec.js b/spec/frontend/boards/components/item_count_spec.js
index 45980c36f1c..06cd3910fc0 100644
--- a/spec/frontend/boards/components/item_count_spec.js
+++ b/spec/frontend/boards/components/item_count_spec.js
@@ -29,7 +29,7 @@ describe('IssueCount', () => {
});
it('does not contains maxIssueCount in the template', () => {
- expect(vm.find('.js-max-issue-size').exists()).toBe(false);
+ expect(vm.find('.max-issue-size').exists()).toBe(false);
});
});
@@ -50,7 +50,7 @@ describe('IssueCount', () => {
});
it('contains maxIssueCount in the template', () => {
- expect(vm.find('.js-max-issue-size').text()).toEqual(String(maxIssueCount));
+ expect(vm.find('.max-issue-size').text()).toEqual(String(maxIssueCount));
});
it('does not have text-danger class when issueSize is less than maxIssueCount', () => {
@@ -75,7 +75,7 @@ describe('IssueCount', () => {
});
it('contains maxIssueCount in the template', () => {
- expect(vm.find('.js-max-issue-size').text()).toEqual(String(maxIssueCount));
+ expect(vm.find('.max-issue-size').text()).toEqual(String(maxIssueCount));
});
it('has text-danger class', () => {
diff --git a/spec/frontend/boards/stores/actions_spec.js b/spec/frontend/boards/stores/actions_spec.js
index ad661a31556..eacf9db191e 100644
--- a/spec/frontend/boards/stores/actions_spec.js
+++ b/spec/frontend/boards/stores/actions_spec.js
@@ -166,31 +166,29 @@ describe('setFilters', () => {
});
describe('performSearch', () => {
- it('should dispatch setFilters, fetchLists and resetIssues action', (done) => {
- testAction(
+ it('should dispatch setFilters, fetchLists and resetIssues action', () => {
+ return testAction(
actions.performSearch,
{},
{},
[],
[{ type: 'setFilters', payload: {} }, { type: 'fetchLists' }, { type: 'resetIssues' }],
- done,
);
});
});
describe('setActiveId', () => {
- it('should commit mutation SET_ACTIVE_ID', (done) => {
+ it('should commit mutation SET_ACTIVE_ID', () => {
const state = {
activeId: inactiveId,
};
- testAction(
+ return testAction(
actions.setActiveId,
{ id: 1, sidebarType: 'something' },
state,
[{ type: types.SET_ACTIVE_ID, payload: { id: 1, sidebarType: 'something' } }],
[],
- done,
);
});
});
@@ -219,10 +217,10 @@ describe('fetchLists', () => {
const formattedLists = formatBoardLists(queryResponse.data.group.board.lists);
- it('should commit mutations RECEIVE_BOARD_LISTS_SUCCESS on success', (done) => {
+ it('should commit mutations RECEIVE_BOARD_LISTS_SUCCESS on success', () => {
jest.spyOn(gqlClient, 'query').mockResolvedValue(queryResponse);
- testAction(
+ return testAction(
actions.fetchLists,
{},
state,
@@ -233,14 +231,13 @@ describe('fetchLists', () => {
},
],
[],
- done,
);
});
- it('should commit mutations RECEIVE_BOARD_LISTS_FAILURE on failure', (done) => {
+ it('should commit mutations RECEIVE_BOARD_LISTS_FAILURE on failure', () => {
jest.spyOn(gqlClient, 'query').mockResolvedValue(Promise.reject());
- testAction(
+ return testAction(
actions.fetchLists,
{},
state,
@@ -250,11 +247,10 @@ describe('fetchLists', () => {
},
],
[],
- done,
);
});
- it('dispatch createList action when backlog list does not exist and is not hidden', (done) => {
+ it('dispatch createList action when backlog list does not exist and is not hidden', () => {
queryResponse = {
data: {
group: {
@@ -269,7 +265,7 @@ describe('fetchLists', () => {
};
jest.spyOn(gqlClient, 'query').mockResolvedValue(queryResponse);
- testAction(
+ return testAction(
actions.fetchLists,
{},
state,
@@ -280,7 +276,6 @@ describe('fetchLists', () => {
},
],
[{ type: 'createList', payload: { backlog: true } }],
- done,
);
});
@@ -951,10 +946,10 @@ describe('fetchItemsForList', () => {
});
});
- it('should commit mutations REQUEST_ITEMS_FOR_LIST and RECEIVE_ITEMS_FOR_LIST_SUCCESS on success', (done) => {
+ it('should commit mutations REQUEST_ITEMS_FOR_LIST and RECEIVE_ITEMS_FOR_LIST_SUCCESS on success', () => {
jest.spyOn(gqlClient, 'query').mockResolvedValue(queryResponse);
- testAction(
+ return testAction(
actions.fetchItemsForList,
{ listId },
state,
@@ -973,14 +968,13 @@ describe('fetchItemsForList', () => {
},
],
[],
- done,
);
});
- it('should commit mutations REQUEST_ITEMS_FOR_LIST and RECEIVE_ITEMS_FOR_LIST_FAILURE on failure', (done) => {
+ it('should commit mutations REQUEST_ITEMS_FOR_LIST and RECEIVE_ITEMS_FOR_LIST_FAILURE on failure', () => {
jest.spyOn(gqlClient, 'query').mockResolvedValue(Promise.reject());
- testAction(
+ return testAction(
actions.fetchItemsForList,
{ listId },
state,
@@ -996,7 +990,6 @@ describe('fetchItemsForList', () => {
{ type: types.RECEIVE_ITEMS_FOR_LIST_FAILURE, payload: listId },
],
[],
- done,
);
});
});
@@ -1398,8 +1391,8 @@ describe('setAssignees', () => {
const node = { username: 'name' };
describe('when succeeds', () => {
- it('calls the correct mutation with the correct values', (done) => {
- testAction(
+ it('calls the correct mutation with the correct values', () => {
+ return testAction(
actions.setAssignees,
{ assignees: [node], iid: '1' },
{ commit: () => {} },
@@ -1410,7 +1403,6 @@ describe('setAssignees', () => {
},
],
[],
- done,
);
});
});
@@ -1728,7 +1720,7 @@ describe('setActiveItemSubscribed', () => {
projectPath: 'gitlab-org/gitlab-test',
};
- it('should commit subscribed status', (done) => {
+ it('should commit subscribed status', () => {
jest.spyOn(gqlClient, 'mutate').mockResolvedValue({
data: {
updateIssuableSubscription: {
@@ -1746,7 +1738,7 @@ describe('setActiveItemSubscribed', () => {
value: subscribedState,
};
- testAction(
+ return testAction(
actions.setActiveItemSubscribed,
input,
{ ...state, ...getters },
@@ -1757,7 +1749,6 @@ describe('setActiveItemSubscribed', () => {
},
],
[],
- done,
);
});
@@ -1783,7 +1774,7 @@ describe('setActiveItemTitle', () => {
projectPath: 'h/b',
};
- it('should commit title after setting the issue', (done) => {
+ it('should commit title after setting the issue', () => {
jest.spyOn(gqlClient, 'mutate').mockResolvedValue({
data: {
updateIssuableTitle: {
@@ -1801,7 +1792,7 @@ describe('setActiveItemTitle', () => {
value: testTitle,
};
- testAction(
+ return testAction(
actions.setActiveItemTitle,
input,
{ ...state, ...getters },
@@ -1812,7 +1803,6 @@ describe('setActiveItemTitle', () => {
},
],
[],
- done,
);
});
@@ -1829,14 +1819,14 @@ describe('setActiveItemConfidential', () => {
const state = { boardItems: { [mockIssue.id]: mockIssue } };
const getters = { activeBoardItem: mockIssue };
- it('set confidential value on board item', (done) => {
+ it('set confidential value on board item', () => {
const payload = {
itemId: getters.activeBoardItem.id,
prop: 'confidential',
value: true,
};
- testAction(
+ return testAction(
actions.setActiveItemConfidential,
true,
{ ...state, ...getters },
@@ -1847,7 +1837,6 @@ describe('setActiveItemConfidential', () => {
},
],
[],
- done,
);
});
});
@@ -1876,10 +1865,10 @@ describe('fetchGroupProjects', () => {
},
};
- it('should commit mutations REQUEST_GROUP_PROJECTS and RECEIVE_GROUP_PROJECTS_SUCCESS on success', (done) => {
+ it('should commit mutations REQUEST_GROUP_PROJECTS and RECEIVE_GROUP_PROJECTS_SUCCESS on success', () => {
jest.spyOn(gqlClient, 'query').mockResolvedValue(queryResponse);
- testAction(
+ return testAction(
actions.fetchGroupProjects,
{},
state,
@@ -1894,14 +1883,13 @@ describe('fetchGroupProjects', () => {
},
],
[],
- done,
);
});
- it('should commit mutations REQUEST_GROUP_PROJECTS and RECEIVE_GROUP_PROJECTS_FAILURE on failure', (done) => {
+ it('should commit mutations REQUEST_GROUP_PROJECTS and RECEIVE_GROUP_PROJECTS_FAILURE on failure', () => {
jest.spyOn(gqlClient, 'query').mockRejectedValue();
- testAction(
+ return testAction(
actions.fetchGroupProjects,
{},
state,
@@ -1915,16 +1903,15 @@ describe('fetchGroupProjects', () => {
},
],
[],
- done,
);
});
});
describe('setSelectedProject', () => {
- it('should commit mutation SET_SELECTED_PROJECT', (done) => {
+ it('should commit mutation SET_SELECTED_PROJECT', () => {
const project = mockGroupProjects[0];
- testAction(
+ return testAction(
actions.setSelectedProject,
project,
{},
@@ -1935,7 +1922,6 @@ describe('setSelectedProject', () => {
},
],
[],
- done,
);
});
});
diff --git a/spec/frontend/captcha/apollo_captcha_link_spec.js b/spec/frontend/captcha/apollo_captcha_link_spec.js
index eab52344d1f..cd32e63d00c 100644
--- a/spec/frontend/captcha/apollo_captcha_link_spec.js
+++ b/spec/frontend/captcha/apollo_captcha_link_spec.js
@@ -95,70 +95,82 @@ describe('apolloCaptchaLink', () => {
return { operationName: 'operation', variables: {}, setContext: mockContext };
}
- it('successful responses are passed through', (done) => {
+ it('successful responses are passed through', () => {
setupLink(SUCCESS_RESPONSE);
- link.request(mockOperation()).subscribe((result) => {
- expect(result).toEqual(SUCCESS_RESPONSE);
- expect(mockLinkImplementation).toHaveBeenCalledTimes(1);
- expect(waitForCaptchaToBeSolved).not.toHaveBeenCalled();
- done();
+
+ return new Promise((resolve) => {
+ link.request(mockOperation()).subscribe((result) => {
+ expect(result).toEqual(SUCCESS_RESPONSE);
+ expect(mockLinkImplementation).toHaveBeenCalledTimes(1);
+ expect(waitForCaptchaToBeSolved).not.toHaveBeenCalled();
+ resolve();
+ });
});
});
- it('non-spam related errors are passed through', (done) => {
+ it('non-spam related errors are passed through', () => {
setupLink(NON_CAPTCHA_ERROR_RESPONSE);
- link.request(mockOperation()).subscribe((result) => {
- expect(result).toEqual(NON_CAPTCHA_ERROR_RESPONSE);
- expect(mockLinkImplementation).toHaveBeenCalledTimes(1);
- expect(mockContext).not.toHaveBeenCalled();
- expect(waitForCaptchaToBeSolved).not.toHaveBeenCalled();
- done();
+
+ return new Promise((resolve) => {
+ link.request(mockOperation()).subscribe((result) => {
+ expect(result).toEqual(NON_CAPTCHA_ERROR_RESPONSE);
+ expect(mockLinkImplementation).toHaveBeenCalledTimes(1);
+ expect(mockContext).not.toHaveBeenCalled();
+ expect(waitForCaptchaToBeSolved).not.toHaveBeenCalled();
+ resolve();
+ });
});
});
- it('unresolvable spam errors are passed through', (done) => {
+ it('unresolvable spam errors are passed through', () => {
setupLink(SPAM_ERROR_RESPONSE);
- link.request(mockOperation()).subscribe((result) => {
- expect(result).toEqual(SPAM_ERROR_RESPONSE);
- expect(mockLinkImplementation).toHaveBeenCalledTimes(1);
- expect(mockContext).not.toHaveBeenCalled();
- expect(waitForCaptchaToBeSolved).not.toHaveBeenCalled();
- done();
+ return new Promise((resolve) => {
+ link.request(mockOperation()).subscribe((result) => {
+ expect(result).toEqual(SPAM_ERROR_RESPONSE);
+ expect(mockLinkImplementation).toHaveBeenCalledTimes(1);
+ expect(mockContext).not.toHaveBeenCalled();
+ expect(waitForCaptchaToBeSolved).not.toHaveBeenCalled();
+ resolve();
+ });
});
});
describe('resolvable spam errors', () => {
- it('re-submits request with spam headers if the captcha modal was solved correctly', (done) => {
+ it('re-submits request with spam headers if the captcha modal was solved correctly', () => {
waitForCaptchaToBeSolved.mockResolvedValue(CAPTCHA_RESPONSE);
setupLink(CAPTCHA_ERROR_RESPONSE, SUCCESS_RESPONSE);
- link.request(mockOperation()).subscribe((result) => {
- expect(result).toEqual(SUCCESS_RESPONSE);
- expect(waitForCaptchaToBeSolved).toHaveBeenCalledWith(CAPTCHA_SITE_KEY);
- expect(mockContext).toHaveBeenCalledWith({
- headers: {
- 'X-GitLab-Captcha-Response': CAPTCHA_RESPONSE,
- 'X-GitLab-Spam-Log-Id': SPAM_LOG_ID,
- },
+ return new Promise((resolve) => {
+ link.request(mockOperation()).subscribe((result) => {
+ expect(result).toEqual(SUCCESS_RESPONSE);
+ expect(waitForCaptchaToBeSolved).toHaveBeenCalledWith(CAPTCHA_SITE_KEY);
+ expect(mockContext).toHaveBeenCalledWith({
+ headers: {
+ 'X-GitLab-Captcha-Response': CAPTCHA_RESPONSE,
+ 'X-GitLab-Spam-Log-Id': SPAM_LOG_ID,
+ },
+ });
+ expect(mockLinkImplementation).toHaveBeenCalledTimes(2);
+ resolve();
});
- expect(mockLinkImplementation).toHaveBeenCalledTimes(2);
- done();
});
});
- it('throws error if the captcha modal was not solved correctly', (done) => {
+ it('throws error if the captcha modal was not solved correctly', () => {
const error = new UnsolvedCaptchaError();
waitForCaptchaToBeSolved.mockRejectedValue(error);
setupLink(CAPTCHA_ERROR_RESPONSE, SUCCESS_RESPONSE);
- link.request(mockOperation()).subscribe({
- next: done.catch,
- error: (result) => {
- expect(result).toEqual(error);
- expect(waitForCaptchaToBeSolved).toHaveBeenCalledWith(CAPTCHA_SITE_KEY);
- expect(mockContext).not.toHaveBeenCalled();
- expect(mockLinkImplementation).toHaveBeenCalledTimes(1);
- done();
- },
+ return new Promise((resolve, reject) => {
+ link.request(mockOperation()).subscribe({
+ next: reject,
+ error: (result) => {
+ expect(result).toEqual(error);
+ expect(waitForCaptchaToBeSolved).toHaveBeenCalledWith(CAPTCHA_SITE_KEY);
+ expect(mockContext).not.toHaveBeenCalled();
+ expect(mockLinkImplementation).toHaveBeenCalledTimes(1);
+ resolve();
+ },
+ });
});
});
});
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 085ab1c0c30..2fedbbecd64 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
@@ -36,7 +36,7 @@ describe('Ci variable modal', () => {
const findAddorUpdateButton = () =>
findModal()
.findAll(GlButton)
- .wrappers.find((button) => button.props('variant') === 'success');
+ .wrappers.find((button) => button.props('variant') === 'confirm');
const deleteVariableButton = () =>
findModal()
.findAll(GlButton)
diff --git a/spec/frontend/ci_variable_list/store/actions_spec.js b/spec/frontend/ci_variable_list/store/actions_spec.js
index 426e6cae8fb..eb31fcd3ef4 100644
--- a/spec/frontend/ci_variable_list/store/actions_spec.js
+++ b/spec/frontend/ci_variable_list/store/actions_spec.js
@@ -86,10 +86,10 @@ describe('CI variable list store actions', () => {
});
describe('deleteVariable', () => {
- it('dispatch correct actions on successful deleted variable', (done) => {
+ it('dispatch correct actions on successful deleted variable', () => {
mock.onPatch(state.endpoint).reply(200);
- testAction(
+ return testAction(
actions.deleteVariable,
{},
state,
@@ -99,16 +99,13 @@ describe('CI variable list store actions', () => {
{ type: 'receiveDeleteVariableSuccess' },
{ type: 'fetchVariables' },
],
- () => {
- done();
- },
);
});
- it('should show flash error and set error in state on delete failure', (done) => {
+ it('should show flash error and set error in state on delete failure', async () => {
mock.onPatch(state.endpoint).reply(500, '');
- testAction(
+ await testAction(
actions.deleteVariable,
{},
state,
@@ -120,19 +117,16 @@ describe('CI variable list store actions', () => {
payload: payloadError,
},
],
- () => {
- expect(createFlash).toHaveBeenCalled();
- done();
- },
);
+ expect(createFlash).toHaveBeenCalled();
});
});
describe('updateVariable', () => {
- it('dispatch correct actions on successful updated variable', (done) => {
+ it('dispatch correct actions on successful updated variable', () => {
mock.onPatch(state.endpoint).reply(200);
- testAction(
+ return testAction(
actions.updateVariable,
{},
state,
@@ -142,16 +136,13 @@ describe('CI variable list store actions', () => {
{ type: 'receiveUpdateVariableSuccess' },
{ type: 'fetchVariables' },
],
- () => {
- done();
- },
);
});
- it('should show flash error and set error in state on update failure', (done) => {
+ it('should show flash error and set error in state on update failure', async () => {
mock.onPatch(state.endpoint).reply(500, '');
- testAction(
+ await testAction(
actions.updateVariable,
mockVariable,
state,
@@ -163,19 +154,16 @@ describe('CI variable list store actions', () => {
payload: payloadError,
},
],
- () => {
- expect(createFlash).toHaveBeenCalled();
- done();
- },
);
+ expect(createFlash).toHaveBeenCalled();
});
});
describe('addVariable', () => {
- it('dispatch correct actions on successful added variable', (done) => {
+ it('dispatch correct actions on successful added variable', () => {
mock.onPatch(state.endpoint).reply(200);
- testAction(
+ return testAction(
actions.addVariable,
{},
state,
@@ -185,16 +173,13 @@ describe('CI variable list store actions', () => {
{ type: 'receiveAddVariableSuccess' },
{ type: 'fetchVariables' },
],
- () => {
- done();
- },
);
});
- it('should show flash error and set error in state on add failure', (done) => {
+ it('should show flash error and set error in state on add failure', async () => {
mock.onPatch(state.endpoint).reply(500, '');
- testAction(
+ await testAction(
actions.addVariable,
{},
state,
@@ -206,19 +191,16 @@ describe('CI variable list store actions', () => {
payload: payloadError,
},
],
- () => {
- expect(createFlash).toHaveBeenCalled();
- done();
- },
);
+ expect(createFlash).toHaveBeenCalled();
});
});
describe('fetchVariables', () => {
- it('dispatch correct actions on fetchVariables', (done) => {
+ it('dispatch correct actions on fetchVariables', () => {
mock.onGet(state.endpoint).reply(200, { variables: mockData.mockVariables });
- testAction(
+ return testAction(
actions.fetchVariables,
{},
state,
@@ -230,29 +212,24 @@ describe('CI variable list store actions', () => {
payload: prepareDataForDisplay(mockData.mockVariables),
},
],
- () => {
- done();
- },
);
});
- it('should show flash error and set error in state on fetch variables failure', (done) => {
+ it('should show flash error and set error in state on fetch variables failure', async () => {
mock.onGet(state.endpoint).reply(500);
- testAction(actions.fetchVariables, {}, state, [], [{ type: 'requestVariables' }], () => {
- expect(createFlash).toHaveBeenCalledWith({
- message: 'There was an error fetching the variables.',
- });
- done();
+ await testAction(actions.fetchVariables, {}, state, [], [{ type: 'requestVariables' }]);
+ expect(createFlash).toHaveBeenCalledWith({
+ message: 'There was an error fetching the variables.',
});
});
});
describe('fetchEnvironments', () => {
- it('dispatch correct actions on fetchEnvironments', (done) => {
+ it('dispatch correct actions on fetchEnvironments', () => {
Api.environments = jest.fn().mockResolvedValue({ data: mockData.mockEnvironments });
- testAction(
+ return testAction(
actions.fetchEnvironments,
{},
state,
@@ -264,28 +241,17 @@ describe('CI variable list store actions', () => {
payload: prepareEnvironments(mockData.mockEnvironments),
},
],
- () => {
- done();
- },
);
});
- it('should show flash error and set error in state on fetch environments failure', (done) => {
+ it('should show flash error and set error in state on fetch environments failure', async () => {
Api.environments = jest.fn().mockRejectedValue();
- testAction(
- actions.fetchEnvironments,
- {},
- state,
- [],
- [{ type: 'requestEnvironments' }],
- () => {
- expect(createFlash).toHaveBeenCalledWith({
- message: 'There was an error fetching the environments information.',
- });
- done();
- },
- );
+ await testAction(actions.fetchEnvironments, {}, state, [], [{ type: 'requestEnvironments' }]);
+
+ expect(createFlash).toHaveBeenCalledWith({
+ message: 'There was an error fetching the environments information.',
+ });
});
});
diff --git a/spec/frontend/clusters_list/components/agent_empty_state_spec.js b/spec/frontend/clusters_list/components/agent_empty_state_spec.js
index ed2a0d0b97b..22775aa6603 100644
--- a/spec/frontend/clusters_list/components/agent_empty_state_spec.js
+++ b/spec/frontend/clusters_list/components/agent_empty_state_spec.js
@@ -1,8 +1,6 @@
-import { GlEmptyState, GlSprintf, GlLink, GlButton } from '@gitlab/ui';
+import { GlEmptyState, GlSprintf, GlLink } from '@gitlab/ui';
import AgentEmptyState from '~/clusters_list/components/agent_empty_state.vue';
-import { INSTALL_AGENT_MODAL_ID } from '~/clusters_list/constants';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import { helpPagePath } from '~/helpers/help_page_helper';
const emptyStateImage = '/path/to/image';
@@ -15,16 +13,12 @@ describe('AgentEmptyStateComponent', () => {
};
const findInstallDocsLink = () => wrapper.findComponent(GlLink);
- const findIntegrationButton = () => wrapper.findComponent(GlButton);
const findEmptyState = () => wrapper.findComponent(GlEmptyState);
beforeEach(() => {
wrapper = shallowMountExtended(AgentEmptyState, {
provide: provideData,
- directives: {
- GlModalDirective: createMockDirective(),
- },
- stubs: { GlEmptyState, GlSprintf },
+ stubs: { GlSprintf },
});
});
@@ -38,17 +32,7 @@ describe('AgentEmptyStateComponent', () => {
expect(findEmptyState().exists()).toBe(true);
});
- it('renders button for the agent registration', () => {
- expect(findIntegrationButton().exists()).toBe(true);
- });
-
it('renders correct href attributes for the docs link', () => {
expect(findInstallDocsLink().attributes('href')).toBe(installDocsUrl);
});
-
- it('renders correct modal id for the agent registration modal', () => {
- const binding = getBinding(findIntegrationButton().element, 'gl-modal-directive');
-
- expect(binding.value).toBe(INSTALL_AGENT_MODAL_ID);
- });
});
diff --git a/spec/frontend/clusters_list/components/agent_table_spec.js b/spec/frontend/clusters_list/components/agent_table_spec.js
index db723622a51..a466a35428a 100644
--- a/spec/frontend/clusters_list/components/agent_table_spec.js
+++ b/spec/frontend/clusters_list/components/agent_table_spec.js
@@ -9,7 +9,7 @@ import timeagoMixin from '~/vue_shared/mixins/timeago';
import { clusterAgents, connectedTimeNow, connectedTimeInactive } from './mock_data';
const defaultConfigHelpUrl =
- '/help/user/clusters/agent/install/index#create-an-agent-without-configuration-file';
+ '/help/user/clusters/agent/install/index#create-an-agent-configuration-file';
const provideData = {
gitlabVersion: '14.8',
diff --git a/spec/frontend/clusters_list/components/agent_token_spec.js b/spec/frontend/clusters_list/components/agent_token_spec.js
index a80c8ffaad4..7f6ec2eb3a2 100644
--- a/spec/frontend/clusters_list/components/agent_token_spec.js
+++ b/spec/frontend/clusters_list/components/agent_token_spec.js
@@ -53,7 +53,7 @@ describe('InstallAgentModal', () => {
});
it('shows agent token as an input value', () => {
- expect(findInput().props('value')).toBe('agent-token');
+ expect(findInput().props('value')).toBe(agentToken);
});
it('renders a copy button', () => {
@@ -65,12 +65,12 @@ describe('InstallAgentModal', () => {
});
it('shows warning alert', () => {
- expect(findAlert().props('title')).toBe(I18N_AGENT_TOKEN.tokenSingleUseWarningTitle);
+ expect(findAlert().text()).toBe(I18N_AGENT_TOKEN.tokenSingleUseWarningTitle);
});
it('shows code block with agent installation command', () => {
- expect(findCodeBlock().props('code')).toContain('--agent-token=agent-token');
- expect(findCodeBlock().props('code')).toContain('--kas-address=kas.example.com');
+ expect(findCodeBlock().props('code')).toContain(`--set config.token=${agentToken}`);
+ expect(findCodeBlock().props('code')).toContain(`--set config.kasAddress=${kasAddress}`);
});
});
});
diff --git a/spec/frontend/clusters_list/components/agents_spec.js b/spec/frontend/clusters_list/components/agents_spec.js
index 3cfa4b92bc0..92cfff7d490 100644
--- a/spec/frontend/clusters_list/components/agents_spec.js
+++ b/spec/frontend/clusters_list/components/agents_spec.js
@@ -308,7 +308,7 @@ describe('Agents', () => {
});
it('displays an alert message', () => {
- expect(findAlert().text()).toBe('An error occurred while loading your Agents');
+ expect(findAlert().text()).toBe('An error occurred while loading your agents');
});
});
diff --git a/spec/frontend/clusters_list/components/available_agents_dropwdown_spec.js b/spec/frontend/clusters_list/components/available_agents_dropwdown_spec.js
index eca2b1f5cb1..197735d3c77 100644
--- a/spec/frontend/clusters_list/components/available_agents_dropwdown_spec.js
+++ b/spec/frontend/clusters_list/components/available_agents_dropwdown_spec.js
@@ -1,5 +1,6 @@
import { GlDropdown, GlDropdownItem, GlSearchBoxByType } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { ENTER_KEY } from '~/lib/utils/keys';
import AvailableAgentsDropdown from '~/clusters_list/components/available_agents_dropdown.vue';
import { I18N_AVAILABLE_AGENTS_DROPDOWN } from '~/clusters_list/constants';
@@ -18,6 +19,7 @@ describe('AvailableAgentsDropdown', () => {
propsData,
stubs: { GlDropdown },
});
+ wrapper.vm.$refs.dropdown.hide = jest.fn();
};
afterEach(() => {
@@ -96,6 +98,25 @@ describe('AvailableAgentsDropdown', () => {
expect(findDropdown().props('text')).toBe('new-agent');
});
});
+
+ describe('click enter to register new agent without configuration', () => {
+ beforeEach(async () => {
+ await findSearchInput().vm.$emit('input', 'new-agent');
+ await findSearchInput().vm.$emit('keydown', new KeyboardEvent({ key: ENTER_KEY }));
+ });
+
+ it('emits agentSelected with the name of the clicked agent', () => {
+ expect(wrapper.emitted('agentSelected')).toEqual([['new-agent']]);
+ });
+
+ it('marks the clicked item as selected', () => {
+ expect(findDropdown().props('text')).toBe('new-agent');
+ });
+
+ it('closes the dropdown', () => {
+ expect(wrapper.vm.$refs.dropdown.hide).toHaveBeenCalledTimes(1);
+ });
+ });
});
describe('registration in progress', () => {
diff --git a/spec/frontend/clusters_list/components/clusters_actions_spec.js b/spec/frontend/clusters_list/components/clusters_actions_spec.js
index 312df12ab5f..21dcc66c639 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, GlButton } from '@gitlab/ui';
+import { 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,14 @@ import { INSTALL_AGENT_MODAL_ID, CLUSTERS_ACTIONS } from '~/clusters_list/consta
describe('ClustersActionsComponent', () => {
let wrapper;
- const newClusterPath = 'path/to/create/cluster';
+ 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,
displayClusterAgents: true,
certificateBasedClustersEnabled: true,
@@ -20,12 +22,13 @@ describe('ClustersActionsComponent', () => {
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');
- const findConnectNewAgentLink = () => wrapper.findByTestId('connect-new-agent-link');
- const findConnectWithAgentButton = () => wrapper.findComponent(GlButton);
const createWrapper = (provideData = {}) => {
wrapper = shallowMountExtended(ClustersActions, {
@@ -35,7 +38,6 @@ describe('ClustersActionsComponent', () => {
},
directives: {
GlModalDirective: createMockDirective(),
- GlTooltip: createMockDirective(),
},
});
};
@@ -49,12 +51,15 @@ describe('ClustersActionsComponent', () => {
});
describe('when the certificate based clusters are enabled', () => {
it('renders actions menu', () => {
- expect(findDropdown().props('text')).toBe(CLUSTERS_ACTIONS.actionsButton);
+ expect(findDropdown().exists()).toBe(true);
});
- it('renders correct href attributes for the links', () => {
- expect(findNewClusterLink().attributes('href')).toBe(newClusterPath);
- expect(findConnectClusterLink().attributes('href')).toBe(addClusterPath);
+ it('shows split button in dropdown', () => {
+ expect(findDropdown().props('split')).toBe(true);
+ });
+
+ it("doesn't show the tooltip", () => {
+ expect(findTooltip().exists()).toBe(false);
});
describe('when user cannot add clusters', () => {
@@ -67,8 +72,7 @@ describe('ClustersActionsComponent', () => {
});
it('shows tooltip explaining why dropdown is disabled', () => {
- const tooltip = getBinding(findDropdown().element, 'gl-tooltip');
- expect(tooltip.value).toBe(CLUSTERS_ACTIONS.dropdownDisabledHint);
+ expect(findTooltip().attributes('title')).toBe(CLUSTERS_ACTIONS.dropdownDisabledHint);
});
it('does not bind split dropdown button', () => {
@@ -79,33 +83,36 @@ describe('ClustersActionsComponent', () => {
});
describe('when on project level', () => {
- it('renders a dropdown with 3 actions items', () => {
- expect(findDropdownItemIds()).toEqual([
- 'connect-new-agent-link',
- 'new-cluster-link',
- 'connect-cluster-link',
- ]);
+ it(`displays default action as ${CLUSTERS_ACTIONS.connectWithAgent}`, () => {
+ expect(findDropdown().props('text')).toBe(CLUSTERS_ACTIONS.connectWithAgent);
});
- it('renders correct modal id for the agent link', () => {
- const binding = getBinding(findConnectNewAgentLink().element, 'gl-modal-directive');
+ it('renders correct modal id for the default action', () => {
+ const binding = getBinding(findDropdown().element, 'gl-modal-directive');
expect(binding.value).toBe(INSTALL_AGENT_MODAL_ID);
});
- it('shows tooltip', () => {
- const tooltip = getBinding(findDropdown().element, 'gl-tooltip');
- expect(tooltip.value).toBe(CLUSTERS_ACTIONS.connectWithAgent);
+ it('renders a dropdown with 3 actions items', () => {
+ expect(findDropdownItemIds()).toEqual([
+ 'create-cluster-link',
+ 'new-cluster-link',
+ 'connect-cluster-link',
+ ]);
});
- it('shows split button in dropdown', () => {
- expect(findDropdown().props('split')).toBe(true);
+ it('renders correct texts for the dropdown items', () => {
+ expect(findDropdownItemTexts()).toEqual([
+ CLUSTERS_ACTIONS.createCluster,
+ CLUSTERS_ACTIONS.createClusterCertificate,
+ CLUSTERS_ACTIONS.connectClusterCertificate,
+ ]);
});
- it('binds split button with modal id', () => {
- const binding = getBinding(findDropdown().element, 'gl-modal-directive');
-
- expect(binding.value).toBe(INSTALL_AGENT_MODAL_ID);
+ 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);
});
});
@@ -114,17 +121,20 @@ describe('ClustersActionsComponent', () => {
createWrapper({ displayClusterAgents: false });
});
- it('renders a dropdown with 2 actions items', () => {
- expect(findDropdownItemIds()).toEqual(['new-cluster-link', 'connect-cluster-link']);
+ it(`displays default action as ${CLUSTERS_ACTIONS.connectClusterDeprecated}`, () => {
+ expect(findDropdown().props('text')).toBe(CLUSTERS_ACTIONS.connectClusterDeprecated);
});
- it('shows tooltip', () => {
- const tooltip = getBinding(findDropdown().element, 'gl-tooltip');
- expect(tooltip.value).toBe(CLUSTERS_ACTIONS.connectExistingCluster);
+ it('renders a dropdown with 1 action item', () => {
+ expect(findDropdownItemIds()).toEqual(['new-cluster-link']);
});
- it('does not show split button in dropdown', () => {
- expect(findDropdown().props('split')).toBe(false);
+ it('renders correct text for the dropdown item', () => {
+ expect(findDropdownItemTexts()).toEqual([CLUSTERS_ACTIONS.createClusterDeprecated]);
+ });
+
+ it('renders correct href attributes for the links', () => {
+ expect(findNewClusterLink().attributes('href')).toBe(newClusterPath);
});
it('does not bind dropdown button to modal', () => {
@@ -140,17 +150,26 @@ describe('ClustersActionsComponent', () => {
createWrapper({ certificateBasedClustersEnabled: false });
});
- it('it does not show the the dropdown', () => {
- expect(findDropdown().exists()).toBe(false);
+ it(`displays default action as ${CLUSTERS_ACTIONS.connectCluster}`, () => {
+ expect(findDropdown().props('text')).toBe(CLUSTERS_ACTIONS.connectCluster);
});
- it('shows the connect with agent button', () => {
- expect(findConnectWithAgentButton().props()).toMatchObject({
- disabled: !defaultProvide.canAddCluster,
- category: 'primary',
- variant: 'confirm',
- });
- expect(findConnectWithAgentButton().text()).toBe(CLUSTERS_ACTIONS.connectWithAgent);
+ it('renders correct modal id for the default action', () => {
+ const binding = getBinding(findDropdown().element, 'gl-modal-directive');
+
+ expect(binding.value).toBe(INSTALL_AGENT_MODAL_ID);
+ });
+
+ it('renders a dropdown with 1 action item', () => {
+ expect(findDropdownItemIds()).toEqual(['create-cluster-link']);
+ });
+
+ it('renders correct text for the dropdown item', () => {
+ expect(findDropdownItemTexts()).toEqual([CLUSTERS_ACTIONS.createCluster]);
+ });
+
+ it('renders correct href attributes for the links', () => {
+ expect(findNewClusterDocsLink().attributes('href')).toBe(newClusterDocsPath);
});
});
});
diff --git a/spec/frontend/clusters_list/components/clusters_empty_state_spec.js b/spec/frontend/clusters_list/components/clusters_empty_state_spec.js
index fe2189296a6..2c3a224f3c8 100644
--- a/spec/frontend/clusters_list/components/clusters_empty_state_spec.js
+++ b/spec/frontend/clusters_list/components/clusters_empty_state_spec.js
@@ -1,10 +1,8 @@
-import { GlEmptyState, GlButton } from '@gitlab/ui';
+import { GlEmptyState } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import ClustersEmptyState from '~/clusters_list/components/clusters_empty_state.vue';
-import ClusterStore from '~/clusters_list/store';
const clustersEmptyStateImage = 'path/to/svg';
-const addClusterPath = '/path/to/connect/cluster';
const emptyStateHelpText = 'empty state text';
describe('ClustersEmptyStateComponent', () => {
@@ -12,52 +10,28 @@ describe('ClustersEmptyStateComponent', () => {
const defaultProvideData = {
clustersEmptyStateImage,
- addClusterPath,
};
- const findButton = () => wrapper.findComponent(GlButton);
const findEmptyStateText = () => wrapper.findByTestId('clusters-empty-state-text');
- const createWrapper = ({
- provideData = { emptyStateHelpText: null },
- isChildComponent = false,
- canAddCluster = true,
- } = {}) => {
+ const createWrapper = ({ provideData = { emptyStateHelpText: null } } = {}) => {
wrapper = shallowMountExtended(ClustersEmptyState, {
- store: ClusterStore({ canAddCluster }),
- propsData: { isChildComponent },
provide: { ...defaultProvideData, ...provideData },
stubs: { GlEmptyState },
});
};
- beforeEach(() => {
- createWrapper();
- });
-
afterEach(() => {
wrapper.destroy();
});
- describe('when the component is loaded independently', () => {
- it('should render the action button', () => {
- expect(findButton().exists()).toBe(true);
- });
- });
-
describe('when the help text is not provided', () => {
- it('should not render the empty state text', () => {
- expect(findEmptyStateText().exists()).toBe(false);
- });
- });
-
- describe('when the component is loaded as a child component', () => {
beforeEach(() => {
- createWrapper({ isChildComponent: true });
+ createWrapper();
});
- it('should not render the action button', () => {
- expect(findButton().exists()).toBe(false);
+ it('should not render the empty state text', () => {
+ expect(findEmptyStateText().exists()).toBe(false);
});
});
@@ -70,13 +44,4 @@ describe('ClustersEmptyStateComponent', () => {
expect(findEmptyStateText().text()).toBe(emptyStateHelpText);
});
});
-
- describe('when the user cannot add clusters', () => {
- beforeEach(() => {
- createWrapper({ canAddCluster: false });
- });
- it('should disable the button', () => {
- expect(findButton().props('disabled')).toBe(true);
- });
- });
});
diff --git a/spec/frontend/clusters_list/components/clusters_view_all_spec.js b/spec/frontend/clusters_list/components/clusters_view_all_spec.js
index 2c1e3d909cc..b4eb9242003 100644
--- a/spec/frontend/clusters_list/components/clusters_view_all_spec.js
+++ b/spec/frontend/clusters_list/components/clusters_view_all_spec.js
@@ -1,24 +1,21 @@
-import { GlCard, GlLoadingIcon, GlButton, GlSprintf, GlBadge } from '@gitlab/ui';
+import { GlCard, GlLoadingIcon, GlSprintf, GlBadge } from '@gitlab/ui';
import Vue from 'vue';
import Vuex from 'vuex';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import ClustersViewAll from '~/clusters_list/components/clusters_view_all.vue';
import Agents from '~/clusters_list/components/agents.vue';
import Clusters from '~/clusters_list/components/clusters.vue';
-import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import {
AGENT,
CERTIFICATE_BASED,
AGENT_CARD_INFO,
CERTIFICATE_BASED_CARD_INFO,
MAX_CLUSTERS_LIST,
- INSTALL_AGENT_MODAL_ID,
} from '~/clusters_list/constants';
import { sprintf } from '~/locale';
Vue.use(Vuex);
-const addClusterPath = '/path/to/add/cluster';
const defaultBranchName = 'default-branch';
describe('ClustersViewAllComponent', () => {
@@ -32,11 +29,6 @@ describe('ClustersViewAllComponent', () => {
defaultBranchName,
};
- const defaultProvide = {
- addClusterPath,
- canAddCluster: true,
- };
-
const entryData = {
loadingClusters: false,
totalClusters: 0,
@@ -46,37 +38,20 @@ describe('ClustersViewAllComponent', () => {
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findAgentsComponent = () => wrapper.findComponent(Agents);
const findClustersComponent = () => wrapper.findComponent(Clusters);
- const findInstallAgentButtonTooltip = () => wrapper.findByTestId('install-agent-button-tooltip');
- const findConnectExistingClusterButtonTooltip = () =>
- wrapper.findByTestId('connect-existing-cluster-button-tooltip');
const findCardsContainer = () => wrapper.findByTestId('clusters-cards-container');
const findAgentCardTitle = () => wrapper.findByTestId('agent-card-title');
const findRecommendedBadge = () => wrapper.findComponent(GlBadge);
const findClustersCardTitle = () => wrapper.findByTestId('clusters-card-title');
- const findFooterButton = (line) => findCards().at(line).findComponent(GlButton);
- const getTooltipText = (el) => {
- const binding = getBinding(el, 'gl-tooltip');
-
- return binding.value;
- };
const createStore = (initialState) =>
new Vuex.Store({
state: initialState,
});
- const createWrapper = ({ initialState = entryData, provideData } = {}) => {
+ const createWrapper = ({ initialState = entryData } = {}) => {
wrapper = shallowMountExtended(ClustersViewAll, {
store: createStore(initialState),
propsData,
- provide: {
- ...defaultProvide,
- ...provideData,
- },
- directives: {
- GlModalDirective: createMockDirective(),
- GlTooltip: createMockDirective(),
- },
stubs: { GlCard, GlSprintf },
});
};
@@ -138,25 +113,10 @@ describe('ClustersViewAllComponent', () => {
expect(findAgentsComponent().props('defaultBranchName')).toBe(defaultBranchName);
});
- it('should show install new Agent button in the footer', () => {
- expect(findFooterButton(0).exists()).toBe(true);
- expect(findFooterButton(0).props('disabled')).toBe(false);
- });
-
- it('does not show tooltip for install new Agent button', () => {
- expect(getTooltipText(findInstallAgentButtonTooltip().element)).toBe('');
- });
-
describe('when there are no agents', () => {
it('should show the empty title', () => {
expect(findAgentCardTitle().text()).toBe(AGENT_CARD_INFO.emptyTitle);
});
-
- it('should render correct modal id for the agent link', () => {
- const binding = getBinding(findFooterButton(0).element, 'gl-modal-directive');
-
- expect(binding.value).toBe(INSTALL_AGENT_MODAL_ID);
- });
});
describe('when the agents are present', () => {
@@ -191,22 +151,6 @@ describe('ClustersViewAllComponent', () => {
});
});
});
-
- describe('when the user cannot add clusters', () => {
- beforeEach(() => {
- createWrapper({ provideData: { canAddCluster: false } });
- });
-
- it('should disable the button', () => {
- expect(findFooterButton(0).props('disabled')).toBe(true);
- });
-
- it('should show a tooltip explaining why the button is disabled', () => {
- expect(getTooltipText(findInstallAgentButtonTooltip().element)).toBe(
- AGENT_CARD_INFO.installAgentDisabledHint,
- );
- });
- });
});
describe('clusters tab', () => {
@@ -214,43 +158,10 @@ describe('ClustersViewAllComponent', () => {
expect(findClustersComponent().props('limit')).toBe(MAX_CLUSTERS_LIST);
});
- it('should pass the is-child-component prop', () => {
- expect(findClustersComponent().props('isChildComponent')).toBe(true);
- });
-
describe('when there are no clusters', () => {
it('should show the empty title', () => {
expect(findClustersCardTitle().text()).toBe(CERTIFICATE_BASED_CARD_INFO.emptyTitle);
});
-
- it('should show install new cluster button in the footer', () => {
- expect(findFooterButton(1).exists()).toBe(true);
- expect(findFooterButton(1).props('disabled')).toBe(false);
- });
-
- it('should render correct href for the button in the footer', () => {
- expect(findFooterButton(1).attributes('href')).toBe(addClusterPath);
- });
-
- it('does not show tooltip for install new cluster button', () => {
- expect(getTooltipText(findConnectExistingClusterButtonTooltip().element)).toBe('');
- });
- });
-
- describe('when the user cannot add clusters', () => {
- beforeEach(() => {
- createWrapper({ provideData: { canAddCluster: false } });
- });
-
- it('should disable the button', () => {
- expect(findFooterButton(1).props('disabled')).toBe(true);
- });
-
- it('should show a tooltip explaining why the button is disabled', () => {
- expect(getTooltipText(findConnectExistingClusterButtonTooltip().element)).toBe(
- CERTIFICATE_BASED_CARD_INFO.connectExistingClusterDisabledHint,
- );
- });
});
describe('when the clusters are present', () => {
diff --git a/spec/frontend/clusters_list/mocks/apollo.js b/spec/frontend/clusters_list/mocks/apollo.js
index b0f2978a230..3467b4c665c 100644
--- a/spec/frontend/clusters_list/mocks/apollo.js
+++ b/spec/frontend/clusters_list/mocks/apollo.js
@@ -1,4 +1,5 @@
const agent = {
+ __typename: 'ClusterAgent',
id: 'agent-id',
name: 'agent-name',
webPath: 'agent-webPath',
diff --git a/spec/frontend/clusters_list/store/actions_spec.js b/spec/frontend/clusters_list/store/actions_spec.js
index f4b69053e14..7663f329b3f 100644
--- a/spec/frontend/clusters_list/store/actions_spec.js
+++ b/spec/frontend/clusters_list/store/actions_spec.js
@@ -24,14 +24,12 @@ describe('Clusters store actions', () => {
captureException.mockRestore();
});
- it('should report sentry error', (done) => {
+ it('should report sentry error', async () => {
const sentryError = new Error('New Sentry Error');
const tag = 'sentryErrorTag';
- testAction(actions.reportSentryError, { error: sentryError, tag }, {}, [], [], () => {
- expect(captureException).toHaveBeenCalledWith(sentryError);
- done();
- });
+ await testAction(actions.reportSentryError, { error: sentryError, tag }, {}, [], []);
+ expect(captureException).toHaveBeenCalledWith(sentryError);
});
});
@@ -62,10 +60,10 @@ describe('Clusters store actions', () => {
afterEach(() => mock.restore());
- it('should commit SET_CLUSTERS_DATA with received response', (done) => {
+ it('should commit SET_CLUSTERS_DATA with received response', () => {
mock.onGet().reply(200, apiData, headers);
- testAction(
+ return testAction(
actions.fetchClusters,
{ endpoint: apiData.endpoint },
{},
@@ -75,14 +73,13 @@ describe('Clusters store actions', () => {
{ type: types.SET_LOADING_CLUSTERS, payload: false },
],
[],
- () => done(),
);
});
- it('should show flash on API error', (done) => {
+ it('should show flash on API error', async () => {
mock.onGet().reply(400, 'Not Found');
- testAction(
+ await testAction(
actions.fetchClusters,
{ endpoint: apiData.endpoint },
{},
@@ -100,13 +97,10 @@ describe('Clusters store actions', () => {
},
},
],
- () => {
- expect(createFlash).toHaveBeenCalledWith({
- message: expect.stringMatching('error'),
- });
- done();
- },
);
+ expect(createFlash).toHaveBeenCalledWith({
+ message: expect.stringMatching('error'),
+ });
});
describe('multiple api requests', () => {
@@ -128,8 +122,8 @@ describe('Clusters store actions', () => {
pollStop.mockRestore();
});
- it('should stop polling after MAX Requests', (done) => {
- testAction(
+ it('should stop polling after MAX Requests', async () => {
+ await testAction(
actions.fetchClusters,
{ endpoint: apiData.endpoint },
{},
@@ -139,47 +133,43 @@ describe('Clusters store actions', () => {
{ type: types.SET_LOADING_CLUSTERS, payload: false },
],
[],
- () => {
- expect(pollRequest).toHaveBeenCalledTimes(1);
+ );
+ expect(pollRequest).toHaveBeenCalledTimes(1);
+ expect(pollStop).toHaveBeenCalledTimes(0);
+ jest.advanceTimersByTime(pollInterval);
+
+ return waitForPromises()
+ .then(() => {
+ expect(pollRequest).toHaveBeenCalledTimes(2);
expect(pollStop).toHaveBeenCalledTimes(0);
jest.advanceTimersByTime(pollInterval);
-
- waitForPromises()
- .then(() => {
- expect(pollRequest).toHaveBeenCalledTimes(2);
- expect(pollStop).toHaveBeenCalledTimes(0);
- jest.advanceTimersByTime(pollInterval);
- })
- .then(() => waitForPromises())
- .then(() => {
- expect(pollRequest).toHaveBeenCalledTimes(MAX_REQUESTS);
- expect(pollStop).toHaveBeenCalledTimes(0);
- jest.advanceTimersByTime(pollInterval);
- })
- .then(() => waitForPromises())
- .then(() => {
- expect(pollRequest).toHaveBeenCalledTimes(MAX_REQUESTS + 1);
- // Stops poll once it exceeds the MAX_REQUESTS limit
- expect(pollStop).toHaveBeenCalledTimes(1);
- jest.advanceTimersByTime(pollInterval);
- })
- .then(() => waitForPromises())
- .then(() => {
- // Additional poll requests are not made once pollStop is called
- expect(pollRequest).toHaveBeenCalledTimes(MAX_REQUESTS + 1);
- expect(pollStop).toHaveBeenCalledTimes(1);
- })
- .then(done)
- .catch(done.fail);
- },
- );
+ })
+ .then(() => waitForPromises())
+ .then(() => {
+ expect(pollRequest).toHaveBeenCalledTimes(MAX_REQUESTS);
+ expect(pollStop).toHaveBeenCalledTimes(0);
+ jest.advanceTimersByTime(pollInterval);
+ })
+ .then(() => waitForPromises())
+ .then(() => {
+ expect(pollRequest).toHaveBeenCalledTimes(MAX_REQUESTS + 1);
+ // Stops poll once it exceeds the MAX_REQUESTS limit
+ expect(pollStop).toHaveBeenCalledTimes(1);
+ jest.advanceTimersByTime(pollInterval);
+ })
+ .then(() => waitForPromises())
+ .then(() => {
+ // Additional poll requests are not made once pollStop is called
+ expect(pollRequest).toHaveBeenCalledTimes(MAX_REQUESTS + 1);
+ expect(pollStop).toHaveBeenCalledTimes(1);
+ });
});
- it('should stop polling and report to Sentry when data is invalid', (done) => {
+ it('should stop polling and report to Sentry when data is invalid', async () => {
const badApiResponse = { clusters: {} };
mock.onGet().reply(200, badApiResponse, pollHeaders);
- testAction(
+ await testAction(
actions.fetchClusters,
{ endpoint: apiData.endpoint },
{},
@@ -202,12 +192,9 @@ describe('Clusters store actions', () => {
},
},
],
- () => {
- expect(pollRequest).toHaveBeenCalledTimes(1);
- expect(pollStop).toHaveBeenCalledTimes(1);
- done();
- },
);
+ expect(pollRequest).toHaveBeenCalledTimes(1);
+ expect(pollStop).toHaveBeenCalledTimes(1);
});
});
});
diff --git a/spec/frontend/code_navigation/components/app_spec.js b/spec/frontend/code_navigation/components/app_spec.js
index 0d7c0360e9b..f2f97092c5a 100644
--- a/spec/frontend/code_navigation/components/app_spec.js
+++ b/spec/frontend/code_navigation/components/app_spec.js
@@ -38,12 +38,17 @@ describe('Code navigation app component', () => {
const codeNavigationPath = 'code/nav/path.js';
const path = 'blob/path.js';
const definitionPathPrefix = 'path/prefix';
+ const wrapTextNodes = true;
- factory({}, { codeNavigationPath, blobPath: path, pathPrefix: definitionPathPrefix });
+ factory(
+ {},
+ { codeNavigationPath, blobPath: path, pathPrefix: definitionPathPrefix, wrapTextNodes },
+ );
expect(setInitialData).toHaveBeenCalledWith(expect.anything(), {
blobs: [{ codeNavigationPath, path }],
definitionPathPrefix,
+ wrapTextNodes,
});
});
diff --git a/spec/frontend/code_navigation/store/actions_spec.js b/spec/frontend/code_navigation/store/actions_spec.js
index 73f935deeca..c26416aca94 100644
--- a/spec/frontend/code_navigation/store/actions_spec.js
+++ b/spec/frontend/code_navigation/store/actions_spec.js
@@ -7,15 +7,16 @@ import axios from '~/lib/utils/axios_utils';
jest.mock('~/code_navigation/utils');
describe('Code navigation actions', () => {
+ const wrapTextNodes = true;
+
describe('setInitialData', () => {
- it('commits SET_INITIAL_DATA', (done) => {
- testAction(
+ it('commits SET_INITIAL_DATA', () => {
+ return testAction(
actions.setInitialData,
- { projectPath: 'test' },
+ { projectPath: 'test', wrapTextNodes },
{},
- [{ type: 'SET_INITIAL_DATA', payload: { projectPath: 'test' } }],
+ [{ type: 'SET_INITIAL_DATA', payload: { projectPath: 'test', wrapTextNodes } }],
[],
- done,
);
});
});
@@ -30,7 +31,7 @@ describe('Code navigation actions', () => {
const codeNavigationPath =
'gitlab-org/gitlab-shell/-/jobs/1114/artifacts/raw/lsif/cmd/check/main.go.json';
- const state = { blobs: [{ path: 'index.js', codeNavigationPath }] };
+ const state = { blobs: [{ path: 'index.js', codeNavigationPath }], wrapTextNodes };
beforeEach(() => {
window.gon = { api_version: '1' };
@@ -57,8 +58,8 @@ describe('Code navigation actions', () => {
]);
});
- it('commits REQUEST_DATA_SUCCESS with normalized data', (done) => {
- testAction(
+ it('commits REQUEST_DATA_SUCCESS with normalized data', () => {
+ return testAction(
actions.fetchData,
null,
state,
@@ -80,12 +81,11 @@ describe('Code navigation actions', () => {
},
],
[],
- done,
);
});
- it('calls addInteractionClass with data', (done) => {
- testAction(
+ it('calls addInteractionClass with data', () => {
+ return testAction(
actions.fetchData,
null,
state,
@@ -107,16 +107,17 @@ describe('Code navigation actions', () => {
},
],
[],
- )
- .then(() => {
- expect(addInteractionClass).toHaveBeenCalledWith('index.js', {
+ ).then(() => {
+ expect(addInteractionClass).toHaveBeenCalledWith({
+ path: 'index.js',
+ d: {
start_line: 0,
start_char: 0,
hover: { value: '123' },
- });
- })
- .then(done)
- .catch(done.fail);
+ },
+ wrapTextNodes,
+ });
+ });
});
});
@@ -125,14 +126,13 @@ describe('Code navigation actions', () => {
mock.onGet(codeNavigationPath).replyOnce(500);
});
- it('dispatches requestDataError', (done) => {
- testAction(
+ it('dispatches requestDataError', () => {
+ return testAction(
actions.fetchData,
null,
state,
[{ type: 'REQUEST_DATA' }],
[{ type: 'requestDataError' }],
- done,
);
});
});
@@ -144,14 +144,19 @@ describe('Code navigation actions', () => {
data: {
'index.js': { '0:0': 'test', '1:1': 'console.log' },
},
+ wrapTextNodes,
};
actions.showBlobInteractionZones({ state }, 'index.js');
expect(addInteractionClass).toHaveBeenCalled();
expect(addInteractionClass.mock.calls.length).toBe(2);
- expect(addInteractionClass.mock.calls[0]).toEqual(['index.js', 'test']);
- expect(addInteractionClass.mock.calls[1]).toEqual(['index.js', 'console.log']);
+ expect(addInteractionClass.mock.calls[0]).toEqual([
+ { path: 'index.js', d: 'test', wrapTextNodes },
+ ]);
+ expect(addInteractionClass.mock.calls[1]).toEqual([
+ { path: 'index.js', d: 'console.log', wrapTextNodes },
+ ]);
});
it('does not call addInteractionClass when no data exists', () => {
@@ -175,20 +180,20 @@ describe('Code navigation actions', () => {
target = document.querySelector('.js-test');
});
- it('returns early when no data exists', (done) => {
- testAction(actions.showDefinition, { target }, {}, [], [], done);
+ it('returns early when no data exists', () => {
+ return testAction(actions.showDefinition, { target }, {}, [], []);
});
- it('commits SET_CURRENT_DEFINITION when target is not code navitation element', (done) => {
- testAction(actions.showDefinition, { target }, { data: {} }, [], [], done);
+ it('commits SET_CURRENT_DEFINITION when target is not code navitation element', () => {
+ return testAction(actions.showDefinition, { target }, { data: {} }, [], []);
});
- it('commits SET_CURRENT_DEFINITION with LSIF data', (done) => {
+ it('commits SET_CURRENT_DEFINITION with LSIF data', () => {
target.classList.add('js-code-navigation');
target.setAttribute('data-line-index', '0');
target.setAttribute('data-char-index', '0');
- testAction(
+ return testAction(
actions.showDefinition,
{ target },
{ data: { 'index.js': { '0:0': { hover: 'test' } } } },
@@ -203,7 +208,6 @@ describe('Code navigation actions', () => {
},
],
[],
- done,
);
});
diff --git a/spec/frontend/code_navigation/store/mutations_spec.js b/spec/frontend/code_navigation/store/mutations_spec.js
index cb10729f4b6..b2f1b3bddfd 100644
--- a/spec/frontend/code_navigation/store/mutations_spec.js
+++ b/spec/frontend/code_navigation/store/mutations_spec.js
@@ -13,10 +13,12 @@ describe('Code navigation mutations', () => {
mutations.SET_INITIAL_DATA(state, {
blobs: ['test'],
definitionPathPrefix: 'https://test.com/blob/main',
+ wrapTextNodes: true,
});
expect(state.blobs).toEqual(['test']);
expect(state.definitionPathPrefix).toBe('https://test.com/blob/main');
+ expect(state.wrapTextNodes).toBe(true);
});
});
diff --git a/spec/frontend/code_navigation/utils/index_spec.js b/spec/frontend/code_navigation/utils/index_spec.js
index 6a01249d2a3..682c8bce8c5 100644
--- a/spec/frontend/code_navigation/utils/index_spec.js
+++ b/spec/frontend/code_navigation/utils/index_spec.js
@@ -45,14 +45,42 @@ describe('addInteractionClass', () => {
${0} | ${0} | ${0}
${0} | ${8} | ${2}
${1} | ${0} | ${0}
+ ${1} | ${0} | ${0}
`(
'it sets code navigation attributes for line $line and character $char',
({ line, char, index }) => {
- addInteractionClass('index.js', { start_line: line, start_char: char });
+ addInteractionClass({ path: 'index.js', d: { start_line: line, start_char: char } });
expect(document.querySelectorAll(`#LC${line + 1} span`)[index].classList).toContain(
'js-code-navigation',
);
},
);
+
+ describe('wrapTextNodes', () => {
+ beforeEach(() => {
+ setFixtures(
+ '<div data-path="index.js"><div class="blob-content"><div id="LC1" class="line"> Text </div></div></div>',
+ );
+ });
+
+ const params = { path: 'index.js', d: { start_line: 0, start_char: 0 } };
+ const findAllSpans = () => document.querySelectorAll('#LC1 span');
+
+ it('does not wrap text nodes by default', () => {
+ addInteractionClass(params);
+ const spans = findAllSpans();
+ expect(spans.length).toBe(0);
+ });
+
+ it('wraps text nodes if wrapTextNodes is true', () => {
+ addInteractionClass({ ...params, wrapTextNodes: true });
+ const spans = findAllSpans();
+
+ expect(spans.length).toBe(3);
+ expect(spans[0].textContent).toBe(' ');
+ expect(spans[1].textContent).toBe('Text');
+ expect(spans[2].textContent).toBe(' ');
+ });
+ });
});
diff --git a/spec/frontend/commit/commit_box_pipeline_mini_graph_spec.js b/spec/frontend/commit/commit_box_pipeline_mini_graph_spec.js
index 1a2e188e7ae..b1c8ba48475 100644
--- a/spec/frontend/commit/commit_box_pipeline_mini_graph_spec.js
+++ b/spec/frontend/commit/commit_box_pipeline_mini_graph_spec.js
@@ -1,7 +1,18 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
import { shallowMount } from '@vue/test-utils';
+import createMockApollo from 'helpers/mock_apollo_helper';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import createFlash from '~/flash';
import CommitBoxPipelineMiniGraph from '~/projects/commit_box/info/components/commit_box_pipeline_mini_graph.vue';
-import { mockStages } from './mock_data';
+import getLinkedPipelinesQuery from '~/projects/commit_box/info/graphql/queries/get_linked_pipelines.query.graphql';
+import getPipelineStagesQuery from '~/projects/commit_box/info/graphql/queries/get_pipeline_stages.query.graphql';
+import { mockPipelineStagesQueryResponse, mockStages } from './mock_data';
+
+jest.mock('~/flash');
+
+Vue.use(VueApollo);
describe('Commit box pipeline mini graph', () => {
let wrapper;
@@ -10,34 +21,36 @@ describe('Commit box pipeline mini graph', () => {
const findUpstream = () => wrapper.findByTestId('commit-box-mini-graph-upstream');
const findDownstream = () => wrapper.findByTestId('commit-box-mini-graph-downstream');
- const createComponent = () => {
+ const stagesHandler = jest.fn().mockResolvedValue(mockPipelineStagesQueryResponse);
+
+ const createComponent = ({ props = {} } = {}) => {
+ const handlers = [
+ [getLinkedPipelinesQuery, {}],
+ [getPipelineStagesQuery, stagesHandler],
+ ];
+
wrapper = extendedWrapper(
shallowMount(CommitBoxPipelineMiniGraph, {
propsData: {
stages: mockStages,
+ ...props,
},
- mocks: {
- $apollo: {
- queries: {
- pipeline: {
- loading: false,
- },
- },
- },
- },
+ apolloProvider: createMockApollo(handlers),
}),
);
- };
- beforeEach(() => {
- createComponent();
- });
+ return waitForPromises();
+ };
afterEach(() => {
wrapper.destroy();
});
describe('linked pipelines', () => {
+ beforeEach(async () => {
+ await createComponent();
+ });
+
it('should display the mini pipeine graph', () => {
expect(findMiniGraph().exists()).toBe(true);
});
@@ -47,4 +60,18 @@ describe('Commit box pipeline mini graph', () => {
expect(findDownstream().exists()).toBe(false);
});
});
+
+ describe('when data is mismatched', () => {
+ beforeEach(async () => {
+ await createComponent({ props: { stages: [] } });
+ });
+
+ it('calls create flash with expected arguments', () => {
+ expect(createFlash).toHaveBeenCalledWith({
+ message: 'There was a problem handling the pipeline data.',
+ captureError: true,
+ error: new Error('Rest stages and graphQl stages must be the same length'),
+ });
+ });
+ });
});
diff --git a/spec/frontend/commit/components/commit_box_pipeline_status_spec.js b/spec/frontend/commit/components/commit_box_pipeline_status_spec.js
new file mode 100644
index 00000000000..db7b7b45397
--- /dev/null
+++ b/spec/frontend/commit/components/commit_box_pipeline_status_spec.js
@@ -0,0 +1,150 @@
+import { GlLoadingIcon, GlLink } 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 CiIcon from '~/vue_shared/components/ci_icon.vue';
+import CommitBoxPipelineStatus from '~/projects/commit_box/info/components/commit_box_pipeline_status.vue';
+import {
+ COMMIT_BOX_POLL_INTERVAL,
+ PIPELINE_STATUS_FETCH_ERROR,
+} from '~/projects/commit_box/info/constants';
+import getLatestPipelineStatusQuery from '~/projects/commit_box/info/graphql/queries/get_latest_pipeline_status.query.graphql';
+import * as graphQlUtils from '~/pipelines/components/graph/utils';
+import { mockPipelineStatusResponse } from '../mock_data';
+
+const mockProvide = {
+ fullPath: 'root/ci-project',
+ iid: '46',
+ graphqlResourceEtag: '/api/graphql:pipelines/id/320',
+};
+
+Vue.use(VueApollo);
+
+jest.mock('~/flash');
+
+describe('Commit box pipeline status', () => {
+ let wrapper;
+
+ const statusSuccessHandler = jest.fn().mockResolvedValue(mockPipelineStatusResponse);
+ const failedHandler = jest.fn().mockRejectedValue(new Error('GraphQL error'));
+
+ const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
+ const findStatusIcon = () => wrapper.findComponent(CiIcon);
+ const findPipelineLink = () => wrapper.findComponent(GlLink);
+
+ const advanceToNextFetch = () => {
+ jest.advanceTimersByTime(COMMIT_BOX_POLL_INTERVAL);
+ };
+
+ const createMockApolloProvider = (handler) => {
+ const requestHandlers = [[getLatestPipelineStatusQuery, handler]];
+
+ return createMockApollo(requestHandlers);
+ };
+
+ const createComponent = (handler = statusSuccessHandler) => {
+ wrapper = shallowMount(CommitBoxPipelineStatus, {
+ provide: {
+ ...mockProvide,
+ },
+ apolloProvider: createMockApolloProvider(handler),
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('loading state', () => {
+ it('should display loading state when loading', () => {
+ createComponent();
+
+ expect(findLoadingIcon().exists()).toBe(true);
+ expect(findStatusIcon().exists()).toBe(false);
+ });
+ });
+
+ describe('loaded state', () => {
+ beforeEach(async () => {
+ createComponent();
+
+ await waitForPromises();
+ });
+
+ it('should display pipeline status after the query is resolved successfully', async () => {
+ expect(findStatusIcon().exists()).toBe(true);
+
+ expect(findLoadingIcon().exists()).toBe(false);
+ expect(createFlash).toHaveBeenCalledTimes(0);
+ });
+
+ it('should link to the latest pipeline', () => {
+ const {
+ data: {
+ project: {
+ pipeline: {
+ detailedStatus: { detailsPath },
+ },
+ },
+ },
+ } = mockPipelineStatusResponse;
+
+ expect(findPipelineLink().attributes('href')).toBe(detailsPath);
+ });
+ });
+
+ describe('error state', () => {
+ it('createFlash should show if there is an error fetching the pipeline status', async () => {
+ createComponent(failedHandler);
+
+ await waitForPromises();
+
+ expect(createFlash).toHaveBeenCalledWith({
+ message: PIPELINE_STATUS_FETCH_ERROR,
+ });
+ });
+ });
+
+ describe('polling', () => {
+ it('polling interval is set for pipeline stages', () => {
+ createComponent();
+
+ const expectedInterval = wrapper.vm.$apollo.queries.pipelineStatus.options.pollInterval;
+
+ expect(expectedInterval).toBe(COMMIT_BOX_POLL_INTERVAL);
+ });
+
+ it('polls for pipeline status', async () => {
+ createComponent();
+
+ await waitForPromises();
+
+ expect(statusSuccessHandler).toHaveBeenCalledTimes(1);
+
+ advanceToNextFetch();
+ await waitForPromises();
+
+ expect(statusSuccessHandler).toHaveBeenCalledTimes(2);
+
+ advanceToNextFetch();
+ await waitForPromises();
+
+ expect(statusSuccessHandler).toHaveBeenCalledTimes(3);
+ });
+
+ it('toggles pipelineStatus polling with visibility check', async () => {
+ jest.spyOn(graphQlUtils, 'toggleQueryPollingByVisibility');
+
+ createComponent();
+
+ await waitForPromises();
+
+ expect(graphQlUtils.toggleQueryPollingByVisibility).toHaveBeenCalledWith(
+ wrapper.vm.$apollo.queries.pipelineStatus,
+ );
+ });
+ });
+});
diff --git a/spec/frontend/commit/mock_data.js b/spec/frontend/commit/mock_data.js
index ef018a4fbd7..8db162c07c2 100644
--- a/spec/frontend/commit/mock_data.js
+++ b/spec/frontend/commit/mock_data.js
@@ -115,3 +115,49 @@ export const mockStages = [
dropdown_path: '/root/ci-project/-/pipelines/611/stage.json?stage=qa',
},
];
+
+export const mockPipelineStagesQueryResponse = {
+ data: {
+ project: {
+ id: 'gid://gitlab/Project/20',
+ pipeline: {
+ id: 'gid://gitlab/Ci::Pipeline/320',
+ stages: {
+ nodes: [
+ {
+ __typename: 'CiStage',
+ id: 'gid://gitlab/Ci::Stage/409',
+ name: 'build',
+ detailedStatus: {
+ id: 'success-409-409',
+ group: 'success',
+ icon: 'status_success',
+ __typename: 'DetailedStatus',
+ },
+ },
+ ],
+ },
+ },
+ },
+ },
+};
+
+export const mockPipelineStatusResponse = {
+ data: {
+ project: {
+ id: 'gid://gitlab/Project/20',
+ pipeline: {
+ id: 'gid://gitlab/Ci::Pipeline/320',
+ detailedStatus: {
+ id: 'pending-320-320',
+ detailsPath: '/root/ci-project/-/pipelines/320',
+ icon: 'status_pending',
+ group: 'pending',
+ __typename: 'DetailedStatus',
+ },
+ __typename: 'Pipeline',
+ },
+ __typename: 'Project',
+ },
+ },
+};
diff --git a/spec/frontend/commit/pipelines/pipelines_table_spec.js b/spec/frontend/commit/pipelines/pipelines_table_spec.js
index 203a4d23160..9b01af1e585 100644
--- a/spec/frontend/commit/pipelines/pipelines_table_spec.js
+++ b/spec/frontend/commit/pipelines/pipelines_table_spec.js
@@ -120,18 +120,20 @@ describe('Pipelines table in Commits and Merge requests', () => {
});
describe('pipeline badge counts', () => {
- it('should receive update-pipelines-count event', (done) => {
+ it('should receive update-pipelines-count event', () => {
const element = document.createElement('div');
document.body.appendChild(element);
- element.addEventListener('update-pipelines-count', (event) => {
- expect(event.detail.pipelineCount).toEqual(10);
- done();
- });
+ return new Promise((resolve) => {
+ element.addEventListener('update-pipelines-count', (event) => {
+ expect(event.detail.pipelineCount).toEqual(10);
+ resolve();
+ });
- createComponent();
+ createComponent();
- element.appendChild(wrapper.vm.$el);
+ element.appendChild(wrapper.vm.$el);
+ });
});
});
});
diff --git a/spec/frontend/commit/pipelines/utils_spec.js b/spec/frontend/commit/pipelines/utils_spec.js
new file mode 100644
index 00000000000..472e35a6eb3
--- /dev/null
+++ b/spec/frontend/commit/pipelines/utils_spec.js
@@ -0,0 +1,59 @@
+import { formatStages } from '~/projects/commit_box/info/utils';
+
+const graphqlStage = [
+ {
+ __typename: 'CiStage',
+ name: 'deploy',
+ detailedStatus: {
+ __typename: 'DetailedStatus',
+ icon: 'status_success',
+ group: 'success',
+ id: 'success-409-409',
+ },
+ },
+];
+
+const restStage = [
+ {
+ name: 'deploy',
+ title: 'deploy: passed',
+ status: {
+ icon: 'status_success',
+ text: 'passed',
+ label: 'passed',
+ group: 'success',
+ tooltip: 'passed',
+ has_details: true,
+ details_path: '/root/ci-project/-/pipelines/318#deploy',
+ illustration: null,
+ favicon:
+ '/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png',
+ },
+ path: '/root/ci-project/-/pipelines/318#deploy',
+ dropdown_path: '/root/ci-project/-/pipelines/318/stage.json?stage=deploy',
+ },
+];
+
+describe('Utils', () => {
+ it('combines REST and GraphQL stages correctly for component', () => {
+ expect(formatStages(graphqlStage, restStage)).toEqual([
+ {
+ dropdown_path: '/root/ci-project/-/pipelines/318/stage.json?stage=deploy',
+ name: 'deploy',
+ status: {
+ __typename: 'DetailedStatus',
+ group: 'success',
+ icon: 'status_success',
+ id: 'success-409-409',
+ },
+ title: 'deploy: passed',
+ },
+ ]);
+ });
+
+ it('throws an error if arrays are not the same length', () => {
+ expect(() => {
+ formatStages(graphqlStage, []);
+ }).toThrow('Rest stages and graphQl stages must be the same length');
+ });
+});
diff --git a/spec/frontend/commits_spec.js b/spec/frontend/commits_spec.js
index 8189ebe6e55..a049a6997f0 100644
--- a/spec/frontend/commits_spec.js
+++ b/spec/frontend/commits_spec.js
@@ -70,29 +70,17 @@ describe('Commits List', () => {
mock.restore();
});
- it('should save the last search string', (done) => {
+ it('should save the last search string', async () => {
commitsList.searchField.val('GitLab');
- commitsList
- .filterResults()
- .then(() => {
- expect(ajaxSpy).toHaveBeenCalled();
- expect(commitsList.lastSearch).toEqual('GitLab');
-
- done();
- })
- .catch(done.fail);
+ await commitsList.filterResults();
+ expect(ajaxSpy).toHaveBeenCalled();
+ expect(commitsList.lastSearch).toEqual('GitLab');
});
- it('should not make ajax call if the input does not change', (done) => {
- commitsList
- .filterResults()
- .then(() => {
- expect(ajaxSpy).not.toHaveBeenCalled();
- expect(commitsList.lastSearch).toEqual('');
-
- done();
- })
- .catch(done.fail);
+ it('should not make ajax call if the input does not change', async () => {
+ await commitsList.filterResults();
+ expect(ajaxSpy).not.toHaveBeenCalled();
+ expect(commitsList.lastSearch).toEqual('');
});
});
});
diff --git a/spec/frontend/confidential_merge_request/components/__snapshots__/project_form_group_spec.js.snap b/spec/frontend/confidential_merge_request/components/__snapshots__/project_form_group_spec.js.snap
index c2fa6556847..d9f161b47b1 100644
--- a/spec/frontend/confidential_merge_request/components/__snapshots__/project_form_group_spec.js.snap
+++ b/spec/frontend/confidential_merge_request/components/__snapshots__/project_form_group_spec.js.snap
@@ -12,7 +12,7 @@ exports[`Confidential merge request project form group component renders empty s
<!---->
<p
- class="text-muted mt-1 mb-0"
+ class="gl-text-gray-600 gl-mt-1 gl-mb-0"
>
No forks are available to you.
@@ -27,7 +27,7 @@ exports[`Confidential merge request project form group component renders empty s
</a>
and set the fork's visibility to private.
<gl-link-stub
- class="w-auto p-0 d-inline-block text-primary bg-transparent"
+ class="gl-w-auto gl-p-0 gl-display-inline-block gl-bg-transparent"
href="/help"
target="_blank"
>
@@ -62,13 +62,13 @@ exports[`Confidential merge request project form group component renders fork dr
/>
<p
- class="text-muted mt-1 mb-0"
+ class="gl-text-gray-600 gl-mt-1 gl-mb-0"
>
To protect this issue's confidentiality, a private fork of this project was selected.
<gl-link-stub
- class="w-auto p-0 d-inline-block text-primary bg-transparent"
+ class="gl-w-auto gl-p-0 gl-display-inline-block gl-bg-transparent"
href="/help"
target="_blank"
>
diff --git a/spec/frontend/content_editor/components/code_block_bubble_menu_spec.js b/spec/frontend/content_editor/components/code_block_bubble_menu_spec.js
new file mode 100644
index 00000000000..074c311495f
--- /dev/null
+++ b/spec/frontend/content_editor/components/code_block_bubble_menu_spec.js
@@ -0,0 +1,142 @@
+import { BubbleMenu } from '@tiptap/vue-2';
+import { GlButton, 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 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';
+
+describe('content_editor/components/code_block_bubble_menu', () => {
+ let wrapper;
+ let tiptapEditor;
+ let bubbleMenu;
+ let eventHub;
+
+ const buildEditor = () => {
+ tiptapEditor = createTestEditor({ extensions: [CodeBlockHighlight] });
+ eventHub = eventHubFactory();
+ };
+
+ const buildWrapper = () => {
+ wrapper = mountExtended(CodeBlockBubbleMenu, {
+ provide: {
+ tiptapEditor,
+ eventHub,
+ },
+ });
+ };
+
+ const findDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
+ const findDropdownItemsData = () =>
+ findDropdownItems().wrappers.map((x) => ({
+ text: x.text(),
+ visible: x.isVisible(),
+ checked: x.props('isChecked'),
+ }));
+
+ beforeEach(() => {
+ buildEditor();
+ buildWrapper();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('renders bubble menu component', async () => {
+ tiptapEditor.commands.insertContent('<pre>test</pre>');
+ bubbleMenu = wrapper.findComponent(BubbleMenu);
+
+ await emitEditorEvent({ event: 'transaction', tiptapEditor });
+
+ expect(bubbleMenu.props('editor')).toBe(tiptapEditor);
+ expect(bubbleMenu.classes()).toEqual(['gl-shadow', 'gl-rounded-base']);
+ });
+
+ it('selects plaintext language by default', async () => {
+ tiptapEditor.commands.insertContent('<pre>test</pre>');
+ bubbleMenu = wrapper.findComponent(BubbleMenu);
+
+ await emitEditorEvent({ event: 'transaction', tiptapEditor });
+
+ expect(wrapper.findComponent(GlDropdown).props('text')).toBe('Plain text');
+ });
+
+ it('selects appropriate language based on the code block', async () => {
+ tiptapEditor.commands.insertContent('<pre lang="javascript">var a = 2;</pre>');
+ bubbleMenu = wrapper.findComponent(BubbleMenu);
+
+ await emitEditorEvent({ event: 'transaction', tiptapEditor });
+
+ expect(wrapper.findComponent(GlDropdown).props('text')).toBe('Javascript');
+ });
+
+ it("selects Custom (syntax) if the language doesn't exist in the list", async () => {
+ tiptapEditor.commands.insertContent('<pre lang="nomnoml">test</pre>');
+ bubbleMenu = wrapper.findComponent(BubbleMenu);
+
+ await emitEditorEvent({ event: 'transaction', tiptapEditor });
+
+ 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>');
+
+ await wrapper.findComponent(GlButton).vm.$emit('click');
+
+ expect(tiptapEditor.getText()).toBe('');
+ });
+
+ describe('when opened and search is changed', () => {
+ beforeEach(async () => {
+ tiptapEditor.commands.insertContent('<pre lang="javascript">var a = 2;</pre>');
+
+ wrapper.findComponent(GlSearchBoxByType).vm.$emit('input', 'js');
+
+ await Vue.nextTick();
+ });
+
+ it('shows dropdown items', () => {
+ expect(findDropdownItemsData()).toEqual([
+ { text: 'Javascript', visible: true, checked: true },
+ { text: 'Java', visible: true, checked: false },
+ { text: 'Javascript', visible: false, checked: false },
+ { text: 'JSON', visible: true, checked: false },
+ ]);
+ });
+
+ describe('when dropdown item is clicked', () => {
+ beforeEach(async () => {
+ jest.spyOn(codeBlockLanguageLoader, 'loadLanguages').mockResolvedValue();
+
+ findDropdownItems().at(1).vm.$emit('click');
+
+ await Vue.nextTick();
+ });
+
+ it('loads language', () => {
+ expect(codeBlockLanguageLoader.loadLanguages).toHaveBeenCalledWith(['java']);
+ });
+
+ it('sets code block', () => {
+ expect(tiptapEditor.getJSON()).toMatchObject({
+ content: [
+ {
+ type: 'codeBlock',
+ attrs: {
+ language: 'java',
+ },
+ },
+ ],
+ });
+ });
+
+ it('updates selected dropdown', () => {
+ expect(wrapper.findComponent(GlDropdown).props('text')).toBe('Java');
+ });
+ });
+ });
+});
diff --git a/spec/frontend/content_editor/components/formatting_bubble_menu_spec.js b/spec/frontend/content_editor/components/formatting_bubble_menu_spec.js
index e44a7fa4ddb..192ddee78c6 100644
--- a/spec/frontend/content_editor/components/formatting_bubble_menu_spec.js
+++ b/spec/frontend/content_editor/components/formatting_bubble_menu_spec.js
@@ -9,7 +9,7 @@ import {
} from '~/content_editor/constants';
import { createTestEditor } from '../test_utils';
-describe('content_editor/components/top_toolbar', () => {
+describe('content_editor/components/formatting_bubble_menu', () => {
let wrapper;
let trackingSpy;
let tiptapEditor;
diff --git a/spec/frontend/content_editor/components/wrappers/image_spec.js b/spec/frontend/content_editor/components/wrappers/media_spec.js
index 7b057f9cabc..3e95e2f3914 100644
--- a/spec/frontend/content_editor/components/wrappers/image_spec.js
+++ b/spec/frontend/content_editor/components/wrappers/media_spec.js
@@ -1,21 +1,24 @@
import { GlLoadingIcon } from '@gitlab/ui';
import { NodeViewWrapper } from '@tiptap/vue-2';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import ImageWrapper from '~/content_editor/components/wrappers/image.vue';
+import MediaWrapper from '~/content_editor/components/wrappers/media.vue';
-describe('content/components/wrappers/image', () => {
+describe('content/components/wrappers/media', () => {
let wrapper;
const createWrapper = async (nodeAttrs = {}) => {
- wrapper = shallowMountExtended(ImageWrapper, {
+ wrapper = shallowMountExtended(MediaWrapper, {
propsData: {
node: {
attrs: nodeAttrs,
+ type: {
+ name: 'image',
+ },
},
},
});
};
- const findImage = () => wrapper.findByTestId('image');
+ const findMedia = () => wrapper.findByTestId('media');
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
afterEach(() => {
@@ -33,7 +36,7 @@ describe('content/components/wrappers/image', () => {
createWrapper({ src });
- expect(findImage().attributes().src).toBe(src);
+ expect(findMedia().attributes().src).toBe(src);
});
describe('when uploading', () => {
@@ -45,8 +48,8 @@ describe('content/components/wrappers/image', () => {
expect(findLoadingIcon().exists()).toBe(true);
});
- it('adds gl-opacity-5 class selector to image', () => {
- expect(findImage().classes()).toContain('gl-opacity-5');
+ it('adds gl-opacity-5 class selector to the media tag', () => {
+ expect(findMedia().classes()).toContain('gl-opacity-5');
});
});
@@ -59,8 +62,8 @@ describe('content/components/wrappers/image', () => {
expect(findLoadingIcon().exists()).toBe(false);
});
- it('does not add gl-opacity-5 class selector to image', () => {
- expect(findImage().classes()).not.toContain('gl-opacity-5');
+ 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 ec67545cf17..d3c42104e47 100644
--- a/spec/frontend/content_editor/extensions/attachment_spec.js
+++ b/spec/frontend/content_editor/extensions/attachment_spec.js
@@ -1,7 +1,10 @@
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
+import waitForPromises from 'helpers/wait_for_promises';
import Attachment from '~/content_editor/extensions/attachment';
import Image from '~/content_editor/extensions/image';
+import Audio from '~/content_editor/extensions/audio';
+import Video from '~/content_editor/extensions/video';
import Link from '~/content_editor/extensions/link';
import Loading from '~/content_editor/extensions/loading';
import { VARIANT_DANGER } from '~/flash';
@@ -14,6 +17,23 @@ const PROJECT_WIKI_ATTACHMENT_IMAGE_HTML = `<p data-sourcepos="1:1-1:27" dir="au
<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>`;
@@ -23,6 +43,8 @@ describe('content_editor/extensions/attachment', () => {
let doc;
let p;
let image;
+ let audio;
+ let video;
let loading;
let link;
let renderMarkdown;
@@ -31,15 +53,18 @@ describe('content_editor/extensions/attachment', () => {
const uploadsPath = '/uploads/';
const imageFile = new File(['foo'], 'test-file.png', { type: 'image/png' });
+ const audioFile = new File(['foo'], 'test-file.mp3', { type: 'audio/mpeg' });
+ const videoFile = new File(['foo'], 'test-file.mp4', { type: 'video/mp4' });
const attachmentFile = new File(['foo'], 'test-file.zip', { type: 'application/zip' });
const expectDocumentAfterTransaction = ({ number, expectedDoc, action }) => {
return new Promise((resolve) => {
let counter = 1;
- const handleTransaction = () => {
+ const handleTransaction = async () => {
if (counter === number) {
expect(tiptapEditor.state.doc.toJSON()).toEqual(expectedDoc.toJSON());
tiptapEditor.off('update', handleTransaction);
+ await waitForPromises();
resolve();
}
@@ -60,18 +85,22 @@ describe('content_editor/extensions/attachment', () => {
Loading,
Link,
Image,
+ Audio,
+ Video,
Attachment.configure({ renderMarkdown, uploadsPath, eventHub }),
],
});
({
- builders: { doc, p, image, loading, link },
+ builders: { doc, p, image, audio, video, loading, link },
} = createDocBuilder({
tiptapEditor,
names: {
loading: { markType: Loading.name },
image: { nodeType: Image.name },
link: { nodeType: Link.name },
+ audio: { nodeType: Audio.name },
+ video: { nodeType: Video.name },
},
}));
@@ -103,17 +132,22 @@ describe('content_editor/extensions/attachment', () => {
tiptapEditor.commands.setContent(initialDoc.toJSON());
});
- describe('when the file has image mime type', () => {
- const base64EncodedFile = 'data:image/png;base64,Zm9v';
+ describe.each`
+ nodeType | mimeType | html | file | mediaType
+ ${'image'} | ${'image/png'} | ${PROJECT_WIKI_ATTACHMENT_IMAGE_HTML} | ${imageFile} | ${(attrs) => image(attrs)}
+ ${'audio'} | ${'audio/mpeg'} | ${PROJECT_WIKI_ATTACHMENT_AUDIO_HTML} | ${audioFile} | ${(attrs) => audio(attrs)}
+ ${'video'} | ${'video/mp4'} | ${PROJECT_WIKI_ATTACHMENT_VIDEO_HTML} | ${videoFile} | ${(attrs) => video(attrs)}
+ `('when the file has $nodeType mime type', ({ mimeType, html, file, mediaType }) => {
+ const base64EncodedFile = `data:${mimeType};base64,Zm9v`;
beforeEach(() => {
- renderMarkdown.mockResolvedValue(PROJECT_WIKI_ATTACHMENT_IMAGE_HTML);
+ renderMarkdown.mockResolvedValue(html);
});
describe('when uploading succeeds', () => {
const successResponse = {
link: {
- markdown: '![test-file](test-file.png)',
+ markdown: `![test-file](${file.name})`,
},
};
@@ -121,21 +155,21 @@ describe('content_editor/extensions/attachment', () => {
mock.onPost().reply(httpStatus.OK, successResponse);
});
- it('inserts an image with src set to the encoded image file and uploading true', async () => {
- const expectedDoc = doc(p(image({ uploading: true, src: base64EncodedFile })));
+ it('inserts a media content with src set to the encoded content and uploading true', async () => {
+ const expectedDoc = doc(p(mediaType({ uploading: true, src: base64EncodedFile })));
await expectDocumentAfterTransaction({
number: 1,
expectedDoc,
- action: () => tiptapEditor.commands.uploadAttachment({ file: imageFile }),
+ action: () => tiptapEditor.commands.uploadAttachment({ file }),
});
});
- it('updates the inserted image with canonicalSrc when upload is successful', async () => {
+ it('updates the inserted content with canonicalSrc when upload is successful', async () => {
const expectedDoc = doc(
p(
- image({
- canonicalSrc: 'test-file.png',
+ mediaType({
+ canonicalSrc: file.name,
src: base64EncodedFile,
alt: 'test-file',
uploading: false,
@@ -146,7 +180,7 @@ describe('content_editor/extensions/attachment', () => {
await expectDocumentAfterTransaction({
number: 2,
expectedDoc,
- action: () => tiptapEditor.commands.uploadAttachment({ file: imageFile }),
+ action: () => tiptapEditor.commands.uploadAttachment({ file }),
});
});
});
@@ -162,17 +196,19 @@ describe('content_editor/extensions/attachment', () => {
await expectDocumentAfterTransaction({
number: 2,
expectedDoc,
- action: () => tiptapEditor.commands.uploadAttachment({ file: imageFile }),
+ action: () => tiptapEditor.commands.uploadAttachment({ file }),
});
});
- it('emits an alert event that includes an error message', (done) => {
- tiptapEditor.commands.uploadAttachment({ file: imageFile });
+ it('emits an alert event that includes an error message', () => {
+ tiptapEditor.commands.uploadAttachment({ file });
- eventHub.$on('alert', ({ message, variant }) => {
- expect(variant).toBe(VARIANT_DANGER);
- expect(message).toBe('An error occurred while uploading the image. Please try again.');
- done();
+ return new Promise((resolve) => {
+ eventHub.$on('alert', ({ message, variant }) => {
+ expect(variant).toBe(VARIANT_DANGER);
+ expect(message).toBe('An error occurred while uploading the file. Please try again.');
+ resolve();
+ });
});
});
});
@@ -243,13 +279,12 @@ describe('content_editor/extensions/attachment', () => {
});
});
- it('emits an alert event that includes an error message', (done) => {
+ it('emits an alert event that includes an error message', () => {
tiptapEditor.commands.uploadAttachment({ file: attachmentFile });
eventHub.$on('alert', ({ message, variant }) => {
expect(variant).toBe(VARIANT_DANGER);
expect(message).toBe('An error occurred while uploading the file. Please try again.');
- done();
});
});
});
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 05fa0f79ef0..02e5b1dc271 100644
--- a/spec/frontend/content_editor/extensions/code_block_highlight_spec.js
+++ b/spec/frontend/content_editor/extensions/code_block_highlight_spec.js
@@ -1,5 +1,5 @@
import CodeBlockHighlight from '~/content_editor/extensions/code_block_highlight';
-import { createTestEditor } from '../test_utils';
+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">
<code>
@@ -12,34 +12,78 @@ const CODE_BLOCK_HTML = `<pre class="code highlight js-syntax-highlight language
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(() => {
- tiptapEditor = createTestEditor({ extensions: [CodeBlockHighlight] });
- parsedCodeBlockHtmlFixture = parseHTML(CODE_BLOCK_HTML);
+ languageLoader = { loadLanguages: jest.fn() };
+ tiptapEditor = createTestEditor({
+ extensions: [CodeBlockHighlight.configure({ languageLoader })],
+ });
- tiptapEditor.commands.setContent(CODE_BLOCK_HTML);
+ ({
+ builders: { doc, codeBlock },
+ } = createDocBuilder({
+ tiptapEditor,
+ names: {
+ codeBlock: { nodeType: CodeBlockHighlight.name },
+ },
+ }));
});
- it('extracts language and params attributes from Markdown API output', () => {
- const language = preElement().getAttribute('lang');
+ describe('when parsing HTML', () => {
+ beforeEach(() => {
+ parsedCodeBlockHtmlFixture = parseHTML(CODE_BLOCK_HTML);
- expect(tiptapEditor.getJSON().content[0].attrs).toMatchObject({
- language,
+ tiptapEditor.commands.setContent(CODE_BLOCK_HTML);
+ });
+ it('extracts language and params attributes from Markdown API output', () => {
+ const language = preElement().getAttribute('lang');
+
+ expect(tiptapEditor.getJSON().content[0].attrs).toMatchObject({
+ language,
+ });
+ });
+
+ it('adds code, highlight, and js-syntax-highlight to code block element', () => {
+ const editorHtmlOutput = parseHTML(tiptapEditor.getHTML()).querySelector('pre');
+
+ expect(editorHtmlOutput.classList.toString()).toContain('code highlight js-syntax-highlight');
});
- });
- it('adds code, highlight, and js-syntax-highlight to code block element', () => {
- const editorHtmlOutput = parseHTML(tiptapEditor.getHTML()).querySelector('pre');
+ it('adds content-editor-code-block class to the pre element', () => {
+ const editorHtmlOutput = parseHTML(tiptapEditor.getHTML()).querySelector('pre');
- expect(editorHtmlOutput.classList.toString()).toContain('code highlight js-syntax-highlight');
+ expect(editorHtmlOutput.classList.toString()).toContain('content-editor-code-block');
+ });
});
- it('adds content-editor-code-block class to the pre element', () => {
- const editorHtmlOutput = parseHTML(tiptapEditor.getHTML()).querySelector('pre');
+ describe.each`
+ inputRule
+ ${'```'}
+ ${'~~~'}
+ `('when typing $inputRule input rule', ({ inputRule }) => {
+ const language = 'javascript';
+
+ beforeEach(() => {
+ triggerNodeInputRule({
+ tiptapEditor,
+ inputRuleText: `${inputRule}${language} `,
+ });
+ });
+
+ it('creates a new code block and loads related language', () => {
+ const expectedDoc = doc(codeBlock({ language }));
- expect(editorHtmlOutput.classList.toString()).toContain('content-editor-code-block');
+ expect(tiptapEditor.getJSON()).toEqual(expectedDoc.toJSON());
+ });
+
+ it('loads language when language loader is available', () => {
+ expect(languageLoader.loadLanguages).toHaveBeenCalledWith([language]);
+ });
});
});
diff --git a/spec/frontend/content_editor/extensions/frontmatter_spec.js b/spec/frontend/content_editor/extensions/frontmatter_spec.js
index a8cbad6ef81..4f80c2cb81a 100644
--- a/spec/frontend/content_editor/extensions/frontmatter_spec.js
+++ b/spec/frontend/content_editor/extensions/frontmatter_spec.js
@@ -23,7 +23,7 @@ describe('content_editor/extensions/frontmatter', () => {
});
it('does not insert a frontmatter block when executing code block input rule', () => {
- const expectedDoc = doc(codeBlock(''));
+ const expectedDoc = doc(codeBlock({ language: 'plaintext' }, ''));
const inputRuleText = '``` ';
triggerNodeInputRule({ tiptapEditor, inputRuleText });
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
new file mode 100644
index 00000000000..905c1685b94
--- /dev/null
+++ b/spec/frontend/content_editor/services/code_block_language_loader_spec.js
@@ -0,0 +1,120 @@
+import codeBlockLanguageBlocker from '~/content_editor/services/code_block_language_loader';
+import waitForPromises from 'helpers/wait_for_promises';
+import { backtickInputRegex } from '~/content_editor/extensions/code_block_highlight';
+
+describe('content_editor/services/code_block_language_loader', () => {
+ let languageLoader;
+ let lowlight;
+
+ beforeEach(() => {
+ lowlight = {
+ languages: [],
+ registerLanguage: jest
+ .fn()
+ .mockImplementation((language) => lowlight.languages.push(language)),
+ registered: jest.fn().mockImplementation((language) => lowlight.languages.includes(language)),
+ };
+ languageLoader = codeBlockLanguageBlocker;
+ languageLoader.lowlight = lowlight;
+ });
+
+ describe('findLanguageBySyntax', () => {
+ it.each`
+ syntax | language
+ ${'javascript'} | ${{ syntax: 'javascript', label: 'Javascript' }}
+ ${'js'} | ${{ syntax: 'javascript', label: 'Javascript' }}
+ ${'jsx'} | ${{ syntax: 'javascript', label: 'Javascript' }}
+ `('returns a language by syntax and its variants', ({ syntax, language }) => {
+ expect(languageLoader.findLanguageBySyntax(syntax)).toMatchObject(language);
+ });
+
+ it('returns Custom (syntax) if the language does not exist', () => {
+ expect(languageLoader.findLanguageBySyntax('foobar')).toMatchObject({
+ syntax: 'foobar',
+ label: 'Custom (foobar)',
+ });
+ });
+
+ it('returns plaintext if no syntax is passed', () => {
+ expect(languageLoader.findLanguageBySyntax('')).toMatchObject({
+ syntax: 'plaintext',
+ label: 'Plain text',
+ });
+ });
+ });
+
+ describe('filterLanguages', () => {
+ it('filters languages by the given search term', () => {
+ expect(languageLoader.filterLanguages('ts')).toEqual([
+ { label: 'Device Tree', syntax: 'dts' },
+ { label: 'Kotlin', syntax: 'kotlin', variants: 'kt, kts' },
+ { label: 'TypeScript', syntax: 'typescript', variants: 'ts, tsx' },
+ ]);
+ });
+ });
+
+ describe('loadLanguages', () => {
+ it('loads highlight.js language packages identified by a list of languages', async () => {
+ const languages = ['javascript', 'ruby'];
+
+ await languageLoader.loadLanguages(languages);
+
+ languages.forEach((language) => {
+ 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);
+
+ 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 ');
+ const attrs = languageLoader.loadLanguageFromInputRule(match);
+
+ await waitForPromises();
+
+ expect(attrs).toEqual({ language: 'javascript' });
+ expect(lowlight.registerLanguage).toHaveBeenCalledWith('javascript', expect.any(Function));
+ });
+ });
+
+ describe('isLanguageLoaded', () => {
+ it('returns true when a language is registered', async () => {
+ const language = 'javascript';
+
+ expect(languageLoader.isLanguageLoaded(language)).toBe(false);
+
+ await languageLoader.loadLanguages([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 3bc72b13302..5b7a27b501d 100644
--- a/spec/frontend/content_editor/services/content_editor_spec.js
+++ b/spec/frontend/content_editor/services/content_editor_spec.js
@@ -11,6 +11,7 @@ describe('content_editor/services/content_editor', () => {
let contentEditor;
let serializer;
let deserializer;
+ let languageLoader;
let eventHub;
let doc;
let p;
@@ -27,8 +28,15 @@ describe('content_editor/services/content_editor', () => {
serializer = { deserialize: jest.fn() };
deserializer = { deserialize: jest.fn() };
+ languageLoader = { loadLanguagesFromDOM: jest.fn() };
eventHub = eventHubFactory();
- contentEditor = new ContentEditor({ tiptapEditor, serializer, deserializer, eventHub });
+ contentEditor = new ContentEditor({
+ tiptapEditor,
+ serializer,
+ deserializer,
+ eventHub,
+ languageLoader,
+ });
});
describe('.dispose', () => {
@@ -43,10 +51,12 @@ describe('content_editor/services/content_editor', () => {
describe('when setSerializedContent succeeds', () => {
let document;
+ const dom = {};
+ const testMarkdown = '**bold text**';
beforeEach(() => {
document = doc(p('document'));
- deserializer.deserialize.mockResolvedValueOnce({ document });
+ deserializer.deserialize.mockResolvedValueOnce({ document, dom });
});
it('emits loadingContent and loadingSuccess event in the eventHub', () => {
@@ -59,14 +69,20 @@ describe('content_editor/services/content_editor', () => {
expect(loadingContentEmitted).toBe(true);
});
- contentEditor.setSerializedContent('**bold text**');
+ contentEditor.setSerializedContent(testMarkdown);
});
it('sets the deserialized document in the tiptap editor object', async () => {
- await contentEditor.setSerializedContent('**bold text**');
+ await contentEditor.setSerializedContent(testMarkdown);
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/contributors/store/actions_spec.js b/spec/frontend/contributors/store/actions_spec.js
index a4054ab1fc8..ef0ff8ca208 100644
--- a/spec/frontend/contributors/store/actions_spec.js
+++ b/spec/frontend/contributors/store/actions_spec.js
@@ -17,10 +17,14 @@ describe('Contributors store actions', () => {
mock = new MockAdapter(axios);
});
- it('should commit SET_CHART_DATA with received response', (done) => {
+ afterEach(() => {
+ mock.restore();
+ });
+
+ it('should commit SET_CHART_DATA with received response', () => {
mock.onGet().reply(200, chartData);
- testAction(
+ return testAction(
actions.fetchChartData,
{ endpoint },
{},
@@ -30,30 +34,22 @@ describe('Contributors store actions', () => {
{ type: types.SET_LOADING_STATE, payload: false },
],
[],
- () => {
- mock.restore();
- done();
- },
);
});
- it('should show flash on API error', (done) => {
+ it('should show flash on API error', async () => {
mock.onGet().reply(400, 'Not Found');
- testAction(
+ await testAction(
actions.fetchChartData,
{ endpoint },
{},
[{ type: types.SET_LOADING_STATE, payload: true }],
[],
- () => {
- expect(createFlash).toHaveBeenCalledWith({
- message: expect.stringMatching('error'),
- });
- mock.restore();
- done();
- },
);
+ expect(createFlash).toHaveBeenCalledWith({
+ message: expect.stringMatching('error'),
+ });
});
});
});
diff --git a/spec/frontend/create_cluster/gke_cluster/stores/actions_spec.js b/spec/frontend/create_cluster/gke_cluster/stores/actions_spec.js
index 55c502b96bb..c365cb6a9f4 100644
--- a/spec/frontend/create_cluster/gke_cluster/stores/actions_spec.js
+++ b/spec/frontend/create_cluster/gke_cluster/stores/actions_spec.js
@@ -14,53 +14,49 @@ import {
describe('GCP Cluster Dropdown Store Actions', () => {
describe('setProject', () => {
- it('should set project', (done) => {
- testAction(
+ it('should set project', () => {
+ return testAction(
actions.setProject,
selectedProjectMock,
{ selectedProject: {} },
[{ type: 'SET_PROJECT', payload: selectedProjectMock }],
[],
- done,
);
});
});
describe('setZone', () => {
- it('should set zone', (done) => {
- testAction(
+ it('should set zone', () => {
+ return testAction(
actions.setZone,
selectedZoneMock,
{ selectedZone: '' },
[{ type: 'SET_ZONE', payload: selectedZoneMock }],
[],
- done,
);
});
});
describe('setMachineType', () => {
- it('should set machine type', (done) => {
- testAction(
+ it('should set machine type', () => {
+ return testAction(
actions.setMachineType,
selectedMachineTypeMock,
{ selectedMachineType: '' },
[{ type: 'SET_MACHINE_TYPE', payload: selectedMachineTypeMock }],
[],
- done,
);
});
});
describe('setIsValidatingProjectBilling', () => {
- it('should set machine type', (done) => {
- testAction(
+ it('should set machine type', () => {
+ return testAction(
actions.setIsValidatingProjectBilling,
true,
{ isValidatingProjectBilling: null },
[{ type: 'SET_IS_VALIDATING_PROJECT_BILLING', payload: true }],
[],
- done,
);
});
});
@@ -94,8 +90,8 @@ describe('GCP Cluster Dropdown Store Actions', () => {
});
describe('validateProjectBilling', () => {
- it('checks project billing status from Google API', (done) => {
- testAction(
+ it('checks project billing status from Google API', () => {
+ return testAction(
actions.validateProjectBilling,
true,
{
@@ -110,7 +106,6 @@ describe('GCP Cluster Dropdown Store Actions', () => {
{ type: 'SET_PROJECT_BILLING_STATUS', payload: true },
],
[{ type: 'setIsValidatingProjectBilling', payload: false }],
- done,
);
});
});
diff --git a/spec/frontend/crm/contact_form_spec.js b/spec/frontend/crm/contact_form_spec.js
deleted file mode 100644
index 0edab4f5ec5..00000000000
--- a/spec/frontend/crm/contact_form_spec.js
+++ /dev/null
@@ -1,157 +0,0 @@
-import { GlAlert } from '@gitlab/ui';
-import Vue from 'vue';
-import VueApollo from 'vue-apollo';
-import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import createMockApollo from 'helpers/mock_apollo_helper';
-import waitForPromises from 'helpers/wait_for_promises';
-import ContactForm from '~/crm/components/contact_form.vue';
-import createContactMutation from '~/crm/components/queries/create_contact.mutation.graphql';
-import updateContactMutation from '~/crm/components/queries/update_contact.mutation.graphql';
-import getGroupContactsQuery from '~/crm/components/queries/get_group_contacts.query.graphql';
-import {
- createContactMutationErrorResponse,
- createContactMutationResponse,
- getGroupContactsQueryResponse,
- updateContactMutationErrorResponse,
- updateContactMutationResponse,
-} from './mock_data';
-
-describe('Customer relations contact form component', () => {
- Vue.use(VueApollo);
- let wrapper;
- let fakeApollo;
- let mutation;
- let queryHandler;
-
- const findSaveContactButton = () => wrapper.findByTestId('save-contact-button');
- const findCancelButton = () => wrapper.findByTestId('cancel-button');
- const findForm = () => wrapper.find('form');
- const findError = () => wrapper.findComponent(GlAlert);
-
- const mountComponent = ({ mountFunction = shallowMountExtended, editForm = false } = {}) => {
- fakeApollo = createMockApollo([[mutation, queryHandler]]);
- fakeApollo.clients.defaultClient.cache.writeQuery({
- query: getGroupContactsQuery,
- variables: { groupFullPath: 'flightjs' },
- data: getGroupContactsQueryResponse.data,
- });
- const propsData = { drawerOpen: true };
- if (editForm)
- propsData.contact = { firstName: 'First', lastName: 'Last', email: 'email@example.com' };
- wrapper = mountFunction(ContactForm, {
- provide: { groupId: 26, groupFullPath: 'flightjs' },
- apolloProvider: fakeApollo,
- propsData,
- });
- };
-
- beforeEach(() => {
- mutation = createContactMutation;
- queryHandler = jest.fn().mockResolvedValue(createContactMutationResponse);
- });
-
- afterEach(() => {
- wrapper.destroy();
- fakeApollo = null;
- });
-
- describe('Save contact button', () => {
- it('should be disabled when required fields are empty', () => {
- mountComponent();
-
- expect(findSaveContactButton().props('disabled')).toBe(true);
- });
-
- it('should not be disabled when required fields have values', async () => {
- mountComponent();
-
- wrapper.find('#contact-first-name').vm.$emit('input', 'A');
- wrapper.find('#contact-last-name').vm.$emit('input', 'B');
- wrapper.find('#contact-email').vm.$emit('input', 'C');
- await waitForPromises();
-
- expect(findSaveContactButton().props('disabled')).toBe(false);
- });
- });
-
- it("should emit 'close' when cancel button is clicked", () => {
- mountComponent();
-
- findCancelButton().vm.$emit('click');
-
- expect(wrapper.emitted().close).toBeTruthy();
- });
-
- describe('when create mutation is successful', () => {
- it("should emit 'close'", async () => {
- mountComponent();
-
- findForm().trigger('submit');
- await waitForPromises();
-
- expect(wrapper.emitted().close).toBeTruthy();
- });
- });
-
- describe('when create mutation fails', () => {
- it('should show error on reject', async () => {
- queryHandler = jest.fn().mockRejectedValue('ERROR');
- mountComponent();
-
- findForm().trigger('submit');
- await waitForPromises();
-
- expect(findError().exists()).toBe(true);
- });
-
- it('should show error on error response', async () => {
- queryHandler = jest.fn().mockResolvedValue(createContactMutationErrorResponse);
- mountComponent();
-
- findForm().trigger('submit');
- await waitForPromises();
-
- expect(findError().exists()).toBe(true);
- expect(findError().text()).toBe('create contact is invalid.');
- });
- });
-
- describe('when update mutation is successful', () => {
- it("should emit 'close'", async () => {
- mutation = updateContactMutation;
- queryHandler = jest.fn().mockResolvedValue(updateContactMutationResponse);
- mountComponent({ editForm: true });
-
- findForm().trigger('submit');
- await waitForPromises();
-
- expect(wrapper.emitted().close).toBeTruthy();
- });
- });
-
- describe('when update mutation fails', () => {
- beforeEach(() => {
- mutation = updateContactMutation;
- });
-
- it('should show error on reject', async () => {
- queryHandler = jest.fn().mockRejectedValue('ERROR');
- mountComponent({ editForm: true });
- findForm().trigger('submit');
- await waitForPromises();
-
- expect(findError().exists()).toBe(true);
- });
-
- it('should show error on error response', async () => {
- queryHandler = jest.fn().mockResolvedValue(updateContactMutationErrorResponse);
- mountComponent({ editForm: true });
-
- findForm().trigger('submit');
- await waitForPromises();
-
- expect(findError().exists()).toBe(true);
- expect(findError().text()).toBe('update contact is invalid.');
- });
- });
-});
diff --git a/spec/frontend/crm/contact_form_wrapper_spec.js b/spec/frontend/crm/contact_form_wrapper_spec.js
new file mode 100644
index 00000000000..6307889a7aa
--- /dev/null
+++ b/spec/frontend/crm/contact_form_wrapper_spec.js
@@ -0,0 +1,88 @@
+import { shallowMountExtended } from 'helpers/vue_test_utils_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';
+
+describe('Customer relations contact form wrapper', () => {
+ let wrapper;
+
+ const findContactForm = () => wrapper.findComponent(ContactForm);
+
+ const $apollo = {
+ queries: {
+ contacts: {
+ loading: false,
+ },
+ },
+ };
+ const $route = {
+ params: {
+ id: 7,
+ },
+ };
+ const contacts = [{ id: 'gid://gitlab/CustomerRelations::Contact/7' }];
+
+ const mountComponent = ({ isEditMode = false } = {}) => {
+ wrapper = shallowMountExtended(ContactFormWrapper, {
+ propsData: {
+ isEditMode,
+ },
+ provide: {
+ groupFullPath: 'flightjs',
+ groupId: 26,
+ },
+ mocks: {
+ $apollo,
+ $route,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('in edit mode', () => {
+ it('should render contact form with correct props', () => {
+ mountComponent({ isEditMode: true });
+
+ 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',
+ });
+ });
+ });
+
+ describe('in create mode', () => {
+ it('should render contact form with correct props', () => {
+ mountComponent();
+
+ 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({
+ groupId: 'gid://gitlab/Group/26',
+ });
+ });
+ });
+});
diff --git a/spec/frontend/crm/contacts_root_spec.js b/spec/frontend/crm/contacts_root_spec.js
index b30349305a3..b02d94e9cb1 100644
--- a/spec/frontend/crm/contacts_root_spec.js
+++ b/spec/frontend/crm/contacts_root_spec.js
@@ -5,11 +5,9 @@ import VueRouter from 'vue-router';
import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import ContactsRoot from '~/crm/components/contacts_root.vue';
-import ContactForm from '~/crm/components/contact_form.vue';
-import getGroupContactsQuery from '~/crm/components/queries/get_group_contacts.query.graphql';
-import { NEW_ROUTE_NAME, EDIT_ROUTE_NAME } from '~/crm/constants';
-import routes from '~/crm/routes';
+import ContactsRoot from '~/crm/contacts/components/contacts_root.vue';
+import getGroupContactsQuery from '~/crm/contacts/components/graphql/get_group_contacts.query.graphql';
+import routes from '~/crm/contacts/routes';
import { getGroupContactsQueryResponse } from './mock_data';
describe('Customer relations contacts root app', () => {
@@ -23,8 +21,6 @@ describe('Customer relations contacts root app', () => {
const findRowByName = (rowName) => wrapper.findAllByRole('row', { name: rowName });
const findIssuesLinks = () => wrapper.findAllByTestId('issues-link');
const findNewContactButton = () => wrapper.findByTestId('new-contact-button');
- const findEditContactButton = () => wrapper.findByTestId('edit-contact-button');
- const findContactForm = () => wrapper.findComponent(ContactForm);
const findError = () => wrapper.findComponent(GlAlert);
const successQueryHandler = jest.fn().mockResolvedValue(getGroupContactsQueryResponse);
@@ -40,8 +36,8 @@ describe('Customer relations contacts root app', () => {
router,
provide: {
groupFullPath: 'flightjs',
- groupIssuesPath: '/issues',
groupId: 26,
+ groupIssuesPath: '/issues',
canAdminCrmContact,
},
apolloProvider: fakeApollo,
@@ -82,71 +78,6 @@ describe('Customer relations contacts root app', () => {
});
});
- describe('contact form', () => {
- it('should not exist by default', async () => {
- mountComponent();
- await waitForPromises();
-
- expect(findContactForm().exists()).toBe(false);
- });
-
- it('should exist when user clicks new contact button', async () => {
- mountComponent();
-
- findNewContactButton().vm.$emit('click');
- await waitForPromises();
-
- expect(findContactForm().exists()).toBe(true);
- });
-
- it('should exist when user navigates directly to `new` route', async () => {
- router.replace({ name: NEW_ROUTE_NAME });
- mountComponent();
- await waitForPromises();
-
- expect(findContactForm().exists()).toBe(true);
- });
-
- it('should exist when user clicks edit contact button', async () => {
- mountComponent({ mountFunction: mountExtended });
- await waitForPromises();
-
- findEditContactButton().vm.$emit('click');
- await waitForPromises();
-
- expect(findContactForm().exists()).toBe(true);
- });
-
- it('should exist when user navigates directly to `edit` route', async () => {
- router.replace({ name: EDIT_ROUTE_NAME, params: { id: 16 } });
- mountComponent();
- await waitForPromises();
-
- expect(findContactForm().exists()).toBe(true);
- });
-
- it('should not exist when new form emits close', async () => {
- router.replace({ name: NEW_ROUTE_NAME });
- mountComponent();
-
- findContactForm().vm.$emit('close');
- await waitForPromises();
-
- expect(findContactForm().exists()).toBe(false);
- });
-
- it('should not exist when edit form emits close', async () => {
- router.replace({ name: EDIT_ROUTE_NAME, params: { id: 16 } });
- mountComponent();
- await waitForPromises();
-
- findContactForm().vm.$emit('close');
- await waitForPromises();
-
- expect(findContactForm().exists()).toBe(false);
- });
- });
-
describe('error', () => {
it('should exist on reject', async () => {
mountComponent({ queryHandler: jest.fn().mockRejectedValue('ERROR') });
diff --git a/spec/frontend/crm/form_spec.js b/spec/frontend/crm/form_spec.js
index 0e3abc05c37..5c349b24ea1 100644
--- a/spec/frontend/crm/form_spec.js
+++ b/spec/frontend/crm/form_spec.js
@@ -6,12 +6,12 @@ import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import Form from '~/crm/components/form.vue';
-import routes from '~/crm/routes';
-import createContactMutation from '~/crm/components/queries/create_contact.mutation.graphql';
-import updateContactMutation from '~/crm/components/queries/update_contact.mutation.graphql';
-import getGroupContactsQuery from '~/crm/components/queries/get_group_contacts.query.graphql';
-import createOrganizationMutation from '~/crm/components/queries/create_organization.mutation.graphql';
-import getGroupOrganizationsQuery from '~/crm/components/queries/get_group_organizations.query.graphql';
+import routes from '~/crm/contacts/routes';
+import createContactMutation from '~/crm/contacts/components/graphql/create_contact.mutation.graphql';
+import updateContactMutation from '~/crm/contacts/components/graphql/update_contact.mutation.graphql';
+import getGroupContactsQuery from '~/crm/contacts/components/graphql/get_group_contacts.query.graphql';
+import createOrganizationMutation from '~/crm/organizations/components/graphql/create_organization.mutation.graphql';
+import getGroupOrganizationsQuery from '~/crm/organizations/components/graphql/get_group_organizations.query.graphql';
import {
createContactMutationErrorResponse,
createContactMutationResponse,
@@ -101,6 +101,11 @@ describe('Reusable form component', () => {
{ name: 'phone', label: 'Phone' },
{ name: 'description', label: 'Description' },
],
+ getQuery: {
+ query: getGroupContactsQuery,
+ variables: { groupFullPath: 'flightjs' },
+ },
+ getQueryNodePath: 'group.contacts',
...propsData,
});
};
@@ -108,13 +113,8 @@ describe('Reusable form component', () => {
const mountContactCreate = () => {
const propsData = {
title: 'New contact',
- successMessage: 'Contact has been added',
+ successMessage: 'Contact has been added.',
buttonLabel: 'Create contact',
- getQuery: {
- query: getGroupContactsQuery,
- variables: { groupFullPath: 'flightjs' },
- },
- getQueryNodePath: 'group.contacts',
mutation: createContactMutation,
additionalCreateParams: { groupId: 'gid://gitlab/Group/26' },
};
@@ -124,14 +124,9 @@ describe('Reusable form component', () => {
const mountContactUpdate = () => {
const propsData = {
title: 'Edit contact',
- successMessage: 'Contact has been updated',
+ successMessage: 'Contact has been updated.',
mutation: updateContactMutation,
- existingModel: {
- id: 'gid://gitlab/CustomerRelations::Contact/12',
- firstName: 'First',
- lastName: 'Last',
- email: 'email@example.com',
- },
+ existingId: 'gid://gitlab/CustomerRelations::Contact/12',
};
mountContact({ propsData });
};
@@ -143,6 +138,11 @@ describe('Reusable form component', () => {
{ name: 'defaultRate', label: 'Default rate', input: { type: 'number', step: '0.01' } },
{ name: 'description', label: 'Description' },
],
+ getQuery: {
+ query: getGroupOrganizationsQuery,
+ variables: { groupFullPath: 'flightjs' },
+ },
+ getQueryNodePath: 'group.organizations',
...propsData,
});
};
@@ -150,13 +150,8 @@ describe('Reusable form component', () => {
const mountOrganizationCreate = () => {
const propsData = {
title: 'New organization',
- successMessage: 'Organization has been added',
+ successMessage: 'Organization has been added.',
buttonLabel: 'Create organization',
- getQuery: {
- query: getGroupOrganizationsQuery,
- variables: { groupFullPath: 'flightjs' },
- },
- getQueryNodePath: 'group.organizations',
mutation: createOrganizationMutation,
additionalCreateParams: { groupId: 'gid://gitlab/Group/26' },
};
@@ -167,17 +162,17 @@ describe('Reusable form component', () => {
[FORM_CREATE_CONTACT]: {
mountFunction: mountContactCreate,
mutationErrorResponse: createContactMutationErrorResponse,
- toastMessage: 'Contact has been added',
+ toastMessage: 'Contact has been added.',
},
[FORM_UPDATE_CONTACT]: {
mountFunction: mountContactUpdate,
mutationErrorResponse: updateContactMutationErrorResponse,
- toastMessage: 'Contact has been updated',
+ toastMessage: 'Contact has been updated.',
},
[FORM_CREATE_ORG]: {
mountFunction: mountOrganizationCreate,
mutationErrorResponse: createOrganizationMutationErrorResponse,
- toastMessage: 'Organization has been added',
+ toastMessage: 'Organization has been added.',
},
};
const asTestParams = (...keys) => keys.map((name) => [name, forms[name]]);
diff --git a/spec/frontend/crm/mock_data.js b/spec/frontend/crm/mock_data.js
index e351e101b29..35bc7fb69b4 100644
--- a/spec/frontend/crm/mock_data.js
+++ b/spec/frontend/crm/mock_data.js
@@ -157,3 +157,28 @@ export const createOrganizationMutationErrorResponse = {
},
},
};
+
+export const updateOrganizationMutationResponse = {
+ data: {
+ customerRelationsOrganizationUpdate: {
+ __typeName: 'CustomerRelationsOrganizationUpdatePayload',
+ organization: {
+ __typename: 'CustomerRelationsOrganization',
+ id: 'gid://gitlab/CustomerRelations::Organization/2',
+ name: 'A',
+ defaultRate: null,
+ description: null,
+ },
+ errors: [],
+ },
+ },
+};
+
+export const updateOrganizationMutationErrorResponse = {
+ data: {
+ customerRelationsOrganizationUpdate: {
+ organization: null,
+ errors: ['Description is invalid.'],
+ },
+ },
+};
diff --git a/spec/frontend/crm/new_organization_form_spec.js b/spec/frontend/crm/new_organization_form_spec.js
deleted file mode 100644
index 0a7909774c9..00000000000
--- a/spec/frontend/crm/new_organization_form_spec.js
+++ /dev/null
@@ -1,109 +0,0 @@
-import { GlAlert } from '@gitlab/ui';
-import Vue from 'vue';
-import VueApollo from 'vue-apollo';
-import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import createMockApollo from 'helpers/mock_apollo_helper';
-import waitForPromises from 'helpers/wait_for_promises';
-import NewOrganizationForm from '~/crm/components/new_organization_form.vue';
-import createOrganizationMutation from '~/crm/components/queries/create_organization.mutation.graphql';
-import getGroupOrganizationsQuery from '~/crm/components/queries/get_group_organizations.query.graphql';
-import {
- createOrganizationMutationErrorResponse,
- createOrganizationMutationResponse,
- getGroupOrganizationsQueryResponse,
-} from './mock_data';
-
-describe('Customer relations organizations root app', () => {
- Vue.use(VueApollo);
- let wrapper;
- let fakeApollo;
- let queryHandler;
-
- const findCreateNewOrganizationButton = () =>
- wrapper.findByTestId('create-new-organization-button');
- const findCancelButton = () => wrapper.findByTestId('cancel-button');
- const findForm = () => wrapper.find('form');
- const findError = () => wrapper.findComponent(GlAlert);
-
- const mountComponent = () => {
- fakeApollo = createMockApollo([[createOrganizationMutation, queryHandler]]);
- fakeApollo.clients.defaultClient.cache.writeQuery({
- query: getGroupOrganizationsQuery,
- variables: { groupFullPath: 'flightjs' },
- data: getGroupOrganizationsQueryResponse.data,
- });
- wrapper = shallowMountExtended(NewOrganizationForm, {
- provide: { groupId: 26, groupFullPath: 'flightjs' },
- apolloProvider: fakeApollo,
- propsData: { drawerOpen: true },
- });
- };
-
- beforeEach(() => {
- queryHandler = jest.fn().mockResolvedValue(createOrganizationMutationResponse);
- });
-
- afterEach(() => {
- wrapper.destroy();
- fakeApollo = null;
- });
-
- describe('Create new organization button', () => {
- it('should be disabled by default', () => {
- mountComponent();
-
- expect(findCreateNewOrganizationButton().attributes('disabled')).toBeTruthy();
- });
-
- it('should not be disabled when first, last and email have values', async () => {
- mountComponent();
-
- wrapper.find('#organization-name').vm.$emit('input', 'A');
- await waitForPromises();
-
- expect(findCreateNewOrganizationButton().attributes('disabled')).toBeFalsy();
- });
- });
-
- it("should emit 'close' when cancel button is clicked", () => {
- mountComponent();
-
- findCancelButton().vm.$emit('click');
-
- expect(wrapper.emitted().close).toBeTruthy();
- });
-
- describe('when query is successful', () => {
- it("should emit 'close'", async () => {
- mountComponent();
-
- findForm().trigger('submit');
- await waitForPromises();
-
- expect(wrapper.emitted().close).toBeTruthy();
- });
- });
-
- describe('when query fails', () => {
- it('should show error on reject', async () => {
- queryHandler = jest.fn().mockRejectedValue('ERROR');
- mountComponent();
-
- findForm().trigger('submit');
- await waitForPromises();
-
- expect(findError().exists()).toBe(true);
- });
-
- it('should show error on error response', async () => {
- queryHandler = jest.fn().mockResolvedValue(createOrganizationMutationErrorResponse);
- mountComponent();
-
- findForm().trigger('submit');
- await waitForPromises();
-
- expect(findError().exists()).toBe(true);
- expect(findError().text()).toBe('create organization is invalid.');
- });
- });
-});
diff --git a/spec/frontend/crm/organization_form_wrapper_spec.js b/spec/frontend/crm/organization_form_wrapper_spec.js
new file mode 100644
index 00000000000..1a5a7c6ca5d
--- /dev/null
+++ b/spec/frontend/crm/organization_form_wrapper_spec.js
@@ -0,0 +1,88 @@
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import OrganizationFormWrapper from '~/crm/organizations/components/organization_form_wrapper.vue';
+import OrganizationForm from '~/crm/components/form.vue';
+import getGroupOrganizationsQuery from '~/crm/organizations/components/graphql/get_group_organizations.query.graphql';
+import createOrganizationMutation from '~/crm/organizations/components/graphql/create_organization.mutation.graphql';
+import updateOrganizationMutation from '~/crm/organizations/components/graphql/update_organization.mutation.graphql';
+
+describe('Customer relations organization form wrapper', () => {
+ let wrapper;
+
+ const findOrganizationForm = () => wrapper.findComponent(OrganizationForm);
+
+ const $apollo = {
+ queries: {
+ organizations: {
+ loading: false,
+ },
+ },
+ };
+ const $route = {
+ params: {
+ id: 7,
+ },
+ };
+ const organizations = [{ id: 'gid://gitlab/CustomerRelations::Organization/7' }];
+
+ const mountComponent = ({ isEditMode = false } = {}) => {
+ wrapper = shallowMountExtended(OrganizationFormWrapper, {
+ propsData: {
+ isEditMode,
+ },
+ provide: {
+ groupFullPath: 'flightjs',
+ groupId: 26,
+ },
+ mocks: {
+ $apollo,
+ $route,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('in edit mode', () => {
+ it('should render organization form with correct props', () => {
+ mountComponent({ isEditMode: true });
+
+ const organizationForm = findOrganizationForm();
+ expect(organizationForm.props('fields')).toHaveLength(3);
+ expect(organizationForm.props('title')).toBe('Edit organization');
+ expect(organizationForm.props('successMessage')).toBe('Organization has been updated.');
+ expect(organizationForm.props('mutation')).toBe(updateOrganizationMutation);
+ expect(organizationForm.props('getQuery')).toMatchObject({
+ query: getGroupOrganizationsQuery,
+ variables: { groupFullPath: 'flightjs' },
+ });
+ expect(organizationForm.props('getQueryNodePath')).toBe('group.organizations');
+ expect(organizationForm.props('existingId')).toBe(organizations[0].id);
+ expect(organizationForm.props('additionalCreateParams')).toMatchObject({
+ groupId: 'gid://gitlab/Group/26',
+ });
+ });
+ });
+
+ describe('in create mode', () => {
+ it('should render organization form with correct props', () => {
+ mountComponent();
+
+ const organizationForm = findOrganizationForm();
+ expect(organizationForm.props('fields')).toHaveLength(3);
+ expect(organizationForm.props('title')).toBe('New organization');
+ expect(organizationForm.props('successMessage')).toBe('Organization has been added.');
+ expect(organizationForm.props('mutation')).toBe(createOrganizationMutation);
+ expect(organizationForm.props('getQuery')).toMatchObject({
+ query: getGroupOrganizationsQuery,
+ variables: { groupFullPath: 'flightjs' },
+ });
+ expect(organizationForm.props('getQueryNodePath')).toBe('group.organizations');
+ expect(organizationForm.props('existingId')).toBeNull();
+ expect(organizationForm.props('additionalCreateParams')).toMatchObject({
+ groupId: 'gid://gitlab/Group/26',
+ });
+ });
+ });
+});
diff --git a/spec/frontend/crm/organizations_root_spec.js b/spec/frontend/crm/organizations_root_spec.js
index aef417964f4..231208d938e 100644
--- a/spec/frontend/crm/organizations_root_spec.js
+++ b/spec/frontend/crm/organizations_root_spec.js
@@ -5,11 +5,9 @@ import VueRouter from 'vue-router';
import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import OrganizationsRoot from '~/crm/components/organizations_root.vue';
-import NewOrganizationForm from '~/crm/components/new_organization_form.vue';
-import { NEW_ROUTE_NAME } from '~/crm/constants';
-import routes from '~/crm/routes';
-import getGroupOrganizationsQuery from '~/crm/components/queries/get_group_organizations.query.graphql';
+import OrganizationsRoot from '~/crm/organizations/components/organizations_root.vue';
+import routes from '~/crm/organizations/routes';
+import getGroupOrganizationsQuery from '~/crm/organizations/components/graphql/get_group_organizations.query.graphql';
import { getGroupOrganizationsQueryResponse } from './mock_data';
describe('Customer relations organizations root app', () => {
@@ -23,7 +21,6 @@ describe('Customer relations organizations root app', () => {
const findRowByName = (rowName) => wrapper.findAllByRole('row', { name: rowName });
const findIssuesLinks = () => wrapper.findAllByTestId('issues-link');
const findNewOrganizationButton = () => wrapper.findByTestId('new-organization-button');
- const findNewOrganizationForm = () => wrapper.findComponent(NewOrganizationForm);
const findError = () => wrapper.findComponent(GlAlert);
const successQueryHandler = jest.fn().mockResolvedValue(getGroupOrganizationsQueryResponse);
@@ -37,7 +34,11 @@ describe('Customer relations organizations root app', () => {
fakeApollo = createMockApollo([[getGroupOrganizationsQuery, queryHandler]]);
wrapper = mountFunction(OrganizationsRoot, {
router,
- provide: { canAdminCrmOrganization, groupFullPath: 'flightjs', groupIssuesPath: '/issues' },
+ provide: {
+ canAdminCrmOrganization,
+ groupFullPath: 'flightjs',
+ groupIssuesPath: '/issues',
+ },
apolloProvider: fakeApollo,
});
};
@@ -76,42 +77,6 @@ describe('Customer relations organizations root app', () => {
});
});
- describe('new organization form', () => {
- it('should not exist by default', async () => {
- mountComponent();
- await waitForPromises();
-
- expect(findNewOrganizationForm().exists()).toBe(false);
- });
-
- it('should exist when user clicks new contact button', async () => {
- mountComponent();
-
- findNewOrganizationButton().vm.$emit('click');
- await waitForPromises();
-
- expect(findNewOrganizationForm().exists()).toBe(true);
- });
-
- it('should exist when user navigates directly to /new', async () => {
- router.replace({ name: NEW_ROUTE_NAME });
- mountComponent();
- await waitForPromises();
-
- expect(findNewOrganizationForm().exists()).toBe(true);
- });
-
- it('should not exist when form emits close', async () => {
- router.replace({ name: NEW_ROUTE_NAME });
- mountComponent();
-
- findNewOrganizationForm().vm.$emit('close');
- await waitForPromises();
-
- expect(findNewOrganizationForm().exists()).toBe(false);
- });
- });
-
it('should render error message on reject', async () => {
mountComponent({ queryHandler: jest.fn().mockRejectedValue('ERROR') });
await waitForPromises();
diff --git a/spec/frontend/design_management/components/design_notes/__snapshots__/design_note_spec.js.snap b/spec/frontend/design_management/components/design_notes/__snapshots__/design_note_spec.js.snap
index 4ecf82a4714..402e55347af 100644
--- a/spec/frontend/design_management/components/design_notes/__snapshots__/design_note_spec.js.snap
+++ b/spec/frontend/design_management/components/design_notes/__snapshots__/design_note_spec.js.snap
@@ -5,16 +5,19 @@ exports[`Design note component should match the snapshot 1`] = `
class="design-note note-form"
id="note_123"
>
- <user-avatar-link-stub
- imgalt="foo-bar"
- imgcssclasses=""
- imgsize="40"
- imgsrc=""
- linkhref=""
- tooltipplacement="top"
- tooltiptext=""
- username=""
- />
+ <gl-avatar-link-stub
+ class="gl-float-left gl-mr-3"
+ href="https://gitlab.com/user"
+ >
+ <gl-avatar-stub
+ alt="avatar"
+ entityid="0"
+ entityname="foo-bar"
+ shape="circle"
+ size="32"
+ src="https://gitlab.com/avatar"
+ />
+ </gl-avatar-link-stub>
<div
class="gl-display-flex gl-justify-content-space-between"
@@ -22,8 +25,10 @@ exports[`Design note component should match the snapshot 1`] = `
<div>
<gl-link-stub
class="js-user-link"
+ data-testid="user-link"
data-user-id="1"
data-username="foo-bar"
+ href="https://gitlab.com/user"
>
<span
class="note-header-author-name gl-font-weight-bold"
@@ -69,8 +74,9 @@ exports[`Design note component should match the snapshot 1`] = `
</div>
<div
- class="note-text js-note-text md"
+ class="note-text md"
data-qa-selector="note_content"
+ data-testid="note-text"
/>
</timeline-entry-item-stub>
diff --git a/spec/frontend/design_management/components/design_notes/design_discussion_spec.js b/spec/frontend/design_management/components/design_notes/design_discussion_spec.js
index bbf2190ad47..77935fbde11 100644
--- a/spec/frontend/design_management/components/design_notes/design_discussion_spec.js
+++ b/spec/frontend/design_management/components/design_notes/design_discussion_spec.js
@@ -31,7 +31,6 @@ describe('Design discussions component', () => {
const findReplyForm = () => wrapper.find(DesignReplyForm);
const findRepliesWidget = () => wrapper.find(ToggleRepliesWidget);
const findResolveButton = () => wrapper.find('[data-testid="resolve-button"]');
- const findResolveIcon = () => wrapper.find('[data-testid="resolve-icon"]');
const findResolvedMessage = () => wrapper.find('[data-testid="resolved-message"]');
const findResolveLoadingIcon = () => wrapper.find(GlLoadingIcon);
const findResolveCheckbox = () => wrapper.find('[data-testid="resolve-checkbox"]');
@@ -117,7 +116,7 @@ describe('Design discussions component', () => {
});
it('does not render an icon to resolve a thread', () => {
- expect(findResolveIcon().exists()).toBe(false);
+ expect(findResolveButton().exists()).toBe(false);
});
it('does not render a checkbox in reply form', async () => {
@@ -147,7 +146,7 @@ describe('Design discussions component', () => {
});
it('renders a correct icon to resolve a thread', () => {
- expect(findResolveIcon().props('name')).toBe('check-circle');
+ expect(findResolveButton().props('icon')).toBe('check-circle');
});
it('renders a checkbox with Resolve thread text in reply form', async () => {
@@ -203,7 +202,7 @@ describe('Design discussions component', () => {
});
it('renders a correct icon to resolve a thread', () => {
- expect(findResolveIcon().props('name')).toBe('check-circle-filled');
+ expect(findResolveButton().props('icon')).toBe('check-circle-filled');
});
it('emit todo:toggle when discussion is resolved', async () => {
diff --git a/spec/frontend/design_management/components/design_notes/design_note_spec.js b/spec/frontend/design_management/components/design_notes/design_note_spec.js
index 35fd1273270..1f84fde9f7f 100644
--- a/spec/frontend/design_management/components/design_notes/design_note_spec.js
+++ b/spec/frontend/design_management/components/design_notes/design_note_spec.js
@@ -1,10 +1,10 @@
-import { shallowMount } from '@vue/test-utils';
import { ApolloMutation } from 'vue-apollo';
import { nextTick } from 'vue';
+import { GlAvatar, GlAvatarLink } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import DesignNote from '~/design_management/components/design_notes/design_note.vue';
import DesignReplyForm from '~/design_management/components/design_notes/design_reply_form.vue';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
-import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
const scrollIntoViewMock = jest.fn();
const note = {
@@ -12,6 +12,8 @@ const note = {
author: {
id: 'gid://gitlab/User/1',
username: 'foo-bar',
+ avatarUrl: 'https://gitlab.com/avatar',
+ webUrl: 'https://gitlab.com/user',
},
body: 'test',
userPermissions: {
@@ -30,14 +32,15 @@ const mutate = jest.fn().mockResolvedValue({ data: { updateNote: {} } });
describe('Design note component', () => {
let wrapper;
- const findUserAvatar = () => wrapper.find(UserAvatarLink);
- const findUserLink = () => wrapper.find('.js-user-link');
- const findReplyForm = () => wrapper.find(DesignReplyForm);
- const findEditButton = () => wrapper.find('.js-note-edit');
- const findNoteContent = () => wrapper.find('.js-note-text');
+ const findUserAvatar = () => wrapper.findComponent(GlAvatar);
+ const findUserAvatarLink = () => wrapper.findComponent(GlAvatarLink);
+ const findUserLink = () => wrapper.findByTestId('user-link');
+ const findReplyForm = () => wrapper.findComponent(DesignReplyForm);
+ const findEditButton = () => wrapper.findByTestId('note-edit');
+ const findNoteContent = () => wrapper.findByTestId('note-text');
function createComponent(props = {}, data = { isEditing: false }) {
- wrapper = shallowMount(DesignNote, {
+ wrapper = shallowMountExtended(DesignNote, {
propsData: {
note: {},
...props,
@@ -71,12 +74,24 @@ describe('Design note component', () => {
expect(wrapper.element).toMatchSnapshot();
});
- it('should render an author', () => {
+ it('should render avatar with correct props', () => {
+ createComponent({
+ note,
+ });
+
+ expect(findUserAvatar().props()).toMatchObject({
+ src: note.author.avatarUrl,
+ entityName: note.author.username,
+ });
+
+ expect(findUserAvatarLink().attributes('href')).toBe(note.author.webUrl);
+ });
+
+ it('should render author details', () => {
createComponent({
note,
});
- expect(findUserAvatar().exists()).toBe(true);
expect(findUserLink().exists()).toBe(true);
});
@@ -107,7 +122,7 @@ describe('Design note component', () => {
},
});
- findEditButton().trigger('click');
+ findEditButton().vm.$emit('click');
await nextTick();
expect(findReplyForm().exists()).toBe(true);
diff --git a/spec/frontend/design_management/pages/index_spec.js b/spec/frontend/design_management/pages/index_spec.js
index a240a41959f..87531e8b645 100644
--- a/spec/frontend/design_management/pages/index_spec.js
+++ b/spec/frontend/design_management/pages/index_spec.js
@@ -183,7 +183,7 @@ describe('Design management index page', () => {
[moveDesignMutation, moveDesignHandler],
];
- fakeApollo = createMockApollo(requestHandlers);
+ fakeApollo = createMockApollo(requestHandlers, {}, { addTypename: true });
wrapper = shallowMount(Index, {
apolloProvider: fakeApollo,
router,
diff --git a/spec/frontend/diffs/components/commit_item_spec.js b/spec/frontend/diffs/components/commit_item_spec.js
index d887029124f..eee17e118a0 100644
--- a/spec/frontend/diffs/components/commit_item_spec.js
+++ b/spec/frontend/diffs/components/commit_item_spec.js
@@ -11,7 +11,9 @@ jest.mock('~/user_popovers');
const TEST_AUTHOR_NAME = 'test';
const TEST_AUTHOR_EMAIL = 'test+test@gitlab.com';
const TEST_AUTHOR_GRAVATAR = `${TEST_HOST}/avatar/test?s=40`;
-const TEST_SIGNATURE_HTML = '<a>Legit commit</a>';
+const TEST_SIGNATURE_HTML = `<a class="btn gpg-status-box valid" data-content="signature-content" data-html="true" data-placement="top" data-title="signature-title" data-toggle="popover" role="button" tabindex="0">
+ Verified
+</a>`;
const TEST_PIPELINE_STATUS_PATH = `${TEST_HOST}/pipeline/status`;
describe('diffs/components/commit_item', () => {
@@ -82,7 +84,7 @@ describe('diffs/components/commit_item', () => {
const imgElement = avatarElement.find('img');
expect(avatarElement.attributes('href')).toBe(commit.author.web_url);
- expect(imgElement.classes()).toContain('s40');
+ expect(imgElement.classes()).toContain('s32');
expect(imgElement.attributes('alt')).toBe(commit.author.name);
expect(imgElement.attributes('src')).toBe(commit.author.avatar_url);
});
@@ -156,8 +158,9 @@ describe('diffs/components/commit_item', () => {
it('renders signature html', () => {
const actionsElement = getCommitActionsElement();
+ const signatureElement = actionsElement.find('.gpg-status-box');
- expect(actionsElement.html()).toContain(TEST_SIGNATURE_HTML);
+ expect(signatureElement.html()).toBe(TEST_SIGNATURE_HTML);
});
});
diff --git a/spec/frontend/diffs/components/diff_line_note_form_spec.js b/spec/frontend/diffs/components/diff_line_note_form_spec.js
index 0ccf996e220..fb9dc22ce25 100644
--- a/spec/frontend/diffs/components/diff_line_note_form_spec.js
+++ b/spec/frontend/diffs/components/diff_line_note_form_spec.js
@@ -4,7 +4,7 @@ import DiffLineNoteForm from '~/diffs/components/diff_line_note_form.vue';
import { createStore } from '~/mr_notes/stores';
import NoteForm from '~/notes/components/note_form.vue';
import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
-import { noteableDataMock } from '../../notes/mock_data';
+import { noteableDataMock } from 'jest/notes/mock_data';
import diffFileMockData from '../mock_data/diff_file';
jest.mock('~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal', () => {
@@ -98,7 +98,7 @@ describe('DiffLineNoteForm', () => {
});
describe('saveNoteForm', () => {
- it('should call saveNote action with proper params', (done) => {
+ it('should call saveNote action with proper params', async () => {
const saveDiffDiscussionSpy = jest
.spyOn(wrapper.vm, 'saveDiffDiscussion')
.mockReturnValue(Promise.resolve());
@@ -123,16 +123,11 @@ describe('DiffLineNoteForm', () => {
lineRange,
};
- wrapper.vm
- .handleSaveNote('note body')
- .then(() => {
- expect(saveDiffDiscussionSpy).toHaveBeenCalledWith({
- note: 'note body',
- formData,
- });
- })
- .then(done)
- .catch(done.fail);
+ await wrapper.vm.handleSaveNote('note body');
+ expect(saveDiffDiscussionSpy).toHaveBeenCalledWith({
+ note: 'note body',
+ formData,
+ });
});
});
});
diff --git a/spec/frontend/diffs/store/actions_spec.js b/spec/frontend/diffs/store/actions_spec.js
index d6a2aa104cd..3b567fbc704 100644
--- a/spec/frontend/diffs/store/actions_spec.js
+++ b/spec/frontend/diffs/store/actions_spec.js
@@ -9,46 +9,7 @@ import {
INLINE_DIFF_VIEW_TYPE,
PARALLEL_DIFF_VIEW_TYPE,
} from '~/diffs/constants';
-import {
- setBaseConfig,
- fetchDiffFilesBatch,
- fetchDiffFilesMeta,
- fetchCoverageFiles,
- assignDiscussionsToDiff,
- removeDiscussionsFromDiff,
- startRenderDiffsQueue,
- setInlineDiffViewType,
- setParallelDiffViewType,
- showCommentForm,
- cancelCommentForm,
- loadMoreLines,
- scrollToLineIfNeededInline,
- scrollToLineIfNeededParallel,
- loadCollapsedDiff,
- toggleFileDiscussions,
- saveDiffDiscussion,
- setHighlightedRow,
- toggleTreeOpen,
- scrollToFile,
- setShowTreeList,
- renderFileForDiscussionId,
- setRenderTreeList,
- setShowWhitespace,
- setRenderIt,
- receiveFullDiffError,
- fetchFullDiff,
- toggleFullDiff,
- switchToFullDiffFromRenamedFile,
- setFileCollapsedByUser,
- setExpandedDiffLines,
- setSuggestPopoverDismissed,
- changeCurrentCommit,
- moveToNeighboringCommit,
- setCurrentDiffFileIdFromNote,
- navigateToDiffFileIndex,
- setFileByFile,
- reviewFile,
-} from '~/diffs/store/actions';
+import * as diffActions from '~/diffs/store/actions';
import * as types from '~/diffs/store/mutation_types';
import * as utils from '~/diffs/store/utils';
import * as treeWorkerUtils from '~/diffs/utils/tree_worker_utils';
@@ -62,6 +23,8 @@ import { diffMetadata } from '../mock_data/diff_metadata';
jest.mock('~/flash');
describe('DiffsStoreActions', () => {
+ let mock;
+
useLocalStorageSpy();
const originalMethods = {
@@ -83,15 +46,20 @@ describe('DiffsStoreActions', () => {
});
});
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+ });
+
afterEach(() => {
['requestAnimationFrame', 'requestIdleCallback'].forEach((method) => {
global[method] = originalMethods[method];
});
createFlash.mockClear();
+ mock.restore();
});
describe('setBaseConfig', () => {
- it('should set given endpoint and project path', (done) => {
+ it('should set given endpoint and project path', () => {
const endpoint = '/diffs/set/endpoint';
const endpointMetadata = '/diffs/set/endpoint/metadata';
const endpointBatch = '/diffs/set/endpoint/batch';
@@ -104,8 +72,8 @@ describe('DiffsStoreActions', () => {
b: ['y', 'hash:a'],
};
- testAction(
- setBaseConfig,
+ return testAction(
+ diffActions.setBaseConfig,
{
endpoint,
endpointBatch,
@@ -153,23 +121,12 @@ describe('DiffsStoreActions', () => {
},
],
[],
- done,
);
});
});
describe('fetchDiffFilesBatch', () => {
- let mock;
-
- beforeEach(() => {
- mock = new MockAdapter(axios);
- });
-
- afterEach(() => {
- mock.restore();
- });
-
- it('should fetch batch diff files', (done) => {
+ it('should fetch batch diff files', () => {
const endpointBatch = '/fetch/diffs_batch';
const res1 = { diff_files: [{ file_hash: 'test' }], pagination: { total_pages: 7 } };
const res2 = { diff_files: [{ file_hash: 'test2' }], pagination: { total_pages: 7 } };
@@ -199,8 +156,8 @@ describe('DiffsStoreActions', () => {
)
.reply(200, res2);
- testAction(
- fetchDiffFilesBatch,
+ return testAction(
+ diffActions.fetchDiffFilesBatch,
{},
{ endpointBatch, diffViewType: 'inline', diffFiles: [] },
[
@@ -216,7 +173,6 @@ describe('DiffsStoreActions', () => {
{ type: types.SET_BATCH_LOADING_STATE, payload: 'error' },
],
[{ type: 'startRenderDiffsQueue' }, { type: 'startRenderDiffsQueue' }],
- done,
);
});
@@ -229,13 +185,14 @@ describe('DiffsStoreActions', () => {
({ viewStyle, otherView }) => {
const endpointBatch = '/fetch/diffs_batch';
- fetchDiffFilesBatch({
- commit: () => {},
- state: {
- endpointBatch: `${endpointBatch}?view=${otherView}`,
- diffViewType: viewStyle,
- },
- })
+ diffActions
+ .fetchDiffFilesBatch({
+ commit: () => {},
+ state: {
+ endpointBatch: `${endpointBatch}?view=${otherView}`,
+ diffViewType: viewStyle,
+ },
+ })
.then(() => {
expect(mock.history.get[0].url).toContain(`view=${viewStyle}`);
expect(mock.history.get[0].url).not.toContain(`view=${otherView}`);
@@ -248,19 +205,16 @@ describe('DiffsStoreActions', () => {
describe('fetchDiffFilesMeta', () => {
const endpointMetadata = '/fetch/diffs_metadata.json?view=inline';
const noFilesData = { ...diffMetadata };
- let mock;
beforeEach(() => {
- mock = new MockAdapter(axios);
-
delete noFilesData.diff_files;
mock.onGet(endpointMetadata).reply(200, diffMetadata);
});
- it('should fetch diff meta information', (done) => {
- testAction(
- fetchDiffFilesMeta,
+ it('should fetch diff meta information', () => {
+ return testAction(
+ diffActions.fetchDiffFilesMeta,
{},
{ endpointMetadata, diffViewType: 'inline' },
[
@@ -275,55 +229,41 @@ describe('DiffsStoreActions', () => {
},
],
[],
- () => {
- mock.restore();
- done();
- },
);
});
});
describe('fetchCoverageFiles', () => {
- let mock;
const endpointCoverage = '/fetch';
- beforeEach(() => {
- mock = new MockAdapter(axios);
- });
-
- afterEach(() => mock.restore());
-
- it('should commit SET_COVERAGE_DATA with received response', (done) => {
+ it('should commit SET_COVERAGE_DATA with received response', () => {
const data = { files: { 'app.js': { 1: 0, 2: 1 } } };
mock.onGet(endpointCoverage).reply(200, { data });
- testAction(
- fetchCoverageFiles,
+ return testAction(
+ diffActions.fetchCoverageFiles,
{},
{ endpointCoverage },
[{ type: types.SET_COVERAGE_DATA, payload: { data } }],
[],
- done,
);
});
- it('should show flash on API error', (done) => {
+ it('should show flash on API error', async () => {
mock.onGet(endpointCoverage).reply(400);
- testAction(fetchCoverageFiles, {}, { endpointCoverage }, [], [], () => {
- expect(createFlash).toHaveBeenCalledTimes(1);
- expect(createFlash).toHaveBeenCalledWith({
- message: expect.stringMatching('Something went wrong'),
- });
- done();
+ await testAction(diffActions.fetchCoverageFiles, {}, { endpointCoverage }, [], []);
+ expect(createFlash).toHaveBeenCalledTimes(1);
+ expect(createFlash).toHaveBeenCalledWith({
+ message: expect.stringMatching('Something went wrong'),
});
});
});
describe('setHighlightedRow', () => {
it('should mark currently selected diff and set lineHash and fileHash of highlightedRow', () => {
- testAction(setHighlightedRow, 'ABC_123', {}, [
+ return testAction(diffActions.setHighlightedRow, 'ABC_123', {}, [
{ type: types.SET_HIGHLIGHTED_ROW, payload: 'ABC_123' },
{ type: types.SET_CURRENT_DIFF_FILE, payload: 'ABC' },
]);
@@ -335,7 +275,7 @@ describe('DiffsStoreActions', () => {
window.location.hash = '';
});
- it('should merge discussions into diffs', (done) => {
+ it('should merge discussions into diffs', () => {
window.location.hash = 'ABC_123';
const state = {
@@ -397,8 +337,8 @@ describe('DiffsStoreActions', () => {
const discussions = [singleDiscussion];
- testAction(
- assignDiscussionsToDiff,
+ return testAction(
+ diffActions.assignDiscussionsToDiff,
discussions,
state,
[
@@ -425,26 +365,24 @@ describe('DiffsStoreActions', () => {
},
],
[],
- done,
);
});
- it('dispatches setCurrentDiffFileIdFromNote with note ID', (done) => {
+ it('dispatches setCurrentDiffFileIdFromNote with note ID', () => {
window.location.hash = 'note_123';
- testAction(
- assignDiscussionsToDiff,
+ return testAction(
+ diffActions.assignDiscussionsToDiff,
[],
{ diffFiles: [] },
[],
[{ type: 'setCurrentDiffFileIdFromNote', payload: '123' }],
- done,
);
});
});
describe('removeDiscussionsFromDiff', () => {
- it('should remove discussions from diffs', (done) => {
+ it('should remove discussions from diffs', () => {
const state = {
diffFiles: [
{
@@ -480,8 +418,8 @@ describe('DiffsStoreActions', () => {
line_code: 'ABC_1_1',
};
- testAction(
- removeDiscussionsFromDiff,
+ return testAction(
+ diffActions.removeDiscussionsFromDiff,
singleDiscussion,
state,
[
@@ -495,7 +433,6 @@ describe('DiffsStoreActions', () => {
},
],
[],
- done,
);
});
});
@@ -528,7 +465,7 @@ describe('DiffsStoreActions', () => {
});
};
- startRenderDiffsQueue({ state, commit: pseudoCommit });
+ diffActions.startRenderDiffsQueue({ state, commit: pseudoCommit });
expect(state.diffFiles[0].renderIt).toBe(true);
expect(state.diffFiles[1].renderIt).toBe(true);
@@ -536,69 +473,61 @@ describe('DiffsStoreActions', () => {
});
describe('setInlineDiffViewType', () => {
- it('should set diff view type to inline and also set the cookie properly', (done) => {
- testAction(
- setInlineDiffViewType,
+ it('should set diff view type to inline and also set the cookie properly', async () => {
+ await testAction(
+ diffActions.setInlineDiffViewType,
null,
{},
[{ type: types.SET_DIFF_VIEW_TYPE, payload: INLINE_DIFF_VIEW_TYPE }],
[],
- () => {
- expect(Cookies.get('diff_view')).toEqual(INLINE_DIFF_VIEW_TYPE);
- done();
- },
);
+ expect(Cookies.get('diff_view')).toEqual(INLINE_DIFF_VIEW_TYPE);
});
});
describe('setParallelDiffViewType', () => {
- it('should set diff view type to parallel and also set the cookie properly', (done) => {
- testAction(
- setParallelDiffViewType,
+ it('should set diff view type to parallel and also set the cookie properly', async () => {
+ await testAction(
+ diffActions.setParallelDiffViewType,
null,
{},
[{ type: types.SET_DIFF_VIEW_TYPE, payload: PARALLEL_DIFF_VIEW_TYPE }],
[],
- () => {
- expect(Cookies.get(DIFF_VIEW_COOKIE_NAME)).toEqual(PARALLEL_DIFF_VIEW_TYPE);
- done();
- },
);
+ expect(Cookies.get(DIFF_VIEW_COOKIE_NAME)).toEqual(PARALLEL_DIFF_VIEW_TYPE);
});
});
describe('showCommentForm', () => {
- it('should call mutation to show comment form', (done) => {
+ it('should call mutation to show comment form', () => {
const payload = { lineCode: 'lineCode', fileHash: 'hash' };
- testAction(
- showCommentForm,
+ return testAction(
+ diffActions.showCommentForm,
payload,
{},
[{ type: types.TOGGLE_LINE_HAS_FORM, payload: { ...payload, hasForm: true } }],
[],
- done,
);
});
});
describe('cancelCommentForm', () => {
- it('should call mutation to cancel comment form', (done) => {
+ it('should call mutation to cancel comment form', () => {
const payload = { lineCode: 'lineCode', fileHash: 'hash' };
- testAction(
- cancelCommentForm,
+ return testAction(
+ diffActions.cancelCommentForm,
payload,
{},
[{ type: types.TOGGLE_LINE_HAS_FORM, payload: { ...payload, hasForm: false } }],
[],
- done,
);
});
});
describe('loadMoreLines', () => {
- it('should call mutation to show comment form', (done) => {
+ it('should call mutation to show comment form', () => {
const endpoint = '/diffs/load/more/lines';
const params = { since: 6, to: 26 };
const lineNumbers = { oldLineNumber: 3, newLineNumber: 5 };
@@ -606,12 +535,11 @@ describe('DiffsStoreActions', () => {
const isExpandDown = false;
const nextLineNumbers = {};
const options = { endpoint, params, lineNumbers, fileHash, isExpandDown, nextLineNumbers };
- const mock = new MockAdapter(axios);
const contextLines = { contextLines: [{ lineCode: 6 }] };
mock.onGet(endpoint).reply(200, contextLines);
- testAction(
- loadMoreLines,
+ return testAction(
+ diffActions.loadMoreLines,
options,
{},
[
@@ -621,31 +549,23 @@ describe('DiffsStoreActions', () => {
},
],
[],
- () => {
- mock.restore();
- done();
- },
);
});
});
describe('loadCollapsedDiff', () => {
const state = { showWhitespace: true };
- it('should fetch data and call mutation with response and the give parameter', (done) => {
+ it('should fetch data and call mutation with response and the give parameter', () => {
const file = { hash: 123, load_collapsed_diff_url: '/load/collapsed/diff/url' };
const data = { hash: 123, parallelDiffLines: [{ lineCode: 1 }] };
- const mock = new MockAdapter(axios);
const commit = jest.fn();
mock.onGet(file.loadCollapsedDiffUrl).reply(200, data);
- loadCollapsedDiff({ commit, getters: { commitId: null }, state }, file)
+ return diffActions
+ .loadCollapsedDiff({ commit, getters: { commitId: null }, state }, file)
.then(() => {
expect(commit).toHaveBeenCalledWith(types.ADD_COLLAPSED_DIFFS, { file, data });
-
- mock.restore();
- done();
- })
- .catch(done.fail);
+ });
});
it('should fetch data without commit ID', () => {
@@ -656,7 +576,7 @@ describe('DiffsStoreActions', () => {
jest.spyOn(axios, 'get').mockReturnValue(Promise.resolve({ data: {} }));
- loadCollapsedDiff({ commit() {}, getters, state }, file);
+ diffActions.loadCollapsedDiff({ commit() {}, getters, state }, file);
expect(axios.get).toHaveBeenCalledWith(file.load_collapsed_diff_url, {
params: { commit_id: null, w: '0' },
@@ -671,7 +591,7 @@ describe('DiffsStoreActions', () => {
jest.spyOn(axios, 'get').mockReturnValue(Promise.resolve({ data: {} }));
- loadCollapsedDiff({ commit() {}, getters, state }, file);
+ diffActions.loadCollapsedDiff({ commit() {}, getters, state }, file);
expect(axios.get).toHaveBeenCalledWith(file.load_collapsed_diff_url, {
params: { commit_id: '123', w: '0' },
@@ -689,7 +609,7 @@ describe('DiffsStoreActions', () => {
const dispatch = jest.fn();
- toggleFileDiscussions({ getters, dispatch });
+ diffActions.toggleFileDiscussions({ getters, dispatch });
expect(dispatch).toHaveBeenCalledWith(
'collapseDiscussion',
@@ -707,7 +627,7 @@ describe('DiffsStoreActions', () => {
const dispatch = jest.fn();
- toggleFileDiscussions({ getters, dispatch });
+ diffActions.toggleFileDiscussions({ getters, dispatch });
expect(dispatch).toHaveBeenCalledWith(
'expandDiscussion',
@@ -725,7 +645,7 @@ describe('DiffsStoreActions', () => {
const dispatch = jest.fn();
- toggleFileDiscussions({ getters, dispatch });
+ diffActions.toggleFileDiscussions({ getters, dispatch });
expect(dispatch).toHaveBeenCalledWith(
'expandDiscussion',
@@ -743,7 +663,7 @@ describe('DiffsStoreActions', () => {
it('should not call handleLocationHash when there is not hash', () => {
window.location.hash = '';
- scrollToLineIfNeededInline({}, lineMock);
+ diffActions.scrollToLineIfNeededInline({}, lineMock);
expect(commonUtils.handleLocationHash).not.toHaveBeenCalled();
});
@@ -751,7 +671,7 @@ describe('DiffsStoreActions', () => {
it('should not call handleLocationHash when the hash does not match any line', () => {
window.location.hash = 'XYZ_456';
- scrollToLineIfNeededInline({}, lineMock);
+ diffActions.scrollToLineIfNeededInline({}, lineMock);
expect(commonUtils.handleLocationHash).not.toHaveBeenCalled();
});
@@ -759,14 +679,14 @@ describe('DiffsStoreActions', () => {
it('should call handleLocationHash only when the hash matches a line', () => {
window.location.hash = 'ABC_123';
- scrollToLineIfNeededInline(
+ diffActions.scrollToLineIfNeededInline(
{},
{
lineCode: 'ABC_456',
},
);
- scrollToLineIfNeededInline({}, lineMock);
- scrollToLineIfNeededInline(
+ diffActions.scrollToLineIfNeededInline({}, lineMock);
+ diffActions.scrollToLineIfNeededInline(
{},
{
lineCode: 'XYZ_456',
@@ -789,7 +709,7 @@ describe('DiffsStoreActions', () => {
it('should not call handleLocationHash when there is not hash', () => {
window.location.hash = '';
- scrollToLineIfNeededParallel({}, lineMock);
+ diffActions.scrollToLineIfNeededParallel({}, lineMock);
expect(commonUtils.handleLocationHash).not.toHaveBeenCalled();
});
@@ -797,7 +717,7 @@ describe('DiffsStoreActions', () => {
it('should not call handleLocationHash when the hash does not match any line', () => {
window.location.hash = 'XYZ_456';
- scrollToLineIfNeededParallel({}, lineMock);
+ diffActions.scrollToLineIfNeededParallel({}, lineMock);
expect(commonUtils.handleLocationHash).not.toHaveBeenCalled();
});
@@ -805,7 +725,7 @@ describe('DiffsStoreActions', () => {
it('should call handleLocationHash only when the hash matches a line', () => {
window.location.hash = 'ABC_123';
- scrollToLineIfNeededParallel(
+ diffActions.scrollToLineIfNeededParallel(
{},
{
left: null,
@@ -814,8 +734,8 @@ describe('DiffsStoreActions', () => {
},
},
);
- scrollToLineIfNeededParallel({}, lineMock);
- scrollToLineIfNeededParallel(
+ diffActions.scrollToLineIfNeededParallel({}, lineMock);
+ diffActions.scrollToLineIfNeededParallel(
{},
{
left: null,
@@ -831,7 +751,7 @@ describe('DiffsStoreActions', () => {
});
describe('saveDiffDiscussion', () => {
- it('dispatches actions', (done) => {
+ it('dispatches actions', () => {
const commitId = 'something';
const formData = {
diffFile: { ...mockDiffFile },
@@ -856,33 +776,29 @@ describe('DiffsStoreActions', () => {
}
});
- saveDiffDiscussion({ state, dispatch }, { note, formData })
- .then(() => {
- expect(dispatch).toHaveBeenCalledTimes(5);
- expect(dispatch).toHaveBeenNthCalledWith(1, 'saveNote', expect.any(Object), {
- root: true,
- });
+ return diffActions.saveDiffDiscussion({ state, dispatch }, { note, formData }).then(() => {
+ expect(dispatch).toHaveBeenCalledTimes(5);
+ expect(dispatch).toHaveBeenNthCalledWith(1, 'saveNote', expect.any(Object), {
+ root: true,
+ });
- const postData = dispatch.mock.calls[0][1];
- expect(postData.data.note.commit_id).toBe(commitId);
+ const postData = dispatch.mock.calls[0][1];
+ expect(postData.data.note.commit_id).toBe(commitId);
- expect(dispatch).toHaveBeenNthCalledWith(2, 'updateDiscussion', 'test', { root: true });
- expect(dispatch).toHaveBeenNthCalledWith(3, 'assignDiscussionsToDiff', ['discussion']);
- })
- .then(done)
- .catch(done.fail);
+ expect(dispatch).toHaveBeenNthCalledWith(2, 'updateDiscussion', 'test', { root: true });
+ expect(dispatch).toHaveBeenNthCalledWith(3, 'assignDiscussionsToDiff', ['discussion']);
+ });
});
});
describe('toggleTreeOpen', () => {
- it('commits TOGGLE_FOLDER_OPEN', (done) => {
- testAction(
- toggleTreeOpen,
+ it('commits TOGGLE_FOLDER_OPEN', () => {
+ return testAction(
+ diffActions.toggleTreeOpen,
'path',
{},
[{ type: types.TOGGLE_FOLDER_OPEN, payload: 'path' }],
[],
- done,
);
});
});
@@ -904,7 +820,7 @@ describe('DiffsStoreActions', () => {
},
};
- scrollToFile({ state, commit, getters }, { path: 'path' });
+ diffActions.scrollToFile({ state, commit, getters }, { path: 'path' });
expect(document.location.hash).toBe('#test');
});
@@ -918,28 +834,27 @@ describe('DiffsStoreActions', () => {
},
};
- scrollToFile({ state, commit, getters }, { path: 'path' });
+ diffActions.scrollToFile({ state, commit, getters }, { path: 'path' });
expect(commit).toHaveBeenCalledWith(types.SET_CURRENT_DIFF_FILE, 'test');
});
});
describe('setShowTreeList', () => {
- it('commits toggle', (done) => {
- testAction(
- setShowTreeList,
+ it('commits toggle', () => {
+ return testAction(
+ diffActions.setShowTreeList,
{ showTreeList: true },
{},
[{ type: types.SET_SHOW_TREE_LIST, payload: true }],
[],
- done,
);
});
it('updates localStorage', () => {
jest.spyOn(localStorage, 'setItem').mockImplementation(() => {});
- setShowTreeList({ commit() {} }, { showTreeList: true });
+ diffActions.setShowTreeList({ commit() {} }, { showTreeList: true });
expect(localStorage.setItem).toHaveBeenCalledWith('mr_tree_show', true);
});
@@ -947,7 +862,7 @@ describe('DiffsStoreActions', () => {
it('does not update localStorage', () => {
jest.spyOn(localStorage, 'setItem').mockImplementation(() => {});
- setShowTreeList({ commit() {} }, { showTreeList: true, saving: false });
+ diffActions.setShowTreeList({ commit() {} }, { showTreeList: true, saving: false });
expect(localStorage.setItem).not.toHaveBeenCalled();
});
@@ -994,7 +909,7 @@ describe('DiffsStoreActions', () => {
it('renders and expands file for the given discussion id', () => {
const localState = state({ collapsed: true, renderIt: false });
- renderFileForDiscussionId({ rootState, state: localState, commit }, '123');
+ diffActions.renderFileForDiscussionId({ rootState, state: localState, commit }, '123');
expect(commit).toHaveBeenCalledWith('RENDER_FILE', localState.diffFiles[0]);
expect($emit).toHaveBeenCalledTimes(1);
@@ -1004,7 +919,7 @@ describe('DiffsStoreActions', () => {
it('jumps to discussion on already rendered and expanded file', () => {
const localState = state({ collapsed: false, renderIt: true });
- renderFileForDiscussionId({ rootState, state: localState, commit }, '123');
+ diffActions.renderFileForDiscussionId({ rootState, state: localState, commit }, '123');
expect(commit).not.toHaveBeenCalled();
expect($emit).toHaveBeenCalledTimes(1);
@@ -1013,19 +928,18 @@ describe('DiffsStoreActions', () => {
});
describe('setRenderTreeList', () => {
- it('commits SET_RENDER_TREE_LIST', (done) => {
- testAction(
- setRenderTreeList,
+ it('commits SET_RENDER_TREE_LIST', () => {
+ return testAction(
+ diffActions.setRenderTreeList,
{ renderTreeList: true },
{},
[{ type: types.SET_RENDER_TREE_LIST, payload: true }],
[],
- done,
);
});
it('sets localStorage', () => {
- setRenderTreeList({ commit() {} }, { renderTreeList: true });
+ diffActions.setRenderTreeList({ commit() {} }, { renderTreeList: true });
expect(localStorage.setItem).toHaveBeenCalledWith('mr_diff_tree_list', true);
});
@@ -1034,11 +948,9 @@ describe('DiffsStoreActions', () => {
describe('setShowWhitespace', () => {
const endpointUpdateUser = 'user/prefs';
let putSpy;
- let mock;
let gon;
beforeEach(() => {
- mock = new MockAdapter(axios);
putSpy = jest.spyOn(axios, 'put');
gon = window.gon;
@@ -1047,25 +959,23 @@ describe('DiffsStoreActions', () => {
});
afterEach(() => {
- mock.restore();
window.gon = gon;
});
- it('commits SET_SHOW_WHITESPACE', (done) => {
- testAction(
- setShowWhitespace,
+ it('commits SET_SHOW_WHITESPACE', () => {
+ return testAction(
+ diffActions.setShowWhitespace,
{ showWhitespace: true, updateDatabase: false },
{},
[{ type: types.SET_SHOW_WHITESPACE, payload: true }],
[],
- done,
);
});
it('saves to the database when the user is logged in', async () => {
window.gon = { current_user_id: 12345 };
- await setShowWhitespace(
+ await diffActions.setShowWhitespace(
{ state: { endpointUpdateUser }, commit() {} },
{ showWhitespace: true, updateDatabase: true },
);
@@ -1076,7 +986,7 @@ describe('DiffsStoreActions', () => {
it('does not try to save to the API if the user is not logged in', async () => {
window.gon = {};
- await setShowWhitespace(
+ await diffActions.setShowWhitespace(
{ state: { endpointUpdateUser }, commit() {} },
{ showWhitespace: true, updateDatabase: true },
);
@@ -1085,7 +995,7 @@ describe('DiffsStoreActions', () => {
});
it('emits eventHub event', async () => {
- await setShowWhitespace(
+ await diffActions.setShowWhitespace(
{ state: {}, commit() {} },
{ showWhitespace: true, updateDatabase: false },
);
@@ -1095,53 +1005,47 @@ describe('DiffsStoreActions', () => {
});
describe('setRenderIt', () => {
- it('commits RENDER_FILE', (done) => {
- testAction(setRenderIt, 'file', {}, [{ type: types.RENDER_FILE, payload: 'file' }], [], done);
+ it('commits RENDER_FILE', () => {
+ return testAction(
+ diffActions.setRenderIt,
+ 'file',
+ {},
+ [{ type: types.RENDER_FILE, payload: 'file' }],
+ [],
+ );
});
});
describe('receiveFullDiffError', () => {
- it('updates state with the file that did not load', (done) => {
- testAction(
- receiveFullDiffError,
+ it('updates state with the file that did not load', () => {
+ return testAction(
+ diffActions.receiveFullDiffError,
'file',
{},
[{ type: types.RECEIVE_FULL_DIFF_ERROR, payload: 'file' }],
[],
- done,
);
});
});
describe('fetchFullDiff', () => {
- let mock;
-
- beforeEach(() => {
- mock = new MockAdapter(axios);
- });
-
- afterEach(() => {
- mock.restore();
- });
-
describe('success', () => {
beforeEach(() => {
mock.onGet(`${TEST_HOST}/context`).replyOnce(200, ['test']);
});
- it('commits the success and dispatches an action to expand the new lines', (done) => {
+ it('commits the success and dispatches an action to expand the new lines', () => {
const file = {
context_lines_path: `${TEST_HOST}/context`,
file_path: 'test',
file_hash: 'test',
};
- testAction(
- fetchFullDiff,
+ return testAction(
+ diffActions.fetchFullDiff,
file,
null,
[{ type: types.RECEIVE_FULL_DIFF_SUCCESS, payload: { filePath: 'test' } }],
[{ type: 'setExpandedDiffLines', payload: { file, data: ['test'] } }],
- done,
);
});
});
@@ -1151,14 +1055,13 @@ describe('DiffsStoreActions', () => {
mock.onGet(`${TEST_HOST}/context`).replyOnce(500);
});
- it('dispatches receiveFullDiffError', (done) => {
- testAction(
- fetchFullDiff,
+ it('dispatches receiveFullDiffError', () => {
+ return testAction(
+ diffActions.fetchFullDiff,
{ context_lines_path: `${TEST_HOST}/context`, file_path: 'test', file_hash: 'test' },
null,
[],
[{ type: 'receiveFullDiffError', payload: 'test' }],
- done,
);
});
});
@@ -1173,14 +1076,13 @@ describe('DiffsStoreActions', () => {
};
});
- it('dispatches fetchFullDiff when file is not expanded', (done) => {
- testAction(
- toggleFullDiff,
+ it('dispatches fetchFullDiff when file is not expanded', () => {
+ return testAction(
+ diffActions.toggleFullDiff,
'test',
state,
[{ type: types.REQUEST_FULL_DIFF, payload: 'test' }],
[{ type: 'fetchFullDiff', payload: state.diffFiles[0] }],
- done,
);
});
});
@@ -1202,16 +1104,13 @@ describe('DiffsStoreActions', () => {
};
const testData = [{ rich_text: 'test' }, { rich_text: 'file2' }];
let renamedFile;
- let mock;
beforeEach(() => {
- mock = new MockAdapter(axios);
jest.spyOn(utils, 'prepareLineForRenamedFile').mockImplementation(() => preparedLine);
});
afterEach(() => {
renamedFile = null;
- mock.restore();
});
describe('success', () => {
@@ -1228,7 +1127,7 @@ describe('DiffsStoreActions', () => {
'performs the correct mutations and starts a render queue for view type $diffViewType',
({ diffViewType }) => {
return testAction(
- switchToFullDiffFromRenamedFile,
+ diffActions.switchToFullDiffFromRenamedFile,
{ diffFile: renamedFile },
{ diffViewType },
[
@@ -1249,9 +1148,9 @@ describe('DiffsStoreActions', () => {
});
describe('setFileUserCollapsed', () => {
- it('commits SET_FILE_COLLAPSED', (done) => {
- testAction(
- setFileCollapsedByUser,
+ it('commits SET_FILE_COLLAPSED', () => {
+ return testAction(
+ diffActions.setFileCollapsedByUser,
{ filePath: 'test', collapsed: true },
null,
[
@@ -1261,7 +1160,6 @@ describe('DiffsStoreActions', () => {
},
],
[],
- done,
);
});
});
@@ -1273,11 +1171,11 @@ describe('DiffsStoreActions', () => {
});
});
- it('commits SET_CURRENT_VIEW_DIFF_FILE_LINES when lines less than MAX_RENDERING_DIFF_LINES', (done) => {
+ it('commits SET_CURRENT_VIEW_DIFF_FILE_LINES when lines less than MAX_RENDERING_DIFF_LINES', () => {
utils.convertExpandLines.mockImplementation(() => ['test']);
- testAction(
- setExpandedDiffLines,
+ return testAction(
+ diffActions.setExpandedDiffLines,
{ file: { file_path: 'path' }, data: [] },
{ diffViewType: 'inline' },
[
@@ -1287,16 +1185,15 @@ describe('DiffsStoreActions', () => {
},
],
[],
- done,
);
});
- it('commits ADD_CURRENT_VIEW_DIFF_FILE_LINES when lines more than MAX_RENDERING_DIFF_LINES', (done) => {
+ it('commits ADD_CURRENT_VIEW_DIFF_FILE_LINES when lines more than MAX_RENDERING_DIFF_LINES', () => {
const lines = new Array(501).fill().map((_, i) => `line-${i}`);
utils.convertExpandLines.mockReturnValue(lines);
- testAction(
- setExpandedDiffLines,
+ return testAction(
+ diffActions.setExpandedDiffLines,
{ file: { file_path: 'path' }, data: [] },
{ diffViewType: 'inline' },
[
@@ -1312,41 +1209,34 @@ describe('DiffsStoreActions', () => {
{ type: 'TOGGLE_DIFF_FILE_RENDERING_MORE', payload: 'path' },
],
[],
- done,
);
});
});
describe('setSuggestPopoverDismissed', () => {
- it('commits SET_SHOW_SUGGEST_POPOVER', (done) => {
+ it('commits SET_SHOW_SUGGEST_POPOVER', async () => {
const state = { dismissEndpoint: `${TEST_HOST}/-/user_callouts` };
- const mock = new MockAdapter(axios);
mock.onPost(state.dismissEndpoint).reply(200, {});
jest.spyOn(axios, 'post');
- testAction(
- setSuggestPopoverDismissed,
+ await testAction(
+ diffActions.setSuggestPopoverDismissed,
null,
state,
[{ type: types.SET_SHOW_SUGGEST_POPOVER }],
[],
- () => {
- expect(axios.post).toHaveBeenCalledWith(state.dismissEndpoint, {
- feature_name: 'suggest_popover_dismissed',
- });
-
- mock.restore();
- done();
- },
);
+ expect(axios.post).toHaveBeenCalledWith(state.dismissEndpoint, {
+ feature_name: 'suggest_popover_dismissed',
+ });
});
});
describe('changeCurrentCommit', () => {
it('commits the new commit information and re-requests the diff metadata for the commit', () => {
return testAction(
- changeCurrentCommit,
+ diffActions.changeCurrentCommit,
{ commitId: 'NEW' },
{
commit: {
@@ -1384,7 +1274,7 @@ describe('DiffsStoreActions', () => {
({ commitId, commit, msg }) => {
const err = new Error(msg);
const actionReturn = testAction(
- changeCurrentCommit,
+ diffActions.changeCurrentCommit,
{ commitId },
{
endpoint: 'URL/OLD',
@@ -1410,7 +1300,7 @@ describe('DiffsStoreActions', () => {
'for the direction "$direction", dispatches the action to move to the SHA "$expected"',
({ direction, expected, currentCommit }) => {
return testAction(
- moveToNeighboringCommit,
+ diffActions.moveToNeighboringCommit,
{ direction },
{ commit: currentCommit },
[],
@@ -1431,7 +1321,7 @@ describe('DiffsStoreActions', () => {
'given `{ "isloading": $diffsAreLoading, "commit": $currentCommit }` in state, no actions are dispatched',
({ direction, diffsAreLoading, currentCommit }) => {
return testAction(
- moveToNeighboringCommit,
+ diffActions.moveToNeighboringCommit,
{ direction },
{ commit: currentCommit, isLoading: diffsAreLoading },
[],
@@ -1450,7 +1340,7 @@ describe('DiffsStoreActions', () => {
notesById: { 1: { discussion_id: '2' } },
};
- setCurrentDiffFileIdFromNote({ commit, state, rootGetters }, '1');
+ diffActions.setCurrentDiffFileIdFromNote({ commit, state, rootGetters }, '1');
expect(commit).toHaveBeenCalledWith(types.SET_CURRENT_DIFF_FILE, '123');
});
@@ -1463,7 +1353,7 @@ describe('DiffsStoreActions', () => {
notesById: { 1: { discussion_id: '2' } },
};
- setCurrentDiffFileIdFromNote({ commit, state, rootGetters }, '1');
+ diffActions.setCurrentDiffFileIdFromNote({ commit, state, rootGetters }, '1');
expect(commit).not.toHaveBeenCalled();
});
@@ -1476,21 +1366,20 @@ describe('DiffsStoreActions', () => {
notesById: { 1: { discussion_id: '2' } },
};
- setCurrentDiffFileIdFromNote({ commit, state, rootGetters }, '1');
+ diffActions.setCurrentDiffFileIdFromNote({ commit, state, rootGetters }, '1');
expect(commit).not.toHaveBeenCalled();
});
});
describe('navigateToDiffFileIndex', () => {
- it('commits SET_CURRENT_DIFF_FILE', (done) => {
- testAction(
- navigateToDiffFileIndex,
+ it('commits SET_CURRENT_DIFF_FILE', () => {
+ return testAction(
+ diffActions.navigateToDiffFileIndex,
0,
{ diffFiles: [{ file_hash: '123' }] },
[{ type: types.SET_CURRENT_DIFF_FILE, payload: '123' }],
[],
- done,
);
});
});
@@ -1498,19 +1387,13 @@ describe('DiffsStoreActions', () => {
describe('setFileByFile', () => {
const updateUserEndpoint = 'user/prefs';
let putSpy;
- let mock;
beforeEach(() => {
- mock = new MockAdapter(axios);
putSpy = jest.spyOn(axios, 'put');
mock.onPut(updateUserEndpoint).reply(200, {});
});
- afterEach(() => {
- mock.restore();
- });
-
it.each`
value
${true}
@@ -1519,7 +1402,7 @@ describe('DiffsStoreActions', () => {
'commits SET_FILE_BY_FILE and persists the File-by-File user preference with the new value $value',
async ({ value }) => {
await testAction(
- setFileByFile,
+ diffActions.setFileByFile,
{ fileByFile: value },
{
viewDiffsFileByFile: null,
@@ -1551,7 +1434,7 @@ describe('DiffsStoreActions', () => {
const commitSpy = jest.fn();
const getterSpy = jest.fn().mockReturnValue([]);
- reviewFile(
+ diffActions.reviewFile(
{
commit: commitSpy,
getters: {
diff --git a/spec/frontend/diffs/store/utils_spec.js b/spec/frontend/diffs/store/utils_spec.js
index 55c0141552d..03bcaab0d2b 100644
--- a/spec/frontend/diffs/store/utils_spec.js
+++ b/spec/frontend/diffs/store/utils_spec.js
@@ -13,7 +13,7 @@ import {
} from '~/diffs/constants';
import * as utils from '~/diffs/store/utils';
import { MERGE_REQUEST_NOTEABLE_TYPE } from '~/notes/constants';
-import { noteableDataMock } from '../../notes/mock_data';
+import { noteableDataMock } from 'jest/notes/mock_data';
import diffFileMockData from '../mock_data/diff_file';
import { diffMetadata } from '../mock_data/diff_metadata';
diff --git a/spec/frontend/editor/components/helpers.js b/spec/frontend/editor/components/helpers.js
new file mode 100644
index 00000000000..3e6cd2a236d
--- /dev/null
+++ b/spec/frontend/editor/components/helpers.js
@@ -0,0 +1,12 @@
+import { EDITOR_TOOLBAR_RIGHT_GROUP } from '~/editor/constants';
+
+export const buildButton = (id = 'foo-bar-btn', options = {}) => {
+ return {
+ __typename: 'Item',
+ id,
+ label: options.label || 'Foo Bar Button',
+ icon: options.icon || 'foo-bar',
+ selected: options.selected || false,
+ group: options.group || EDITOR_TOOLBAR_RIGHT_GROUP,
+ };
+};
diff --git a/spec/frontend/editor/components/source_editor_toolbar_button_spec.js b/spec/frontend/editor/components/source_editor_toolbar_button_spec.js
new file mode 100644
index 00000000000..5135091af4a
--- /dev/null
+++ b/spec/frontend/editor/components/source_editor_toolbar_button_spec.js
@@ -0,0 +1,146 @@
+import Vue, { nextTick } from 'vue';
+import VueApollo from 'vue-apollo';
+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,
+ },
+ },
+ });
+
+ wrapper = shallowMount(SourceEditorToolbarButton, {
+ propsData,
+ apolloProvider: mockApollo,
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ mockApollo = null;
+ });
+
+ describe('default', () => {
+ const defaultProps = {
+ category: 'primary',
+ variant: 'default',
+ };
+ const customProps = {
+ category: 'secondary',
+ variant: 'info',
+ };
+ it('renders a default button without props', async () => {
+ createComponentWithApollo();
+ 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,
+ },
+ });
+ 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', () => {
+ const spy = jest.fn();
+ createComponentWithApollo({
+ propsData: {
+ button: {
+ onClick: spy,
+ },
+ },
+ });
+ expect(spy).not.toHaveBeenCalled();
+ findButton().vm.$emit('click');
+ expect(spy).toHaveBeenCalled();
+ });
+ it('emits the "click" event', () => {
+ createComponentWithApollo();
+ jest.spyOn(wrapper.vm, '$emit');
+ expect(wrapper.vm.$emit).not.toHaveBeenCalled();
+ findButton().vm.$emit('click');
+ 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_spec.js b/spec/frontend/editor/components/source_editor_toolbar_spec.js
new file mode 100644
index 00000000000..6e99eadbd97
--- /dev/null
+++ b/spec/frontend/editor/components/source_editor_toolbar_spec.js
@@ -0,0 +1,116 @@
+import Vue, { nextTick } from 'vue';
+import VueApollo from 'vue-apollo';
+import { GlButtonGroup } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import SourceEditorToolbar from '~/editor/components/source_editor_toolbar.vue';
+import SourceEditorToolbarButton from '~/editor/components/source_editor_toolbar_button.vue';
+import { EDITOR_TOOLBAR_LEFT_GROUP, EDITOR_TOOLBAR_RIGHT_GROUP } from '~/editor/constants';
+import getToolbarItemsQuery from '~/editor/graphql/get_items.query.graphql';
+import { buildButton } from './helpers';
+
+Vue.use(VueApollo);
+
+describe('Source Editor Toolbar', () => {
+ let wrapper;
+ let mockApollo;
+
+ const findButtons = () => wrapper.findAllComponents(SourceEditorToolbarButton);
+
+ const createApolloMockWithCache = (items = []) => {
+ mockApollo = createMockApollo();
+ mockApollo.clients.defaultClient.cache.writeQuery({
+ query: getToolbarItemsQuery,
+ data: {
+ items: {
+ nodes: items,
+ },
+ },
+ });
+ };
+
+ const createComponentWithApollo = (items = []) => {
+ createApolloMockWithCache(items);
+ wrapper = shallowMount(SourceEditorToolbar, {
+ apolloProvider: mockApollo,
+ stubs: {
+ GlButtonGroup,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ mockApollo = null;
+ });
+
+ describe('groups', () => {
+ it.each`
+ group | expectedGroup
+ ${EDITOR_TOOLBAR_LEFT_GROUP} | ${EDITOR_TOOLBAR_LEFT_GROUP}
+ ${EDITOR_TOOLBAR_RIGHT_GROUP} | ${EDITOR_TOOLBAR_RIGHT_GROUP}
+ ${undefined} | ${EDITOR_TOOLBAR_RIGHT_GROUP}
+ ${'non-existing'} | ${EDITOR_TOOLBAR_RIGHT_GROUP}
+ `('puts item with group="$group" into $expectedGroup group', ({ group, expectedGroup }) => {
+ const item = buildButton('first', {
+ group,
+ });
+ createComponentWithApollo([item]);
+ expect(findButtons()).toHaveLength(1);
+ [EDITOR_TOOLBAR_RIGHT_GROUP, EDITOR_TOOLBAR_LEFT_GROUP].forEach((g) => {
+ if (g === expectedGroup) {
+ expect(wrapper.vm.getGroupItems(g)).toEqual([expect.objectContaining({ id: 'first' })]);
+ } else {
+ expect(wrapper.vm.getGroupItems(g)).toHaveLength(0);
+ }
+ });
+ });
+ });
+
+ describe('buttons update', () => {
+ it('it properly updates buttons on Apollo cache update', async () => {
+ const item = buildButton('first', {
+ group: EDITOR_TOOLBAR_RIGHT_GROUP,
+ });
+ createComponentWithApollo();
+
+ expect(findButtons()).toHaveLength(0);
+
+ mockApollo.clients.defaultClient.cache.writeQuery({
+ query: getToolbarItemsQuery,
+ data: {
+ items: {
+ nodes: [item],
+ },
+ },
+ });
+
+ jest.runOnlyPendingTimers();
+ await nextTick();
+
+ expect(findButtons()).toHaveLength(1);
+ });
+ });
+
+ describe('click handler', () => {
+ it('emits the "click" event when a button is clicked', () => {
+ const item1 = buildButton('first', {
+ group: EDITOR_TOOLBAR_LEFT_GROUP,
+ });
+ const item2 = buildButton('second', {
+ group: EDITOR_TOOLBAR_RIGHT_GROUP,
+ });
+ createComponentWithApollo([item1, item2]);
+ jest.spyOn(wrapper.vm, '$emit');
+ expect(wrapper.vm.$emit).not.toHaveBeenCalled();
+
+ findButtons().at(0).vm.$emit('click');
+ expect(wrapper.vm.$emit).toHaveBeenCalledWith('click', item1);
+
+ findButtons().at(1).vm.$emit('click');
+ expect(wrapper.vm.$emit).toHaveBeenCalledWith('click', item2);
+
+ expect(wrapper.vm.$emit.mock.calls).toHaveLength(2);
+ });
+ });
+});
diff --git a/spec/frontend/editor/schema/ci/ci_schema_spec.js b/spec/frontend/editor/schema/ci/ci_schema_spec.js
new file mode 100644
index 00000000000..628c34a27c1
--- /dev/null
+++ b/spec/frontend/editor/schema/ci/ci_schema_spec.js
@@ -0,0 +1,90 @@
+import Ajv from 'ajv';
+import AjvFormats from 'ajv-formats';
+import CiSchema from '~/editor/schema/ci.json';
+
+// JSON POSITIVE TESTS
+import AllowFailureJson from './json_tests/positive_tests/allow_failure.json';
+import EnvironmentJson from './json_tests/positive_tests/environment.json';
+import GitlabCiDependenciesJson from './json_tests/positive_tests/gitlab-ci-dependencies.json';
+import GitlabCiJson from './json_tests/positive_tests/gitlab-ci.json';
+import InheritJson from './json_tests/positive_tests/inherit.json';
+import MultipleCachesJson from './json_tests/positive_tests/multiple-caches.json';
+import RetryJson from './json_tests/positive_tests/retry.json';
+import TerraformReportJson from './json_tests/positive_tests/terraform_report.json';
+import VariablesMixStringAndUserInputJson from './json_tests/positive_tests/variables_mix_string_and_user_input.json';
+import VariablesJson from './json_tests/positive_tests/variables.json';
+
+// JSON NEGATIVE TESTS
+import DefaultNoAdditionalPropertiesJson from './json_tests/negative_tests/default_no_additional_properties.json';
+import InheritDefaultNoAdditionalPropertiesJson from './json_tests/negative_tests/inherit_default_no_additional_properties.json';
+import JobVariablesMustNotContainObjectsJson from './json_tests/negative_tests/job_variables_must_not_contain_objects.json';
+import ReleaseAssetsLinksEmptyJson from './json_tests/negative_tests/release_assets_links_empty.json';
+import ReleaseAssetsLinksInvalidLinkTypeJson from './json_tests/negative_tests/release_assets_links_invalid_link_type.json';
+import ReleaseAssetsLinksMissingJson from './json_tests/negative_tests/release_assets_links_missing.json';
+import RetryUnknownWhenJson from './json_tests/negative_tests/retry_unknown_when.json';
+
+// YAML POSITIVE TEST
+import CacheYaml from './yaml_tests/positive_tests/cache.yml';
+import FilterYaml from './yaml_tests/positive_tests/filter.yml';
+import IncludeYaml from './yaml_tests/positive_tests/include.yml';
+import RulesYaml from './yaml_tests/positive_tests/rules.yml';
+
+// YAML NEGATIVE TEST
+import CacheNegativeYaml from './yaml_tests/negative_tests/cache.yml';
+import IncludeNegativeYaml from './yaml_tests/negative_tests/include.yml';
+
+const ajv = new Ajv({
+ strictTypes: false,
+ strictTuples: false,
+ allowMatchingProperties: true,
+});
+
+AjvFormats(ajv);
+const schema = ajv.compile(CiSchema);
+
+describe('positive tests', () => {
+ it.each(
+ Object.entries({
+ // JSON
+ AllowFailureJson,
+ EnvironmentJson,
+ GitlabCiDependenciesJson,
+ GitlabCiJson,
+ InheritJson,
+ MultipleCachesJson,
+ RetryJson,
+ TerraformReportJson,
+ VariablesMixStringAndUserInputJson,
+ VariablesJson,
+
+ // YAML
+ CacheYaml,
+ FilterYaml,
+ IncludeYaml,
+ RulesYaml,
+ }),
+ )('schema validates %s', (_, input) => {
+ expect(input).toValidateJsonSchema(schema);
+ });
+});
+
+describe('negative tests', () => {
+ it.each(
+ Object.entries({
+ // JSON
+ DefaultNoAdditionalPropertiesJson,
+ JobVariablesMustNotContainObjectsJson,
+ InheritDefaultNoAdditionalPropertiesJson,
+ ReleaseAssetsLinksEmptyJson,
+ ReleaseAssetsLinksInvalidLinkTypeJson,
+ ReleaseAssetsLinksMissingJson,
+ RetryUnknownWhenJson,
+
+ // YAML
+ CacheNegativeYaml,
+ IncludeNegativeYaml,
+ }),
+ )('schema validates %s', (_, input) => {
+ expect(input).not.toValidateJsonSchema(schema);
+ });
+});
diff --git a/spec/frontend/editor/schema/ci/json_tests/negative_tests/default_no_additional_properties.json b/spec/frontend/editor/schema/ci/json_tests/negative_tests/default_no_additional_properties.json
new file mode 100644
index 00000000000..955c19ef1ab
--- /dev/null
+++ b/spec/frontend/editor/schema/ci/json_tests/negative_tests/default_no_additional_properties.json
@@ -0,0 +1,12 @@
+{
+ "default": {
+ "secrets": {
+ "DATABASE_PASSWORD": {
+ "vault": "production/db/password"
+ }
+ },
+ "environment": {
+ "name": "test"
+ }
+ }
+}
diff --git a/spec/frontend/editor/schema/ci/json_tests/negative_tests/inherit_default_no_additional_properties.json b/spec/frontend/editor/schema/ci/json_tests/negative_tests/inherit_default_no_additional_properties.json
new file mode 100644
index 00000000000..7411e4c2434
--- /dev/null
+++ b/spec/frontend/editor/schema/ci/json_tests/negative_tests/inherit_default_no_additional_properties.json
@@ -0,0 +1,8 @@
+{
+ "karma": {
+ "inherit": {
+ "default": ["secrets"]
+ },
+ "script": "karma"
+ }
+}
diff --git a/spec/frontend/editor/schema/ci/json_tests/negative_tests/job_variables_must_not_contain_objects.json b/spec/frontend/editor/schema/ci/json_tests/negative_tests/job_variables_must_not_contain_objects.json
new file mode 100644
index 00000000000..bfdbf26ee70
--- /dev/null
+++ b/spec/frontend/editor/schema/ci/json_tests/negative_tests/job_variables_must_not_contain_objects.json
@@ -0,0 +1,12 @@
+{
+ "gitlab-ci-variables-object": {
+ "stage": "test",
+ "script": ["true"],
+ "variables": {
+ "DEPLOY_ENVIRONMENT": {
+ "value": "staging",
+ "description": "The deployment target. Change this variable to 'canary' or 'production' if needed."
+ }
+ }
+ }
+}
diff --git a/spec/frontend/editor/schema/ci/json_tests/negative_tests/release_assets_links_empty.json b/spec/frontend/editor/schema/ci/json_tests/negative_tests/release_assets_links_empty.json
new file mode 100644
index 00000000000..84a1aa14698
--- /dev/null
+++ b/spec/frontend/editor/schema/ci/json_tests/negative_tests/release_assets_links_empty.json
@@ -0,0 +1,13 @@
+{
+ "gitlab-ci-release-assets-links-empty": {
+ "script": "dostuff",
+ "stage": "deploy",
+ "release": {
+ "description": "Created using the release-cli $EXTRA_DESCRIPTION",
+ "tag_name": "$CI_COMMIT_TAG",
+ "assets": {
+ "links": []
+ }
+ }
+ }
+}
diff --git a/spec/frontend/editor/schema/ci/json_tests/negative_tests/release_assets_links_invalid_link_type.json b/spec/frontend/editor/schema/ci/json_tests/negative_tests/release_assets_links_invalid_link_type.json
new file mode 100644
index 00000000000..048911aefa3
--- /dev/null
+++ b/spec/frontend/editor/schema/ci/json_tests/negative_tests/release_assets_links_invalid_link_type.json
@@ -0,0 +1,24 @@
+{
+ "gitlab-ci-release-assets-links-invalid-link-type": {
+ "script": "dostuff",
+ "stage": "deploy",
+ "release": {
+ "description": "Created using the release-cli $EXTRA_DESCRIPTION",
+ "tag_name": "$CI_COMMIT_TAG",
+ "assets": {
+ "links": [
+ {
+ "name": "asset1",
+ "url": "https://example.com/assets/1"
+ },
+ {
+ "name": "asset2",
+ "url": "https://example.com/assets/2",
+ "filepath": "/pretty/url/1",
+ "link_type": "invalid"
+ }
+ ]
+ }
+ }
+ }
+}
diff --git a/spec/frontend/editor/schema/ci/json_tests/negative_tests/release_assets_links_missing.json b/spec/frontend/editor/schema/ci/json_tests/negative_tests/release_assets_links_missing.json
new file mode 100644
index 00000000000..6f0b5a3bff8
--- /dev/null
+++ b/spec/frontend/editor/schema/ci/json_tests/negative_tests/release_assets_links_missing.json
@@ -0,0 +1,11 @@
+{
+ "gitlab-ci-release-assets-links-missing": {
+ "script": "dostuff",
+ "stage": "deploy",
+ "release": {
+ "description": "Created using the release-cli $EXTRA_DESCRIPTION",
+ "tag_name": "$CI_COMMIT_TAG",
+ "assets": {}
+ }
+ }
+}
diff --git a/spec/frontend/editor/schema/ci/json_tests/negative_tests/retry_unknown_when.json b/spec/frontend/editor/schema/ci/json_tests/negative_tests/retry_unknown_when.json
new file mode 100644
index 00000000000..433504f52c6
--- /dev/null
+++ b/spec/frontend/editor/schema/ci/json_tests/negative_tests/retry_unknown_when.json
@@ -0,0 +1,9 @@
+{
+ "gitlab-ci-retry-object-unknown-when": {
+ "stage": "test",
+ "script": "rspec",
+ "retry": {
+ "when": "gitlab-ci-retry-object-unknown-when"
+ }
+ }
+}
diff --git a/spec/frontend/editor/schema/ci/json_tests/positive_tests/allow_failure.json b/spec/frontend/editor/schema/ci/json_tests/positive_tests/allow_failure.json
new file mode 100644
index 00000000000..44d42116c1a
--- /dev/null
+++ b/spec/frontend/editor/schema/ci/json_tests/positive_tests/allow_failure.json
@@ -0,0 +1,19 @@
+{
+ "job1": {
+ "stage": "test",
+ "script": ["execute_script_that_will_fail"],
+ "allow_failure": true
+ },
+ "job2": {
+ "script": ["exit 1"],
+ "allow_failure": {
+ "exit_codes": 137
+ }
+ },
+ "job3": {
+ "script": ["exit 137"],
+ "allow_failure": {
+ "exit_codes": [137, 255]
+ }
+ }
+}
diff --git a/spec/frontend/editor/schema/ci/json_tests/positive_tests/environment.json b/spec/frontend/editor/schema/ci/json_tests/positive_tests/environment.json
new file mode 100644
index 00000000000..0c6f7935063
--- /dev/null
+++ b/spec/frontend/editor/schema/ci/json_tests/positive_tests/environment.json
@@ -0,0 +1,75 @@
+{
+ "deploy to production 1": {
+ "stage": "deploy",
+ "script": "git push production HEAD: master",
+ "environment": "production"
+ },
+ "deploy to production 2": {
+ "stage": "deploy",
+ "script": "git push production HEAD:master",
+ "environment": {
+ "name": "production"
+ }
+ },
+ "deploy to production 3": {
+ "stage": "deploy",
+ "script": "git push production HEAD:master",
+ "environment": {
+ "name": "production",
+ "url": "https://prod.example.com"
+ }
+ },
+ "review_app 1": {
+ "stage": "deploy",
+ "script": "make deploy-app",
+ "environment": {
+ "name": "review/$CI_COMMIT_REF_NAME",
+ "url": "https://$CI_ENVIRONMENT_SLUG.example.com",
+ "on_stop": "stop_review_app"
+ }
+ },
+ "stop_review_app": {
+ "stage": "deploy",
+ "variables": {
+ "GIT_STRATEGY": "none"
+ },
+ "script": "make delete-app",
+ "when": "manual",
+ "environment": {
+ "name": "review/$CI_COMMIT_REF_NAME",
+ "action": "stop"
+ }
+ },
+ "review_app 2": {
+ "script": "deploy-review-app",
+ "environment": {
+ "name": "review/$CI_COMMIT_REF_NAME",
+ "auto_stop_in": "1 day"
+ }
+ },
+ "deploy 1": {
+ "stage": "deploy",
+ "script": "make deploy-app",
+ "environment": {
+ "name": "production",
+ "kubernetes": {
+ "namespace": "production"
+ }
+ }
+ },
+ "deploy 2": {
+ "script": "echo",
+ "environment": {
+ "name": "customer-portal",
+ "deployment_tier": "production"
+ }
+ },
+ "deploy as review app": {
+ "stage": "deploy",
+ "script": "make deploy",
+ "environment": {
+ "name": "review/$CI_COMMIT_REF_NAME",
+ "url": "https://$CI_ENVIRONMENT_SLUG.example.com/"
+ }
+ }
+}
diff --git a/spec/frontend/editor/schema/ci/json_tests/positive_tests/gitlab-ci-dependencies.json b/spec/frontend/editor/schema/ci/json_tests/positive_tests/gitlab-ci-dependencies.json
new file mode 100644
index 00000000000..5ffa7fa799e
--- /dev/null
+++ b/spec/frontend/editor/schema/ci/json_tests/positive_tests/gitlab-ci-dependencies.json
@@ -0,0 +1,68 @@
+{
+ ".build_config": {
+ "before_script": ["echo test"]
+ },
+ ".build_script": "echo build script",
+ "default": {
+ "image": "ruby:2.5",
+ "services": ["docker:dind"],
+ "cache": {
+ "paths": ["vendor/"]
+ },
+ "before_script": ["bundle install --path vendor/"],
+ "after_script": ["rm -rf tmp/"]
+ },
+ "stages": ["install", "build", "test", "deploy"],
+ "image": "foo:latest",
+ "install task1": {
+ "image": "node:latest",
+ "stage": "install",
+ "script": "npm install",
+ "artifacts": {
+ "paths": ["node_modules/"]
+ }
+ },
+ "build dev": {
+ "image": "node:latest",
+ "stage": "build",
+ "needs": [
+ {
+ "job": "install task1"
+ }
+ ],
+ "script": "npm run build:dev"
+ },
+ "build prod": {
+ "image": "node:latest",
+ "stage": "build",
+ "needs": ["install task1"],
+ "script": "npm run build:prod"
+ },
+ "test": {
+ "image": "node:latest",
+ "stage": "build",
+ "needs": [
+ "install task1",
+ {
+ "job": "build dev",
+ "artifacts": true
+ }
+ ],
+ "script": "npm run test"
+ },
+ "deploy it": {
+ "image": "node:latest",
+ "stage": "deploy",
+ "needs": [
+ {
+ "job": "build dev",
+ "artifacts": false
+ },
+ {
+ "job": "build prod",
+ "artifacts": true
+ }
+ ],
+ "script": "npm run test"
+ }
+}
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
new file mode 100644
index 00000000000..89420bbc35f
--- /dev/null
+++ b/spec/frontend/editor/schema/ci/json_tests/positive_tests/gitlab-ci.json
@@ -0,0 +1,350 @@
+{
+ ".build_config": {
+ "before_script": ["echo test"]
+ },
+ ".build_script": "echo build script",
+ ".example_variables": {
+ "foo": "hello",
+ "bar": 42
+ },
+ ".example_services": [
+ "docker:dind",
+ {
+ "name": "sql:latest",
+ "command": ["/usr/bin/super-sql", "run"]
+ }
+ ],
+ "default": {
+ "image": "ruby:2.5",
+ "services": ["docker:dind"],
+ "cache": {
+ "paths": ["vendor/"]
+ },
+ "before_script": ["bundle install --path vendor/"],
+ "after_script": ["rm -rf tmp/"],
+ "tags": ["ruby", "postgres"],
+ "artifacts": {
+ "name": "%CI_COMMIT_REF_NAME%",
+ "expose_as": "artifact 1",
+ "paths": ["path/to/file.txt", "target/*.war"],
+ "when": "on_failure"
+ },
+ "retry": 2,
+ "timeout": "3 hours 30 minutes",
+ "interruptible": true
+ },
+ "stages": ["build", "test", "deploy", "random"],
+ "image": "foo:latest",
+ "services": ["sql:latest"],
+ "before_script": ["echo test", "echo test2"],
+ "after_script": [],
+ "cache": {
+ "key": "asd",
+ "paths": ["dist/", ".foo"],
+ "untracked": false,
+ "policy": "pull"
+ },
+ "variables": {
+ "STAGE": "yep",
+ "PROD": "nope"
+ },
+ "include": [
+ "https://gitlab.com/awesome-project/raw/master/.before-script-template.yml",
+ "/templates/.after-script-template.yml",
+ { "template": "Auto-DevOps.gitlab-ci.yml" },
+ {
+ "project": "my-group/my-project",
+ "ref": "master",
+ "file": "/templates/.gitlab-ci-template.yml"
+ },
+ {
+ "project": "my-group/my-project",
+ "ref": "master",
+ "file": ["/templates/.gitlab-ci-template.yml", "/templates/another-template-to-include.yml"]
+ }
+ ],
+ "build": {
+ "image": {
+ "name": "node:latest"
+ },
+ "services": [],
+ "stage": "build",
+ "script": "npm run build",
+ "before_script": ["npm install"],
+ "rules": [
+ {
+ "if": "moo",
+ "changes": ["Moofile"],
+ "exists": ["main.cow"],
+ "when": "delayed",
+ "start_in": "3 hours"
+ }
+ ],
+ "retry": {
+ "max": 1,
+ "when": "stuck_or_timeout_failure"
+ },
+ "cache": {
+ "key": "$CI_COMMIT_REF_NAME",
+ "paths": ["node_modules/"],
+ "policy": "pull-push"
+ },
+ "artifacts": {
+ "paths": ["dist/"],
+ "expose_as": "link_name_in_merge_request",
+ "name": "bundles",
+ "when": "on_success",
+ "expire_in": "1 week",
+ "reports": {
+ "junit": "result.xml",
+ "cobertura": "cobertura-coverage.xml",
+ "codequality": "codequality.json",
+ "sast": "sast.json",
+ "dependency_scanning": "scan.json",
+ "container_scanning": "scan2.json",
+ "dast": "dast.json",
+ "license_management": "license.json",
+ "performance": "performance.json",
+ "metrics": "metrics.txt"
+ }
+ },
+ "variables": {
+ "FOO_BAR": "..."
+ },
+ "only": {
+ "kubernetes": "active",
+ "variables": ["$FOO_BAR == '...'"],
+ "changes": ["/path/to/file", "/another/file"]
+ },
+ "except": ["master", "tags"],
+ "tags": ["docker"],
+ "allow_failure": true,
+ "when": "manual"
+ },
+ "error-report": {
+ "when": "on_failure",
+ "script": "report error",
+ "stage": "test"
+ },
+ "test": {
+ "image": {
+ "name": "node:latest",
+ "entrypoint": [""]
+ },
+ "stage": "test",
+ "script": "npm test",
+ "parallel": 5,
+ "retry": {
+ "max": 2,
+ "when": [
+ "runner_system_failure",
+ "stuck_or_timeout_failure",
+ "script_failure",
+ "unknown_failure",
+ "always"
+ ]
+ },
+ "artifacts": {
+ "reports": {
+ "junit": ["result.xml"],
+ "cobertura": ["cobertura-coverage.xml"],
+ "codequality": ["codequality.json"],
+ "sast": ["sast.json"],
+ "dependency_scanning": ["scan.json"],
+ "container_scanning": ["scan2.json"],
+ "dast": ["dast.json"],
+ "license_management": ["license.json"],
+ "performance": ["performance.json"],
+ "metrics": ["metrics.txt"]
+ }
+ },
+ "coverage": "/Cycles: \\d+\\.\\d+$/",
+ "dependencies": []
+ },
+ "docker": {
+ "script": "docker build -t foo:latest",
+ "when": "delayed",
+ "start_in": "10 min",
+ "timeout": "1h",
+ "retry": 1,
+ "only": {
+ "changes": ["Dockerfile", "docker/scripts/*"]
+ }
+ },
+ "deploy": {
+ "services": [
+ {
+ "name": "sql:latest",
+ "entrypoint": [""],
+ "command": ["/usr/bin/super-sql", "run"],
+ "alias": "super-sql"
+ },
+ "sql:latest",
+ {
+ "name": "sql:latest",
+ "alias": "default-sql"
+ }
+ ],
+ "script": "dostuff",
+ "stage": "deploy",
+ "environment": {
+ "name": "prod",
+ "url": "http://example.com",
+ "on_stop": "stop-deploy"
+ },
+ "only": ["master"],
+ "release": {
+ "name": "Release $CI_COMMIT_TAG",
+ "description": "Created using the release-cli $EXTRA_DESCRIPTION",
+ "tag_name": "$CI_COMMIT_TAG",
+ "ref": "$CI_COMMIT_TAG",
+ "milestones": ["m1", "m2", "m3"],
+ "released_at": "2020-07-15T08:00:00Z",
+ "assets": {
+ "links": [
+ {
+ "name": "asset1",
+ "url": "https://example.com/assets/1"
+ },
+ {
+ "name": "asset2",
+ "url": "https://example.com/assets/2",
+ "filepath": "/pretty/url/1",
+ "link_type": "other"
+ },
+ {
+ "name": "asset3",
+ "url": "https://example.com/assets/3",
+ "link_type": "runbook"
+ },
+ {
+ "name": "asset4",
+ "url": "https://example.com/assets/4",
+ "link_type": "package"
+ },
+ {
+ "name": "asset5",
+ "url": "https://example.com/assets/5",
+ "link_type": "image"
+ }
+ ]
+ }
+ }
+ },
+ ".performance-tmpl": {
+ "after_script": ["echo after"],
+ "before_script": ["echo before"],
+ "variables": {
+ "SCRIPT_NOT_REQUIRED": "true"
+ }
+ },
+ "performance-a": {
+ "extends": ".performance-tmpl",
+ "script": "echo test"
+ },
+ "performance-b": {
+ "extends": ".performance-tmpl"
+ },
+ "workflow": {
+ "rules": [
+ {
+ "if": "$CI_COMMIT_REF_NAME =~ /-wip$/",
+ "when": "never"
+ },
+ {
+ "if": "$CI_COMMIT_TAG",
+ "when": "never"
+ },
+ {
+ "when": "always"
+ }
+ ]
+ },
+ "job": {
+ "script": "echo Hello, Rules!",
+ "rules": [
+ {
+ "if": "$CI_MERGE_REQUEST_TARGET_BRANCH_NAME == \"master\"",
+ "when": "manual",
+ "allow_failure": true
+ }
+ ]
+ },
+ "microservice_a": {
+ "trigger": {
+ "include": "path/to/microservice_a.yml"
+ }
+ },
+ "microservice_b": {
+ "trigger": {
+ "include": [{ "local": "path/to/microservice_b.yml" }, { "template": "SAST.gitlab-cy.yml" }],
+ "strategy": "depend"
+ }
+ },
+ "child-pipeline": {
+ "stage": "test",
+ "trigger": {
+ "include": [
+ {
+ "artifact": "generated-config.yml",
+ "job": "generate-config"
+ }
+ ]
+ }
+ },
+ "child-pipeline-simple": {
+ "stage": "test",
+ "trigger": {
+ "include": "other/file.yml"
+ }
+ },
+ "complex": {
+ "stage": "deploy",
+ "trigger": {
+ "project": "my/deployment",
+ "branch": "stable"
+ }
+ },
+ "parallel-integer": {
+ "stage": "test",
+ "script": ["echo ${CI_NODE_INDEX} ${CI_NODE_TOTAL}"],
+ "parallel": 5
+ },
+ "parallel-matrix-simple": {
+ "stage": "test",
+ "script": ["echo ${MY_VARIABLE}"],
+ "parallel": {
+ "matrix": [
+ {
+ "MY_VARIABLE": 0
+ },
+ {
+ "MY_VARIABLE": "sample"
+ },
+ {
+ "MY_VARIABLE": ["element0", 1, "element2"]
+ }
+ ]
+ }
+ },
+ "parallel-matrix-gitlab-docs": {
+ "stage": "deploy",
+ "script": ["bin/deploy"],
+ "parallel": {
+ "matrix": [
+ {
+ "PROVIDER": "aws",
+ "STACK": ["app1", "app2"]
+ },
+ {
+ "PROVIDER": "ovh",
+ "STACK": ["monitoring", "backup", "app"]
+ },
+ {
+ "PROVIDER": ["gcp", "vultr"],
+ "STACK": ["data", "processing"]
+ }
+ ]
+ }
+ }
+}
diff --git a/spec/frontend/editor/schema/ci/json_tests/positive_tests/inherit.json b/spec/frontend/editor/schema/ci/json_tests/positive_tests/inherit.json
new file mode 100644
index 00000000000..3f72afa6ceb
--- /dev/null
+++ b/spec/frontend/editor/schema/ci/json_tests/positive_tests/inherit.json
@@ -0,0 +1,54 @@
+{
+ "default": {
+ "image": "ruby:2.4",
+ "before_script": ["echo Hello World"]
+ },
+ "variables": {
+ "DOMAIN": "example.com",
+ "WEBHOOK_URL": "https://my-webhook.example.com"
+ },
+ "rubocop": {
+ "inherit": {
+ "default": false,
+ "variables": false
+ },
+ "script": "bundle exec rubocop"
+ },
+ "rspec": {
+ "inherit": {
+ "default": ["image"],
+ "variables": ["WEBHOOK_URL"]
+ },
+ "script": "bundle exec rspec"
+ },
+ "capybara": {
+ "inherit": {
+ "variables": false
+ },
+ "script": "bundle exec capybara"
+ },
+ "karma": {
+ "inherit": {
+ "default": true,
+ "variables": ["DOMAIN"]
+ },
+ "script": "karma"
+ },
+ "inherit literally all": {
+ "inherit": {
+ "default": [
+ "after_script",
+ "artifacts",
+ "before_script",
+ "cache",
+ "image",
+ "interruptible",
+ "retry",
+ "services",
+ "tags",
+ "timeout"
+ ]
+ },
+ "script": "true"
+ }
+}
diff --git a/spec/frontend/editor/schema/ci/json_tests/positive_tests/multiple-caches.json b/spec/frontend/editor/schema/ci/json_tests/positive_tests/multiple-caches.json
new file mode 100644
index 00000000000..360938e5ce7
--- /dev/null
+++ b/spec/frontend/editor/schema/ci/json_tests/positive_tests/multiple-caches.json
@@ -0,0 +1,24 @@
+{
+ "test-job": {
+ "stage": "build",
+ "cache": [
+ {
+ "key": {
+ "files": ["Gemfile.lock"]
+ },
+ "paths": ["vendor/ruby"]
+ },
+ {
+ "key": {
+ "files": ["yarn.lock"]
+ },
+ "paths": [".yarn-cache/"]
+ }
+ ],
+ "script": [
+ "bundle install --path=vendor",
+ "yarn install --cache-folder .yarn-cache",
+ "echo Run tests..."
+ ]
+ }
+}
diff --git a/spec/frontend/editor/schema/ci/json_tests/positive_tests/retry.json b/spec/frontend/editor/schema/ci/json_tests/positive_tests/retry.json
new file mode 100644
index 00000000000..1337e5e7bc8
--- /dev/null
+++ b/spec/frontend/editor/schema/ci/json_tests/positive_tests/retry.json
@@ -0,0 +1,60 @@
+{
+ "gitlab-ci-retry-int": {
+ "stage": "test",
+ "script": "rspec",
+ "retry": 2
+ },
+ "gitlab-ci-retry-object-no-max": {
+ "stage": "test",
+ "script": "rspec",
+ "retry": {
+ "when": "runner_system_failure"
+ }
+ },
+ "gitlab-ci-retry-object-single-when": {
+ "stage": "test",
+ "script": "rspec",
+ "retry": {
+ "max": 2,
+ "when": "runner_system_failure"
+ }
+ },
+ "gitlab-ci-retry-object-multiple-when": {
+ "stage": "test",
+ "script": "rspec",
+ "retry": {
+ "max": 2,
+ "when": ["runner_system_failure", "stuck_or_timeout_failure"]
+ }
+ },
+ "gitlab-ci-retry-object-multiple-when-dupes": {
+ "stage": "test",
+ "script": "rspec",
+ "retry": {
+ "max": 2,
+ "when": ["runner_system_failure", "runner_system_failure"]
+ }
+ },
+ "gitlab-ci-retry-object-all-when": {
+ "stage": "test",
+ "script": "rspec",
+ "retry": {
+ "max": 2,
+ "when": [
+ "always",
+ "unknown_failure",
+ "script_failure",
+ "api_failure",
+ "stuck_or_timeout_failure",
+ "runner_system_failure",
+ "runner_unsupported",
+ "stale_schedule",
+ "job_execution_timeout",
+ "archived_failure",
+ "unmet_prerequisites",
+ "scheduler_failure",
+ "data_integrity_failure"
+ ]
+ }
+ }
+}
diff --git a/spec/frontend/editor/schema/ci/json_tests/positive_tests/terraform_report.json b/spec/frontend/editor/schema/ci/json_tests/positive_tests/terraform_report.json
new file mode 100644
index 00000000000..0e444a4ba62
--- /dev/null
+++ b/spec/frontend/editor/schema/ci/json_tests/positive_tests/terraform_report.json
@@ -0,0 +1,50 @@
+{
+ "image": {
+ "name": "registry.gitlab.com/gitlab-org/gitlab-build-images:terraform",
+ "entrypoint": [
+ "/usr/bin/env",
+ "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
+ ]
+ },
+ "variables": {
+ "PLAN": "plan.tfplan",
+ "JSON_PLAN_FILE": "tfplan.json"
+ },
+ "cache": {
+ "paths": [".terraform"]
+ },
+ "before_script": [
+ "alias convert_report=\"jq -r '([.resource_changes[]?.change.actions?]|flatten)|{\"create\":(map(select(.==\"create\"))|length),\"update\":(map(select(.==\"update\"))|length),\"delete\":(map(select(.==\"delete\"))|length)}'\"",
+ "terraform --version",
+ "terraform init"
+ ],
+ "stages": ["validate", "build", "test", "deploy"],
+ "validate": {
+ "stage": "validate",
+ "script": ["terraform validate"]
+ },
+ "plan": {
+ "stage": "build",
+ "script": [
+ "terraform plan -out=$PLAN",
+ "terraform show --json $PLAN | convert_report > $JSON_PLAN_FILE"
+ ],
+ "artifacts": {
+ "name": "plan",
+ "paths": ["$PLAN"],
+ "reports": {
+ "terraform": "$JSON_PLAN_FILE"
+ }
+ }
+ },
+ "apply": {
+ "stage": "deploy",
+ "environment": {
+ "name": "production"
+ },
+ "script": ["terraform apply -input=false $PLAN"],
+ "dependencies": ["plan"],
+ "when": "manual",
+ "only": ["master"]
+ }
+}
diff --git a/spec/frontend/editor/schema/ci/json_tests/positive_tests/variables.json b/spec/frontend/editor/schema/ci/json_tests/positive_tests/variables.json
new file mode 100644
index 00000000000..ce59b3fbbec
--- /dev/null
+++ b/spec/frontend/editor/schema/ci/json_tests/positive_tests/variables.json
@@ -0,0 +1,22 @@
+{
+ "variables": {
+ "DEPLOY_ENVIRONMENT": {
+ "value": "staging",
+ "description": "The deployment target. Change this variable to 'canary' or 'production' if needed."
+ }
+ },
+ "gitlab-ci-variables-string": {
+ "stage": "test",
+ "script": ["true"],
+ "variables": {
+ "TEST_VAR": "String variable"
+ }
+ },
+ "gitlab-ci-variables-integer": {
+ "stage": "test",
+ "script": ["true"],
+ "variables": {
+ "canonical": 685230
+ }
+ }
+}
diff --git a/spec/frontend/editor/schema/ci/json_tests/positive_tests/variables_mix_string_and_user_input.json b/spec/frontend/editor/schema/ci/json_tests/positive_tests/variables_mix_string_and_user_input.json
new file mode 100644
index 00000000000..87a9ec05b57
--- /dev/null
+++ b/spec/frontend/editor/schema/ci/json_tests/positive_tests/variables_mix_string_and_user_input.json
@@ -0,0 +1,10 @@
+{
+ "variables": {
+ "SOME_STR": "--batch-mode --errors --fail-at-end --show-version",
+ "SOME_INT": 10,
+ "SOME_USER_INPUT_FLAG": {
+ "value": "flag value",
+ "description": "Some Flag!"
+ }
+ }
+}
diff --git a/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/cache.yml b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/cache.yml
new file mode 100644
index 00000000000..ee533f54d3b
--- /dev/null
+++ b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/cache.yml
@@ -0,0 +1,15 @@
+# Covers https://gitlab.com/gitlab-org/gitlab/-/merge_requests/70779
+stages:
+ - prepare
+
+# invalid cache:when value
+job1:
+ stage: prepare
+ cache:
+ when: 0
+
+# invalid cache:when value
+job2:
+ stage: prepare
+ cache:
+ when: 'never'
diff --git a/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/include.yml b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/include.yml
new file mode 100644
index 00000000000..287150a765f
--- /dev/null
+++ b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/include.yml
@@ -0,0 +1,17 @@
+# Covers https://gitlab.com/gitlab-org/gitlab/-/merge_requests/70779
+stages:
+ - prepare
+
+# missing file property
+childPipeline:
+ stage: prepare
+ trigger:
+ include:
+ - project: 'my-group/my-pipeline-library'
+
+# missing project property
+childPipeline2:
+ stage: prepare
+ trigger:
+ include:
+ - file: '.gitlab-ci.yml'
diff --git a/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/cache.yml b/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/cache.yml
new file mode 100644
index 00000000000..436c7d72699
--- /dev/null
+++ b/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/cache.yml
@@ -0,0 +1,25 @@
+# Covers https://gitlab.com/gitlab-org/gitlab/-/merge_requests/70779
+stages:
+ - prepare
+
+# test for cache:when values
+job1:
+ stage: prepare
+ script:
+ - echo 'running job'
+ cache:
+ when: 'on_success'
+
+job2:
+ stage: prepare
+ script:
+ - echo 'running job'
+ cache:
+ when: 'on_failure'
+
+job3:
+ stage: prepare
+ script:
+ - echo 'running job'
+ cache:
+ when: 'always'
diff --git a/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/filter.yml b/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/filter.yml
new file mode 100644
index 00000000000..2b29c24fa3c
--- /dev/null
+++ b/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/filter.yml
@@ -0,0 +1,18 @@
+# Covers https://gitlab.com/gitlab-org/gitlab/-/merge_requests/79335
+deploy-template:
+ script:
+ - echo "hello world"
+ only:
+ - foo
+ except:
+ - bar
+
+# null value allowed
+deploy-without-only:
+ extends: deploy-template
+ only:
+
+# null value allowed
+deploy-without-except:
+ extends: deploy-template
+ except:
diff --git a/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/include.yml b/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/include.yml
new file mode 100644
index 00000000000..3497be28058
--- /dev/null
+++ b/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/include.yml
@@ -0,0 +1,32 @@
+# Covers https://gitlab.com/gitlab-org/gitlab/-/merge_requests/70779
+
+# test for include:rules
+include:
+ - local: builds.yml
+ rules:
+ - if: '$INCLUDE_BUILDS == "true"'
+ when: always
+
+stages:
+ - prepare
+
+# test for trigger:include
+childPipeline:
+ stage: prepare
+ script:
+ - echo 'creating pipeline...'
+ trigger:
+ include:
+ - project: 'my-group/my-pipeline-library'
+ file: '.gitlab-ci.yml'
+
+# accepts optional ref property
+childPipeline2:
+ stage: prepare
+ script:
+ - echo 'creating pipeline...'
+ trigger:
+ include:
+ - project: 'my-group/my-pipeline-library'
+ file: '.gitlab-ci.yml'
+ ref: 'main'
diff --git a/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/rules.yml b/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/rules.yml
new file mode 100644
index 00000000000..27a199cff13
--- /dev/null
+++ b/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/rules.yml
@@ -0,0 +1,13 @@
+# Covers https://gitlab.com/gitlab-org/gitlab/-/merge_requests/74164
+
+# test for workflow:rules:changes and workflow:rules:exists
+workflow:
+ rules:
+ - if: '$CI_PIPELINE_SOURCE == "schedule"'
+ exists:
+ - Dockerfile
+ changes:
+ - Dockerfile
+ variables:
+ IS_A_FEATURE: 'true'
+ when: always
diff --git a/spec/frontend/environments/deploy_board_component_spec.js b/spec/frontend/environments/deploy_board_component_spec.js
index f0fb4d1027c..6bf87f7b07f 100644
--- a/spec/frontend/environments/deploy_board_component_spec.js
+++ b/spec/frontend/environments/deploy_board_component_spec.js
@@ -23,9 +23,9 @@ describe('Deploy Board', () => {
});
describe('with valid data', () => {
- beforeEach((done) => {
+ beforeEach(() => {
wrapper = createComponent();
- nextTick(done);
+ return nextTick();
});
it('should render percentage with completion value provided', () => {
@@ -127,14 +127,14 @@ describe('Deploy Board', () => {
});
describe('with empty state', () => {
- beforeEach((done) => {
+ beforeEach(() => {
wrapper = createComponent({
deployBoardData: {},
isLoading: false,
isEmpty: true,
logsPath,
});
- nextTick(done);
+ return nextTick();
});
it('should render the empty state', () => {
@@ -146,14 +146,14 @@ describe('Deploy Board', () => {
});
describe('with loading state', () => {
- beforeEach((done) => {
+ beforeEach(() => {
wrapper = createComponent({
deployBoardData: {},
isLoading: true,
isEmpty: false,
logsPath,
});
- nextTick(done);
+ return nextTick();
});
it('should render loading spinner', () => {
@@ -163,7 +163,7 @@ describe('Deploy Board', () => {
describe('has legend component', () => {
let statuses = [];
- beforeEach((done) => {
+ beforeEach(() => {
wrapper = createComponent({
isLoading: false,
isEmpty: false,
@@ -171,7 +171,7 @@ describe('Deploy Board', () => {
deployBoardData: deployBoardMockData,
});
({ statuses } = wrapper.vm);
- nextTick(done);
+ return nextTick();
});
it('with all the possible statuses', () => {
diff --git a/spec/frontend/environments/empty_state_spec.js b/spec/frontend/environments/empty_state_spec.js
new file mode 100644
index 00000000000..974afc6d032
--- /dev/null
+++ b/spec/frontend/environments/empty_state_spec.js
@@ -0,0 +1,53 @@
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import { s__ } from '~/locale';
+import EmptyState from '~/environments/components/empty_state.vue';
+import { ENVIRONMENTS_SCOPE } from '~/environments/constants';
+
+const HELP_PATH = '/help';
+
+describe('~/environments/components/empty_state.vue', () => {
+ let wrapper;
+
+ const createWrapper = ({ propsData = {} } = {}) =>
+ mountExtended(EmptyState, {
+ propsData: {
+ scope: ENVIRONMENTS_SCOPE.AVAILABLE,
+ helpPath: HELP_PATH,
+ ...propsData,
+ },
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('shows an empty state for available environments', () => {
+ wrapper = createWrapper();
+
+ const title = wrapper.findByRole('heading', {
+ name: s__("Environments|You don't have any environments."),
+ });
+
+ expect(title.exists()).toBe(true);
+ });
+
+ it('shows an empty state for stopped environments', () => {
+ wrapper = createWrapper({ propsData: { scope: ENVIRONMENTS_SCOPE.STOPPED } });
+
+ const title = wrapper.findByRole('heading', {
+ name: s__("Environments|You don't have any stopped environments."),
+ });
+
+ expect(title.exists()).toBe(true);
+ });
+
+ it('shows a link to the the help path', () => {
+ wrapper = createWrapper();
+
+ const link = wrapper.findByRole('link', {
+ name: s__('Environments|How do I create an environment?'),
+ });
+
+ expect(link.attributes('href')).toBe(HELP_PATH);
+ });
+});
diff --git a/spec/frontend/environments/emtpy_state_spec.js b/spec/frontend/environments/emtpy_state_spec.js
deleted file mode 100644
index 862d90e50dc..00000000000
--- a/spec/frontend/environments/emtpy_state_spec.js
+++ /dev/null
@@ -1,24 +0,0 @@
-import { shallowMount } from '@vue/test-utils';
-import EmptyState from '~/environments/components/empty_state.vue';
-
-describe('environments empty state', () => {
- let vm;
-
- beforeEach(() => {
- vm = shallowMount(EmptyState, {
- propsData: {
- helpPath: 'bar',
- },
- });
- });
-
- afterEach(() => {
- vm.destroy();
- });
-
- it('renders the empty state', () => {
- expect(vm.find('.js-blank-state-title').text()).toEqual(
- "You don't have any environments right now",
- );
- });
-});
diff --git a/spec/frontend/environments/environment_item_spec.js b/spec/frontend/environments/environment_item_spec.js
index 0b36d2a940d..0761d04229c 100644
--- a/spec/frontend/environments/environment_item_spec.js
+++ b/spec/frontend/environments/environment_item_spec.js
@@ -1,4 +1,5 @@
import { mount } from '@vue/test-utils';
+import { GlAvatarLink, GlAvatar } from '@gitlab/ui';
import { cloneDeep } from 'lodash';
import { format } from 'timeago.js';
import { mockTracking, unmockTracking, triggerEvent } from 'helpers/tracking_helper';
@@ -44,10 +45,16 @@ describe('Environment item', () => {
const findAutoStop = () => wrapper.find('.js-auto-stop');
const findUpcomingDeployment = () => wrapper.find('[data-testid="upcoming-deployment"]');
+ const findLastDeployment = () => wrapper.find('[data-testid="environment-deployment-id-cell"]');
const findUpcomingDeploymentContent = () =>
wrapper.find('[data-testid="upcoming-deployment-content"]');
const findUpcomingDeploymentStatusLink = () =>
wrapper.find('[data-testid="upcoming-deployment-status-link"]');
+ const findLastDeploymentAvatarLink = () => findLastDeployment().findComponent(GlAvatarLink);
+ const findLastDeploymentAvatar = () => findLastDeployment().findComponent(GlAvatar);
+ const findUpcomingDeploymentAvatarLink = () =>
+ findUpcomingDeployment().findComponent(GlAvatarLink);
+ const findUpcomingDeploymentAvatar = () => findUpcomingDeployment().findComponent(GlAvatar);
afterEach(() => {
wrapper.destroy();
@@ -79,9 +86,19 @@ describe('Environment item', () => {
describe('With user information', () => {
it('should render user avatar with link to profile', () => {
- expect(wrapper.find('.js-deploy-user-container').props('linkHref')).toEqual(
- environment.last_deployment.user.web_url,
- );
+ const avatarLink = findLastDeploymentAvatarLink();
+ const avatar = findLastDeploymentAvatar();
+ const { username, avatar_url, web_url } = environment.last_deployment.user;
+
+ expect(avatarLink.attributes('href')).toBe(web_url);
+ expect(avatar.props()).toMatchObject({
+ src: avatar_url,
+ entityName: username,
+ });
+ expect(avatar.attributes()).toMatchObject({
+ title: username,
+ alt: `${username}'s avatar`,
+ });
});
});
@@ -108,9 +125,16 @@ describe('Environment item', () => {
describe('When the envionment has an upcoming deployment', () => {
describe('When the upcoming deployment has a deployable', () => {
it('should render the build ID and user', () => {
- expect(findUpcomingDeploymentContent().text()).toMatchInterpolatedText(
- '#27 by upcoming-username',
- );
+ const avatarLink = findUpcomingDeploymentAvatarLink();
+ const avatar = findUpcomingDeploymentAvatar();
+ const { username, avatar_url, web_url } = environment.upcoming_deployment.user;
+
+ expect(findUpcomingDeploymentContent().text()).toMatchInterpolatedText('#27 by');
+ expect(avatarLink.attributes('href')).toBe(web_url);
+ expect(avatar.props()).toMatchObject({
+ src: avatar_url,
+ entityName: username,
+ });
});
it('should render a status icon with a link and tooltip', () => {
@@ -139,10 +163,17 @@ describe('Environment item', () => {
});
});
- it('should still renders the build ID and user', () => {
- expect(findUpcomingDeploymentContent().text()).toMatchInterpolatedText(
- '#27 by upcoming-username',
- );
+ it('should still render the build ID and user avatar', () => {
+ const avatarLink = findUpcomingDeploymentAvatarLink();
+ const avatar = findUpcomingDeploymentAvatar();
+ const { username, avatar_url, web_url } = environment.upcoming_deployment.user;
+
+ expect(findUpcomingDeploymentContent().text()).toMatchInterpolatedText('#27 by');
+ expect(avatarLink.attributes('href')).toBe(web_url);
+ expect(avatar.props()).toMatchObject({
+ src: avatar_url,
+ entityName: username,
+ });
});
it('should not render the status icon', () => {
@@ -383,7 +414,7 @@ describe('Environment item', () => {
});
it('should hide non-folder properties', () => {
- expect(wrapper.find('[data-testid="environment-deployment-id-cell"]').exists()).toBe(false);
+ expect(findLastDeployment().exists()).toBe(false);
expect(wrapper.find('[data-testid="environment-build-cell"]').exists()).toBe(false);
});
});
diff --git a/spec/frontend/environments/environment_table_spec.js b/spec/frontend/environments/environment_table_spec.js
index c7582e4b06d..666e87c748e 100644
--- a/spec/frontend/environments/environment_table_spec.js
+++ b/spec/frontend/environments/environment_table_spec.js
@@ -122,7 +122,7 @@ describe('Environment table', () => {
expect(wrapper.find('.deploy-board-icon').exists()).toBe(true);
});
- it('should toggle deploy board visibility when arrow is clicked', (done) => {
+ it('should toggle deploy board visibility when arrow is clicked', async () => {
const mockItem = {
name: 'review',
size: 1,
@@ -142,7 +142,6 @@ describe('Environment table', () => {
eventHub.$on('toggleDeployBoard', (env) => {
expect(env.id).toEqual(mockItem.id);
- done();
});
factory({
@@ -154,7 +153,7 @@ describe('Environment table', () => {
},
});
- wrapper.find('.deploy-board-icon').trigger('click');
+ await wrapper.find('.deploy-board-icon').trigger('click');
});
it('should set the environment to change and weight when a change canary weight event is recevied', async () => {
diff --git a/spec/frontend/environments/graphql/mock_data.js b/spec/frontend/environments/graphql/mock_data.js
index 1b7b35702de..7e436476a8f 100644
--- a/spec/frontend/environments/graphql/mock_data.js
+++ b/spec/frontend/environments/graphql/mock_data.js
@@ -543,6 +543,7 @@ export const resolvedEnvironment = {
externalUrl: 'https://example.org',
environmentType: 'review',
nameWithoutType: 'hello',
+ tier: 'development',
lastDeployment: {
id: 78,
iid: 24,
@@ -551,6 +552,7 @@ export const resolvedEnvironment = {
status: 'success',
createdAt: '2022-01-07T15:47:27.415Z',
deployedAt: '2022-01-07T15:47:32.450Z',
+ tierInYaml: 'staging',
tag: false,
isLast: true,
user: {
diff --git a/spec/frontend/environments/new_environment_item_spec.js b/spec/frontend/environments/new_environment_item_spec.js
index 1d7a33fb95b..cf0c8a7e7ca 100644
--- a/spec/frontend/environments/new_environment_item_spec.js
+++ b/spec/frontend/environments/new_environment_item_spec.js
@@ -73,6 +73,34 @@ describe('~/environments/components/new_environment_item.vue', () => {
expect(name.text()).toHaveLength(80);
});
+ describe('tier', () => {
+ it('displays the tier of the environment when defined in yaml', () => {
+ wrapper = createWrapper({ apolloProvider: createApolloProvider() });
+
+ const tier = wrapper.findByTitle(s__('Environment|Deployment tier'));
+
+ expect(tier.text()).toBe(resolvedEnvironment.lastDeployment.tierInYaml);
+ });
+
+ it('does not display the tier if not defined in yaml', () => {
+ const environment = {
+ ...resolvedEnvironment,
+ lastDeployment: {
+ ...resolvedEnvironment.lastDeployment,
+ tierInYaml: null,
+ },
+ };
+ wrapper = createWrapper({
+ propsData: { environment },
+ apolloProvider: createApolloProvider(),
+ });
+
+ const tier = wrapper.findByTitle(s__('Environment|Deployment tier'));
+
+ expect(tier.exists()).toBe(false);
+ });
+ });
+
describe('url', () => {
it('shows a link for the url if one is present', () => {
wrapper = createWrapper({ apolloProvider: createApolloProvider() });
diff --git a/spec/frontend/error_tracking/store/actions_spec.js b/spec/frontend/error_tracking/store/actions_spec.js
index aaaa1194a29..6bac21341a7 100644
--- a/spec/frontend/error_tracking/store/actions_spec.js
+++ b/spec/frontend/error_tracking/store/actions_spec.js
@@ -28,9 +28,9 @@ describe('Sentry common store actions', () => {
const params = { endpoint, redirectUrl, status };
describe('updateStatus', () => {
- it('should handle successful status update', (done) => {
+ it('should handle successful status update', async () => {
mock.onPut().reply(200, {});
- testAction(
+ await testAction(
actions.updateStatus,
params,
{},
@@ -41,20 +41,15 @@ describe('Sentry common store actions', () => {
},
],
[],
- () => {
- done();
- expect(visitUrl).toHaveBeenCalledWith(redirectUrl);
- },
);
+ expect(visitUrl).toHaveBeenCalledWith(redirectUrl);
});
- it('should handle unsuccessful status update', (done) => {
+ it('should handle unsuccessful status update', async () => {
mock.onPut().reply(400, {});
- testAction(actions.updateStatus, params, {}, [], [], () => {
- expect(visitUrl).not.toHaveBeenCalled();
- expect(createFlash).toHaveBeenCalledTimes(1);
- done();
- });
+ await testAction(actions.updateStatus, params, {}, [], []);
+ expect(visitUrl).not.toHaveBeenCalled();
+ expect(createFlash).toHaveBeenCalledTimes(1);
});
});
diff --git a/spec/frontend/error_tracking/store/details/actions_spec.js b/spec/frontend/error_tracking/store/details/actions_spec.js
index 623cb82851d..a3a6f7cc309 100644
--- a/spec/frontend/error_tracking/store/details/actions_spec.js
+++ b/spec/frontend/error_tracking/store/details/actions_spec.js
@@ -28,10 +28,10 @@ describe('Sentry error details store actions', () => {
describe('startPollingStacktrace', () => {
const endpoint = '123/stacktrace';
- it('should commit SET_ERROR with received response', (done) => {
+ it('should commit SET_ERROR with received response', () => {
const payload = { error: [1, 2, 3] };
mockedAdapter.onGet().reply(200, payload);
- testAction(
+ return testAction(
actions.startPollingStacktrace,
{ endpoint },
{},
@@ -40,37 +40,29 @@ describe('Sentry error details store actions', () => {
{ type: types.SET_LOADING_STACKTRACE, payload: false },
],
[],
- () => {
- done();
- },
);
});
- it('should show flash on API error', (done) => {
+ it('should show flash on API error', async () => {
mockedAdapter.onGet().reply(400);
- testAction(
+ await testAction(
actions.startPollingStacktrace,
{ endpoint },
{},
[{ type: types.SET_LOADING_STACKTRACE, payload: false }],
[],
- () => {
- expect(createFlash).toHaveBeenCalledTimes(1);
- done();
- },
);
+ expect(createFlash).toHaveBeenCalledTimes(1);
});
- it('should not restart polling when receiving an empty 204 response', (done) => {
+ it('should not restart polling when receiving an empty 204 response', async () => {
mockedRestart = jest.spyOn(Poll.prototype, 'restart');
mockedAdapter.onGet().reply(204);
- testAction(actions.startPollingStacktrace, { endpoint }, {}, [], [], () => {
- mockedRestart = jest.spyOn(Poll.prototype, 'restart');
- expect(mockedRestart).toHaveBeenCalledTimes(0);
- done();
- });
+ await testAction(actions.startPollingStacktrace, { endpoint }, {}, [], []);
+ mockedRestart = jest.spyOn(Poll.prototype, 'restart');
+ expect(mockedRestart).toHaveBeenCalledTimes(0);
});
});
});
diff --git a/spec/frontend/error_tracking/store/list/actions_spec.js b/spec/frontend/error_tracking/store/list/actions_spec.js
index 5465bde397c..7173f68bb96 100644
--- a/spec/frontend/error_tracking/store/list/actions_spec.js
+++ b/spec/frontend/error_tracking/store/list/actions_spec.js
@@ -20,11 +20,11 @@ describe('error tracking actions', () => {
});
describe('startPolling', () => {
- it('should start polling for data', (done) => {
+ it('should start polling for data', () => {
const payload = { errors: [{ id: 1 }, { id: 2 }] };
mock.onGet().reply(httpStatusCodes.OK, payload);
- testAction(
+ return testAction(
actions.startPolling,
{},
{},
@@ -35,16 +35,13 @@ describe('error tracking actions', () => {
{ type: types.SET_LOADING, payload: false },
],
[{ type: 'stopPolling' }],
- () => {
- done();
- },
);
});
- it('should show flash on API error', (done) => {
+ it('should show flash on API error', async () => {
mock.onGet().reply(httpStatusCodes.BAD_REQUEST);
- testAction(
+ await testAction(
actions.startPolling,
{},
{},
@@ -53,11 +50,8 @@ describe('error tracking actions', () => {
{ type: types.SET_LOADING, payload: false },
],
[],
- () => {
- expect(createFlash).toHaveBeenCalledTimes(1);
- done();
- },
);
+ expect(createFlash).toHaveBeenCalledTimes(1);
});
});
diff --git a/spec/frontend/error_tracking_settings/store/actions_spec.js b/spec/frontend/error_tracking_settings/store/actions_spec.js
index 1b9be042dd4..bcd816c2ae0 100644
--- a/spec/frontend/error_tracking_settings/store/actions_spec.js
+++ b/spec/frontend/error_tracking_settings/store/actions_spec.js
@@ -27,9 +27,9 @@ describe('error tracking settings actions', () => {
refreshCurrentPage.mockClear();
});
- it('should request and transform the project list', (done) => {
+ it('should request and transform the project list', async () => {
mock.onGet(TEST_HOST).reply(() => [200, { projects: projectList }]);
- testAction(
+ await testAction(
actions.fetchProjects,
null,
state,
@@ -41,16 +41,13 @@ describe('error tracking settings actions', () => {
payload: projectList.map(convertObjectPropsToCamelCase),
},
],
- () => {
- expect(mock.history.get.length).toBe(1);
- done();
- },
);
+ expect(mock.history.get.length).toBe(1);
});
- it('should handle a server error', (done) => {
+ it('should handle a server error', async () => {
mock.onGet(`${TEST_HOST}.json`).reply(() => [400]);
- testAction(
+ await testAction(
actions.fetchProjects,
null,
state,
@@ -61,27 +58,23 @@ describe('error tracking settings actions', () => {
type: 'receiveProjectsError',
},
],
- () => {
- expect(mock.history.get.length).toBe(1);
- done();
- },
);
+ expect(mock.history.get.length).toBe(1);
});
- it('should request projects correctly', (done) => {
- testAction(
+ it('should request projects correctly', () => {
+ return testAction(
actions.requestProjects,
null,
state,
[{ type: types.SET_PROJECTS_LOADING, payload: true }, { type: types.RESET_CONNECT }],
[],
- done,
);
});
- it('should receive projects correctly', (done) => {
+ it('should receive projects correctly', () => {
const testPayload = [];
- testAction(
+ return testAction(
actions.receiveProjectsSuccess,
testPayload,
state,
@@ -91,13 +84,12 @@ describe('error tracking settings actions', () => {
{ type: types.SET_PROJECTS_LOADING, payload: false },
],
[],
- done,
);
});
- it('should handle errors when receiving projects', (done) => {
+ it('should handle errors when receiving projects', () => {
const testPayload = [];
- testAction(
+ return testAction(
actions.receiveProjectsError,
testPayload,
state,
@@ -107,7 +99,6 @@ describe('error tracking settings actions', () => {
{ type: types.SET_PROJECTS_LOADING, payload: false },
],
[],
- done,
);
});
});
@@ -126,18 +117,16 @@ describe('error tracking settings actions', () => {
mock.restore();
});
- it('should save the page', (done) => {
+ it('should save the page', async () => {
mock.onPatch(TEST_HOST).reply(200);
- testAction(actions.updateSettings, null, state, [], [{ type: 'requestSettings' }], () => {
- expect(mock.history.patch.length).toBe(1);
- expect(refreshCurrentPage).toHaveBeenCalled();
- done();
- });
+ await testAction(actions.updateSettings, null, state, [], [{ type: 'requestSettings' }]);
+ expect(mock.history.patch.length).toBe(1);
+ expect(refreshCurrentPage).toHaveBeenCalled();
});
- it('should handle a server error', (done) => {
+ it('should handle a server error', async () => {
mock.onPatch(TEST_HOST).reply(400);
- testAction(
+ await testAction(
actions.updateSettings,
null,
state,
@@ -149,57 +138,50 @@ describe('error tracking settings actions', () => {
payload: new Error('Request failed with status code 400'),
},
],
- () => {
- expect(mock.history.patch.length).toBe(1);
- done();
- },
);
+ expect(mock.history.patch.length).toBe(1);
});
- it('should request to save the page', (done) => {
- testAction(
+ it('should request to save the page', () => {
+ return testAction(
actions.requestSettings,
null,
state,
[{ type: types.UPDATE_SETTINGS_LOADING, payload: true }],
[],
- done,
);
});
- it('should handle errors when requesting to save the page', (done) => {
- testAction(
+ it('should handle errors when requesting to save the page', () => {
+ return testAction(
actions.receiveSettingsError,
{},
state,
[{ type: types.UPDATE_SETTINGS_LOADING, payload: false }],
[],
- done,
);
});
});
describe('generic actions to update the store', () => {
const testData = 'test';
- it('should reset the `connect success` flag when updating the api host', (done) => {
- testAction(
+ it('should reset the `connect success` flag when updating the api host', () => {
+ return testAction(
actions.updateApiHost,
testData,
state,
[{ type: types.UPDATE_API_HOST, payload: testData }, { type: types.RESET_CONNECT }],
[],
- done,
);
});
- it('should reset the `connect success` flag when updating the token', (done) => {
- testAction(
+ it('should reset the `connect success` flag when updating the token', () => {
+ return testAction(
actions.updateToken,
testData,
state,
[{ type: types.UPDATE_TOKEN, payload: testData }, { type: types.RESET_CONNECT }],
[],
- done,
);
});
diff --git a/spec/frontend/feature_flags/store/edit/actions_spec.js b/spec/frontend/feature_flags/store/edit/actions_spec.js
index 12fccd79170..b6114cb0c9f 100644
--- a/spec/frontend/feature_flags/store/edit/actions_spec.js
+++ b/spec/frontend/feature_flags/store/edit/actions_spec.js
@@ -40,7 +40,7 @@ describe('Feature flags Edit Module actions', () => {
});
describe('success', () => {
- it('dispatches requestUpdateFeatureFlag and receiveUpdateFeatureFlagSuccess ', (done) => {
+ it('dispatches requestUpdateFeatureFlag and receiveUpdateFeatureFlagSuccess ', () => {
const featureFlag = {
name: 'name',
description: 'description',
@@ -57,7 +57,7 @@ describe('Feature flags Edit Module actions', () => {
};
mock.onPut(mockedState.endpoint, mapStrategiesToRails(featureFlag)).replyOnce(200);
- testAction(
+ return testAction(
updateFeatureFlag,
featureFlag,
mockedState,
@@ -70,16 +70,15 @@ describe('Feature flags Edit Module actions', () => {
type: 'receiveUpdateFeatureFlagSuccess',
},
],
- done,
);
});
});
describe('error', () => {
- it('dispatches requestUpdateFeatureFlag and receiveUpdateFeatureFlagError ', (done) => {
+ it('dispatches requestUpdateFeatureFlag and receiveUpdateFeatureFlagError ', () => {
mock.onPut(`${TEST_HOST}/endpoint.json`).replyOnce(500, { message: [] });
- testAction(
+ return testAction(
updateFeatureFlag,
{
name: 'feature_flag',
@@ -97,28 +96,26 @@ describe('Feature flags Edit Module actions', () => {
payload: { message: [] },
},
],
- done,
);
});
});
});
describe('requestUpdateFeatureFlag', () => {
- it('should commit REQUEST_UPDATE_FEATURE_FLAG mutation', (done) => {
- testAction(
+ it('should commit REQUEST_UPDATE_FEATURE_FLAG mutation', () => {
+ return testAction(
requestUpdateFeatureFlag,
null,
mockedState,
[{ type: types.REQUEST_UPDATE_FEATURE_FLAG }],
[],
- done,
);
});
});
describe('receiveUpdateFeatureFlagSuccess', () => {
- it('should commit RECEIVE_UPDATE_FEATURE_FLAG_SUCCESS mutation', (done) => {
- testAction(
+ it('should commit RECEIVE_UPDATE_FEATURE_FLAG_SUCCESS mutation', () => {
+ return testAction(
receiveUpdateFeatureFlagSuccess,
null,
mockedState,
@@ -128,20 +125,18 @@ describe('Feature flags Edit Module actions', () => {
},
],
[],
- done,
);
});
});
describe('receiveUpdateFeatureFlagError', () => {
- it('should commit RECEIVE_UPDATE_FEATURE_FLAG_ERROR mutation', (done) => {
- testAction(
+ it('should commit RECEIVE_UPDATE_FEATURE_FLAG_ERROR mutation', () => {
+ return testAction(
receiveUpdateFeatureFlagError,
'There was an error',
mockedState,
[{ type: types.RECEIVE_UPDATE_FEATURE_FLAG_ERROR, payload: 'There was an error' }],
[],
- done,
);
});
});
@@ -159,10 +154,10 @@ describe('Feature flags Edit Module actions', () => {
});
describe('success', () => {
- it('dispatches requestFeatureFlag and receiveFeatureFlagSuccess ', (done) => {
+ it('dispatches requestFeatureFlag and receiveFeatureFlagSuccess ', () => {
mock.onGet(`${TEST_HOST}/endpoint.json`).replyOnce(200, { id: 1 });
- testAction(
+ return testAction(
fetchFeatureFlag,
{ id: 1 },
mockedState,
@@ -176,16 +171,15 @@ describe('Feature flags Edit Module actions', () => {
payload: { id: 1 },
},
],
- done,
);
});
});
describe('error', () => {
- it('dispatches requestFeatureFlag and receiveUpdateFeatureFlagError ', (done) => {
+ it('dispatches requestFeatureFlag and receiveUpdateFeatureFlagError ', () => {
mock.onGet(`${TEST_HOST}/endpoint.json`, {}).replyOnce(500, {});
- testAction(
+ return testAction(
fetchFeatureFlag,
null,
mockedState,
@@ -198,41 +192,38 @@ describe('Feature flags Edit Module actions', () => {
type: 'receiveFeatureFlagError',
},
],
- done,
);
});
});
});
describe('requestFeatureFlag', () => {
- it('should commit REQUEST_FEATURE_FLAG mutation', (done) => {
- testAction(
+ it('should commit REQUEST_FEATURE_FLAG mutation', () => {
+ return testAction(
requestFeatureFlag,
null,
mockedState,
[{ type: types.REQUEST_FEATURE_FLAG }],
[],
- done,
);
});
});
describe('receiveFeatureFlagSuccess', () => {
- it('should commit RECEIVE_FEATURE_FLAG_SUCCESS mutation', (done) => {
- testAction(
+ it('should commit RECEIVE_FEATURE_FLAG_SUCCESS mutation', () => {
+ return testAction(
receiveFeatureFlagSuccess,
{ id: 1 },
mockedState,
[{ type: types.RECEIVE_FEATURE_FLAG_SUCCESS, payload: { id: 1 } }],
[],
- done,
);
});
});
describe('receiveFeatureFlagError', () => {
- it('should commit RECEIVE_FEATURE_FLAG_ERROR mutation', (done) => {
- testAction(
+ it('should commit RECEIVE_FEATURE_FLAG_ERROR mutation', () => {
+ return testAction(
receiveFeatureFlagError,
null,
mockedState,
@@ -242,20 +233,18 @@ describe('Feature flags Edit Module actions', () => {
},
],
[],
- done,
);
});
});
describe('toggelActive', () => {
- it('should commit TOGGLE_ACTIVE mutation', (done) => {
- testAction(
+ it('should commit TOGGLE_ACTIVE mutation', () => {
+ return testAction(
toggleActive,
true,
mockedState,
[{ type: types.TOGGLE_ACTIVE, payload: true }],
[],
- done,
);
});
});
diff --git a/spec/frontend/feature_flags/store/index/actions_spec.js b/spec/frontend/feature_flags/store/index/actions_spec.js
index a59f99f538c..ce62c3b0473 100644
--- a/spec/frontend/feature_flags/store/index/actions_spec.js
+++ b/spec/frontend/feature_flags/store/index/actions_spec.js
@@ -32,14 +32,13 @@ describe('Feature flags actions', () => {
});
describe('setFeatureFlagsOptions', () => {
- it('should commit SET_FEATURE_FLAGS_OPTIONS mutation', (done) => {
- testAction(
+ it('should commit SET_FEATURE_FLAGS_OPTIONS mutation', () => {
+ return testAction(
setFeatureFlagsOptions,
{ page: '1', scope: 'all' },
mockedState,
[{ type: types.SET_FEATURE_FLAGS_OPTIONS, payload: { page: '1', scope: 'all' } }],
[],
- done,
);
});
});
@@ -57,10 +56,10 @@ describe('Feature flags actions', () => {
});
describe('success', () => {
- it('dispatches requestFeatureFlags and receiveFeatureFlagsSuccess ', (done) => {
+ it('dispatches requestFeatureFlags and receiveFeatureFlagsSuccess ', () => {
mock.onGet(`${TEST_HOST}/endpoint.json`).replyOnce(200, getRequestData, {});
- testAction(
+ return testAction(
fetchFeatureFlags,
null,
mockedState,
@@ -74,16 +73,15 @@ describe('Feature flags actions', () => {
type: 'receiveFeatureFlagsSuccess',
},
],
- done,
);
});
});
describe('error', () => {
- it('dispatches requestFeatureFlags and receiveFeatureFlagsError ', (done) => {
+ it('dispatches requestFeatureFlags and receiveFeatureFlagsError ', () => {
mock.onGet(`${TEST_HOST}/endpoint.json`, {}).replyOnce(500, {});
- testAction(
+ return testAction(
fetchFeatureFlags,
null,
mockedState,
@@ -96,28 +94,26 @@ describe('Feature flags actions', () => {
type: 'receiveFeatureFlagsError',
},
],
- done,
);
});
});
});
describe('requestFeatureFlags', () => {
- it('should commit RECEIVE_FEATURE_FLAGS_SUCCESS mutation', (done) => {
- testAction(
+ it('should commit RECEIVE_FEATURE_FLAGS_SUCCESS mutation', () => {
+ return testAction(
requestFeatureFlags,
null,
mockedState,
[{ type: types.REQUEST_FEATURE_FLAGS }],
[],
- done,
);
});
});
describe('receiveFeatureFlagsSuccess', () => {
- it('should commit RECEIVE_FEATURE_FLAGS_SUCCESS mutation', (done) => {
- testAction(
+ it('should commit RECEIVE_FEATURE_FLAGS_SUCCESS mutation', () => {
+ return testAction(
receiveFeatureFlagsSuccess,
{ data: getRequestData, headers: {} },
mockedState,
@@ -128,20 +124,18 @@ describe('Feature flags actions', () => {
},
],
[],
- done,
);
});
});
describe('receiveFeatureFlagsError', () => {
- it('should commit RECEIVE_FEATURE_FLAGS_ERROR mutation', (done) => {
- testAction(
+ it('should commit RECEIVE_FEATURE_FLAGS_ERROR mutation', () => {
+ return testAction(
receiveFeatureFlagsError,
null,
mockedState,
[{ type: types.RECEIVE_FEATURE_FLAGS_ERROR }],
[],
- done,
);
});
});
@@ -159,10 +153,10 @@ describe('Feature flags actions', () => {
});
describe('success', () => {
- it('dispatches requestRotateInstanceId and receiveRotateInstanceIdSuccess ', (done) => {
+ it('dispatches requestRotateInstanceId and receiveRotateInstanceIdSuccess ', () => {
mock.onPost(`${TEST_HOST}/endpoint.json`).replyOnce(200, rotateData, {});
- testAction(
+ return testAction(
rotateInstanceId,
null,
mockedState,
@@ -176,16 +170,15 @@ describe('Feature flags actions', () => {
type: 'receiveRotateInstanceIdSuccess',
},
],
- done,
);
});
});
describe('error', () => {
- it('dispatches requestRotateInstanceId and receiveRotateInstanceIdError ', (done) => {
+ it('dispatches requestRotateInstanceId and receiveRotateInstanceIdError ', () => {
mock.onGet(`${TEST_HOST}/endpoint.json`, {}).replyOnce(500, {});
- testAction(
+ return testAction(
rotateInstanceId,
null,
mockedState,
@@ -198,28 +191,26 @@ describe('Feature flags actions', () => {
type: 'receiveRotateInstanceIdError',
},
],
- done,
);
});
});
});
describe('requestRotateInstanceId', () => {
- it('should commit REQUEST_ROTATE_INSTANCE_ID mutation', (done) => {
- testAction(
+ it('should commit REQUEST_ROTATE_INSTANCE_ID mutation', () => {
+ return testAction(
requestRotateInstanceId,
null,
mockedState,
[{ type: types.REQUEST_ROTATE_INSTANCE_ID }],
[],
- done,
);
});
});
describe('receiveRotateInstanceIdSuccess', () => {
- it('should commit RECEIVE_ROTATE_INSTANCE_ID_SUCCESS mutation', (done) => {
- testAction(
+ it('should commit RECEIVE_ROTATE_INSTANCE_ID_SUCCESS mutation', () => {
+ return testAction(
receiveRotateInstanceIdSuccess,
{ data: rotateData, headers: {} },
mockedState,
@@ -230,20 +221,18 @@ describe('Feature flags actions', () => {
},
],
[],
- done,
);
});
});
describe('receiveRotateInstanceIdError', () => {
- it('should commit RECEIVE_ROTATE_INSTANCE_ID_ERROR mutation', (done) => {
- testAction(
+ it('should commit RECEIVE_ROTATE_INSTANCE_ID_ERROR mutation', () => {
+ return testAction(
receiveRotateInstanceIdError,
null,
mockedState,
[{ type: types.RECEIVE_ROTATE_INSTANCE_ID_ERROR }],
[],
- done,
);
});
});
@@ -262,10 +251,10 @@ describe('Feature flags actions', () => {
mock.restore();
});
describe('success', () => {
- it('dispatches updateFeatureFlag and receiveUpdateFeatureFlagSuccess', (done) => {
+ it('dispatches updateFeatureFlag and receiveUpdateFeatureFlagSuccess', () => {
mock.onPut(featureFlag.update_path).replyOnce(200, featureFlag, {});
- testAction(
+ return testAction(
toggleFeatureFlag,
featureFlag,
mockedState,
@@ -280,15 +269,15 @@ describe('Feature flags actions', () => {
type: 'receiveUpdateFeatureFlagSuccess',
},
],
- done,
);
});
});
+
describe('error', () => {
- it('dispatches updateFeatureFlag and receiveUpdateFeatureFlagSuccess', (done) => {
+ it('dispatches updateFeatureFlag and receiveUpdateFeatureFlagSuccess', () => {
mock.onPut(featureFlag.update_path).replyOnce(500);
- testAction(
+ return testAction(
toggleFeatureFlag,
featureFlag,
mockedState,
@@ -303,7 +292,6 @@ describe('Feature flags actions', () => {
type: 'receiveUpdateFeatureFlagError',
},
],
- done,
);
});
});
@@ -315,8 +303,8 @@ describe('Feature flags actions', () => {
}));
});
- it('commits UPDATE_FEATURE_FLAG with the given flag', (done) => {
- testAction(
+ it('commits UPDATE_FEATURE_FLAG with the given flag', () => {
+ return testAction(
updateFeatureFlag,
featureFlag,
mockedState,
@@ -327,7 +315,6 @@ describe('Feature flags actions', () => {
},
],
[],
- done,
);
});
});
@@ -338,8 +325,8 @@ describe('Feature flags actions', () => {
}));
});
- it('commits RECEIVE_UPDATE_FEATURE_FLAG_SUCCESS with the given flag', (done) => {
- testAction(
+ it('commits RECEIVE_UPDATE_FEATURE_FLAG_SUCCESS with the given flag', () => {
+ return testAction(
receiveUpdateFeatureFlagSuccess,
featureFlag,
mockedState,
@@ -350,7 +337,6 @@ describe('Feature flags actions', () => {
},
],
[],
- done,
);
});
});
@@ -361,8 +347,8 @@ describe('Feature flags actions', () => {
}));
});
- it('commits RECEIVE_UPDATE_FEATURE_FLAG_ERROR with the given flag id', (done) => {
- testAction(
+ it('commits RECEIVE_UPDATE_FEATURE_FLAG_ERROR with the given flag id', () => {
+ return testAction(
receiveUpdateFeatureFlagError,
featureFlag.id,
mockedState,
@@ -373,22 +359,20 @@ describe('Feature flags actions', () => {
},
],
[],
- done,
);
});
});
describe('clearAlert', () => {
- it('should commit RECEIVE_CLEAR_ALERT', (done) => {
+ it('should commit RECEIVE_CLEAR_ALERT', () => {
const alertIndex = 3;
- testAction(
+ return testAction(
clearAlert,
alertIndex,
mockedState,
[{ type: 'RECEIVE_CLEAR_ALERT', payload: alertIndex }],
[],
- done,
);
});
});
diff --git a/spec/frontend/feature_flags/store/new/actions_spec.js b/spec/frontend/feature_flags/store/new/actions_spec.js
index 7900b200eb2..1dcd2da1d93 100644
--- a/spec/frontend/feature_flags/store/new/actions_spec.js
+++ b/spec/frontend/feature_flags/store/new/actions_spec.js
@@ -33,7 +33,7 @@ describe('Feature flags New Module Actions', () => {
});
describe('success', () => {
- it('dispatches requestCreateFeatureFlag and receiveCreateFeatureFlagSuccess ', (done) => {
+ it('dispatches requestCreateFeatureFlag and receiveCreateFeatureFlagSuccess ', () => {
const actionParams = {
name: 'name',
description: 'description',
@@ -50,7 +50,7 @@ describe('Feature flags New Module Actions', () => {
};
mock.onPost(mockedState.endpoint, mapStrategiesToRails(actionParams)).replyOnce(200);
- testAction(
+ return testAction(
createFeatureFlag,
actionParams,
mockedState,
@@ -63,13 +63,12 @@ describe('Feature flags New Module Actions', () => {
type: 'receiveCreateFeatureFlagSuccess',
},
],
- done,
);
});
});
describe('error', () => {
- it('dispatches requestCreateFeatureFlag and receiveCreateFeatureFlagError ', (done) => {
+ it('dispatches requestCreateFeatureFlag and receiveCreateFeatureFlagError ', () => {
const actionParams = {
name: 'name',
description: 'description',
@@ -88,7 +87,7 @@ describe('Feature flags New Module Actions', () => {
.onPost(mockedState.endpoint, mapStrategiesToRails(actionParams))
.replyOnce(500, { message: [] });
- testAction(
+ return testAction(
createFeatureFlag,
actionParams,
mockedState,
@@ -102,28 +101,26 @@ describe('Feature flags New Module Actions', () => {
payload: { message: [] },
},
],
- done,
);
});
});
});
describe('requestCreateFeatureFlag', () => {
- it('should commit REQUEST_CREATE_FEATURE_FLAG mutation', (done) => {
- testAction(
+ it('should commit REQUEST_CREATE_FEATURE_FLAG mutation', () => {
+ return testAction(
requestCreateFeatureFlag,
null,
mockedState,
[{ type: types.REQUEST_CREATE_FEATURE_FLAG }],
[],
- done,
);
});
});
describe('receiveCreateFeatureFlagSuccess', () => {
- it('should commit RECEIVE_CREATE_FEATURE_FLAG_SUCCESS mutation', (done) => {
- testAction(
+ it('should commit RECEIVE_CREATE_FEATURE_FLAG_SUCCESS mutation', () => {
+ return testAction(
receiveCreateFeatureFlagSuccess,
null,
mockedState,
@@ -133,20 +130,18 @@ describe('Feature flags New Module Actions', () => {
},
],
[],
- done,
);
});
});
describe('receiveCreateFeatureFlagError', () => {
- it('should commit RECEIVE_CREATE_FEATURE_FLAG_ERROR mutation', (done) => {
- testAction(
+ it('should commit RECEIVE_CREATE_FEATURE_FLAG_ERROR mutation', () => {
+ return testAction(
receiveCreateFeatureFlagError,
'There was an error',
mockedState,
[{ type: types.RECEIVE_CREATE_FEATURE_FLAG_ERROR, payload: 'There was an error' }],
[],
- done,
);
});
});
diff --git a/spec/frontend/filtered_search/droplab/plugins/ajax_filter_spec.js b/spec/frontend/filtered_search/droplab/plugins/ajax_filter_spec.js
index 88b3fc236e4..212b9ffc8f9 100644
--- a/spec/frontend/filtered_search/droplab/plugins/ajax_filter_spec.js
+++ b/spec/frontend/filtered_search/droplab/plugins/ajax_filter_spec.js
@@ -38,35 +38,25 @@ describe('AjaxFilter', () => {
dummyList.list.appendChild(dynamicList);
});
- it('calls onLoadingFinished after loading data', (done) => {
+ it('calls onLoadingFinished after loading data', async () => {
ajaxSpy = (url) => {
expect(url).toBe('dummy endpoint?dummy search key=');
return Promise.resolve(dummyData);
};
- AjaxFilter.trigger()
- .then(() => {
- expect(dummyConfig.onLoadingFinished.mock.calls.length).toBe(1);
- })
- .then(done)
- .catch(done.fail);
+ await AjaxFilter.trigger();
+ expect(dummyConfig.onLoadingFinished.mock.calls.length).toBe(1);
});
- it('does not call onLoadingFinished if Ajax call fails', (done) => {
+ it('does not call onLoadingFinished if Ajax call fails', async () => {
const dummyError = new Error('My dummy is sick! :-(');
ajaxSpy = (url) => {
expect(url).toBe('dummy endpoint?dummy search key=');
return Promise.reject(dummyError);
};
- AjaxFilter.trigger()
- .then(done.fail)
- .catch((error) => {
- expect(error).toBe(dummyError);
- expect(dummyConfig.onLoadingFinished.mock.calls.length).toBe(0);
- })
- .then(done)
- .catch(done.fail);
+ await expect(AjaxFilter.trigger()).rejects.toEqual(dummyError);
+ expect(dummyConfig.onLoadingFinished.mock.calls.length).toBe(0);
});
});
});
diff --git a/spec/frontend/filtered_search/filtered_search_manager_spec.js b/spec/frontend/filtered_search/filtered_search_manager_spec.js
index 83e7f6c9b3f..911a507af4c 100644
--- a/spec/frontend/filtered_search/filtered_search_manager_spec.js
+++ b/spec/frontend/filtered_search/filtered_search_manager_spec.js
@@ -190,43 +190,40 @@ describe('Filtered Search Manager', () => {
const defaultParams = '?scope=all';
const defaultState = '&state=opened';
- it('should search with a single word', (done) => {
+ it('should search with a single word', () => {
initializeManager();
input.value = 'searchTerm';
visitUrl.mockImplementation((url) => {
expect(url).toEqual(`${defaultParams}&search=searchTerm`);
- done();
});
manager.search();
});
- it('sets default state', (done) => {
+ it('sets default state', () => {
initializeManager({ useDefaultState: true });
input.value = 'searchTerm';
visitUrl.mockImplementation((url) => {
expect(url).toEqual(`${defaultParams}${defaultState}&search=searchTerm`);
- done();
});
manager.search();
});
- it('should search with multiple words', (done) => {
+ it('should search with multiple words', () => {
initializeManager();
input.value = 'awesome search terms';
visitUrl.mockImplementation((url) => {
expect(url).toEqual(`${defaultParams}&search=awesome+search+terms`);
- done();
});
manager.search();
});
- it('should search with special characters', (done) => {
+ it('should search with special characters', () => {
initializeManager();
input.value = '~!@#$%^&*()_+{}:<>,.?/';
@@ -234,13 +231,12 @@ describe('Filtered Search Manager', () => {
expect(url).toEqual(
`${defaultParams}&search=~!%40%23%24%25%5E%26*()_%2B%7B%7D%3A%3C%3E%2C.%3F%2F`,
);
- done();
});
manager.search();
});
- it('should use replacement URL for condition', (done) => {
+ it('should use replacement URL for condition', () => {
initializeManager();
tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(
FilteredSearchSpecHelper.createFilterVisualTokenHTML('milestone', '=', '13', true),
@@ -248,7 +244,6 @@ describe('Filtered Search Manager', () => {
visitUrl.mockImplementation((url) => {
expect(url).toEqual(`${defaultParams}&milestone_title=replaced`);
- done();
});
manager.filteredSearchTokenKeys.conditions.push({
@@ -261,7 +256,7 @@ describe('Filtered Search Manager', () => {
manager.search();
});
- it('removes duplicated tokens', (done) => {
+ it('removes duplicated tokens', () => {
initializeManager();
tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(`
${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '=', '~bug')}
@@ -270,7 +265,6 @@ describe('Filtered Search Manager', () => {
visitUrl.mockImplementation((url) => {
expect(url).toEqual(`${defaultParams}&label_name[]=bug`);
- done();
});
manager.search();
diff --git a/spec/frontend/filtered_search/services/recent_searches_service_spec.js b/spec/frontend/filtered_search/services/recent_searches_service_spec.js
index dfa53652eb1..426a60df427 100644
--- a/spec/frontend/filtered_search/services/recent_searches_service_spec.js
+++ b/spec/frontend/filtered_search/services/recent_searches_service_spec.js
@@ -18,53 +18,47 @@ describe('RecentSearchesService', () => {
jest.spyOn(RecentSearchesService, 'isAvailable').mockReturnValue(true);
});
- it('should default to empty array', (done) => {
+ it('should default to empty array', () => {
const fetchItemsPromise = service.fetch();
- fetchItemsPromise
- .then((items) => {
- expect(items).toEqual([]);
- })
- .then(done)
- .catch(done.fail);
+ return fetchItemsPromise.then((items) => {
+ expect(items).toEqual([]);
+ });
});
- it('should reject when unable to parse', (done) => {
+ it('should reject when unable to parse', () => {
jest.spyOn(localStorage, 'getItem').mockReturnValue('fail');
const fetchItemsPromise = service.fetch();
- fetchItemsPromise
- .then(done.fail)
+ return fetchItemsPromise
+ .then(() => {
+ throw new Error();
+ })
.catch((error) => {
expect(error).toEqual(expect.any(SyntaxError));
- })
- .then(done)
- .catch(done.fail);
+ });
});
- it('should reject when service is unavailable', (done) => {
+ it('should reject when service is unavailable', () => {
RecentSearchesService.isAvailable.mockReturnValue(false);
- service
+ return service
.fetch()
- .then(done.fail)
+ .then(() => {
+ throw new Error();
+ })
.catch((error) => {
expect(error).toEqual(expect.any(Error));
- })
- .then(done)
- .catch(done.fail);
+ });
});
- it('should return items from localStorage', (done) => {
+ it('should return items from localStorage', () => {
jest.spyOn(localStorage, 'getItem').mockReturnValue('["foo", "bar"]');
const fetchItemsPromise = service.fetch();
- fetchItemsPromise
- .then((items) => {
- expect(items).toEqual(['foo', 'bar']);
- })
- .then(done)
- .catch(done.fail);
+ return fetchItemsPromise.then((items) => {
+ expect(items).toEqual(['foo', 'bar']);
+ });
});
describe('if .isAvailable returns `false`', () => {
@@ -74,16 +68,16 @@ describe('RecentSearchesService', () => {
jest.spyOn(Storage.prototype, 'getItem').mockImplementation(() => {});
});
- it('should not call .getItem', (done) => {
- RecentSearchesService.prototype
+ it('should not call .getItem', () => {
+ return RecentSearchesService.prototype
.fetch()
- .then(done.fail)
+ .then(() => {
+ throw new Error();
+ })
.catch((err) => {
expect(err).toEqual(new RecentSearchesServiceError());
expect(localStorage.getItem).not.toHaveBeenCalled();
- })
- .then(done)
- .catch(done.fail);
+ });
});
});
});
diff --git a/spec/frontend/filtered_search/visual_token_value_spec.js b/spec/frontend/filtered_search/visual_token_value_spec.js
index 8ac5b6fbea6..bf526a8d371 100644
--- a/spec/frontend/filtered_search/visual_token_value_spec.js
+++ b/spec/frontend/filtered_search/visual_token_value_spec.js
@@ -46,7 +46,7 @@ describe('Filtered Search Visual Tokens', () => {
jest.spyOn(UsersCache, 'retrieve').mockImplementation((username) => usersCacheSpy(username));
});
- it('ignores error if UsersCache throws', (done) => {
+ it('ignores error if UsersCache throws', async () => {
const dummyError = new Error('Earth rotated backwards');
const { subject, tokenValueContainer, tokenValueElement } = findElements(authorToken);
const tokenValue = tokenValueElement.innerText;
@@ -55,16 +55,11 @@ describe('Filtered Search Visual Tokens', () => {
return Promise.reject(dummyError);
};
- subject
- .updateUserTokenAppearance(tokenValueContainer, tokenValueElement, tokenValue)
- .then(() => {
- expect(createFlash.mock.calls.length).toBe(0);
- })
- .then(done)
- .catch(done.fail);
+ await subject.updateUserTokenAppearance(tokenValueContainer, tokenValueElement, tokenValue);
+ expect(createFlash.mock.calls.length).toBe(0);
});
- it('does nothing if user cannot be found', (done) => {
+ it('does nothing if user cannot be found', async () => {
const { subject, tokenValueContainer, tokenValueElement } = findElements(authorToken);
const tokenValue = tokenValueElement.innerText;
usersCacheSpy = (username) => {
@@ -72,16 +67,11 @@ describe('Filtered Search Visual Tokens', () => {
return Promise.resolve(undefined);
};
- subject
- .updateUserTokenAppearance(tokenValueContainer, tokenValueElement, tokenValue)
- .then(() => {
- expect(tokenValueElement.innerText).toBe(tokenValue);
- })
- .then(done)
- .catch(done.fail);
+ await subject.updateUserTokenAppearance(tokenValueContainer, tokenValueElement, tokenValue);
+ expect(tokenValueElement.innerText).toBe(tokenValue);
});
- it('replaces author token with avatar and display name', (done) => {
+ it('replaces author token with avatar and display name', async () => {
const dummyUser = {
name: 'Important Person',
avatar_url: 'https://host.invalid/mypics/avatar.png',
@@ -93,21 +83,16 @@ describe('Filtered Search Visual Tokens', () => {
return Promise.resolve(dummyUser);
};
- subject
- .updateUserTokenAppearance(tokenValueContainer, tokenValueElement, tokenValue)
- .then(() => {
- expect(tokenValueContainer.dataset.originalValue).toBe(tokenValue);
- expect(tokenValueElement.innerText.trim()).toBe(dummyUser.name);
- const avatar = tokenValueElement.querySelector('img.avatar');
-
- expect(avatar.getAttribute('src')).toBe(dummyUser.avatar_url);
- expect(avatar.getAttribute('alt')).toBe('');
- })
- .then(done)
- .catch(done.fail);
+ await subject.updateUserTokenAppearance(tokenValueContainer, tokenValueElement, tokenValue);
+ expect(tokenValueContainer.dataset.originalValue).toBe(tokenValue);
+ expect(tokenValueElement.innerText.trim()).toBe(dummyUser.name);
+ const avatar = tokenValueElement.querySelector('img.avatar');
+
+ expect(avatar.getAttribute('src')).toBe(dummyUser.avatar_url);
+ expect(avatar.getAttribute('alt')).toBe('');
});
- it('escapes user name when creating token', (done) => {
+ it('escapes user name when creating token', async () => {
const dummyUser = {
name: '<script>',
avatar_url: `${TEST_HOST}/mypics/avatar.png`,
@@ -119,16 +104,11 @@ describe('Filtered Search Visual Tokens', () => {
return Promise.resolve(dummyUser);
};
- subject
- .updateUserTokenAppearance(tokenValueContainer, tokenValueElement, tokenValue)
- .then(() => {
- expect(tokenValueElement.innerText.trim()).toBe(dummyUser.name);
- tokenValueElement.querySelector('.avatar').remove();
+ await subject.updateUserTokenAppearance(tokenValueContainer, tokenValueElement, tokenValue);
+ expect(tokenValueElement.innerText.trim()).toBe(dummyUser.name);
+ tokenValueElement.querySelector('.avatar').remove();
- expect(tokenValueElement.innerHTML.trim()).toBe(escape(dummyUser.name));
- })
- .then(done)
- .catch(done.fail);
+ expect(tokenValueElement.innerHTML.trim()).toBe(escape(dummyUser.name));
});
});
@@ -177,48 +157,33 @@ describe('Filtered Search Visual Tokens', () => {
const findLabel = (tokenValue) =>
labelData.find((label) => tokenValue === `~${DropdownUtils.getEscapedText(label.title)}`);
- it('updates the color of a label token', (done) => {
+ it('updates the color of a label token', async () => {
const { subject, tokenValueContainer, tokenValueElement } = findElements(bugLabelToken);
const tokenValue = tokenValueElement.innerText;
const matchingLabel = findLabel(tokenValue);
- subject
- .updateLabelTokenColor(tokenValueContainer, tokenValue)
- .then(() => {
- expectValueContainerStyle(tokenValueContainer, matchingLabel);
- })
- .then(done)
- .catch(done.fail);
+ await subject.updateLabelTokenColor(tokenValueContainer, tokenValue);
+ expectValueContainerStyle(tokenValueContainer, matchingLabel);
});
- it('updates the color of a label token with spaces', (done) => {
+ it('updates the color of a label token with spaces', async () => {
const { subject, tokenValueContainer, tokenValueElement } = findElements(spaceLabelToken);
const tokenValue = tokenValueElement.innerText;
const matchingLabel = findLabel(tokenValue);
- subject
- .updateLabelTokenColor(tokenValueContainer, tokenValue)
- .then(() => {
- expectValueContainerStyle(tokenValueContainer, matchingLabel);
- })
- .then(done)
- .catch(done.fail);
+ await subject.updateLabelTokenColor(tokenValueContainer, tokenValue);
+ expectValueContainerStyle(tokenValueContainer, matchingLabel);
});
- it('does not change color of a missing label', (done) => {
+ it('does not change color of a missing label', async () => {
const { subject, tokenValueContainer, tokenValueElement } = findElements(missingLabelToken);
const tokenValue = tokenValueElement.innerText;
const matchingLabel = findLabel(tokenValue);
expect(matchingLabel).toBe(undefined);
- subject
- .updateLabelTokenColor(tokenValueContainer, tokenValue)
- .then(() => {
- expect(tokenValueContainer.getAttribute('style')).toBe(null);
- })
- .then(done)
- .catch(done.fail);
+ await subject.updateLabelTokenColor(tokenValueContainer, tokenValue);
+ expect(tokenValueContainer.getAttribute('style')).toBe(null);
});
});
diff --git a/spec/frontend/fixtures/startup_css.rb b/spec/frontend/fixtures/startup_css.rb
index e19a98c3bab..cf7383fa6ca 100644
--- a/spec/frontend/fixtures/startup_css.rb
+++ b/spec/frontend/fixtures/startup_css.rb
@@ -41,12 +41,12 @@ RSpec.describe 'Startup CSS fixtures', type: :controller do
expect(response).to be_successful
end
- # This Feature Flag is off by default
+ # This Feature Flag is on by default
# This ensures that the correct css is generated
- # When the feature flag is off, the general startup will capture it
+ # When the feature flag is on, the general startup will capture it
# This will be removed as part of https://gitlab.com/gitlab-org/gitlab/-/issues/339348
- it "startup_css/project-#{type}-search-ff-on.html" do
- stub_feature_flags(new_header_search: true)
+ it "startup_css/project-#{type}-search-ff-off.html" do
+ stub_feature_flags(new_header_search: false)
get :show, params: {
namespace_id: project.namespace.to_param,
diff --git a/spec/frontend/frequent_items/store/actions_spec.js b/spec/frontend/frequent_items/store/actions_spec.js
index fb0321545c2..3fc3eaf52a2 100644
--- a/spec/frontend/frequent_items/store/actions_spec.js
+++ b/spec/frontend/frequent_items/store/actions_spec.js
@@ -29,136 +29,126 @@ describe('Frequent Items Dropdown Store Actions', () => {
});
describe('setNamespace', () => {
- it('should set namespace', (done) => {
- testAction(
+ it('should set namespace', () => {
+ return testAction(
actions.setNamespace,
mockNamespace,
mockedState,
[{ type: types.SET_NAMESPACE, payload: mockNamespace }],
[],
- done,
);
});
});
describe('setStorageKey', () => {
- it('should set storage key', (done) => {
- testAction(
+ it('should set storage key', () => {
+ return testAction(
actions.setStorageKey,
mockStorageKey,
mockedState,
[{ type: types.SET_STORAGE_KEY, payload: mockStorageKey }],
[],
- done,
);
});
});
describe('requestFrequentItems', () => {
- it('should request frequent items', (done) => {
- testAction(
+ it('should request frequent items', () => {
+ return testAction(
actions.requestFrequentItems,
null,
mockedState,
[{ type: types.REQUEST_FREQUENT_ITEMS }],
[],
- done,
);
});
});
describe('receiveFrequentItemsSuccess', () => {
- it('should set frequent items', (done) => {
- testAction(
+ it('should set frequent items', () => {
+ return testAction(
actions.receiveFrequentItemsSuccess,
mockFrequentProjects,
mockedState,
[{ type: types.RECEIVE_FREQUENT_ITEMS_SUCCESS, payload: mockFrequentProjects }],
[],
- done,
);
});
});
describe('receiveFrequentItemsError', () => {
- it('should set frequent items error state', (done) => {
- testAction(
+ it('should set frequent items error state', () => {
+ return testAction(
actions.receiveFrequentItemsError,
null,
mockedState,
[{ type: types.RECEIVE_FREQUENT_ITEMS_ERROR }],
[],
- done,
);
});
});
describe('fetchFrequentItems', () => {
- it('should dispatch `receiveFrequentItemsSuccess`', (done) => {
+ it('should dispatch `receiveFrequentItemsSuccess`', () => {
mockedState.namespace = mockNamespace;
mockedState.storageKey = mockStorageKey;
- testAction(
+ return testAction(
actions.fetchFrequentItems,
null,
mockedState,
[],
[{ type: 'requestFrequentItems' }, { type: 'receiveFrequentItemsSuccess', payload: [] }],
- done,
);
});
- it('should dispatch `receiveFrequentItemsError`', (done) => {
+ it('should dispatch `receiveFrequentItemsError`', () => {
jest.spyOn(AccessorUtilities, 'canUseLocalStorage').mockReturnValue(false);
mockedState.namespace = mockNamespace;
mockedState.storageKey = mockStorageKey;
- testAction(
+ return testAction(
actions.fetchFrequentItems,
null,
mockedState,
[],
[{ type: 'requestFrequentItems' }, { type: 'receiveFrequentItemsError' }],
- done,
);
});
});
describe('requestSearchedItems', () => {
- it('should request searched items', (done) => {
- testAction(
+ it('should request searched items', () => {
+ return testAction(
actions.requestSearchedItems,
null,
mockedState,
[{ type: types.REQUEST_SEARCHED_ITEMS }],
[],
- done,
);
});
});
describe('receiveSearchedItemsSuccess', () => {
- it('should set searched items', (done) => {
- testAction(
+ it('should set searched items', () => {
+ return testAction(
actions.receiveSearchedItemsSuccess,
mockSearchedProjects,
mockedState,
[{ type: types.RECEIVE_SEARCHED_ITEMS_SUCCESS, payload: mockSearchedProjects }],
[],
- done,
);
});
});
describe('receiveSearchedItemsError', () => {
- it('should set searched items error state', (done) => {
- testAction(
+ it('should set searched items error state', () => {
+ return testAction(
actions.receiveSearchedItemsError,
null,
mockedState,
[{ type: types.RECEIVE_SEARCHED_ITEMS_ERROR }],
[],
- done,
);
});
});
@@ -168,10 +158,10 @@ describe('Frequent Items Dropdown Store Actions', () => {
gon.api_version = 'v4';
});
- it('should dispatch `receiveSearchedItemsSuccess`', (done) => {
+ it('should dispatch `receiveSearchedItemsSuccess`', () => {
mock.onGet(/\/api\/v4\/projects.json(.*)$/).replyOnce(200, mockSearchedProjects, {});
- testAction(
+ return testAction(
actions.fetchSearchedItems,
null,
mockedState,
@@ -183,45 +173,41 @@ describe('Frequent Items Dropdown Store Actions', () => {
payload: { data: mockSearchedProjects, headers: {} },
},
],
- done,
);
});
- it('should dispatch `receiveSearchedItemsError`', (done) => {
+ it('should dispatch `receiveSearchedItemsError`', () => {
gon.api_version = 'v4';
mock.onGet(/\/api\/v4\/projects.json(.*)$/).replyOnce(500);
- testAction(
+ return testAction(
actions.fetchSearchedItems,
null,
mockedState,
[],
[{ type: 'requestSearchedItems' }, { type: 'receiveSearchedItemsError' }],
- done,
);
});
});
describe('setSearchQuery', () => {
- it('should commit query and dispatch `fetchSearchedItems` when query is present', (done) => {
- testAction(
+ it('should commit query and dispatch `fetchSearchedItems` when query is present', () => {
+ return testAction(
actions.setSearchQuery,
{ query: 'test' },
mockedState,
[{ type: types.SET_SEARCH_QUERY, payload: { query: 'test' } }],
[{ type: 'fetchSearchedItems', payload: { query: 'test' } }],
- done,
);
});
- it('should commit query and dispatch `fetchFrequentItems` when query is empty', (done) => {
- testAction(
+ it('should commit query and dispatch `fetchFrequentItems` when query is empty', () => {
+ return testAction(
actions.setSearchQuery,
null,
mockedState,
[{ type: types.SET_SEARCH_QUERY, payload: null }],
[{ type: 'fetchFrequentItems' }],
- done,
);
});
});
diff --git a/spec/frontend/google_cloud/components/app_spec.js b/spec/frontend/google_cloud/components/app_spec.js
index 50b05fb30e0..0cafe6d3b9d 100644
--- a/spec/frontend/google_cloud/components/app_spec.js
+++ b/spec/frontend/google_cloud/components/app_spec.js
@@ -8,7 +8,7 @@ import GcpError from '~/google_cloud/components/errors/gcp_error.vue';
import NoGcpProjects from '~/google_cloud/components/errors/no_gcp_projects.vue';
const BASE_FEEDBACK_URL =
- 'https://gitlab.com/gitlab-org/incubation-engineering/five-minute-production/meta/-/issues/new';
+ 'https://gitlab.com/gitlab-org/incubation-engineering/five-minute-production/feedback/-/issues/new';
const SCREEN_COMPONENTS = {
Home,
ServiceAccountsForm,
diff --git a/spec/frontend/gpg_badges_spec.js b/spec/frontend/gpg_badges_spec.js
index 44c70f1ad4d..0bb50fc3e6f 100644
--- a/spec/frontend/gpg_badges_spec.js
+++ b/spec/frontend/gpg_badges_spec.js
@@ -40,30 +40,22 @@ describe('GpgBadges', () => {
mock.restore();
});
- it('does not make a request if there is no container element', (done) => {
+ it('does not make a request if there is no container element', async () => {
setFixtures('');
jest.spyOn(axios, 'get').mockImplementation(() => {});
- GpgBadges.fetch()
- .then(() => {
- expect(axios.get).not.toHaveBeenCalled();
- })
- .then(done)
- .catch(done.fail);
+ await GpgBadges.fetch();
+ expect(axios.get).not.toHaveBeenCalled();
});
- it('throws an error if the endpoint is missing', (done) => {
+ it('throws an error if the endpoint is missing', async () => {
setFixtures('<div class="js-signature-container"></div>');
jest.spyOn(axios, 'get').mockImplementation(() => {});
- GpgBadges.fetch()
- .then(() => done.fail('Expected error to be thrown'))
- .catch((error) => {
- expect(error.message).toBe('Missing commit signatures endpoint!');
- expect(axios.get).not.toHaveBeenCalled();
- })
- .then(done)
- .catch(done.fail);
+ await expect(GpgBadges.fetch()).rejects.toEqual(
+ new Error('Missing commit signatures endpoint!'),
+ );
+ expect(axios.get).not.toHaveBeenCalled();
});
it('fetches commit signatures', async () => {
@@ -104,31 +96,23 @@ describe('GpgBadges', () => {
});
});
- it('displays a loading spinner', (done) => {
+ it('displays a loading spinner', async () => {
mock.onGet(dummyUrl).replyOnce(200);
- GpgBadges.fetch()
- .then(() => {
- expect(document.querySelector('.js-loading-gpg-badge:empty')).toBe(null);
- const spinners = document.querySelectorAll('.js-loading-gpg-badge span.gl-spinner');
+ await GpgBadges.fetch();
+ expect(document.querySelector('.js-loading-gpg-badge:empty')).toBe(null);
+ const spinners = document.querySelectorAll('.js-loading-gpg-badge span.gl-spinner');
- expect(spinners.length).toBe(1);
- done();
- })
- .catch(done.fail);
+ expect(spinners.length).toBe(1);
});
- it('replaces the loading spinner', (done) => {
+ it('replaces the loading spinner', async () => {
mock.onGet(dummyUrl).replyOnce(200, dummyResponse);
- GpgBadges.fetch()
- .then(() => {
- expect(document.querySelector('.js-loading-gpg-badge')).toBe(null);
- const parentContainer = document.querySelector('.parent-container');
+ await GpgBadges.fetch();
+ expect(document.querySelector('.js-loading-gpg-badge')).toBe(null);
+ const parentContainer = document.querySelector('.parent-container');
- expect(parentContainer.innerHTML.trim()).toEqual(dummyBadgeHtml);
- done();
- })
- .catch(done.fail);
+ expect(parentContainer.innerHTML.trim()).toEqual(dummyBadgeHtml);
});
});
diff --git a/spec/frontend/groups/components/item_type_icon_spec.js b/spec/frontend/groups/components/item_type_icon_spec.js
index 9310943841e..f3652f1a410 100644
--- a/spec/frontend/groups/components/item_type_icon_spec.js
+++ b/spec/frontend/groups/components/item_type_icon_spec.js
@@ -8,7 +8,6 @@ describe('ItemTypeIcon', () => {
const defaultProps = {
itemType: ITEM_TYPE.GROUP,
- isGroupOpen: false,
};
const createComponent = (props = {}) => {
@@ -34,20 +33,14 @@ describe('ItemTypeIcon', () => {
});
it.each`
- type | isGroupOpen | icon
- ${ITEM_TYPE.GROUP} | ${true} | ${'folder-open'}
- ${ITEM_TYPE.GROUP} | ${false} | ${'folder-o'}
- ${ITEM_TYPE.PROJECT} | ${true} | ${'bookmark'}
- ${ITEM_TYPE.PROJECT} | ${false} | ${'bookmark'}
- `(
- 'shows "$icon" icon when `itemType` is "$type" and `isGroupOpen` is $isGroupOpen',
- ({ type, isGroupOpen, icon }) => {
- createComponent({
- itemType: type,
- isGroupOpen,
- });
- expect(findGlIcon().props('name')).toBe(icon);
- },
- );
+ type | icon
+ ${ITEM_TYPE.GROUP} | ${'subgroup'}
+ ${ITEM_TYPE.PROJECT} | ${'project'}
+ `('shows "$icon" icon when `itemType` is "$type"', ({ type, icon }) => {
+ createComponent({
+ itemType: type,
+ });
+ expect(findGlIcon().props('name')).toBe(icon);
+ });
});
});
diff --git a/spec/frontend/header_search/components/app_spec.js b/spec/frontend/header_search/components/app_spec.js
index dcbeeeffb2d..f0de5b083ae 100644
--- a/spec/frontend/header_search/components/app_spec.js
+++ b/spec/frontend/header_search/components/app_spec.js
@@ -16,6 +16,7 @@ import {
MOCK_USERNAME,
MOCK_DEFAULT_SEARCH_OPTIONS,
MOCK_SCOPED_SEARCH_OPTIONS,
+ MOCK_SORTED_AUTOCOMPLETE_OPTIONS,
} from '../mock_data';
Vue.use(Vuex);
@@ -108,6 +109,11 @@ describe('HeaderSearchApp', () => {
search | showDefault | showScoped | showAutocomplete | showDropdownNavigation
${null} | ${true} | ${false} | ${false} | ${true}
${''} | ${true} | ${false} | ${false} | ${true}
+ ${'1'} | ${false} | ${false} | ${false} | ${false}
+ ${')'} | ${false} | ${false} | ${false} | ${false}
+ ${'t'} | ${false} | ${false} | ${true} | ${true}
+ ${'te'} | ${false} | ${true} | ${true} | ${true}
+ ${'tes'} | ${false} | ${true} | ${true} | ${true}
${MOCK_SEARCH} | ${false} | ${true} | ${true} | ${true}
`(
'Header Search Dropdown Items',
@@ -115,7 +121,13 @@ describe('HeaderSearchApp', () => {
describe(`when search is ${search}`, () => {
beforeEach(() => {
window.gon.current_username = MOCK_USERNAME;
- createComponent({ search });
+ createComponent(
+ { search },
+ {
+ autocompleteGroupedSearchOptions: () =>
+ search.match(/^[A-Za-z]+$/g) ? MOCK_SORTED_AUTOCOMPLETE_OPTIONS : [],
+ },
+ );
findHeaderSearchInput().vm.$emit('click');
});
diff --git a/spec/frontend/header_search/components/header_search_autocomplete_items_spec.js b/spec/frontend/header_search/components/header_search_autocomplete_items_spec.js
index f427482be46..7952661e2d2 100644
--- a/spec/frontend/header_search/components/header_search_autocomplete_items_spec.js
+++ b/spec/frontend/header_search/components/header_search_autocomplete_items_spec.js
@@ -1,4 +1,4 @@
-import { GlDropdownItem, GlLoadingIcon, GlAvatar, GlAlert } from '@gitlab/ui';
+import { GlDropdownItem, GlLoadingIcon, GlAvatar, GlAlert, GlDropdownDivider } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
@@ -8,8 +8,18 @@ import {
LARGE_AVATAR_PX,
PROJECTS_CATEGORY,
SMALL_AVATAR_PX,
+ ISSUES_CATEGORY,
+ MERGE_REQUEST_CATEGORY,
+ RECENT_EPICS_CATEGORY,
} from '~/header_search/constants';
-import { MOCK_GROUPED_AUTOCOMPLETE_OPTIONS, MOCK_SORTED_AUTOCOMPLETE_OPTIONS } from '../mock_data';
+import {
+ MOCK_GROUPED_AUTOCOMPLETE_OPTIONS,
+ MOCK_SORTED_AUTOCOMPLETE_OPTIONS,
+ MOCK_GROUPED_AUTOCOMPLETE_OPTIONS_SETTINGS_HELP,
+ MOCK_GROUPED_AUTOCOMPLETE_OPTIONS_HELP,
+ MOCK_SEARCH,
+ MOCK_GROUPED_AUTOCOMPLETE_OPTIONS_2,
+} from '../mock_data';
Vue.use(Vuex);
@@ -41,8 +51,14 @@ describe('HeaderSearchAutocompleteItems', () => {
});
const findDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
+ const findGlDropdownDividers = () => wrapper.findAllComponents(GlDropdownDivider);
const findFirstDropdownItem = () => findDropdownItems().at(0);
- const findDropdownItemTitles = () => findDropdownItems().wrappers.map((w) => w.text());
+ const findDropdownItemTitles = () =>
+ findDropdownItems().wrappers.map((w) => w.findAll('span').at(1).text());
+ const findDropdownItemSubTitles = () =>
+ findDropdownItems()
+ .wrappers.filter((w) => w.findAll('span').length > 2)
+ .map((w) => w.findAll('span').at(2).text());
const findDropdownItemLinks = () => findDropdownItems().wrappers.map((w) => w.attributes('href'));
const findGlLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findGlAvatar = () => wrapper.findComponent(GlAvatar);
@@ -87,10 +103,17 @@ describe('HeaderSearchAutocompleteItems', () => {
});
it('renders titles correctly', () => {
- const expectedTitles = MOCK_SORTED_AUTOCOMPLETE_OPTIONS.map((o) => o.label);
+ const expectedTitles = MOCK_SORTED_AUTOCOMPLETE_OPTIONS.map((o) => o.value || o.label);
expect(findDropdownItemTitles()).toStrictEqual(expectedTitles);
});
+ it('renders sub-titles correctly', () => {
+ const expectedSubTitles = MOCK_SORTED_AUTOCOMPLETE_OPTIONS.filter((o) => o.value).map(
+ (o) => o.label,
+ );
+ expect(findDropdownItemSubTitles()).toStrictEqual(expectedSubTitles);
+ });
+
it('renders links correctly', () => {
const expectedLinks = MOCK_SORTED_AUTOCOMPLETE_OPTIONS.map((o) => o.url);
expect(findDropdownItemLinks()).toStrictEqual(expectedLinks);
@@ -98,15 +121,30 @@ describe('HeaderSearchAutocompleteItems', () => {
});
describe.each`
- item | showAvatar | avatarSize
- ${{ data: [{ category: PROJECTS_CATEGORY, avatar_url: null }] }} | ${true} | ${String(LARGE_AVATAR_PX)}
- ${{ data: [{ category: GROUPS_CATEGORY, avatar_url: '/123' }] }} | ${true} | ${String(LARGE_AVATAR_PX)}
- ${{ data: [{ category: 'Help', avatar_url: '' }] }} | ${true} | ${String(SMALL_AVATAR_PX)}
- ${{ data: [{ category: 'Settings' }] }} | ${false} | ${false}
- `('GlAvatar', ({ item, showAvatar, avatarSize }) => {
+ item | showAvatar | avatarSize | searchContext | entityId | entityName
+ ${{ data: [{ category: PROJECTS_CATEGORY, avatar_url: null }] }} | ${true} | ${String(LARGE_AVATAR_PX)} | ${{ project: { id: 29 } }} | ${'29'} | ${''}
+ ${{ data: [{ category: GROUPS_CATEGORY, avatar_url: '/123' }] }} | ${true} | ${String(LARGE_AVATAR_PX)} | ${{ group: { id: 12 } }} | ${'12'} | ${''}
+ ${{ data: [{ category: 'Help', avatar_url: '' }] }} | ${true} | ${String(SMALL_AVATAR_PX)} | ${null} | ${'0'} | ${''}
+ ${{ data: [{ category: 'Settings' }] }} | ${false} | ${false} | ${null} | ${false} | ${false}
+ ${{ data: [{ category: GROUPS_CATEGORY, avatar_url: null }] }} | ${true} | ${String(LARGE_AVATAR_PX)} | ${{ group: { id: 1, name: 'test1' } }} | ${'1'} | ${'test1'}
+ ${{ data: [{ category: PROJECTS_CATEGORY, avatar_url: null }] }} | ${true} | ${String(LARGE_AVATAR_PX)} | ${{ project: { id: 2, name: 'test2' } }} | ${'2'} | ${'test2'}
+ ${{ data: [{ category: ISSUES_CATEGORY, avatar_url: null }] }} | ${true} | ${String(SMALL_AVATAR_PX)} | ${{ project: { id: 3, name: 'test3' } }} | ${'3'} | ${'test3'}
+ ${{ data: [{ category: MERGE_REQUEST_CATEGORY, avatar_url: null }] }} | ${true} | ${String(SMALL_AVATAR_PX)} | ${{ project: { id: 4, name: 'test4' } }} | ${'4'} | ${'test4'}
+ ${{ data: [{ category: RECENT_EPICS_CATEGORY, avatar_url: null }] }} | ${true} | ${String(SMALL_AVATAR_PX)} | ${{ group: { id: 5, name: 'test5' } }} | ${'5'} | ${'test5'}
+ ${{ data: [{ category: GROUPS_CATEGORY, avatar_url: null, group_id: 6, group_name: 'test6' }] }} | ${true} | ${String(LARGE_AVATAR_PX)} | ${null} | ${'6'} | ${'test6'}
+ ${{ data: [{ category: PROJECTS_CATEGORY, avatar_url: null, project_id: 7, project_name: 'test7' }] }} | ${true} | ${String(LARGE_AVATAR_PX)} | ${null} | ${'7'} | ${'test7'}
+ ${{ data: [{ category: ISSUES_CATEGORY, avatar_url: null, project_id: 8, project_name: 'test8' }] }} | ${true} | ${String(SMALL_AVATAR_PX)} | ${null} | ${'8'} | ${'test8'}
+ ${{ data: [{ category: MERGE_REQUEST_CATEGORY, avatar_url: null, project_id: 9, project_name: 'test9' }] }} | ${true} | ${String(SMALL_AVATAR_PX)} | ${null} | ${'9'} | ${'test9'}
+ ${{ data: [{ category: RECENT_EPICS_CATEGORY, avatar_url: null, group_id: 10, group_name: 'test10' }] }} | ${true} | ${String(SMALL_AVATAR_PX)} | ${null} | ${'10'} | ${'test10'}
+ ${{ data: [{ category: GROUPS_CATEGORY, avatar_url: null, group_id: 11, group_name: 'test11' }] }} | ${true} | ${String(LARGE_AVATAR_PX)} | ${{ group: { id: 1, name: 'test1' } }} | ${'11'} | ${'test11'}
+ ${{ data: [{ category: PROJECTS_CATEGORY, avatar_url: null, project_id: 12, project_name: 'test12' }] }} | ${true} | ${String(LARGE_AVATAR_PX)} | ${{ project: { id: 2, name: 'test2' } }} | ${'12'} | ${'test12'}
+ ${{ data: [{ category: ISSUES_CATEGORY, avatar_url: null, project_id: 13, project_name: 'test13' }] }} | ${true} | ${String(SMALL_AVATAR_PX)} | ${{ project: { id: 3, name: 'test3' } }} | ${'13'} | ${'test13'}
+ ${{ data: [{ category: MERGE_REQUEST_CATEGORY, avatar_url: null, project_id: 14, project_name: 'test14' }] }} | ${true} | ${String(SMALL_AVATAR_PX)} | ${{ project: { id: 4, name: 'test4' } }} | ${'14'} | ${'test14'}
+ ${{ data: [{ category: RECENT_EPICS_CATEGORY, avatar_url: null, group_id: 15, group_name: 'test15' }] }} | ${true} | ${String(SMALL_AVATAR_PX)} | ${{ group: { id: 5, name: 'test5' } }} | ${'15'} | ${'test15'}
+ `('GlAvatar', ({ item, showAvatar, avatarSize, searchContext, entityId, entityName }) => {
describe(`when category is ${item.data[0].category} and avatar_url is ${item.data[0].avatar_url}`, () => {
beforeEach(() => {
- createComponent({}, { autocompleteGroupedSearchOptions: () => [item] });
+ createComponent({ searchContext }, { autocompleteGroupedSearchOptions: () => [item] });
});
it(`should${showAvatar ? '' : ' not'} render`, () => {
@@ -116,6 +154,16 @@ describe('HeaderSearchAutocompleteItems', () => {
it(`should set avatarSize to ${avatarSize}`, () => {
expect(findGlAvatar().exists() && findGlAvatar().attributes('size')).toBe(avatarSize);
});
+
+ it(`should set avatar entityId to ${entityId}`, () => {
+ expect(findGlAvatar().exists() && findGlAvatar().attributes('entityid')).toBe(entityId);
+ });
+
+ it(`should set avatar entityName to ${entityName}`, () => {
+ expect(findGlAvatar().exists() && findGlAvatar().attributes('entityname')).toBe(
+ entityName,
+ );
+ });
});
});
});
@@ -140,6 +188,34 @@ describe('HeaderSearchAutocompleteItems', () => {
});
});
});
+
+ describe.each`
+ search | items | dividerCount
+ ${null} | ${[]} | ${0}
+ ${''} | ${[]} | ${0}
+ ${'1'} | ${[]} | ${0}
+ ${')'} | ${[]} | ${0}
+ ${'t'} | ${MOCK_GROUPED_AUTOCOMPLETE_OPTIONS_SETTINGS_HELP} | ${1}
+ ${'te'} | ${MOCK_GROUPED_AUTOCOMPLETE_OPTIONS_HELP} | ${0}
+ ${'tes'} | ${MOCK_GROUPED_AUTOCOMPLETE_OPTIONS_2} | ${1}
+ ${MOCK_SEARCH} | ${MOCK_GROUPED_AUTOCOMPLETE_OPTIONS_2} | ${1}
+ `('Header Search Dropdown Dividers', ({ search, items, dividerCount }) => {
+ describe(`when search is ${search}`, () => {
+ beforeEach(() => {
+ createComponent(
+ { search },
+ {
+ autocompleteGroupedSearchOptions: () => items,
+ },
+ {},
+ );
+ });
+
+ it(`component should have ${dividerCount} dividers`, () => {
+ expect(findGlDropdownDividers()).toHaveLength(dividerCount);
+ });
+ });
+ });
});
describe('watchers', () => {
diff --git a/spec/frontend/header_search/components/header_search_scoped_items_spec.js b/spec/frontend/header_search/components/header_search_scoped_items_spec.js
index a65b4d8b813..8788fb23458 100644
--- a/spec/frontend/header_search/components/header_search_scoped_items_spec.js
+++ b/spec/frontend/header_search/components/header_search_scoped_items_spec.js
@@ -1,17 +1,21 @@
-import { GlDropdownItem } from '@gitlab/ui';
+import { GlDropdownItem, GlDropdownDivider } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import Vuex from 'vuex';
import { trimText } from 'helpers/text_helper';
import HeaderSearchScopedItems from '~/header_search/components/header_search_scoped_items.vue';
-import { MOCK_SEARCH, MOCK_SCOPED_SEARCH_OPTIONS } from '../mock_data';
+import {
+ MOCK_SEARCH,
+ MOCK_SCOPED_SEARCH_OPTIONS,
+ MOCK_GROUPED_AUTOCOMPLETE_OPTIONS,
+} from '../mock_data';
Vue.use(Vuex);
describe('HeaderSearchScopedItems', () => {
let wrapper;
- const createComponent = (initialState, props) => {
+ const createComponent = (initialState, mockGetters, props) => {
const store = new Vuex.Store({
state: {
search: MOCK_SEARCH,
@@ -19,6 +23,8 @@ describe('HeaderSearchScopedItems', () => {
},
getters: {
scopedSearchOptions: () => MOCK_SCOPED_SEARCH_OPTIONS,
+ autocompleteGroupedSearchOptions: () => MOCK_GROUPED_AUTOCOMPLETE_OPTIONS,
+ ...mockGetters,
},
});
@@ -35,6 +41,7 @@ describe('HeaderSearchScopedItems', () => {
});
const findDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
+ const findGlDropdownDivider = () => wrapper.findComponent(GlDropdownDivider);
const findFirstDropdownItem = () => findDropdownItems().at(0);
const findDropdownItemTitles = () => findDropdownItems().wrappers.map((w) => trimText(w.text()));
const findDropdownItemAriaLabels = () =>
@@ -79,7 +86,7 @@ describe('HeaderSearchScopedItems', () => {
`('isOptionFocused', ({ currentFocusedOption, isFocused, ariaSelected }) => {
describe(`when currentFocusedOption.html_id is ${currentFocusedOption?.html_id}`, () => {
beforeEach(() => {
- createComponent({}, { currentFocusedOption });
+ createComponent({}, {}, { currentFocusedOption });
});
it(`should${isFocused ? '' : ' not'} have gl-bg-gray-50 applied`, () => {
@@ -91,5 +98,21 @@ describe('HeaderSearchScopedItems', () => {
});
});
});
+
+ describe.each`
+ autosuggestResults | showDivider
+ ${[]} | ${false}
+ ${MOCK_GROUPED_AUTOCOMPLETE_OPTIONS} | ${true}
+ `('scoped search items', ({ autosuggestResults, showDivider }) => {
+ describe(`when when we have ${autosuggestResults.length} auto-sugest results`, () => {
+ beforeEach(() => {
+ createComponent({}, { autocompleteGroupedSearchOptions: () => autosuggestResults }, {});
+ });
+
+ it(`divider should${showDivider ? '' : ' not'} be shown`, () => {
+ expect(findGlDropdownDivider().exists()).toBe(showDivider);
+ });
+ });
+ });
});
});
diff --git a/spec/frontend/header_search/mock_data.js b/spec/frontend/header_search/mock_data.js
index 1d980679547..b6f0fdcc29d 100644
--- a/spec/frontend/header_search/mock_data.js
+++ b/spec/frontend/header_search/mock_data.js
@@ -96,19 +96,22 @@ export const MOCK_AUTOCOMPLETE_OPTIONS_RES = [
{
category: 'Projects',
id: 1,
- label: 'MockProject1',
+ label: 'Gitlab Org / MockProject1',
+ value: 'MockProject1',
url: 'project/1',
},
{
category: 'Groups',
id: 1,
- label: 'MockGroup1',
+ label: 'Gitlab Org / MockGroup1',
+ value: 'MockGroup1',
url: 'group/1',
},
{
category: 'Projects',
id: 2,
- label: 'MockProject2',
+ label: 'Gitlab Org / MockProject2',
+ value: 'MockProject2',
url: 'project/2',
},
{
@@ -123,21 +126,24 @@ export const MOCK_AUTOCOMPLETE_OPTIONS = [
category: 'Projects',
html_id: 'autocomplete-Projects-0',
id: 1,
- label: 'MockProject1',
+ label: 'Gitlab Org / MockProject1',
+ value: 'MockProject1',
url: 'project/1',
},
{
category: 'Groups',
html_id: 'autocomplete-Groups-1',
id: 1,
- label: 'MockGroup1',
+ label: 'Gitlab Org / MockGroup1',
+ value: 'MockGroup1',
url: 'group/1',
},
{
category: 'Projects',
html_id: 'autocomplete-Projects-2',
id: 2,
- label: 'MockProject2',
+ label: 'Gitlab Org / MockProject2',
+ value: 'MockProject2',
url: 'project/2',
},
{
@@ -157,7 +163,8 @@ export const MOCK_GROUPED_AUTOCOMPLETE_OPTIONS = [
html_id: 'autocomplete-Projects-0',
id: 1,
- label: 'MockProject1',
+ label: 'Gitlab Org / MockProject1',
+ value: 'MockProject1',
url: 'project/1',
},
{
@@ -165,7 +172,8 @@ export const MOCK_GROUPED_AUTOCOMPLETE_OPTIONS = [
html_id: 'autocomplete-Projects-2',
id: 2,
- label: 'MockProject2',
+ label: 'Gitlab Org / MockProject2',
+ value: 'MockProject2',
url: 'project/2',
},
],
@@ -178,7 +186,8 @@ export const MOCK_GROUPED_AUTOCOMPLETE_OPTIONS = [
html_id: 'autocomplete-Groups-1',
id: 1,
- label: 'MockGroup1',
+ label: 'Gitlab Org / MockGroup1',
+ value: 'MockGroup1',
url: 'group/1',
},
],
@@ -202,21 +211,24 @@ export const MOCK_SORTED_AUTOCOMPLETE_OPTIONS = [
category: 'Projects',
html_id: 'autocomplete-Projects-0',
id: 1,
- label: 'MockProject1',
+ label: 'Gitlab Org / MockProject1',
+ value: 'MockProject1',
url: 'project/1',
},
{
category: 'Projects',
html_id: 'autocomplete-Projects-2',
id: 2,
- label: 'MockProject2',
+ label: 'Gitlab Org / MockProject2',
+ value: 'MockProject2',
url: 'project/2',
},
{
category: 'Groups',
html_id: 'autocomplete-Groups-1',
id: 1,
- label: 'MockGroup1',
+ label: 'Gitlab Org / MockGroup1',
+ value: 'MockGroup1',
url: 'group/1',
},
{
@@ -226,3 +238,98 @@ export const MOCK_SORTED_AUTOCOMPLETE_OPTIONS = [
url: 'help/gitlab',
},
];
+
+export const MOCK_GROUPED_AUTOCOMPLETE_OPTIONS_HELP = [
+ {
+ category: 'Help',
+ data: [
+ {
+ html_id: 'autocomplete-Help-1',
+ category: 'Help',
+ label: 'Rake Tasks Help',
+ url: '/help/raketasks/index',
+ },
+ {
+ html_id: 'autocomplete-Help-2',
+ category: 'Help',
+ label: 'System Hooks Help',
+ url: '/help/system_hooks/system_hooks',
+ },
+ ],
+ },
+];
+
+export const MOCK_GROUPED_AUTOCOMPLETE_OPTIONS_SETTINGS_HELP = [
+ {
+ category: 'Settings',
+ data: [
+ {
+ html_id: 'autocomplete-Settings-0',
+ category: 'Settings',
+ label: 'User settings',
+ url: '/-/profile',
+ },
+ {
+ html_id: 'autocomplete-Settings-3',
+ category: 'Settings',
+ label: 'Admin Section',
+ url: '/admin',
+ },
+ ],
+ },
+ {
+ category: 'Help',
+ data: [
+ {
+ html_id: 'autocomplete-Help-1',
+ category: 'Help',
+ label: 'Rake Tasks Help',
+ url: '/help/raketasks/index',
+ },
+ {
+ html_id: 'autocomplete-Help-2',
+ category: 'Help',
+ label: 'System Hooks Help',
+ url: '/help/system_hooks/system_hooks',
+ },
+ ],
+ },
+];
+
+export const MOCK_GROUPED_AUTOCOMPLETE_OPTIONS_2 = [
+ {
+ category: 'Groups',
+ data: [
+ {
+ html_id: 'autocomplete-Groups-0',
+ category: 'Groups',
+ id: 148,
+ label: 'Jashkenas / Test Subgroup / test-subgroup',
+ url: '/jashkenas/test-subgroup/test-subgroup',
+ avatar_url: '',
+ },
+ {
+ html_id: 'autocomplete-Groups-1',
+ category: 'Groups',
+ id: 147,
+ label: 'Jashkenas / Test Subgroup',
+ url: '/jashkenas/test-subgroup',
+ avatar_url: '',
+ },
+ ],
+ },
+ {
+ category: 'Projects',
+ data: [
+ {
+ html_id: 'autocomplete-Projects-2',
+ category: 'Projects',
+ id: 1,
+ value: 'Gitlab Test',
+ label: 'Gitlab Org / Gitlab Test',
+ url: '/gitlab-org/gitlab-test',
+ avatar_url: '/uploads/-/system/project/avatar/1/icons8-gitlab-512.png',
+ },
+ ],
+ },
+];
diff --git a/spec/frontend/header_spec.js b/spec/frontend/header_spec.js
index 0d43accb7e5..937bc9aa478 100644
--- a/spec/frontend/header_spec.js
+++ b/spec/frontend/header_spec.js
@@ -60,7 +60,6 @@ describe('Header', () => {
setFixtures(`
<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>
- <a class="js-upgrade-plan-link" data-track-action="click_upgrade_link" data-track-label="free" data-track-property="user_dropdown">Upgrade</a>
</li>`);
trackingSpy = mockTracking('_category_', $('.js-nav-user-dropdown').element, jest.spyOn);
@@ -81,14 +80,5 @@ describe('Header', () => {
property: 'user_dropdown',
});
});
-
- it('sends a tracking event when the dropdown is opened and contains Upgrade link', () => {
- $('.js-nav-user-dropdown').trigger('shown.bs.dropdown');
-
- expect(trackingSpy).toHaveBeenCalledWith('some:page', 'show_upgrade_link', {
- label: 'free',
- property: 'user_dropdown',
- });
- });
});
});
diff --git a/spec/frontend/ide/components/commit_sidebar/form_spec.js b/spec/frontend/ide/components/commit_sidebar/form_spec.js
index d3b2923ac6c..28f62a9775a 100644
--- a/spec/frontend/ide/components/commit_sidebar/form_spec.js
+++ b/spec/frontend/ide/components/commit_sidebar/form_spec.js
@@ -120,7 +120,7 @@ describe('IDE commit form', () => {
it('renders commit button in compact mode', () => {
expect(findBeginCommitButton().exists()).toBe(true);
- expect(findBeginCommitButton().text()).toBe('Commit…');
+ expect(findBeginCommitButton().text()).toBe('Create commit...');
});
it('does not render form', () => {
diff --git a/spec/frontend/ide/components/ide_side_bar_spec.js b/spec/frontend/ide/components/ide_side_bar_spec.js
index 34f14ef23a4..ace8988b8c9 100644
--- a/spec/frontend/ide/components/ide_side_bar_spec.js
+++ b/spec/frontend/ide/components/ide_side_bar_spec.js
@@ -1,4 +1,4 @@
-import { GlSkeletonLoading } from '@gitlab/ui';
+import { GlDeprecatedSkeletonLoading as GlSkeletonLoading } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
diff --git a/spec/frontend/ide/components/new_dropdown/upload_spec.js b/spec/frontend/ide/components/new_dropdown/upload_spec.js
index 7303f81aad0..5a7419d6dce 100644
--- a/spec/frontend/ide/components/new_dropdown/upload_spec.js
+++ b/spec/frontend/ide/components/new_dropdown/upload_spec.js
@@ -69,25 +69,21 @@ describe('new dropdown upload', () => {
jest.spyOn(FileReader.prototype, 'readAsText');
});
- it('calls readAsText and creates file in plain text (without encoding) if the file content is plain text', (done) => {
+ 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));
vm.createFile(textTarget, textFile);
expect(FileReader.prototype.readAsText).toHaveBeenCalledWith(textFile);
- waitForCreate
- .then(() => {
- expect(vm.$emit).toHaveBeenCalledWith('create', {
- name: textFile.name,
- type: 'blob',
- content: 'plain text',
- rawPath: '',
- mimeType: 'test/mime-text',
- });
- })
- .then(done)
- .catch(done.fail);
+ await waitForCreate;
+ expect(vm.$emit).toHaveBeenCalledWith('create', {
+ name: textFile.name,
+ type: 'blob',
+ content: 'plain text',
+ rawPath: '',
+ mimeType: 'test/mime-text',
+ });
});
it('creates a blob URL for the content if binary', () => {
diff --git a/spec/frontend/ide/stores/actions/merge_request_spec.js b/spec/frontend/ide/stores/actions/merge_request_spec.js
index e62811a4517..5592e2664c4 100644
--- a/spec/frontend/ide/stores/actions/merge_request_spec.js
+++ b/spec/frontend/ide/stores/actions/merge_request_spec.js
@@ -63,56 +63,47 @@ describe('IDE store merge request actions', () => {
mock.onGet(/api\/(.*)\/projects\/abcproject\/merge_requests/).reply(200, mockData);
});
- it('calls getProjectMergeRequests service method', (done) => {
- store
- .dispatch('getMergeRequestsForBranch', { projectId: TEST_PROJECT, branchId: 'bar' })
- .then(() => {
- expect(service.getProjectMergeRequests).toHaveBeenCalledWith(TEST_PROJECT, {
- source_branch: 'bar',
- source_project_id: TEST_PROJECT_ID,
- state: 'opened',
- order_by: 'created_at',
- per_page: 1,
- });
-
- done();
- })
- .catch(done.fail);
+ it('calls getProjectMergeRequests service method', async () => {
+ await store.dispatch('getMergeRequestsForBranch', {
+ projectId: TEST_PROJECT,
+ branchId: 'bar',
+ });
+ expect(service.getProjectMergeRequests).toHaveBeenCalledWith(TEST_PROJECT, {
+ source_branch: 'bar',
+ source_project_id: TEST_PROJECT_ID,
+ state: 'opened',
+ order_by: 'created_at',
+ per_page: 1,
+ });
});
- it('sets the "Merge Request" Object', (done) => {
- store
- .dispatch('getMergeRequestsForBranch', { projectId: TEST_PROJECT, branchId: 'bar' })
- .then(() => {
- expect(store.state.projects.abcproject.mergeRequests).toEqual({
- 2: expect.objectContaining(mrData),
- });
- done();
- })
- .catch(done.fail);
+ it('sets the "Merge Request" Object', async () => {
+ await store.dispatch('getMergeRequestsForBranch', {
+ projectId: TEST_PROJECT,
+ branchId: 'bar',
+ });
+ expect(store.state.projects.abcproject.mergeRequests).toEqual({
+ 2: expect.objectContaining(mrData),
+ });
});
- it('sets "Current Merge Request" object to the most recent MR', (done) => {
- store
- .dispatch('getMergeRequestsForBranch', { projectId: TEST_PROJECT, branchId: 'bar' })
- .then(() => {
- expect(store.state.currentMergeRequestId).toEqual('2');
- done();
- })
- .catch(done.fail);
+ it('sets "Current Merge Request" object to the most recent MR', async () => {
+ await store.dispatch('getMergeRequestsForBranch', {
+ projectId: TEST_PROJECT,
+ branchId: 'bar',
+ });
+ expect(store.state.currentMergeRequestId).toEqual('2');
});
- it('does nothing if user cannot read MRs', (done) => {
+ it('does nothing if user cannot read MRs', async () => {
store.state.projects[TEST_PROJECT].userPermissions[PERMISSION_READ_MR] = false;
- store
- .dispatch('getMergeRequestsForBranch', { projectId: TEST_PROJECT, branchId: 'bar' })
- .then(() => {
- expect(service.getProjectMergeRequests).not.toHaveBeenCalled();
- expect(store.state.currentMergeRequestId).toBe('');
- })
- .then(done)
- .catch(done.fail);
+ await store.dispatch('getMergeRequestsForBranch', {
+ projectId: TEST_PROJECT,
+ branchId: 'bar',
+ });
+ expect(service.getProjectMergeRequests).not.toHaveBeenCalled();
+ expect(store.state.currentMergeRequestId).toBe('');
});
});
@@ -122,15 +113,13 @@ describe('IDE store merge request actions', () => {
mock.onGet(/api\/(.*)\/projects\/abcproject\/merge_requests/).reply(200, []);
});
- it('does not fail if there are no merge requests for current branch', (done) => {
- store
- .dispatch('getMergeRequestsForBranch', { projectId: TEST_PROJECT, branchId: 'foo' })
- .then(() => {
- expect(store.state.projects[TEST_PROJECT].mergeRequests).toEqual({});
- expect(store.state.currentMergeRequestId).toEqual('');
- done();
- })
- .catch(done.fail);
+ it('does not fail if there are no merge requests for current branch', async () => {
+ await store.dispatch('getMergeRequestsForBranch', {
+ projectId: TEST_PROJECT,
+ branchId: 'foo',
+ });
+ expect(store.state.projects[TEST_PROJECT].mergeRequests).toEqual({});
+ expect(store.state.currentMergeRequestId).toEqual('');
});
});
});
@@ -140,17 +129,18 @@ describe('IDE store merge request actions', () => {
mock.onGet(/api\/(.*)\/projects\/abcproject\/merge_requests/).networkError();
});
- it('flashes message, if error', (done) => {
- store
- .dispatch('getMergeRequestsForBranch', { projectId: TEST_PROJECT, branchId: 'bar' })
+ it('flashes message, if error', () => {
+ return store
+ .dispatch('getMergeRequestsForBranch', {
+ projectId: TEST_PROJECT,
+ branchId: 'bar',
+ })
.catch(() => {
expect(createFlash).toHaveBeenCalled();
expect(createFlash.mock.calls[0][0].message).toBe(
'Error fetching merge requests for bar',
);
- })
- .then(done)
- .catch(done.fail);
+ });
});
});
});
@@ -165,29 +155,15 @@ describe('IDE store merge request actions', () => {
.reply(200, { title: 'mergerequest' });
});
- it('calls getProjectMergeRequestData service method', (done) => {
- store
- .dispatch('getMergeRequestData', { projectId: TEST_PROJECT, mergeRequestId: 1 })
- .then(() => {
- expect(service.getProjectMergeRequestData).toHaveBeenCalledWith(TEST_PROJECT, 1);
-
- done();
- })
- .catch(done.fail);
+ it('calls getProjectMergeRequestData service method', async () => {
+ await store.dispatch('getMergeRequestData', { projectId: TEST_PROJECT, mergeRequestId: 1 });
+ expect(service.getProjectMergeRequestData).toHaveBeenCalledWith(TEST_PROJECT, 1);
});
- it('sets the Merge Request Object', (done) => {
- store
- .dispatch('getMergeRequestData', { projectId: TEST_PROJECT, mergeRequestId: 1 })
- .then(() => {
- expect(store.state.currentMergeRequestId).toBe(1);
- expect(store.state.projects[TEST_PROJECT].mergeRequests['1'].title).toBe(
- 'mergerequest',
- );
-
- done();
- })
- .catch(done.fail);
+ it('sets the Merge Request Object', async () => {
+ await store.dispatch('getMergeRequestData', { projectId: TEST_PROJECT, mergeRequestId: 1 });
+ expect(store.state.currentMergeRequestId).toBe(1);
+ expect(store.state.projects[TEST_PROJECT].mergeRequests['1'].title).toBe('mergerequest');
});
});
@@ -196,32 +172,28 @@ describe('IDE store merge request actions', () => {
mock.onGet(/api\/(.*)\/projects\/abcproject\/merge_requests\/1/).networkError();
});
- it('dispatches error action', (done) => {
+ it('dispatches error action', () => {
const dispatch = jest.fn();
- getMergeRequestData(
+ return getMergeRequestData(
{
commit() {},
dispatch,
state: store.state,
},
{ projectId: TEST_PROJECT, mergeRequestId: 1 },
- )
- .then(done.fail)
- .catch(() => {
- expect(dispatch).toHaveBeenCalledWith('setErrorMessage', {
- text: 'An error occurred while loading the merge request.',
- action: expect.any(Function),
- actionText: 'Please try again',
- actionPayload: {
- projectId: TEST_PROJECT,
- mergeRequestId: 1,
- force: false,
- },
- });
-
- done();
+ ).catch(() => {
+ expect(dispatch).toHaveBeenCalledWith('setErrorMessage', {
+ text: 'An error occurred while loading the merge request.',
+ action: expect.any(Function),
+ actionText: 'Please try again',
+ actionPayload: {
+ projectId: TEST_PROJECT,
+ mergeRequestId: 1,
+ force: false,
+ },
});
+ });
});
});
});
@@ -240,27 +212,22 @@ describe('IDE store merge request actions', () => {
.reply(200, { title: 'mergerequest' });
});
- it('calls getProjectMergeRequestChanges service method', (done) => {
- store
- .dispatch('getMergeRequestChanges', { projectId: TEST_PROJECT, mergeRequestId: 1 })
- .then(() => {
- expect(service.getProjectMergeRequestChanges).toHaveBeenCalledWith(TEST_PROJECT, 1);
-
- done();
- })
- .catch(done.fail);
+ it('calls getProjectMergeRequestChanges service method', async () => {
+ await store.dispatch('getMergeRequestChanges', {
+ projectId: TEST_PROJECT,
+ mergeRequestId: 1,
+ });
+ expect(service.getProjectMergeRequestChanges).toHaveBeenCalledWith(TEST_PROJECT, 1);
});
- it('sets the Merge Request Changes Object', (done) => {
- store
- .dispatch('getMergeRequestChanges', { projectId: TEST_PROJECT, mergeRequestId: 1 })
- .then(() => {
- expect(store.state.projects[TEST_PROJECT].mergeRequests['1'].changes.title).toBe(
- 'mergerequest',
- );
- done();
- })
- .catch(done.fail);
+ it('sets the Merge Request Changes Object', async () => {
+ await store.dispatch('getMergeRequestChanges', {
+ projectId: TEST_PROJECT,
+ mergeRequestId: 1,
+ });
+ expect(store.state.projects[TEST_PROJECT].mergeRequests['1'].changes.title).toBe(
+ 'mergerequest',
+ );
});
});
@@ -269,32 +236,30 @@ describe('IDE store merge request actions', () => {
mock.onGet(/api\/(.*)\/projects\/abcproject\/merge_requests\/1\/changes/).networkError();
});
- it('dispatches error action', (done) => {
+ it('dispatches error action', async () => {
const dispatch = jest.fn();
- getMergeRequestChanges(
- {
- commit() {},
- dispatch,
- state: store.state,
+ await expect(
+ getMergeRequestChanges(
+ {
+ commit() {},
+ dispatch,
+ state: store.state,
+ },
+ { projectId: TEST_PROJECT, mergeRequestId: 1 },
+ ),
+ ).rejects.toEqual(new Error('Merge request changes not loaded abcproject'));
+
+ expect(dispatch).toHaveBeenCalledWith('setErrorMessage', {
+ text: 'An error occurred while loading the merge request changes.',
+ action: expect.any(Function),
+ actionText: 'Please try again',
+ actionPayload: {
+ projectId: TEST_PROJECT,
+ mergeRequestId: 1,
+ force: false,
},
- { projectId: TEST_PROJECT, mergeRequestId: 1 },
- )
- .then(done.fail)
- .catch(() => {
- expect(dispatch).toHaveBeenCalledWith('setErrorMessage', {
- text: 'An error occurred while loading the merge request changes.',
- action: expect.any(Function),
- actionText: 'Please try again',
- actionPayload: {
- projectId: TEST_PROJECT,
- mergeRequestId: 1,
- force: false,
- },
- });
-
- done();
- });
+ });
});
});
});
@@ -312,25 +277,20 @@ describe('IDE store merge request actions', () => {
jest.spyOn(service, 'getProjectMergeRequestVersions');
});
- it('calls getProjectMergeRequestVersions service method', (done) => {
- store
- .dispatch('getMergeRequestVersions', { projectId: TEST_PROJECT, mergeRequestId: 1 })
- .then(() => {
- expect(service.getProjectMergeRequestVersions).toHaveBeenCalledWith(TEST_PROJECT, 1);
-
- done();
- })
- .catch(done.fail);
+ it('calls getProjectMergeRequestVersions service method', async () => {
+ await store.dispatch('getMergeRequestVersions', {
+ projectId: TEST_PROJECT,
+ mergeRequestId: 1,
+ });
+ expect(service.getProjectMergeRequestVersions).toHaveBeenCalledWith(TEST_PROJECT, 1);
});
- it('sets the Merge Request Versions Object', (done) => {
- store
- .dispatch('getMergeRequestVersions', { projectId: TEST_PROJECT, mergeRequestId: 1 })
- .then(() => {
- expect(store.state.projects[TEST_PROJECT].mergeRequests['1'].versions.length).toBe(1);
- done();
- })
- .catch(done.fail);
+ it('sets the Merge Request Versions Object', async () => {
+ await store.dispatch('getMergeRequestVersions', {
+ projectId: TEST_PROJECT,
+ mergeRequestId: 1,
+ });
+ expect(store.state.projects[TEST_PROJECT].mergeRequests['1'].versions.length).toBe(1);
});
});
@@ -339,32 +299,28 @@ describe('IDE store merge request actions', () => {
mock.onGet(/api\/(.*)\/projects\/abcproject\/merge_requests\/1\/versions/).networkError();
});
- it('dispatches error action', (done) => {
+ it('dispatches error action', () => {
const dispatch = jest.fn();
- getMergeRequestVersions(
+ return getMergeRequestVersions(
{
commit() {},
dispatch,
state: store.state,
},
{ projectId: TEST_PROJECT, mergeRequestId: 1 },
- )
- .then(done.fail)
- .catch(() => {
- expect(dispatch).toHaveBeenCalledWith('setErrorMessage', {
- text: 'An error occurred while loading the merge request version data.',
- action: expect.any(Function),
- actionText: 'Please try again',
- actionPayload: {
- projectId: TEST_PROJECT,
- mergeRequestId: 1,
- force: false,
- },
- });
-
- done();
+ ).catch(() => {
+ expect(dispatch).toHaveBeenCalledWith('setErrorMessage', {
+ text: 'An error occurred while loading the merge request version data.',
+ action: expect.any(Function),
+ actionText: 'Please try again',
+ actionPayload: {
+ projectId: TEST_PROJECT,
+ mergeRequestId: 1,
+ force: false,
+ },
});
+ });
});
});
});
@@ -503,37 +459,36 @@ describe('IDE store merge request actions', () => {
);
});
- it('dispatches actions for merge request data', (done) => {
- openMergeRequest({ state: store.state, dispatch: store.dispatch, getters: mockGetters }, mr)
- .then(() => {
- expect(store.dispatch.mock.calls).toEqual([
- ['getMergeRequestData', mr],
- ['setCurrentBranchId', testMergeRequest.source_branch],
- [
- 'getBranchData',
- {
- projectId: mr.projectId,
- branchId: testMergeRequest.source_branch,
- },
- ],
- [
- 'getFiles',
- {
- projectId: mr.projectId,
- branchId: testMergeRequest.source_branch,
- ref: 'abcd2322',
- },
- ],
- ['getMergeRequestVersions', mr],
- ['getMergeRequestChanges', mr],
- ['openMergeRequestChanges', testMergeRequestChanges.changes],
- ]);
- })
- .then(done)
- .catch(done.fail);
+ it('dispatches actions for merge request data', async () => {
+ await openMergeRequest(
+ { state: store.state, dispatch: store.dispatch, getters: mockGetters },
+ mr,
+ );
+ expect(store.dispatch.mock.calls).toEqual([
+ ['getMergeRequestData', mr],
+ ['setCurrentBranchId', testMergeRequest.source_branch],
+ [
+ 'getBranchData',
+ {
+ projectId: mr.projectId,
+ branchId: testMergeRequest.source_branch,
+ },
+ ],
+ [
+ 'getFiles',
+ {
+ projectId: mr.projectId,
+ branchId: testMergeRequest.source_branch,
+ ref: 'abcd2322',
+ },
+ ],
+ ['getMergeRequestVersions', mr],
+ ['getMergeRequestChanges', mr],
+ ['openMergeRequestChanges', testMergeRequestChanges.changes],
+ ]);
});
- it('updates activity bar view and gets file data, if changes are found', (done) => {
+ it('updates activity bar view and gets file data, if changes are found', async () => {
store.state.entries.foo = {
type: 'blob',
path: 'foo',
@@ -548,28 +503,24 @@ describe('IDE store merge request actions', () => {
{ new_path: 'bar', path: 'bar' },
];
- openMergeRequest({ state: store.state, dispatch: store.dispatch, getters: mockGetters }, mr)
- .then(() => {
- expect(store.dispatch).toHaveBeenCalledWith(
- 'openMergeRequestChanges',
- testMergeRequestChanges.changes,
- );
- })
- .then(done)
- .catch(done.fail);
+ await openMergeRequest(
+ { state: store.state, dispatch: store.dispatch, getters: mockGetters },
+ mr,
+ );
+ expect(store.dispatch).toHaveBeenCalledWith(
+ 'openMergeRequestChanges',
+ testMergeRequestChanges.changes,
+ );
});
- it('flashes message, if error', (done) => {
+ it('flashes message, if error', () => {
store.dispatch.mockRejectedValue();
- openMergeRequest(store, mr)
- .catch(() => {
- expect(createFlash).toHaveBeenCalledWith({
- message: expect.any(String),
- });
- })
- .then(done)
- .catch(done.fail);
+ return openMergeRequest(store, mr).catch(() => {
+ expect(createFlash).toHaveBeenCalledWith({
+ message: expect.any(String),
+ });
+ });
});
});
});
diff --git a/spec/frontend/ide/stores/actions/project_spec.js b/spec/frontend/ide/stores/actions/project_spec.js
index e07dcf22860..cc7d39b4d43 100644
--- a/spec/frontend/ide/stores/actions/project_spec.js
+++ b/spec/frontend/ide/stores/actions/project_spec.js
@@ -146,22 +146,16 @@ describe('IDE store project actions', () => {
});
});
- it('calls the service', (done) => {
- store
- .dispatch('refreshLastCommitData', {
- projectId: store.state.currentProjectId,
- branchId: store.state.currentBranchId,
- })
- .then(() => {
- expect(service.getBranchData).toHaveBeenCalledWith('abc/def', 'main');
-
- done();
- })
- .catch(done.fail);
+ it('calls the service', async () => {
+ await store.dispatch('refreshLastCommitData', {
+ projectId: store.state.currentProjectId,
+ branchId: store.state.currentBranchId,
+ });
+ expect(service.getBranchData).toHaveBeenCalledWith('abc/def', 'main');
});
- it('commits getBranchData', (done) => {
- testAction(
+ it('commits getBranchData', () => {
+ return testAction(
refreshLastCommitData,
{
projectId: store.state.currentProjectId,
@@ -181,14 +175,13 @@ describe('IDE store project actions', () => {
],
// action
[],
- done,
);
});
});
describe('showBranchNotFoundError', () => {
- it('dispatches setErrorMessage', (done) => {
- testAction(
+ it('dispatches setErrorMessage', () => {
+ return testAction(
showBranchNotFoundError,
'main',
null,
@@ -204,7 +197,6 @@ describe('IDE store project actions', () => {
},
},
],
- done,
);
});
});
@@ -216,8 +208,8 @@ describe('IDE store project actions', () => {
jest.spyOn(api, 'createBranch').mockResolvedValue();
});
- it('calls API', (done) => {
- createNewBranchFromDefault(
+ it('calls API', async () => {
+ await createNewBranchFromDefault(
{
state: {
currentProjectId: 'project-path',
@@ -230,21 +222,17 @@ describe('IDE store project actions', () => {
dispatch() {},
},
'new-branch-name',
- )
- .then(() => {
- expect(api.createBranch).toHaveBeenCalledWith('project-path', {
- ref: 'main',
- branch: 'new-branch-name',
- });
- })
- .then(done)
- .catch(done.fail);
+ );
+ expect(api.createBranch).toHaveBeenCalledWith('project-path', {
+ ref: 'main',
+ branch: 'new-branch-name',
+ });
});
- it('clears error message', (done) => {
+ it('clears error message', async () => {
const dispatchSpy = jest.fn().mockName('dispatch');
- createNewBranchFromDefault(
+ await createNewBranchFromDefault(
{
state: {
currentProjectId: 'project-path',
@@ -257,16 +245,12 @@ describe('IDE store project actions', () => {
dispatch: dispatchSpy,
},
'new-branch-name',
- )
- .then(() => {
- expect(dispatchSpy).toHaveBeenCalledWith('setErrorMessage', null);
- })
- .then(done)
- .catch(done.fail);
+ );
+ expect(dispatchSpy).toHaveBeenCalledWith('setErrorMessage', null);
});
- it('reloads window', (done) => {
- createNewBranchFromDefault(
+ it('reloads window', async () => {
+ await createNewBranchFromDefault(
{
state: {
currentProjectId: 'project-path',
@@ -279,18 +263,14 @@ describe('IDE store project actions', () => {
dispatch() {},
},
'new-branch-name',
- )
- .then(() => {
- expect(window.location.reload).toHaveBeenCalled();
- })
- .then(done)
- .catch(done.fail);
+ );
+ expect(window.location.reload).toHaveBeenCalled();
});
});
describe('loadEmptyBranch', () => {
- it('creates a blank tree and sets loading state to false', (done) => {
- testAction(
+ it('creates a blank tree and sets loading state to false', () => {
+ return testAction(
loadEmptyBranch,
{ projectId: TEST_PROJECT_ID, branchId: 'main' },
store.state,
@@ -302,20 +282,18 @@ describe('IDE store project actions', () => {
},
],
expect.any(Object),
- done,
);
});
- it('does nothing, if tree already exists', (done) => {
+ it('does nothing, if tree already exists', () => {
const trees = { [`${TEST_PROJECT_ID}/main`]: [] };
- testAction(
+ return testAction(
loadEmptyBranch,
{ projectId: TEST_PROJECT_ID, branchId: 'main' },
{ trees },
[],
[],
- done,
);
});
});
@@ -372,56 +350,48 @@ describe('IDE store project actions', () => {
const branchId = '123-lorem';
const ref = 'abcd2322';
- it('when empty repo, loads empty branch', (done) => {
+ it('when empty repo, loads empty branch', () => {
const mockGetters = { emptyRepo: true };
- testAction(
+ return testAction(
loadBranch,
{ projectId, branchId },
{ ...store.state, ...mockGetters },
[],
[{ type: 'loadEmptyBranch', payload: { projectId, branchId } }],
- done,
);
});
- it('when branch already exists, does nothing', (done) => {
+ it('when branch already exists, does nothing', () => {
store.state.projects[projectId].branches[branchId] = {};
- testAction(loadBranch, { projectId, branchId }, store.state, [], [], done);
+ return testAction(loadBranch, { projectId, branchId }, store.state, [], []);
});
- it('fetches branch data', (done) => {
+ it('fetches branch data', async () => {
const mockGetters = { findBranch: () => ({ commit: { id: ref } }) };
jest.spyOn(store, 'dispatch').mockResolvedValue();
- loadBranch(
+ await loadBranch(
{ getters: mockGetters, state: store.state, dispatch: store.dispatch },
{ projectId, branchId },
- )
- .then(() => {
- expect(store.dispatch.mock.calls).toEqual([
- ['getBranchData', { projectId, branchId }],
- ['getMergeRequestsForBranch', { projectId, branchId }],
- ['getFiles', { projectId, branchId, ref }],
- ]);
- })
- .then(done)
- .catch(done.fail);
+ );
+ expect(store.dispatch.mock.calls).toEqual([
+ ['getBranchData', { projectId, branchId }],
+ ['getMergeRequestsForBranch', { projectId, branchId }],
+ ['getFiles', { projectId, branchId, ref }],
+ ]);
});
- it('shows an error if branch can not be fetched', (done) => {
+ it('shows an error if branch can not be fetched', async () => {
jest.spyOn(store, 'dispatch').mockReturnValue(Promise.reject());
- loadBranch(store, { projectId, branchId })
- .then(done.fail)
- .catch(() => {
- expect(store.dispatch.mock.calls).toEqual([
- ['getBranchData', { projectId, branchId }],
- ['showBranchNotFoundError', branchId],
- ]);
- done();
- });
+ await expect(loadBranch(store, { projectId, branchId })).rejects.toBeUndefined();
+
+ expect(store.dispatch.mock.calls).toEqual([
+ ['getBranchData', { projectId, branchId }],
+ ['showBranchNotFoundError', branchId],
+ ]);
});
});
@@ -449,17 +419,13 @@ describe('IDE store project actions', () => {
jest.spyOn(store, 'dispatch').mockResolvedValue();
});
- it('dispatches branch actions', (done) => {
- openBranch(store, branch)
- .then(() => {
- expect(store.dispatch.mock.calls).toEqual([
- ['setCurrentBranchId', branchId],
- ['loadBranch', { projectId, branchId }],
- ['loadFile', { basePath: undefined }],
- ]);
- })
- .then(done)
- .catch(done.fail);
+ it('dispatches branch actions', async () => {
+ await openBranch(store, branch);
+ expect(store.dispatch.mock.calls).toEqual([
+ ['setCurrentBranchId', branchId],
+ ['loadBranch', { projectId, branchId }],
+ ['loadFile', { basePath: undefined }],
+ ]);
});
});
@@ -468,22 +434,18 @@ describe('IDE store project actions', () => {
jest.spyOn(store, 'dispatch').mockReturnValue(Promise.reject());
});
- it('dispatches correct branch actions', (done) => {
- openBranch(store, branch)
- .then((val) => {
- expect(store.dispatch.mock.calls).toEqual([
- ['setCurrentBranchId', branchId],
- ['loadBranch', { projectId, branchId }],
- ]);
-
- expect(val).toEqual(
- new Error(
- `An error occurred while getting files for - <strong>${projectId}/${branchId}</strong>`,
- ),
- );
- })
- .then(done)
- .catch(done.fail);
+ it('dispatches correct branch actions', async () => {
+ const val = await openBranch(store, branch);
+ expect(store.dispatch.mock.calls).toEqual([
+ ['setCurrentBranchId', branchId],
+ ['loadBranch', { projectId, branchId }],
+ ]);
+
+ expect(val).toEqual(
+ new Error(
+ `An error occurred while getting files for - <strong>${projectId}/${branchId}</strong>`,
+ ),
+ );
});
});
});
diff --git a/spec/frontend/ide/stores/actions/tree_spec.js b/spec/frontend/ide/stores/actions/tree_spec.js
index 8d7328725e9..fc44cbb21ae 100644
--- a/spec/frontend/ide/stores/actions/tree_spec.js
+++ b/spec/frontend/ide/stores/actions/tree_spec.js
@@ -62,27 +62,21 @@ describe('Multi-file store tree actions', () => {
});
});
- it('adds data into tree', (done) => {
- store
- .dispatch('getFiles', basicCallParameters)
- .then(() => {
- projectTree = store.state.trees['abcproject/main'];
-
- expect(projectTree.tree.length).toBe(2);
- expect(projectTree.tree[0].type).toBe('tree');
- expect(projectTree.tree[0].tree[1].name).toBe('fileinfolder.js');
- expect(projectTree.tree[1].type).toBe('blob');
- expect(projectTree.tree[0].tree[0].tree[0].type).toBe('blob');
- expect(projectTree.tree[0].tree[0].tree[0].name).toBe('fileinsubfolder.js');
-
- done();
- })
- .catch(done.fail);
+ it('adds data into tree', async () => {
+ await store.dispatch('getFiles', basicCallParameters);
+ projectTree = store.state.trees['abcproject/main'];
+
+ expect(projectTree.tree.length).toBe(2);
+ expect(projectTree.tree[0].type).toBe('tree');
+ expect(projectTree.tree[0].tree[1].name).toBe('fileinfolder.js');
+ expect(projectTree.tree[1].type).toBe('blob');
+ expect(projectTree.tree[0].tree[0].tree[0].type).toBe('blob');
+ expect(projectTree.tree[0].tree[0].tree[0].name).toBe('fileinsubfolder.js');
});
});
describe('error', () => {
- it('dispatches error action', (done) => {
+ it('dispatches error action', async () => {
const dispatch = jest.fn();
store.state.projects = {
@@ -103,28 +97,26 @@ describe('Multi-file store tree actions', () => {
mock.onGet(/(.*)/).replyOnce(500);
- getFiles(
- {
- commit() {},
- dispatch,
- state: store.state,
- getters,
- },
- {
- projectId: 'abc/def',
- branchId: 'main-testing',
- },
- )
- .then(done.fail)
- .catch(() => {
- expect(dispatch).toHaveBeenCalledWith('setErrorMessage', {
- text: 'An error occurred while loading all the files.',
- action: expect.any(Function),
- actionText: 'Please try again',
- actionPayload: { projectId: 'abc/def', branchId: 'main-testing' },
- });
- done();
- });
+ await expect(
+ getFiles(
+ {
+ commit() {},
+ dispatch,
+ state: store.state,
+ getters,
+ },
+ {
+ projectId: 'abc/def',
+ branchId: 'main-testing',
+ },
+ ),
+ ).rejects.toEqual(new Error('Request failed with status code 500'));
+ expect(dispatch).toHaveBeenCalledWith('setErrorMessage', {
+ text: 'An error occurred while loading all the files.',
+ action: expect.any(Function),
+ actionText: 'Please try again',
+ actionPayload: { projectId: 'abc/def', branchId: 'main-testing' },
+ });
});
});
});
@@ -137,15 +129,9 @@ describe('Multi-file store tree actions', () => {
store.state.entries[tree.path] = tree;
});
- it('toggles the tree open', (done) => {
- store
- .dispatch('toggleTreeOpen', tree.path)
- .then(() => {
- expect(tree.opened).toBeTruthy();
-
- done();
- })
- .catch(done.fail);
+ it('toggles the tree open', async () => {
+ await store.dispatch('toggleTreeOpen', tree.path);
+ expect(tree.opened).toBeTruthy();
});
});
@@ -163,24 +149,23 @@ describe('Multi-file store tree actions', () => {
Object.assign(store.state.entries, createEntriesFromPaths(paths));
});
- it('opens the parents', (done) => {
- testAction(
+ it('opens the parents', () => {
+ return testAction(
showTreeEntry,
'grandparent/parent/child.txt',
store.state,
[{ type: types.SET_TREE_OPEN, payload: 'grandparent/parent' }],
[{ type: 'showTreeEntry', payload: 'grandparent/parent' }],
- done,
);
});
});
describe('setDirectoryData', () => {
- it('sets tree correctly if there are no opened files yet', (done) => {
+ it('sets tree correctly if there are no opened files yet', () => {
const treeFile = file({ name: 'README.md' });
store.state.trees['abcproject/main'] = {};
- testAction(
+ return testAction(
setDirectoryData,
{ projectId: 'abcproject', branchId: 'main', treeList: [treeFile] },
store.state,
@@ -201,7 +186,6 @@ describe('Multi-file store tree actions', () => {
},
],
[],
- done,
);
});
});
diff --git a/spec/frontend/ide/stores/actions_spec.js b/spec/frontend/ide/stores/actions_spec.js
index be43c618095..3889c4f11c3 100644
--- a/spec/frontend/ide/stores/actions_spec.js
+++ b/spec/frontend/ide/stores/actions_spec.js
@@ -43,15 +43,9 @@ describe('Multi-file store actions', () => {
});
describe('redirectToUrl', () => {
- it('calls visitUrl', (done) => {
- store
- .dispatch('redirectToUrl', 'test')
- .then(() => {
- expect(visitUrl).toHaveBeenCalledWith('test');
-
- done();
- })
- .catch(done.fail);
+ it('calls visitUrl', async () => {
+ await store.dispatch('redirectToUrl', 'test');
+ expect(visitUrl).toHaveBeenCalledWith('test');
});
});
@@ -89,15 +83,10 @@ describe('Multi-file store actions', () => {
expect(store.dispatch.mock.calls).toEqual(expect.arrayContaining(expectedCalls));
});
- it('removes all files from changedFiles state', (done) => {
- store
- .dispatch('discardAllChanges')
- .then(() => {
- expect(store.state.changedFiles.length).toBe(0);
- expect(store.state.openFiles.length).toBe(2);
- })
- .then(done)
- .catch(done.fail);
+ it('removes all files from changedFiles state', async () => {
+ await store.dispatch('discardAllChanges');
+ expect(store.state.changedFiles.length).toBe(0);
+ expect(store.state.openFiles.length).toBe(2);
});
});
@@ -121,24 +110,18 @@ describe('Multi-file store actions', () => {
});
describe('tree', () => {
- it('creates temp tree', (done) => {
- store
- .dispatch('createTempEntry', {
- name: 'test',
- type: 'tree',
- })
- .then(() => {
- const entry = store.state.entries.test;
-
- expect(entry).not.toBeNull();
- expect(entry.type).toBe('tree');
+ it('creates temp tree', async () => {
+ await store.dispatch('createTempEntry', {
+ name: 'test',
+ type: 'tree',
+ });
+ const entry = store.state.entries.test;
- done();
- })
- .catch(done.fail);
+ expect(entry).not.toBeNull();
+ expect(entry.type).toBe('tree');
});
- it('creates new folder inside another tree', (done) => {
+ it('creates new folder inside another tree', async () => {
const tree = {
type: 'tree',
name: 'testing',
@@ -148,22 +131,16 @@ describe('Multi-file store actions', () => {
store.state.entries[tree.path] = tree;
- store
- .dispatch('createTempEntry', {
- name: 'testing/test',
- type: 'tree',
- })
- .then(() => {
- expect(tree.tree[0].tempFile).toBeTruthy();
- expect(tree.tree[0].name).toBe('test');
- expect(tree.tree[0].type).toBe('tree');
-
- done();
- })
- .catch(done.fail);
+ await store.dispatch('createTempEntry', {
+ name: 'testing/test',
+ type: 'tree',
+ });
+ expect(tree.tree[0].tempFile).toBeTruthy();
+ expect(tree.tree[0].name).toBe('test');
+ expect(tree.tree[0].type).toBe('tree');
});
- it('does not create new tree if already exists', (done) => {
+ it('does not create new tree if already exists', async () => {
const tree = {
type: 'tree',
path: 'testing',
@@ -173,76 +150,52 @@ describe('Multi-file store actions', () => {
store.state.entries[tree.path] = tree;
- store
- .dispatch('createTempEntry', {
- name: 'testing',
- type: 'tree',
- })
- .then(() => {
- expect(store.state.entries[tree.path].tempFile).toEqual(false);
- expect(document.querySelector('.flash-alert')).not.toBeNull();
-
- done();
- })
- .catch(done.fail);
+ await store.dispatch('createTempEntry', {
+ name: 'testing',
+ type: 'tree',
+ });
+ expect(store.state.entries[tree.path].tempFile).toEqual(false);
+ expect(document.querySelector('.flash-alert')).not.toBeNull();
});
});
describe('blob', () => {
- it('creates temp file', (done) => {
+ it('creates temp file', async () => {
const name = 'test';
- store
- .dispatch('createTempEntry', {
- name,
- type: 'blob',
- mimeType: 'test/mime',
- })
- .then(() => {
- const f = store.state.entries[name];
-
- expect(f.tempFile).toBeTruthy();
- expect(f.mimeType).toBe('test/mime');
- expect(store.state.trees['abcproject/mybranch'].tree.length).toBe(1);
-
- done();
- })
- .catch(done.fail);
+ await store.dispatch('createTempEntry', {
+ name,
+ type: 'blob',
+ mimeType: 'test/mime',
+ });
+ const f = store.state.entries[name];
+
+ expect(f.tempFile).toBeTruthy();
+ expect(f.mimeType).toBe('test/mime');
+ expect(store.state.trees['abcproject/mybranch'].tree.length).toBe(1);
});
- it('adds tmp file to open files', (done) => {
+ it('adds tmp file to open files', async () => {
const name = 'test';
- store
- .dispatch('createTempEntry', {
- name,
- type: 'blob',
- })
- .then(() => {
- const f = store.state.entries[name];
-
- expect(store.state.openFiles.length).toBe(1);
- expect(store.state.openFiles[0].name).toBe(f.name);
+ await store.dispatch('createTempEntry', {
+ name,
+ type: 'blob',
+ });
+ const f = store.state.entries[name];
- done();
- })
- .catch(done.fail);
+ expect(store.state.openFiles.length).toBe(1);
+ expect(store.state.openFiles[0].name).toBe(f.name);
});
- it('adds tmp file to staged files', (done) => {
+ it('adds tmp file to staged files', async () => {
const name = 'test';
- store
- .dispatch('createTempEntry', {
- name,
- type: 'blob',
- })
- .then(() => {
- expect(store.state.stagedFiles).toEqual([expect.objectContaining({ name })]);
-
- done();
- })
- .catch(done.fail);
+ await store.dispatch('createTempEntry', {
+ name,
+ type: 'blob',
+ });
+ expect(store.state.stagedFiles).toEqual([expect.objectContaining({ name })]);
});
it('sets tmp file as active', () => {
@@ -251,24 +204,18 @@ describe('Multi-file store actions', () => {
expect(store.dispatch).toHaveBeenCalledWith('setFileActive', 'test');
});
- it('creates flash message if file already exists', (done) => {
+ it('creates flash message if file already exists', async () => {
const f = file('test', '1', 'blob');
store.state.trees['abcproject/mybranch'].tree = [f];
store.state.entries[f.path] = f;
- store
- .dispatch('createTempEntry', {
- name: 'test',
- type: 'blob',
- })
- .then(() => {
- expect(document.querySelector('.flash-alert')?.textContent.trim()).toEqual(
- `The name "${f.name}" is already taken in this directory.`,
- );
-
- done();
- })
- .catch(done.fail);
+ await store.dispatch('createTempEntry', {
+ name: 'test',
+ type: 'blob',
+ });
+ expect(document.querySelector('.flash-alert')?.textContent.trim()).toEqual(
+ `The name "${f.name}" is already taken in this directory.`,
+ );
});
});
});
@@ -372,45 +319,38 @@ describe('Multi-file store actions', () => {
});
describe('updateViewer', () => {
- it('updates viewer state', (done) => {
- store
- .dispatch('updateViewer', 'diff')
- .then(() => {
- expect(store.state.viewer).toBe('diff');
- })
- .then(done)
- .catch(done.fail);
+ it('updates viewer state', async () => {
+ await store.dispatch('updateViewer', 'diff');
+ expect(store.state.viewer).toBe('diff');
});
});
describe('updateActivityBarView', () => {
- it('commits UPDATE_ACTIVITY_BAR_VIEW', (done) => {
- testAction(
+ it('commits UPDATE_ACTIVITY_BAR_VIEW', () => {
+ return testAction(
updateActivityBarView,
'test',
{},
[{ type: 'UPDATE_ACTIVITY_BAR_VIEW', payload: 'test' }],
[],
- done,
);
});
});
describe('setEmptyStateSvgs', () => {
- it('commits setEmptyStateSvgs', (done) => {
- testAction(
+ it('commits setEmptyStateSvgs', () => {
+ return testAction(
setEmptyStateSvgs,
'svg',
{},
[{ type: 'SET_EMPTY_STATE_SVGS', payload: 'svg' }],
[],
- done,
);
});
});
describe('updateTempFlagForEntry', () => {
- it('commits UPDATE_TEMP_FLAG', (done) => {
+ it('commits UPDATE_TEMP_FLAG', () => {
const f = {
...file(),
path: 'test',
@@ -418,17 +358,16 @@ describe('Multi-file store actions', () => {
};
store.state.entries[f.path] = f;
- testAction(
+ return testAction(
updateTempFlagForEntry,
{ file: f, tempFile: false },
store.state,
[{ type: 'UPDATE_TEMP_FLAG', payload: { path: f.path, tempFile: false } }],
[],
- done,
);
});
- it('commits UPDATE_TEMP_FLAG and dispatches for parent', (done) => {
+ it('commits UPDATE_TEMP_FLAG and dispatches for parent', () => {
const parent = {
...file(),
path: 'testing',
@@ -441,17 +380,16 @@ describe('Multi-file store actions', () => {
store.state.entries[parent.path] = parent;
store.state.entries[f.path] = f;
- testAction(
+ return testAction(
updateTempFlagForEntry,
{ file: f, tempFile: false },
store.state,
[{ type: 'UPDATE_TEMP_FLAG', payload: { path: f.path, tempFile: false } }],
[{ type: 'updateTempFlagForEntry', payload: { file: parent, tempFile: false } }],
- done,
);
});
- it('does not dispatch for parent, if parent does not exist', (done) => {
+ it('does not dispatch for parent, if parent does not exist', () => {
const f = {
...file(),
path: 'test',
@@ -459,71 +397,66 @@ describe('Multi-file store actions', () => {
};
store.state.entries[f.path] = f;
- testAction(
+ return testAction(
updateTempFlagForEntry,
{ file: f, tempFile: false },
store.state,
[{ type: 'UPDATE_TEMP_FLAG', payload: { path: f.path, tempFile: false } }],
[],
- done,
);
});
});
describe('setCurrentBranchId', () => {
- it('commits setCurrentBranchId', (done) => {
- testAction(
+ it('commits setCurrentBranchId', () => {
+ return testAction(
setCurrentBranchId,
'branchId',
{},
[{ type: 'SET_CURRENT_BRANCH', payload: 'branchId' }],
[],
- done,
);
});
});
describe('toggleFileFinder', () => {
- it('commits TOGGLE_FILE_FINDER', (done) => {
- testAction(
+ it('commits TOGGLE_FILE_FINDER', () => {
+ return testAction(
toggleFileFinder,
true,
null,
[{ type: 'TOGGLE_FILE_FINDER', payload: true }],
[],
- done,
);
});
});
describe('setErrorMessage', () => {
- it('commis error messsage', (done) => {
- testAction(
+ it('commis error messsage', () => {
+ return testAction(
setErrorMessage,
'error',
null,
[{ type: types.SET_ERROR_MESSAGE, payload: 'error' }],
[],
- done,
);
});
});
describe('deleteEntry', () => {
- it('commits entry deletion', (done) => {
+ it('commits entry deletion', () => {
store.state.entries.path = 'testing';
- testAction(
+ return testAction(
deleteEntry,
'path',
store.state,
[{ type: types.DELETE_ENTRY, payload: 'path' }],
[{ type: 'stageChange', payload: 'path' }, createTriggerChangeAction()],
- done,
);
});
- it('does not delete a folder after it is emptied', (done) => {
+ it('does not delete a folder after it is emptied', () => {
const testFolder = {
type: 'tree',
tree: [],
@@ -540,7 +473,7 @@ describe('Multi-file store actions', () => {
'testFolder/entry-to-delete': testEntry,
};
- testAction(
+ return testAction(
deleteEntry,
'testFolder/entry-to-delete',
store.state,
@@ -549,7 +482,6 @@ describe('Multi-file store actions', () => {
{ type: 'stageChange', payload: 'testFolder/entry-to-delete' },
createTriggerChangeAction(),
],
- done,
);
});
@@ -569,8 +501,8 @@ describe('Multi-file store actions', () => {
});
describe('and previous does not exist', () => {
- it('reverts the rename before deleting', (done) => {
- testAction(
+ it('reverts the rename before deleting', () => {
+ return testAction(
deleteEntry,
testEntry.path,
store.state,
@@ -589,7 +521,6 @@ describe('Multi-file store actions', () => {
payload: testEntry.prevPath,
},
],
- done,
);
});
});
@@ -604,21 +535,20 @@ describe('Multi-file store actions', () => {
store.state.entries[oldEntry.path] = oldEntry;
});
- it('does not revert rename before deleting', (done) => {
- testAction(
+ it('does not revert rename before deleting', () => {
+ return testAction(
deleteEntry,
testEntry.path,
store.state,
[{ type: types.DELETE_ENTRY, payload: testEntry.path }],
[{ type: 'stageChange', payload: testEntry.path }, createTriggerChangeAction()],
- done,
);
});
- it('when previous is deleted, it reverts rename before deleting', (done) => {
+ it('when previous is deleted, it reverts rename before deleting', () => {
store.state.entries[testEntry.prevPath].deleted = true;
- testAction(
+ return testAction(
deleteEntry,
testEntry.path,
store.state,
@@ -637,7 +567,6 @@ describe('Multi-file store actions', () => {
payload: testEntry.prevPath,
},
],
- done,
);
});
});
@@ -650,7 +579,7 @@ describe('Multi-file store actions', () => {
jest.spyOn(eventHub, '$emit').mockImplementation();
});
- it('does not purge model cache for temporary entries that got renamed', (done) => {
+ it('does not purge model cache for temporary entries that got renamed', async () => {
Object.assign(store.state.entries, {
test: {
...file('test'),
@@ -660,19 +589,14 @@ describe('Multi-file store actions', () => {
},
});
- store
- .dispatch('renameEntry', {
- path: 'test',
- name: 'new',
- })
- .then(() => {
- expect(eventHub.$emit.mock.calls).not.toContain('editor.update.model.dispose.foo-bar');
- })
- .then(done)
- .catch(done.fail);
+ await store.dispatch('renameEntry', {
+ path: 'test',
+ name: 'new',
+ });
+ expect(eventHub.$emit.mock.calls).not.toContain('editor.update.model.dispose.foo-bar');
});
- it('purges model cache for renamed entry', (done) => {
+ it('purges model cache for renamed entry', async () => {
Object.assign(store.state.entries, {
test: {
...file('test'),
@@ -682,17 +606,12 @@ describe('Multi-file store actions', () => {
},
});
- store
- .dispatch('renameEntry', {
- path: 'test',
- name: 'new',
- })
- .then(() => {
- expect(eventHub.$emit).toHaveBeenCalled();
- expect(eventHub.$emit).toHaveBeenCalledWith(`editor.update.model.dispose.foo-key`);
- })
- .then(done)
- .catch(done.fail);
+ await store.dispatch('renameEntry', {
+ path: 'test',
+ name: 'new',
+ });
+ expect(eventHub.$emit).toHaveBeenCalled();
+ expect(eventHub.$emit).toHaveBeenCalledWith(`editor.update.model.dispose.foo-key`);
});
});
@@ -731,8 +650,8 @@ describe('Multi-file store actions', () => {
]);
});
- it('if not changed, completely unstages and discards entry if renamed to original', (done) => {
- testAction(
+ it('if not changed, completely unstages and discards entry if renamed to original', () => {
+ return testAction(
renameEntry,
{ path: 'renamed', name: 'orig' },
store.state,
@@ -751,24 +670,22 @@ describe('Multi-file store actions', () => {
},
],
[createTriggerRenameAction('renamed', 'orig')],
- done,
);
});
- it('if already in changed, does not add to change', (done) => {
+ it('if already in changed, does not add to change', () => {
store.state.changedFiles.push(renamedEntry);
- testAction(
+ return testAction(
renameEntry,
{ path: 'orig', name: 'renamed' },
store.state,
[expect.objectContaining({ type: types.RENAME_ENTRY })],
[createTriggerRenameAction('orig', 'renamed')],
- done,
);
});
- it('routes to the renamed file if the original file has been opened', (done) => {
+ it('routes to the renamed file if the original file has been opened', async () => {
store.state.currentProjectId = 'test/test';
store.state.currentBranchId = 'main';
@@ -776,17 +693,12 @@ describe('Multi-file store actions', () => {
opened: true,
});
- store
- .dispatch('renameEntry', {
- path: 'orig',
- name: 'renamed',
- })
- .then(() => {
- expect(router.push.mock.calls).toHaveLength(1);
- expect(router.push).toHaveBeenCalledWith(`/project/test/test/tree/main/-/renamed/`);
- })
- .then(done)
- .catch(done.fail);
+ await store.dispatch('renameEntry', {
+ path: 'orig',
+ name: 'renamed',
+ });
+ expect(router.push.mock.calls).toHaveLength(1);
+ expect(router.push).toHaveBeenCalledWith(`/project/test/test/tree/main/-/renamed/`);
});
});
@@ -809,25 +721,20 @@ describe('Multi-file store actions', () => {
});
});
- it('updates entries in a folder correctly, when folder is renamed', (done) => {
- store
- .dispatch('renameEntry', {
- path: 'folder',
- name: 'new-folder',
- })
- .then(() => {
- const keys = Object.keys(store.state.entries);
-
- expect(keys.length).toBe(3);
- expect(keys.indexOf('new-folder')).toBe(0);
- expect(keys.indexOf('new-folder/file-1')).toBe(1);
- expect(keys.indexOf('new-folder/file-2')).toBe(2);
- })
- .then(done)
- .catch(done.fail);
+ it('updates entries in a folder correctly, when folder is renamed', async () => {
+ await store.dispatch('renameEntry', {
+ path: 'folder',
+ name: 'new-folder',
+ });
+ const keys = Object.keys(store.state.entries);
+
+ expect(keys.length).toBe(3);
+ expect(keys.indexOf('new-folder')).toBe(0);
+ expect(keys.indexOf('new-folder/file-1')).toBe(1);
+ expect(keys.indexOf('new-folder/file-2')).toBe(2);
});
- it('discards renaming of an entry if the root folder is renamed back to a previous name', (done) => {
+ it('discards renaming of an entry if the root folder is renamed back to a previous name', async () => {
const rootFolder = file('old-folder', 'old-folder', 'tree');
const testEntry = file('test', 'test', 'blob', rootFolder);
@@ -841,53 +748,45 @@ describe('Multi-file store actions', () => {
},
});
- store
- .dispatch('renameEntry', {
- path: 'old-folder',
- name: 'new-folder',
- })
- .then(() => {
- const { entries } = store.state;
-
- expect(Object.keys(entries).length).toBe(2);
- expect(entries['old-folder']).toBeUndefined();
- expect(entries['old-folder/test']).toBeUndefined();
-
- expect(entries['new-folder']).toBeDefined();
- expect(entries['new-folder/test']).toEqual(
- expect.objectContaining({
- path: 'new-folder/test',
- name: 'test',
- prevPath: 'old-folder/test',
- prevName: 'test',
- }),
- );
- })
- .then(() =>
- store.dispatch('renameEntry', {
- path: 'new-folder',
- name: 'old-folder',
- }),
- )
- .then(() => {
- const { entries } = store.state;
-
- expect(Object.keys(entries).length).toBe(2);
- expect(entries['new-folder']).toBeUndefined();
- expect(entries['new-folder/test']).toBeUndefined();
-
- expect(entries['old-folder']).toBeDefined();
- expect(entries['old-folder/test']).toEqual(
- expect.objectContaining({
- path: 'old-folder/test',
- name: 'test',
- prevPath: undefined,
- prevName: undefined,
- }),
- );
- })
- .then(done)
- .catch(done.fail);
+ await store.dispatch('renameEntry', {
+ path: 'old-folder',
+ name: 'new-folder',
+ });
+ const { entries } = store.state;
+
+ expect(Object.keys(entries).length).toBe(2);
+ expect(entries['old-folder']).toBeUndefined();
+ expect(entries['old-folder/test']).toBeUndefined();
+
+ expect(entries['new-folder']).toBeDefined();
+ expect(entries['new-folder/test']).toEqual(
+ expect.objectContaining({
+ path: 'new-folder/test',
+ name: 'test',
+ prevPath: 'old-folder/test',
+ prevName: 'test',
+ }),
+ );
+
+ await store.dispatch('renameEntry', {
+ path: 'new-folder',
+ name: 'old-folder',
+ });
+ const { entries: newEntries } = store.state;
+
+ expect(Object.keys(newEntries).length).toBe(2);
+ expect(newEntries['new-folder']).toBeUndefined();
+ expect(newEntries['new-folder/test']).toBeUndefined();
+
+ expect(newEntries['old-folder']).toBeDefined();
+ expect(newEntries['old-folder/test']).toEqual(
+ expect.objectContaining({
+ path: 'old-folder/test',
+ name: 'test',
+ prevPath: undefined,
+ prevName: undefined,
+ }),
+ );
});
describe('with file in directory', () => {
@@ -919,24 +818,21 @@ describe('Multi-file store actions', () => {
});
});
- it('creates new directory', (done) => {
+ it('creates new directory', async () => {
expect(store.state.entries[newParentPath]).toBeUndefined();
- store
- .dispatch('renameEntry', { path: filePath, name: fileName, parentPath: newParentPath })
- .then(() => {
- expect(store.state.entries[newParentPath]).toEqual(
- expect.objectContaining({
- path: newParentPath,
- type: 'tree',
- tree: expect.arrayContaining([
- store.state.entries[`${newParentPath}/${fileName}`],
- ]),
- }),
- );
- })
- .then(done)
- .catch(done.fail);
+ await store.dispatch('renameEntry', {
+ path: filePath,
+ name: fileName,
+ parentPath: newParentPath,
+ });
+ expect(store.state.entries[newParentPath]).toEqual(
+ expect.objectContaining({
+ path: newParentPath,
+ type: 'tree',
+ tree: expect.arrayContaining([store.state.entries[`${newParentPath}/${fileName}`]]),
+ }),
+ );
});
describe('when new directory exists', () => {
@@ -949,40 +845,30 @@ describe('Multi-file store actions', () => {
rootDir.tree.push(newDir);
});
- it('inserts in new directory', (done) => {
+ it('inserts in new directory', async () => {
expect(newDir.tree).toEqual([]);
- store
- .dispatch('renameEntry', {
- path: filePath,
- name: fileName,
- parentPath: newParentPath,
- })
- .then(() => {
- expect(newDir.tree).toEqual([store.state.entries[`${newParentPath}/${fileName}`]]);
- })
- .then(done)
- .catch(done.fail);
+ await store.dispatch('renameEntry', {
+ path: filePath,
+ name: fileName,
+ parentPath: newParentPath,
+ });
+ expect(newDir.tree).toEqual([store.state.entries[`${newParentPath}/${fileName}`]]);
});
- it('when new directory is deleted, it undeletes it', (done) => {
- store.dispatch('deleteEntry', newParentPath);
+ it('when new directory is deleted, it undeletes it', async () => {
+ await store.dispatch('deleteEntry', newParentPath);
expect(store.state.entries[newParentPath].deleted).toBe(true);
expect(rootDir.tree.some((x) => x.path === newParentPath)).toBe(false);
- store
- .dispatch('renameEntry', {
- path: filePath,
- name: fileName,
- parentPath: newParentPath,
- })
- .then(() => {
- expect(store.state.entries[newParentPath].deleted).toBe(false);
- expect(rootDir.tree.some((x) => x.path === newParentPath)).toBe(true);
- })
- .then(done)
- .catch(done.fail);
+ await store.dispatch('renameEntry', {
+ path: filePath,
+ name: fileName,
+ parentPath: newParentPath,
+ });
+ expect(store.state.entries[newParentPath].deleted).toBe(false);
+ expect(rootDir.tree.some((x) => x.path === newParentPath)).toBe(true);
});
});
});
@@ -1023,30 +909,25 @@ describe('Multi-file store actions', () => {
document.querySelector('.flash-container').remove();
});
- it('passes the error further unchanged without dispatching any action when response is 404', (done) => {
+ it('passes the error further unchanged without dispatching any action when response is 404', async () => {
mock.onGet(/(.*)/).replyOnce(404);
- getBranchData(...callParams)
- .then(done.fail)
- .catch((e) => {
- expect(dispatch.mock.calls).toHaveLength(0);
- expect(e.response.status).toEqual(404);
- expect(document.querySelector('.flash-alert')).toBeNull();
- done();
- });
+ await expect(getBranchData(...callParams)).rejects.toEqual(
+ new Error('Request failed with status code 404'),
+ );
+ expect(dispatch.mock.calls).toHaveLength(0);
+ expect(document.querySelector('.flash-alert')).toBeNull();
});
- it('does not pass the error further and flashes an alert if error is not 404', (done) => {
+ it('does not pass the error further and flashes an alert if error is not 404', async () => {
mock.onGet(/(.*)/).replyOnce(418);
- getBranchData(...callParams)
- .then(done.fail)
- .catch((e) => {
- expect(dispatch.mock.calls).toHaveLength(0);
- expect(e.response).toBeUndefined();
- expect(document.querySelector('.flash-alert')).not.toBeNull();
- done();
- });
+ await expect(getBranchData(...callParams)).rejects.toEqual(
+ new Error('Branch not loaded - <strong>abc/def/main-testing</strong>'),
+ );
+
+ expect(dispatch.mock.calls).toHaveLength(0);
+ expect(document.querySelector('.flash-alert')).not.toBeNull();
});
});
});
diff --git a/spec/frontend/ide/stores/modules/branches/actions_spec.js b/spec/frontend/ide/stores/modules/branches/actions_spec.js
index 135dbc1f746..306330e3ba2 100644
--- a/spec/frontend/ide/stores/modules/branches/actions_spec.js
+++ b/spec/frontend/ide/stores/modules/branches/actions_spec.js
@@ -42,21 +42,20 @@ describe('IDE branches actions', () => {
});
describe('requestBranches', () => {
- it('should commit request', (done) => {
- testAction(
+ it('should commit request', () => {
+ return testAction(
requestBranches,
null,
mockedContext.state,
[{ type: types.REQUEST_BRANCHES }],
[],
- done,
);
});
});
describe('receiveBranchesError', () => {
- it('should commit error', (done) => {
- testAction(
+ it('should commit error', () => {
+ return testAction(
receiveBranchesError,
{ search: TEST_SEARCH },
mockedContext.state,
@@ -72,20 +71,18 @@ describe('IDE branches actions', () => {
},
},
],
- done,
);
});
});
describe('receiveBranchesSuccess', () => {
- it('should commit received data', (done) => {
- testAction(
+ it('should commit received data', () => {
+ return testAction(
receiveBranchesSuccess,
branches,
mockedContext.state,
[{ type: types.RECEIVE_BRANCHES_SUCCESS, payload: branches }],
[],
- done,
);
});
});
@@ -110,8 +107,8 @@ describe('IDE branches actions', () => {
});
});
- it('dispatches success with received data', (done) => {
- testAction(
+ it('dispatches success with received data', () => {
+ return testAction(
fetchBranches,
{ search: TEST_SEARCH },
mockedState,
@@ -121,7 +118,6 @@ describe('IDE branches actions', () => {
{ type: 'resetBranches' },
{ type: 'receiveBranchesSuccess', payload: branches },
],
- done,
);
});
});
@@ -131,8 +127,8 @@ describe('IDE branches actions', () => {
mock.onGet(/\/api\/v4\/projects\/\d+\/repository\/branches(.*)$/).replyOnce(500);
});
- it('dispatches error', (done) => {
- testAction(
+ it('dispatches error', () => {
+ return testAction(
fetchBranches,
{ search: TEST_SEARCH },
mockedState,
@@ -142,20 +138,18 @@ describe('IDE branches actions', () => {
{ type: 'resetBranches' },
{ type: 'receiveBranchesError', payload: { search: TEST_SEARCH } },
],
- done,
);
});
});
describe('resetBranches', () => {
- it('commits reset', (done) => {
- testAction(
+ it('commits reset', () => {
+ return testAction(
resetBranches,
null,
mockedContext.state,
[{ type: types.RESET_BRANCHES }],
[],
- done,
);
});
});
diff --git a/spec/frontend/ide/stores/modules/clientside/actions_spec.js b/spec/frontend/ide/stores/modules/clientside/actions_spec.js
index d2777623b0d..c2b9de192d9 100644
--- a/spec/frontend/ide/stores/modules/clientside/actions_spec.js
+++ b/spec/frontend/ide/stores/modules/clientside/actions_spec.js
@@ -26,15 +26,13 @@ describe('IDE store module clientside actions', () => {
});
describe('pingUsage', () => {
- it('posts to usage endpoint', (done) => {
+ it('posts to usage endpoint', async () => {
const usageSpy = jest.fn(() => [200]);
mock.onPost(TEST_USAGE_URL).reply(() => usageSpy());
- testAction(actions.pingUsage, PING_USAGE_PREVIEW_KEY, rootGetters, [], [], () => {
- expect(usageSpy).toHaveBeenCalled();
- done();
- });
+ await testAction(actions.pingUsage, PING_USAGE_PREVIEW_KEY, rootGetters, [], []);
+ expect(usageSpy).toHaveBeenCalled();
});
});
});
diff --git a/spec/frontend/ide/stores/modules/commit/actions_spec.js b/spec/frontend/ide/stores/modules/commit/actions_spec.js
index cb6bb7c1202..d65039e89cc 100644
--- a/spec/frontend/ide/stores/modules/commit/actions_spec.js
+++ b/spec/frontend/ide/stores/modules/commit/actions_spec.js
@@ -57,40 +57,25 @@ describe('IDE commit module actions', () => {
});
describe('updateCommitMessage', () => {
- it('updates store with new commit message', (done) => {
- store
- .dispatch('commit/updateCommitMessage', 'testing')
- .then(() => {
- expect(store.state.commit.commitMessage).toBe('testing');
- })
- .then(done)
- .catch(done.fail);
+ it('updates store with new commit message', async () => {
+ await store.dispatch('commit/updateCommitMessage', 'testing');
+ expect(store.state.commit.commitMessage).toBe('testing');
});
});
describe('discardDraft', () => {
- it('resets commit message to blank', (done) => {
+ it('resets commit message to blank', async () => {
store.state.commit.commitMessage = 'testing';
- store
- .dispatch('commit/discardDraft')
- .then(() => {
- expect(store.state.commit.commitMessage).not.toBe('testing');
- })
- .then(done)
- .catch(done.fail);
+ await store.dispatch('commit/discardDraft');
+ expect(store.state.commit.commitMessage).not.toBe('testing');
});
});
describe('updateCommitAction', () => {
- it('updates store with new commit action', (done) => {
- store
- .dispatch('commit/updateCommitAction', '1')
- .then(() => {
- expect(store.state.commit.commitAction).toBe('1');
- })
- .then(done)
- .catch(done.fail);
+ it('updates store with new commit action', async () => {
+ await store.dispatch('commit/updateCommitAction', '1');
+ expect(store.state.commit.commitAction).toBe('1');
});
});
@@ -139,34 +124,24 @@ describe('IDE commit module actions', () => {
});
});
- it('updates commit message with short_id', (done) => {
- store
- .dispatch('commit/setLastCommitMessage', { short_id: '123' })
- .then(() => {
- expect(store.state.lastCommitMsg).toContain(
- 'Your changes have been committed. Commit <a href="http://testing/-/commit/123" class="commit-sha">123</a>',
- );
- })
- .then(done)
- .catch(done.fail);
+ it('updates commit message with short_id', async () => {
+ await store.dispatch('commit/setLastCommitMessage', { short_id: '123' });
+ expect(store.state.lastCommitMsg).toContain(
+ 'Your changes have been committed. Commit <a href="http://testing/-/commit/123" class="commit-sha">123</a>',
+ );
});
- it('updates commit message with stats', (done) => {
- store
- .dispatch('commit/setLastCommitMessage', {
- short_id: '123',
- stats: {
- additions: '1',
- deletions: '2',
- },
- })
- .then(() => {
- expect(store.state.lastCommitMsg).toBe(
- 'Your changes have been committed. Commit <a href="http://testing/-/commit/123" class="commit-sha">123</a> with 1 additions, 2 deletions.',
- );
- })
- .then(done)
- .catch(done.fail);
+ it('updates commit message with stats', async () => {
+ await store.dispatch('commit/setLastCommitMessage', {
+ short_id: '123',
+ stats: {
+ additions: '1',
+ deletions: '2',
+ },
+ });
+ expect(store.state.lastCommitMsg).toBe(
+ 'Your changes have been committed. Commit <a href="http://testing/-/commit/123" class="commit-sha">123</a> with 1 additions, 2 deletions.',
+ );
});
});
@@ -221,74 +196,49 @@ describe('IDE commit module actions', () => {
});
});
- it('updates stores working reference', (done) => {
- store
- .dispatch('commit/updateFilesAfterCommit', {
- data,
- branch,
- })
- .then(() => {
- expect(store.state.projects.abcproject.branches.main.workingReference).toBe(data.id);
- })
- .then(done)
- .catch(done.fail);
+ it('updates stores working reference', async () => {
+ await store.dispatch('commit/updateFilesAfterCommit', {
+ data,
+ branch,
+ });
+ expect(store.state.projects.abcproject.branches.main.workingReference).toBe(data.id);
});
- it('resets all files changed status', (done) => {
- store
- .dispatch('commit/updateFilesAfterCommit', {
- data,
- branch,
- })
- .then(() => {
- store.state.openFiles.forEach((entry) => {
- expect(entry.changed).toBeFalsy();
- });
- })
- .then(done)
- .catch(done.fail);
+ it('resets all files changed status', async () => {
+ await store.dispatch('commit/updateFilesAfterCommit', {
+ data,
+ branch,
+ });
+ store.state.openFiles.forEach((entry) => {
+ expect(entry.changed).toBeFalsy();
+ });
});
- it('sets files commit data', (done) => {
- store
- .dispatch('commit/updateFilesAfterCommit', {
- data,
- branch,
- })
- .then(() => {
- expect(f.lastCommitSha).toBe(data.id);
- })
- .then(done)
- .catch(done.fail);
+ it('sets files commit data', async () => {
+ await store.dispatch('commit/updateFilesAfterCommit', {
+ data,
+ branch,
+ });
+ expect(f.lastCommitSha).toBe(data.id);
});
- it('updates raw content for changed file', (done) => {
- store
- .dispatch('commit/updateFilesAfterCommit', {
- data,
- branch,
- })
- .then(() => {
- expect(f.raw).toBe(f.content);
- })
- .then(done)
- .catch(done.fail);
+ it('updates raw content for changed file', async () => {
+ await store.dispatch('commit/updateFilesAfterCommit', {
+ data,
+ branch,
+ });
+ expect(f.raw).toBe(f.content);
});
- it('emits changed event for file', (done) => {
- store
- .dispatch('commit/updateFilesAfterCommit', {
- data,
- branch,
- })
- .then(() => {
- expect(eventHub.$emit).toHaveBeenCalledWith(`editor.update.model.content.${f.key}`, {
- content: f.content,
- changed: false,
- });
- })
- .then(done)
- .catch(done.fail);
+ it('emits changed event for file', async () => {
+ await store.dispatch('commit/updateFilesAfterCommit', {
+ data,
+ branch,
+ });
+ expect(eventHub.$emit).toHaveBeenCalledWith(`editor.update.model.content.${f.key}`, {
+ content: f.content,
+ changed: false,
+ });
});
});
@@ -349,138 +299,93 @@ describe('IDE commit module actions', () => {
jest.spyOn(service, 'commit').mockResolvedValue({ data: COMMIT_RESPONSE });
});
- it('calls service', (done) => {
- store
- .dispatch('commit/commitChanges')
- .then(() => {
- expect(service.commit).toHaveBeenCalledWith('abcproject', {
- branch: expect.anything(),
- commit_message: 'testing 123',
- actions: [
- {
- action: commitActionTypes.update,
- file_path: expect.anything(),
- content: '\n',
- encoding: expect.anything(),
- last_commit_id: undefined,
- previous_path: undefined,
- },
- ],
- start_sha: TEST_COMMIT_SHA,
- });
-
- done();
- })
- .catch(done.fail);
+ it('calls service', async () => {
+ await store.dispatch('commit/commitChanges');
+ expect(service.commit).toHaveBeenCalledWith('abcproject', {
+ branch: expect.anything(),
+ commit_message: 'testing 123',
+ actions: [
+ {
+ action: commitActionTypes.update,
+ file_path: expect.anything(),
+ content: '\n',
+ encoding: expect.anything(),
+ last_commit_id: undefined,
+ previous_path: undefined,
+ },
+ ],
+ start_sha: TEST_COMMIT_SHA,
+ });
});
- it('sends lastCommit ID when not creating new branch', (done) => {
+ it('sends lastCommit ID when not creating new branch', async () => {
store.state.commit.commitAction = '1';
- store
- .dispatch('commit/commitChanges')
- .then(() => {
- expect(service.commit).toHaveBeenCalledWith('abcproject', {
- branch: expect.anything(),
- commit_message: 'testing 123',
- actions: [
- {
- action: commitActionTypes.update,
- file_path: expect.anything(),
- content: '\n',
- encoding: expect.anything(),
- last_commit_id: TEST_COMMIT_SHA,
- previous_path: undefined,
- },
- ],
- start_sha: undefined,
- });
-
- done();
- })
- .catch(done.fail);
+ await store.dispatch('commit/commitChanges');
+ expect(service.commit).toHaveBeenCalledWith('abcproject', {
+ branch: expect.anything(),
+ commit_message: 'testing 123',
+ actions: [
+ {
+ action: commitActionTypes.update,
+ file_path: expect.anything(),
+ content: '\n',
+ encoding: expect.anything(),
+ last_commit_id: TEST_COMMIT_SHA,
+ previous_path: undefined,
+ },
+ ],
+ start_sha: undefined,
+ });
});
- it('sets last Commit Msg', (done) => {
- store
- .dispatch('commit/commitChanges')
- .then(() => {
- expect(store.state.lastCommitMsg).toBe(
- 'Your changes have been committed. Commit <a href="webUrl/-/commit/123" class="commit-sha">123</a> with 1 additions, 2 deletions.',
- );
-
- done();
- })
- .catch(done.fail);
+ it('sets last Commit Msg', async () => {
+ await store.dispatch('commit/commitChanges');
+ expect(store.state.lastCommitMsg).toBe(
+ 'Your changes have been committed. Commit <a href="webUrl/-/commit/123" class="commit-sha">123</a> with 1 additions, 2 deletions.',
+ );
});
- it('adds commit data to files', (done) => {
- store
- .dispatch('commit/commitChanges')
- .then(() => {
- expect(store.state.entries[store.state.openFiles[0].path].lastCommitSha).toBe(
- COMMIT_RESPONSE.id,
- );
-
- done();
- })
- .catch(done.fail);
+ it('adds commit data to files', async () => {
+ await store.dispatch('commit/commitChanges');
+ expect(store.state.entries[store.state.openFiles[0].path].lastCommitSha).toBe(
+ COMMIT_RESPONSE.id,
+ );
});
- it('resets stores commit actions', (done) => {
+ it('resets stores commit actions', async () => {
store.state.commit.commitAction = COMMIT_TO_NEW_BRANCH;
- store
- .dispatch('commit/commitChanges')
- .then(() => {
- expect(store.state.commit.commitAction).not.toBe(COMMIT_TO_NEW_BRANCH);
- })
- .then(done)
- .catch(done.fail);
+ await store.dispatch('commit/commitChanges');
+ expect(store.state.commit.commitAction).not.toBe(COMMIT_TO_NEW_BRANCH);
});
- it('removes all staged files', (done) => {
- store
- .dispatch('commit/commitChanges')
- .then(() => {
- expect(store.state.stagedFiles.length).toBe(0);
- })
- .then(done)
- .catch(done.fail);
+ it('removes all staged files', async () => {
+ await store.dispatch('commit/commitChanges');
+ expect(store.state.stagedFiles.length).toBe(0);
});
describe('merge request', () => {
- it('redirects to new merge request page', (done) => {
+ it('redirects to new merge request page', async () => {
jest.spyOn(eventHub, '$on').mockImplementation();
store.state.commit.commitAction = COMMIT_TO_NEW_BRANCH;
store.state.commit.shouldCreateMR = true;
- store
- .dispatch('commit/commitChanges')
- .then(() => {
- expect(visitUrl).toHaveBeenCalledWith(
- `webUrl/-/merge_requests/new?merge_request[source_branch]=${store.getters['commit/placeholderBranchName']}&merge_request[target_branch]=main&nav_source=webide`,
- );
-
- done();
- })
- .catch(done.fail);
+ await store.dispatch('commit/commitChanges');
+ expect(visitUrl).toHaveBeenCalledWith(
+ `webUrl/-/merge_requests/new?merge_request[source_branch]=${store.getters['commit/placeholderBranchName']}&merge_request[target_branch]=main&nav_source=webide`,
+ );
});
- it('does not redirect to new merge request page when shouldCreateMR is not checked', (done) => {
+ it('does not redirect to new merge request page when shouldCreateMR is not checked', async () => {
jest.spyOn(eventHub, '$on').mockImplementation();
store.state.commit.commitAction = COMMIT_TO_NEW_BRANCH;
store.state.commit.shouldCreateMR = false;
- store
- .dispatch('commit/commitChanges')
- .then(() => {
- expect(visitUrl).not.toHaveBeenCalled();
- done();
- })
- .catch(done.fail);
+ await store.dispatch('commit/commitChanges');
+ expect(visitUrl).not.toHaveBeenCalled();
});
it('does not redirect to merge request page if shouldCreateMR is checked, but branch is the default branch', async () => {
@@ -514,17 +419,11 @@ describe('IDE commit module actions', () => {
});
});
- it('shows failed message', (done) => {
- store
- .dispatch('commit/commitChanges')
- .then(() => {
- const alert = document.querySelector('.flash-container');
-
- expect(alert.textContent.trim()).toBe('failed message');
+ it('shows failed message', async () => {
+ await store.dispatch('commit/commitChanges');
+ const alert = document.querySelector('.flash-container');
- done();
- })
- .catch(done.fail);
+ expect(alert.textContent.trim()).toBe('failed message');
});
});
@@ -548,52 +447,37 @@ describe('IDE commit module actions', () => {
});
describe('first commit of a branch', () => {
- it('commits TOGGLE_EMPTY_STATE mutation on empty repo', (done) => {
+ it('commits TOGGLE_EMPTY_STATE mutation on empty repo', async () => {
jest.spyOn(service, 'commit').mockResolvedValue({ data: COMMIT_RESPONSE });
jest.spyOn(store, 'commit');
- store
- .dispatch('commit/commitChanges')
- .then(() => {
- expect(store.commit.mock.calls).toEqual(
- expect.arrayContaining([
- ['TOGGLE_EMPTY_STATE', expect.any(Object), expect.any(Object)],
- ]),
- );
- done();
- })
- .catch(done.fail);
+ await store.dispatch('commit/commitChanges');
+ expect(store.commit.mock.calls).toEqual(
+ expect.arrayContaining([['TOGGLE_EMPTY_STATE', expect.any(Object), expect.any(Object)]]),
+ );
});
- it('does not commmit TOGGLE_EMPTY_STATE mutation on existing project', (done) => {
+ it('does not commmit TOGGLE_EMPTY_STATE mutation on existing project', async () => {
COMMIT_RESPONSE.parent_ids.push('1234');
jest.spyOn(service, 'commit').mockResolvedValue({ data: COMMIT_RESPONSE });
jest.spyOn(store, 'commit');
- store
- .dispatch('commit/commitChanges')
- .then(() => {
- expect(store.commit.mock.calls).not.toEqual(
- expect.arrayContaining([
- ['TOGGLE_EMPTY_STATE', expect.any(Object), expect.any(Object)],
- ]),
- );
- done();
- })
- .catch(done.fail);
+ await store.dispatch('commit/commitChanges');
+ expect(store.commit.mock.calls).not.toEqual(
+ expect.arrayContaining([['TOGGLE_EMPTY_STATE', expect.any(Object), expect.any(Object)]]),
+ );
});
});
});
describe('toggleShouldCreateMR', () => {
- it('commits both toggle and interacting with MR checkbox actions', (done) => {
- testAction(
+ it('commits both toggle and interacting with MR checkbox actions', () => {
+ return testAction(
actions.toggleShouldCreateMR,
{},
store.state,
[{ type: mutationTypes.TOGGLE_SHOULD_CREATE_MR }],
[],
- done,
);
});
});
diff --git a/spec/frontend/ide/stores/modules/file_templates/actions_spec.js b/spec/frontend/ide/stores/modules/file_templates/actions_spec.js
index 9ff950b0875..1080a30d2d8 100644
--- a/spec/frontend/ide/stores/modules/file_templates/actions_spec.js
+++ b/spec/frontend/ide/stores/modules/file_templates/actions_spec.js
@@ -20,21 +20,20 @@ describe('IDE file templates actions', () => {
});
describe('requestTemplateTypes', () => {
- it('commits REQUEST_TEMPLATE_TYPES', (done) => {
- testAction(
+ it('commits REQUEST_TEMPLATE_TYPES', () => {
+ return testAction(
actions.requestTemplateTypes,
null,
state,
[{ type: types.REQUEST_TEMPLATE_TYPES }],
[],
- done,
);
});
});
describe('receiveTemplateTypesError', () => {
- it('commits RECEIVE_TEMPLATE_TYPES_ERROR and dispatches setErrorMessage', (done) => {
- testAction(
+ it('commits RECEIVE_TEMPLATE_TYPES_ERROR and dispatches setErrorMessage', () => {
+ return testAction(
actions.receiveTemplateTypesError,
null,
state,
@@ -49,20 +48,18 @@ describe('IDE file templates actions', () => {
},
},
],
- done,
);
});
});
describe('receiveTemplateTypesSuccess', () => {
- it('commits RECEIVE_TEMPLATE_TYPES_SUCCESS', (done) => {
- testAction(
+ it('commits RECEIVE_TEMPLATE_TYPES_SUCCESS', () => {
+ return testAction(
actions.receiveTemplateTypesSuccess,
'test',
state,
[{ type: types.RECEIVE_TEMPLATE_TYPES_SUCCESS, payload: 'test' }],
[],
- done,
);
});
});
@@ -81,23 +78,17 @@ describe('IDE file templates actions', () => {
});
});
- it('rejects if selectedTemplateType is empty', (done) => {
+ it('rejects if selectedTemplateType is empty', async () => {
const dispatch = jest.fn().mockName('dispatch');
- actions
- .fetchTemplateTypes({ dispatch, state })
- .then(done.fail)
- .catch(() => {
- expect(dispatch).not.toHaveBeenCalled();
-
- done();
- });
+ await expect(actions.fetchTemplateTypes({ dispatch, state })).rejects.toBeUndefined();
+ expect(dispatch).not.toHaveBeenCalled();
});
- it('dispatches actions', (done) => {
+ it('dispatches actions', () => {
state.selectedTemplateType = { key: 'licenses' };
- testAction(
+ return testAction(
actions.fetchTemplateTypes,
null,
state,
@@ -111,7 +102,6 @@ describe('IDE file templates actions', () => {
payload: pages[0].concat(pages[1]).concat(pages[2]),
},
],
- done,
);
});
});
@@ -121,16 +111,15 @@ describe('IDE file templates actions', () => {
mock.onGet(/api\/(.*)\/templates\/licenses/).replyOnce(500);
});
- it('dispatches actions', (done) => {
+ it('dispatches actions', () => {
state.selectedTemplateType = { key: 'licenses' };
- testAction(
+ return testAction(
actions.fetchTemplateTypes,
null,
state,
[],
[{ type: 'requestTemplateTypes' }, { type: 'receiveTemplateTypesError' }],
- done,
);
});
});
@@ -184,8 +173,8 @@ describe('IDE file templates actions', () => {
});
describe('receiveTemplateError', () => {
- it('dispatches setErrorMessage', (done) => {
- testAction(
+ it('dispatches setErrorMessage', () => {
+ return testAction(
actions.receiveTemplateError,
'test',
state,
@@ -201,7 +190,6 @@ describe('IDE file templates actions', () => {
},
},
],
- done,
);
});
});
@@ -217,46 +205,43 @@ describe('IDE file templates actions', () => {
.replyOnce(200, { content: 'testing content' });
});
- it('dispatches setFileTemplate if template already has content', (done) => {
+ it('dispatches setFileTemplate if template already has content', () => {
const template = { content: 'already has content' };
- testAction(
+ return testAction(
actions.fetchTemplate,
template,
state,
[],
[{ type: 'setFileTemplate', payload: template }],
- done,
);
});
- it('dispatches success', (done) => {
+ it('dispatches success', () => {
const template = { key: 'mit' };
state.selectedTemplateType = { key: 'licenses' };
- testAction(
+ return testAction(
actions.fetchTemplate,
template,
state,
[],
[{ type: 'setFileTemplate', payload: { content: 'MIT content' } }],
- done,
);
});
- it('dispatches success and uses name key for API call', (done) => {
+ it('dispatches success and uses name key for API call', () => {
const template = { name: 'testing' };
state.selectedTemplateType = { key: 'licenses' };
- testAction(
+ return testAction(
actions.fetchTemplate,
template,
state,
[],
[{ type: 'setFileTemplate', payload: { content: 'testing content' } }],
- done,
);
});
});
@@ -266,18 +251,17 @@ describe('IDE file templates actions', () => {
mock.onGet(/api\/(.*)\/templates\/licenses\/mit/).replyOnce(500);
});
- it('dispatches error', (done) => {
+ it('dispatches error', () => {
const template = { name: 'testing' };
state.selectedTemplateType = { key: 'licenses' };
- testAction(
+ return testAction(
actions.fetchTemplate,
template,
state,
[],
[{ type: 'receiveTemplateError', payload: template }],
- done,
);
});
});
diff --git a/spec/frontend/ide/stores/modules/merge_requests/actions_spec.js b/spec/frontend/ide/stores/modules/merge_requests/actions_spec.js
index e1f2b165dd9..344fe3a41c3 100644
--- a/spec/frontend/ide/stores/modules/merge_requests/actions_spec.js
+++ b/spec/frontend/ide/stores/modules/merge_requests/actions_spec.js
@@ -28,21 +28,20 @@ describe('IDE merge requests actions', () => {
});
describe('requestMergeRequests', () => {
- it('should commit request', (done) => {
- testAction(
+ it('should commit request', () => {
+ return testAction(
requestMergeRequests,
null,
mockedState,
[{ type: types.REQUEST_MERGE_REQUESTS }],
[],
- done,
);
});
});
describe('receiveMergeRequestsError', () => {
- it('should commit error', (done) => {
- testAction(
+ it('should commit error', () => {
+ return testAction(
receiveMergeRequestsError,
{ type: 'created', search: '' },
mockedState,
@@ -58,20 +57,18 @@ describe('IDE merge requests actions', () => {
},
},
],
- done,
);
});
});
describe('receiveMergeRequestsSuccess', () => {
- it('should commit received data', (done) => {
- testAction(
+ it('should commit received data', () => {
+ return testAction(
receiveMergeRequestsSuccess,
mergeRequests,
mockedState,
[{ type: types.RECEIVE_MERGE_REQUESTS_SUCCESS, payload: mergeRequests }],
[],
- done,
);
});
});
@@ -118,8 +115,8 @@ describe('IDE merge requests actions', () => {
});
});
- it('dispatches success with received data', (done) => {
- testAction(
+ it('dispatches success with received data', () => {
+ return testAction(
fetchMergeRequests,
{ type: 'created' },
mockedState,
@@ -129,7 +126,6 @@ describe('IDE merge requests actions', () => {
{ type: 'resetMergeRequests' },
{ type: 'receiveMergeRequestsSuccess', payload: mergeRequests },
],
- done,
);
});
});
@@ -156,8 +152,8 @@ describe('IDE merge requests actions', () => {
);
});
- it('dispatches success with received data', (done) => {
- testAction(
+ it('dispatches success with received data', () => {
+ return testAction(
fetchMergeRequests,
{ type: null },
{ ...mockedState, ...mockedRootState },
@@ -167,7 +163,6 @@ describe('IDE merge requests actions', () => {
{ type: 'resetMergeRequests' },
{ type: 'receiveMergeRequestsSuccess', payload: mergeRequests },
],
- done,
);
});
});
@@ -177,8 +172,8 @@ describe('IDE merge requests actions', () => {
mock.onGet(/\/api\/v4\/merge_requests(.*)$/).replyOnce(500);
});
- it('dispatches error', (done) => {
- testAction(
+ it('dispatches error', () => {
+ return testAction(
fetchMergeRequests,
{ type: 'created', search: '' },
mockedState,
@@ -188,21 +183,19 @@ describe('IDE merge requests actions', () => {
{ type: 'resetMergeRequests' },
{ type: 'receiveMergeRequestsError', payload: { type: 'created', search: '' } },
],
- done,
);
});
});
});
describe('resetMergeRequests', () => {
- it('commits reset', (done) => {
- testAction(
+ it('commits reset', () => {
+ return testAction(
resetMergeRequests,
null,
mockedState,
[{ type: types.RESET_MERGE_REQUESTS }],
[],
- done,
);
});
});
diff --git a/spec/frontend/ide/stores/modules/pane/actions_spec.js b/spec/frontend/ide/stores/modules/pane/actions_spec.js
index 42fe8b400b8..98c4f22dac8 100644
--- a/spec/frontend/ide/stores/modules/pane/actions_spec.js
+++ b/spec/frontend/ide/stores/modules/pane/actions_spec.js
@@ -7,19 +7,19 @@ describe('IDE pane module actions', () => {
const TEST_VIEW_KEEP_ALIVE = { name: 'test-keep-alive', keepAlive: true };
describe('toggleOpen', () => {
- it('dispatches open if closed', (done) => {
- testAction(actions.toggleOpen, TEST_VIEW, { isOpen: false }, [], [{ type: 'open' }], done);
+ it('dispatches open if closed', () => {
+ return testAction(actions.toggleOpen, TEST_VIEW, { isOpen: false }, [], [{ type: 'open' }]);
});
- it('dispatches close if opened', (done) => {
- testAction(actions.toggleOpen, TEST_VIEW, { isOpen: true }, [], [{ type: 'close' }], done);
+ it('dispatches close if opened', () => {
+ return testAction(actions.toggleOpen, TEST_VIEW, { isOpen: true }, [], [{ type: 'close' }]);
});
});
describe('open', () => {
describe('with a view specified', () => {
- it('commits SET_OPEN and SET_CURRENT_VIEW', (done) => {
- testAction(
+ it('commits SET_OPEN and SET_CURRENT_VIEW', () => {
+ return testAction(
actions.open,
TEST_VIEW,
{},
@@ -28,12 +28,11 @@ describe('IDE pane module actions', () => {
{ type: types.SET_CURRENT_VIEW, payload: TEST_VIEW.name },
],
[],
- done,
);
});
- it('commits KEEP_ALIVE_VIEW if keepAlive is true', (done) => {
- testAction(
+ it('commits KEEP_ALIVE_VIEW if keepAlive is true', () => {
+ return testAction(
actions.open,
TEST_VIEW_KEEP_ALIVE,
{},
@@ -43,28 +42,26 @@ describe('IDE pane module actions', () => {
{ type: types.KEEP_ALIVE_VIEW, payload: TEST_VIEW_KEEP_ALIVE.name },
],
[],
- done,
);
});
});
describe('without a view specified', () => {
- it('commits SET_OPEN', (done) => {
- testAction(
+ it('commits SET_OPEN', () => {
+ return testAction(
actions.open,
undefined,
{},
[{ type: types.SET_OPEN, payload: true }],
[],
- done,
);
});
});
});
describe('close', () => {
- it('commits SET_OPEN', (done) => {
- testAction(actions.close, null, {}, [{ type: types.SET_OPEN, payload: false }], [], done);
+ it('commits SET_OPEN', () => {
+ return testAction(actions.close, null, {}, [{ type: types.SET_OPEN, payload: false }], []);
});
});
});
diff --git a/spec/frontend/ide/stores/modules/pipelines/actions_spec.js b/spec/frontend/ide/stores/modules/pipelines/actions_spec.js
index 3ede37e2eed..b76b673c3a2 100644
--- a/spec/frontend/ide/stores/modules/pipelines/actions_spec.js
+++ b/spec/frontend/ide/stores/modules/pipelines/actions_spec.js
@@ -25,6 +25,7 @@ import {
import * as types from '~/ide/stores/modules/pipelines/mutation_types';
import state from '~/ide/stores/modules/pipelines/state';
import axios from '~/lib/utils/axios_utils';
+import waitForPromises from 'helpers/wait_for_promises';
import { pipelines, jobs } from '../../../mock_data';
describe('IDE pipelines actions', () => {
@@ -44,32 +45,30 @@ describe('IDE pipelines actions', () => {
});
describe('requestLatestPipeline', () => {
- it('commits request', (done) => {
- testAction(
+ it('commits request', () => {
+ return testAction(
requestLatestPipeline,
null,
mockedState,
[{ type: types.REQUEST_LATEST_PIPELINE }],
[],
- done,
);
});
});
describe('receiveLatestPipelineError', () => {
- it('commits error', (done) => {
- testAction(
+ it('commits error', () => {
+ return testAction(
receiveLatestPipelineError,
{ status: 404 },
mockedState,
[{ type: types.RECEIVE_LASTEST_PIPELINE_ERROR }],
[{ type: 'stopPipelinePolling' }],
- done,
);
});
- it('dispatches setErrorMessage is not 404', (done) => {
- testAction(
+ it('dispatches setErrorMessage is not 404', () => {
+ return testAction(
receiveLatestPipelineError,
{ status: 500 },
mockedState,
@@ -86,7 +85,6 @@ describe('IDE pipelines actions', () => {
},
{ type: 'stopPipelinePolling' },
],
- done,
);
});
});
@@ -123,7 +121,7 @@ describe('IDE pipelines actions', () => {
.reply(200, { data: { foo: 'bar' } }, { 'poll-interval': '10000' });
});
- it('dispatches request', (done) => {
+ it('dispatches request', async () => {
jest.spyOn(axios, 'get');
jest.spyOn(Visibility, 'hidden').mockReturnValue(false);
@@ -133,34 +131,21 @@ describe('IDE pipelines actions', () => {
currentProject: { path_with_namespace: 'abc/def' },
};
- fetchLatestPipeline({ dispatch, rootGetters });
+ await fetchLatestPipeline({ dispatch, rootGetters });
expect(dispatch).toHaveBeenCalledWith('requestLatestPipeline');
- jest.advanceTimersByTime(1000);
-
- new Promise((resolve) => requestAnimationFrame(resolve))
- .then(() => {
- expect(axios.get).toHaveBeenCalled();
- expect(axios.get).toHaveBeenCalledTimes(1);
- expect(dispatch).toHaveBeenCalledWith(
- 'receiveLatestPipelineSuccess',
- expect.anything(),
- );
-
- jest.advanceTimersByTime(10000);
- })
- .then(() => new Promise((resolve) => requestAnimationFrame(resolve)))
- .then(() => {
- expect(axios.get).toHaveBeenCalled();
- expect(axios.get).toHaveBeenCalledTimes(2);
- expect(dispatch).toHaveBeenCalledWith(
- 'receiveLatestPipelineSuccess',
- expect.anything(),
- );
- })
- .then(done)
- .catch(done.fail);
+ await waitForPromises();
+
+ expect(axios.get).toHaveBeenCalled();
+ expect(axios.get).toHaveBeenCalledTimes(1);
+ expect(dispatch).toHaveBeenCalledWith('receiveLatestPipelineSuccess', expect.anything());
+
+ jest.advanceTimersByTime(10000);
+
+ expect(axios.get).toHaveBeenCalled();
+ expect(axios.get).toHaveBeenCalledTimes(2);
+ expect(dispatch).toHaveBeenCalledWith('receiveLatestPipelineSuccess', expect.anything());
});
});
@@ -169,27 +154,22 @@ describe('IDE pipelines actions', () => {
mock.onGet('/abc/def/commit/abc123def456ghi789jkl/pipelines').reply(500);
});
- it('dispatches error', (done) => {
+ it('dispatches error', async () => {
const dispatch = jest.fn().mockName('dispatch');
const rootGetters = {
lastCommit: { id: 'abc123def456ghi789jkl' },
currentProject: { path_with_namespace: 'abc/def' },
};
- fetchLatestPipeline({ dispatch, rootGetters });
+ await fetchLatestPipeline({ dispatch, rootGetters });
- jest.advanceTimersByTime(1500);
+ await waitForPromises();
- new Promise((resolve) => requestAnimationFrame(resolve))
- .then(() => {
- expect(dispatch).toHaveBeenCalledWith('receiveLatestPipelineError', expect.anything());
- })
- .then(done)
- .catch(done.fail);
+ expect(dispatch).toHaveBeenCalledWith('receiveLatestPipelineError', expect.anything());
});
});
- it('sets latest pipeline to `null` and stops polling on empty project', (done) => {
+ it('sets latest pipeline to `null` and stops polling on empty project', () => {
mockedState = {
...mockedState,
rootGetters: {
@@ -197,26 +177,31 @@ describe('IDE pipelines actions', () => {
},
};
- testAction(
+ return testAction(
fetchLatestPipeline,
{},
mockedState,
[{ type: types.RECEIVE_LASTEST_PIPELINE_SUCCESS, payload: null }],
[{ type: 'stopPipelinePolling' }],
- done,
);
});
});
describe('requestJobs', () => {
- it('commits request', (done) => {
- testAction(requestJobs, 1, mockedState, [{ type: types.REQUEST_JOBS, payload: 1 }], [], done);
+ it('commits request', () => {
+ return testAction(
+ requestJobs,
+ 1,
+ mockedState,
+ [{ type: types.REQUEST_JOBS, payload: 1 }],
+ [],
+ );
});
});
describe('receiveJobsError', () => {
- it('commits error', (done) => {
- testAction(
+ it('commits error', () => {
+ return testAction(
receiveJobsError,
{ id: 1 },
mockedState,
@@ -232,20 +217,18 @@ describe('IDE pipelines actions', () => {
},
},
],
- done,
);
});
});
describe('receiveJobsSuccess', () => {
- it('commits data', (done) => {
- testAction(
+ it('commits data', () => {
+ return testAction(
receiveJobsSuccess,
{ id: 1, data: jobs },
mockedState,
[{ type: types.RECEIVE_JOBS_SUCCESS, payload: { id: 1, data: jobs } }],
[],
- done,
);
});
});
@@ -258,8 +241,8 @@ describe('IDE pipelines actions', () => {
mock.onGet(stage.dropdownPath).replyOnce(200, jobs);
});
- it('dispatches request', (done) => {
- testAction(
+ it('dispatches request', () => {
+ return testAction(
fetchJobs,
stage,
mockedState,
@@ -268,7 +251,6 @@ describe('IDE pipelines actions', () => {
{ type: 'requestJobs', payload: stage.id },
{ type: 'receiveJobsSuccess', payload: { id: stage.id, data: jobs } },
],
- done,
);
});
});
@@ -278,8 +260,8 @@ describe('IDE pipelines actions', () => {
mock.onGet(stage.dropdownPath).replyOnce(500);
});
- it('dispatches error', (done) => {
- testAction(
+ it('dispatches error', () => {
+ return testAction(
fetchJobs,
stage,
mockedState,
@@ -288,69 +270,64 @@ describe('IDE pipelines actions', () => {
{ type: 'requestJobs', payload: stage.id },
{ type: 'receiveJobsError', payload: stage },
],
- done,
);
});
});
});
describe('toggleStageCollapsed', () => {
- it('commits collapse', (done) => {
- testAction(
+ it('commits collapse', () => {
+ return testAction(
toggleStageCollapsed,
1,
mockedState,
[{ type: types.TOGGLE_STAGE_COLLAPSE, payload: 1 }],
[],
- done,
);
});
});
describe('setDetailJob', () => {
- it('commits job', (done) => {
- testAction(
+ it('commits job', () => {
+ return testAction(
setDetailJob,
'job',
mockedState,
[{ type: types.SET_DETAIL_JOB, payload: 'job' }],
[{ type: 'rightPane/open', payload: rightSidebarViews.jobsDetail }],
- done,
);
});
- it('dispatches rightPane/open as pipeline when job is null', (done) => {
- testAction(
+ it('dispatches rightPane/open as pipeline when job is null', () => {
+ return testAction(
setDetailJob,
null,
mockedState,
[{ type: types.SET_DETAIL_JOB, payload: null }],
[{ type: 'rightPane/open', payload: rightSidebarViews.pipelines }],
- done,
);
});
- it('dispatches rightPane/open as job', (done) => {
- testAction(
+ it('dispatches rightPane/open as job', () => {
+ return testAction(
setDetailJob,
'job',
mockedState,
[{ type: types.SET_DETAIL_JOB, payload: 'job' }],
[{ type: 'rightPane/open', payload: rightSidebarViews.jobsDetail }],
- done,
);
});
});
describe('requestJobLogs', () => {
- it('commits request', (done) => {
- testAction(requestJobLogs, null, mockedState, [{ type: types.REQUEST_JOB_LOGS }], [], done);
+ it('commits request', () => {
+ return testAction(requestJobLogs, null, mockedState, [{ type: types.REQUEST_JOB_LOGS }], []);
});
});
describe('receiveJobLogsError', () => {
- it('commits error', (done) => {
- testAction(
+ it('commits error', () => {
+ return testAction(
receiveJobLogsError,
null,
mockedState,
@@ -366,20 +343,18 @@ describe('IDE pipelines actions', () => {
},
},
],
- done,
);
});
});
describe('receiveJobLogsSuccess', () => {
- it('commits data', (done) => {
- testAction(
+ it('commits data', () => {
+ return testAction(
receiveJobLogsSuccess,
'data',
mockedState,
[{ type: types.RECEIVE_JOB_LOGS_SUCCESS, payload: 'data' }],
[],
- done,
);
});
});
@@ -395,8 +370,8 @@ describe('IDE pipelines actions', () => {
mock.onGet(`${TEST_HOST}/project/builds/trace`).replyOnce(200, { html: 'html' });
});
- it('dispatches request', (done) => {
- testAction(
+ it('dispatches request', () => {
+ return testAction(
fetchJobLogs,
null,
mockedState,
@@ -405,7 +380,6 @@ describe('IDE pipelines actions', () => {
{ type: 'requestJobLogs' },
{ type: 'receiveJobLogsSuccess', payload: { html: 'html' } },
],
- done,
);
});
@@ -426,22 +400,21 @@ describe('IDE pipelines actions', () => {
mock.onGet(`${TEST_HOST}/project/builds/trace`).replyOnce(500);
});
- it('dispatches error', (done) => {
- testAction(
+ it('dispatches error', () => {
+ return testAction(
fetchJobLogs,
null,
mockedState,
[],
[{ type: 'requestJobLogs' }, { type: 'receiveJobLogsError' }],
- done,
);
});
});
});
describe('resetLatestPipeline', () => {
- it('commits reset mutations', (done) => {
- testAction(
+ it('commits reset mutations', () => {
+ return testAction(
resetLatestPipeline,
null,
mockedState,
@@ -450,7 +423,6 @@ describe('IDE pipelines actions', () => {
{ type: types.SET_DETAIL_JOB, payload: null },
],
[],
- done,
);
});
});
diff --git a/spec/frontend/ide/stores/modules/terminal_sync/actions_spec.js b/spec/frontend/ide/stores/modules/terminal_sync/actions_spec.js
index 22b0615c6d0..448fd909f39 100644
--- a/spec/frontend/ide/stores/modules/terminal_sync/actions_spec.js
+++ b/spec/frontend/ide/stores/modules/terminal_sync/actions_spec.js
@@ -22,43 +22,37 @@ describe('ide/stores/modules/terminal_sync/actions', () => {
});
describe('upload', () => {
- it('uploads to mirror and sets success', (done) => {
+ it('uploads to mirror and sets success', async () => {
mirror.upload.mockReturnValue(Promise.resolve());
- testAction(
+ await testAction(
actions.upload,
null,
rootState,
[{ type: types.START_LOADING }, { type: types.SET_SUCCESS }],
[],
- () => {
- expect(mirror.upload).toHaveBeenCalledWith(rootState);
- done();
- },
);
+ expect(mirror.upload).toHaveBeenCalledWith(rootState);
});
- it('sets error when failed', (done) => {
+ it('sets error when failed', () => {
const err = { message: 'it failed!' };
mirror.upload.mockReturnValue(Promise.reject(err));
- testAction(
+ return testAction(
actions.upload,
null,
rootState,
[{ type: types.START_LOADING }, { type: types.SET_ERROR, payload: err }],
[],
- done,
);
});
});
describe('stop', () => {
- it('disconnects from mirror', (done) => {
- testAction(actions.stop, null, rootState, [{ type: types.STOP }], [], () => {
- expect(mirror.disconnect).toHaveBeenCalled();
- done();
- });
+ it('disconnects from mirror', async () => {
+ await testAction(actions.stop, null, rootState, [{ type: types.STOP }], []);
+ expect(mirror.disconnect).toHaveBeenCalled();
});
});
@@ -83,20 +77,17 @@ describe('ide/stores/modules/terminal_sync/actions', () => {
};
});
- it('connects to mirror and sets success', (done) => {
+ it('connects to mirror and sets success', async () => {
mirror.connect.mockReturnValue(Promise.resolve());
- testAction(
+ await testAction(
actions.start,
null,
rootState,
[{ type: types.START_LOADING }, { type: types.SET_SUCCESS }],
[],
- () => {
- expect(mirror.connect).toHaveBeenCalledWith(TEST_SESSION.proxyWebsocketPath);
- done();
- },
);
+ expect(mirror.connect).toHaveBeenCalledWith(TEST_SESSION.proxyWebsocketPath);
});
it('sets error if connection fails', () => {
diff --git a/spec/frontend/ide/stores/plugins/terminal_spec.js b/spec/frontend/ide/stores/plugins/terminal_spec.js
index 912de88cb39..193300540fd 100644
--- a/spec/frontend/ide/stores/plugins/terminal_spec.js
+++ b/spec/frontend/ide/stores/plugins/terminal_spec.js
@@ -6,10 +6,10 @@ import { SET_BRANCH_WORKING_REFERENCE } from '~/ide/stores/mutation_types';
import createTerminalPlugin from '~/ide/stores/plugins/terminal';
const TEST_DATASET = {
- eeWebTerminalSvgPath: `${TEST_HOST}/web/terminal/svg`,
- eeWebTerminalHelpPath: `${TEST_HOST}/web/terminal/help`,
- eeWebTerminalConfigHelpPath: `${TEST_HOST}/web/terminal/config/help`,
- eeWebTerminalRunnersHelpPath: `${TEST_HOST}/web/terminal/runners/help`,
+ webTerminalSvgPath: `${TEST_HOST}/web/terminal/svg`,
+ webTerminalHelpPath: `${TEST_HOST}/web/terminal/help`,
+ webTerminalConfigHelpPath: `${TEST_HOST}/web/terminal/config/help`,
+ webTerminalRunnersHelpPath: `${TEST_HOST}/web/terminal/runners/help`,
};
Vue.use(Vuex);
@@ -40,10 +40,10 @@ describe('ide/stores/extend', () => {
it('dispatches terminal/setPaths', () => {
expect(store.dispatch).toHaveBeenCalledWith('terminal/setPaths', {
- webTerminalSvgPath: TEST_DATASET.eeWebTerminalSvgPath,
- webTerminalHelpPath: TEST_DATASET.eeWebTerminalHelpPath,
- webTerminalConfigHelpPath: TEST_DATASET.eeWebTerminalConfigHelpPath,
- webTerminalRunnersHelpPath: TEST_DATASET.eeWebTerminalRunnersHelpPath,
+ webTerminalSvgPath: TEST_DATASET.webTerminalSvgPath,
+ webTerminalHelpPath: TEST_DATASET.webTerminalHelpPath,
+ webTerminalConfigHelpPath: TEST_DATASET.webTerminalConfigHelpPath,
+ webTerminalRunnersHelpPath: TEST_DATASET.webTerminalRunnersHelpPath,
});
});
diff --git a/spec/frontend/image_diff/init_discussion_tab_spec.js b/spec/frontend/image_diff/init_discussion_tab_spec.js
index 5bc0c738944..f6f05037c95 100644
--- a/spec/frontend/image_diff/init_discussion_tab_spec.js
+++ b/spec/frontend/image_diff/init_discussion_tab_spec.js
@@ -11,23 +11,21 @@ describe('initDiscussionTab', () => {
`);
});
- it('should pass canCreateNote as false to initImageDiff', (done) => {
+ it('should pass canCreateNote as false to initImageDiff', () => {
jest
.spyOn(initImageDiffHelper, 'initImageDiff')
.mockImplementation((diffFileEl, canCreateNote) => {
expect(canCreateNote).toEqual(false);
- done();
});
initDiscussionTab();
});
- it('should pass renderCommentBadge as true to initImageDiff', (done) => {
+ it('should pass renderCommentBadge as true to initImageDiff', () => {
jest
.spyOn(initImageDiffHelper, 'initImageDiff')
.mockImplementation((diffFileEl, canCreateNote, renderCommentBadge) => {
expect(renderCommentBadge).toEqual(true);
- done();
});
initDiscussionTab();
diff --git a/spec/frontend/image_diff/replaced_image_diff_spec.js b/spec/frontend/image_diff/replaced_image_diff_spec.js
index cc4a2530fc4..2b401fc46bf 100644
--- a/spec/frontend/image_diff/replaced_image_diff_spec.js
+++ b/spec/frontend/image_diff/replaced_image_diff_spec.js
@@ -176,34 +176,36 @@ describe('ReplacedImageDiff', () => {
expect(ImageDiff.prototype.bindEvents).toHaveBeenCalled();
});
- it('should register click eventlistener to 2-up view mode', (done) => {
- jest.spyOn(ReplacedImageDiff.prototype, 'changeView').mockImplementation((viewMode) => {
- expect(viewMode).toEqual(viewTypes.TWO_UP);
- done();
- });
+ it('should register click eventlistener to 2-up view mode', () => {
+ const changeViewSpy = jest
+ .spyOn(ReplacedImageDiff.prototype, 'changeView')
+ .mockImplementation(() => {});
replacedImageDiff.bindEvents();
replacedImageDiff.viewModesEls[viewTypes.TWO_UP].click();
+
+ expect(changeViewSpy).toHaveBeenCalledWith(viewTypes.TWO_UP, expect.any(Object));
});
- it('should register click eventlistener to swipe view mode', (done) => {
- jest.spyOn(ReplacedImageDiff.prototype, 'changeView').mockImplementation((viewMode) => {
- expect(viewMode).toEqual(viewTypes.SWIPE);
- done();
- });
+ it('should register click eventlistener to swipe view mode', () => {
+ const changeViewSpy = jest
+ .spyOn(ReplacedImageDiff.prototype, 'changeView')
+ .mockImplementation(() => {});
replacedImageDiff.bindEvents();
replacedImageDiff.viewModesEls[viewTypes.SWIPE].click();
+
+ expect(changeViewSpy).toHaveBeenCalledWith(viewTypes.SWIPE, expect.any(Object));
});
- it('should register click eventlistener to onion skin view mode', (done) => {
- jest.spyOn(ReplacedImageDiff.prototype, 'changeView').mockImplementation((viewMode) => {
- expect(viewMode).toEqual(viewTypes.SWIPE);
- done();
- });
+ it('should register click eventlistener to onion skin view mode', () => {
+ const changeViewSpy = jest
+ .spyOn(ReplacedImageDiff.prototype, 'changeView')
+ .mockImplementation(() => {});
replacedImageDiff.bindEvents();
replacedImageDiff.viewModesEls[viewTypes.SWIPE].click();
+ expect(changeViewSpy).toHaveBeenCalledWith(viewTypes.SWIPE, expect.any(Object));
});
});
@@ -325,32 +327,34 @@ describe('ReplacedImageDiff', () => {
setupImageFrameEls();
});
- it('should pass showCommentIndicator normalized indicator values', (done) => {
+ it('should pass showCommentIndicator normalized indicator values', () => {
jest.spyOn(imageDiffHelper, 'showCommentIndicator').mockImplementation(() => {});
- jest
+ const resizeCoordinatesToImageElementSpy = jest
.spyOn(imageDiffHelper, 'resizeCoordinatesToImageElement')
- .mockImplementation((imageEl, meta) => {
- expect(meta.x).toEqual(indicator.x);
- expect(meta.y).toEqual(indicator.y);
- expect(meta.width).toEqual(indicator.image.width);
- expect(meta.height).toEqual(indicator.image.height);
- done();
- });
+ .mockImplementation(() => {});
+
replacedImageDiff.renderNewView(indicator);
+
+ expect(resizeCoordinatesToImageElementSpy).toHaveBeenCalledWith(undefined, {
+ x: indicator.x,
+ y: indicator.y,
+ width: indicator.image.width,
+ height: indicator.image.height,
+ });
});
- it('should call showCommentIndicator', (done) => {
+ it('should call showCommentIndicator', () => {
const normalized = {
normalized: true,
};
jest.spyOn(imageDiffHelper, 'resizeCoordinatesToImageElement').mockReturnValue(normalized);
- jest
+ const showCommentIndicatorSpy = jest
.spyOn(imageDiffHelper, 'showCommentIndicator')
- .mockImplementation((imageFrameEl, normalizedIndicator) => {
- expect(normalizedIndicator).toEqual(normalized);
- done();
- });
+ .mockImplementation(() => {});
+
replacedImageDiff.renderNewView(indicator);
+
+ expect(showCommentIndicatorSpy).toHaveBeenCalledWith(undefined, normalized);
});
});
});
diff --git a/spec/frontend/import_entities/components/import_status_spec.js b/spec/frontend/import_entities/components/import_status_spec.js
new file mode 100644
index 00000000000..686a21e3923
--- /dev/null
+++ b/spec/frontend/import_entities/components/import_status_spec.js
@@ -0,0 +1,145 @@
+import { GlAccordionItem, GlBadge, GlIcon } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import ImportStatus from '~/import_entities/components/import_status.vue';
+import { STATUSES } from '~/import_entities/constants';
+
+describe('Import entities status component', () => {
+ let wrapper;
+
+ const createComponent = (propsData) => {
+ wrapper = shallowMount(ImportStatus, {
+ propsData,
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('success status', () => {
+ const getStatusText = () => wrapper.findComponent(GlBadge).text();
+
+ it('displays finished status as complete when no stats are provided', () => {
+ createComponent({
+ status: STATUSES.FINISHED,
+ });
+ expect(getStatusText()).toBe('Complete');
+ });
+
+ it('displays finished status as complete when all stats items were processed', () => {
+ const statItems = { label: 100, note: 200 };
+
+ createComponent({
+ status: STATUSES.FINISHED,
+ stats: {
+ fetched: { ...statItems },
+ imported: { ...statItems },
+ },
+ });
+
+ expect(getStatusText()).toBe('Complete');
+ });
+
+ it('displays finished status as partial when all stats items were processed', () => {
+ const statItems = { label: 100, note: 200 };
+
+ createComponent({
+ status: STATUSES.FINISHED,
+ stats: {
+ fetched: { ...statItems },
+ imported: { ...statItems, label: 50 },
+ },
+ });
+
+ expect(getStatusText()).toBe('Partial import');
+ });
+ });
+
+ describe('details drawer', () => {
+ const findDetailsDrawer = () => wrapper.findComponent(GlAccordionItem);
+
+ it('renders details drawer to be present when stats are provided', () => {
+ createComponent({
+ status: 'created',
+ stats: { fetched: { label: 1 }, imported: { label: 0 } },
+ });
+
+ expect(findDetailsDrawer().exists()).toBe(true);
+ });
+
+ it('does not render details drawer when no stats are provided', () => {
+ createComponent({
+ status: 'created',
+ });
+
+ expect(findDetailsDrawer().exists()).toBe(false);
+ });
+
+ it('does not render details drawer when stats are empty', () => {
+ createComponent({
+ status: 'created',
+ stats: { fetched: {}, imported: {} },
+ });
+
+ expect(findDetailsDrawer().exists()).toBe(false);
+ });
+
+ it('does not render details drawer when no known stats are provided', () => {
+ createComponent({
+ status: 'created',
+ stats: {
+ fetched: {
+ UNKNOWN_STAT: 100,
+ },
+ imported: {
+ UNKNOWN_STAT: 0,
+ },
+ },
+ });
+
+ expect(findDetailsDrawer().exists()).toBe(false);
+ });
+ });
+
+ describe('stats display', () => {
+ const getStatusIcon = () =>
+ wrapper.findComponent(GlAccordionItem).findComponent(GlIcon).props().name;
+
+ const createComponentWithStats = ({ fetched, imported }) => {
+ createComponent({
+ status: 'created',
+ stats: {
+ fetched: { label: fetched },
+ imported: { label: imported },
+ },
+ });
+ };
+
+ it('displays scheduled status when imported is 0', () => {
+ createComponentWithStats({
+ fetched: 100,
+ imported: 0,
+ });
+
+ expect(getStatusIcon()).toBe('status-scheduled');
+ });
+
+ it('displays running status when imported is not equal to fetched', () => {
+ createComponentWithStats({
+ fetched: 100,
+ imported: 10,
+ });
+
+ expect(getStatusIcon()).toBe('status-running');
+ });
+
+ it('displays success status when imported is equal to fetched', () => {
+ createComponentWithStats({
+ fetched: 100,
+ imported: 100,
+ });
+
+ expect(getStatusIcon()).toBe('status-success');
+ });
+ });
+});
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 16adf88700f..88fcedd31b2 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
@@ -31,7 +31,7 @@ describe('ImportProjectsTable', () => {
const findImportAllButton = () =>
wrapper
.findAll(GlButton)
- .filter((w) => w.props().variant === 'success')
+ .filter((w) => w.props().variant === 'confirm')
.at(0);
const findImportAllModal = () => wrapper.find({ ref: 'importAllModal' });
diff --git a/spec/frontend/import_entities/import_projects/components/provider_repo_table_row_spec.js b/spec/frontend/import_entities/import_projects/components/provider_repo_table_row_spec.js
index c8afa9ea57d..41a005199e1 100644
--- a/spec/frontend/import_entities/import_projects/components/provider_repo_table_row_spec.js
+++ b/spec/frontend/import_entities/import_projects/components/provider_repo_table_row_spec.js
@@ -98,6 +98,8 @@ describe('ProviderRepoTableRow', () => {
});
describe('when rendering imported project', () => {
+ const FAKE_STATS = {};
+
const repo = {
importSource: {
id: 'remote-1',
@@ -109,6 +111,7 @@ describe('ProviderRepoTableRow', () => {
fullPath: 'fullPath',
importSource: 'importSource',
importStatus: STATUSES.FINISHED,
+ stats: FAKE_STATS,
},
};
@@ -134,6 +137,10 @@ describe('ProviderRepoTableRow', () => {
it('does not render import button', () => {
expect(findImportButton().exists()).toBe(false);
});
+
+ it('passes stats to import status component', () => {
+ expect(wrapper.find(ImportStatus).props().stats).toBe(FAKE_STATS);
+ });
});
describe('when rendering incompatible project', () => {
diff --git a/spec/frontend/import_entities/import_projects/store/mutations_spec.js b/spec/frontend/import_entities/import_projects/store/mutations_spec.js
index e062d889325..77fae951300 100644
--- a/spec/frontend/import_entities/import_projects/store/mutations_spec.js
+++ b/spec/frontend/import_entities/import_projects/store/mutations_spec.js
@@ -232,6 +232,35 @@ describe('import_projects store mutations', () => {
updatedProjects[0].importStatus,
);
});
+
+ it('updates import stats of project', () => {
+ const repoId = 1;
+ state = {
+ repositories: [
+ { importedProject: { id: repoId, stats: {} }, importStatus: STATUSES.STARTED },
+ ],
+ };
+ const newStats = {
+ fetched: {
+ label: 10,
+ },
+ imported: {
+ label: 1,
+ },
+ };
+
+ const updatedProjects = [
+ {
+ id: repoId,
+ importStatus: STATUSES.FINISHED,
+ stats: newStats,
+ },
+ ];
+
+ mutations[types.RECEIVE_JOBS_SUCCESS](state, updatedProjects);
+
+ expect(state.repositories[0].importedProject.stats).toStrictEqual(newStats);
+ });
});
describe(`${types.REQUEST_NAMESPACES}`, () => {
diff --git a/spec/frontend/incidents/components/incidents_list_spec.js b/spec/frontend/incidents/components/incidents_list_spec.js
index 9ed0294e876..a556f3c17f3 100644
--- a/spec/frontend/incidents/components/incidents_list_spec.js
+++ b/spec/frontend/incidents/components/incidents_list_spec.js
@@ -7,6 +7,7 @@ import {
I18N,
TH_CREATED_AT_TEST_ID,
TH_SEVERITY_TEST_ID,
+ TH_ESCALATION_STATUS_TEST_ID,
TH_PUBLISHED_TEST_ID,
TH_INCIDENT_SLA_TEST_ID,
trackIncidentCreateNewOptions,
@@ -170,6 +171,7 @@ describe('Incidents List', () => {
expect(link.text()).toBe(title);
expect(link.attributes('href')).toContain(`issues/incident/${iid}`);
+ expect(link.find('.gl-text-truncate').exists()).toBe(true);
});
describe('Assignees', () => {
@@ -200,15 +202,14 @@ describe('Incidents List', () => {
describe('Escalation status', () => {
it('renders escalation status per row', () => {
- expect(findEscalationStatus().length).toBe(mockIncidents.length);
-
- const actualStatuses = findEscalationStatus().wrappers.map((status) => status.text());
- expect(actualStatuses).toEqual([
- 'Triggered',
- 'Acknowledged',
- 'Resolved',
- I18N.noEscalationStatus,
- ]);
+ const statuses = findEscalationStatus().wrappers;
+ const expectedStatuses = ['Triggered', 'Acknowledged', 'Resolved', I18N.noEscalationStatus];
+
+ expect(statuses.length).toBe(mockIncidents.length);
+ statuses.forEach((status, index) => {
+ expect(status.text()).toEqual(expectedStatuses[index]);
+ expect(status.classes('gl-text-truncate')).toBe(true);
+ });
});
describe('when feature is disabled', () => {
@@ -294,11 +295,12 @@ describe('Incidents List', () => {
const noneSort = 'none';
it.each`
- description | selector | initialSort | firstSort | nextSort
- ${'creation date'} | ${TH_CREATED_AT_TEST_ID} | ${descSort} | ${ascSort} | ${descSort}
- ${'severity'} | ${TH_SEVERITY_TEST_ID} | ${noneSort} | ${descSort} | ${ascSort}
- ${'publish date'} | ${TH_PUBLISHED_TEST_ID} | ${noneSort} | ${descSort} | ${ascSort}
- ${'due date'} | ${TH_INCIDENT_SLA_TEST_ID} | ${noneSort} | ${ascSort} | ${descSort}
+ description | selector | initialSort | firstSort | nextSort
+ ${'creation date'} | ${TH_CREATED_AT_TEST_ID} | ${descSort} | ${ascSort} | ${descSort}
+ ${'severity'} | ${TH_SEVERITY_TEST_ID} | ${noneSort} | ${descSort} | ${ascSort}
+ ${'status'} | ${TH_ESCALATION_STATUS_TEST_ID} | ${noneSort} | ${descSort} | ${ascSort}
+ ${'publish date'} | ${TH_PUBLISHED_TEST_ID} | ${noneSort} | ${descSort} | ${ascSort}
+ ${'due date'} | ${TH_INCIDENT_SLA_TEST_ID} | ${noneSort} | ${ascSort} | ${descSort}
`(
'updates sort with new direction when sorting by $description',
async ({ selector, initialSort, firstSort, nextSort }) => {
diff --git a/spec/frontend/integrations/edit/components/integration_form_spec.js b/spec/frontend/integrations/edit/components/integration_form_spec.js
index c4569070d09..ca481e009cf 100644
--- a/spec/frontend/integrations/edit/components/integration_form_spec.js
+++ b/spec/frontend/integrations/edit/components/integration_form_spec.js
@@ -9,8 +9,6 @@ import ActiveCheckbox from '~/integrations/edit/components/active_checkbox.vue';
import ConfirmationModal from '~/integrations/edit/components/confirmation_modal.vue';
import DynamicField from '~/integrations/edit/components/dynamic_field.vue';
import IntegrationForm from '~/integrations/edit/components/integration_form.vue';
-import JiraIssuesFields from '~/integrations/edit/components/jira_issues_fields.vue';
-import JiraTriggerFields from '~/integrations/edit/components/jira_trigger_fields.vue';
import OverrideDropdown from '~/integrations/edit/components/override_dropdown.vue';
import ResetConfirmationModal from '~/integrations/edit/components/reset_confirmation_modal.vue';
import TriggerFields from '~/integrations/edit/components/trigger_fields.vue';
@@ -55,7 +53,6 @@ describe('IntegrationForm', () => {
OverrideDropdown,
ActiveCheckbox,
ConfirmationModal,
- JiraTriggerFields,
TriggerFields,
},
mocks: {
@@ -74,8 +71,6 @@ describe('IntegrationForm', () => {
const findProjectSaveButton = () => wrapper.findByTestId('save-button');
const findInstanceOrGroupSaveButton = () => wrapper.findByTestId('save-button-instance-group');
const findTestButton = () => wrapper.findByTestId('test-button');
- const findJiraTriggerFields = () => wrapper.findComponent(JiraTriggerFields);
- const findJiraIssuesFields = () => wrapper.findComponent(JiraIssuesFields);
const findTriggerFields = () => wrapper.findComponent(TriggerFields);
const findGlForm = () => wrapper.findComponent(GlForm);
const findRedirectToField = () => wrapper.findByTestId('redirect-to-field');
@@ -198,49 +193,6 @@ describe('IntegrationForm', () => {
});
});
- describe('type is "slack"', () => {
- beforeEach(() => {
- createComponent({
- customStateProps: { type: 'slack' },
- });
- });
-
- it('does not render JiraTriggerFields', () => {
- expect(findJiraTriggerFields().exists()).toBe(false);
- });
-
- it('does not render JiraIssuesFields', () => {
- expect(findJiraIssuesFields().exists()).toBe(false);
- });
- });
-
- describe('type is "jira"', () => {
- beforeEach(() => {
- jest.spyOn(document, 'querySelector').mockReturnValue(document.createElement('form'));
-
- createComponent({
- customStateProps: { type: 'jira', testPath: '/test' },
- mountFn: mountExtended,
- });
- });
-
- it('renders JiraTriggerFields', () => {
- expect(findJiraTriggerFields().exists()).toBe(true);
- });
-
- it('renders JiraIssuesFields', () => {
- expect(findJiraIssuesFields().exists()).toBe(true);
- });
-
- describe('when JiraIssueFields emits `request-jira-issue-types` event', () => {
- it('dispatches `requestJiraIssueTypes` action', () => {
- findJiraIssuesFields().vm.$emit('request-jira-issue-types');
-
- expect(dispatch).toHaveBeenCalledWith('requestJiraIssueTypes', expect.any(FormData));
- });
- });
- });
-
describe('triggerEvents is present', () => {
it('renders TriggerFields', () => {
const events = [{ title: 'push' }];
@@ -272,9 +224,6 @@ describe('IntegrationForm', () => {
];
createComponent({
- provide: {
- glFeatures: { integrationFormSections: true },
- },
customStateProps: {
sections: [mockSectionConnection],
fields: [...sectionFields, ...nonSectionFields],
@@ -363,9 +312,6 @@ describe('IntegrationForm', () => {
describe('when integration has sections', () => {
beforeEach(() => {
createComponent({
- provide: {
- glFeatures: { integrationFormSections: true },
- },
customStateProps: {
sections: [mockSectionConnection],
},
@@ -396,9 +342,6 @@ describe('IntegrationForm', () => {
];
createComponent({
- provide: {
- glFeatures: { integrationFormSections: true },
- },
customStateProps: {
sections: [mockSectionConnection],
fields: [...sectionFields, ...nonSectionFields],
@@ -417,9 +360,6 @@ describe('IntegrationForm', () => {
({ formActive, novalidate }) => {
beforeEach(() => {
createComponent({
- provide: {
- glFeatures: { integrationFormSections: true },
- },
customStateProps: {
sections: [mockSectionConnection],
showActive: true,
@@ -441,9 +381,6 @@ describe('IntegrationForm', () => {
jest.spyOn(document, 'querySelector').mockReturnValue(document.createElement('form'));
createComponent({
- provide: {
- glFeatures: { integrationFormSections: true },
- },
customStateProps: {
sections: [mockSectionConnection],
testPath: '/test',
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 33fd08a5959..94e370a485f 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 = {
- editProjectPath: '/edit',
showJiraIssuesIntegration: true,
showJiraVulnerabilitiesIntegration: true,
upgradePlanPath: 'https://gitlab.com',
@@ -46,7 +45,6 @@ describe('JiraIssuesFields', () => {
const findPremiumUpgradeCTA = () => wrapper.findByTestId('premium-upgrade-cta');
const findUltimateUpgradeCTA = () => wrapper.findByTestId('ultimate-upgrade-cta');
const findJiraForVulnerabilities = () => wrapper.findByTestId('jira-for-vulnerabilities');
- const findConflictWarning = () => wrapper.findByTestId('conflict-warning-text');
const setEnableCheckbox = async (isEnabled = true) =>
findEnableCheckbox().vm.$emit('input', isEnabled);
@@ -75,10 +73,9 @@ describe('JiraIssuesFields', () => {
});
if (showJiraIssuesIntegration) {
- it('renders checkbox and input field', () => {
+ it('renders enable checkbox', () => {
expect(findEnableCheckbox().exists()).toBe(true);
expect(findEnableCheckboxDisabled()).toBeUndefined();
- expect(findProjectKey().exists()).toBe(true);
});
it('does not render the Premium CTA', () => {
@@ -98,9 +95,8 @@ describe('JiraIssuesFields', () => {
});
}
} else {
- it('does not render checkbox and input field', () => {
+ it('does not render enable checkbox', () => {
expect(findEnableCheckbox().exists()).toBe(false);
- expect(findProjectKey().exists()).toBe(false);
});
it('renders the Premium CTA', () => {
@@ -122,12 +118,8 @@ describe('JiraIssuesFields', () => {
createComponent({ props: { initialProjectKey: '' } });
});
- it('renders disabled project_key input', () => {
- const projectKey = findProjectKey();
-
- expect(projectKey.exists()).toBe(true);
- expect(projectKey.attributes('disabled')).toBe('disabled');
- expect(projectKey.attributes('required')).toBeUndefined();
+ it('does not render project_key input', () => {
+ expect(findProjectKey().exists()).toBe(false);
});
// As per https://vuejs.org/v2/guide/forms.html#Checkbox-1,
@@ -137,45 +129,23 @@ describe('JiraIssuesFields', () => {
});
describe('when isInheriting = true', () => {
- it('disables checkbox and sets input as readonly', () => {
+ it('disables checkbox', () => {
createComponent({ isInheriting: true });
expect(findEnableCheckboxDisabled()).toBe('disabled');
- expect(findProjectKey().attributes('readonly')).toBe('readonly');
});
});
describe('on enable issues', () => {
- it('enables project_key input as required', async () => {
+ it('renders project_key input as required', async () => {
await setEnableCheckbox(true);
- expect(findProjectKey().attributes('disabled')).toBeUndefined();
+ expect(findProjectKey().exists()).toBe(true);
expect(findProjectKey().attributes('required')).toBe('required');
});
});
});
- it('contains link to editProjectPath', () => {
- createComponent();
-
- expect(wrapper.find(`a[href="${defaultProps.editProjectPath}"]`).exists()).toBe(true);
- });
-
- describe('GitLab issues warning', () => {
- it.each`
- gitlabIssuesEnabled | scenario
- ${true} | ${'displays conflict warning'}
- ${false} | ${'does not display conflict warning'}
- `(
- '$scenario when `gitlabIssuesEnabled` is `$gitlabIssuesEnabled`',
- ({ gitlabIssuesEnabled }) => {
- createComponent({ props: { gitlabIssuesEnabled } });
-
- expect(findConflictWarning().exists()).toBe(gitlabIssuesEnabled);
- },
- );
- });
-
describe('Vulnerabilities creation', () => {
beforeEach(() => {
createComponent();
diff --git a/spec/frontend/invite_members/components/group_select_spec.js b/spec/frontend/invite_members/components/group_select_spec.js
index 192f3fdd381..e1563a7bb3a 100644
--- a/spec/frontend/invite_members/components/group_select_spec.js
+++ b/spec/frontend/invite_members/components/group_select_spec.js
@@ -4,7 +4,6 @@ import waitForPromises from 'helpers/wait_for_promises';
import * as groupsApi from '~/api/groups_api';
import GroupSelect from '~/invite_members/components/group_select.vue';
-const accessLevels = { Guest: 10, Reporter: 20, Developer: 30, Maintainer: 40, Owner: 50 };
const group1 = { id: 1, full_name: 'Group One', avatar_url: 'test' };
const group2 = { id: 2, full_name: 'Group Two', avatar_url: 'test' };
const allGroups = [group1, group2];
@@ -13,7 +12,6 @@ const createComponent = (props = {}) => {
return mount(GroupSelect, {
propsData: {
invalidGroups: [],
- accessLevels,
...props,
},
});
@@ -66,9 +64,8 @@ describe('GroupSelect', () => {
resolveApiRequest({ data: allGroups });
expect(groupsApi.getGroups).toHaveBeenCalledWith(group1.name, {
- active: true,
exclude_internal: true,
- min_access_level: accessLevels.Guest,
+ active: true,
});
});
diff --git a/spec/frontend/invite_members/components/invite_groups_modal_spec.js b/spec/frontend/invite_members/components/invite_groups_modal_spec.js
index 8085f48f6e2..f9cb4a149f2 100644
--- a/spec/frontend/invite_members/components/invite_groups_modal_spec.js
+++ b/spec/frontend/invite_members/components/invite_groups_modal_spec.js
@@ -42,18 +42,19 @@ describe('InviteGroupsModal', () => {
wrapper = null;
});
+ const findModal = () => wrapper.findComponent(GlModal);
const findGroupSelect = () => wrapper.findComponent(GroupSelect);
const findIntroText = () => wrapper.findByTestId('modal-base-intro-text').text();
- const findCancelButton = () => wrapper.findByTestId('cancel-button');
- const findInviteButton = () => wrapper.findByTestId('invite-button');
const findMembersFormGroup = () => wrapper.findByTestId('members-form-group');
const membersFormGroupInvalidFeedback = () =>
findMembersFormGroup().attributes('invalid-feedback');
- const clickInviteButton = () => findInviteButton().vm.$emit('click');
- const clickCancelButton = () => findCancelButton().vm.$emit('click');
- const triggerGroupSelect = (val) => findGroupSelect().vm.$emit('input', val);
const findBase = () => wrapper.findComponent(InviteModalBase);
- const hideModal = () => wrapper.findComponent(GlModal).vm.$emit('hide');
+ const triggerGroupSelect = (val) => findGroupSelect().vm.$emit('input', val);
+ const emitEventFromModal = (eventName) => () =>
+ findModal().vm.$emit(eventName, { preventDefault: jest.fn() });
+ const hideModal = emitEventFromModal('hidden');
+ const clickInviteButton = emitEventFromModal('primary');
+ const clickCancelButton = emitEventFromModal('cancel');
describe('displaying the correct introText and form group description', () => {
describe('when inviting to a project', () => {
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 dd16bb48cb8..84317da39e6 100644
--- a/spec/frontend/invite_members/components/invite_members_modal_spec.js
+++ b/spec/frontend/invite_members/components/invite_members_modal_spec.js
@@ -23,7 +23,7 @@ import ContentTransition from '~/vue_shared/components/content_transition.vue';
import axios from '~/lib/utils/axios_utils';
import httpStatus from '~/lib/utils/http_status';
import { getParameterValues } from '~/lib/utils/url_utility';
-import { apiPaths, membersApiResponse, invitationsApiResponse } from '../mock_data/api_responses';
+import { GROUPS_INVITATIONS_PATH, invitationsApiResponse } from '../mock_data/api_responses';
import {
propsData,
inviteSource,
@@ -85,12 +85,13 @@ describe('InviteMembersModal', () => {
mock.restore();
});
+ const findModal = () => wrapper.findComponent(GlModal);
const findBase = () => wrapper.findComponent(InviteModalBase);
const findIntroText = () => wrapper.findByTestId('modal-base-intro-text').text();
- const findCancelButton = () => wrapper.findByTestId('cancel-button');
- const findInviteButton = () => wrapper.findByTestId('invite-button');
- const clickInviteButton = () => findInviteButton().vm.$emit('click');
- const clickCancelButton = () => findCancelButton().vm.$emit('click');
+ const emitEventFromModal = (eventName) => () =>
+ findModal().vm.$emit(eventName, { preventDefault: jest.fn() });
+ const clickInviteButton = emitEventFromModal('primary');
+ const clickCancelButton = emitEventFromModal('cancel');
const findMembersFormGroup = () => wrapper.findByTestId('members-form-group');
const membersFormGroupInvalidFeedback = () =>
findMembersFormGroup().attributes('invalid-feedback');
@@ -276,7 +277,7 @@ describe('InviteMembersModal', () => {
});
it('renders the modal with the correct title', () => {
- expect(wrapper.findComponent(GlModal).props('title')).toBe(MEMBERS_MODAL_CELEBRATE_TITLE);
+ expect(findModal().props('title')).toBe(MEMBERS_MODAL_CELEBRATE_TITLE);
});
it('includes the correct celebration text and emoji', () => {
@@ -300,11 +301,8 @@ describe('InviteMembersModal', () => {
});
describe('submitting the invite form', () => {
- const mockMembersApi = (code, data) => {
- mock.onPost(apiPaths.GROUPS_MEMBERS).reply(code, data);
- };
const mockInvitationsApi = (code, data) => {
- mock.onPost(apiPaths.GROUPS_INVITATIONS).reply(code, data);
+ mock.onPost(GROUPS_INVITATIONS_PATH).reply(code, data);
};
const expectedEmailRestrictedError =
@@ -328,7 +326,7 @@ describe('InviteMembersModal', () => {
await triggerMembersTokenSelect([user1, user2]);
wrapper.vm.$toast = { show: jest.fn() };
- jest.spyOn(Api, 'addGroupMembersByUserId').mockResolvedValue({ data: postData });
+ jest.spyOn(Api, 'inviteGroupMembers').mockResolvedValue({ data: postData });
});
describe('when triggered from regular mounting', () => {
@@ -336,12 +334,8 @@ describe('InviteMembersModal', () => {
clickInviteButton();
});
- it('sets isLoading on the Invite button when it is clicked', () => {
- expect(findInviteButton().props('loading')).toBe(true);
- });
-
- it('calls Api addGroupMembersByUserId with the correct params', () => {
- expect(Api.addGroupMembersByUserId).toHaveBeenCalledWith(propsData.id, postData);
+ it('calls Api inviteGroupMembers with the correct params', () => {
+ expect(Api.inviteGroupMembers).toHaveBeenCalledWith(propsData.id, postData);
});
it('displays the successful toastMessage', () => {
@@ -371,21 +365,9 @@ describe('InviteMembersModal', () => {
await triggerMembersTokenSelect([user1]);
});
- it('displays "Member already exists" api message for http status conflict', async () => {
- mockMembersApi(httpStatus.CONFLICT, membersApiResponse.MEMBER_ALREADY_EXISTS);
-
- clickInviteButton();
-
- await waitForPromises();
-
- expect(membersFormGroupInvalidFeedback()).toBe('Member already exists');
- expect(findMembersSelect().props('validationState')).toBe(false);
- expect(findInviteButton().props('loading')).toBe(false);
- });
-
describe('clearing the invalid state and message', () => {
beforeEach(async () => {
- mockMembersApi(httpStatus.CONFLICT, membersApiResponse.MEMBER_ALREADY_EXISTS);
+ mockInvitationsApi(httpStatus.CREATED, invitationsApiResponse.EMAIL_TAKEN);
clickInviteButton();
@@ -393,7 +375,9 @@ describe('InviteMembersModal', () => {
});
it('clears the error when the list of members to invite is cleared', async () => {
- expect(membersFormGroupInvalidFeedback()).toBe('Member already exists');
+ expect(membersFormGroupInvalidFeedback()).toBe(
+ Object.values(invitationsApiResponse.EMAIL_TAKEN.message)[0],
+ );
expect(findMembersSelect().props('validationState')).toBe(false);
findMembersSelect().vm.$emit('clear');
@@ -414,7 +398,7 @@ describe('InviteMembersModal', () => {
});
it('clears the error when the modal is hidden', async () => {
- wrapper.findComponent(GlModal).vm.$emit('hide');
+ findModal().vm.$emit('hidden');
await nextTick();
@@ -424,15 +408,17 @@ describe('InviteMembersModal', () => {
});
it('clears the invalid state and message once the list of members to invite is cleared', async () => {
- mockMembersApi(httpStatus.CONFLICT, membersApiResponse.MEMBER_ALREADY_EXISTS);
+ mockInvitationsApi(httpStatus.CREATED, invitationsApiResponse.EMAIL_TAKEN);
clickInviteButton();
await waitForPromises();
- expect(membersFormGroupInvalidFeedback()).toBe('Member already exists');
+ expect(membersFormGroupInvalidFeedback()).toBe(
+ Object.values(invitationsApiResponse.EMAIL_TAKEN.message)[0],
+ );
expect(findMembersSelect().props('validationState')).toBe(false);
- expect(findInviteButton().props('loading')).toBe(false);
+ expect(findModal().props('actionPrimary').attributes.loading).toBe(false);
findMembersSelect().vm.$emit('clear');
@@ -440,11 +426,14 @@ describe('InviteMembersModal', () => {
expect(membersFormGroupInvalidFeedback()).toBe('');
expect(findMembersSelect().props('validationState')).toBe(null);
- expect(findInviteButton().props('loading')).toBe(false);
+ expect(findModal().props('actionPrimary').attributes.loading).toBe(false);
});
it('displays the generic error for http server error', async () => {
- mockMembersApi(httpStatus.INTERNAL_SERVER_ERROR, 'Request failed with status code 500');
+ mockInvitationsApi(
+ httpStatus.INTERNAL_SERVER_ERROR,
+ 'Request failed with status code 500',
+ );
clickInviteButton();
@@ -454,7 +443,7 @@ describe('InviteMembersModal', () => {
});
it('displays the restricted user api message for response with bad request', async () => {
- mockMembersApi(httpStatus.BAD_REQUEST, membersApiResponse.SINGLE_USER_RESTRICTED);
+ mockInvitationsApi(httpStatus.CREATED, invitationsApiResponse.EMAIL_RESTRICTED);
clickInviteButton();
@@ -464,7 +453,7 @@ describe('InviteMembersModal', () => {
});
it('displays the first part of the error when multiple existing users are restricted by email', async () => {
- mockMembersApi(httpStatus.CREATED, membersApiResponse.MULTIPLE_USERS_RESTRICTED);
+ mockInvitationsApi(httpStatus.CREATED, invitationsApiResponse.MULTIPLE_RESTRICTED);
clickInviteButton();
@@ -475,19 +464,6 @@ describe('InviteMembersModal', () => {
);
expect(findMembersSelect().props('validationState')).toBe(false);
});
-
- it('displays an access_level error message received for the existing user', async () => {
- mockMembersApi(httpStatus.BAD_REQUEST, membersApiResponse.SINGLE_USER_ACCESS_LEVEL);
-
- clickInviteButton();
-
- await waitForPromises();
-
- expect(membersFormGroupInvalidFeedback()).toBe(
- 'should be greater than or equal to Owner inherited membership from group Gitlab Org',
- );
- expect(findMembersSelect().props('validationState')).toBe(false);
- });
});
});
@@ -508,7 +484,7 @@ describe('InviteMembersModal', () => {
await triggerMembersTokenSelect([user3]);
wrapper.vm.$toast = { show: jest.fn() };
- jest.spyOn(Api, 'inviteGroupMembersByEmail').mockResolvedValue({ data: postData });
+ jest.spyOn(Api, 'inviteGroupMembers').mockResolvedValue({ data: postData });
});
describe('when triggered from regular mounting', () => {
@@ -516,8 +492,8 @@ describe('InviteMembersModal', () => {
clickInviteButton();
});
- it('calls Api inviteGroupMembersByEmail with the correct params', () => {
- expect(Api.inviteGroupMembersByEmail).toHaveBeenCalledWith(propsData.id, postData);
+ it('calls Api inviteGroupMembers with the correct params', () => {
+ expect(Api.inviteGroupMembers).toHaveBeenCalledWith(propsData.id, postData);
});
it('displays the successful toastMessage', () => {
@@ -542,7 +518,7 @@ describe('InviteMembersModal', () => {
expect(membersFormGroupInvalidFeedback()).toBe(expectedSyntaxError);
expect(findMembersSelect().props('validationState')).toBe(false);
- expect(findInviteButton().props('loading')).toBe(false);
+ expect(findModal().props('actionPrimary').attributes.loading).toBe(false);
});
it('displays the restricted email error when restricted email is invited', async () => {
@@ -554,23 +530,11 @@ describe('InviteMembersModal', () => {
expect(membersFormGroupInvalidFeedback()).toContain(expectedEmailRestrictedError);
expect(findMembersSelect().props('validationState')).toBe(false);
- expect(findInviteButton().props('loading')).toBe(false);
- });
-
- it('displays the successful toast message when email has already been invited', async () => {
- mockInvitationsApi(httpStatus.CREATED, invitationsApiResponse.EMAIL_TAKEN);
- wrapper.vm.$toast = { show: jest.fn() };
-
- clickInviteButton();
-
- await waitForPromises();
-
- expect(wrapper.vm.$toast.show).toHaveBeenCalledWith('Members were successfully added');
- expect(findMembersSelect().props('validationState')).toBe(null);
+ expect(findModal().props('actionPrimary').attributes.loading).toBe(false);
});
it('displays the first error message when multiple emails return a restricted error message', async () => {
- mockInvitationsApi(httpStatus.CREATED, invitationsApiResponse.MULTIPLE_EMAIL_RESTRICTED);
+ mockInvitationsApi(httpStatus.CREATED, invitationsApiResponse.MULTIPLE_RESTRICTED);
clickInviteButton();
@@ -617,19 +581,17 @@ describe('InviteMembersModal', () => {
format: 'json',
tasks_to_be_done: [],
tasks_project_id: '',
+ user_id: '1',
+ email: 'email@example.com',
};
- const emailPostData = { ...postData, email: 'email@example.com' };
- const idPostData = { ...postData, user_id: '1' };
-
describe('when invites are sent successfully', () => {
beforeEach(async () => {
createComponent();
await triggerMembersTokenSelect([user1, user3]);
wrapper.vm.$toast = { show: jest.fn() };
- jest.spyOn(Api, 'inviteGroupMembersByEmail').mockResolvedValue({ data: postData });
- jest.spyOn(Api, 'addGroupMembersByUserId').mockResolvedValue({ data: postData });
+ jest.spyOn(Api, 'inviteGroupMembers').mockResolvedValue({ data: postData });
});
describe('when triggered from regular mounting', () => {
@@ -637,12 +599,8 @@ describe('InviteMembersModal', () => {
clickInviteButton();
});
- it('calls Api inviteGroupMembersByEmail with the correct params', () => {
- expect(Api.inviteGroupMembersByEmail).toHaveBeenCalledWith(propsData.id, emailPostData);
- });
-
- it('calls Api addGroupMembersByUserId with the correct params', () => {
- expect(Api.addGroupMembersByUserId).toHaveBeenCalledWith(propsData.id, idPostData);
+ it('calls Api inviteGroupMembers with the correct params', () => {
+ expect(Api.inviteGroupMembers).toHaveBeenCalledWith(propsData.id, postData);
});
it('displays the successful toastMessage', () => {
@@ -655,12 +613,8 @@ describe('InviteMembersModal', () => {
clickInviteButton();
- expect(Api.inviteGroupMembersByEmail).toHaveBeenCalledWith(propsData.id, {
- ...emailPostData,
- invite_source: '_invite_source_',
- });
- expect(Api.addGroupMembersByUserId).toHaveBeenCalledWith(propsData.id, {
- ...idPostData,
+ expect(Api.inviteGroupMembers).toHaveBeenCalledWith(propsData.id, {
+ ...postData,
invite_source: '_invite_source_',
});
});
@@ -673,7 +627,6 @@ describe('InviteMembersModal', () => {
await triggerMembersTokenSelect([user1, user3]);
mockInvitationsApi(httpStatus.BAD_REQUEST, invitationsApiResponse.EMAIL_INVALID);
- mockMembersApi(httpStatus.OK, '200 OK');
clickInviteButton();
});
@@ -692,7 +645,7 @@ describe('InviteMembersModal', () => {
await triggerMembersTokenSelect([user3]);
wrapper.vm.$toast = { show: jest.fn() };
- jest.spyOn(Api, 'inviteGroupMembersByEmail').mockResolvedValue({});
+ jest.spyOn(Api, 'inviteGroupMembers').mockResolvedValue({});
});
it('tracks the view for learn_gitlab source', () => {
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 9e17112fb15..8355ae67f20 100644
--- a/spec/frontend/invite_members/components/invite_modal_base_spec.js
+++ b/spec/frontend/invite_members/components/invite_modal_base_spec.js
@@ -49,8 +49,6 @@ describe('InviteModalBase', () => {
const findDatepicker = () => wrapper.findComponent(GlDatepicker);
const findLink = () => wrapper.findComponent(GlLink);
const findIntroText = () => wrapper.findByTestId('modal-base-intro-text').text();
- const findCancelButton = () => wrapper.findByTestId('cancel-button');
- const findInviteButton = () => wrapper.findByTestId('invite-button');
const findMembersFormGroup = () => wrapper.findByTestId('members-form-group');
describe('rendering the modal', () => {
@@ -67,15 +65,21 @@ describe('InviteModalBase', () => {
});
it('renders the Cancel button text correctly', () => {
- expect(findCancelButton().text()).toBe(CANCEL_BUTTON_TEXT);
- });
-
- it('renders the Invite button text correctly', () => {
- expect(findInviteButton().text()).toBe(INVITE_BUTTON_TEXT);
+ expect(wrapper.findComponent(GlModal).props('actionCancel')).toMatchObject({
+ text: CANCEL_BUTTON_TEXT,
+ });
});
- it('renders the Invite button modal without isLoading', () => {
- expect(findInviteButton().props('loading')).toBe(false);
+ it('renders the Invite button correctly', () => {
+ expect(wrapper.findComponent(GlModal).props('actionPrimary')).toMatchObject({
+ text: INVITE_BUTTON_TEXT,
+ attributes: {
+ variant: 'confirm',
+ disabled: false,
+ loading: false,
+ 'data-qa-selector': 'invite_button',
+ },
+ });
});
describe('rendering the access levels dropdown', () => {
@@ -114,7 +118,7 @@ describe('InviteModalBase', () => {
isLoading: true,
});
- expect(findInviteButton().props('loading')).toBe(true);
+ expect(wrapper.findComponent(GlModal).props('actionPrimary').attributes.loading).toBe(true);
});
it('with invalidFeedbackMessage, set members form group validation state', () => {
diff --git a/spec/frontend/invite_members/components/members_token_select_spec.js b/spec/frontend/invite_members/components/members_token_select_spec.js
index 196a716d08c..bf5564e4d63 100644
--- a/spec/frontend/invite_members/components/members_token_select_spec.js
+++ b/spec/frontend/invite_members/components/members_token_select_spec.js
@@ -95,7 +95,7 @@ describe('MembersTokenSelect', () => {
expect(UserApi.getUsers).toHaveBeenCalledWith(searchParam, {
active: true,
- exclude_internal: true,
+ without_project_bots: true,
});
expect(tokenSelector.props('hideDropdownWithNoItems')).toBe(false);
});
@@ -172,7 +172,7 @@ describe('MembersTokenSelect', () => {
expect(UserApi.getUsers).toHaveBeenCalledWith(searchParam, {
active: true,
- exclude_internal: true,
+ without_project_bots: true,
saml_provider_id: samlProviderId,
});
});
diff --git a/spec/frontend/invite_members/components/user_limit_notification_spec.js b/spec/frontend/invite_members/components/user_limit_notification_spec.js
new file mode 100644
index 00000000000..c779cf2ee3f
--- /dev/null
+++ b/spec/frontend/invite_members/components/user_limit_notification_spec.js
@@ -0,0 +1,71 @@
+import { GlAlert, GlSprintf } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import UserLimitNotification from '~/invite_members/components/user_limit_notification.vue';
+
+describe('UserLimitNotification', () => {
+ let wrapper;
+
+ const findAlert = () => wrapper.findComponent(GlAlert);
+
+ const createComponent = (providers = {}) => {
+ wrapper = shallowMountExtended(UserLimitNotification, {
+ provide: {
+ name: 'my group',
+ newTrialRegistrationPath: 'newTrialRegistrationPath',
+ purchasePath: 'purchasePath',
+ freeUsersLimit: 5,
+ membersCount: 1,
+ ...providers,
+ },
+ stubs: { GlSprintf },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('when limit is not reached', () => {
+ beforeEach(() => {
+ 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", () => {
+ const alert = findAlert();
+
+ expect(alert.attributes('title')).toEqual(
+ 'You only have space for 2 more members in my group',
+ );
+
+ expect(alert.text()).toEqual(
+ 'To get more members an owner of this namespace can start a trial or upgrade to a paid tier.',
+ );
+ });
+ });
+
+ describe('when limit is reached', () => {
+ beforeEach(() => {
+ createComponent({ membersCount: 5 });
+ });
+
+ it("renders user's limit notification", () => {
+ const alert = findAlert();
+
+ expect(alert.attributes('title')).toEqual("You've reached your 5 members limit for my group");
+
+ 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.',
+ );
+ });
+ });
+});
diff --git a/spec/frontend/invite_members/mock_data/api_responses.js b/spec/frontend/invite_members/mock_data/api_responses.js
index a3e426376d8..4ad3b6aeb66 100644
--- a/spec/frontend/invite_members/mock_data/api_responses.js
+++ b/spec/frontend/invite_members/mock_data/api_responses.js
@@ -1,12 +1,12 @@
-const INVITATIONS_API_EMAIL_INVALID = {
+const EMAIL_INVALID = {
message: { error: 'email contains an invalid email address' },
};
-const INVITATIONS_API_ERROR_EMAIL_INVALID = {
+const ERROR_EMAIL_INVALID = {
error: 'email contains an invalid email address',
};
-const INVITATIONS_API_EMAIL_RESTRICTED = {
+const EMAIL_RESTRICTED = {
message: {
'email@example.com':
"The member's email address is not allowed for this project. Go to the Admin area > Sign-up restrictions, and check Allowed domains for sign-ups.",
@@ -14,65 +14,31 @@ const INVITATIONS_API_EMAIL_RESTRICTED = {
status: 'error',
};
-const INVITATIONS_API_MULTIPLE_EMAIL_RESTRICTED = {
+const MULTIPLE_RESTRICTED = {
message: {
'email@example.com':
"The member's email address is not allowed for this project. Go to the Admin area > Sign-up restrictions, and check Allowed domains for sign-ups.",
'email4@example.com':
"The member's email address is not allowed for this project. Go to the Admin area > Sign-up restrictions, and check the Domain denylist.",
- },
- status: 'error',
-};
-
-const INVITATIONS_API_EMAIL_TAKEN = {
- message: {
- 'email@example.org': 'Invite email has already been taken',
- },
- status: 'error',
-};
-
-const MEMBERS_API_MEMBER_ALREADY_EXISTS = {
- message: 'Member already exists',
-};
-
-const MEMBERS_API_SINGLE_USER_RESTRICTED = {
- message: {
- user: [
+ root:
"The member's email address is not allowed for this project. Go to the Admin area > Sign-up restrictions, and check Allowed domains for sign-ups.",
- ],
},
+ status: 'error',
};
-const MEMBERS_API_SINGLE_USER_ACCESS_LEVEL = {
+const EMAIL_TAKEN = {
message: {
- access_level: [
- 'should be greater than or equal to Owner inherited membership from group Gitlab Org',
- ],
+ 'email@example.org': "The member's email address has already been taken",
},
-};
-
-const MEMBERS_API_MULTIPLE_USERS_RESTRICTED = {
- message:
- "root: The member's email address is not allowed for this project. Go to the Admin area > Sign-up restrictions, and check Allowed domains for sign-ups. and user18: The member's email address is not allowed for this project. Go to the Admin area > Sign-up restrictions, and check the Domain denylist. and john_doe31: The member's email address is not allowed for this project. Go to the Admin area > Sign-up restrictions, and check Email restrictions for sign-ups.",
status: 'error',
};
-export const apiPaths = {
- GROUPS_MEMBERS: '/api/v4/groups/1/members',
- GROUPS_INVITATIONS: '/api/v4/groups/1/invitations',
-};
-
-export const membersApiResponse = {
- MEMBER_ALREADY_EXISTS: MEMBERS_API_MEMBER_ALREADY_EXISTS,
- SINGLE_USER_ACCESS_LEVEL: MEMBERS_API_SINGLE_USER_ACCESS_LEVEL,
- SINGLE_USER_RESTRICTED: MEMBERS_API_SINGLE_USER_RESTRICTED,
- MULTIPLE_USERS_RESTRICTED: MEMBERS_API_MULTIPLE_USERS_RESTRICTED,
-};
+export const GROUPS_INVITATIONS_PATH = '/api/v4/groups/1/invitations';
export const invitationsApiResponse = {
- EMAIL_INVALID: INVITATIONS_API_EMAIL_INVALID,
- ERROR_EMAIL_INVALID: INVITATIONS_API_ERROR_EMAIL_INVALID,
- EMAIL_RESTRICTED: INVITATIONS_API_EMAIL_RESTRICTED,
- MULTIPLE_EMAIL_RESTRICTED: INVITATIONS_API_MULTIPLE_EMAIL_RESTRICTED,
- EMAIL_TAKEN: INVITATIONS_API_EMAIL_TAKEN,
+ EMAIL_INVALID,
+ ERROR_EMAIL_INVALID,
+ EMAIL_RESTRICTED,
+ MULTIPLE_RESTRICTED,
+ EMAIL_TAKEN,
};
diff --git a/spec/frontend/invite_members/mock_data/group_modal.js b/spec/frontend/invite_members/mock_data/group_modal.js
index c05c4edb7d0..c8588683885 100644
--- a/spec/frontend/invite_members/mock_data/group_modal.js
+++ b/spec/frontend/invite_members/mock_data/group_modal.js
@@ -1,5 +1,6 @@
export const propsData = {
id: '1',
+ rootId: '1',
name: 'test name',
isProject: false,
invalidGroups: [],
diff --git a/spec/frontend/invite_members/mock_data/member_modal.js b/spec/frontend/invite_members/mock_data/member_modal.js
index 590502909b2..1b0cc57fb5b 100644
--- a/spec/frontend/invite_members/mock_data/member_modal.js
+++ b/spec/frontend/invite_members/mock_data/member_modal.js
@@ -1,5 +1,6 @@
export const propsData = {
id: '1',
+ rootId: '1',
name: 'test name',
isProject: false,
accessLevels: { Guest: 10, Reporter: 20, Developer: 30, Maintainer: 40, Owner: 50 },
diff --git a/spec/frontend/invite_members/utils/response_message_parser_spec.js b/spec/frontend/invite_members/utils/response_message_parser_spec.js
index e2cc87c8547..8b2064df374 100644
--- a/spec/frontend/invite_members/utils/response_message_parser_spec.js
+++ b/spec/frontend/invite_members/utils/response_message_parser_spec.js
@@ -2,23 +2,19 @@ import {
responseMessageFromSuccess,
responseMessageFromError,
} from '~/invite_members/utils/response_message_parser';
-import { membersApiResponse, invitationsApiResponse } from '../mock_data/api_responses';
+import { invitationsApiResponse } from '../mock_data/api_responses';
describe('Response message parser', () => {
const expectedMessage = 'expected display and message.';
describe('parse message from successful response', () => {
const exampleKeyedMsg = { 'email@example.com': expectedMessage };
- const exampleFirstPartMultiple = 'username1: expected display and message.';
- const exampleUserMsgMultiple =
- ' and username2: id not found and restricted email. and username3: email is restricted.';
it.each([
- [[{ data: { message: expectedMessage } }]],
- [[{ data: { message: exampleFirstPartMultiple + exampleUserMsgMultiple } }]],
- [[{ data: { error: expectedMessage } }]],
- [[{ data: { message: [expectedMessage] } }]],
- [[{ data: { message: exampleKeyedMsg } }]],
+ [{ data: { message: expectedMessage } }],
+ [{ data: { error: expectedMessage } }],
+ [{ data: { message: [expectedMessage] } }],
+ [{ data: { message: exampleKeyedMsg } }],
])(`returns "${expectedMessage}" from success response: %j`, (successResponse) => {
expect(responseMessageFromSuccess(successResponse)).toBe(expectedMessage);
});
@@ -27,8 +23,6 @@ describe('Response message parser', () => {
describe('message from error response', () => {
it.each([
[{ response: { data: { error: expectedMessage } } }],
- [{ response: { data: { message: { user: [expectedMessage] } } } }],
- [{ response: { data: { message: { access_level: [expectedMessage] } } } }],
[{ response: { data: { message: { error: expectedMessage } } } }],
[{ response: { data: { message: expectedMessage } } }],
])(`returns "${expectedMessage}" from error response: %j`, (errorResponse) => {
@@ -41,18 +35,10 @@ describe('Response message parser', () => {
"The member's email address is not allowed for this project. Go to the Admin area > Sign-up restrictions, and check Allowed domains for sign-ups.";
it.each([
- [[{ data: membersApiResponse.MULTIPLE_USERS_RESTRICTED }]],
- [[{ data: invitationsApiResponse.MULTIPLE_EMAIL_RESTRICTED }]],
- [[{ data: invitationsApiResponse.EMAIL_RESTRICTED }]],
+ [{ data: invitationsApiResponse.MULTIPLE_RESTRICTED }],
+ [{ data: invitationsApiResponse.EMAIL_RESTRICTED }],
])(`returns "${expectedMessage}" from success response: %j`, (restrictedResponse) => {
expect(responseMessageFromSuccess(restrictedResponse)).toBe(expected);
});
-
- it.each([[{ response: { data: membersApiResponse.SINGLE_USER_RESTRICTED } }]])(
- `returns "${expectedMessage}" from error response: %j`,
- (singleRestrictedResponse) => {
- expect(responseMessageFromError(singleRestrictedResponse)).toBe(expected);
- },
- );
});
});
diff --git a/spec/frontend/issuable/issuable_form_spec.js b/spec/frontend/issuable/issuable_form_spec.js
index 321c61ead1e..99ed18cf5bd 100644
--- a/spec/frontend/issuable/issuable_form_spec.js
+++ b/spec/frontend/issuable/issuable_form_spec.js
@@ -1,20 +1,46 @@
import $ from 'jquery';
import IssuableForm from '~/issuable/issuable_form';
-
-function createIssuable() {
- const instance = new IssuableForm($(document.createElement('form')));
-
- instance.titleField = $(document.createElement('input'));
-
- return instance;
-}
+import setWindowLocation from 'helpers/set_window_location_helper';
describe('IssuableForm', () => {
let instance;
+ const createIssuable = (form) => {
+ instance = new IssuableForm(form);
+ };
+
beforeEach(() => {
- instance = createIssuable();
+ setFixtures(`
+ <form>
+ <input name="[title]" />
+ </form>
+ `);
+ createIssuable($('form'));
+ });
+
+ describe('initAutosave', () => {
+ it('creates autosave with the searchTerm included', () => {
+ setWindowLocation('https://gitlab.test/foo?bar=true');
+ const autosave = instance.initAutosave();
+
+ expect(autosave.key.includes('bar=true')).toBe(true);
+ });
+
+ it("creates autosave fields without the searchTerm if it's an issue new form", () => {
+ setFixtures(`
+ <form data-new-issue-path="/issues/new">
+ <input name="[title]" />
+ </form>
+ `);
+ createIssuable($('form'));
+
+ setWindowLocation('https://gitlab.test/issues/new?bar=true');
+
+ const autosave = instance.initAutosave();
+
+ expect(autosave.key.includes('bar=true')).toBe(false);
+ });
});
describe('removeWip', () => {
diff --git a/spec/frontend/issues/create_merge_request_dropdown_spec.js b/spec/frontend/issues/create_merge_request_dropdown_spec.js
index c2cfb16fdf7..20b26f5abba 100644
--- a/spec/frontend/issues/create_merge_request_dropdown_spec.js
+++ b/spec/frontend/issues/create_merge_request_dropdown_spec.js
@@ -15,7 +15,7 @@ describe('CreateMergeRequestDropdown', () => {
<div id="dummy-wrapper-element">
<div class="available"></div>
<div class="unavailable">
- <div class="gl-spinner"></div>
+ <div class="js-create-mr-spinner"></div>
<div class="text"></div>
</div>
<div class="js-ref"></div>
@@ -38,21 +38,16 @@ describe('CreateMergeRequestDropdown', () => {
});
describe('getRef', () => {
- it('escapes branch names correctly', (done) => {
+ it('escapes branch names correctly', async () => {
const endpoint = `${dropdown.refsPath}contains%23hash`;
jest.spyOn(axios, 'get');
axiosMock.onGet(endpoint).replyOnce({});
- dropdown
- .getRef('contains#hash')
- .then(() => {
- expect(axios.get).toHaveBeenCalledWith(
- endpoint,
- expect.objectContaining({ cancelToken: expect.anything() }),
- );
- })
- .then(done)
- .catch(done.fail);
+ await dropdown.getRef('contains#hash');
+ expect(axios.get).toHaveBeenCalledWith(
+ endpoint,
+ expect.objectContaining({ cancelToken: expect.anything() }),
+ );
});
});
diff --git a/spec/frontend/issues/list/components/issue_card_time_info_spec.js b/spec/frontend/issues/list/components/issue_card_time_info_spec.js
index e9c48b60da4..c3f13ca6f9a 100644
--- a/spec/frontend/issues/list/components/issue_card_time_info_spec.js
+++ b/spec/frontend/issues/list/components/issue_card_time_info_spec.js
@@ -1,10 +1,11 @@
import { GlIcon, GlLink } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { useFakeDate } from 'helpers/fake_date';
+import { IssuableStatus } from '~/issues/constants';
import IssueCardTimeInfo from '~/issues/list/components/issue_card_time_info.vue';
describe('CE IssueCardTimeInfo component', () => {
- useFakeDate(2020, 11, 11);
+ useFakeDate(2020, 11, 11); // 2020 Dec 11
let wrapper;
@@ -24,7 +25,7 @@ describe('CE IssueCardTimeInfo component', () => {
const findDueDate = () => wrapper.find('[data-testid="issuable-due-date"]');
const mountComponent = ({
- closedAt = null,
+ state = IssuableStatus.Open,
dueDate = issue.dueDate,
milestoneDueDate = issue.milestone.dueDate,
milestoneStartDate = issue.milestone.startDate,
@@ -38,7 +39,7 @@ describe('CE IssueCardTimeInfo component', () => {
dueDate: milestoneDueDate,
startDate: milestoneStartDate,
},
- closedAt,
+ state,
dueDate,
},
},
@@ -91,7 +92,7 @@ describe('CE IssueCardTimeInfo component', () => {
describe('when in the past', () => {
describe('when issue is open', () => {
it('renders in red', () => {
- wrapper = mountComponent({ dueDate: new Date('2020-10-10') });
+ wrapper = mountComponent({ dueDate: '2020-10-10' });
expect(findDueDate().classes()).toContain('gl-text-red-500');
});
@@ -100,8 +101,8 @@ describe('CE IssueCardTimeInfo component', () => {
describe('when issue is closed', () => {
it('does not render in red', () => {
wrapper = mountComponent({
- dueDate: new Date('2020-10-10'),
- closedAt: '2020-09-05T13:06:25Z',
+ dueDate: '2020-10-10',
+ state: IssuableStatus.Closed,
});
expect(findDueDate().classes()).not.toContain('gl-text-red-500');
diff --git a/spec/frontend/issues/list/components/issues_list_app_spec.js b/spec/frontend/issues/list/components/issues_list_app_spec.js
index 33c7ccac180..5a9bd1ff8e4 100644
--- a/spec/frontend/issues/list/components/issues_list_app_spec.js
+++ b/spec/frontend/issues/list/components/issues_list_app_spec.js
@@ -452,13 +452,26 @@ describe('CE IssuesListApp component', () => {
});
describe('IssuableByEmail component', () => {
- describe.each([true, false])(`when issue creation by email is enabled=%s`, (enabled) => {
- it(`${enabled ? 'renders' : 'does not render'}`, () => {
- wrapper = mountComponent({ provide: { initialEmail: enabled } });
-
- expect(findIssuableByEmail().exists()).toBe(enabled);
- });
- });
+ describe.each`
+ initialEmail | hasAnyIssues | isSignedIn | exists
+ ${false} | ${false} | ${false} | ${false}
+ ${false} | ${true} | ${false} | ${false}
+ ${false} | ${false} | ${true} | ${false}
+ ${false} | ${true} | ${true} | ${false}
+ ${true} | ${false} | ${false} | ${false}
+ ${true} | ${true} | ${false} | ${false}
+ ${true} | ${false} | ${true} | ${true}
+ ${true} | ${true} | ${true} | ${true}
+ `(
+ `when issue creation by email is enabled=$initialEmail`,
+ ({ initialEmail, hasAnyIssues, isSignedIn, exists }) => {
+ it(`${initialEmail ? 'renders' : 'does not render'}`, () => {
+ wrapper = mountComponent({ provide: { initialEmail, hasAnyIssues, isSignedIn } });
+
+ expect(findIssuableByEmail().exists()).toBe(exists);
+ });
+ },
+ );
});
describe('empty states', () => {
diff --git a/spec/frontend/issues/list/mock_data.js b/spec/frontend/issues/list/mock_data.js
index c883b20682e..b1a135ceb18 100644
--- a/spec/frontend/issues/list/mock_data.js
+++ b/spec/frontend/issues/list/mock_data.js
@@ -21,7 +21,6 @@ export const getIssuesQueryResponse = {
__typename: 'Issue',
id: 'gid://gitlab/Issue/123456',
iid: '789',
- closedAt: null,
confidential: false,
createdAt: '2021-05-22T04:08:01Z',
downvotes: 2,
@@ -30,6 +29,7 @@ export const getIssuesQueryResponse = {
humanTimeEstimate: null,
mergeRequestsCount: false,
moved: false,
+ state: 'opened',
title: 'Issue title',
updatedAt: '2021-05-22T04:08:01Z',
upvotes: 3,
diff --git a/spec/frontend/issues/related_merge_requests/store/actions_spec.js b/spec/frontend/issues/related_merge_requests/store/actions_spec.js
index 5f232fee09b..4327fac15d4 100644
--- a/spec/frontend/issues/related_merge_requests/store/actions_spec.js
+++ b/spec/frontend/issues/related_merge_requests/store/actions_spec.js
@@ -23,90 +23,82 @@ describe('RelatedMergeRequest store actions', () => {
});
describe('setInitialState', () => {
- it('commits types.SET_INITIAL_STATE with given props', (done) => {
+ it('commits types.SET_INITIAL_STATE with given props', () => {
const props = { a: 1, b: 2 };
- testAction(
+ return testAction(
actions.setInitialState,
props,
{},
[{ type: types.SET_INITIAL_STATE, payload: props }],
[],
- done,
);
});
});
describe('requestData', () => {
- it('commits types.REQUEST_DATA', (done) => {
- testAction(actions.requestData, null, {}, [{ type: types.REQUEST_DATA }], [], done);
+ it('commits types.REQUEST_DATA', () => {
+ return testAction(actions.requestData, null, {}, [{ type: types.REQUEST_DATA }], []);
});
});
describe('receiveDataSuccess', () => {
- it('commits types.RECEIVE_DATA_SUCCESS with data', (done) => {
+ it('commits types.RECEIVE_DATA_SUCCESS with data', () => {
const data = { a: 1, b: 2 };
- testAction(
+ return testAction(
actions.receiveDataSuccess,
data,
{},
[{ type: types.RECEIVE_DATA_SUCCESS, payload: data }],
[],
- done,
);
});
});
describe('receiveDataError', () => {
- it('commits types.RECEIVE_DATA_ERROR', (done) => {
- testAction(
+ it('commits types.RECEIVE_DATA_ERROR', () => {
+ return testAction(
actions.receiveDataError,
null,
{},
[{ type: types.RECEIVE_DATA_ERROR }],
[],
- done,
);
});
});
describe('fetchMergeRequests', () => {
describe('for a successful request', () => {
- it('should dispatch success action', (done) => {
+ it('should dispatch success action', () => {
const data = { a: 1 };
mock.onGet(`${state.apiEndpoint}?per_page=100`).replyOnce(200, data, { 'x-total': 2 });
- testAction(
+ return testAction(
actions.fetchMergeRequests,
null,
state,
[],
[{ type: 'requestData' }, { type: 'receiveDataSuccess', payload: { data, total: 2 } }],
- done,
);
});
});
describe('for a failing request', () => {
- it('should dispatch error action', (done) => {
+ it('should dispatch error action', async () => {
mock.onGet(`${state.apiEndpoint}?per_page=100`).replyOnce(400);
- testAction(
+ await testAction(
actions.fetchMergeRequests,
null,
state,
[],
[{ type: 'requestData' }, { type: 'receiveDataError' }],
- () => {
- expect(createFlash).toHaveBeenCalledTimes(1);
- expect(createFlash).toHaveBeenCalledWith({
- message: expect.stringMatching('Something went wrong'),
- });
-
- done();
- },
);
+ expect(createFlash).toHaveBeenCalledTimes(1);
+ expect(createFlash).toHaveBeenCalledWith({
+ message: expect.stringMatching('Something went wrong'),
+ });
});
});
});
diff --git a/spec/frontend/issues/show/components/app_spec.js b/spec/frontend/issues/show/components/app_spec.js
index ac2717a5028..5ab64d8e9ca 100644
--- a/spec/frontend/issues/show/components/app_spec.js
+++ b/spec/frontend/issues/show/components/app_spec.js
@@ -2,11 +2,14 @@ import { GlIntersectionObserver } from '@gitlab/ui';
import MockAdapter from 'axios-mock-adapter';
import { nextTick } from 'vue';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
-import { mountExtended } from 'helpers/vue_test_utils_helper';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import '~/behaviors/markdown/render_gfm';
import { IssuableStatus, IssuableStatusText } 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';
+import FormComponent from '~/issues/show/components/form.vue';
+import TitleComponent from '~/issues/show/components/title.vue';
import IncidentTabs from '~/issues/show/components/incidents/incident_tabs.vue';
import PinnedLinks from '~/issues/show/components/pinned_links.vue';
import { POLLING_DELAY } from '~/issues/show/constants';
@@ -21,10 +24,6 @@ import {
zoomMeetingUrl,
} from '../mock_data/mock_data';
-function formatText(text) {
- return text.trim().replace(/\s\s+/g, ' ');
-}
-
jest.mock('~/lib/utils/url_utility');
jest.mock('~/issues/show/event_hub');
@@ -39,10 +38,15 @@ describe('Issuable output', () => {
const findLockedBadge = () => wrapper.findByTestId('locked');
const findConfidentialBadge = () => wrapper.findByTestId('confidential');
const findHiddenBadge = () => wrapper.findByTestId('hidden');
- const findAlert = () => wrapper.find('.alert');
+
+ const findTitle = () => wrapper.findComponent(TitleComponent);
+ const findDescription = () => wrapper.findComponent(DescriptionComponent);
+ const findEdited = () => wrapper.findComponent(EditedComponent);
+ const findForm = () => wrapper.findComponent(FormComponent);
+ const findPinnedLinks = () => wrapper.findComponent(PinnedLinks);
const mountComponent = (props = {}, options = {}, data = {}) => {
- wrapper = mountExtended(IssuableApp, {
+ wrapper = shallowMountExtended(IssuableApp, {
directives: {
GlTooltip: createMockDirective(),
},
@@ -104,23 +108,15 @@ describe('Issuable output', () => {
});
it('should render a title/description/edited and update title/description/edited on update', () => {
- let editedText;
return axios
.waitForAll()
.then(() => {
- editedText = wrapper.find('.edited-text');
- })
- .then(() => {
- expect(document.querySelector('title').innerText).toContain('this is a title (#1)');
- expect(wrapper.find('.title').text()).toContain('this is a title');
- expect(wrapper.find('.md').text()).toContain('this is a description!');
- expect(wrapper.find('.js-task-list-field').element.value).toContain(
- 'this is a description',
- );
+ expect(findTitle().props('titleText')).toContain('this is a title');
+ expect(findDescription().props('descriptionText')).toContain('this is a description');
- expect(formatText(editedText.text())).toMatch(/Edited[\s\S]+?by Some User/);
- expect(editedText.find('.author-link').attributes('href')).toMatch(/\/some_user$/);
- expect(editedText.find('time').text()).toBeTruthy();
+ expect(findEdited().exists()).toBe(true);
+ expect(findEdited().props('updatedByPath')).toMatch(/\/some_user$/);
+ expect(findEdited().props('updatedAt')).toBeTruthy();
expect(wrapper.vm.state.lock_version).toBe(initialRequest.lock_version);
})
.then(() => {
@@ -128,20 +124,13 @@ describe('Issuable output', () => {
return axios.waitForAll();
})
.then(() => {
- expect(document.querySelector('title').innerText).toContain('2 (#1)');
- expect(wrapper.find('.title').text()).toContain('2');
- expect(wrapper.find('.md').text()).toContain('42');
- expect(wrapper.find('.js-task-list-field').element.value).toContain('42');
- expect(wrapper.find('.edited-text').text()).toBeTruthy();
- expect(formatText(wrapper.find('.edited-text').text())).toMatch(
- /Edited[\s\S]+?by Other User/,
- );
+ expect(findTitle().props('titleText')).toContain('2');
+ expect(findDescription().props('descriptionText')).toContain('42');
- expect(editedText.find('.author-link').attributes('href')).toMatch(/\/other_user$/);
- expect(editedText.find('time').text()).toBeTruthy();
- // As the lock_version value does not differ from the server,
- // we should not see an alert
- expect(findAlert().exists()).toBe(false);
+ expect(findEdited().exists()).toBe(true);
+ expect(findEdited().props('updatedByName')).toBe('Other User');
+ expect(findEdited().props('updatedByPath')).toMatch(/\/other_user$/);
+ expect(findEdited().props('updatedAt')).toBeTruthy();
});
});
@@ -149,7 +138,7 @@ describe('Issuable output', () => {
wrapper.vm.showForm = true;
await nextTick();
- expect(wrapper.find('.markdown-selector').exists()).toBe(true);
+ expect(findForm().exists()).toBe(true);
});
it('does not show actions if permissions are incorrect', async () => {
@@ -157,7 +146,7 @@ describe('Issuable output', () => {
wrapper.setProps({ canUpdate: false });
await nextTick();
- expect(wrapper.find('.markdown-selector').exists()).toBe(false);
+ expect(findForm().exists()).toBe(false);
});
it('does not update formState if form is already open', async () => {
@@ -177,8 +166,7 @@ describe('Issuable output', () => {
${'zoomMeetingUrl'} | ${zoomMeetingUrl}
${'publishedIncidentUrl'} | ${publishedIncidentUrl}
`('sets the $prop correctly on underlying pinned links', ({ prop, value }) => {
- expect(wrapper.vm[prop]).toBe(value);
- expect(wrapper.find(`[data-testid="${prop}"]`).attributes('href')).toBe(value);
+ expect(findPinnedLinks().props(prop)).toBe(value);
});
});
@@ -327,7 +315,6 @@ describe('Issuable output', () => {
expect(wrapper.vm.formState.lockedWarningVisible).toBe(true);
expect(wrapper.vm.formState.lock_version).toBe(1);
- expect(findAlert().exists()).toBe(true);
});
});
@@ -374,15 +361,22 @@ describe('Issuable output', () => {
});
describe('show inline edit button', () => {
- it('should not render by default', () => {
- expect(wrapper.find('.btn-edit').exists()).toBe(true);
+ it('should render by default', () => {
+ expect(findTitle().props('showInlineEditButton')).toBe(true);
});
it('should render if showInlineEditButton', async () => {
wrapper.setProps({ showInlineEditButton: true });
await nextTick();
- expect(wrapper.find('.btn-edit').exists()).toBe(true);
+ expect(findTitle().props('showInlineEditButton')).toBe(true);
+ });
+
+ it('should not render if showInlineEditButton is false', async () => {
+ wrapper.setProps({ showInlineEditButton: false });
+
+ await nextTick();
+ expect(findTitle().props('showInlineEditButton')).toBe(false);
});
});
@@ -533,13 +527,11 @@ describe('Issuable output', () => {
describe('Composable description component', () => {
const findIncidentTabs = () => wrapper.findComponent(IncidentTabs);
- const findDescriptionComponent = () => wrapper.findComponent(DescriptionComponent);
- const findPinnedLinks = () => wrapper.findComponent(PinnedLinks);
const borderClass = 'gl-border-b-1 gl-border-b-gray-100 gl-border-b-solid gl-mb-6';
describe('when using description component', () => {
it('renders the description component', () => {
- expect(findDescriptionComponent().exists()).toBe(true);
+ expect(findDescription().exists()).toBe(true);
});
it('does not render incident tabs', () => {
@@ -572,8 +564,8 @@ describe('Issuable output', () => {
);
});
- it('renders the description component', () => {
- expect(findDescriptionComponent().exists()).toBe(true);
+ it('does not the description component', () => {
+ expect(findDescription().exists()).toBe(false);
});
it('renders incident tabs', () => {
diff --git a/spec/frontend/issues/show/components/description_spec.js b/spec/frontend/issues/show/components/description_spec.js
index 08f8996de6f..0b3daadae1d 100644
--- a/spec/frontend/issues/show/components/description_spec.js
+++ b/spec/frontend/issues/show/components/description_spec.js
@@ -1,26 +1,35 @@
import $ from 'jquery';
import { nextTick } from 'vue';
import '~/behaviors/markdown/render_gfm';
-import { GlPopover, GlModal } from '@gitlab/ui';
+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 { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import createFlash from '~/flash';
import Description from '~/issues/show/components/description.vue';
+import { updateHistory } from '~/lib/utils/url_utility';
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';
import {
descriptionProps as initialProps,
descriptionHtmlWithCheckboxes,
+ descriptionHtmlWithTask,
} from '../mock_data/mock_data';
jest.mock('~/flash');
+jest.mock('~/lib/utils/url_utility', () => ({
+ ...jest.requireActual('~/lib/utils/url_utility'),
+ updateHistory: jest.fn(),
+}));
jest.mock('~/task_list');
const showModal = jest.fn();
const hideModal = jest.fn();
+const $toast = {
+ show: jest.fn(),
+};
describe('Description component', () => {
let wrapper;
@@ -28,10 +37,9 @@ describe('Description component', () => {
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('[data-testid="convert-to-task"]');
- const findTaskSvg = () => wrapper.find('[data-testid="issue-open-m-icon"]');
+ const findConvertToTaskButton = () => wrapper.find('.js-add-task');
- const findPopovers = () => wrapper.findAllComponents(GlPopover);
+ const findTooltips = () => wrapper.findAllComponents(GlTooltip);
const findModal = () => wrapper.findComponent(GlModal);
const findCreateWorkItem = () => wrapper.findComponent(CreateWorkItem);
const findWorkItemDetailModal = () => wrapper.findComponent(WorkItemDetailModal);
@@ -39,10 +47,14 @@ describe('Description component', () => {
function createComponent({ props = {}, provide = {} } = {}) {
wrapper = shallowMountExtended(Description, {
propsData: {
+ issueId: 1,
...initialProps,
...props,
},
provide,
+ mocks: {
+ $toast,
+ },
stubs: {
GlModal: stubComponent(GlModal, {
methods: {
@@ -50,12 +62,13 @@ describe('Description component', () => {
hide: hideModal,
},
}),
- GlPopover,
},
});
}
beforeEach(() => {
+ setWindowLocation(TEST_HOST);
+
if (!document.querySelector('.issuable-meta')) {
const metaData = document.createElement('div');
metaData.classList.add('issuable-meta');
@@ -253,9 +266,9 @@ describe('Description component', () => {
expect(findTaskActionButtons()).toHaveLength(3);
});
- it('renders a list of popovers corresponding to checkboxes in description HTML', () => {
- expect(findPopovers()).toHaveLength(3);
- expect(findPopovers().at(0).props('target')).toBe(
+ it('renders a list of tooltips corresponding to checkboxes in description HTML', () => {
+ expect(findTooltips()).toHaveLength(3);
+ expect(findTooltips().at(0).props('target')).toBe(
findTaskActionButtons().at(0).attributes('id'),
);
});
@@ -264,92 +277,113 @@ describe('Description component', () => {
expect(findModal().props('visible')).toBe(false);
});
- it('opens a modal when a button on popover is clicked and displays correct title', async () => {
- findConvertToTaskButton().vm.$emit('click');
- expect(showModal).toHaveBeenCalled();
- await nextTick();
+ it('opens a modal when a button is clicked and displays correct title', async () => {
+ await findConvertToTaskButton().trigger('click');
expect(findCreateWorkItem().props('initialTitle').trim()).toBe('todo 1');
});
- it('closes the modal on `closeCreateTaskModal` event', () => {
- findConvertToTaskButton().vm.$emit('click');
+ it('closes the modal on `closeCreateTaskModal` event', async () => {
+ await findConvertToTaskButton().trigger('click');
findCreateWorkItem().vm.$emit('closeModal');
expect(hideModal).toHaveBeenCalled();
});
- it('updates description HTML on `onCreate` event', async () => {
- const newTitle = 'New title';
- findConvertToTaskButton().vm.$emit('click');
- findCreateWorkItem().vm.$emit('onCreate', { title: newTitle });
+ it('emits `updateDescription` on `onCreate` event', () => {
+ const newDescription = `<p>New description</p>`;
+ findCreateWorkItem().vm.$emit('onCreate', newDescription);
expect(hideModal).toHaveBeenCalled();
- await nextTick();
+ expect(wrapper.emitted('updateDescription')).toEqual([[newDescription]]);
+ });
+
+ it('shows toast after delete success', async () => {
+ findWorkItemDetailModal().vm.$emit('workItemDeleted');
- expect(findTaskSvg().exists()).toBe(true);
- expect(wrapper.text()).toContain(newTitle);
+ expect($toast.show).toHaveBeenCalledWith('Work item deleted');
});
});
describe('work items detail', () => {
- const id = '1';
- const title = 'my first task';
- const type = 'task';
+ const findTaskLink = () => wrapper.find('a.gfm-issue');
- const createThenClickOnTask = () => {
- findConvertToTaskButton().vm.$emit('click');
- findCreateWorkItem().vm.$emit('onCreate', { id, title, type });
- return wrapper.findByRole('button', { name: title }).trigger('click');
- };
-
- beforeEach(() => {
- createComponent({
- props: {
- descriptionHtml: descriptionHtmlWithCheckboxes,
- },
- provide: {
- glFeatures: { workItems: true },
- },
+ describe('when opening and closing', () => {
+ beforeEach(() => {
+ createComponent({
+ props: {
+ descriptionHtml: descriptionHtmlWithTask,
+ },
+ provide: {
+ glFeatures: { workItems: true },
+ },
+ });
+ return nextTick();
});
- return nextTick();
- });
- it('opens when task button is clicked', async () => {
- expect(findWorkItemDetailModal().props('visible')).toBe(false);
+ it('opens when task button is clicked', async () => {
+ expect(findWorkItemDetailModal().props('visible')).toBe(false);
- await createThenClickOnTask();
+ await findTaskLink().trigger('click');
- expect(findWorkItemDetailModal().props('visible')).toBe(true);
- });
+ expect(findWorkItemDetailModal().props('visible')).toBe(true);
+ expect(updateHistory).toHaveBeenCalledWith({
+ url: `${TEST_HOST}/?work_item_id=2`,
+ replace: true,
+ });
+ });
- it('closes from an open state', async () => {
- await createThenClickOnTask();
+ it('closes from an open state', async () => {
+ await findTaskLink().trigger('click');
- expect(findWorkItemDetailModal().props('visible')).toBe(true);
+ expect(findWorkItemDetailModal().props('visible')).toBe(true);
- findWorkItemDetailModal().vm.$emit('close');
- await nextTick();
+ findWorkItemDetailModal().vm.$emit('close');
+ await nextTick();
- expect(findWorkItemDetailModal().props('visible')).toBe(false);
- });
+ expect(findWorkItemDetailModal().props('visible')).toBe(false);
+ expect(updateHistory).toHaveBeenLastCalledWith({
+ url: `${TEST_HOST}/`,
+ replace: true,
+ });
+ });
- it('shows error on error', async () => {
- const message = 'I am error';
+ it('tracks when opened', async () => {
+ const trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
- await createThenClickOnTask();
- findWorkItemDetailModal().vm.$emit('error', message);
+ await findTaskLink().trigger('click');
- expect(createFlash).toHaveBeenCalledWith({ message });
+ expect(trackingSpy).toHaveBeenCalledWith(
+ 'workItems:show',
+ 'viewed_work_item_from_modal',
+ {
+ category: 'workItems:show',
+ label: 'work_item_view',
+ property: 'type_task',
+ },
+ );
+ });
});
- it('tracks when opened', async () => {
- const trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
-
- await createThenClickOnTask();
-
- expect(trackingSpy).toHaveBeenCalledWith('workItems:show', 'viewed_work_item_from_modal', {
- category: 'workItems:show',
- label: 'work_item_view',
- property: 'type_task',
- });
+ 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 when url contains `work_item_id=$workItemId`',
+ async ({ workItemId, visible }) => {
+ setWindowLocation(`?work_item_id=${workItemId}`);
+
+ createComponent({
+ props: { descriptionHtml: descriptionHtmlWithTask },
+ provide: { glFeatures: { workItems: true } },
+ });
+
+ expect(findWorkItemDetailModal().props('visible')).toBe(visible);
+ },
+ );
});
});
});
diff --git a/spec/frontend/issues/show/components/fields/description_spec.js b/spec/frontend/issues/show/components/fields/description_spec.js
index dd511c3945c..0dcd70ac19b 100644
--- a/spec/frontend/issues/show/components/fields/description_spec.js
+++ b/spec/frontend/issues/show/components/fields/description_spec.js
@@ -14,9 +14,8 @@ describe('Description field component', () => {
propsData: {
markdownPreviewPath: '/',
markdownDocsPath: '/',
- formState: {
- description,
- },
+ quickActionsDocsPath: '/',
+ value: description,
},
stubs: {
MarkdownField,
diff --git a/spec/frontend/issues/show/components/fields/description_template_spec.js b/spec/frontend/issues/show/components/fields/description_template_spec.js
index abe2805e5b2..79a3bfa9840 100644
--- a/spec/frontend/issues/show/components/fields/description_template_spec.js
+++ b/spec/frontend/issues/show/components/fields/description_template_spec.js
@@ -1,74 +1,65 @@
-import Vue from 'vue';
+import { shallowMount } from '@vue/test-utils';
import descriptionTemplate from '~/issues/show/components/fields/description_template.vue';
describe('Issue description template component with templates as hash', () => {
- let vm;
- let formState;
+ let wrapper;
+ const defaultOptions = {
+ propsData: {
+ value: 'test',
+ issuableTemplates: {
+ test: [{ name: 'test', id: 'test', project_path: '/', namespace_path: '/' }],
+ },
+ projectId: 1,
+ projectPath: '/',
+ namespacePath: '/',
+ projectNamespace: '/',
+ },
+ };
- beforeEach(() => {
- const Component = Vue.extend(descriptionTemplate);
- formState = {
- description: 'test',
- };
+ const findIssuableSelector = () => wrapper.find('.js-issuable-selector');
- vm = new Component({
- propsData: {
- formState,
- issuableTemplates: {
- test: [{ name: 'test', id: 'test', project_path: '/', namespace_path: '/' }],
- },
- projectId: 1,
- projectPath: '/',
- namespacePath: '/',
- projectNamespace: '/',
- },
- }).$mount();
+ const createComponent = (options = defaultOptions) => {
+ wrapper = shallowMount(descriptionTemplate, options);
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
});
it('renders templates as JSON hash in data attribute', () => {
- expect(vm.$el.querySelector('.js-issuable-selector').getAttribute('data-data')).toBe(
+ createComponent();
+ expect(findIssuableSelector().attributes('data-data')).toBe(
'{"test":[{"name":"test","id":"test","project_path":"/","namespace_path":"/"}]}',
);
});
- it('updates formState when changing template', () => {
- vm.issuableTemplate.editor.setValue('test new template');
+ it('emits input event', () => {
+ createComponent();
+ wrapper.vm.issuableTemplate.editor.setValue('test new template');
- expect(formState.description).toBe('test new template');
+ expect(wrapper.emitted('input')).toEqual([['test new template']]);
});
- it('returns formState description with editor getValue', () => {
- formState.description = 'testing new template';
-
- expect(vm.issuableTemplate.editor.getValue()).toBe('testing new template');
+ it('returns value with editor getValue', () => {
+ createComponent();
+ expect(wrapper.vm.issuableTemplate.editor.getValue()).toBe('test');
});
-});
-
-describe('Issue description template component with templates as array', () => {
- let vm;
- let formState;
- beforeEach(() => {
- const Component = Vue.extend(descriptionTemplate);
- formState = {
- description: 'test',
- };
-
- vm = new Component({
- propsData: {
- formState,
- issuableTemplates: [{ name: 'test', id: 'test', project_path: '/', namespace_path: '/' }],
- projectId: 1,
- projectPath: '/',
- namespacePath: '/',
- projectNamespace: '/',
- },
- }).$mount();
- });
-
- it('renders templates as JSON array in data attribute', () => {
- expect(vm.$el.querySelector('.js-issuable-selector').getAttribute('data-data')).toBe(
- '[{"name":"test","id":"test","project_path":"/","namespace_path":"/"}]',
- );
+ describe('Issue description template component with templates as array', () => {
+ it('renders templates as JSON array in data attribute', () => {
+ createComponent({
+ propsData: {
+ value: 'test',
+ issuableTemplates: [{ name: 'test', id: 'test', project_path: '/', namespace_path: '/' }],
+ projectId: 1,
+ projectPath: '/',
+ namespacePath: '/',
+ projectNamespace: '/',
+ },
+ });
+ expect(findIssuableSelector().attributes('data-data')).toBe(
+ '[{"name":"test","id":"test","project_path":"/","namespace_path":"/"}]',
+ );
+ });
});
});
diff --git a/spec/frontend/issues/show/components/fields/title_spec.js b/spec/frontend/issues/show/components/fields/title_spec.js
index efd0b6fbd30..de04405d89b 100644
--- a/spec/frontend/issues/show/components/fields/title_spec.js
+++ b/spec/frontend/issues/show/components/fields/title_spec.js
@@ -12,9 +12,7 @@ describe('Title field component', () => {
wrapper = shallowMount(TitleField, {
propsData: {
- formState: {
- title: 'test',
- },
+ value: 'test',
},
});
});
diff --git a/spec/frontend/issues/show/components/incidents/incident_tabs_spec.js b/spec/frontend/issues/show/components/incidents/incident_tabs_spec.js
index 20c6cda33d4..35acca60de7 100644
--- a/spec/frontend/issues/show/components/incidents/incident_tabs_spec.js
+++ b/spec/frontend/issues/show/components/incidents/incident_tabs_spec.js
@@ -34,8 +34,9 @@ describe('Incident Tabs component', () => {
provide: {
fullPath: '',
iid: '',
+ projectId: '',
uploadMetricsFeatureAvailable: true,
- glFeatures: { incidentTimelineEventTab: true, incidentTimelineEvents: true },
+ glFeatures: { incidentTimeline: true, incidentTimelineEvents: true },
},
data() {
return { alert: mockAlert, ...data };
@@ -57,7 +58,6 @@ describe('Incident Tabs component', () => {
const findTabs = () => wrapper.findAll(GlTab);
const findSummaryTab = () => findTabs().at(0);
- const findMetricsTab = () => wrapper.find('[data-testid="metrics-tab"]');
const findAlertDetailsTab = () => wrapper.find('[data-testid="alert-details-tab"]');
const findAlertDetailsComponent = () => wrapper.find(AlertDetailsTable);
const findDescriptionComponent = () => wrapper.find(DescriptionComponent);
@@ -111,20 +111,6 @@ describe('Incident Tabs component', () => {
});
});
- describe('upload metrics feature available', () => {
- it('shows the metric tab when metrics are available', () => {
- mountComponent({}, { provide: { uploadMetricsFeatureAvailable: true } });
-
- expect(findMetricsTab().exists()).toBe(true);
- });
-
- it('hides the tab when metrics are not available', () => {
- mountComponent({}, { provide: { uploadMetricsFeatureAvailable: false } });
-
- expect(findMetricsTab().exists()).toBe(false);
- });
- });
-
describe('Snowplow tracking', () => {
beforeEach(() => {
jest.spyOn(Tracking, 'event');
diff --git a/spec/frontend/issues/show/mock_data/mock_data.js b/spec/frontend/issues/show/mock_data/mock_data.js
index 89653ff82b2..7b0b8ca686a 100644
--- a/spec/frontend/issues/show/mock_data/mock_data.js
+++ b/spec/frontend/issues/show/mock_data/mock_data.js
@@ -72,3 +72,18 @@ export const descriptionHtmlWithCheckboxes = `
</li>
</ul>
`;
+
+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>
+ </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>
+`;
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 b0d5859cd31..3d7bf7acb41 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
@@ -72,7 +72,7 @@ describe('GroupsListItem', () => {
expect(addSubscriptionSpy).toHaveBeenCalledWith(mockSubscriptionPath, mockGroup1.full_path);
expect(persistAlert).toHaveBeenCalledWith({
- linkUrl: '/help/integration/jira_development_panel.html#usage',
+ 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',
diff --git a/spec/frontend/jira_connect/subscriptions/components/app_spec.js b/spec/frontend/jira_connect/subscriptions/components/app_spec.js
index 6b3ca7ffd65..ce02144f22f 100644
--- a/spec/frontend/jira_connect/subscriptions/components/app_spec.js
+++ b/spec/frontend/jira_connect/subscriptions/components/app_spec.js
@@ -6,10 +6,12 @@ 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 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';
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 { mockSubscription } from '../mock_data';
jest.mock('~/jira_connect/subscriptions/utils', () => ({
@@ -26,6 +28,7 @@ describe('JiraConnectApp', () => {
const findSignInPage = () => wrapper.findComponent(SignInPage);
const findSubscriptionsPage = () => wrapper.findComponent(SubscriptionsPage);
const findUserLink = () => wrapper.findComponent(UserLink);
+ const findBrowserSupportAlert = () => wrapper.findComponent(BrowserSupportAlert);
const createComponent = ({ provide, mountFn = shallowMountExtended } = {}) => {
store = createStore();
@@ -207,4 +210,29 @@ describe('JiraConnectApp', () => {
});
});
});
+
+ describe.each`
+ jiraConnectOauthEnabled | canUseCrypto | shouldShowAlert
+ ${false} | ${false} | ${false}
+ ${false} | ${true} | ${false}
+ ${true} | ${false} | ${true}
+ ${true} | ${true} | ${false}
+ `(
+ 'when `jiraConnectOauth` feature flag is $jiraConnectOauthEnabled and `AccessorUtilities.canUseCrypto` returns $canUseCrypto',
+ ({ jiraConnectOauthEnabled, canUseCrypto, shouldShowAlert }) => {
+ beforeEach(() => {
+ jest.spyOn(AccessorUtilities, 'canUseCrypto').mockReturnValue(canUseCrypto);
+
+ createComponent({ provide: { glFeatures: { jiraConnectOauth: jiraConnectOauthEnabled } } });
+ });
+
+ it(`does ${shouldShowAlert ? '' : 'not'} render BrowserSupportAlert component`, () => {
+ expect(findBrowserSupportAlert().exists()).toBe(shouldShowAlert);
+ });
+
+ it(`does ${!shouldShowAlert ? '' : 'not'} render the main Jira Connect app template`, () => {
+ expect(wrapper.findByTestId('jira-connect-app').exists()).toBe(!shouldShowAlert);
+ });
+ },
+ );
});
diff --git a/spec/frontend/jira_connect/subscriptions/components/browser_support_alert_spec.js b/spec/frontend/jira_connect/subscriptions/components/browser_support_alert_spec.js
new file mode 100644
index 00000000000..aa93a6be3c8
--- /dev/null
+++ b/spec/frontend/jira_connect/subscriptions/components/browser_support_alert_spec.js
@@ -0,0 +1,37 @@
+import { GlAlert, GlLink } from '@gitlab/ui';
+import { shallowMount, mount } from '@vue/test-utils';
+import BrowserSupportAlert from '~/jira_connect/subscriptions/components/browser_support_alert.vue';
+
+describe('BrowserSupportAlert', () => {
+ let wrapper;
+
+ const createComponent = ({ mountFn = shallowMount } = {}) => {
+ wrapper = mountFn(BrowserSupportAlert);
+ };
+
+ const findAlert = () => wrapper.findComponent(GlAlert);
+ const findLink = () => wrapper.findComponent(GlLink);
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('displays a non-dismissible alert', () => {
+ createComponent();
+
+ expect(findAlert().exists()).toBe(true);
+ expect(findAlert().props()).toMatchObject({
+ dismissible: false,
+ title: BrowserSupportAlert.i18n.title,
+ variant: 'danger',
+ });
+ });
+
+ it('renders help link with target="_blank" and rel="noopener noreferrer"', () => {
+ createComponent({ mountFn: mount });
+ expect(findLink().attributes()).toMatchObject({
+ target: '_blank',
+ rel: 'noopener',
+ });
+ });
+});
diff --git a/spec/frontend/jira_connect/subscriptions/components/compatibility_alert_spec.js b/spec/frontend/jira_connect/subscriptions/components/compatibility_alert_spec.js
index f8ee8c2c664..5f38a0acb9d 100644
--- a/spec/frontend/jira_connect/subscriptions/components/compatibility_alert_spec.js
+++ b/spec/frontend/jira_connect/subscriptions/components/compatibility_alert_spec.js
@@ -29,7 +29,7 @@ describe('CompatibilityAlert', () => {
createComponent({ mountFn: mount });
expect(findLink().attributes()).toMatchObject({
target: '_blank',
- rel: 'noopener noreferrer',
+ rel: 'noopener',
});
});
diff --git a/spec/frontend/jira_connect/subscriptions/pages/sign_in_spec.js b/spec/frontend/jira_connect/subscriptions/pages/sign_in_spec.js
index 175896c4ab0..97d1b077164 100644
--- a/spec/frontend/jira_connect/subscriptions/pages/sign_in_spec.js
+++ b/spec/frontend/jira_connect/subscriptions/pages/sign_in_spec.js
@@ -5,7 +5,7 @@ import SignInLegacyButton from '~/jira_connect/subscriptions/components/sign_in_
import SignInOauthButton from '~/jira_connect/subscriptions/components/sign_in_oauth_button.vue';
import SubscriptionsList from '~/jira_connect/subscriptions/components/subscriptions_list.vue';
import createStore from '~/jira_connect/subscriptions/store';
-import { I18N_DEFAULT_SIGN_IN_BUTTON_TEXT } from '../../../../../app/assets/javascripts/jira_connect/subscriptions/constants';
+import { I18N_DEFAULT_SIGN_IN_BUTTON_TEXT } from '~/jira_connect/subscriptions/constants';
jest.mock('~/jira_connect/subscriptions/utils');
diff --git a/spec/frontend/jira_import/components/jira_import_form_spec.js b/spec/frontend/jira_import/components/jira_import_form_spec.js
index 7a550d85204..41d3cd46d01 100644
--- a/spec/frontend/jira_import/components/jira_import_form_spec.js
+++ b/spec/frontend/jira_import/components/jira_import_form_spec.js
@@ -6,7 +6,7 @@ import {
GlFormSelect,
GlLabel,
GlSearchBoxByType,
- GlTable,
+ GlTableLite,
} from '@gitlab/ui';
import { getByRole } from '@testing-library/dom';
import { mount, shallowMount } from '@vue/test-utils';
@@ -34,19 +34,19 @@ describe('JiraImportForm', () => {
const currentUsername = 'mrgitlab';
- const getAlert = () => wrapper.find(GlAlert);
+ const getAlert = () => wrapper.findComponent(GlAlert);
- const getSelectDropdown = () => wrapper.find(GlFormSelect);
+ const getSelectDropdown = () => wrapper.findComponent(GlFormSelect);
- const getContinueButton = () => wrapper.find(GlButton);
+ const getContinueButton = () => wrapper.findComponent(GlButton);
- const getCancelButton = () => wrapper.findAll(GlButton).at(1);
+ const getCancelButton = () => wrapper.findAllComponents(GlButton).at(1);
- const getLabel = () => wrapper.find(GlLabel);
+ const getLabel = () => wrapper.findComponent(GlLabel);
- const getTable = () => wrapper.find(GlTable);
+ const getTable = () => wrapper.findComponent(GlTableLite);
- const getUserDropdown = () => getTable().find(GlDropdown);
+ const getUserDropdown = () => getTable().findComponent(GlDropdown);
const getHeader = (name) => getByRole(wrapper.element, 'columnheader', { name });
@@ -107,14 +107,13 @@ describe('JiraImportForm', () => {
mutateSpy.mockRestore();
querySpy.mockRestore();
wrapper.destroy();
- wrapper = null;
});
describe('select dropdown project selection', () => {
it('is shown', () => {
wrapper = mountComponent();
- expect(wrapper.find(GlFormSelect).exists()).toBe(true);
+ expect(getSelectDropdown().exists()).toBe(true);
});
it('contains a list of Jira projects to select from', () => {
@@ -273,7 +272,7 @@ describe('JiraImportForm', () => {
wrapper = mountComponent({ mountFunction: mount });
- wrapper.find(GlSearchBoxByType).vm.$emit('input', 'fred');
+ wrapper.findComponent(GlSearchBoxByType).vm.$emit('input', 'fred');
});
it('makes a GraphQL call', () => {
diff --git a/spec/frontend/jobs/components/filtered_search/jobs_filtered_search_spec.js b/spec/frontend/jobs/components/filtered_search/jobs_filtered_search_spec.js
new file mode 100644
index 00000000000..322cfa3ba1f
--- /dev/null
+++ b/spec/frontend/jobs/components/filtered_search/jobs_filtered_search_spec.js
@@ -0,0 +1,49 @@
+import { GlFilteredSearch } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import { OPERATOR_IS_ONLY } from '~/vue_shared/components/filtered_search_bar/constants';
+import JobsFilteredSearch from '~/jobs/components/filtered_search/jobs_filtered_search.vue';
+import { mockFailedSearchToken } from '../../mock_data';
+
+describe('Jobs filtered search', () => {
+ let wrapper;
+
+ const findFilteredSearch = () => wrapper.findComponent(GlFilteredSearch);
+ const getSearchToken = (type) =>
+ findFilteredSearch()
+ .props('availableTokens')
+ .find((token) => token.type === type);
+
+ const findStatusToken = () => getSearchToken('status');
+
+ const createComponent = () => {
+ wrapper = shallowMount(JobsFilteredSearch);
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('displays filtered search', () => {
+ expect(findFilteredSearch().exists()).toBe(true);
+ });
+
+ it('displays status token', () => {
+ expect(findStatusToken()).toMatchObject({
+ type: 'status',
+ icon: 'status',
+ title: 'Status',
+ unique: true,
+ operators: OPERATOR_IS_ONLY,
+ });
+ });
+
+ it('emits filter token to parent component', () => {
+ findFilteredSearch().vm.$emit('submit', mockFailedSearchToken);
+
+ expect(wrapper.emitted('filterJobsBySearch')).toEqual([[mockFailedSearchToken]]);
+ });
+});
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
new file mode 100644
index 00000000000..ce8e482cc16
--- /dev/null
+++ b/spec/frontend/jobs/components/filtered_search/tokens/job_status_token_spec.js
@@ -0,0 +1,57 @@
+import { GlFilteredSearchToken, GlFilteredSearchSuggestion, GlIcon } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import { stubComponent } from 'helpers/stub_component';
+import JobStatusToken from '~/jobs/components/filtered_search/tokens/job_status_token.vue';
+
+describe('Job Status Token', () => {
+ let wrapper;
+
+ const findFilteredSearchToken = () => wrapper.findComponent(GlFilteredSearchToken);
+ const findAllFilteredSearchSuggestions = () =>
+ wrapper.findAllComponents(GlFilteredSearchSuggestion);
+ const findAllGlIcons = () => wrapper.findAllComponents(GlIcon);
+
+ const defaultProps = {
+ config: {
+ type: 'status',
+ icon: 'status',
+ title: 'Status',
+ unique: true,
+ },
+ value: {
+ data: '',
+ },
+ };
+
+ const createComponent = () => {
+ wrapper = shallowMount(JobStatusToken, {
+ propsData: {
+ ...defaultProps,
+ },
+ stubs: {
+ GlFilteredSearchToken: stubComponent(GlFilteredSearchToken, {
+ template: `<div><slot name="suggestions"></slot></div>`,
+ }),
+ },
+ });
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('passes config correctly', () => {
+ expect(findFilteredSearchToken().props('config')).toEqual(defaultProps.config);
+ });
+
+ it('renders all job statuses available', () => {
+ const expectedLength = 11;
+
+ expect(findAllFilteredSearchSuggestions()).toHaveLength(expectedLength);
+ expect(findAllGlIcons()).toHaveLength(expectedLength);
+ });
+});
diff --git a/spec/frontend/jobs/components/job_app_spec.js b/spec/frontend/jobs/components/job_app_spec.js
index 06ebcd7f134..9abe66b4696 100644
--- a/spec/frontend/jobs/components/job_app_spec.js
+++ b/spec/frontend/jobs/components/job_app_spec.js
@@ -375,8 +375,8 @@ describe('Job App', () => {
});
describe('sidebar', () => {
- it('has no blank blocks', (done) => {
- setupAndMount({
+ it('has no blank blocks', async () => {
+ await setupAndMount({
jobData: {
duration: null,
finished_at: null,
@@ -387,17 +387,14 @@ describe('Job App', () => {
tags: [],
cancel_path: null,
},
- })
- .then(() => {
- const blocks = wrapper.findAll('.blocks-container > *').wrappers;
- expect(blocks.length).toBeGreaterThan(0);
-
- blocks.forEach((block) => {
- expect(block.text().trim()).not.toBe('');
- });
- })
- .then(done)
- .catch(done.fail);
+ });
+
+ const blocks = wrapper.findAll('.blocks-container > *').wrappers;
+ expect(blocks.length).toBeGreaterThan(0);
+
+ blocks.forEach((block) => {
+ expect(block.text().trim()).not.toBe('');
+ });
});
});
});
diff --git a/spec/frontend/jobs/components/table/graphql/cache_config_spec.js b/spec/frontend/jobs/components/table/graphql/cache_config_spec.js
index ac79186cb46..88c97285b85 100644
--- a/spec/frontend/jobs/components/table/graphql/cache_config_spec.js
+++ b/spec/frontend/jobs/components/table/graphql/cache_config_spec.js
@@ -33,6 +33,26 @@ describe('jobs/components/table/graphql/cache_config', () => {
);
});
+ it('should not add to existing cache if the incoming elements are the same', () => {
+ // simulate that this is the last page
+ const finalExistingCache = {
+ ...CIJobConnectionExistingCache,
+ pageInfo: {
+ hasNextPage: false,
+ },
+ };
+
+ const res = cacheConfig.typePolicies.CiJobConnection.merge(
+ CIJobConnectionExistingCache,
+ finalExistingCache,
+ {
+ args: firstLoadArgs,
+ },
+ );
+
+ expect(res.nodes).toHaveLength(CIJobConnectionExistingCache.nodes.length);
+ });
+
it('should contain the pageInfo key as part of the result', () => {
const res = cacheConfig.typePolicies.CiJobConnection.merge({}, CIJobConnectionIncomingCache, {
args: firstLoadArgs,
diff --git a/spec/frontend/jobs/components/table/job_table_app_spec.js b/spec/frontend/jobs/components/table/job_table_app_spec.js
index 4d51624dfff..986fba21fb9 100644
--- a/spec/frontend/jobs/components/table/job_table_app_spec.js
+++ b/spec/frontend/jobs/components/table/job_table_app_spec.js
@@ -1,30 +1,48 @@
-import { GlSkeletonLoader, GlAlert, GlEmptyState, GlIntersectionObserver } from '@gitlab/ui';
+import {
+ GlSkeletonLoader,
+ GlAlert,
+ GlEmptyState,
+ GlIntersectionObserver,
+ GlLoadingIcon,
+} from '@gitlab/ui';
import { mount, shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
+import { s__ } from '~/locale';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
+import createFlash from '~/flash';
import getJobsQuery from '~/jobs/components/table/graphql/queries/get_jobs.query.graphql';
import JobsTable from '~/jobs/components/table/jobs_table.vue';
import JobsTableApp from '~/jobs/components/table/jobs_table_app.vue';
import JobsTableTabs from '~/jobs/components/table/jobs_table_tabs.vue';
-import { mockJobsQueryResponse, mockJobsQueryEmptyResponse } from '../../mock_data';
+import JobsFilteredSearch from '~/jobs/components/filtered_search/jobs_filtered_search.vue';
+import {
+ mockJobsQueryResponse,
+ mockJobsQueryEmptyResponse,
+ mockFailedSearchToken,
+} from '../../mock_data';
const projectPath = 'gitlab-org/gitlab';
Vue.use(VueApollo);
+jest.mock('~/flash');
+
describe('Job table app', () => {
let wrapper;
+ let jobsTableVueSearch = true;
const successHandler = jest.fn().mockResolvedValue(mockJobsQueryResponse);
const failedHandler = jest.fn().mockRejectedValue(new Error('GraphQL error'));
const emptyHandler = jest.fn().mockResolvedValue(mockJobsQueryEmptyResponse);
const findSkeletonLoader = () => wrapper.findComponent(GlSkeletonLoader);
+ const findLoadingSpinner = () => wrapper.findComponent(GlLoadingIcon);
const findTable = () => wrapper.findComponent(JobsTable);
const findTabs = () => wrapper.findComponent(JobsTableTabs);
const findAlert = () => wrapper.findComponent(GlAlert);
const findEmptyState = () => wrapper.findComponent(GlEmptyState);
+ const findFilteredSearch = () => wrapper.findComponent(JobsFilteredSearch);
const triggerInfiniteScroll = () =>
wrapper.findComponent(GlIntersectionObserver).vm.$emit('appear');
@@ -48,6 +66,7 @@ describe('Job table app', () => {
},
provide: {
fullPath: projectPath,
+ glFeatures: { jobsTableVueSearch },
},
apolloProvider: createMockApolloProvider(handler),
});
@@ -58,11 +77,21 @@ describe('Job table app', () => {
});
describe('loading state', () => {
- it('should display skeleton loader when loading', () => {
+ beforeEach(() => {
createComponent();
+ });
+ it('should display skeleton loader when loading', () => {
expect(findSkeletonLoader().exists()).toBe(true);
expect(findTable().exists()).toBe(false);
+ expect(findLoadingSpinner().exists()).toBe(false);
+ });
+
+ it('when switching tabs only the skeleton loader should show', () => {
+ findTabs().vm.$emit('fetchJobsByStatus', null);
+
+ expect(findSkeletonLoader().exists()).toBe(true);
+ expect(findLoadingSpinner().exists()).toBe(false);
});
});
@@ -76,6 +105,7 @@ describe('Job table app', () => {
it('should display the jobs table with data', () => {
expect(findTable().exists()).toBe(true);
expect(findSkeletonLoader().exists()).toBe(false);
+ expect(findLoadingSpinner().exists()).toBe(false);
});
it('should refetch jobs query on fetchJobsByStatus event', async () => {
@@ -98,8 +128,12 @@ describe('Job table app', () => {
});
it('handles infinite scrolling by calling fetch more', async () => {
+ expect(findLoadingSpinner().exists()).toBe(true);
+
await waitForPromises();
+ expect(findLoadingSpinner().exists()).toBe(false);
+
expect(successHandler).toHaveBeenCalledWith({
after: 'eyJpZCI6IjIzMTcifQ',
fullPath: 'gitlab-org/gitlab',
@@ -137,4 +171,69 @@ describe('Job table app', () => {
expect(findTable().exists()).toBe(true);
});
});
+
+ describe('filtered search', () => {
+ it('should display filtered search', () => {
+ createComponent();
+
+ expect(findFilteredSearch().exists()).toBe(true);
+ });
+
+ // this test should be updated once BE supports tab and filtered search filtering
+ // https://gitlab.com/gitlab-org/gitlab/-/issues/356210
+ it.each`
+ scope | shouldDisplay
+ ${null} | ${true}
+ ${['FAILED', 'SUCCESS', 'CANCELED']} | ${false}
+ `(
+ 'with tab scope $scope the filtered search displays $shouldDisplay',
+ async ({ scope, shouldDisplay }) => {
+ createComponent();
+
+ await waitForPromises();
+
+ await findTabs().vm.$emit('fetchJobsByStatus', scope);
+
+ expect(findFilteredSearch().exists()).toBe(shouldDisplay);
+ },
+ );
+
+ it('refetches jobs query when filtering', async () => {
+ createComponent();
+
+ jest.spyOn(wrapper.vm.$apollo.queries.jobs, 'refetch').mockImplementation(jest.fn());
+
+ expect(wrapper.vm.$apollo.queries.jobs.refetch).toHaveBeenCalledTimes(0);
+
+ await findFilteredSearch().vm.$emit('filterJobsBySearch', [mockFailedSearchToken]);
+
+ expect(wrapper.vm.$apollo.queries.jobs.refetch).toHaveBeenCalledTimes(1);
+ });
+
+ it('shows raw text warning when user inputs raw text', async () => {
+ const expectedWarning = {
+ message: s__(
+ 'Jobs|Raw text search is not currently supported for the jobs filtered search feature. Please use the available search tokens.',
+ ),
+ type: 'warning',
+ };
+
+ createComponent();
+
+ jest.spyOn(wrapper.vm.$apollo.queries.jobs, 'refetch').mockImplementation(jest.fn());
+
+ await findFilteredSearch().vm.$emit('filterJobsBySearch', ['raw text']);
+
+ expect(createFlash).toHaveBeenCalledWith(expectedWarning);
+ expect(wrapper.vm.$apollo.queries.jobs.refetch).toHaveBeenCalledTimes(0);
+ });
+
+ it('should not display filtered search', () => {
+ jobsTableVueSearch = false;
+
+ createComponent();
+
+ expect(findFilteredSearch().exists()).toBe(false);
+ });
+ });
});
diff --git a/spec/frontend/jobs/components/table/jobs_table_tabs_spec.js b/spec/frontend/jobs/components/table/jobs_table_tabs_spec.js
index ac9b45be932..23632001060 100644
--- a/spec/frontend/jobs/components/table/jobs_table_tabs_spec.js
+++ b/spec/frontend/jobs/components/table/jobs_table_tabs_spec.js
@@ -1,3 +1,4 @@
+import { GlTab } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import { trimText } from 'helpers/text_helper';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
@@ -7,16 +8,31 @@ describe('Jobs Table Tabs', () => {
let wrapper;
const defaultProps = {
- jobCounts: { all: 848, pending: 0, running: 0, finished: 704 },
+ allJobsCount: 286,
+ loading: false,
};
- const findTab = (testId) => wrapper.findByTestId(testId);
+ const statuses = {
+ success: 'SUCCESS',
+ failed: 'FAILED',
+ canceled: 'CANCELED',
+ };
+
+ const findAllTab = () => wrapper.findByTestId('jobs-all-tab');
+ const findFinishedTab = () => wrapper.findByTestId('jobs-finished-tab');
+
+ const triggerTabChange = (index) => wrapper.findAllComponents(GlTab).at(index).vm.$emit('click');
- const createComponent = () => {
+ const createComponent = (props = defaultProps) => {
wrapper = extendedWrapper(
mount(JobsTableTabs, {
provide: {
- ...defaultProps,
+ jobStatuses: {
+ ...statuses,
+ },
+ },
+ propsData: {
+ ...props,
},
}),
);
@@ -30,13 +46,21 @@ describe('Jobs Table Tabs', () => {
wrapper.destroy();
});
+ it('displays All tab with count', () => {
+ expect(trimText(findAllTab().text())).toBe(`All ${defaultProps.allJobsCount}`);
+ });
+
+ it('displays Finished tab with no count', () => {
+ expect(findFinishedTab().text()).toBe('Finished');
+ });
+
it.each`
- tabId | text | count
- ${'jobs-all-tab'} | ${'All'} | ${defaultProps.jobCounts.all}
- ${'jobs-pending-tab'} | ${'Pending'} | ${defaultProps.jobCounts.pending}
- ${'jobs-running-tab'} | ${'Running'} | ${defaultProps.jobCounts.running}
- ${'jobs-finished-tab'} | ${'Finished'} | ${defaultProps.jobCounts.finished}
- `('displays the right tab text and badge count', ({ tabId, text, count }) => {
- expect(trimText(findTab(tabId).text())).toBe(`${text} ${count}`);
+ tabIndex | expectedScope
+ ${0} | ${null}
+ ${1} | ${[statuses.success, statuses.failed, statuses.canceled]}
+ `('emits fetchJobsByStatus with $expectedScope on tab change', ({ tabIndex, expectedScope }) => {
+ triggerTabChange(tabIndex);
+
+ expect(wrapper.emitted()).toEqual({ fetchJobsByStatus: [[expectedScope]] });
});
});
diff --git a/spec/frontend/jobs/components/trigger_block_spec.js b/spec/frontend/jobs/components/trigger_block_spec.js
index e0eb873dc2f..78596612d23 100644
--- a/spec/frontend/jobs/components/trigger_block_spec.js
+++ b/spec/frontend/jobs/components/trigger_block_spec.js
@@ -1,12 +1,12 @@
-import { GlButton, GlTable } from '@gitlab/ui';
+import { GlButton, GlTableLite } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import TriggerBlock from '~/jobs/components/trigger_block.vue';
describe('Trigger block', () => {
let wrapper;
- const findRevealButton = () => wrapper.find(GlButton);
- const findVariableTable = () => wrapper.find(GlTable);
+ const findRevealButton = () => wrapper.findComponent(GlButton);
+ const findVariableTable = () => wrapper.findComponent(GlTableLite);
const findShortToken = () => wrapper.find('[data-testid="trigger-short-token"]');
const findVariableValue = (index) =>
wrapper.findAll('[data-testid="trigger-build-value"]').at(index);
@@ -22,7 +22,6 @@ describe('Trigger block', () => {
afterEach(() => {
wrapper.destroy();
- wrapper = null;
});
describe('with short token and no variables', () => {
diff --git a/spec/frontend/jobs/mock_data.js b/spec/frontend/jobs/mock_data.js
index 73b9df1853d..27b6c04eded 100644
--- a/spec/frontend/jobs/mock_data.js
+++ b/spec/frontend/jobs/mock_data.js
@@ -1481,6 +1481,7 @@ export const mockJobsQueryResponse = {
project: {
id: '1',
jobs: {
+ count: 1,
pageInfo: {
endCursor: 'eyJpZCI6IjIzMTcifQ',
hasNextPage: true,
@@ -1911,10 +1912,19 @@ export const CIJobConnectionIncomingCacheRunningStatus = {
};
export const CIJobConnectionExistingCache = {
+ pageInfo: {
+ __typename: 'PageInfo',
+ endCursor: 'eyJpZCI6IjIwNTEifQ',
+ hasNextPage: true,
+ hasPreviousPage: false,
+ startCursor: 'eyJpZCI6IjIxNzMifQ',
+ },
nodes: [
- { __ref: 'CiJob:gid://gitlab/Ci::Build/2057' },
- { __ref: 'CiJob:gid://gitlab/Ci::Build/2056' },
- { __ref: 'CiJob:gid://gitlab/Ci::Build/2051' },
+ { __ref: 'CiJob:gid://gitlab/Ci::Build/2100' },
+ { __ref: 'CiJob:gid://gitlab/Ci::Build/2101' },
+ { __ref: 'CiJob:gid://gitlab/Ci::Build/2102' },
],
statuses: 'PENDING',
};
+
+export const mockFailedSearchToken = { type: 'status', value: { data: 'FAILED', operator: '=' } };
diff --git a/spec/frontend/jobs/store/actions_spec.js b/spec/frontend/jobs/store/actions_spec.js
index 16448d6a3ca..b9f97a3c3ae 100644
--- a/spec/frontend/jobs/store/actions_spec.js
+++ b/spec/frontend/jobs/store/actions_spec.js
@@ -39,62 +39,60 @@ describe('Job State actions', () => {
});
describe('setJobEndpoint', () => {
- it('should commit SET_JOB_ENDPOINT mutation', (done) => {
- testAction(
+ it('should commit SET_JOB_ENDPOINT mutation', () => {
+ return testAction(
setJobEndpoint,
'job/872324.json',
mockedState,
[{ type: types.SET_JOB_ENDPOINT, payload: 'job/872324.json' }],
[],
- done,
);
});
});
describe('setJobLogOptions', () => {
- it('should commit SET_JOB_LOG_OPTIONS mutation', (done) => {
- testAction(
+ it('should commit SET_JOB_LOG_OPTIONS mutation', () => {
+ return testAction(
setJobLogOptions,
{ pagePath: 'job/872324/trace.json' },
mockedState,
[{ type: types.SET_JOB_LOG_OPTIONS, payload: { pagePath: 'job/872324/trace.json' } }],
[],
- done,
);
});
});
describe('hideSidebar', () => {
- it('should commit HIDE_SIDEBAR mutation', (done) => {
- testAction(hideSidebar, null, mockedState, [{ type: types.HIDE_SIDEBAR }], [], done);
+ it('should commit HIDE_SIDEBAR mutation', () => {
+ return testAction(hideSidebar, null, mockedState, [{ type: types.HIDE_SIDEBAR }], []);
});
});
describe('showSidebar', () => {
- it('should commit HIDE_SIDEBAR mutation', (done) => {
- testAction(showSidebar, null, mockedState, [{ type: types.SHOW_SIDEBAR }], [], done);
+ it('should commit SHOW_SIDEBAR mutation', () => {
+ return testAction(showSidebar, null, mockedState, [{ type: types.SHOW_SIDEBAR }], []);
});
});
describe('toggleSidebar', () => {
describe('when isSidebarOpen is true', () => {
- it('should dispatch hideSidebar', (done) => {
- testAction(toggleSidebar, null, mockedState, [], [{ type: 'hideSidebar' }], done);
+ it('should dispatch hideSidebar', () => {
+ return testAction(toggleSidebar, null, mockedState, [], [{ type: 'hideSidebar' }]);
});
});
describe('when isSidebarOpen is false', () => {
- it('should dispatch showSidebar', (done) => {
+ it('should dispatch showSidebar', () => {
mockedState.isSidebarOpen = false;
- testAction(toggleSidebar, null, mockedState, [], [{ type: 'showSidebar' }], done);
+ return testAction(toggleSidebar, null, mockedState, [], [{ type: 'showSidebar' }]);
});
});
});
describe('requestJob', () => {
- it('should commit REQUEST_JOB mutation', (done) => {
- testAction(requestJob, null, mockedState, [{ type: types.REQUEST_JOB }], [], done);
+ it('should commit REQUEST_JOB mutation', () => {
+ return testAction(requestJob, null, mockedState, [{ type: types.REQUEST_JOB }], []);
});
});
@@ -113,10 +111,10 @@ describe('Job State actions', () => {
});
describe('success', () => {
- it('dispatches requestJob and receiveJobSuccess ', (done) => {
+ it('dispatches requestJob and receiveJobSuccess ', () => {
mock.onGet(`${TEST_HOST}/endpoint.json`).replyOnce(200, { id: 121212, name: 'karma' });
- testAction(
+ return testAction(
fetchJob,
null,
mockedState,
@@ -130,7 +128,6 @@ describe('Job State actions', () => {
type: 'receiveJobSuccess',
},
],
- done,
);
});
});
@@ -140,8 +137,8 @@ describe('Job State actions', () => {
mock.onGet(`${TEST_HOST}/endpoint.json`).reply(500);
});
- it('dispatches requestJob and receiveJobError ', (done) => {
- testAction(
+ it('dispatches requestJob and receiveJobError ', () => {
+ return testAction(
fetchJob,
null,
mockedState,
@@ -154,46 +151,50 @@ describe('Job State actions', () => {
type: 'receiveJobError',
},
],
- done,
);
});
});
});
describe('receiveJobSuccess', () => {
- it('should commit RECEIVE_JOB_SUCCESS mutation', (done) => {
- testAction(
+ it('should commit RECEIVE_JOB_SUCCESS mutation', () => {
+ return testAction(
receiveJobSuccess,
{ id: 121232132 },
mockedState,
[{ type: types.RECEIVE_JOB_SUCCESS, payload: { id: 121232132 } }],
[],
- done,
);
});
});
describe('receiveJobError', () => {
- it('should commit RECEIVE_JOB_ERROR mutation', (done) => {
- testAction(receiveJobError, null, mockedState, [{ type: types.RECEIVE_JOB_ERROR }], [], done);
+ it('should commit RECEIVE_JOB_ERROR mutation', () => {
+ return testAction(
+ receiveJobError,
+ null,
+ mockedState,
+ [{ type: types.RECEIVE_JOB_ERROR }],
+ [],
+ );
});
});
describe('scrollTop', () => {
- it('should dispatch toggleScrollButtons action', (done) => {
- testAction(scrollTop, null, mockedState, [], [{ type: 'toggleScrollButtons' }], done);
+ it('should dispatch toggleScrollButtons action', () => {
+ return testAction(scrollTop, null, mockedState, [], [{ type: 'toggleScrollButtons' }]);
});
});
describe('scrollBottom', () => {
- it('should dispatch toggleScrollButtons action', (done) => {
- testAction(scrollBottom, null, mockedState, [], [{ type: 'toggleScrollButtons' }], done);
+ it('should dispatch toggleScrollButtons action', () => {
+ return testAction(scrollBottom, null, mockedState, [], [{ type: 'toggleScrollButtons' }]);
});
});
describe('requestJobLog', () => {
- it('should commit REQUEST_JOB_LOG mutation', (done) => {
- testAction(requestJobLog, null, mockedState, [{ type: types.REQUEST_JOB_LOG }], [], done);
+ it('should commit REQUEST_JOB_LOG mutation', () => {
+ return testAction(requestJobLog, null, mockedState, [{ type: types.REQUEST_JOB_LOG }], []);
});
});
@@ -212,13 +213,13 @@ describe('Job State actions', () => {
});
describe('success', () => {
- it('dispatches requestJobLog, receiveJobLogSuccess and stopPollingJobLog when job is complete', (done) => {
+ it('dispatches requestJobLog, receiveJobLogSuccess and stopPollingJobLog when job is complete', () => {
mock.onGet(`${TEST_HOST}/endpoint/trace.json`).replyOnce(200, {
html: 'I, [2018-08-17T22:57:45.707325 #1841] INFO -- :',
complete: true,
});
- testAction(
+ return testAction(
fetchJobLog,
null,
mockedState,
@@ -239,7 +240,6 @@ describe('Job State actions', () => {
type: 'stopPollingJobLog',
},
],
- done,
);
});
@@ -255,8 +255,8 @@ describe('Job State actions', () => {
mock.onGet(`${TEST_HOST}/endpoint/trace.json`).replyOnce(200, jobLogPayload);
});
- it('dispatches startPollingJobLog', (done) => {
- testAction(
+ it('dispatches startPollingJobLog', () => {
+ return testAction(
fetchJobLog,
null,
mockedState,
@@ -266,14 +266,13 @@ describe('Job State actions', () => {
{ type: 'receiveJobLogSuccess', payload: jobLogPayload },
{ type: 'startPollingJobLog' },
],
- done,
);
});
- it('does not dispatch startPollingJobLog when timeout is non-empty', (done) => {
+ it('does not dispatch startPollingJobLog when timeout is non-empty', () => {
mockedState.jobLogTimeout = 1;
- testAction(
+ return testAction(
fetchJobLog,
null,
mockedState,
@@ -282,7 +281,6 @@ describe('Job State actions', () => {
{ type: 'toggleScrollisInBottom', payload: true },
{ type: 'receiveJobLogSuccess', payload: jobLogPayload },
],
- done,
);
});
});
@@ -293,8 +291,8 @@ describe('Job State actions', () => {
mock.onGet(`${TEST_HOST}/endpoint/trace.json`).reply(500);
});
- it('dispatches requestJobLog and receiveJobLogError ', (done) => {
- testAction(
+ it('dispatches requestJobLog and receiveJobLogError ', () => {
+ return testAction(
fetchJobLog,
null,
mockedState,
@@ -304,7 +302,6 @@ describe('Job State actions', () => {
type: 'receiveJobLogError',
},
],
- done,
);
});
});
@@ -358,65 +355,58 @@ describe('Job State actions', () => {
window.clearTimeout = origTimeout;
});
- it('should commit STOP_POLLING_JOB_LOG mutation ', (done) => {
+ it('should commit STOP_POLLING_JOB_LOG mutation ', async () => {
const jobLogTimeout = 7;
- testAction(
+ await testAction(
stopPollingJobLog,
null,
{ ...mockedState, jobLogTimeout },
[{ type: types.SET_JOB_LOG_TIMEOUT, payload: 0 }, { type: types.STOP_POLLING_JOB_LOG }],
[],
- )
- .then(() => {
- expect(window.clearTimeout).toHaveBeenCalledWith(jobLogTimeout);
- })
- .then(done)
- .catch(done.fail);
+ );
+ expect(window.clearTimeout).toHaveBeenCalledWith(jobLogTimeout);
});
});
describe('receiveJobLogSuccess', () => {
- it('should commit RECEIVE_JOB_LOG_SUCCESS mutation ', (done) => {
- testAction(
+ it('should commit RECEIVE_JOB_LOG_SUCCESS mutation ', () => {
+ return testAction(
receiveJobLogSuccess,
'hello world',
mockedState,
[{ type: types.RECEIVE_JOB_LOG_SUCCESS, payload: 'hello world' }],
[],
- done,
);
});
});
describe('receiveJobLogError', () => {
- it('should commit stop polling job log', (done) => {
- testAction(receiveJobLogError, null, mockedState, [], [{ type: 'stopPollingJobLog' }], done);
+ it('should commit stop polling job log', () => {
+ return testAction(receiveJobLogError, null, mockedState, [], [{ type: 'stopPollingJobLog' }]);
});
});
describe('toggleCollapsibleLine', () => {
- it('should commit TOGGLE_COLLAPSIBLE_LINE mutation ', (done) => {
- testAction(
+ it('should commit TOGGLE_COLLAPSIBLE_LINE mutation ', () => {
+ return testAction(
toggleCollapsibleLine,
{ isClosed: true },
mockedState,
[{ type: types.TOGGLE_COLLAPSIBLE_LINE, payload: { isClosed: true } }],
[],
- done,
);
});
});
describe('requestJobsForStage', () => {
- it('should commit REQUEST_JOBS_FOR_STAGE mutation ', (done) => {
- testAction(
+ it('should commit REQUEST_JOBS_FOR_STAGE mutation ', () => {
+ return testAction(
requestJobsForStage,
{ name: 'deploy' },
mockedState,
[{ type: types.REQUEST_JOBS_FOR_STAGE, payload: { name: 'deploy' } }],
[],
- done,
);
});
});
@@ -433,12 +423,12 @@ describe('Job State actions', () => {
});
describe('success', () => {
- it('dispatches requestJobsForStage and receiveJobsForStageSuccess ', (done) => {
+ it('dispatches requestJobsForStage and receiveJobsForStageSuccess ', () => {
mock
.onGet(`${TEST_HOST}/jobs.json`)
.replyOnce(200, { latest_statuses: [{ id: 121212, name: 'build' }], retried: [] });
- testAction(
+ return testAction(
fetchJobsForStage,
{ dropdown_path: `${TEST_HOST}/jobs.json` },
mockedState,
@@ -453,7 +443,6 @@ describe('Job State actions', () => {
type: 'receiveJobsForStageSuccess',
},
],
- done,
);
});
});
@@ -463,8 +452,8 @@ describe('Job State actions', () => {
mock.onGet(`${TEST_HOST}/jobs.json`).reply(500);
});
- it('dispatches requestJobsForStage and receiveJobsForStageError', (done) => {
- testAction(
+ it('dispatches requestJobsForStage and receiveJobsForStageError', () => {
+ return testAction(
fetchJobsForStage,
{ dropdown_path: `${TEST_HOST}/jobs.json` },
mockedState,
@@ -478,34 +467,31 @@ describe('Job State actions', () => {
type: 'receiveJobsForStageError',
},
],
- done,
);
});
});
});
describe('receiveJobsForStageSuccess', () => {
- it('should commit RECEIVE_JOBS_FOR_STAGE_SUCCESS mutation ', (done) => {
- testAction(
+ it('should commit RECEIVE_JOBS_FOR_STAGE_SUCCESS mutation ', () => {
+ return testAction(
receiveJobsForStageSuccess,
[{ id: 121212, name: 'karma' }],
mockedState,
[{ type: types.RECEIVE_JOBS_FOR_STAGE_SUCCESS, payload: [{ id: 121212, name: 'karma' }] }],
[],
- done,
);
});
});
describe('receiveJobsForStageError', () => {
- it('should commit RECEIVE_JOBS_FOR_STAGE_ERROR mutation ', (done) => {
- testAction(
+ it('should commit RECEIVE_JOBS_FOR_STAGE_ERROR mutation ', () => {
+ return testAction(
receiveJobsForStageError,
null,
mockedState,
[{ type: types.RECEIVE_JOBS_FOR_STAGE_ERROR }],
[],
- done,
);
});
});
diff --git a/spec/frontend/labels/components/promote_label_modal_spec.js b/spec/frontend/labels/components/promote_label_modal_spec.js
index d2fbdfc9a8d..8cfaba6f98a 100644
--- a/spec/frontend/labels/components/promote_label_modal_spec.js
+++ b/spec/frontend/labels/components/promote_label_modal_spec.js
@@ -50,7 +50,7 @@ describe('Promote label modal', () => {
vm.$destroy();
});
- it('redirects when a label is promoted', (done) => {
+ it('redirects when a label is promoted', () => {
const responseURL = `${TEST_HOST}/dummy/endpoint`;
jest.spyOn(axios, 'post').mockImplementation((url) => {
expect(url).toBe(labelMockData.url);
@@ -65,39 +65,35 @@ describe('Promote label modal', () => {
});
});
- vm.onSubmit()
- .then(() => {
- expect(eventHub.$emit).toHaveBeenCalledWith('promoteLabelModal.requestFinished', {
- labelUrl: labelMockData.url,
- successful: true,
- });
- })
- .then(done)
- .catch(done.fail);
+ return vm.onSubmit().then(() => {
+ expect(eventHub.$emit).toHaveBeenCalledWith('promoteLabelModal.requestFinished', {
+ labelUrl: labelMockData.url,
+ successful: true,
+ });
+ });
});
- it('displays an error if promoting a label failed', (done) => {
+ it('displays an error if promoting a label failed', () => {
const dummyError = new Error('promoting label failed');
dummyError.response = { status: 500 };
+
jest.spyOn(axios, 'post').mockImplementation((url) => {
expect(url).toBe(labelMockData.url);
expect(eventHub.$emit).toHaveBeenCalledWith(
'promoteLabelModal.requestStarted',
labelMockData.url,
);
+
return Promise.reject(dummyError);
});
- vm.onSubmit()
- .catch((error) => {
- expect(error).toBe(dummyError);
- expect(eventHub.$emit).toHaveBeenCalledWith('promoteLabelModal.requestFinished', {
- labelUrl: labelMockData.url,
- successful: false,
- });
- })
- .then(done)
- .catch(done.fail);
+ return vm.onSubmit().catch((error) => {
+ expect(error).toBe(dummyError);
+ expect(eventHub.$emit).toHaveBeenCalledWith('promoteLabelModal.requestFinished', {
+ labelUrl: labelMockData.url,
+ successful: false,
+ });
+ });
});
});
});
diff --git a/spec/frontend/lib/apollo/suppress_network_errors_during_navigation_link_spec.js b/spec/frontend/lib/apollo/suppress_network_errors_during_navigation_link_spec.js
index 971ba8b583c..5ac7a7985a8 100644
--- a/spec/frontend/lib/apollo/suppress_network_errors_during_navigation_link_spec.js
+++ b/spec/frontend/lib/apollo/suppress_network_errors_during_navigation_link_spec.js
@@ -82,34 +82,39 @@ describe('getSuppressNetworkErrorsDuringNavigationLink', () => {
isNavigatingAway.mockReturnValue(false);
});
- it('forwards successful requests', (done) => {
+ it('forwards successful requests', () => {
createSubscription(makeMockSuccessLink(), {
next({ data }) {
expect(data).toEqual({ foo: { id: 1 } });
},
- error: () => done.fail('Should not happen'),
- complete: () => done(),
+ error: () => {
+ throw new Error('Should not happen');
+ },
});
});
- it('forwards GraphQL errors', (done) => {
+ it('forwards GraphQL errors', () => {
createSubscription(makeMockGraphQLErrorLink(), {
next({ errors }) {
expect(errors).toEqual([{ message: 'foo' }]);
},
- error: () => done.fail('Should not happen'),
- complete: () => done(),
+ error: () => {
+ throw new Error('Should not happen');
+ },
});
});
- it('forwards network errors', (done) => {
+ it('forwards network errors', () => {
createSubscription(makeMockNetworkErrorLink(), {
- next: () => done.fail('Should not happen'),
+ next: () => {
+ throw new Error('Should not happen');
+ },
error: (error) => {
expect(error.message).toBe('NetworkError');
- done();
},
- complete: () => done.fail('Should not happen'),
+ complete: () => {
+ throw new Error('Should not happen');
+ },
});
});
});
@@ -119,23 +124,25 @@ describe('getSuppressNetworkErrorsDuringNavigationLink', () => {
isNavigatingAway.mockReturnValue(true);
});
- it('forwards successful requests', (done) => {
+ it('forwards successful requests', () => {
createSubscription(makeMockSuccessLink(), {
next({ data }) {
expect(data).toEqual({ foo: { id: 1 } });
},
- error: () => done.fail('Should not happen'),
- complete: () => done(),
+ error: () => {
+ throw new Error('Should not happen');
+ },
});
});
- it('forwards GraphQL errors', (done) => {
+ it('forwards GraphQL errors', () => {
createSubscription(makeMockGraphQLErrorLink(), {
next({ errors }) {
expect(errors).toEqual([{ message: 'foo' }]);
},
- error: () => done.fail('Should not happen'),
- complete: () => done(),
+ error: () => {
+ throw new Error('Should not happen');
+ },
});
});
});
diff --git a/spec/frontend/lib/gfm/index_spec.js b/spec/frontend/lib/gfm/index_spec.js
new file mode 100644
index 00000000000..5c72b5a51a7
--- /dev/null
+++ b/spec/frontend/lib/gfm/index_spec.js
@@ -0,0 +1,46 @@
+import { render } from '~/lib/gfm';
+
+describe('gfm', () => {
+ describe('render', () => {
+ it('processes Commonmark and provides an ast to the renderer function', async () => {
+ let result;
+
+ await render({
+ markdown: 'This is text',
+ renderer: (tree) => {
+ result = tree;
+ },
+ });
+
+ expect(result.type).toBe('root');
+ });
+
+ it('transforms raw HTML into individual nodes in the AST', async () => {
+ let result;
+
+ await render({
+ markdown: '<strong>This is bold text</strong>',
+ renderer: (tree) => {
+ result = tree;
+ },
+ });
+
+ expect(result.children[0].children[0]).toMatchObject({
+ type: 'element',
+ tagName: 'strong',
+ properties: {},
+ });
+ });
+
+ it('returns the result of executing the renderer function', async () => {
+ const result = await render({
+ markdown: '<strong>This is bold text</strong>',
+ renderer: () => {
+ return 'rendered tree';
+ },
+ });
+
+ expect(result).toBe('rendered tree');
+ });
+ });
+});
diff --git a/spec/frontend/lib/utils/apollo_startup_js_link_spec.js b/spec/frontend/lib/utils/apollo_startup_js_link_spec.js
index e58bc063004..06573f346e0 100644
--- a/spec/frontend/lib/utils/apollo_startup_js_link_spec.js
+++ b/spec/frontend/lib/utils/apollo_startup_js_link_spec.js
@@ -58,17 +58,16 @@ describe('StartupJSLink', () => {
link = ApolloLink.from([startupLink, new ApolloLink(() => Observable.of(FORWARDED_RESPONSE))]);
};
- it('forwards requests if no calls are set up', (done) => {
+ it('forwards requests if no calls are set up', () => {
setupLink();
link.request(mockOperation()).subscribe((result) => {
expect(result).toEqual(FORWARDED_RESPONSE);
expect(startupLink.startupCalls).toBe(null);
expect(startupLink.request).toEqual(StartupJSLink.noopRequest);
- done();
});
});
- it('forwards requests if the operation is not pre-loaded', (done) => {
+ it('forwards requests if the operation is not pre-loaded', () => {
window.gl = {
startup_graphql_calls: [
{
@@ -82,12 +81,11 @@ describe('StartupJSLink', () => {
link.request(mockOperation({ operationName: 'notLoaded' })).subscribe((result) => {
expect(result).toEqual(FORWARDED_RESPONSE);
expect(startupLink.startupCalls.size).toBe(1);
- done();
});
});
describe('variable match errors: ', () => {
- it('forwards requests if the variables are not matching', (done) => {
+ it('forwards requests if the variables are not matching', () => {
window.gl = {
startup_graphql_calls: [
{
@@ -101,11 +99,10 @@ describe('StartupJSLink', () => {
link.request(mockOperation()).subscribe((result) => {
expect(result).toEqual(FORWARDED_RESPONSE);
expect(startupLink.startupCalls.size).toBe(0);
- done();
});
});
- it('forwards requests if more variables are set in the operation', (done) => {
+ it('forwards requests if more variables are set in the operation', () => {
window.gl = {
startup_graphql_calls: [
{
@@ -118,11 +115,10 @@ describe('StartupJSLink', () => {
link.request(mockOperation()).subscribe((result) => {
expect(result).toEqual(FORWARDED_RESPONSE);
expect(startupLink.startupCalls.size).toBe(0);
- done();
});
});
- it('forwards requests if less variables are set in the operation', (done) => {
+ it('forwards requests if less variables are set in the operation', () => {
window.gl = {
startup_graphql_calls: [
{
@@ -136,11 +132,10 @@ describe('StartupJSLink', () => {
link.request(mockOperation({ variables: { id: 3 } })).subscribe((result) => {
expect(result).toEqual(FORWARDED_RESPONSE);
expect(startupLink.startupCalls.size).toBe(0);
- done();
});
});
- it('forwards requests if different variables are set', (done) => {
+ it('forwards requests if different variables are set', () => {
window.gl = {
startup_graphql_calls: [
{
@@ -154,11 +149,10 @@ describe('StartupJSLink', () => {
link.request(mockOperation({ variables: { id: 3 } })).subscribe((result) => {
expect(result).toEqual(FORWARDED_RESPONSE);
expect(startupLink.startupCalls.size).toBe(0);
- done();
});
});
- it('forwards requests if array variables have a different order', (done) => {
+ it('forwards requests if array variables have a different order', () => {
window.gl = {
startup_graphql_calls: [
{
@@ -172,13 +166,12 @@ describe('StartupJSLink', () => {
link.request(mockOperation({ variables: { id: [4, 3] } })).subscribe((result) => {
expect(result).toEqual(FORWARDED_RESPONSE);
expect(startupLink.startupCalls.size).toBe(0);
- done();
});
});
});
describe('error handling', () => {
- it('forwards the call if the fetchCall is failing with a HTTP Error', (done) => {
+ it('forwards the call if the fetchCall is failing with a HTTP Error', () => {
window.gl = {
startup_graphql_calls: [
{
@@ -192,11 +185,10 @@ describe('StartupJSLink', () => {
link.request(mockOperation()).subscribe((result) => {
expect(result).toEqual(FORWARDED_RESPONSE);
expect(startupLink.startupCalls.size).toBe(0);
- done();
});
});
- it('forwards the call if it errors (e.g. failing JSON)', (done) => {
+ it('forwards the call if it errors (e.g. failing JSON)', () => {
window.gl = {
startup_graphql_calls: [
{
@@ -210,11 +202,10 @@ describe('StartupJSLink', () => {
link.request(mockOperation()).subscribe((result) => {
expect(result).toEqual(FORWARDED_RESPONSE);
expect(startupLink.startupCalls.size).toBe(0);
- done();
});
});
- it('forwards the call if the response contains an error', (done) => {
+ it('forwards the call if the response contains an error', () => {
window.gl = {
startup_graphql_calls: [
{
@@ -228,11 +219,10 @@ describe('StartupJSLink', () => {
link.request(mockOperation()).subscribe((result) => {
expect(result).toEqual(FORWARDED_RESPONSE);
expect(startupLink.startupCalls.size).toBe(0);
- done();
});
});
- it("forwards the call if the response doesn't contain a data object", (done) => {
+ it("forwards the call if the response doesn't contain a data object", () => {
window.gl = {
startup_graphql_calls: [
{
@@ -246,12 +236,11 @@ describe('StartupJSLink', () => {
link.request(mockOperation()).subscribe((result) => {
expect(result).toEqual(FORWARDED_RESPONSE);
expect(startupLink.startupCalls.size).toBe(0);
- done();
});
});
});
- it('resolves the request if the operation is matching', (done) => {
+ it('resolves the request if the operation is matching', () => {
window.gl = {
startup_graphql_calls: [
{
@@ -265,11 +254,10 @@ describe('StartupJSLink', () => {
link.request(mockOperation()).subscribe((result) => {
expect(result).toEqual(STARTUP_JS_RESPONSE);
expect(startupLink.startupCalls.size).toBe(0);
- done();
});
});
- it('resolves the request exactly once', (done) => {
+ it('resolves the request exactly once', () => {
window.gl = {
startup_graphql_calls: [
{
@@ -285,12 +273,11 @@ describe('StartupJSLink', () => {
expect(startupLink.startupCalls.size).toBe(0);
link.request(mockOperation()).subscribe((result2) => {
expect(result2).toEqual(FORWARDED_RESPONSE);
- done();
});
});
});
- it('resolves the request if the variables have a different order', (done) => {
+ it('resolves the request if the variables have a different order', () => {
window.gl = {
startup_graphql_calls: [
{
@@ -304,11 +291,10 @@ describe('StartupJSLink', () => {
link.request(mockOperation({ variables: { name: 'foo', id: 3 } })).subscribe((result) => {
expect(result).toEqual(STARTUP_JS_RESPONSE);
expect(startupLink.startupCalls.size).toBe(0);
- done();
});
});
- it('resolves the request if the variables have undefined values', (done) => {
+ it('resolves the request if the variables have undefined values', () => {
window.gl = {
startup_graphql_calls: [
{
@@ -324,11 +310,10 @@ describe('StartupJSLink', () => {
.subscribe((result) => {
expect(result).toEqual(STARTUP_JS_RESPONSE);
expect(startupLink.startupCalls.size).toBe(0);
- done();
});
});
- it('resolves the request if the variables are of an array format', (done) => {
+ it('resolves the request if the variables are of an array format', () => {
window.gl = {
startup_graphql_calls: [
{
@@ -342,11 +327,10 @@ describe('StartupJSLink', () => {
link.request(mockOperation({ variables: { id: [3, 4] } })).subscribe((result) => {
expect(result).toEqual(STARTUP_JS_RESPONSE);
expect(startupLink.startupCalls.size).toBe(0);
- done();
});
});
- it('resolves multiple requests correctly', (done) => {
+ it('resolves multiple requests correctly', () => {
window.gl = {
startup_graphql_calls: [
{
@@ -368,7 +352,6 @@ describe('StartupJSLink', () => {
link.request(mockOperation({ operationName: OPERATION_NAME })).subscribe((result2) => {
expect(result2).toEqual(STARTUP_JS_RESPONSE);
expect(startupLink.startupCalls.size).toBe(0);
- done();
});
});
});
diff --git a/spec/frontend/lib/utils/common_utils_spec.js b/spec/frontend/lib/utils/common_utils_spec.js
index 0be0bf89210..763a9bd30fe 100644
--- a/spec/frontend/lib/utils/common_utils_spec.js
+++ b/spec/frontend/lib/utils/common_utils_spec.js
@@ -266,15 +266,18 @@ describe('common_utils', () => {
});
describe('debounceByAnimationFrame', () => {
- it('debounces a function to allow a maximum of one call per animation frame', (done) => {
+ it('debounces a function to allow a maximum of one call per animation frame', () => {
const spy = jest.fn();
const debouncedSpy = commonUtils.debounceByAnimationFrame(spy);
- window.requestAnimationFrame(() => {
- debouncedSpy();
- debouncedSpy();
+
+ return new Promise((resolve) => {
window.requestAnimationFrame(() => {
- expect(spy).toHaveBeenCalledTimes(1);
- done();
+ debouncedSpy();
+ debouncedSpy();
+ window.requestAnimationFrame(() => {
+ expect(spy).toHaveBeenCalledTimes(1);
+ resolve();
+ });
});
});
});
@@ -372,28 +375,24 @@ describe('common_utils', () => {
jest.spyOn(window, 'setTimeout');
});
- it('solves the promise from the callback', (done) => {
+ it('solves the promise from the callback', () => {
const expectedResponseValue = 'Success!';
- commonUtils
+ return commonUtils
.backOff((next, stop) =>
new Promise((resolve) => {
resolve(expectedResponseValue);
- })
- .then((resp) => {
- stop(resp);
- })
- .catch(done.fail),
+ }).then((resp) => {
+ stop(resp);
+ }),
)
.then((respBackoff) => {
expect(respBackoff).toBe(expectedResponseValue);
- done();
- })
- .catch(done.fail);
+ });
});
- it('catches the rejected promise from the callback ', (done) => {
+ it('catches the rejected promise from the callback ', () => {
const errorMessage = 'Mistakes were made!';
- commonUtils
+ return commonUtils
.backOff((next, stop) => {
new Promise((resolve, reject) => {
reject(new Error(errorMessage));
@@ -406,39 +405,34 @@ describe('common_utils', () => {
.catch((errBackoffResp) => {
expect(errBackoffResp instanceof Error).toBe(true);
expect(errBackoffResp.message).toBe(errorMessage);
- done();
});
});
- it('solves the promise correctly after retrying a third time', (done) => {
+ it('solves the promise correctly after retrying a third time', () => {
let numberOfCalls = 1;
const expectedResponseValue = 'Success!';
- commonUtils
+ return commonUtils
.backOff((next, stop) =>
- Promise.resolve(expectedResponseValue)
- .then((resp) => {
- if (numberOfCalls < 3) {
- numberOfCalls += 1;
- next();
- jest.runOnlyPendingTimers();
- } else {
- stop(resp);
- }
- })
- .catch(done.fail),
+ Promise.resolve(expectedResponseValue).then((resp) => {
+ if (numberOfCalls < 3) {
+ numberOfCalls += 1;
+ next();
+ jest.runOnlyPendingTimers();
+ } else {
+ stop(resp);
+ }
+ }),
)
.then((respBackoff) => {
const timeouts = window.setTimeout.mock.calls.map(([, timeout]) => timeout);
expect(timeouts).toEqual([2000, 4000]);
expect(respBackoff).toBe(expectedResponseValue);
- done();
- })
- .catch(done.fail);
+ });
});
- it('rejects the backOff promise after timing out', (done) => {
- commonUtils
+ it('rejects the backOff promise after timing out', () => {
+ return commonUtils
.backOff((next) => {
next();
jest.runOnlyPendingTimers();
@@ -449,7 +443,6 @@ describe('common_utils', () => {
expect(timeouts).toEqual([2000, 4000, 8000, 16000, 32000, 32000]);
expect(errBackoffResp instanceof Error).toBe(true);
expect(errBackoffResp.message).toBe('BACKOFF_TIMEOUT');
- done();
});
});
});
diff --git a/spec/frontend/lib/utils/confirm_via_gl_modal/confirm_modal_spec.js b/spec/frontend/lib/utils/confirm_via_gl_modal/confirm_modal_spec.js
index e06d1384610..d6131b1a1d7 100644
--- a/spec/frontend/lib/utils/confirm_via_gl_modal/confirm_modal_spec.js
+++ b/spec/frontend/lib/utils/confirm_via_gl_modal/confirm_modal_spec.js
@@ -5,12 +5,23 @@ import ConfirmModal from '~/lib/utils/confirm_via_gl_modal/confirm_modal.vue';
describe('Confirm Modal', () => {
let wrapper;
let modal;
+ const SECONDARY_TEXT = 'secondaryText';
+ const SECONDARY_VARIANT = 'danger';
- const createComponent = ({ primaryText, primaryVariant, title, hideCancel = false } = {}) => {
+ const createComponent = ({
+ primaryText,
+ primaryVariant,
+ secondaryText,
+ secondaryVariant,
+ title,
+ hideCancel = false,
+ } = {}) => {
wrapper = mount(ConfirmModal, {
propsData: {
primaryText,
primaryVariant,
+ secondaryText,
+ secondaryVariant,
hideCancel,
title,
},
@@ -65,6 +76,19 @@ describe('Confirm Modal', () => {
expect(props.actionCancel).toBeNull();
});
+ it('should not show secondary Button when secondary Text is not set', () => {
+ createComponent();
+ const props = findGlModal().props();
+ expect(props.actionSecondary).toBeNull();
+ });
+
+ it('should show secondary Button when secondaryText is set', () => {
+ createComponent({ secondaryText: SECONDARY_TEXT, secondaryVariant: SECONDARY_VARIANT });
+ const actionSecondary = findGlModal().props('actionSecondary');
+ expect(actionSecondary.text).toEqual(SECONDARY_TEXT);
+ expect(actionSecondary.attributes.variant).toEqual(SECONDARY_VARIANT);
+ });
+
it('should set the modal title when the `title` prop is set', () => {
const title = 'Modal title';
createComponent({ title });
diff --git a/spec/frontend/lib/utils/datetime/date_calculation_utility_spec.js b/spec/frontend/lib/utils/datetime/date_calculation_utility_spec.js
new file mode 100644
index 00000000000..47bb512cbb5
--- /dev/null
+++ b/spec/frontend/lib/utils/datetime/date_calculation_utility_spec.js
@@ -0,0 +1,17 @@
+import { newDateAsLocaleTime } from '~/lib/utils/datetime/date_calculation_utility';
+
+describe('newDateAsLocaleTime', () => {
+ it.each`
+ string | expected
+ ${'2022-03-22'} | ${new Date('2022-03-22T00:00:00.000Z')}
+ ${'2022-03-22T00:00:00.000Z'} | ${new Date('2022-03-22T00:00:00.000Z')}
+ ${2022} | ${null}
+ ${[]} | ${null}
+ ${{}} | ${null}
+ ${true} | ${null}
+ ${null} | ${null}
+ ${undefined} | ${null}
+ `('returns $expected given $string', ({ string, expected }) => {
+ expect(newDateAsLocaleTime(string)).toEqual(expected);
+ });
+});
diff --git a/spec/frontend/lib/utils/datetime/date_format_utility_spec.js b/spec/frontend/lib/utils/datetime/date_format_utility_spec.js
index 1adc70450e8..018ae12c908 100644
--- a/spec/frontend/lib/utils/datetime/date_format_utility_spec.js
+++ b/spec/frontend/lib/utils/datetime/date_format_utility_spec.js
@@ -133,3 +133,15 @@ describe('formatTimeAsSummary', () => {
expect(utils.formatTimeAsSummary({ [unit]: value })).toBe(result);
});
});
+
+describe('durationTimeFormatted', () => {
+ it.each`
+ duration | expectedOutput
+ ${87} | ${'00:01:27'}
+ ${141} | ${'00:02:21'}
+ ${12} | ${'00:00:12'}
+ ${60} | ${'00:01:00'}
+ `('returns $expectedOutput when provided $duration', ({ duration, expectedOutput }) => {
+ expect(utils.durationTimeFormatted(duration)).toBe(expectedOutput);
+ });
+});
diff --git a/spec/frontend/lib/utils/datetime/timeago_utility_spec.js b/spec/frontend/lib/utils/datetime/timeago_utility_spec.js
index 2314ec678d3..1ef7047d959 100644
--- a/spec/frontend/lib/utils/datetime/timeago_utility_spec.js
+++ b/spec/frontend/lib/utils/datetime/timeago_utility_spec.js
@@ -1,4 +1,4 @@
-import { getTimeago, localTimeAgo, timeFor } from '~/lib/utils/datetime/timeago_utility';
+import { getTimeago, localTimeAgo, timeFor, duration } from '~/lib/utils/datetime/timeago_utility';
import { s__ } from '~/locale';
import '~/commons/bootstrap';
@@ -66,6 +66,54 @@ describe('TimeAgo utils', () => {
});
});
+ describe('duration', () => {
+ const ONE_DAY = 24 * 60 * 60;
+
+ it.each`
+ secs | formatted
+ ${0} | ${'0 seconds'}
+ ${30} | ${'30 seconds'}
+ ${59} | ${'59 seconds'}
+ ${60} | ${'1 minute'}
+ ${-60} | ${'1 minute'}
+ ${2 * 60} | ${'2 minutes'}
+ ${60 * 60} | ${'1 hour'}
+ ${2 * 60 * 60} | ${'2 hours'}
+ ${ONE_DAY} | ${'1 day'}
+ ${2 * ONE_DAY} | ${'2 days'}
+ ${7 * ONE_DAY} | ${'1 week'}
+ ${14 * ONE_DAY} | ${'2 weeks'}
+ ${31 * ONE_DAY} | ${'1 month'}
+ ${61 * ONE_DAY} | ${'2 months'}
+ ${365 * ONE_DAY} | ${'1 year'}
+ ${365 * 2 * ONE_DAY} | ${'2 years'}
+ `('formats $secs as "$formatted"', ({ secs, formatted }) => {
+ const ms = secs * 1000;
+
+ expect(duration(ms)).toBe(formatted);
+ });
+
+ // `duration` can be used to format Rails month durations.
+ // Ensure formatting for quantities such as `2.months.to_i`
+ // based on ActiveSupport::Duration::SECONDS_PER_MONTH.
+ // See: https://api.rubyonrails.org/classes/ActiveSupport/Duration.html
+ const SECONDS_PER_MONTH = 2629746; // 1.month.to_i
+
+ it.each`
+ duration | secs | formatted
+ ${'1.month'} | ${SECONDS_PER_MONTH} | ${'1 month'}
+ ${'2.months'} | ${SECONDS_PER_MONTH * 2} | ${'2 months'}
+ ${'3.months'} | ${SECONDS_PER_MONTH * 3} | ${'3 months'}
+ `(
+ 'formats ActiveSupport::Duration of `$duration` ($secs) as "$formatted"',
+ ({ secs, formatted }) => {
+ const ms = secs * 1000;
+
+ expect(duration(ms)).toBe(formatted);
+ },
+ );
+ });
+
describe('localTimeAgo', () => {
beforeEach(() => {
document.body.innerHTML =
diff --git a/spec/frontend/lib/utils/poll_spec.js b/spec/frontend/lib/utils/poll_spec.js
index 861808e3ad8..1f150599983 100644
--- a/spec/frontend/lib/utils/poll_spec.js
+++ b/spec/frontend/lib/utils/poll_spec.js
@@ -50,58 +50,48 @@ describe('Poll', () => {
};
});
- it('calls the success callback when no header for interval is provided', (done) => {
+ it('calls the success callback when no header for interval is provided', () => {
mockServiceCall({ status: 200 });
setup();
- waitForAllCallsToFinish(1, () => {
+ return waitForAllCallsToFinish(1, () => {
expect(callbacks.success).toHaveBeenCalled();
expect(callbacks.error).not.toHaveBeenCalled();
-
- done();
});
});
- it('calls the error callback when the http request returns an error', (done) => {
+ it('calls the error callback when the http request returns an error', () => {
mockServiceCall({ status: 500 }, true);
setup();
- waitForAllCallsToFinish(1, () => {
+ return waitForAllCallsToFinish(1, () => {
expect(callbacks.success).not.toHaveBeenCalled();
expect(callbacks.error).toHaveBeenCalled();
-
- done();
});
});
- it('skips the error callback when request is aborted', (done) => {
+ it('skips the error callback when request is aborted', () => {
mockServiceCall({ status: 0 }, true);
setup();
- waitForAllCallsToFinish(1, () => {
+ return waitForAllCallsToFinish(1, () => {
expect(callbacks.success).not.toHaveBeenCalled();
expect(callbacks.error).not.toHaveBeenCalled();
expect(callbacks.notification).toHaveBeenCalled();
-
- done();
});
});
- it('should call the success callback when the interval header is -1', (done) => {
+ it('should call the success callback when the interval header is -1', () => {
mockServiceCall({ status: 200, headers: { 'poll-interval': -1 } });
- setup()
- .then(() => {
- expect(callbacks.success).toHaveBeenCalled();
- expect(callbacks.error).not.toHaveBeenCalled();
-
- done();
- })
- .catch(done.fail);
+ return setup().then(() => {
+ expect(callbacks.success).toHaveBeenCalled();
+ expect(callbacks.error).not.toHaveBeenCalled();
+ });
});
describe('for 2xx status code', () => {
successCodes.forEach((httpCode) => {
- it(`starts polling when http status is ${httpCode} and interval header is provided`, (done) => {
+ it(`starts polling when http status is ${httpCode} and interval header is provided`, () => {
mockServiceCall({ status: httpCode, headers: { 'poll-interval': 1 } });
const Polling = new Poll({
@@ -114,22 +104,20 @@ describe('Poll', () => {
Polling.makeRequest();
- waitForAllCallsToFinish(2, () => {
+ return waitForAllCallsToFinish(2, () => {
Polling.stop();
expect(service.fetch.mock.calls).toHaveLength(2);
expect(service.fetch).toHaveBeenCalledWith({ page: 1 });
expect(callbacks.success).toHaveBeenCalled();
expect(callbacks.error).not.toHaveBeenCalled();
-
- done();
});
});
});
});
describe('with delayed initial request', () => {
- it('delays the first request', async (done) => {
+ it('delays the first request', async () => {
mockServiceCall({ status: 200, headers: { 'poll-interval': 1 } });
const Polling = new Poll({
@@ -144,21 +132,19 @@ describe('Poll', () => {
expect(Polling.timeoutID).toBeTruthy();
- waitForAllCallsToFinish(2, () => {
+ return waitForAllCallsToFinish(2, () => {
Polling.stop();
expect(service.fetch.mock.calls).toHaveLength(2);
expect(service.fetch).toHaveBeenCalledWith({ page: 1 });
expect(callbacks.success).toHaveBeenCalled();
expect(callbacks.error).not.toHaveBeenCalled();
-
- done();
});
});
});
describe('stop', () => {
- it('stops polling when method is called', (done) => {
+ it('stops polling when method is called', () => {
mockServiceCall({ status: 200, headers: { 'poll-interval': 1 } });
const Polling = new Poll({
@@ -175,18 +161,16 @@ describe('Poll', () => {
Polling.makeRequest();
- waitForAllCallsToFinish(1, () => {
+ return waitForAllCallsToFinish(1, () => {
expect(service.fetch.mock.calls).toHaveLength(1);
expect(service.fetch).toHaveBeenCalledWith({ page: 1 });
expect(Polling.stop).toHaveBeenCalled();
-
- done();
});
});
});
describe('enable', () => {
- it('should enable polling upon a response', (done) => {
+ it('should enable polling upon a response', () => {
mockServiceCall({ status: 200 });
const Polling = new Poll({
resource: service,
@@ -200,19 +184,18 @@ describe('Poll', () => {
response: { status: 200, headers: { 'poll-interval': 1 } },
});
- waitForAllCallsToFinish(1, () => {
+ return waitForAllCallsToFinish(1, () => {
Polling.stop();
expect(service.fetch.mock.calls).toHaveLength(1);
expect(service.fetch).toHaveBeenCalledWith({ page: 4 });
expect(Polling.options.data).toEqual({ page: 4 });
- done();
});
});
});
describe('restart', () => {
- it('should restart polling when its called', (done) => {
+ it('should restart polling when its called', () => {
mockServiceCall({ status: 200, headers: { 'poll-interval': 1 } });
const Polling = new Poll({
@@ -238,7 +221,7 @@ describe('Poll', () => {
Polling.makeRequest();
- waitForAllCallsToFinish(2, () => {
+ return waitForAllCallsToFinish(2, () => {
Polling.stop();
expect(service.fetch.mock.calls).toHaveLength(2);
@@ -247,7 +230,6 @@ describe('Poll', () => {
expect(Polling.enable).toHaveBeenCalled();
expect(Polling.restart).toHaveBeenCalled();
expect(Polling.options.data).toEqual({ page: 4 });
- done();
});
});
});
diff --git a/spec/frontend/lib/utils/text_markdown_spec.js b/spec/frontend/lib/utils/text_markdown_spec.js
index a5877aa6e3e..103305f0797 100644
--- a/spec/frontend/lib/utils/text_markdown_spec.js
+++ b/spec/frontend/lib/utils/text_markdown_spec.js
@@ -178,12 +178,23 @@ describe('init markdown', () => {
it.each`
text | expected
${'- item'} | ${'- item\n- '}
+ ${'* item'} | ${'* item\n* '}
+ ${'+ item'} | ${'+ item\n+ '}
${'- [ ] item'} | ${'- [ ] item\n- [ ] '}
- ${'- [x] item'} | ${'- [x] item\n- [x] '}
+ ${'- [x] item'} | ${'- [x] item\n- [ ] '}
+ ${'- [X] item'} | ${'- [X] item\n- [ ] '}
+ ${'- [ ] nbsp (U+00A0)'} | ${'- [ ] nbsp (U+00A0)\n- [ ] '}
${'- item\n - second'} | ${'- item\n - second\n - '}
+ ${'- - -'} | ${'- - -'}
+ ${'- --'} | ${'- --'}
+ ${'* **'} | ${'* **'}
+ ${' ** * ** * ** * **'} | ${' ** * ** * ** * **'}
+ ${'- - -x'} | ${'- - -x\n- '}
+ ${'+ ++'} | ${'+ ++\n+ '}
${'1. item'} | ${'1. item\n2. '}
${'1. [ ] item'} | ${'1. [ ] item\n2. [ ] '}
- ${'1. [x] item'} | ${'1. [x] item\n2. [x] '}
+ ${'1. [x] item'} | ${'1. [x] item\n2. [ ] '}
+ ${'1. [X] item'} | ${'1. [X] item\n2. [ ] '}
${'108. item'} | ${'108. item\n109. '}
${'108. item\n - second'} | ${'108. item\n - second\n - '}
${'108. item\n 1. second'} | ${'108. item\n 1. second\n 2. '}
@@ -207,10 +218,12 @@ describe('init markdown', () => {
${'- item\n- '} | ${'- item\n'}
${'- [ ] item\n- [ ] '} | ${'- [ ] item\n'}
${'- [x] item\n- [x] '} | ${'- [x] item\n'}
+ ${'- [X] item\n- [X] '} | ${'- [X] item\n'}
${'- item\n - second\n - '} | ${'- item\n - second\n'}
${'1. item\n2. '} | ${'1. item\n'}
${'1. [ ] item\n2. [ ] '} | ${'1. [ ] item\n'}
${'1. [x] item\n2. [x] '} | ${'1. [x] item\n'}
+ ${'1. [X] item\n2. [X] '} | ${'1. [X] item\n'}
${'108. item\n109. '} | ${'108. item\n'}
${'108. item\n - second\n - '} | ${'108. item\n - second\n'}
${'108. item\n 1. second\n 1. '} | ${'108. item\n 1. second\n'}
diff --git a/spec/frontend/lib/utils/unit_format/formatter_factory_spec.js b/spec/frontend/lib/utils/unit_format/formatter_factory_spec.js
index 0ca70e0a77e..9632d0f98f4 100644
--- a/spec/frontend/lib/utils/unit_format/formatter_factory_spec.js
+++ b/spec/frontend/lib/utils/unit_format/formatter_factory_spec.js
@@ -31,12 +31,17 @@ describe('unit_format/formatter_factory', () => {
expect(formatNumber(12.345, 4)).toBe('12.3450');
});
- it('formats a large integer with a length limit', () => {
+ it('formats a large integer with a max length - using legacy positional argument', () => {
expect(formatNumber(10 ** 7, undefined)).toBe('10,000,000');
expect(formatNumber(10 ** 7, undefined, 9)).toBe('1.00e+7');
expect(formatNumber(10 ** 7, undefined, 10)).toBe('10,000,000');
});
+ it('formats a large integer with a max length', () => {
+ expect(formatNumber(10 ** 7, undefined, { maxLength: 9 })).toBe('1.00e+7');
+ expect(formatNumber(10 ** 7, undefined, { maxLength: 10 })).toBe('10,000,000');
+ });
+
describe('formats with a different locale', () => {
let originalLang;
@@ -92,7 +97,7 @@ describe('unit_format/formatter_factory', () => {
expect(formatSuffix(-1000000)).toBe('-1,000,000pop.');
});
- it('formats a floating point nugative number', () => {
+ it('formats a floating point negative number', () => {
expect(formatSuffix(-0.1)).toBe('-0.1pop.');
expect(formatSuffix(-0.1, 0)).toBe('-0pop.');
expect(formatSuffix(-0.1, 2)).toBe('-0.10pop.');
@@ -108,10 +113,20 @@ describe('unit_format/formatter_factory', () => {
expect(formatSuffix(10 ** 10)).toBe('10,000,000,000pop.');
});
- it('formats a large integer with a length limit', () => {
+ it('formats using a unit separator', () => {
+ expect(formatSuffix(10, 0, { unitSeparator: ' ' })).toBe('10 pop.');
+ expect(formatSuffix(10, 0, { unitSeparator: ' x ' })).toBe('10 x pop.');
+ });
+
+ it('formats a large integer with a max length - using legacy positional argument', () => {
expect(formatSuffix(10 ** 7, undefined, 10)).toBe('1.00e+7pop.');
expect(formatSuffix(10 ** 10, undefined, 10)).toBe('1.00e+10pop.');
});
+
+ it('formats a large integer with a max length', () => {
+ expect(formatSuffix(10 ** 7, undefined, { maxLength: 10 })).toBe('1.00e+7pop.');
+ expect(formatSuffix(10 ** 10, undefined, { maxLength: 10 })).toBe('1.00e+10pop.');
+ });
});
describe('scaledSIFormatter', () => {
@@ -143,6 +158,10 @@ describe('unit_format/formatter_factory', () => {
expect(formatGibibytes(10 ** 10)).toBe('10GB');
expect(formatGibibytes(10 ** 11)).toBe('100GB');
});
+
+ it('formats bytes using a unit separator', () => {
+ expect(formatGibibytes(1, 0, { unitSeparator: ' ' })).toBe('1 B');
+ });
});
describe('scaled format with offset', () => {
@@ -174,6 +193,19 @@ describe('unit_format/formatter_factory', () => {
expect(formatGigaBytes(10 ** 9)).toBe('1EB');
});
+ it('formats bytes using a unit separator', () => {
+ expect(formatGigaBytes(1, undefined, { unitSeparator: ' ' })).toBe('1 GB');
+ });
+
+ it('formats long byte numbers with max length - using legacy positional argument', () => {
+ expect(formatGigaBytes(1, 8, 7)).toBe('1.00e+0GB');
+ });
+
+ it('formats long byte numbers with max length', () => {
+ expect(formatGigaBytes(1, 8)).toBe('1.00000000GB');
+ expect(formatGigaBytes(1, 8, { maxLength: 7 })).toBe('1.00e+0GB');
+ });
+
it('formatting of too large numbers is not suported', () => {
// formatting YB is out of range
expect(() => scaledSIFormatter('B', 9)).toThrow();
@@ -216,6 +248,10 @@ describe('unit_format/formatter_factory', () => {
expect(formatMilligrams(-100)).toBe('-100mg');
expect(formatMilligrams(-(10 ** 4))).toBe('-10g');
});
+
+ it('formats using a unit separator', () => {
+ expect(formatMilligrams(1, undefined, { unitSeparator: ' ' })).toBe('1 mg');
+ });
});
});
@@ -253,6 +289,10 @@ describe('unit_format/formatter_factory', () => {
expect(formatScaledBin(10 * 1024 ** 3)).toBe('10GiB');
expect(formatScaledBin(100 * 1024 ** 3)).toBe('100GiB');
});
+
+ it('formats using a unit separator', () => {
+ expect(formatScaledBin(1, undefined, { unitSeparator: ' ' })).toBe('1 B');
+ });
});
describe('scaled format with offset', () => {
@@ -288,6 +328,10 @@ describe('unit_format/formatter_factory', () => {
expect(formatGibibytes(100 * 1024 ** 3)).toBe('100EiB');
});
+ it('formats using a unit separator', () => {
+ expect(formatGibibytes(1, undefined, { unitSeparator: ' ' })).toBe('1 GiB');
+ });
+
it('formatting of too large numbers is not suported', () => {
// formatting YB is out of range
expect(() => scaledBinaryFormatter('B', 9)).toThrow();
diff --git a/spec/frontend/lib/utils/unit_format/index_spec.js b/spec/frontend/lib/utils/unit_format/index_spec.js
index 7fd273f1b58..dc9d6ece48e 100644
--- a/spec/frontend/lib/utils/unit_format/index_spec.js
+++ b/spec/frontend/lib/utils/unit_format/index_spec.js
@@ -74,10 +74,13 @@ describe('unit_format', () => {
it('seconds', () => {
expect(seconds(1)).toBe('1s');
+ expect(seconds(1, undefined, { unitSeparator: ' ' })).toBe('1 s');
});
it('milliseconds', () => {
expect(milliseconds(1)).toBe('1ms');
+ expect(milliseconds(1, undefined, { unitSeparator: ' ' })).toBe('1 ms');
+
expect(milliseconds(100)).toBe('100ms');
expect(milliseconds(1000)).toBe('1,000ms');
expect(milliseconds(10_000)).toBe('10,000ms');
@@ -87,6 +90,7 @@ describe('unit_format', () => {
it('decimalBytes', () => {
expect(decimalBytes(1)).toBe('1B');
expect(decimalBytes(1, 1)).toBe('1.0B');
+ expect(decimalBytes(1, 1, { unitSeparator: ' ' })).toBe('1.0 B');
expect(decimalBytes(10)).toBe('10B');
expect(decimalBytes(10 ** 2)).toBe('100B');
@@ -104,31 +108,37 @@ describe('unit_format', () => {
it('kilobytes', () => {
expect(kilobytes(1)).toBe('1kB');
expect(kilobytes(1, 1)).toBe('1.0kB');
+ expect(kilobytes(1, 1, { unitSeparator: ' ' })).toBe('1.0 kB');
});
it('megabytes', () => {
expect(megabytes(1)).toBe('1MB');
expect(megabytes(1, 1)).toBe('1.0MB');
+ expect(megabytes(1, 1, { unitSeparator: ' ' })).toBe('1.0 MB');
});
it('gigabytes', () => {
expect(gigabytes(1)).toBe('1GB');
expect(gigabytes(1, 1)).toBe('1.0GB');
+ expect(gigabytes(1, 1, { unitSeparator: ' ' })).toBe('1.0 GB');
});
it('terabytes', () => {
expect(terabytes(1)).toBe('1TB');
expect(terabytes(1, 1)).toBe('1.0TB');
+ expect(terabytes(1, 1, { unitSeparator: ' ' })).toBe('1.0 TB');
});
it('petabytes', () => {
expect(petabytes(1)).toBe('1PB');
expect(petabytes(1, 1)).toBe('1.0PB');
+ expect(petabytes(1, 1, { unitSeparator: ' ' })).toBe('1.0 PB');
});
it('bytes', () => {
expect(bytes(1)).toBe('1B');
expect(bytes(1, 1)).toBe('1.0B');
+ expect(bytes(1, 1, { unitSeparator: ' ' })).toBe('1.0 B');
expect(bytes(10)).toBe('10B');
expect(bytes(100)).toBe('100B');
@@ -142,26 +152,31 @@ describe('unit_format', () => {
it('kibibytes', () => {
expect(kibibytes(1)).toBe('1KiB');
expect(kibibytes(1, 1)).toBe('1.0KiB');
+ expect(kibibytes(1, 1, { unitSeparator: ' ' })).toBe('1.0 KiB');
});
it('mebibytes', () => {
expect(mebibytes(1)).toBe('1MiB');
expect(mebibytes(1, 1)).toBe('1.0MiB');
+ expect(mebibytes(1, 1, { unitSeparator: ' ' })).toBe('1.0 MiB');
});
it('gibibytes', () => {
expect(gibibytes(1)).toBe('1GiB');
expect(gibibytes(1, 1)).toBe('1.0GiB');
+ expect(gibibytes(1, 1, { unitSeparator: ' ' })).toBe('1.0 GiB');
});
it('tebibytes', () => {
expect(tebibytes(1)).toBe('1TiB');
expect(tebibytes(1, 1)).toBe('1.0TiB');
+ expect(tebibytes(1, 1, { unitSeparator: ' ' })).toBe('1.0 TiB');
});
it('pebibytes', () => {
expect(pebibytes(1)).toBe('1PiB');
expect(pebibytes(1, 1)).toBe('1.0PiB');
+ expect(pebibytes(1, 1, { unitSeparator: ' ' })).toBe('1.0 PiB');
});
describe('getFormatter', () => {
diff --git a/spec/frontend/lib/utils/users_cache_spec.js b/spec/frontend/lib/utils/users_cache_spec.js
index 4034f39ee9c..30bdddd8e73 100644
--- a/spec/frontend/lib/utils/users_cache_spec.js
+++ b/spec/frontend/lib/utils/users_cache_spec.js
@@ -93,7 +93,7 @@ describe('UsersCache', () => {
.mockImplementation((query, options) => apiSpy(query, options));
});
- it('stores and returns data from API call if cache is empty', (done) => {
+ it('stores and returns data from API call if cache is empty', async () => {
apiSpy = (query, options) => {
expect(query).toBe('');
expect(options).toEqual({
@@ -105,16 +105,12 @@ describe('UsersCache', () => {
});
};
- UsersCache.retrieve(dummyUsername)
- .then((user) => {
- expect(user).toBe(dummyUser);
- expect(UsersCache.internalStorage[dummyUsername]).toBe(dummyUser);
- })
- .then(done)
- .catch(done.fail);
+ const user = await UsersCache.retrieve(dummyUsername);
+ expect(user).toBe(dummyUser);
+ expect(UsersCache.internalStorage[dummyUsername]).toBe(dummyUser);
});
- it('returns undefined if Ajax call fails and cache is empty', (done) => {
+ it('returns undefined if Ajax call fails and cache is empty', async () => {
const dummyError = new Error('server exploded');
apiSpy = (query, options) => {
@@ -126,26 +122,18 @@ describe('UsersCache', () => {
return Promise.reject(dummyError);
};
- UsersCache.retrieve(dummyUsername)
- .then((user) => done.fail(`Received unexpected user: ${JSON.stringify(user)}`))
- .catch((error) => {
- expect(error).toBe(dummyError);
- })
- .then(done)
- .catch(done.fail);
+ await expect(UsersCache.retrieve(dummyUsername)).rejects.toEqual(dummyError);
});
- it('makes no Ajax call if matching data exists', (done) => {
+ it('makes no Ajax call if matching data exists', async () => {
UsersCache.internalStorage[dummyUsername] = dummyUser;
- apiSpy = () => done.fail(new Error('expected no Ajax call!'));
+ apiSpy = () => {
+ throw new Error('expected no Ajax call!');
+ };
- UsersCache.retrieve(dummyUsername)
- .then((user) => {
- expect(user).toBe(dummyUser);
- })
- .then(done)
- .catch(done.fail);
+ const user = await UsersCache.retrieve(dummyUsername);
+ expect(user).toBe(dummyUser);
});
});
@@ -156,7 +144,7 @@ describe('UsersCache', () => {
jest.spyOn(UserApi, 'getUser').mockImplementation((id) => apiSpy(id));
});
- it('stores and returns data from API call if cache is empty', (done) => {
+ it('stores and returns data from API call if cache is empty', async () => {
apiSpy = (id) => {
expect(id).toBe(dummyUserId);
@@ -165,16 +153,12 @@ describe('UsersCache', () => {
});
};
- UsersCache.retrieveById(dummyUserId)
- .then((user) => {
- expect(user).toBe(dummyUser);
- expect(UsersCache.internalStorage[dummyUserId]).toBe(dummyUser);
- })
- .then(done)
- .catch(done.fail);
+ const user = await UsersCache.retrieveById(dummyUserId);
+ expect(user).toBe(dummyUser);
+ expect(UsersCache.internalStorage[dummyUserId]).toBe(dummyUser);
});
- it('returns undefined if Ajax call fails and cache is empty', (done) => {
+ it('returns undefined if Ajax call fails and cache is empty', async () => {
const dummyError = new Error('server exploded');
apiSpy = (id) => {
@@ -183,26 +167,18 @@ describe('UsersCache', () => {
return Promise.reject(dummyError);
};
- UsersCache.retrieveById(dummyUserId)
- .then((user) => done.fail(`Received unexpected user: ${JSON.stringify(user)}`))
- .catch((error) => {
- expect(error).toBe(dummyError);
- })
- .then(done)
- .catch(done.fail);
+ await expect(UsersCache.retrieveById(dummyUserId)).rejects.toEqual(dummyError);
});
- it('makes no Ajax call if matching data exists', (done) => {
+ it('makes no Ajax call if matching data exists', async () => {
UsersCache.internalStorage[dummyUserId] = dummyUser;
- apiSpy = () => done.fail(new Error('expected no Ajax call!'));
+ apiSpy = () => {
+ throw new Error('expected no Ajax call!');
+ };
- UsersCache.retrieveById(dummyUserId)
- .then((user) => {
- expect(user).toBe(dummyUser);
- })
- .then(done)
- .catch(done.fail);
+ const user = await UsersCache.retrieveById(dummyUserId);
+ expect(user).toBe(dummyUser);
});
});
@@ -213,7 +189,7 @@ describe('UsersCache', () => {
jest.spyOn(UserApi, 'getUserStatus').mockImplementation((id) => apiSpy(id));
});
- it('stores and returns data from API call if cache is empty', (done) => {
+ it('stores and returns data from API call if cache is empty', async () => {
apiSpy = (id) => {
expect(id).toBe(dummyUserId);
@@ -222,16 +198,12 @@ describe('UsersCache', () => {
});
};
- UsersCache.retrieveStatusById(dummyUserId)
- .then((userStatus) => {
- expect(userStatus).toBe(dummyUserStatus);
- expect(UsersCache.internalStorage[dummyUserId].status).toBe(dummyUserStatus);
- })
- .then(done)
- .catch(done.fail);
+ const userStatus = await UsersCache.retrieveStatusById(dummyUserId);
+ expect(userStatus).toBe(dummyUserStatus);
+ expect(UsersCache.internalStorage[dummyUserId].status).toBe(dummyUserStatus);
});
- it('returns undefined if Ajax call fails and cache is empty', (done) => {
+ it('returns undefined if Ajax call fails and cache is empty', async () => {
const dummyError = new Error('server exploded');
apiSpy = (id) => {
@@ -240,28 +212,20 @@ describe('UsersCache', () => {
return Promise.reject(dummyError);
};
- UsersCache.retrieveStatusById(dummyUserId)
- .then((userStatus) => done.fail(`Received unexpected user: ${JSON.stringify(userStatus)}`))
- .catch((error) => {
- expect(error).toBe(dummyError);
- })
- .then(done)
- .catch(done.fail);
+ await expect(UsersCache.retrieveStatusById(dummyUserId)).rejects.toEqual(dummyError);
});
- it('makes no Ajax call if matching data exists', (done) => {
+ it('makes no Ajax call if matching data exists', async () => {
UsersCache.internalStorage[dummyUserId] = {
status: dummyUserStatus,
};
- apiSpy = () => done.fail(new Error('expected no Ajax call!'));
+ apiSpy = () => {
+ throw new Error('expected no Ajax call!');
+ };
- UsersCache.retrieveStatusById(dummyUserId)
- .then((userStatus) => {
- expect(userStatus).toBe(dummyUserStatus);
- })
- .then(done)
- .catch(done.fail);
+ const userStatus = await UsersCache.retrieveStatusById(dummyUserId);
+ expect(userStatus).toBe(dummyUserStatus);
});
});
});
diff --git a/spec/frontend/members/components/table/members_table_spec.js b/spec/frontend/members/components/table/members_table_spec.js
index b2756e506eb..298a01e4f4d 100644
--- a/spec/frontend/members/components/table/members_table_spec.js
+++ b/spec/frontend/members/components/table/members_table_spec.js
@@ -10,6 +10,7 @@ import MemberAvatar from '~/members/components/table/member_avatar.vue';
import MemberSource from '~/members/components/table/member_source.vue';
import MembersTable from '~/members/components/table/members_table.vue';
import RoleDropdown from '~/members/components/table/role_dropdown.vue';
+import UserDate from '~/vue_shared/components/user_date.vue';
import {
MEMBER_TYPES,
MEMBER_STATE_CREATED,
@@ -106,14 +107,16 @@ describe('MembersTable', () => {
};
it.each`
- field | label | member | expectedComponent
- ${'account'} | ${'Account'} | ${memberMock} | ${MemberAvatar}
- ${'source'} | ${'Source'} | ${memberMock} | ${MemberSource}
- ${'granted'} | ${'Access granted'} | ${memberMock} | ${CreatedAt}
- ${'invited'} | ${'Invited'} | ${invite} | ${CreatedAt}
- ${'requested'} | ${'Requested'} | ${accessRequest} | ${CreatedAt}
- ${'maxRole'} | ${'Max role'} | ${memberCanUpdate} | ${RoleDropdown}
- ${'expiration'} | ${'Expiration'} | ${memberMock} | ${ExpirationDatepicker}
+ field | label | member | expectedComponent
+ ${'account'} | ${'Account'} | ${memberMock} | ${MemberAvatar}
+ ${'source'} | ${'Source'} | ${memberMock} | ${MemberSource}
+ ${'granted'} | ${'Access granted'} | ${memberMock} | ${CreatedAt}
+ ${'invited'} | ${'Invited'} | ${invite} | ${CreatedAt}
+ ${'requested'} | ${'Requested'} | ${accessRequest} | ${CreatedAt}
+ ${'maxRole'} | ${'Max role'} | ${memberCanUpdate} | ${RoleDropdown}
+ ${'expiration'} | ${'Expiration'} | ${memberMock} | ${ExpirationDatepicker}
+ ${'userCreatedAt'} | ${'Created on'} | ${memberMock} | ${UserDate}
+ ${'lastActivityOn'} | ${'Last activity'} | ${memberMock} | ${UserDate}
`('renders the $label field', ({ field, label, member, expectedComponent }) => {
createComponent({
members: [member],
diff --git a/spec/frontend/members/mock_data.js b/spec/frontend/members/mock_data.js
index 83856a00a15..06ccd107ce3 100644
--- a/spec/frontend/members/mock_data.js
+++ b/spec/frontend/members/mock_data.js
@@ -17,6 +17,7 @@ export const member = {
state: MEMBER_STATE_CREATED,
user: {
id: 123,
+ createdAt: '2022-03-10T18:03:04.812Z',
name: 'Administrator',
username: 'root',
webUrl: 'https://gitlab.com/root',
@@ -26,6 +27,7 @@ export const member = {
oncallSchedules: [{ name: 'schedule 1' }],
escalationPolicies: [{ name: 'policy 1' }],
availability: null,
+ lastActivityOn: '2022-03-15',
showStatus: true,
},
id: 238,
@@ -56,6 +58,7 @@ export const group = {
webUrl: 'https://gitlab.com/groups/parent-group/commit451',
},
id: 3,
+ isDirectMember: true,
createdAt: '2020-08-06T15:31:07.662Z',
expiresAt: null,
validRoles: { Guest: 10, Reporter: 20, Developer: 30, Maintainer: 40, Owner: 50 },
diff --git a/spec/frontend/merge_conflicts/store/actions_spec.js b/spec/frontend/merge_conflicts/store/actions_spec.js
index 3e1774a6d56..1b6a0f9e977 100644
--- a/spec/frontend/merge_conflicts/store/actions_spec.js
+++ b/spec/frontend/merge_conflicts/store/actions_spec.js
@@ -34,9 +34,9 @@ describe('merge conflicts actions', () => {
describe('fetchConflictsData', () => {
const conflictsPath = 'conflicts/path/mock';
- it('on success dispatches setConflictsData', (done) => {
+ it('on success dispatches setConflictsData', () => {
mock.onGet(conflictsPath).reply(200, {});
- testAction(
+ return testAction(
actions.fetchConflictsData,
conflictsPath,
{},
@@ -45,13 +45,12 @@ describe('merge conflicts actions', () => {
{ type: types.SET_LOADING_STATE, payload: false },
],
[{ type: 'setConflictsData', payload: {} }],
- done,
);
});
- it('when data has type equal to error ', (done) => {
+ it('when data has type equal to error ', () => {
mock.onGet(conflictsPath).reply(200, { type: 'error', message: 'error message' });
- testAction(
+ return testAction(
actions.fetchConflictsData,
conflictsPath,
{},
@@ -61,13 +60,12 @@ describe('merge conflicts actions', () => {
{ type: types.SET_LOADING_STATE, payload: false },
],
[],
- done,
);
});
- it('when request fails ', (done) => {
+ it('when request fails ', () => {
mock.onGet(conflictsPath).reply(400);
- testAction(
+ return testAction(
actions.fetchConflictsData,
conflictsPath,
{},
@@ -77,15 +75,14 @@ describe('merge conflicts actions', () => {
{ type: types.SET_LOADING_STATE, payload: false },
],
[],
- done,
);
});
});
describe('setConflictsData', () => {
- it('INTERACTIVE_RESOLVE_MODE updates the correct file ', (done) => {
+ it('INTERACTIVE_RESOLVE_MODE updates the correct file ', () => {
decorateFiles.mockReturnValue([{ bar: 'baz' }]);
- testAction(
+ return testAction(
actions.setConflictsData,
{ files, foo: 'bar' },
{},
@@ -96,7 +93,6 @@ describe('merge conflicts actions', () => {
},
],
[],
- done,
);
});
});
@@ -105,24 +101,21 @@ describe('merge conflicts actions', () => {
useMockLocationHelper();
const resolveConflictsPath = 'resolve/conflicts/path/mock';
- it('on success reloads the page', (done) => {
+ it('on success reloads the page', async () => {
mock.onPost(resolveConflictsPath).reply(200, { redirect_to: 'hrefPath' });
- testAction(
+ await testAction(
actions.submitResolvedConflicts,
resolveConflictsPath,
{},
[{ type: types.SET_SUBMIT_STATE, payload: true }],
[],
- () => {
- expect(window.location.assign).toHaveBeenCalledWith('hrefPath');
- done();
- },
);
+ expect(window.location.assign).toHaveBeenCalledWith('hrefPath');
});
- it('on errors shows flash', (done) => {
+ it('on errors shows flash', async () => {
mock.onPost(resolveConflictsPath).reply(400);
- testAction(
+ await testAction(
actions.submitResolvedConflicts,
resolveConflictsPath,
{},
@@ -131,13 +124,10 @@ describe('merge conflicts actions', () => {
{ type: types.SET_SUBMIT_STATE, payload: false },
],
[],
- () => {
- expect(createFlash).toHaveBeenCalledWith({
- message: 'Failed to save merge conflicts resolutions. Please try again!',
- });
- done();
- },
);
+ expect(createFlash).toHaveBeenCalledWith({
+ message: 'Failed to save merge conflicts resolutions. Please try again!',
+ });
});
});
@@ -193,9 +183,9 @@ describe('merge conflicts actions', () => {
});
describe('setViewType', () => {
- it('commits the right mutation', (done) => {
+ it('commits the right mutation', async () => {
const payload = 'viewType';
- testAction(
+ await testAction(
actions.setViewType,
payload,
{},
@@ -206,14 +196,11 @@ describe('merge conflicts actions', () => {
},
],
[],
- () => {
- expect(Cookies.set).toHaveBeenCalledWith('diff_view', payload, {
- expires: 365,
- secure: false,
- });
- done();
- },
);
+ expect(Cookies.set).toHaveBeenCalledWith('diff_view', payload, {
+ expires: 365,
+ secure: false,
+ });
});
});
@@ -252,8 +239,8 @@ describe('merge conflicts actions', () => {
});
describe('setFileResolveMode', () => {
- it('INTERACTIVE_RESOLVE_MODE updates the correct file ', (done) => {
- testAction(
+ it('INTERACTIVE_RESOLVE_MODE updates the correct file ', () => {
+ return testAction(
actions.setFileResolveMode,
{ file: files[0], mode: INTERACTIVE_RESOLVE_MODE },
{ conflictsData: { files }, getFileIndex: () => 0 },
@@ -267,11 +254,10 @@ describe('merge conflicts actions', () => {
},
],
[],
- done,
);
});
- it('EDIT_RESOLVE_MODE updates the correct file ', (done) => {
+ it('EDIT_RESOLVE_MODE updates the correct file ', async () => {
restoreFileLinesState.mockReturnValue([]);
const file = {
...files[0],
@@ -280,7 +266,7 @@ describe('merge conflicts actions', () => {
resolutionData: {},
resolveMode: EDIT_RESOLVE_MODE,
};
- testAction(
+ await testAction(
actions.setFileResolveMode,
{ file: files[0], mode: EDIT_RESOLVE_MODE },
{ conflictsData: { files }, getFileIndex: () => 0 },
@@ -294,17 +280,14 @@ describe('merge conflicts actions', () => {
},
],
[],
- () => {
- expect(restoreFileLinesState).toHaveBeenCalledWith(file);
- done();
- },
);
+ expect(restoreFileLinesState).toHaveBeenCalledWith(file);
});
});
describe('setPromptConfirmationState', () => {
- it('updates the correct file ', (done) => {
- testAction(
+ it('updates the correct file ', () => {
+ return testAction(
actions.setPromptConfirmationState,
{ file: files[0], promptDiscardConfirmation: true },
{ conflictsData: { files }, getFileIndex: () => 0 },
@@ -318,7 +301,6 @@ describe('merge conflicts actions', () => {
},
],
[],
- done,
);
});
});
@@ -333,11 +315,11 @@ describe('merge conflicts actions', () => {
],
};
- it('updates the correct file ', (done) => {
+ it('updates the correct file ', async () => {
const marLikeMockReturn = { foo: 'bar' };
markLine.mockReturnValue(marLikeMockReturn);
- testAction(
+ await testAction(
actions.handleSelected,
{ file, line: { id: 1, section: 'baz' } },
{ conflictsData: { files }, getFileIndex: () => 0 },
@@ -359,11 +341,8 @@ describe('merge conflicts actions', () => {
},
],
[],
- () => {
- expect(markLine).toHaveBeenCalledTimes(3);
- done();
- },
);
+ expect(markLine).toHaveBeenCalledTimes(3);
});
});
});
diff --git a/spec/frontend/milestones/components/delete_milestone_modal_spec.js b/spec/frontend/milestones/components/delete_milestone_modal_spec.js
index 8978de0e0e0..b9ba0833c4f 100644
--- a/spec/frontend/milestones/components/delete_milestone_modal_spec.js
+++ b/spec/frontend/milestones/components/delete_milestone_modal_spec.js
@@ -32,7 +32,7 @@ describe('delete_milestone_modal.vue', () => {
jest.spyOn(eventHub, '$emit').mockImplementation(() => {});
});
- it('deletes milestone and redirects to overview page', (done) => {
+ it('deletes milestone and redirects to overview page', async () => {
const responseURL = `${TEST_HOST}/delete_milestone_modal.vue/milestoneOverview`;
jest.spyOn(axios, 'delete').mockImplementation((url) => {
expect(url).toBe(props.milestoneUrl);
@@ -48,19 +48,15 @@ describe('delete_milestone_modal.vue', () => {
});
});
- vm.onSubmit()
- .then(() => {
- expect(redirectTo).toHaveBeenCalledWith(responseURL);
- expect(eventHub.$emit).toHaveBeenCalledWith('deleteMilestoneModal.requestFinished', {
- milestoneUrl: props.milestoneUrl,
- successful: true,
- });
- })
- .then(done)
- .catch(done.fail);
+ await vm.onSubmit();
+ expect(redirectTo).toHaveBeenCalledWith(responseURL);
+ expect(eventHub.$emit).toHaveBeenCalledWith('deleteMilestoneModal.requestFinished', {
+ milestoneUrl: props.milestoneUrl,
+ successful: true,
+ });
});
- it('displays error if deleting milestone failed', (done) => {
+ it('displays error if deleting milestone failed', async () => {
const dummyError = new Error('deleting milestone failed');
dummyError.response = { status: 418 };
jest.spyOn(axios, 'delete').mockImplementation((url) => {
@@ -73,17 +69,12 @@ describe('delete_milestone_modal.vue', () => {
return Promise.reject(dummyError);
});
- vm.onSubmit()
- .catch((error) => {
- expect(error).toBe(dummyError);
- expect(redirectTo).not.toHaveBeenCalled();
- expect(eventHub.$emit).toHaveBeenCalledWith('deleteMilestoneModal.requestFinished', {
- milestoneUrl: props.milestoneUrl,
- successful: false,
- });
- })
- .then(done)
- .catch(done.fail);
+ await expect(vm.onSubmit()).rejects.toEqual(dummyError);
+ expect(redirectTo).not.toHaveBeenCalled();
+ expect(eventHub.$emit).toHaveBeenCalledWith('deleteMilestoneModal.requestFinished', {
+ milestoneUrl: props.milestoneUrl,
+ successful: false,
+ });
});
});
diff --git a/spec/frontend/milestones/components/milestone_combobox_spec.js b/spec/frontend/milestones/components/milestone_combobox_spec.js
index 1af39aff30c..afd85fb78ce 100644
--- a/spec/frontend/milestones/components/milestone_combobox_spec.js
+++ b/spec/frontend/milestones/components/milestone_combobox_spec.js
@@ -340,7 +340,9 @@ describe('Milestone combobox component', () => {
await nextTick();
expect(
- findFirstProjectMilestonesDropdownItem().find('span').classes('selected-item'),
+ findFirstProjectMilestonesDropdownItem()
+ .find('svg')
+ .classes('gl-new-dropdown-item-check-icon'),
).toBe(true);
selectFirstProjectMilestone();
@@ -348,8 +350,8 @@ describe('Milestone combobox component', () => {
await nextTick();
expect(
- findFirstProjectMilestonesDropdownItem().find('span').classes('selected-item'),
- ).toBe(false);
+ findFirstProjectMilestonesDropdownItem().find('svg').classes('gl-visibility-hidden'),
+ ).toBe(true);
});
describe('when a project milestones is selected', () => {
@@ -464,17 +466,19 @@ describe('Milestone combobox component', () => {
await nextTick();
- expect(findFirstGroupMilestonesDropdownItem().find('span').classes('selected-item')).toBe(
- true,
- );
+ expect(
+ findFirstGroupMilestonesDropdownItem()
+ .find('svg')
+ .classes('gl-new-dropdown-item-check-icon'),
+ ).toBe(true);
selectFirstGroupMilestone();
await nextTick();
- expect(findFirstGroupMilestonesDropdownItem().find('span').classes('selected-item')).toBe(
- false,
- );
+ expect(
+ findFirstGroupMilestonesDropdownItem().find('svg').classes('gl-visibility-hidden'),
+ ).toBe(true);
});
describe('when a group milestones is selected', () => {
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 bd2e818df4f..28039321428 100644
--- a/spec/frontend/monitoring/components/__snapshots__/dashboard_template_spec.js.snap
+++ b/spec/frontend/monitoring/components/__snapshots__/dashboard_template_spec.js.snap
@@ -5,7 +5,7 @@ exports[`Dashboard template matches the default snapshot 1`] = `
class="prometheus-graphs"
data-qa-selector="prometheus_graphs"
environmentstate="available"
- metricsdashboardbasepath="/monitoring/monitor-project/-/environments/1/metrics"
+ metricsdashboardbasepath="/monitoring/monitor-project/-/metrics?environment=1"
metricsendpoint="/monitoring/monitor-project/-/environments/1/additional_metrics.json"
>
<div>
@@ -17,11 +17,11 @@ exports[`Dashboard template matches the default snapshot 1`] = `
primarybuttontext=""
secondarybuttonlink=""
secondarybuttontext=""
- title="Feature deprecation and removal"
- variant="danger"
+ title="Feature deprecation"
+ variant="warning"
>
<gl-sprintf-stub
- message="The metrics, logs and tracing features were deprecated in GitLab 14.7 and are %{epicStart} scheduled for removal %{epicEnd} in GitLab 15.0."
+ message="The metrics feature was deprecated in GitLab 14.7."
/>
<gl-sprintf-stub
diff --git a/spec/frontend/monitoring/components/__snapshots__/empty_state_spec.js.snap b/spec/frontend/monitoring/components/__snapshots__/empty_state_spec.js.snap
index 4f8a82692b8..08487a7a796 100644
--- a/spec/frontend/monitoring/components/__snapshots__/empty_state_spec.js.snap
+++ b/spec/frontend/monitoring/components/__snapshots__/empty_state_spec.js.snap
@@ -6,6 +6,7 @@ exports[`EmptyState shows gettingStarted state 1`] = `
<gl-empty-state-stub
description="Stay updated about the performance and health of your environment by configuring Prometheus to monitor your deployments."
+ invertindarkmode="true"
primarybuttonlink="/clustersPath"
primarybuttontext="Install on clusters"
secondarybuttonlink="/settingsPath"
@@ -22,6 +23,7 @@ exports[`EmptyState shows noData state 1`] = `
<gl-empty-state-stub
description="You are connected to the Prometheus server, but there is currently no data to display."
+ invertindarkmode="true"
primarybuttonlink="/settingsPath"
primarybuttontext="Configure Prometheus"
secondarybuttonlink=""
@@ -38,6 +40,7 @@ exports[`EmptyState shows unableToConnect state 1`] = `
<gl-empty-state-stub
description="Ensure connectivity is available from the GitLab server to the Prometheus server"
+ invertindarkmode="true"
primarybuttonlink="/documentationPath"
primarybuttontext="View documentation"
secondarybuttonlink="/settingsPath"
diff --git a/spec/frontend/monitoring/components/__snapshots__/group_empty_state_spec.js.snap b/spec/frontend/monitoring/components/__snapshots__/group_empty_state_spec.js.snap
index 9b2aa3a5b5b..1d7ff420a17 100644
--- a/spec/frontend/monitoring/components/__snapshots__/group_empty_state_spec.js.snap
+++ b/spec/frontend/monitoring/components/__snapshots__/group_empty_state_spec.js.snap
@@ -4,6 +4,7 @@ exports[`GroupEmptyState given state BAD_QUERY passes the expected props to GlEm
Object {
"compact": true,
"description": null,
+ "invertInDarkMode": true,
"primaryButtonLink": "/path/to/settings",
"primaryButtonText": "Verify configuration",
"secondaryButtonLink": null,
@@ -31,6 +32,7 @@ exports[`GroupEmptyState given state CONNECTION_FAILED passes the expected props
Object {
"compact": true,
"description": "We couldn't reach the Prometheus server. Either the server no longer exists or the configuration details need updating.",
+ "invertInDarkMode": true,
"primaryButtonLink": "/path/to/settings",
"primaryButtonText": "Verify configuration",
"secondaryButtonLink": null,
@@ -47,6 +49,7 @@ exports[`GroupEmptyState given state FOO STATE passes the expected props to GlEm
Object {
"compact": true,
"description": "An error occurred while loading the data. Please try again.",
+ "invertInDarkMode": true,
"primaryButtonLink": null,
"primaryButtonText": null,
"secondaryButtonLink": null,
@@ -63,6 +66,7 @@ exports[`GroupEmptyState given state LOADING passes the expected props to GlEmpt
Object {
"compact": true,
"description": "Creating graphs uses the data from the Prometheus server. If this takes a long time, ensure that data is available.",
+ "invertInDarkMode": true,
"primaryButtonLink": null,
"primaryButtonText": null,
"secondaryButtonLink": null,
@@ -79,6 +83,7 @@ exports[`GroupEmptyState given state NO_DATA passes the expected props to GlEmpt
Object {
"compact": true,
"description": null,
+ "invertInDarkMode": true,
"primaryButtonLink": null,
"primaryButtonText": null,
"secondaryButtonLink": null,
@@ -106,6 +111,7 @@ exports[`GroupEmptyState given state TIMEOUT passes the expected props to GlEmpt
Object {
"compact": true,
"description": null,
+ "invertInDarkMode": true,
"primaryButtonLink": null,
"primaryButtonText": null,
"secondaryButtonLink": null,
@@ -133,6 +139,7 @@ exports[`GroupEmptyState given state UNKNOWN_ERROR passes the expected props to
Object {
"compact": true,
"description": "An error occurred while loading the data. Please try again.",
+ "invertInDarkMode": true,
"primaryButtonLink": null,
"primaryButtonText": null,
"secondaryButtonLink": null,
diff --git a/spec/frontend/monitoring/components/dashboard_actions_menu_spec.js b/spec/frontend/monitoring/components/dashboard_actions_menu_spec.js
index d0d0c3071d5..d74f959ac0f 100644
--- a/spec/frontend/monitoring/components/dashboard_actions_menu_spec.js
+++ b/spec/frontend/monitoring/components/dashboard_actions_menu_spec.js
@@ -109,7 +109,7 @@ describe('Actions menu', () => {
describe('adding new metric from modal', () => {
let origPage;
- beforeEach((done) => {
+ beforeEach(() => {
jest.spyOn(Tracking, 'event').mockReturnValue();
createShallowWrapper();
@@ -118,7 +118,7 @@ describe('Actions menu', () => {
origPage = document.body.dataset.page;
document.body.dataset.page = 'projects:environments:metrics';
- nextTick(done);
+ return nextTick();
});
afterEach(() => {
diff --git a/spec/frontend/monitoring/components/dashboard_url_time_spec.js b/spec/frontend/monitoring/components/dashboard_url_time_spec.js
index 246dd598d19..64c48100b31 100644
--- a/spec/frontend/monitoring/components/dashboard_url_time_spec.js
+++ b/spec/frontend/monitoring/components/dashboard_url_time_spec.js
@@ -126,7 +126,7 @@ describe('dashboard invalid url parameters', () => {
});
it('redirects to different time range', async () => {
- const toUrl = `${mockProjectDir}/-/environments/1/metrics`;
+ const toUrl = `${mockProjectDir}/-/metrics?environment=1`;
removeParams.mockReturnValueOnce(toUrl);
createMountedWrapper();
diff --git a/spec/frontend/monitoring/store/actions_spec.js b/spec/frontend/monitoring/store/actions_spec.js
index f60c531e3f6..d1a13fbf9cd 100644
--- a/spec/frontend/monitoring/store/actions_spec.js
+++ b/spec/frontend/monitoring/store/actions_spec.js
@@ -7,9 +7,9 @@ import * as commonUtils from '~/lib/utils/common_utils';
import statusCodes from '~/lib/utils/http_status';
import { ENVIRONMENT_AVAILABLE_STATE } from '~/monitoring/constants';
-import getAnnotations from '~/monitoring/queries/getAnnotations.query.graphql';
-import getDashboardValidationWarnings from '~/monitoring/queries/getDashboardValidationWarnings.query.graphql';
-import getEnvironments from '~/monitoring/queries/getEnvironments.query.graphql';
+import getAnnotations from '~/monitoring/queries/get_annotations.query.graphql';
+import getDashboardValidationWarnings from '~/monitoring/queries/get_dashboard_validation_warnings.query.graphql';
+import getEnvironments from '~/monitoring/queries/get_environments.query.graphql';
import { createStore } from '~/monitoring/stores';
import {
setGettingStartedEmptyState,
@@ -88,8 +88,8 @@ describe('Monitoring store actions', () => {
// Setup
describe('setGettingStartedEmptyState', () => {
- it('should commit SET_GETTING_STARTED_EMPTY_STATE mutation', (done) => {
- testAction(
+ it('should commit SET_GETTING_STARTED_EMPTY_STATE mutation', () => {
+ return testAction(
setGettingStartedEmptyState,
null,
state,
@@ -99,14 +99,13 @@ describe('Monitoring store actions', () => {
},
],
[],
- done,
);
});
});
describe('setInitialState', () => {
- it('should commit SET_INITIAL_STATE mutation', (done) => {
- testAction(
+ it('should commit SET_INITIAL_STATE mutation', () => {
+ return testAction(
setInitialState,
{
currentDashboard: '.gitlab/dashboards/dashboard.yml',
@@ -123,7 +122,6 @@ describe('Monitoring store actions', () => {
},
],
[],
- done,
);
});
});
@@ -233,51 +231,39 @@ describe('Monitoring store actions', () => {
};
});
- it('dispatches a failure', (done) => {
- result()
- .then(() => {
- expect(commit).toHaveBeenCalledWith(
- types.SET_ALL_DASHBOARDS,
- mockDashboardsErrorResponse.all_dashboards,
- );
- expect(dispatch).toHaveBeenCalledWith(
- 'receiveMetricsDashboardFailure',
- new Error('Request failed with status code 500'),
- );
- expect(createFlash).toHaveBeenCalled();
- done();
- })
- .catch(done.fail);
+ it('dispatches a failure', async () => {
+ await result();
+ expect(commit).toHaveBeenCalledWith(
+ types.SET_ALL_DASHBOARDS,
+ mockDashboardsErrorResponse.all_dashboards,
+ );
+ expect(dispatch).toHaveBeenCalledWith(
+ 'receiveMetricsDashboardFailure',
+ new Error('Request failed with status code 500'),
+ );
+ expect(createFlash).toHaveBeenCalled();
});
- it('dispatches a failure action when a message is returned', (done) => {
- result()
- .then(() => {
- expect(dispatch).toHaveBeenCalledWith(
- 'receiveMetricsDashboardFailure',
- new Error('Request failed with status code 500'),
- );
- expect(createFlash).toHaveBeenCalledWith({
- message: expect.stringContaining(mockDashboardsErrorResponse.message),
- });
- done();
- })
- .catch(done.fail);
+ it('dispatches a failure action when a message is returned', async () => {
+ await result();
+ expect(dispatch).toHaveBeenCalledWith(
+ 'receiveMetricsDashboardFailure',
+ new Error('Request failed with status code 500'),
+ );
+ expect(createFlash).toHaveBeenCalledWith({
+ message: expect.stringContaining(mockDashboardsErrorResponse.message),
+ });
});
- it('does not show a flash error when showErrorBanner is disabled', (done) => {
+ it('does not show a flash error when showErrorBanner is disabled', async () => {
state.showErrorBanner = false;
- result()
- .then(() => {
- expect(dispatch).toHaveBeenCalledWith(
- 'receiveMetricsDashboardFailure',
- new Error('Request failed with status code 500'),
- );
- expect(createFlash).not.toHaveBeenCalled();
- done();
- })
- .catch(done.fail);
+ await result();
+ expect(dispatch).toHaveBeenCalledWith(
+ 'receiveMetricsDashboardFailure',
+ new Error('Request failed with status code 500'),
+ );
+ expect(createFlash).not.toHaveBeenCalled();
});
});
});
@@ -322,38 +308,30 @@ describe('Monitoring store actions', () => {
state.timeRange = defaultTimeRange;
});
- it('commits empty state when state.groups is empty', (done) => {
+ it('commits empty state when state.groups is empty', async () => {
const localGetters = {
metricsWithData: () => [],
};
- fetchDashboardData({ state, commit, dispatch, getters: localGetters })
- .then(() => {
- expect(Tracking.event).toHaveBeenCalledWith(
- document.body.dataset.page,
- 'dashboard_fetch',
- {
- label: 'custom_metrics_dashboard',
- property: 'count',
- value: 0,
- },
- );
- expect(dispatch).toHaveBeenCalledTimes(2);
- expect(dispatch).toHaveBeenCalledWith('fetchDeploymentsData');
- expect(dispatch).toHaveBeenCalledWith('fetchVariableMetricLabelValues', {
- defaultQueryParams: {
- start_time: expect.any(String),
- end_time: expect.any(String),
- step: expect.any(Number),
- },
- });
+ await fetchDashboardData({ state, commit, dispatch, getters: localGetters });
+ expect(Tracking.event).toHaveBeenCalledWith(document.body.dataset.page, 'dashboard_fetch', {
+ label: 'custom_metrics_dashboard',
+ property: 'count',
+ value: 0,
+ });
+ expect(dispatch).toHaveBeenCalledTimes(2);
+ expect(dispatch).toHaveBeenCalledWith('fetchDeploymentsData');
+ expect(dispatch).toHaveBeenCalledWith('fetchVariableMetricLabelValues', {
+ defaultQueryParams: {
+ start_time: expect.any(String),
+ end_time: expect.any(String),
+ step: expect.any(Number),
+ },
+ });
- expect(createFlash).not.toHaveBeenCalled();
- done();
- })
- .catch(done.fail);
+ expect(createFlash).not.toHaveBeenCalled();
});
- it('dispatches fetchPrometheusMetric for each panel query', (done) => {
+ it('dispatches fetchPrometheusMetric for each panel query', async () => {
state.dashboard.panelGroups = convertObjectPropsToCamelCase(
metricsDashboardResponse.dashboard.panel_groups,
);
@@ -363,34 +341,24 @@ describe('Monitoring store actions', () => {
metricsWithData: () => [metric.id],
};
- fetchDashboardData({ state, commit, dispatch, getters: localGetters })
- .then(() => {
- expect(dispatch).toHaveBeenCalledWith('fetchPrometheusMetric', {
- metric,
- defaultQueryParams: {
- start_time: expect.any(String),
- end_time: expect.any(String),
- step: expect.any(Number),
- },
- });
-
- expect(Tracking.event).toHaveBeenCalledWith(
- document.body.dataset.page,
- 'dashboard_fetch',
- {
- label: 'custom_metrics_dashboard',
- property: 'count',
- value: 1,
- },
- );
+ await fetchDashboardData({ state, commit, dispatch, getters: localGetters });
+ expect(dispatch).toHaveBeenCalledWith('fetchPrometheusMetric', {
+ metric,
+ defaultQueryParams: {
+ start_time: expect.any(String),
+ end_time: expect.any(String),
+ step: expect.any(Number),
+ },
+ });
- done();
- })
- .catch(done.fail);
- done();
+ expect(Tracking.event).toHaveBeenCalledWith(document.body.dataset.page, 'dashboard_fetch', {
+ label: 'custom_metrics_dashboard',
+ property: 'count',
+ value: 1,
+ });
});
- it('dispatches fetchPrometheusMetric for each panel query, handles an error', (done) => {
+ it('dispatches fetchPrometheusMetric for each panel query, handles an error', async () => {
state.dashboard.panelGroups = metricsDashboardViewModel.panelGroups;
const metric = state.dashboard.panelGroups[0].panels[0].metrics[0];
@@ -400,30 +368,24 @@ describe('Monitoring store actions', () => {
dispatch.mockRejectedValueOnce(new Error('Error fetching this metric'));
dispatch.mockResolvedValue();
- fetchDashboardData({ state, commit, dispatch })
- .then(() => {
- const defaultQueryParams = {
- start_time: expect.any(String),
- end_time: expect.any(String),
- step: expect.any(Number),
- };
-
- expect(dispatch).toHaveBeenCalledTimes(metricsDashboardPanelCount + 2); // plus 1 for deployments
- expect(dispatch).toHaveBeenCalledWith('fetchDeploymentsData');
- expect(dispatch).toHaveBeenCalledWith('fetchVariableMetricLabelValues', {
- defaultQueryParams,
- });
- expect(dispatch).toHaveBeenCalledWith('fetchPrometheusMetric', {
- metric,
- defaultQueryParams,
- });
+ await fetchDashboardData({ state, commit, dispatch });
+ const defaultQueryParams = {
+ start_time: expect.any(String),
+ end_time: expect.any(String),
+ step: expect.any(Number),
+ };
- expect(createFlash).toHaveBeenCalledTimes(1);
+ expect(dispatch).toHaveBeenCalledTimes(metricsDashboardPanelCount + 2); // plus 1 for deployments
+ expect(dispatch).toHaveBeenCalledWith('fetchDeploymentsData');
+ expect(dispatch).toHaveBeenCalledWith('fetchVariableMetricLabelValues', {
+ defaultQueryParams,
+ });
+ expect(dispatch).toHaveBeenCalledWith('fetchPrometheusMetric', {
+ metric,
+ defaultQueryParams,
+ });
- done();
- })
- .catch(done.fail);
- done();
+ expect(createFlash).toHaveBeenCalledTimes(1);
});
});
@@ -449,10 +411,10 @@ describe('Monitoring store actions', () => {
};
});
- it('commits result', (done) => {
+ it('commits result', () => {
mock.onGet(prometheusEndpointPath).reply(200, { data }); // One attempt
- testAction(
+ return testAction(
fetchPrometheusMetric,
{ metric, defaultQueryParams },
state,
@@ -472,10 +434,7 @@ describe('Monitoring store actions', () => {
},
],
[],
- () => {
- done();
- },
- ).catch(done.fail);
+ );
});
describe('without metric defined step', () => {
@@ -485,10 +444,10 @@ describe('Monitoring store actions', () => {
step: 60,
};
- it('uses calculated step', (done) => {
+ it('uses calculated step', async () => {
mock.onGet(prometheusEndpointPath).reply(200, { data }); // One attempt
- testAction(
+ await testAction(
fetchPrometheusMetric,
{ metric, defaultQueryParams },
state,
@@ -508,11 +467,8 @@ describe('Monitoring store actions', () => {
},
],
[],
- () => {
- expect(mock.history.get[0].params).toEqual(expectedParams);
- done();
- },
- ).catch(done.fail);
+ );
+ expect(mock.history.get[0].params).toEqual(expectedParams);
});
});
@@ -527,10 +483,10 @@ describe('Monitoring store actions', () => {
step: 7,
};
- it('uses metric step', (done) => {
+ it('uses metric step', async () => {
mock.onGet(prometheusEndpointPath).reply(200, { data }); // One attempt
- testAction(
+ await testAction(
fetchPrometheusMetric,
{ metric, defaultQueryParams },
state,
@@ -550,43 +506,39 @@ describe('Monitoring store actions', () => {
},
],
[],
- () => {
- expect(mock.history.get[0].params).toEqual(expectedParams);
- done();
- },
- ).catch(done.fail);
+ );
+ expect(mock.history.get[0].params).toEqual(expectedParams);
});
});
- it('commits failure, when waiting for results and getting a server error', (done) => {
+ it('commits failure, when waiting for results and getting a server error', async () => {
mock.onGet(prometheusEndpointPath).reply(500);
const error = new Error('Request failed with status code 500');
- testAction(
- fetchPrometheusMetric,
- { metric, defaultQueryParams },
- state,
- [
- {
- type: types.REQUEST_METRIC_RESULT,
- payload: {
- metricId: metric.metricId,
+ await expect(
+ testAction(
+ fetchPrometheusMetric,
+ { metric, defaultQueryParams },
+ state,
+ [
+ {
+ type: types.REQUEST_METRIC_RESULT,
+ payload: {
+ metricId: metric.metricId,
+ },
},
- },
- {
- type: types.RECEIVE_METRIC_RESULT_FAILURE,
- payload: {
- metricId: metric.metricId,
- error,
+ {
+ type: types.RECEIVE_METRIC_RESULT_FAILURE,
+ payload: {
+ metricId: metric.metricId,
+ error,
+ },
},
- },
- ],
- [],
- ).catch((e) => {
- expect(e).toEqual(error);
- done();
- });
+ ],
+ [],
+ ),
+ ).rejects.toEqual(error);
});
});
@@ -991,20 +943,16 @@ describe('Monitoring store actions', () => {
state.dashboardsEndpoint = '/dashboards.json';
});
- it('Succesful POST request resolves', (done) => {
+ it('Succesful POST request resolves', async () => {
mock.onPost(state.dashboardsEndpoint).reply(statusCodes.CREATED, {
dashboard: dashboardGitResponse[1],
});
- testAction(duplicateSystemDashboard, {}, state, [], [])
- .then(() => {
- expect(mock.history.post).toHaveLength(1);
- done();
- })
- .catch(done.fail);
+ await testAction(duplicateSystemDashboard, {}, state, [], []);
+ expect(mock.history.post).toHaveLength(1);
});
- it('Succesful POST request resolves to a dashboard', (done) => {
+ it('Succesful POST request resolves to a dashboard', async () => {
const mockCreatedDashboard = dashboardGitResponse[1];
const params = {
@@ -1025,50 +973,40 @@ describe('Monitoring store actions', () => {
dashboard: mockCreatedDashboard,
});
- testAction(duplicateSystemDashboard, params, state, [], [])
- .then((result) => {
- expect(mock.history.post).toHaveLength(1);
- expect(mock.history.post[0].data).toEqual(expectedPayload);
- expect(result).toEqual(mockCreatedDashboard);
-
- done();
- })
- .catch(done.fail);
+ const result = await testAction(duplicateSystemDashboard, params, state, [], []);
+ expect(mock.history.post).toHaveLength(1);
+ expect(mock.history.post[0].data).toEqual(expectedPayload);
+ expect(result).toEqual(mockCreatedDashboard);
});
- it('Failed POST request throws an error', (done) => {
+ it('Failed POST request throws an error', async () => {
mock.onPost(state.dashboardsEndpoint).reply(statusCodes.BAD_REQUEST);
- testAction(duplicateSystemDashboard, {}, state, [], []).catch((err) => {
- expect(mock.history.post).toHaveLength(1);
- expect(err).toEqual(expect.any(String));
-
- done();
- });
+ await expect(testAction(duplicateSystemDashboard, {}, state, [], [])).rejects.toEqual(
+ 'There was an error creating the dashboard.',
+ );
+ expect(mock.history.post).toHaveLength(1);
});
- it('Failed POST request throws an error with a description', (done) => {
+ it('Failed POST request throws an error with a description', async () => {
const backendErrorMsg = 'This file already exists!';
mock.onPost(state.dashboardsEndpoint).reply(statusCodes.BAD_REQUEST, {
error: backendErrorMsg,
});
- testAction(duplicateSystemDashboard, {}, state, [], []).catch((err) => {
- expect(mock.history.post).toHaveLength(1);
- expect(err).toEqual(expect.any(String));
- expect(err).toEqual(expect.stringContaining(backendErrorMsg));
-
- done();
- });
+ await expect(testAction(duplicateSystemDashboard, {}, state, [], [])).rejects.toEqual(
+ `There was an error creating the dashboard. ${backendErrorMsg}`,
+ );
+ expect(mock.history.post).toHaveLength(1);
});
});
// Variables manipulation
describe('updateVariablesAndFetchData', () => {
- it('should commit UPDATE_VARIABLE_VALUE mutation and fetch data', (done) => {
- testAction(
+ it('should commit UPDATE_VARIABLE_VALUE mutation and fetch data', () => {
+ return testAction(
updateVariablesAndFetchData,
{ pod: 'POD' },
state,
@@ -1083,7 +1021,6 @@ describe('Monitoring store actions', () => {
type: 'fetchDashboardData',
},
],
- done,
);
});
});
diff --git a/spec/frontend/monitoring/store/utils_spec.js b/spec/frontend/monitoring/store/utils_spec.js
index 697bdb9185f..c25de8caa95 100644
--- a/spec/frontend/monitoring/store/utils_spec.js
+++ b/spec/frontend/monitoring/store/utils_spec.js
@@ -547,7 +547,7 @@ describe('parseEnvironmentsResponse', () => {
{
id: 1,
name: 'env-1',
- metrics_path: `${projectPath}/environments/1/metrics`,
+ metrics_path: `${projectPath}/-/metrics?environment=1`,
},
],
},
@@ -562,7 +562,7 @@ describe('parseEnvironmentsResponse', () => {
{
id: 12,
name: 'env-12',
- metrics_path: `${projectPath}/environments/12/metrics`,
+ metrics_path: `${projectPath}/-/metrics?environment=12`,
},
],
},
diff --git a/spec/frontend/mr_notes/stores/actions_spec.js b/spec/frontend/mr_notes/stores/actions_spec.js
index c6578453d85..568c1b930c9 100644
--- a/spec/frontend/mr_notes/stores/actions_spec.js
+++ b/spec/frontend/mr_notes/stores/actions_spec.js
@@ -1,64 +1,37 @@
import MockAdapter from 'axios-mock-adapter';
-
-import testAction from 'helpers/vuex_action_helper';
import axios from '~/lib/utils/axios_utils';
-import { setEndpoints, setMrMetadata, fetchMrMetadata } from '~/mr_notes/stores/actions';
-import mutationTypes from '~/mr_notes/stores/mutation_types';
+import { createStore } from '~/mr_notes/stores';
describe('MR Notes Mutator Actions', () => {
+ let store;
+
+ beforeEach(() => {
+ store = createStore();
+ });
+
describe('setEndpoints', () => {
- it('should trigger the SET_ENDPOINTS state mutation', (done) => {
+ it('sets endpoints', async () => {
const endpoints = { endpointA: 'a' };
- testAction(
- setEndpoints,
- endpoints,
- {},
- [
- {
- type: mutationTypes.SET_ENDPOINTS,
- payload: endpoints,
- },
- ],
- [],
- done,
- );
- });
- });
+ await store.dispatch('setEndpoints', endpoints);
- describe('setMrMetadata', () => {
- it('should trigger the SET_MR_METADATA state mutation', async () => {
- const mrMetadata = { propA: 'a', propB: 'b' };
-
- await testAction(
- setMrMetadata,
- mrMetadata,
- {},
- [
- {
- type: mutationTypes.SET_MR_METADATA,
- payload: mrMetadata,
- },
- ],
- [],
- );
+ expect(store.state.page.endpoints).toEqual(endpoints);
});
});
describe('fetchMrMetadata', () => {
const mrMetadata = { meta: true, data: 'foo' };
- const state = {
- endpoints: {
- metadata: 'metadata',
- },
- };
+ const metadata = 'metadata';
+ const endpoints = { metadata };
let mock;
- beforeEach(() => {
+ beforeEach(async () => {
+ await store.dispatch('setEndpoints', endpoints);
+
mock = new MockAdapter(axios);
- mock.onGet(state.endpoints.metadata).reply(200, mrMetadata);
+ mock.onGet(metadata).reply(200, mrMetadata);
});
afterEach(() => {
@@ -66,27 +39,26 @@ describe('MR Notes Mutator Actions', () => {
});
it('should fetch the data from the API', async () => {
- await fetchMrMetadata({ state, dispatch: () => {} });
+ await store.dispatch('fetchMrMetadata');
await axios.waitForAll();
expect(mock.history.get).toHaveLength(1);
- expect(mock.history.get[0].url).toBe(state.endpoints.metadata);
+ expect(mock.history.get[0].url).toBe(metadata);
+ });
+
+ it('should set the fetched data into state', async () => {
+ await store.dispatch('fetchMrMetadata');
+
+ expect(store.state.page.mrMetadata).toEqual(mrMetadata);
});
- it('should set the fetched data into state', () => {
- return testAction(
- fetchMrMetadata,
- {},
- state,
- [],
- [
- {
- type: 'setMrMetadata',
- payload: mrMetadata,
- },
- ],
- );
+ it('should set failedToLoadMetadata flag when request fails', async () => {
+ mock.onGet(metadata).reply(500);
+
+ await store.dispatch('fetchMrMetadata');
+
+ expect(store.state.page.failedToLoadMetadata).toBe(true);
});
});
});
diff --git a/spec/frontend/notes/components/diff_discussion_header_spec.js b/spec/frontend/notes/components/diff_discussion_header_spec.js
index 9f94dd693cb..7878737fd31 100644
--- a/spec/frontend/notes/components/diff_discussion_header_spec.js
+++ b/spec/frontend/notes/components/diff_discussion_header_spec.js
@@ -4,7 +4,7 @@ import { nextTick } from 'vue';
import diffDiscussionHeader from '~/notes/components/diff_discussion_header.vue';
import createStore from '~/notes/stores';
-import mockDiffFile from '../../diffs/mock_data/diff_discussions';
+import mockDiffFile from 'jest/diffs/mock_data/diff_discussions';
import { discussionMock } from '../mock_data';
describe('diff_discussion_header component', () => {
diff --git a/spec/frontend/notes/components/note_actions_spec.js b/spec/frontend/notes/components/note_actions_spec.js
index 780f24b3aa8..bf5a6b4966a 100644
--- a/spec/frontend/notes/components/note_actions_spec.js
+++ b/spec/frontend/notes/components/note_actions_spec.js
@@ -87,8 +87,7 @@ describe('noteActions', () => {
});
it('should render emoji link', () => {
- expect(wrapper.find('.js-add-award').exists()).toBe(true);
- expect(wrapper.find('.js-add-award').attributes('data-position')).toBe('right');
+ expect(wrapper.find('[data-testid="note-emoji-button"]').exists()).toBe(true);
});
describe('actions dropdown', () => {
diff --git a/spec/frontend/notes/components/note_form_spec.js b/spec/frontend/notes/components/note_form_spec.js
index 3e80b24f128..b709141f4ac 100644
--- a/spec/frontend/notes/components/note_form_spec.js
+++ b/spec/frontend/notes/components/note_form_spec.js
@@ -81,7 +81,6 @@ describe('issue_note_form component', () => {
it('should show conflict message if note changes outside the component', async () => {
wrapper.setProps({
...props,
- isEditing: true,
noteBody: 'Foo',
});
@@ -111,6 +110,12 @@ describe('issue_note_form component', () => {
);
});
+ it('should set data-supports-quick-actions to enable autocomplete', () => {
+ const textarea = wrapper.find('textarea');
+
+ expect(textarea.attributes('data-supports-quick-actions')).toBe('true');
+ });
+
it('should link to markdown docs', () => {
const { markdownDocsPath } = notesDataMock;
const markdownField = wrapper.find(MarkdownField);
@@ -171,7 +176,6 @@ describe('issue_note_form component', () => {
it('should be possible to cancel', async () => {
wrapper.setProps({
...props,
- isEditing: true,
});
await nextTick();
@@ -185,7 +189,6 @@ describe('issue_note_form component', () => {
it('should be possible to update the note', async () => {
wrapper.setProps({
...props,
- isEditing: true,
});
await nextTick();
diff --git a/spec/frontend/notes/components/note_header_spec.js b/spec/frontend/notes/components/note_header_spec.js
index 4671d33219d..3513b562e0a 100644
--- a/spec/frontend/notes/components/note_header_spec.js
+++ b/spec/frontend/notes/components/note_header_spec.js
@@ -263,7 +263,7 @@ describe('NoteHeader component', () => {
});
describe('when author username link is hovered', () => {
- it('toggles hover specific CSS classes on author name link', (done) => {
+ it('toggles hover specific CSS classes on author name link', async () => {
createComponent({ author });
const authorUsernameLink = wrapper.find({ ref: 'authorUsernameLink' });
@@ -271,19 +271,15 @@ describe('NoteHeader component', () => {
authorUsernameLink.trigger('mouseenter');
- nextTick(() => {
- expect(authorNameLink.classes()).toContain('hover');
- expect(authorNameLink.classes()).toContain('text-underline');
+ await nextTick();
+ expect(authorNameLink.classes()).toContain('hover');
+ expect(authorNameLink.classes()).toContain('text-underline');
- authorUsernameLink.trigger('mouseleave');
+ authorUsernameLink.trigger('mouseleave');
- nextTick(() => {
- expect(authorNameLink.classes()).not.toContain('hover');
- expect(authorNameLink.classes()).not.toContain('text-underline');
-
- done();
- });
- });
+ await nextTick();
+ expect(authorNameLink.classes()).not.toContain('hover');
+ expect(authorNameLink.classes()).not.toContain('text-underline');
});
});
@@ -296,5 +292,13 @@ describe('NoteHeader component', () => {
createComponent({ isConfidential: status });
expect(findConfidentialIndicator().exists()).toBe(status);
});
+
+ it('shows confidential indicator tooltip for project context', () => {
+ createComponent({ isConfidential: true, noteableType: 'issue' });
+
+ expect(findConfidentialIndicator().attributes('title')).toBe(
+ 'This comment is confidential and only visible to project members',
+ );
+ });
});
});
diff --git a/spec/frontend/notes/components/noteable_discussion_spec.js b/spec/frontend/notes/components/noteable_discussion_spec.js
index 727ef02dcbb..c46d3bbe5b2 100644
--- a/spec/frontend/notes/components/noteable_discussion_spec.js
+++ b/spec/frontend/notes/components/noteable_discussion_spec.js
@@ -86,7 +86,6 @@ describe('noteable_discussion component', () => {
const noteFormProps = noteForm.props();
expect(noteFormProps.discussion).toBe(discussionMock);
- expect(noteFormProps.isEditing).toBe(false);
expect(noteFormProps.line).toBe(null);
expect(noteFormProps.saveButtonTitle).toBe('Comment');
expect(noteFormProps.autosaveKey).toBe(`Note/Issue/${discussionMock.id}/Reply`);
diff --git a/spec/frontend/notes/components/noteable_note_spec.js b/spec/frontend/notes/components/noteable_note_spec.js
index c7115a5911b..385edc59eb6 100644
--- a/spec/frontend/notes/components/noteable_note_spec.js
+++ b/spec/frontend/notes/components/noteable_note_spec.js
@@ -11,6 +11,7 @@ import NoteBody from '~/notes/components/note_body.vue';
import NoteHeader from '~/notes/components/note_header.vue';
import issueNote from '~/notes/components/noteable_note.vue';
import NotesModule from '~/notes/stores/modules';
+import { NOTEABLE_TYPE_MAPPING } from '~/notes/constants';
import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
@@ -226,6 +227,7 @@ describe('issue_note', () => {
expect(noteHeaderProps.author).toBe(note.author);
expect(noteHeaderProps.createdAt).toBe(note.created_at);
expect(noteHeaderProps.noteId).toBe(note.id);
+ expect(noteHeaderProps.noteableType).toBe(NOTEABLE_TYPE_MAPPING[note.noteable_type]);
});
it('should render note actions', () => {
diff --git a/spec/frontend/notes/components/notes_app_spec.js b/spec/frontend/notes/components/notes_app_spec.js
index bf36d6cb7a2..e227af88d3f 100644
--- a/spec/frontend/notes/components/notes_app_spec.js
+++ b/spec/frontend/notes/components/notes_app_spec.js
@@ -300,16 +300,18 @@ describe('note_app', () => {
await nextTick();
expect(wrapper.find(`.edit-note a[href="${markdownDocsPath}"]`).text().trim()).toEqual(
- 'Markdown is supported',
+ 'Markdown',
);
});
- it('should not render quick actions docs url', async () => {
+ it('should render quick actions docs url', async () => {
wrapper.find('.js-note-edit').trigger('click');
const { quickActionsDocsPath } = mockData.notesDataMock;
await nextTick();
- expect(wrapper.find(`.edit-note a[href="${quickActionsDocsPath}"]`).exists()).toBe(false);
+ expect(wrapper.find(`.edit-note a[href="${quickActionsDocsPath}"]`).text().trim()).toEqual(
+ 'quick actions',
+ );
});
});
diff --git a/spec/frontend/notes/components/sort_discussion_spec.js b/spec/frontend/notes/components/sort_discussion_spec.js
index a279dfd1ef3..bde27b7e5fc 100644
--- a/spec/frontend/notes/components/sort_discussion_spec.js
+++ b/spec/frontend/notes/components/sort_discussion_spec.js
@@ -38,8 +38,8 @@ describe('Sort Discussion component', () => {
createComponent();
});
- it('has local storage sync', () => {
- expect(findLocalStorageSync().exists()).toBe(true);
+ it('has local storage sync with the correct props', () => {
+ expect(findLocalStorageSync().props('asString')).toBe(true);
});
it('calls setDiscussionSortDirection when update is emitted', () => {
diff --git a/spec/frontend/notes/deprecated_notes_spec.js b/spec/frontend/notes/deprecated_notes_spec.js
index 7c52920da90..7193475c96a 100644
--- a/spec/frontend/notes/deprecated_notes_spec.js
+++ b/spec/frontend/notes/deprecated_notes_spec.js
@@ -561,7 +561,7 @@ describe.skip('Old Notes (~/deprecated_notes.js)', () => {
});
describe('postComment', () => {
- it('disables the submit button', (done) => {
+ it('disables the submit button', async () => {
const $submitButton = $form.find('.js-comment-submit-button');
expect($submitButton).not.toBeDisabled();
@@ -574,13 +574,8 @@ describe.skip('Old Notes (~/deprecated_notes.js)', () => {
return [200, note];
});
- notes
- .postComment(dummyEvent)
- .then(() => {
- expect($submitButton).not.toBeDisabled();
- })
- .then(done)
- .catch(done.fail);
+ await notes.postComment(dummyEvent);
+ expect($submitButton).not.toBeDisabled();
});
});
diff --git a/spec/frontend/notes/stores/actions_spec.js b/spec/frontend/notes/stores/actions_spec.js
index 7424a87bc0f..75e7756cd6b 100644
--- a/spec/frontend/notes/stores/actions_spec.js
+++ b/spec/frontend/notes/stores/actions_spec.js
@@ -62,118 +62,109 @@ describe('Actions Notes Store', () => {
});
describe('setNotesData', () => {
- it('should set received notes data', (done) => {
- testAction(
+ it('should set received notes data', () => {
+ return testAction(
actions.setNotesData,
notesDataMock,
{ notesData: {} },
[{ type: 'SET_NOTES_DATA', payload: notesDataMock }],
[],
- done,
);
});
});
describe('setNoteableData', () => {
- it('should set received issue data', (done) => {
- testAction(
+ it('should set received issue data', () => {
+ return testAction(
actions.setNoteableData,
noteableDataMock,
{ noteableData: {} },
[{ type: 'SET_NOTEABLE_DATA', payload: noteableDataMock }],
[],
- done,
);
});
});
describe('setUserData', () => {
- it('should set received user data', (done) => {
- testAction(
+ it('should set received user data', () => {
+ return testAction(
actions.setUserData,
userDataMock,
{ userData: {} },
[{ type: 'SET_USER_DATA', payload: userDataMock }],
[],
- done,
);
});
});
describe('setLastFetchedAt', () => {
- it('should set received timestamp', (done) => {
- testAction(
+ it('should set received timestamp', () => {
+ return testAction(
actions.setLastFetchedAt,
'timestamp',
{ lastFetchedAt: {} },
[{ type: 'SET_LAST_FETCHED_AT', payload: 'timestamp' }],
[],
- done,
);
});
});
describe('setInitialNotes', () => {
- it('should set initial notes', (done) => {
- testAction(
+ it('should set initial notes', () => {
+ return testAction(
actions.setInitialNotes,
[individualNote],
{ notes: [] },
[{ type: 'ADD_OR_UPDATE_DISCUSSIONS', payload: [individualNote] }],
[],
- done,
);
});
});
describe('setTargetNoteHash', () => {
- it('should set target note hash', (done) => {
- testAction(
+ it('should set target note hash', () => {
+ return testAction(
actions.setTargetNoteHash,
'hash',
{ notes: [] },
[{ type: 'SET_TARGET_NOTE_HASH', payload: 'hash' }],
[],
- done,
);
});
});
describe('toggleDiscussion', () => {
- it('should toggle discussion', (done) => {
- testAction(
+ it('should toggle discussion', () => {
+ return testAction(
actions.toggleDiscussion,
{ discussionId: discussionMock.id },
{ notes: [discussionMock] },
[{ type: 'TOGGLE_DISCUSSION', payload: { discussionId: discussionMock.id } }],
[],
- done,
);
});
});
describe('expandDiscussion', () => {
- it('should expand discussion', (done) => {
- testAction(
+ it('should expand discussion', () => {
+ return testAction(
actions.expandDiscussion,
{ discussionId: discussionMock.id },
{ notes: [discussionMock] },
[{ type: 'EXPAND_DISCUSSION', payload: { discussionId: discussionMock.id } }],
[{ type: 'diffs/renderFileForDiscussionId', payload: discussionMock.id }],
- done,
);
});
});
describe('collapseDiscussion', () => {
- it('should commit collapse discussion', (done) => {
- testAction(
+ it('should commit collapse discussion', () => {
+ return testAction(
actions.collapseDiscussion,
{ discussionId: discussionMock.id },
{ notes: [discussionMock] },
[{ type: 'COLLAPSE_DISCUSSION', payload: { discussionId: discussionMock.id } }],
[],
- done,
);
});
});
@@ -184,28 +175,18 @@ describe('Actions Notes Store', () => {
});
describe('closeMergeRequest', () => {
- it('sets state as closed', (done) => {
- store
- .dispatch('closeIssuable', { notesData: { closeIssuePath: '' } })
- .then(() => {
- expect(store.state.noteableData.state).toEqual('closed');
- expect(store.state.isToggleStateButtonLoading).toEqual(false);
- done();
- })
- .catch(done.fail);
+ it('sets state as closed', async () => {
+ await store.dispatch('closeIssuable', { notesData: { closeIssuePath: '' } });
+ expect(store.state.noteableData.state).toEqual('closed');
+ expect(store.state.isToggleStateButtonLoading).toEqual(false);
});
});
describe('reopenMergeRequest', () => {
- it('sets state as reopened', (done) => {
- store
- .dispatch('reopenIssuable', { notesData: { reopenIssuePath: '' } })
- .then(() => {
- expect(store.state.noteableData.state).toEqual('reopened');
- expect(store.state.isToggleStateButtonLoading).toEqual(false);
- done();
- })
- .catch(done.fail);
+ it('sets state as reopened', async () => {
+ await store.dispatch('reopenIssuable', { notesData: { reopenIssuePath: '' } });
+ expect(store.state.noteableData.state).toEqual('reopened');
+ expect(store.state.isToggleStateButtonLoading).toEqual(false);
});
});
});
@@ -222,42 +203,39 @@ describe('Actions Notes Store', () => {
});
describe('toggleStateButtonLoading', () => {
- it('should set loading as true', (done) => {
- testAction(
+ it('should set loading as true', () => {
+ return testAction(
actions.toggleStateButtonLoading,
true,
{},
[{ type: 'TOGGLE_STATE_BUTTON_LOADING', payload: true }],
[],
- done,
);
});
- it('should set loading as false', (done) => {
- testAction(
+ it('should set loading as false', () => {
+ return testAction(
actions.toggleStateButtonLoading,
false,
{},
[{ type: 'TOGGLE_STATE_BUTTON_LOADING', payload: false }],
[],
- done,
);
});
});
describe('toggleIssueLocalState', () => {
- it('sets issue state as closed', (done) => {
- testAction(actions.toggleIssueLocalState, 'closed', {}, [{ type: 'CLOSE_ISSUE' }], [], done);
+ it('sets issue state as closed', () => {
+ return testAction(actions.toggleIssueLocalState, 'closed', {}, [{ type: 'CLOSE_ISSUE' }], []);
});
- it('sets issue state as reopened', (done) => {
- testAction(
+ it('sets issue state as reopened', () => {
+ return testAction(
actions.toggleIssueLocalState,
'reopened',
{},
[{ type: 'REOPEN_ISSUE' }],
[],
- done,
);
});
});
@@ -291,8 +269,8 @@ describe('Actions Notes Store', () => {
return store.dispatch('stopPolling');
};
- beforeEach((done) => {
- store.dispatch('setNotesData', notesDataMock).then(done).catch(done.fail);
+ beforeEach(() => {
+ return store.dispatch('setNotesData', notesDataMock);
});
afterEach(() => {
@@ -405,14 +383,13 @@ describe('Actions Notes Store', () => {
});
describe('setNotesFetchedState', () => {
- it('should set notes fetched state', (done) => {
- testAction(
+ it('should set notes fetched state', () => {
+ return testAction(
actions.setNotesFetchedState,
true,
{},
[{ type: 'SET_NOTES_FETCHED_STATE', payload: true }],
[],
- done,
);
});
});
@@ -432,10 +409,10 @@ describe('Actions Notes Store', () => {
document.body.setAttribute('data-page', '');
});
- it('commits DELETE_NOTE and dispatches updateMergeRequestWidget', (done) => {
+ it('commits DELETE_NOTE and dispatches updateMergeRequestWidget', () => {
const note = { path: endpoint, id: 1 };
- testAction(
+ return testAction(
actions.removeNote,
note,
store.state,
@@ -453,16 +430,15 @@ describe('Actions Notes Store', () => {
type: 'updateResolvableDiscussionsCounts',
},
],
- done,
);
});
- it('dispatches removeDiscussionsFromDiff on merge request page', (done) => {
+ it('dispatches removeDiscussionsFromDiff on merge request page', () => {
const note = { path: endpoint, id: 1 };
document.body.setAttribute('data-page', 'projects:merge_requests:show');
- testAction(
+ return testAction(
actions.removeNote,
note,
store.state,
@@ -483,7 +459,6 @@ describe('Actions Notes Store', () => {
type: 'diffs/removeDiscussionsFromDiff',
},
],
- done,
);
});
});
@@ -503,10 +478,10 @@ describe('Actions Notes Store', () => {
document.body.setAttribute('data-page', '');
});
- it('dispatches removeNote', (done) => {
+ it('dispatches removeNote', () => {
const note = { path: endpoint, id: 1 };
- testAction(
+ return testAction(
actions.deleteNote,
note,
{},
@@ -520,7 +495,6 @@ describe('Actions Notes Store', () => {
},
},
],
- done,
);
});
});
@@ -536,8 +510,8 @@ describe('Actions Notes Store', () => {
axiosMock.onAny().reply(200, res);
});
- it('commits ADD_NEW_NOTE and dispatches updateMergeRequestWidget', (done) => {
- testAction(
+ it('commits ADD_NEW_NOTE and dispatches updateMergeRequestWidget', () => {
+ return testAction(
actions.createNewNote,
{ endpoint: `${TEST_HOST}`, data: {} },
store.state,
@@ -558,7 +532,6 @@ describe('Actions Notes Store', () => {
type: 'updateResolvableDiscussionsCounts',
},
],
- done,
);
});
});
@@ -572,14 +545,13 @@ describe('Actions Notes Store', () => {
axiosMock.onAny().replyOnce(200, res);
});
- it('does not commit ADD_NEW_NOTE or dispatch updateMergeRequestWidget', (done) => {
- testAction(
+ it('does not commit ADD_NEW_NOTE or dispatch updateMergeRequestWidget', () => {
+ return testAction(
actions.createNewNote,
{ endpoint: `${TEST_HOST}`, data: {} },
store.state,
[],
[],
- done,
);
});
});
@@ -595,8 +567,8 @@ describe('Actions Notes Store', () => {
});
describe('as note', () => {
- it('commits UPDATE_NOTE and dispatches updateMergeRequestWidget', (done) => {
- testAction(
+ it('commits UPDATE_NOTE and dispatches updateMergeRequestWidget', () => {
+ return testAction(
actions.toggleResolveNote,
{ endpoint: `${TEST_HOST}`, isResolved: true, discussion: false },
store.state,
@@ -614,14 +586,13 @@ describe('Actions Notes Store', () => {
type: 'updateMergeRequestWidget',
},
],
- done,
);
});
});
describe('as discussion', () => {
- it('commits UPDATE_DISCUSSION and dispatches updateMergeRequestWidget', (done) => {
- testAction(
+ it('commits UPDATE_DISCUSSION and dispatches updateMergeRequestWidget', () => {
+ return testAction(
actions.toggleResolveNote,
{ endpoint: `${TEST_HOST}`, isResolved: true, discussion: true },
store.state,
@@ -639,7 +610,6 @@ describe('Actions Notes Store', () => {
type: 'updateMergeRequestWidget',
},
],
- done,
);
});
});
@@ -656,41 +626,38 @@ describe('Actions Notes Store', () => {
});
describe('setCommentsDisabled', () => {
- it('should set comments disabled state', (done) => {
- testAction(
+ it('should set comments disabled state', () => {
+ return testAction(
actions.setCommentsDisabled,
true,
null,
[{ type: 'DISABLE_COMMENTS', payload: true }],
[],
- done,
);
});
});
describe('updateResolvableDiscussionsCounts', () => {
- it('commits UPDATE_RESOLVABLE_DISCUSSIONS_COUNTS', (done) => {
- testAction(
+ it('commits UPDATE_RESOLVABLE_DISCUSSIONS_COUNTS', () => {
+ return testAction(
actions.updateResolvableDiscussionsCounts,
null,
{},
[{ type: 'UPDATE_RESOLVABLE_DISCUSSIONS_COUNTS' }],
[],
- done,
);
});
});
describe('convertToDiscussion', () => {
- it('commits CONVERT_TO_DISCUSSION with noteId', (done) => {
+ it('commits CONVERT_TO_DISCUSSION with noteId', () => {
const noteId = 'dummy-note-id';
- testAction(
+ return testAction(
actions.convertToDiscussion,
noteId,
{},
[{ type: 'CONVERT_TO_DISCUSSION', payload: noteId }],
[],
- done,
);
});
});
@@ -786,11 +753,11 @@ describe('Actions Notes Store', () => {
describe('replyToDiscussion', () => {
const payload = { endpoint: TEST_HOST, data: {} };
- it('updates discussion if response contains disussion', (done) => {
+ it('updates discussion if response contains disussion', () => {
const discussion = { notes: [] };
axiosMock.onAny().reply(200, { discussion });
- testAction(
+ return testAction(
actions.replyToDiscussion,
payload,
{
@@ -802,15 +769,14 @@ describe('Actions Notes Store', () => {
{ type: 'startTaskList' },
{ type: 'updateResolvableDiscussionsCounts' },
],
- done,
);
});
- it('adds a reply to a discussion', (done) => {
+ it('adds a reply to a discussion', () => {
const res = {};
axiosMock.onAny().reply(200, res);
- testAction(
+ return testAction(
actions.replyToDiscussion,
payload,
{
@@ -818,21 +784,19 @@ describe('Actions Notes Store', () => {
},
[{ type: mutationTypes.ADD_NEW_REPLY_TO_DISCUSSION, payload: res }],
[],
- done,
);
});
});
describe('removeConvertedDiscussion', () => {
- it('commits CONVERT_TO_DISCUSSION with noteId', (done) => {
+ it('commits CONVERT_TO_DISCUSSION with noteId', () => {
const noteId = 'dummy-id';
- testAction(
+ return testAction(
actions.removeConvertedDiscussion,
noteId,
{},
[{ type: 'REMOVE_CONVERTED_DISCUSSION', payload: noteId }],
[],
- done,
);
});
});
@@ -849,8 +813,8 @@ describe('Actions Notes Store', () => {
};
});
- it('when unresolved, dispatches action', (done) => {
- testAction(
+ it('when unresolved, dispatches action', () => {
+ return testAction(
actions.resolveDiscussion,
{ discussionId },
{ ...state, ...getters },
@@ -865,20 +829,18 @@ describe('Actions Notes Store', () => {
},
},
],
- done,
);
});
- it('when resolved, does nothing', (done) => {
+ it('when resolved, does nothing', () => {
getters.isDiscussionResolved = (id) => id === discussionId;
- testAction(
+ return testAction(
actions.resolveDiscussion,
{ discussionId },
{ ...state, ...getters },
[],
[],
- done,
);
});
});
@@ -891,22 +853,17 @@ describe('Actions Notes Store', () => {
const res = { errors: { something: ['went wrong'] } };
const error = { message: 'Unprocessable entity', response: { data: res } };
- it('throws an error', (done) => {
- actions
- .saveNote(
+ it('throws an error', async () => {
+ await expect(
+ actions.saveNote(
{
commit() {},
dispatch: () => Promise.reject(error),
},
payload,
- )
- .then(() => done.fail('Expected error to be thrown!'))
- .catch((err) => {
- expect(err).toBe(error);
- expect(createFlash).not.toHaveBeenCalled();
- })
- .then(done)
- .catch(done.fail);
+ ),
+ ).rejects.toEqual(error);
+ expect(createFlash).not.toHaveBeenCalled();
});
});
@@ -914,46 +871,35 @@ describe('Actions Notes Store', () => {
const res = { errors: { base: ['something went wrong'] } };
const error = { message: 'Unprocessable entity', response: { data: res } };
- it('sets flash alert using errors.base message', (done) => {
- actions
- .saveNote(
- {
- commit() {},
- dispatch: () => Promise.reject(error),
- },
- { ...payload, flashContainer },
- )
- .then((resp) => {
- expect(resp.hasFlash).toBe(true);
- expect(createFlash).toHaveBeenCalledWith({
- message: 'Your comment could not be submitted because something went wrong',
- parent: flashContainer,
- });
- })
- .catch(() => done.fail('Expected success response!'))
- .then(done)
- .catch(done.fail);
+ it('sets flash alert using errors.base message', async () => {
+ const resp = await actions.saveNote(
+ {
+ commit() {},
+ dispatch: () => Promise.reject(error),
+ },
+ { ...payload, flashContainer },
+ );
+ expect(resp.hasFlash).toBe(true);
+ expect(createFlash).toHaveBeenCalledWith({
+ message: 'Your comment could not be submitted because something went wrong',
+ parent: flashContainer,
+ });
});
});
describe('if response contains no errors', () => {
const res = { valid: true };
- it('returns the response', (done) => {
- actions
- .saveNote(
- {
- commit() {},
- dispatch: () => Promise.resolve(res),
- },
- payload,
- )
- .then((data) => {
- expect(data).toBe(res);
- expect(createFlash).not.toHaveBeenCalled();
- })
- .then(done)
- .catch(done.fail);
+ it('returns the response', async () => {
+ const data = await actions.saveNote(
+ {
+ commit() {},
+ dispatch: () => Promise.resolve(res),
+ },
+ payload,
+ );
+ expect(data).toBe(res);
+ expect(createFlash).not.toHaveBeenCalled();
});
});
});
@@ -970,19 +916,17 @@ describe('Actions Notes Store', () => {
flashContainer = {};
});
- const testSubmitSuggestion = (done, expectFn) => {
- actions
- .submitSuggestion(
- { commit, dispatch },
- { discussionId, noteId, suggestionId, flashContainer },
- )
- .then(expectFn)
- .then(done)
- .catch(done.fail);
+ const testSubmitSuggestion = async (expectFn) => {
+ await actions.submitSuggestion(
+ { commit, dispatch },
+ { discussionId, noteId, suggestionId, flashContainer },
+ );
+
+ expectFn();
};
- it('when service success, commits and resolves discussion', (done) => {
- testSubmitSuggestion(done, () => {
+ it('when service success, commits and resolves discussion', () => {
+ testSubmitSuggestion(() => {
expect(commit.mock.calls).toEqual([
[mutationTypes.SET_RESOLVING_DISCUSSION, true],
[mutationTypes.SET_RESOLVING_DISCUSSION, false],
@@ -997,12 +941,12 @@ describe('Actions Notes Store', () => {
});
});
- it('when service fails, flashes error message', (done) => {
+ it('when service fails, flashes error message', () => {
const response = { response: { data: { message: TEST_ERROR_MESSAGE } } };
Api.applySuggestion.mockReturnValue(Promise.reject(response));
- testSubmitSuggestion(done, () => {
+ return testSubmitSuggestion(() => {
expect(commit.mock.calls).toEqual([
[mutationTypes.SET_RESOLVING_DISCUSSION, true],
[mutationTypes.SET_RESOLVING_DISCUSSION, false],
@@ -1015,12 +959,12 @@ describe('Actions Notes Store', () => {
});
});
- it('when service fails, and no error message available, uses default message', (done) => {
+ it('when service fails, and no error message available, uses default message', () => {
const response = { response: 'foo' };
Api.applySuggestion.mockReturnValue(Promise.reject(response));
- testSubmitSuggestion(done, () => {
+ return testSubmitSuggestion(() => {
expect(commit.mock.calls).toEqual([
[mutationTypes.SET_RESOLVING_DISCUSSION, true],
[mutationTypes.SET_RESOLVING_DISCUSSION, false],
@@ -1033,10 +977,10 @@ describe('Actions Notes Store', () => {
});
});
- it('when resolve discussion fails, fail gracefully', (done) => {
+ it('when resolve discussion fails, fail gracefully', () => {
dispatch.mockReturnValue(Promise.reject());
- testSubmitSuggestion(done, () => {
+ return testSubmitSuggestion(() => {
expect(createFlash).not.toHaveBeenCalled();
});
});
@@ -1056,16 +1000,14 @@ describe('Actions Notes Store', () => {
flashContainer = {};
});
- const testSubmitSuggestionBatch = (done, expectFn) => {
- actions
- .submitSuggestionBatch({ commit, dispatch, state }, { flashContainer })
- .then(expectFn)
- .then(done)
- .catch(done.fail);
+ const testSubmitSuggestionBatch = async (expectFn) => {
+ await actions.submitSuggestionBatch({ commit, dispatch, state }, { flashContainer });
+
+ expectFn();
};
- it('when service succeeds, commits, resolves discussions, resets batch and applying batch state', (done) => {
- testSubmitSuggestionBatch(done, () => {
+ it('when service succeeds, commits, resolves discussions, resets batch and applying batch state', () => {
+ testSubmitSuggestionBatch(() => {
expect(commit.mock.calls).toEqual([
[mutationTypes.SET_APPLYING_BATCH_STATE, true],
[mutationTypes.SET_RESOLVING_DISCUSSION, true],
@@ -1085,12 +1027,12 @@ describe('Actions Notes Store', () => {
});
});
- it('when service fails, flashes error message, resets applying batch state', (done) => {
+ it('when service fails, flashes error message, resets applying batch state', () => {
const response = { response: { data: { message: TEST_ERROR_MESSAGE } } };
Api.applySuggestionBatch.mockReturnValue(Promise.reject(response));
- testSubmitSuggestionBatch(done, () => {
+ testSubmitSuggestionBatch(() => {
expect(commit.mock.calls).toEqual([
[mutationTypes.SET_APPLYING_BATCH_STATE, true],
[mutationTypes.SET_RESOLVING_DISCUSSION, true],
@@ -1106,12 +1048,12 @@ describe('Actions Notes Store', () => {
});
});
- it('when service fails, and no error message available, uses default message', (done) => {
+ it('when service fails, and no error message available, uses default message', () => {
const response = { response: 'foo' };
Api.applySuggestionBatch.mockReturnValue(Promise.reject(response));
- testSubmitSuggestionBatch(done, () => {
+ testSubmitSuggestionBatch(() => {
expect(commit.mock.calls).toEqual([
[mutationTypes.SET_APPLYING_BATCH_STATE, true],
[mutationTypes.SET_RESOLVING_DISCUSSION, true],
@@ -1128,10 +1070,10 @@ describe('Actions Notes Store', () => {
});
});
- it('when resolve discussions fails, fails gracefully, resets batch and applying batch state', (done) => {
+ it('when resolve discussions fails, fails gracefully, resets batch and applying batch state', () => {
dispatch.mockReturnValue(Promise.reject());
- testSubmitSuggestionBatch(done, () => {
+ testSubmitSuggestionBatch(() => {
expect(commit.mock.calls).toEqual([
[mutationTypes.SET_APPLYING_BATCH_STATE, true],
[mutationTypes.SET_RESOLVING_DISCUSSION, true],
@@ -1148,14 +1090,13 @@ describe('Actions Notes Store', () => {
describe('addSuggestionInfoToBatch', () => {
const suggestionInfo = batchSuggestionsInfoMock[0];
- it("adds a suggestion's info to the current batch", (done) => {
- testAction(
+ it("adds a suggestion's info to the current batch", () => {
+ return testAction(
actions.addSuggestionInfoToBatch,
suggestionInfo,
{ batchSuggestionsInfo: [] },
[{ type: 'ADD_SUGGESTION_TO_BATCH', payload: suggestionInfo }],
[],
- done,
);
});
});
@@ -1163,14 +1104,13 @@ describe('Actions Notes Store', () => {
describe('removeSuggestionInfoFromBatch', () => {
const suggestionInfo = batchSuggestionsInfoMock[0];
- it("removes a suggestion's info the current batch", (done) => {
- testAction(
+ it("removes a suggestion's info the current batch", () => {
+ return testAction(
actions.removeSuggestionInfoFromBatch,
suggestionInfo.suggestionId,
{ batchSuggestionsInfo: [suggestionInfo] },
[{ type: 'REMOVE_SUGGESTION_FROM_BATCH', payload: suggestionInfo.suggestionId }],
[],
- done,
);
});
});
@@ -1209,8 +1149,8 @@ describe('Actions Notes Store', () => {
});
describe('setDiscussionSortDirection', () => {
- it('calls the correct mutation with the correct args', (done) => {
- testAction(
+ it('calls the correct mutation with the correct args', () => {
+ return testAction(
actions.setDiscussionSortDirection,
{ direction: notesConstants.DESC, persist: false },
{},
@@ -1221,20 +1161,18 @@ describe('Actions Notes Store', () => {
},
],
[],
- done,
);
});
});
describe('setSelectedCommentPosition', () => {
- it('calls the correct mutation with the correct args', (done) => {
- testAction(
+ it('calls the correct mutation with the correct args', () => {
+ return testAction(
actions.setSelectedCommentPosition,
{},
{},
[{ type: mutationTypes.SET_SELECTED_COMMENT_POSITION, payload: {} }],
[],
- done,
);
});
});
@@ -1248,9 +1186,9 @@ describe('Actions Notes Store', () => {
};
describe('if response contains no errors', () => {
- it('dispatches requestDeleteDescriptionVersion', (done) => {
+ it('dispatches requestDeleteDescriptionVersion', () => {
axiosMock.onDelete(endpoint).replyOnce(200);
- testAction(
+ return testAction(
actions.softDeleteDescriptionVersion,
payload,
{},
@@ -1264,35 +1202,33 @@ describe('Actions Notes Store', () => {
payload: payload.versionId,
},
],
- done,
);
});
});
describe('if response contains errors', () => {
const errorMessage = 'Request failed with status code 503';
- it('dispatches receiveDeleteDescriptionVersionError and throws an error', (done) => {
+ it('dispatches receiveDeleteDescriptionVersionError and throws an error', async () => {
axiosMock.onDelete(endpoint).replyOnce(503);
- testAction(
- actions.softDeleteDescriptionVersion,
- payload,
- {},
- [],
- [
- {
- type: 'requestDeleteDescriptionVersion',
- },
- {
- type: 'receiveDeleteDescriptionVersionError',
- payload: new Error(errorMessage),
- },
- ],
- )
- .then(() => done.fail('Expected error to be thrown'))
- .catch(() => {
- expect(createFlash).toHaveBeenCalled();
- done();
- });
+ await expect(
+ testAction(
+ actions.softDeleteDescriptionVersion,
+ payload,
+ {},
+ [],
+ [
+ {
+ type: 'requestDeleteDescriptionVersion',
+ },
+ {
+ type: 'receiveDeleteDescriptionVersionError',
+ payload: new Error(errorMessage),
+ },
+ ],
+ ),
+ ).rejects.toEqual(new Error());
+
+ expect(createFlash).toHaveBeenCalled();
});
});
});
@@ -1306,14 +1242,13 @@ describe('Actions Notes Store', () => {
});
describe('updateAssignees', () => {
- it('update the assignees state', (done) => {
- testAction(
+ it('update the assignees state', () => {
+ return testAction(
actions.updateAssignees,
[userDataMock.id],
{ state: noteableDataMock },
[{ type: mutationTypes.UPDATE_ASSIGNEES, payload: [userDataMock.id] }],
[],
- done,
);
});
});
@@ -1376,28 +1311,26 @@ describe('Actions Notes Store', () => {
});
describe('updateDiscussionPosition', () => {
- it('update the assignees state', (done) => {
+ it('update the assignees state', () => {
const updatedPosition = { discussionId: 1, position: { test: true } };
- testAction(
+ return testAction(
actions.updateDiscussionPosition,
updatedPosition,
{ state: { discussions: [] } },
[{ type: mutationTypes.UPDATE_DISCUSSION_POSITION, payload: updatedPosition }],
[],
- done,
);
});
});
describe('setFetchingState', () => {
- it('commits SET_NOTES_FETCHING_STATE', (done) => {
- testAction(
+ it('commits SET_NOTES_FETCHING_STATE', () => {
+ return testAction(
actions.setFetchingState,
true,
null,
[{ type: mutationTypes.SET_NOTES_FETCHING_STATE, payload: true }],
[],
- done,
);
});
});
@@ -1409,9 +1342,9 @@ describe('Actions Notes Store', () => {
window.gon = {};
});
- it('updates the discussions and dispatches `updateResolvableDiscussionsCounts`', (done) => {
+ it('updates the discussions and dispatches `updateResolvableDiscussionsCounts`', () => {
axiosMock.onAny().reply(200, { discussion });
- testAction(
+ return testAction(
actions.fetchDiscussions,
{},
null,
@@ -1420,14 +1353,13 @@ describe('Actions Notes Store', () => {
{ type: mutationTypes.SET_FETCHING_DISCUSSIONS, payload: false },
],
[{ type: 'updateResolvableDiscussionsCounts' }],
- done,
);
});
- it('dispatches `fetchDiscussionsBatch` action if `paginatedIssueDiscussions` feature flag is enabled', (done) => {
+ it('dispatches `fetchDiscussionsBatch` action if `paginatedIssueDiscussions` feature flag is enabled', () => {
window.gon = { features: { paginatedIssueDiscussions: true } };
- testAction(
+ return testAction(
actions.fetchDiscussions,
{ path: 'test-path', filter: 'test-filter', persistFilter: 'test-persist-filter' },
null,
@@ -1444,7 +1376,6 @@ describe('Actions Notes Store', () => {
},
},
],
- done,
);
});
});
@@ -1458,9 +1389,9 @@ describe('Actions Notes Store', () => {
const actionPayload = { config, path: 'test-path', perPage: 20 };
- it('updates the discussions and dispatches `updateResolvableDiscussionsCounts if there are no headers', (done) => {
+ it('updates the discussions and dispatches `updateResolvableDiscussionsCounts if there are no headers', () => {
axiosMock.onAny().reply(200, { discussion }, {});
- testAction(
+ return testAction(
actions.fetchDiscussionsBatch,
actionPayload,
null,
@@ -1469,13 +1400,12 @@ describe('Actions Notes Store', () => {
{ type: mutationTypes.SET_FETCHING_DISCUSSIONS, payload: false },
],
[{ type: 'updateResolvableDiscussionsCounts' }],
- done,
);
});
- it('dispatches itself if there is `x-next-page-cursor` header', (done) => {
+ it('dispatches itself if there is `x-next-page-cursor` header', () => {
axiosMock.onAny().reply(200, { discussion }, { 'x-next-page-cursor': 1 });
- testAction(
+ return testAction(
actions.fetchDiscussionsBatch,
actionPayload,
null,
@@ -1486,7 +1416,6 @@ describe('Actions Notes Store', () => {
payload: { ...actionPayload, perPage: 30, cursor: 1 },
},
],
- done,
);
});
});
diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/components/delete_button_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/components/delete_button_spec.js
index 6d7bf528495..ad67128502a 100644
--- a/spec/frontend/packages_and_registries/container_registry/explorer/components/delete_button_spec.js
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/components/delete_button_spec.js
@@ -1,7 +1,7 @@
-import { GlButton } from '@gitlab/ui';
+import { GlButton, GlTooltip, GlSprintf } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import component from '~/packages_and_registries/container_registry/explorer/components/delete_button.vue';
+import { LIST_DELETE_BUTTON_DISABLED_FOR_MIGRATION } from '~/packages_and_registries/container_registry/explorer/constants/list';
describe('delete_button', () => {
let wrapper;
@@ -12,6 +12,7 @@ describe('delete_button', () => {
};
const findButton = () => wrapper.find(GlButton);
+ const findTooltip = () => wrapper.find(GlTooltip);
const mountComponent = (props) => {
wrapper = shallowMount(component, {
@@ -19,8 +20,9 @@ describe('delete_button', () => {
...defaultProps,
...props,
},
- directives: {
- GlTooltip: createMockDirective(),
+ stubs: {
+ GlTooltip,
+ GlSprintf,
},
});
};
@@ -33,41 +35,50 @@ describe('delete_button', () => {
describe('tooltip', () => {
it('the title is controlled by tooltipTitle prop', () => {
mountComponent();
- const tooltip = getBinding(wrapper.element, 'gl-tooltip');
+ const tooltip = findTooltip();
expect(tooltip).toBeDefined();
- expect(tooltip.value.title).toBe(defaultProps.tooltipTitle);
+ expect(tooltip.text()).toBe(defaultProps.tooltipTitle);
});
it('is disabled when tooltipTitle is disabled', () => {
mountComponent({ tooltipDisabled: true });
- const tooltip = getBinding(wrapper.element, 'gl-tooltip');
- expect(tooltip.value.disabled).toBe(true);
+ expect(findTooltip().props('disabled')).toBe(true);
});
- describe('button', () => {
- it('exists', () => {
- mountComponent();
- expect(findButton().exists()).toBe(true);
+ it('works with a link', () => {
+ mountComponent({
+ tooltipTitle: LIST_DELETE_BUTTON_DISABLED_FOR_MIGRATION,
+ tooltipLink: 'foo',
});
+ expect(findTooltip().text()).toMatchInterpolatedText(
+ LIST_DELETE_BUTTON_DISABLED_FOR_MIGRATION,
+ );
+ });
+ });
- it('has the correct props/attributes bound', () => {
- mountComponent({ disabled: true });
- expect(findButton().attributes()).toMatchObject({
- 'aria-label': 'Foo title',
- icon: 'remove',
- title: 'Foo title',
- variant: 'danger',
- disabled: 'true',
- category: 'secondary',
- });
- });
+ describe('button', () => {
+ it('exists', () => {
+ mountComponent();
+ expect(findButton().exists()).toBe(true);
+ });
- it('emits a delete event', () => {
- mountComponent();
- expect(wrapper.emitted('delete')).toEqual(undefined);
- findButton().vm.$emit('click');
- expect(wrapper.emitted('delete')).toEqual([[]]);
+ it('has the correct props/attributes bound', () => {
+ mountComponent({ disabled: true });
+ expect(findButton().attributes()).toMatchObject({
+ 'aria-label': 'Foo title',
+ icon: 'remove',
+ title: 'Foo title',
+ variant: 'danger',
+ disabled: 'true',
+ category: 'secondary',
});
});
+
+ it('emits a delete event', () => {
+ mountComponent();
+ expect(wrapper.emitted('delete')).toEqual(undefined);
+ findButton().vm.$emit('click');
+ expect(wrapper.emitted('delete')).toEqual([[]]);
+ });
});
});
diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/image_list_row_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/image_list_row_spec.js
index 411bef54e40..690d827ec67 100644
--- a/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/image_list_row_spec.js
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/image_list_row_spec.js
@@ -10,6 +10,7 @@ import {
LIST_DELETE_BUTTON_DISABLED,
REMOVE_REPOSITORY_LABEL,
IMAGE_DELETE_SCHEDULED_STATUS,
+ IMAGE_MIGRATING_STATE,
SCHEDULED_STATUS,
ROOT_IMAGE_TEXT,
} from '~/packages_and_registries/container_registry/explorer/constants';
@@ -41,6 +42,9 @@ describe('Image List Row', () => {
item,
...props,
},
+ provide: {
+ config: {},
+ },
directives: {
GlTooltip: createMockDirective(),
},
@@ -178,6 +182,12 @@ describe('Image List Row', () => {
expect(findDeleteBtn().props('disabled')).toBe(state);
},
);
+
+ it('is disabled when migrationState is importing', () => {
+ mountComponent({ item: { ...item, migrationState: IMAGE_MIGRATING_STATE } });
+
+ expect(findDeleteBtn().props('disabled')).toBe(true);
+ });
});
describe('tags count', () => {
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 c91a9c0f0fb..7d09c09d03b 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
@@ -1,4 +1,4 @@
-import { GlSprintf } from '@gitlab/ui';
+import { GlSprintf, GlLink } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
import Component from '~/packages_and_registries/container_registry/explorer/components/list_page/registry_header.vue';
@@ -6,6 +6,7 @@ import {
CONTAINER_REGISTRY_TITLE,
LIST_INTRO_TEXT,
EXPIRATION_POLICY_DISABLED_TEXT,
+ SET_UP_CLEANUP,
} from '~/packages_and_registries/container_registry/explorer/constants';
import TitleArea from '~/vue_shared/components/registry/title_area.vue';
@@ -21,6 +22,7 @@ describe('registry_header', () => {
const findCommandsSlot = () => wrapper.find('[data-testid="commands-slot"]');
const findImagesCountSubHeader = () => wrapper.find('[data-testid="images-count"]');
const findExpirationPolicySubHeader = () => wrapper.find('[data-testid="expiration-policy"]');
+ const findSetupCleanUpLink = () => wrapper.findComponent(GlLink);
const mountComponent = async (propsData, slots) => {
wrapper = shallowMount(Component, {
@@ -88,6 +90,7 @@ describe('registry_header', () => {
});
const text = findExpirationPolicySubHeader();
+
expect(text.exists()).toBe(true);
expect(text.props()).toMatchObject({
text: EXPIRATION_POLICY_DISABLED_TEXT,
@@ -100,12 +103,17 @@ describe('registry_header', () => {
await mountComponent({
expirationPolicy: { enabled: true },
expirationPolicyHelpPagePath: 'foo',
+ showCleanupPolicyLink: true,
imagesCount: 1,
});
const text = findExpirationPolicySubHeader();
+ const cleanupLink = findSetupCleanUpLink();
+
expect(text.exists()).toBe(true);
expect(text.props('text')).toBe('Expiration policy will run in ');
+ expect(cleanupLink.exists()).toBe(true);
+ expect(cleanupLink.text()).toBe(SET_UP_CLEANUP);
});
it('when the expiration policy is completely disabled', async () => {
await mountComponent({
diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/mock_data.js b/spec/frontend/packages_and_registries/container_registry/explorer/mock_data.js
index fda1db4b7e1..7e6f88fe5bc 100644
--- a/spec/frontend/packages_and_registries/container_registry/explorer/mock_data.js
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/mock_data.js
@@ -5,6 +5,7 @@ export const imagesListResponse = [
name: 'rails-12009',
path: 'gitlab-org/gitlab-test/rails-12009',
status: null,
+ migrationState: 'default',
location: '0.0.0.0:5000/gitlab-org/gitlab-test/rails-12009',
canDelete: true,
createdAt: '2020-11-03T13:29:21Z',
@@ -17,6 +18,7 @@ export const imagesListResponse = [
name: 'rails-20572',
path: 'gitlab-org/gitlab-test/rails-20572',
status: null,
+ migrationState: 'default',
location: '0.0.0.0:5000/gitlab-org/gitlab-test/rails-20572',
canDelete: true,
createdAt: '2020-09-21T06:57:43Z',
diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/pages/list_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/pages/list_spec.js
index da4bfcde217..79403d29d18 100644
--- a/spec/frontend/packages_and_registries/container_registry/explorer/pages/list_spec.js
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/pages/list_spec.js
@@ -6,7 +6,6 @@ import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import getContainerRepositoriesQuery from 'shared_queries/container_registry/get_container_repositories.query.graphql';
-import CleanupPolicyEnabledAlert from '~/packages_and_registries/shared/components/cleanup_policy_enabled_alert.vue';
import { FILTERED_SEARCH_TERM } from '~/packages_and_registries/shared/constants';
import DeleteImage from '~/packages_and_registries/container_registry/explorer/components/delete_image.vue';
import CliCommands from '~/packages_and_registries/shared/components/cli_commands.vue';
@@ -58,7 +57,6 @@ describe('List Page', () => {
const findPersistedSearch = () => wrapper.findComponent(PersistedSearch);
const findEmptySearchMessage = () => wrapper.find('[data-testid="emptySearch"]');
const findDeleteImage = () => wrapper.findComponent(DeleteImage);
- const findCleanupAlert = () => wrapper.findComponent(CleanupPolicyEnabledAlert);
const fireFirstSortUpdate = () => {
findPersistedSearch().vm.$emit('update', { sort: 'UPDATED_DESC', filters: [] });
@@ -511,33 +509,4 @@ describe('List Page', () => {
testTrackingCall('confirm_delete');
});
});
-
- describe('cleanup is on alert', () => {
- it('exist when showCleanupPolicyOnAlert is true and has the correct props', async () => {
- mountComponent({
- config: {
- showCleanupPolicyOnAlert: true,
- projectPath: 'foo',
- isGroupPage: false,
- cleanupPoliciesSettingsPath: 'bar',
- },
- });
-
- await waitForApolloRequestRender();
-
- expect(findCleanupAlert().exists()).toBe(true);
- expect(findCleanupAlert().props()).toMatchObject({
- projectPath: 'foo',
- cleanupPoliciesSettingsPath: 'bar',
- });
- });
-
- it('is hidden when showCleanupPolicyOnAlert is false', async () => {
- mountComponent();
-
- await waitForApolloRequestRender();
-
- expect(findCleanupAlert().exists()).toBe(false);
- });
- });
});
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 79894e25889..dbe9793fb8c 100644
--- a/spec/frontend/packages_and_registries/dependency_proxy/app_spec.js
+++ b/spec/frontend/packages_and_registries/dependency_proxy/app_spec.js
@@ -1,19 +1,26 @@
import {
+ GlAlert,
+ GlDropdown,
+ GlDropdownItem,
GlFormInputGroup,
GlFormGroup,
+ GlModal,
GlSkeletonLoader,
GlSprintf,
GlEmptyState,
} from '@gitlab/ui';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
+import MockAdapter from 'axios-mock-adapter';
import createMockApollo from 'helpers/mock_apollo_helper';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { stripTypenames } from 'helpers/graphql_helpers';
import waitForPromises from 'helpers/wait_for_promises';
import { GRAPHQL_PAGE_SIZE } from '~/packages_and_registries/dependency_proxy/constants';
+import axios from '~/lib/utils/axios_utils';
import DependencyProxyApp from '~/packages_and_registries/dependency_proxy/app.vue';
+import TitleArea from '~/vue_shared/components/registry/title_area.vue';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import ManifestsList from '~/packages_and_registries/dependency_proxy/components/manifests_list.vue';
@@ -21,13 +28,25 @@ import getDependencyProxyDetailsQuery from '~/packages_and_registries/dependency
import { proxyDetailsQuery, proxyData, pagination, proxyManifests } from './mock_data';
+const dummyApiVersion = 'v3000';
+const dummyGrouptId = 1;
+const dummyUrlRoot = '/gitlab';
+const dummyGon = {
+ api_version: dummyApiVersion,
+ relative_url_root: dummyUrlRoot,
+};
+let originalGon;
+const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/groups/${dummyGrouptId}/dependency_proxy/cache`;
+
describe('DependencyProxyApp', () => {
let wrapper;
let apolloProvider;
let resolver;
+ let mock;
const provideDefaults = {
groupPath: 'gitlab-org',
+ groupId: dummyGrouptId,
dependencyProxyAvailable: true,
noManifestsIllustration: 'noManifestsIllustration',
};
@@ -43,9 +62,14 @@ describe('DependencyProxyApp', () => {
apolloProvider,
provide,
stubs: {
+ GlAlert,
+ GlDropdown,
+ GlDropdownItem,
GlFormInputGroup,
GlFormGroup,
+ GlModal,
GlSprintf,
+ TitleArea,
},
});
}
@@ -59,13 +83,24 @@ describe('DependencyProxyApp', () => {
const findProxyCountText = () => wrapper.findByTestId('proxy-count');
const findManifestList = () => wrapper.findComponent(ManifestsList);
const findEmptyState = () => wrapper.findComponent(GlEmptyState);
+ const findClearCacheDropdownList = () => wrapper.findComponent(GlDropdown);
+ const findClearCacheModal = () => wrapper.findComponent(GlModal);
+ const findClearCacheAlert = () => wrapper.findComponent(GlAlert);
beforeEach(() => {
resolver = jest.fn().mockResolvedValue(proxyDetailsQuery());
+
+ originalGon = window.gon;
+ window.gon = { ...dummyGon };
+
+ mock = new MockAdapter(axios);
+ mock.onDelete(expectedUrl).reply(202, {});
});
afterEach(() => {
wrapper.destroy();
+ window.gon = originalGon;
+ mock.restore();
});
describe('when the dependency proxy is not available', () => {
@@ -95,6 +130,12 @@ describe('DependencyProxyApp', () => {
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', () => {
@@ -165,6 +206,7 @@ describe('DependencyProxyApp', () => {
}),
);
createComponent();
+
return waitForPromises();
});
@@ -214,6 +256,34 @@ describe('DependencyProxyApp', () => {
fullPath: provideDefaults.groupPath,
});
});
+
+ it('shows the clear cache dropdown list', () => {
+ expect(findClearCacheDropdownList().exists()).toBe(true);
+
+ const clearCacheDropdownItem = findClearCacheDropdownList().findComponent(
+ GlDropdownItem,
+ );
+
+ expect(clearCacheDropdownItem.text()).toBe('Clear cache');
+ });
+
+ it('shows the clear cache confirmation modal', () => {
+ const modal = findClearCacheModal();
+
+ expect(modal.find('.modal-title').text()).toContain('Clear 2 images from cache?');
+ expect(modal.props('actionPrimary').text).toBe('Clear cache');
+ });
+
+ it('submits the clear cache request', async () => {
+ findClearCacheModal().vm.$emit('primary', { preventDefault: jest.fn() });
+
+ await waitForPromises();
+
+ expect(findClearCacheAlert().exists()).toBe(true);
+ expect(findClearCacheAlert().text()).toBe(
+ 'All items in the cache are scheduled for removal.',
+ );
+ });
});
});
});
diff --git a/spec/frontend/packages_and_registries/harbor_registry/components/list/harbor_list_header_spec.js b/spec/frontend/packages_and_registries/harbor_registry/components/list/harbor_list_header_spec.js
new file mode 100644
index 00000000000..636f3eeb04a
--- /dev/null
+++ b/spec/frontend/packages_and_registries/harbor_registry/components/list/harbor_list_header_spec.js
@@ -0,0 +1,88 @@
+import { shallowMount } from '@vue/test-utils';
+import { GlSprintf } from '@gitlab/ui';
+import { nextTick } from 'vue';
+import TitleArea from '~/vue_shared/components/registry/title_area.vue';
+import HarborListHeader from '~/packages_and_registries/harbor_registry/components/list/harbor_list_header.vue';
+import MetadataItem from '~/vue_shared/components/registry/metadata_item.vue';
+import {
+ HARBOR_REGISTRY_TITLE,
+ LIST_INTRO_TEXT,
+} from '~/packages_and_registries/harbor_registry/constants/index';
+
+describe('harbor_list_header', () => {
+ let wrapper;
+
+ const findTitleArea = () => wrapper.find(TitleArea);
+ const findCommandsSlot = () => wrapper.find('[data-testid="commands-slot"]');
+ const findImagesMetaDataItem = () => wrapper.find(MetadataItem);
+
+ const mountComponent = async (propsData, slots) => {
+ wrapper = shallowMount(HarborListHeader, {
+ stubs: {
+ GlSprintf,
+ TitleArea,
+ },
+ propsData,
+ slots,
+ });
+ await nextTick();
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('header', () => {
+ it('has a title', () => {
+ mountComponent({ metadataLoading: true });
+
+ expect(findTitleArea().props()).toMatchObject({
+ title: HARBOR_REGISTRY_TITLE,
+ metadataLoading: true,
+ });
+ });
+
+ it('has a commands slot', () => {
+ mountComponent(null, { commands: '<div data-testid="commands-slot">baz</div>' });
+
+ expect(findCommandsSlot().text()).toBe('baz');
+ });
+
+ describe('sub header parts', () => {
+ describe('images count', () => {
+ it('exists', async () => {
+ await mountComponent({ imagesCount: 1 });
+
+ expect(findImagesMetaDataItem().exists()).toBe(true);
+ });
+
+ it('when there is one image', async () => {
+ await mountComponent({ imagesCount: 1 });
+
+ expect(findImagesMetaDataItem().props()).toMatchObject({
+ text: '1 Image repository',
+ icon: 'container-image',
+ });
+ });
+
+ it('when there is more than one image', async () => {
+ await mountComponent({ imagesCount: 3 });
+
+ expect(findImagesMetaDataItem().props('text')).toBe('3 Image repositories');
+ });
+ });
+ });
+ });
+
+ describe('info messages', () => {
+ describe('default message', () => {
+ it('is correctly bound to title_area props', () => {
+ mountComponent({ helpPagePath: 'foo' });
+
+ expect(findTitleArea().props('infoMessages')).toEqual([
+ { text: LIST_INTRO_TEXT, link: 'foo' },
+ ]);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/packages_and_registries/harbor_registry/components/list/harbor_list_row_spec.js b/spec/frontend/packages_and_registries/harbor_registry/components/list/harbor_list_row_spec.js
new file mode 100644
index 00000000000..8560c4f78f7
--- /dev/null
+++ b/spec/frontend/packages_and_registries/harbor_registry/components/list/harbor_list_row_spec.js
@@ -0,0 +1,99 @@
+import { shallowMount, RouterLinkStub as RouterLink } from '@vue/test-utils';
+import { GlIcon, GlSprintf, GlSkeletonLoader } from '@gitlab/ui';
+
+import HarborListRow from '~/packages_and_registries/harbor_registry/components/list/harbor_list_row.vue';
+import ListItem from '~/vue_shared/components/registry/list_item.vue';
+import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
+import { harborListResponse } from '../../mock_data';
+
+describe('Harbor List Row', () => {
+ let wrapper;
+ const [item] = harborListResponse.repositories;
+
+ const findDetailsLink = () => wrapper.find(RouterLink);
+ const findClipboardButton = () => wrapper.findComponent(ClipboardButton);
+ const findTagsCount = () => wrapper.find('[data-testid="tags-count"]');
+ const findSkeletonLoader = () => wrapper.findComponent(GlSkeletonLoader);
+
+ const mountComponent = (props) => {
+ wrapper = shallowMount(HarborListRow, {
+ stubs: {
+ RouterLink,
+ GlSprintf,
+ ListItem,
+ },
+ propsData: {
+ item,
+ ...props,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('image title and path', () => {
+ it('contains a link to the details page', () => {
+ mountComponent();
+
+ const link = findDetailsLink();
+ expect(link.text()).toBe(item.name);
+ expect(findDetailsLink().props('to')).toMatchObject({
+ name: 'details',
+ params: {
+ id: item.id,
+ },
+ });
+ });
+
+ it('contains a clipboard button', () => {
+ mountComponent();
+ const button = findClipboardButton();
+ expect(button.exists()).toBe(true);
+ expect(button.props('text')).toBe(item.location);
+ expect(button.props('title')).toBe(item.location);
+ });
+ });
+
+ describe('tags count', () => {
+ it('exists', () => {
+ mountComponent();
+ expect(findTagsCount().exists()).toBe(true);
+ });
+
+ it('contains a tag icon', () => {
+ mountComponent();
+ const icon = findTagsCount().find(GlIcon);
+ expect(icon.exists()).toBe(true);
+ expect(icon.props('name')).toBe('tag');
+ });
+
+ describe('loading state', () => {
+ it('shows a loader when metadataLoading is true', () => {
+ mountComponent({ metadataLoading: true });
+
+ expect(findSkeletonLoader().exists()).toBe(true);
+ });
+
+ it('hides the tags count while loading', () => {
+ mountComponent({ metadataLoading: true });
+
+ expect(findTagsCount().exists()).toBe(false);
+ });
+ });
+
+ describe('tags count text', () => {
+ it('with one tag in the image', () => {
+ mountComponent({ item: { ...item, artifactCount: 1 } });
+
+ expect(findTagsCount().text()).toMatchInterpolatedText('1 Tag');
+ });
+ it('with more than one tag in the image', () => {
+ mountComponent({ item: { ...item, artifactCount: 3 } });
+
+ expect(findTagsCount().text()).toMatchInterpolatedText('3 Tags');
+ });
+ });
+ });
+});
diff --git a/spec/frontend/packages_and_registries/harbor_registry/components/list/harbor_list_spec.js b/spec/frontend/packages_and_registries/harbor_registry/components/list/harbor_list_spec.js
new file mode 100644
index 00000000000..f018eff58c9
--- /dev/null
+++ b/spec/frontend/packages_and_registries/harbor_registry/components/list/harbor_list_spec.js
@@ -0,0 +1,39 @@
+import { shallowMount } from '@vue/test-utils';
+import HarborList from '~/packages_and_registries/harbor_registry/components/list/harbor_list.vue';
+import HarborListRow from '~/packages_and_registries/harbor_registry/components/list/harbor_list_row.vue';
+import RegistryList from '~/packages_and_registries/shared/components/registry_list.vue';
+import { harborListResponse } from '../../mock_data';
+
+describe('Harbor List', () => {
+ let wrapper;
+
+ const findHarborListRow = () => wrapper.findAll(HarborListRow);
+
+ const mountComponent = (props) => {
+ wrapper = shallowMount(HarborList, {
+ stubs: { RegistryList },
+ propsData: {
+ images: harborListResponse.repositories,
+ pageInfo: harborListResponse.pageInfo,
+ ...props,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('list', () => {
+ it('contains one list element for each image', () => {
+ mountComponent();
+
+ expect(findHarborListRow().length).toBe(harborListResponse.repositories.length);
+ });
+
+ it('passes down the metadataLoading prop', () => {
+ mountComponent({ metadataLoading: true });
+ expect(findHarborListRow().at(0).props('metadataLoading')).toBe(true);
+ });
+ });
+});
diff --git a/spec/frontend/packages_and_registries/harbor_registry/mock_data.js b/spec/frontend/packages_and_registries/harbor_registry/mock_data.js
new file mode 100644
index 00000000000..85399c22e79
--- /dev/null
+++ b/spec/frontend/packages_and_registries/harbor_registry/mock_data.js
@@ -0,0 +1,175 @@
+export const harborListResponse = {
+ repositories: [
+ {
+ artifactCount: 1,
+ creationTime: '2022-03-02T06:35:53.205Z',
+ id: 25,
+ name: 'shao/flinkx',
+ projectId: 21,
+ pullCount: 0,
+ updateTime: '2022-03-02T06:35:53.205Z',
+ location: 'demo.harbor.com/gitlab-cn/build/cng-images/gitlab-kas',
+ },
+ {
+ artifactCount: 1,
+ creationTime: '2022-03-02T06:35:53.205Z',
+ id: 26,
+ name: 'shao/flinkx1',
+ projectId: 21,
+ pullCount: 0,
+ updateTime: '2022-03-02T06:35:53.205Z',
+ location: 'demo.harbor.com/gitlab-cn/build/cng-images/gitlab-kas',
+ },
+ {
+ artifactCount: 1,
+ creationTime: '2022-03-02T06:35:53.205Z',
+ id: 27,
+ name: 'shao/flinkx2',
+ projectId: 21,
+ pullCount: 0,
+ updateTime: '2022-03-02T06:35:53.205Z',
+ location: 'demo.harbor.com/gitlab-cn/build/cng-images/gitlab-kas',
+ },
+ ],
+ totalCount: 3,
+ pageInfo: {
+ hasNextPage: false,
+ hasPreviousPage: false,
+ },
+};
+
+export const harborTagsResponse = {
+ tags: [
+ {
+ digest: 'sha256:7f386a1844faf341353e1c20f2f39f11f397604fedc475435d13f756eeb235d1',
+ location:
+ 'registry.gitlab.com/gitlab-org/gitlab/gitlab-ee-qa/cache:02310e655103823920157bc4410ea361dc638bc2cda59667d2cb1f2a988e264c',
+ path:
+ 'gitlab-org/gitlab/gitlab-ee-qa/cache:02310e655103823920157bc4410ea361dc638bc2cda59667d2cb1f2a988e264c',
+ name: '02310e655103823920157bc4410ea361dc638bc2cda59667d2cb1f2a988e264c',
+ revision: 'f53bde3d44699e04e11cf15fb415046a0913e2623d878d89bc21adb2cbda5255',
+ shortRevision: 'f53bde3d4',
+ createdAt: '2022-03-02T23:59:05+00:00',
+ totalSize: '6623124',
+ },
+ {
+ digest: 'sha256:4554416b84c4568fe93086620b637064ed029737aabe7308b96d50e3d9d92ed7',
+ location:
+ 'registry.gitlab.com/gitlab-org/gitlab/gitlab-ee-qa/cache:02deb4dddf177212b50e883d5e4f6c03731fad1a18cd27261736cd9dbba79160',
+ path:
+ 'gitlab-org/gitlab/gitlab-ee-qa/cache:02deb4dddf177212b50e883d5e4f6c03731fad1a18cd27261736cd9dbba79160',
+ name: '02deb4dddf177212b50e883d5e4f6c03731fad1a18cd27261736cd9dbba79160',
+ revision: 'e1fe52d8bab66d71bd54a6b8784d3b9edbc68adbd6ea87f5fa44d9974144ef9e',
+ shortRevision: 'e1fe52d8b',
+ createdAt: '2022-02-10T01:09:56+00:00',
+ totalSize: '920760',
+ },
+ {
+ digest: 'sha256:14f37b60e52b9ce0e9f8f7094b311d265384798592f783487c30aaa3d58e6345',
+ location:
+ 'registry.gitlab.com/gitlab-org/gitlab/gitlab-ee-qa/cache:03bc5971bab1e849ba52a20a31e7273053f22b2ddb1d04bd6b77d53a2635727a',
+ path:
+ 'gitlab-org/gitlab/gitlab-ee-qa/cache:03bc5971bab1e849ba52a20a31e7273053f22b2ddb1d04bd6b77d53a2635727a',
+ name: '03bc5971bab1e849ba52a20a31e7273053f22b2ddb1d04bd6b77d53a2635727a',
+ revision: 'c72770c6eb93c421bc496964b4bffc742b1ec2e642cdab876be7afda1856029f',
+ shortRevision: 'c72770c6e',
+ createdAt: '2021-12-22T04:48:48+00:00',
+ totalSize: '48609053',
+ },
+ {
+ digest: 'sha256:e925e3b8277ea23f387ed5fba5e78280cfac7cfb261a78cf046becf7b6a3faae',
+ location:
+ 'registry.gitlab.com/gitlab-org/gitlab/gitlab-ee-qa/cache:03f495bc5714bff78bb14293320d336afdf47fd47ddff0c3c5f09f8da86d5d19',
+ path:
+ 'gitlab-org/gitlab/gitlab-ee-qa/cache:03f495bc5714bff78bb14293320d336afdf47fd47ddff0c3c5f09f8da86d5d19',
+ name: '03f495bc5714bff78bb14293320d336afdf47fd47ddff0c3c5f09f8da86d5d19',
+ revision: '1ac2a43194f4e15166abdf3f26e6ec92215240490b9cac834d63de1a3d87494a',
+ shortRevision: '1ac2a4319',
+ createdAt: '2022-03-09T11:02:27+00:00',
+ totalSize: '35141894',
+ },
+ {
+ digest: 'sha256:7d8303fd5c077787a8c879f8f66b69e2b5605f48ccd3f286e236fb0749fcc1ca',
+ location:
+ 'registry.gitlab.com/gitlab-org/gitlab/gitlab-ee-qa/cache:05a4e58231e54b70aab2d6f22ba4fbe10e48aa4ddcbfef11c5662241c2ae4fda',
+ path:
+ 'gitlab-org/gitlab/gitlab-ee-qa/cache:05a4e58231e54b70aab2d6f22ba4fbe10e48aa4ddcbfef11c5662241c2ae4fda',
+ name: '05a4e58231e54b70aab2d6f22ba4fbe10e48aa4ddcbfef11c5662241c2ae4fda',
+ revision: 'cf8fee086701016e1a84e6824f0c896951fef4cce9d4745459558b87eec3232c',
+ shortRevision: 'cf8fee086',
+ createdAt: '2022-01-21T11:31:43+00:00',
+ totalSize: '48716070',
+ },
+ {
+ digest: 'sha256:b33611cefe20e4a41a6e0dce356a5d7ef3c177ea7536a58652f5b3a4f2f83549',
+ location:
+ 'registry.gitlab.com/gitlab-org/gitlab/gitlab-ee-qa/cache:093d2746876997723541aec8b88687a4cdb3b5bbb0279c5089b7891317741a9a',
+ path:
+ 'gitlab-org/gitlab/gitlab-ee-qa/cache:093d2746876997723541aec8b88687a4cdb3b5bbb0279c5089b7891317741a9a',
+ name: '093d2746876997723541aec8b88687a4cdb3b5bbb0279c5089b7891317741a9a',
+ revision: '1a4b48198b13d55242c5164e64d41c4e9f75b5d9506bc6e0efc1534dd0dd1f15',
+ shortRevision: '1a4b48198',
+ createdAt: '2022-01-21T11:31:51+00:00',
+ totalSize: '6623127',
+ },
+ {
+ digest: 'sha256:d25c3c020e2dbd4711a67b9fe308f4cbb7b0bb21815e722a02f91c570dc5d519',
+ location:
+ 'registry.gitlab.com/gitlab-org/gitlab/gitlab-ee-qa/cache:09698b3fae81dfd6e02554dbc82930f304a6356c8f541c80e8598a42aed985f7',
+ path:
+ 'gitlab-org/gitlab/gitlab-ee-qa/cache:09698b3fae81dfd6e02554dbc82930f304a6356c8f541c80e8598a42aed985f7',
+ name: '09698b3fae81dfd6e02554dbc82930f304a6356c8f541c80e8598a42aed985f7',
+ revision: '03e2e2777dde01c30469ee8c710973dd08a7a4f70494d7dc1583c24b525d7f61',
+ shortRevision: '03e2e2777',
+ createdAt: '2022-03-02T23:58:20+00:00',
+ totalSize: '911377',
+ },
+ {
+ digest: 'sha256:fb760e4d2184e9e8e39d6917534d4610fe01009734698a5653b2de1391ba28f4',
+ location:
+ 'registry.gitlab.com/gitlab-org/gitlab/gitlab-ee-qa/cache:09b830c3eaf80d547f3b523d8e242a2c411085c349dab86c520f36c7b7644f95',
+ path:
+ 'gitlab-org/gitlab/gitlab-ee-qa/cache:09b830c3eaf80d547f3b523d8e242a2c411085c349dab86c520f36c7b7644f95',
+ name: '09b830c3eaf80d547f3b523d8e242a2c411085c349dab86c520f36c7b7644f95',
+ revision: '350e78d60646bf6967244448c6aaa14d21ecb9a0c6cf87e9ff0361cbe59b9012',
+ shortRevision: '350e78d60',
+ createdAt: '2022-01-19T13:49:14+00:00',
+ totalSize: '48710241',
+ },
+ {
+ digest: 'sha256:407250f380cea92729cbc038c420e74900f53b852e11edc6404fe75a0fd2c402',
+ location:
+ 'registry.gitlab.com/gitlab-org/gitlab/gitlab-ee-qa/cache:0d03504a17b467eafc8c96bde70af26c74bd459a32b7eb2dd189dd6b3c121557',
+ path:
+ 'gitlab-org/gitlab/gitlab-ee-qa/cache:0d03504a17b467eafc8c96bde70af26c74bd459a32b7eb2dd189dd6b3c121557',
+ name: '0d03504a17b467eafc8c96bde70af26c74bd459a32b7eb2dd189dd6b3c121557',
+ revision: '76038370b7f3904364891457c4a6a234897255e6b9f45d0a852bf3a7e5257e18',
+ shortRevision: '76038370b',
+ createdAt: '2022-01-24T12:56:22+00:00',
+ totalSize: '280065',
+ },
+ {
+ digest: 'sha256:ada87f25218542951ce6720c27f3d0758e90c2540bd129f5cfb9e15b31e07b07',
+ location:
+ 'registry.gitlab.com/gitlab-org/gitlab/gitlab-ee-qa/cache:0eb20a4a7cac2ebea821d420b3279654fe550fd8502f1785c1927aa84e5949eb',
+ path:
+ 'gitlab-org/gitlab/gitlab-ee-qa/cache:0eb20a4a7cac2ebea821d420b3279654fe550fd8502f1785c1927aa84e5949eb',
+ name: '0eb20a4a7cac2ebea821d420b3279654fe550fd8502f1785c1927aa84e5949eb',
+ revision: '3d4b49a7bbb36c48bb721f4d0e76e7950bec3878ee29cdfdd6da39f575d6d37f',
+ shortRevision: '3d4b49a7b',
+ createdAt: '2022-02-17T17:37:52+00:00',
+ totalSize: '48655767',
+ },
+ ],
+ totalCount: 100,
+ pageInfo: {
+ hasNextPage: false,
+ hasPreviousPage: false,
+ },
+};
+
+export const dockerCommands = {
+ dockerBuildCommand: 'foofoo',
+ dockerPushCommand: 'barbar',
+ dockerLoginCommand: 'bazbaz',
+};
diff --git a/spec/frontend/packages_and_registries/harbor_registry/pages/index_spec.js b/spec/frontend/packages_and_registries/harbor_registry/pages/index_spec.js
new file mode 100644
index 00000000000..55fc8066f65
--- /dev/null
+++ b/spec/frontend/packages_and_registries/harbor_registry/pages/index_spec.js
@@ -0,0 +1,24 @@
+import { shallowMount } from '@vue/test-utils';
+import component from '~/packages_and_registries/harbor_registry/pages/index.vue';
+
+describe('List Page', () => {
+ let wrapper;
+
+ const findRouterView = () => wrapper.find({ ref: 'router-view' });
+
+ const mountComponent = () => {
+ wrapper = shallowMount(component, {
+ stubs: {
+ RouterView: true,
+ },
+ });
+ };
+
+ beforeEach(() => {
+ mountComponent();
+ });
+
+ it('has a router view', () => {
+ expect(findRouterView().exists()).toBe(true);
+ });
+});
diff --git a/spec/frontend/packages_and_registries/harbor_registry/pages/list_spec.js b/spec/frontend/packages_and_registries/harbor_registry/pages/list_spec.js
new file mode 100644
index 00000000000..61ee36a2794
--- /dev/null
+++ b/spec/frontend/packages_and_registries/harbor_registry/pages/list_spec.js
@@ -0,0 +1,140 @@
+import { shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
+import { GlSkeletonLoader } from '@gitlab/ui';
+import HarborListHeader from '~/packages_and_registries/harbor_registry/components/list/harbor_list_header.vue';
+import HarborRegistryList from '~/packages_and_registries/harbor_registry/pages/list.vue';
+import PersistedSearch from '~/packages_and_registries/shared/components/persisted_search.vue';
+import waitForPromises from 'helpers/wait_for_promises';
+// import { harborListResponse } from '~/packages_and_registries/harbor_registry/mock_api.js';
+import HarborList from '~/packages_and_registries/harbor_registry/components/list/harbor_list.vue';
+import CliCommands from '~/packages_and_registries/shared/components/cli_commands.vue';
+import { SORT_FIELDS } from '~/packages_and_registries/harbor_registry/constants/index';
+import { harborListResponse, dockerCommands } from '../mock_data';
+
+let mockHarborListResponse;
+jest.mock('~/packages_and_registries/harbor_registry/mock_api.js', () => ({
+ harborListResponse: () => mockHarborListResponse,
+}));
+
+describe('Harbor List Page', () => {
+ let wrapper;
+
+ const waitForHarborPageRequest = async () => {
+ await waitForPromises();
+ await nextTick();
+ };
+
+ beforeEach(() => {
+ mockHarborListResponse = Promise.resolve(harborListResponse);
+ });
+
+ const findHarborListHeader = () => wrapper.findComponent(HarborListHeader);
+ const findPersistedSearch = () => wrapper.findComponent(PersistedSearch);
+ const findSkeletonLoader = () => wrapper.findComponent(GlSkeletonLoader);
+ const findHarborList = () => wrapper.findComponent(HarborList);
+ const findCliCommands = () => wrapper.findComponent(CliCommands);
+
+ const fireFirstSortUpdate = () => {
+ findPersistedSearch().vm.$emit('update', { sort: 'UPDATED_DESC', filters: [] });
+ };
+
+ const mountComponent = ({ config = { isGroupPage: false } } = {}) => {
+ wrapper = shallowMount(HarborRegistryList, {
+ stubs: {
+ HarborListHeader,
+ },
+ provide() {
+ return {
+ config,
+ ...dockerCommands,
+ };
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('contains harbor registry header', async () => {
+ mountComponent();
+ fireFirstSortUpdate();
+ await waitForHarborPageRequest();
+ await nextTick();
+
+ expect(findHarborListHeader().exists()).toBe(true);
+ expect(findHarborListHeader().props()).toMatchObject({
+ imagesCount: 3,
+ metadataLoading: false,
+ });
+ });
+
+ describe('isLoading is true', () => {
+ it('shows the skeleton loader', async () => {
+ mountComponent();
+ fireFirstSortUpdate();
+
+ expect(findSkeletonLoader().exists()).toBe(true);
+ });
+
+ it('harborList is not visible', () => {
+ mountComponent();
+
+ expect(findHarborList().exists()).toBe(false);
+ });
+
+ it('cli commands is not visible', () => {
+ mountComponent();
+
+ expect(findCliCommands().exists()).toBe(false);
+ });
+
+ it('title has the metadataLoading props set to true', async () => {
+ mountComponent();
+ fireFirstSortUpdate();
+
+ expect(findHarborListHeader().props('metadataLoading')).toBe(true);
+ });
+ });
+
+ describe('list is not empty', () => {
+ describe('unfiltered state', () => {
+ it('quick start is visible', async () => {
+ mountComponent();
+ fireFirstSortUpdate();
+
+ await waitForHarborPageRequest();
+ await nextTick();
+
+ expect(findCliCommands().exists()).toBe(true);
+ });
+
+ it('list component is visible', async () => {
+ mountComponent();
+ fireFirstSortUpdate();
+
+ await waitForHarborPageRequest();
+ await nextTick();
+
+ expect(findHarborList().exists()).toBe(true);
+ });
+ });
+
+ describe('search and sorting', () => {
+ it('has a persisted search box element', async () => {
+ mountComponent();
+ fireFirstSortUpdate();
+ await waitForHarborPageRequest();
+ await nextTick();
+
+ const harborRegistrySearch = findPersistedSearch();
+ expect(harborRegistrySearch.exists()).toBe(true);
+ expect(harborRegistrySearch.props()).toMatchObject({
+ defaultOrder: 'UPDATED',
+ defaultSort: 'desc',
+ sortableFields: SORT_FIELDS,
+ });
+ });
+ });
+ });
+});
diff --git a/spec/frontend/packages_and_registries/infrastructure_registry/components/details/store/actions_spec.js b/spec/frontend/packages_and_registries/infrastructure_registry/components/details/store/actions_spec.js
index b9383d6c38c..31ab108558c 100644
--- a/spec/frontend/packages_and_registries/infrastructure_registry/components/details/store/actions_spec.js
+++ b/spec/frontend/packages_and_registries/infrastructure_registry/components/details/store/actions_spec.js
@@ -20,10 +20,10 @@ jest.mock('~/api.js');
describe('Actions Package details store', () => {
describe('fetchPackageVersions', () => {
- it('should fetch the package versions', (done) => {
+ it('should fetch the package versions', async () => {
Api.projectPackage = jest.fn().mockResolvedValue({ data: packageEntity });
- testAction(
+ await testAction(
fetchPackageVersions,
undefined,
{ packageEntity },
@@ -33,20 +33,14 @@ describe('Actions Package details store', () => {
{ type: types.SET_LOADING, payload: false },
],
[],
- () => {
- expect(Api.projectPackage).toHaveBeenCalledWith(
- packageEntity.project_id,
- packageEntity.id,
- );
- done();
- },
);
+ expect(Api.projectPackage).toHaveBeenCalledWith(packageEntity.project_id, packageEntity.id);
});
- it("does not set the versions if they don't exist", (done) => {
+ it("does not set the versions if they don't exist", async () => {
Api.projectPackage = jest.fn().mockResolvedValue({ data: { packageEntity, versions: null } });
- testAction(
+ await testAction(
fetchPackageVersions,
undefined,
{ packageEntity },
@@ -55,20 +49,14 @@ describe('Actions Package details store', () => {
{ type: types.SET_LOADING, payload: false },
],
[],
- () => {
- expect(Api.projectPackage).toHaveBeenCalledWith(
- packageEntity.project_id,
- packageEntity.id,
- );
- done();
- },
);
+ expect(Api.projectPackage).toHaveBeenCalledWith(packageEntity.project_id, packageEntity.id);
});
- it('should create flash on API error', (done) => {
+ it('should create flash on API error', async () => {
Api.projectPackage = jest.fn().mockRejectedValue();
- testAction(
+ await testAction(
fetchPackageVersions,
undefined,
{ packageEntity },
@@ -77,41 +65,31 @@ describe('Actions Package details store', () => {
{ type: types.SET_LOADING, payload: false },
],
[],
- () => {
- expect(Api.projectPackage).toHaveBeenCalledWith(
- packageEntity.project_id,
- packageEntity.id,
- );
- expect(createFlash).toHaveBeenCalledWith({
- message: FETCH_PACKAGE_VERSIONS_ERROR,
- type: 'warning',
- });
- done();
- },
);
+ expect(Api.projectPackage).toHaveBeenCalledWith(packageEntity.project_id, packageEntity.id);
+ expect(createFlash).toHaveBeenCalledWith({
+ message: FETCH_PACKAGE_VERSIONS_ERROR,
+ type: 'warning',
+ });
});
});
describe('deletePackage', () => {
- it('should call Api.deleteProjectPackage', (done) => {
+ it('should call Api.deleteProjectPackage', async () => {
Api.deleteProjectPackage = jest.fn().mockResolvedValue();
- testAction(deletePackage, undefined, { packageEntity }, [], [], () => {
- expect(Api.deleteProjectPackage).toHaveBeenCalledWith(
- packageEntity.project_id,
- packageEntity.id,
- );
- done();
- });
+ await testAction(deletePackage, undefined, { packageEntity }, [], []);
+ expect(Api.deleteProjectPackage).toHaveBeenCalledWith(
+ packageEntity.project_id,
+ packageEntity.id,
+ );
});
- it('should create flash on API error', (done) => {
+ it('should create flash on API error', async () => {
Api.deleteProjectPackage = jest.fn().mockRejectedValue();
- testAction(deletePackage, undefined, { packageEntity }, [], [], () => {
- expect(createFlash).toHaveBeenCalledWith({
- message: DELETE_PACKAGE_ERROR_MESSAGE,
- type: 'warning',
- });
- done();
+ await testAction(deletePackage, undefined, { packageEntity }, [], []);
+ expect(createFlash).toHaveBeenCalledWith({
+ message: DELETE_PACKAGE_ERROR_MESSAGE,
+ type: 'warning',
});
});
});
@@ -119,37 +97,33 @@ describe('Actions Package details store', () => {
describe('deletePackageFile', () => {
const fileId = 'a_file_id';
- it('should call Api.deleteProjectPackageFile and commit the right data', (done) => {
+ it('should call Api.deleteProjectPackageFile and commit the right data', async () => {
const packageFiles = [{ id: 'foo' }, { id: fileId }];
Api.deleteProjectPackageFile = jest.fn().mockResolvedValue();
- testAction(
+ await testAction(
deletePackageFile,
fileId,
{ packageEntity, packageFiles },
[{ type: types.UPDATE_PACKAGE_FILES, payload: [{ id: 'foo' }] }],
[],
- () => {
- expect(Api.deleteProjectPackageFile).toHaveBeenCalledWith(
- packageEntity.project_id,
- packageEntity.id,
- fileId,
- );
- expect(createFlash).toHaveBeenCalledWith({
- message: DELETE_PACKAGE_FILE_SUCCESS_MESSAGE,
- type: 'success',
- });
- done();
- },
);
+ expect(Api.deleteProjectPackageFile).toHaveBeenCalledWith(
+ packageEntity.project_id,
+ packageEntity.id,
+ fileId,
+ );
+ expect(createFlash).toHaveBeenCalledWith({
+ message: DELETE_PACKAGE_FILE_SUCCESS_MESSAGE,
+ type: 'success',
+ });
});
- it('should create flash on API error', (done) => {
+
+ it('should create flash on API error', async () => {
Api.deleteProjectPackageFile = jest.fn().mockRejectedValue();
- testAction(deletePackageFile, fileId, { packageEntity }, [], [], () => {
- expect(createFlash).toHaveBeenCalledWith({
- message: DELETE_PACKAGE_FILE_ERROR_MESSAGE,
- type: 'warning',
- });
- done();
+ await testAction(deletePackageFile, fileId, { packageEntity }, [], []);
+ expect(createFlash).toHaveBeenCalledWith({
+ message: DELETE_PACKAGE_FILE_ERROR_MESSAGE,
+ type: 'warning',
});
});
});
diff --git a/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/__snapshots__/packages_list_app_spec.js.snap b/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/__snapshots__/packages_list_app_spec.js.snap
index d82af8f9e63..a33528d2d91 100644
--- a/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/__snapshots__/packages_list_app_spec.js.snap
+++ b/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/__snapshots__/packages_list_app_spec.js.snap
@@ -21,7 +21,7 @@ exports[`packages_list_app renders 1`] = `
>
<img
alt=""
- class="gl-max-w-full"
+ class="gl-max-w-full gl-dark-invert-keep-hue"
role="img"
src="helpSvg"
/>
diff --git a/spec/frontend/packages_and_registries/infrastructure_registry/components/list/stores/actions_spec.js b/spec/frontend/packages_and_registries/infrastructure_registry/components/list/stores/actions_spec.js
index 3fbfe1060dc..d596f2dae33 100644
--- a/spec/frontend/packages_and_registries/infrastructure_registry/components/list/stores/actions_spec.js
+++ b/spec/frontend/packages_and_registries/infrastructure_registry/components/list/stores/actions_spec.js
@@ -32,8 +32,8 @@ describe('Actions Package list store', () => {
};
const filter = [];
- it('should fetch the project packages list when isGroupPage is false', (done) => {
- testAction(
+ it('should fetch the project packages list when isGroupPage is false', async () => {
+ await testAction(
actions.requestPackagesList,
undefined,
{ config: { isGroupPage: false, resourceId: 1 }, sorting, filter },
@@ -43,17 +43,14 @@ describe('Actions Package list store', () => {
{ type: 'receivePackagesListSuccess', payload: { data: 'foo', headers } },
{ type: 'setLoading', payload: false },
],
- () => {
- expect(Api.projectPackages).toHaveBeenCalledWith(1, {
- params: { page: 1, per_page: 20, sort: sorting.sort, order_by: sorting.orderBy },
- });
- done();
- },
);
+ expect(Api.projectPackages).toHaveBeenCalledWith(1, {
+ params: { page: 1, per_page: 20, sort: sorting.sort, order_by: sorting.orderBy },
+ });
});
- it('should fetch the group packages list when isGroupPage is true', (done) => {
- testAction(
+ it('should fetch the group packages list when isGroupPage is true', async () => {
+ await testAction(
actions.requestPackagesList,
undefined,
{ config: { isGroupPage: true, resourceId: 2 }, sorting, filter },
@@ -63,19 +60,16 @@ describe('Actions Package list store', () => {
{ type: 'receivePackagesListSuccess', payload: { data: 'baz', headers } },
{ type: 'setLoading', payload: false },
],
- () => {
- expect(Api.groupPackages).toHaveBeenCalledWith(2, {
- params: { page: 1, per_page: 20, sort: sorting.sort, order_by: sorting.orderBy },
- });
- done();
- },
);
+ expect(Api.groupPackages).toHaveBeenCalledWith(2, {
+ params: { page: 1, per_page: 20, sort: sorting.sort, order_by: sorting.orderBy },
+ });
});
- it('should fetch packages of a certain type when a filter with a type is present', (done) => {
+ it('should fetch packages of a certain type when a filter with a type is present', async () => {
const packageType = 'maven';
- testAction(
+ await testAction(
actions.requestPackagesList,
undefined,
{
@@ -89,24 +83,21 @@ describe('Actions Package list store', () => {
{ type: 'receivePackagesListSuccess', payload: { data: 'foo', headers } },
{ type: 'setLoading', payload: false },
],
- () => {
- expect(Api.projectPackages).toHaveBeenCalledWith(1, {
- params: {
- page: 1,
- per_page: 20,
- sort: sorting.sort,
- order_by: sorting.orderBy,
- package_type: packageType,
- },
- });
- done();
- },
);
+ expect(Api.projectPackages).toHaveBeenCalledWith(1, {
+ params: {
+ page: 1,
+ per_page: 20,
+ sort: sorting.sort,
+ order_by: sorting.orderBy,
+ package_type: packageType,
+ },
+ });
});
- it('should create flash on API error', (done) => {
+ it('should create flash on API error', async () => {
Api.projectPackages = jest.fn().mockRejectedValue();
- testAction(
+ await testAction(
actions.requestPackagesList,
undefined,
{ config: { isGroupPage: false, resourceId: 2 }, sorting, filter },
@@ -115,15 +106,12 @@ describe('Actions Package list store', () => {
{ type: 'setLoading', payload: true },
{ type: 'setLoading', payload: false },
],
- () => {
- expect(createFlash).toHaveBeenCalled();
- done();
- },
);
+ expect(createFlash).toHaveBeenCalled();
});
- it('should force the terraform_module type when forceTerraform is true', (done) => {
- testAction(
+ it('should force the terraform_module type when forceTerraform is true', async () => {
+ await testAction(
actions.requestPackagesList,
undefined,
{ config: { isGroupPage: false, resourceId: 1, forceTerraform: true }, sorting, filter },
@@ -133,27 +121,24 @@ describe('Actions Package list store', () => {
{ type: 'receivePackagesListSuccess', payload: { data: 'foo', headers } },
{ type: 'setLoading', payload: false },
],
- () => {
- expect(Api.projectPackages).toHaveBeenCalledWith(1, {
- params: {
- page: 1,
- per_page: 20,
- sort: sorting.sort,
- order_by: sorting.orderBy,
- package_type: 'terraform_module',
- },
- });
- done();
- },
);
+ expect(Api.projectPackages).toHaveBeenCalledWith(1, {
+ params: {
+ page: 1,
+ per_page: 20,
+ sort: sorting.sort,
+ order_by: sorting.orderBy,
+ package_type: 'terraform_module',
+ },
+ });
});
});
describe('receivePackagesListSuccess', () => {
- it('should set received packages', (done) => {
+ it('should set received packages', () => {
const data = 'foo';
- testAction(
+ return testAction(
actions.receivePackagesListSuccess,
{ data, headers },
null,
@@ -162,33 +147,30 @@ describe('Actions Package list store', () => {
{ type: types.SET_PAGINATION, payload: headers },
],
[],
- done,
);
});
});
describe('setInitialState', () => {
- it('should commit setInitialState', (done) => {
- testAction(
+ it('should commit setInitialState', () => {
+ return testAction(
actions.setInitialState,
'1',
null,
[{ type: types.SET_INITIAL_STATE, payload: '1' }],
[],
- done,
);
});
});
describe('setLoading', () => {
- it('should commit set main loading', (done) => {
- testAction(
+ it('should commit set main loading', () => {
+ return testAction(
actions.setLoading,
true,
null,
[{ type: types.SET_MAIN_LOADING, payload: true }],
[],
- done,
);
});
});
@@ -199,11 +181,11 @@ describe('Actions Package list store', () => {
delete_api_path: 'foo',
},
};
- it('should perform a delete operation on _links.delete_api_path', (done) => {
+ it('should perform a delete operation on _links.delete_api_path', () => {
mock.onDelete(payload._links.delete_api_path).replyOnce(200);
Api.projectPackages = jest.fn().mockResolvedValue({ data: 'foo' });
- testAction(
+ return testAction(
actions.requestDeletePackage,
payload,
{ pagination: { page: 1 } },
@@ -212,13 +194,12 @@ describe('Actions Package list store', () => {
{ type: 'setLoading', payload: true },
{ type: 'requestPackagesList', payload: { page: 1 } },
],
- done,
);
});
- it('should stop the loading and call create flash on api error', (done) => {
+ it('should stop the loading and call create flash on api error', async () => {
mock.onDelete(payload._links.delete_api_path).replyOnce(400);
- testAction(
+ await testAction(
actions.requestDeletePackage,
payload,
null,
@@ -227,50 +208,44 @@ describe('Actions Package list store', () => {
{ type: 'setLoading', payload: true },
{ type: 'setLoading', payload: false },
],
- () => {
- expect(createFlash).toHaveBeenCalled();
- done();
- },
);
+ expect(createFlash).toHaveBeenCalled();
});
it.each`
property | actionPayload
${'_links'} | ${{}}
${'delete_api_path'} | ${{ _links: {} }}
- `('should reject and createFlash when $property is missing', ({ actionPayload }, done) => {
- testAction(actions.requestDeletePackage, actionPayload, null, [], []).catch((e) => {
+ `('should reject and createFlash when $property is missing', ({ actionPayload }) => {
+ return testAction(actions.requestDeletePackage, actionPayload, null, [], []).catch((e) => {
expect(e).toEqual(new Error(MISSING_DELETE_PATH_ERROR));
expect(createFlash).toHaveBeenCalledWith({
message: DELETE_PACKAGE_ERROR_MESSAGE,
});
- done();
});
});
});
describe('setSorting', () => {
- it('should commit SET_SORTING', (done) => {
- testAction(
+ it('should commit SET_SORTING', () => {
+ return testAction(
actions.setSorting,
'foo',
null,
[{ type: types.SET_SORTING, payload: 'foo' }],
[],
- done,
);
});
});
describe('setFilter', () => {
- it('should commit SET_FILTER', (done) => {
- testAction(
+ it('should commit SET_FILTER', () => {
+ return testAction(
actions.setFilter,
'foo',
null,
[{ type: types.SET_FILTER, payload: 'foo' }],
[],
- done,
);
});
});
diff --git a/spec/frontend/packages_and_registries/package_registry/components/list/packages_search_spec.js b/spec/frontend/packages_and_registries/package_registry/components/list/packages_search_spec.js
index 9e91b15bc6e..3670cfca8ea 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/list/packages_search_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/list/packages_search_spec.js
@@ -73,7 +73,6 @@ describe('Package Search', () => {
mountComponent();
expect(findLocalStorageSync().props()).toMatchObject({
- asJson: true,
storageKey: 'package_registry_list_sorting',
value: {
orderBy: LIST_KEY_CREATED_AT,
diff --git a/spec/frontend/packages_and_registries/package_registry/pages/__snapshots__/list_spec.js.snap b/spec/frontend/packages_and_registries/package_registry/pages/__snapshots__/list_spec.js.snap
index 0154486e224..17905a8db2d 100644
--- a/spec/frontend/packages_and_registries/package_registry/pages/__snapshots__/list_spec.js.snap
+++ b/spec/frontend/packages_and_registries/package_registry/pages/__snapshots__/list_spec.js.snap
@@ -21,7 +21,7 @@ exports[`PackagesListApp renders 1`] = `
>
<img
alt=""
- class="gl-max-w-full"
+ class="gl-max-w-full gl-dark-invert-keep-hue"
role="img"
src="emptyListIllustration"
/>
diff --git a/spec/frontend/packages_and_registries/settings/project/settings/components/registry_settings_app_spec.js b/spec/frontend/packages_and_registries/settings/project/settings/components/registry_settings_app_spec.js
index a6c929844b1..0a72f0269ee 100644
--- a/spec/frontend/packages_and_registries/settings/project/settings/components/registry_settings_app_spec.js
+++ b/spec/frontend/packages_and_registries/settings/project/settings/components/registry_settings_app_spec.js
@@ -12,7 +12,6 @@ import {
UNAVAILABLE_USER_FEATURE_TEXT,
} from '~/packages_and_registries/settings/project/constants';
import expirationPolicyQuery from '~/packages_and_registries/settings/project/graphql/queries/get_expiration_policy.query.graphql';
-import CleanupPolicyEnabledAlert from '~/packages_and_registries/shared/components/cleanup_policy_enabled_alert.vue';
import SettingsBlock from '~/vue_shared/components/settings/settings_block.vue';
import {
@@ -31,12 +30,11 @@ describe('Registry Settings App', () => {
adminSettingsPath: 'settingsPath',
enableHistoricEntries: false,
helpPagePath: 'helpPagePath',
- showCleanupPolicyOnAlert: false,
+ showCleanupPolicyLink: false,
};
const findSettingsComponent = () => wrapper.find(SettingsForm);
const findAlert = () => wrapper.find(GlAlert);
- const findCleanupAlert = () => wrapper.findComponent(CleanupPolicyEnabledAlert);
const mountComponent = (provide = defaultProvidedValues, config) => {
wrapper = shallowMount(component, {
@@ -69,26 +67,6 @@ describe('Registry Settings App', () => {
wrapper.destroy();
});
- describe('cleanup is on alert', () => {
- it('exist when showCleanupPolicyOnAlert is true and has the correct props', () => {
- mountComponent({
- ...defaultProvidedValues,
- showCleanupPolicyOnAlert: true,
- });
-
- expect(findCleanupAlert().exists()).toBe(true);
- expect(findCleanupAlert().props()).toMatchObject({
- projectPath: 'path',
- });
- });
-
- it('is hidden when showCleanupPolicyOnAlert is false', async () => {
- mountComponent();
-
- expect(findCleanupAlert().exists()).toBe(false);
- });
- });
-
describe('isEdited status', () => {
it.each`
description | apiResponse | workingCopy | result
diff --git a/spec/frontend/packages_and_registries/shared/components/__snapshots__/cleanup_policy_enabled_alert_spec.js.snap b/spec/frontend/packages_and_registries/shared/components/__snapshots__/cleanup_policy_enabled_alert_spec.js.snap
deleted file mode 100644
index 2cded2ead2e..00000000000
--- a/spec/frontend/packages_and_registries/shared/components/__snapshots__/cleanup_policy_enabled_alert_spec.js.snap
+++ /dev/null
@@ -1,19 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`CleanupPolicyEnabledAlert renders 1`] = `
-<gl-alert-stub
- class="gl-mt-2"
- dismissible="true"
- dismisslabel="Dismiss"
- primarybuttonlink=""
- primarybuttontext=""
- secondarybuttonlink=""
- secondarybuttontext=""
- title=""
- variant="info"
->
- <gl-sprintf-stub
- message="Cleanup policies are now available for this project. %{linkStart}Click here to get started.%{linkEnd}"
- />
-</gl-alert-stub>
-`;
diff --git a/spec/frontend/packages_and_registries/shared/components/__snapshots__/registry_breadcrumb_spec.js.snap b/spec/frontend/packages_and_registries/shared/components/__snapshots__/registry_breadcrumb_spec.js.snap
index ceae8eebaef..3dd6023140f 100644
--- a/spec/frontend/packages_and_registries/shared/components/__snapshots__/registry_breadcrumb_spec.js.snap
+++ b/spec/frontend/packages_and_registries/shared/components/__snapshots__/registry_breadcrumb_spec.js.snap
@@ -10,11 +10,10 @@ exports[`Registry Breadcrumb when is not rootRoute renders 1`] = `
class="breadcrumb gl-breadcrumb-list"
>
<li
- class="breadcrumb-item gl-breadcrumb-item"
+ class="gl-breadcrumb-item"
>
<a
class=""
- href="/"
target="_self"
>
<span>
@@ -45,9 +44,10 @@ exports[`Registry Breadcrumb when is not rootRoute renders 1`] = `
<!---->
<li
- class="breadcrumb-item gl-breadcrumb-item"
+ class="gl-breadcrumb-item"
>
<a
+ aria-current="page"
class=""
href="#"
target="_self"
@@ -75,11 +75,11 @@ exports[`Registry Breadcrumb when is rootRoute renders 1`] = `
class="breadcrumb gl-breadcrumb-list"
>
<li
- class="breadcrumb-item gl-breadcrumb-item"
+ class="gl-breadcrumb-item"
>
<a
+ aria-current="page"
class=""
- href="/"
target="_self"
>
<span>
diff --git a/spec/frontend/packages_and_registries/shared/components/cleanup_policy_enabled_alert_spec.js b/spec/frontend/packages_and_registries/shared/components/cleanup_policy_enabled_alert_spec.js
deleted file mode 100644
index 269e087f5ac..00000000000
--- a/spec/frontend/packages_and_registries/shared/components/cleanup_policy_enabled_alert_spec.js
+++ /dev/null
@@ -1,49 +0,0 @@
-import { GlAlert } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
-import { nextTick } from 'vue';
-import component from '~/packages_and_registries/shared/components/cleanup_policy_enabled_alert.vue';
-import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
-
-describe('CleanupPolicyEnabledAlert', () => {
- let wrapper;
-
- const defaultProps = {
- projectPath: 'foo',
- cleanupPoliciesSettingsPath: 'label-bar',
- };
-
- const findAlert = () => wrapper.findComponent(GlAlert);
-
- const mountComponent = (props) => {
- wrapper = shallowMount(component, {
- stubs: {
- LocalStorageSync,
- },
- propsData: {
- ...defaultProps,
- ...props,
- },
- });
- };
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- it('renders', () => {
- mountComponent();
-
- expect(wrapper.element).toMatchSnapshot();
- });
-
- it('when dismissed is not visible', async () => {
- mountComponent();
-
- expect(findAlert().exists()).toBe(true);
- findAlert().vm.$emit('dismiss');
-
- await nextTick();
-
- expect(findAlert().exists()).toBe(false);
- });
-});
diff --git a/spec/frontend/packages_and_registries/shared/components/registry_breadcrumb_spec.js b/spec/frontend/packages_and_registries/shared/components/registry_breadcrumb_spec.js
index 6dfe116c285..15db454ac68 100644
--- a/spec/frontend/packages_and_registries/shared/components/registry_breadcrumb_spec.js
+++ b/spec/frontend/packages_and_registries/shared/components/registry_breadcrumb_spec.js
@@ -1,4 +1,4 @@
-import { mount } from '@vue/test-utils';
+import { mount, RouterLinkStub } from '@vue/test-utils';
import component from '~/packages_and_registries/shared/components/registry_breadcrumb.vue';
@@ -21,6 +21,9 @@ describe('Registry Breadcrumb', () => {
},
},
},
+ stubs: {
+ RouterLink: RouterLinkStub,
+ },
});
};
@@ -30,7 +33,6 @@ describe('Registry Breadcrumb', () => {
afterEach(() => {
wrapper.destroy();
- wrapper = null;
});
describe('when is rootRoute', () => {
@@ -46,7 +48,6 @@ describe('Registry Breadcrumb', () => {
const links = wrapper.findAll('a');
expect(links).toHaveLength(1);
- expect(links.at(0).attributes('href')).toBe('/');
});
it('the link text is calculated by nameGenerator', () => {
@@ -67,7 +68,6 @@ describe('Registry Breadcrumb', () => {
const links = wrapper.findAll('a');
expect(links).toHaveLength(2);
- expect(links.at(0).attributes('href')).toBe('/');
expect(links.at(1).attributes('href')).toBe('#');
});
diff --git a/spec/frontend/pager_spec.js b/spec/frontend/pager_spec.js
index 043ea470436..9df69124d66 100644
--- a/spec/frontend/pager_spec.js
+++ b/spec/frontend/pager_spec.js
@@ -68,34 +68,34 @@ describe('pager', () => {
it('shows loader while loading next page', async () => {
mockSuccess();
- jest.spyOn(Pager.loading, 'show').mockImplementation(() => {});
+ jest.spyOn(Pager.$loading, 'show').mockImplementation(() => {});
Pager.getOld();
await waitForPromises();
- expect(Pager.loading.show).toHaveBeenCalled();
+ expect(Pager.$loading.show).toHaveBeenCalled();
});
it('hides loader on success', async () => {
mockSuccess();
- jest.spyOn(Pager.loading, 'hide').mockImplementation(() => {});
+ jest.spyOn(Pager.$loading, 'hide').mockImplementation(() => {});
Pager.getOld();
await waitForPromises();
- expect(Pager.loading.hide).toHaveBeenCalled();
+ expect(Pager.$loading.hide).toHaveBeenCalled();
});
it('hides loader on error', async () => {
mockError();
- jest.spyOn(Pager.loading, 'hide').mockImplementation(() => {});
+ jest.spyOn(Pager.$loading, 'hide').mockImplementation(() => {});
Pager.getOld();
await waitForPromises();
- expect(Pager.loading.hide).toHaveBeenCalled();
+ expect(Pager.$loading.hide).toHaveBeenCalled();
});
it('sends request to url with offset and limit params', async () => {
@@ -122,12 +122,12 @@ describe('pager', () => {
Pager.limit = 20;
mockSuccess(1);
- jest.spyOn(Pager.loading, 'hide').mockImplementation(() => {});
+ jest.spyOn(Pager.$loading, 'hide').mockImplementation(() => {});
Pager.getOld();
await waitForPromises();
- expect(Pager.loading.hide).toHaveBeenCalled();
+ expect(Pager.$loading.hide).toHaveBeenCalled();
expect(Pager.disable).toBe(true);
});
@@ -175,5 +175,46 @@ describe('pager', () => {
expect(axios.get).toHaveBeenCalledWith(href, expect.any(Object));
});
});
+
+ describe('when `container` is passed', () => {
+ const href = '/some_list';
+ const container = '#js-pager';
+ let endlessScrollCallback;
+
+ beforeEach(() => {
+ jest.spyOn(axios, 'get');
+ jest.spyOn($.fn, 'endlessScroll').mockImplementation(({ callback }) => {
+ endlessScrollCallback = callback;
+ });
+ });
+
+ describe('when `container` is visible', () => {
+ it('makes API request', () => {
+ setFixtures(
+ `<div id="js-pager"><div class="content_list" data-href="${href}"></div></div>`,
+ );
+
+ Pager.init({ container });
+
+ endlessScrollCallback();
+
+ expect(axios.get).toHaveBeenCalledWith(href, expect.any(Object));
+ });
+ });
+
+ describe('when `container` is not visible', () => {
+ it('does not make API request', () => {
+ setFixtures(
+ `<div id="js-pager" style="display: none;"><div class="content_list" data-href="${href}"></div></div>`,
+ );
+
+ Pager.init({ container });
+
+ endlessScrollCallback();
+
+ expect(axios.get).not.toHaveBeenCalled();
+ });
+ });
+ });
});
});
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 9f326dc33c0..3a4f93d4464 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
@@ -23,13 +23,12 @@ describe('AccountAndLimits', () => {
expect($userInternalRegex.readOnly).toBeTruthy();
});
- it('is checked', (done) => {
+ it('is checked', () => {
if (!$userDefaultExternal.prop('checked')) $userDefaultExternal.click();
expect($userDefaultExternal.prop('checked')).toBeTruthy();
expect($userInternalRegex.placeholder).toEqual(PLACEHOLDER_USER_EXTERNAL_DEFAULT_TRUE);
expect($userInternalRegex.readOnly).toBeFalsy();
- done();
});
});
});
diff --git a/spec/frontend/pages/admin/jobs/index/components/stop_jobs_modal_spec.js b/spec/frontend/pages/admin/jobs/index/components/stop_jobs_modal_spec.js
index 52648d3ce00..ebf21c01324 100644
--- a/spec/frontend/pages/admin/jobs/index/components/stop_jobs_modal_spec.js
+++ b/spec/frontend/pages/admin/jobs/index/components/stop_jobs_modal_spec.js
@@ -26,7 +26,7 @@ describe('stop_jobs_modal.vue', () => {
});
describe('onSubmit', () => {
- it('stops jobs and redirects to overview page', (done) => {
+ it('stops jobs and redirects to overview page', async () => {
const responseURL = `${TEST_HOST}/stop_jobs_modal.vue/jobs`;
jest.spyOn(axios, 'post').mockImplementation((url) => {
expect(url).toBe(props.url);
@@ -37,29 +37,19 @@ describe('stop_jobs_modal.vue', () => {
});
});
- vm.onSubmit()
- .then(() => {
- expect(redirectTo).toHaveBeenCalledWith(responseURL);
- })
- .then(done)
- .catch(done.fail);
+ await vm.onSubmit();
+ expect(redirectTo).toHaveBeenCalledWith(responseURL);
});
- it('displays error if stopping jobs failed', (done) => {
+ it('displays error if stopping jobs failed', async () => {
const dummyError = new Error('stopping jobs failed');
jest.spyOn(axios, 'post').mockImplementation((url) => {
expect(url).toBe(props.url);
return Promise.reject(dummyError);
});
- vm.onSubmit()
- .then(done.fail)
- .catch((error) => {
- expect(error).toBe(dummyError);
- expect(redirectTo).not.toHaveBeenCalled();
- })
- .then(done)
- .catch(done.fail);
+ await expect(vm.onSubmit()).rejects.toEqual(dummyError);
+ expect(redirectTo).not.toHaveBeenCalled();
});
});
});
diff --git a/spec/frontend/pages/dashboard/todos/index/todos_spec.js b/spec/frontend/pages/dashboard/todos/index/todos_spec.js
index ef295e7d1ba..ae53afa7fba 100644
--- a/spec/frontend/pages/dashboard/todos/index/todos_spec.js
+++ b/spec/frontend/pages/dashboard/todos/index/todos_spec.js
@@ -31,15 +31,17 @@ describe('Todos', () => {
});
describe('goToTodoUrl', () => {
- it('opens the todo url', (done) => {
+ it('opens the todo url', () => {
const todoLink = todoItem.dataset.url;
+ let expectedUrl = null;
visitUrl.mockImplementation((url) => {
- expect(url).toEqual(todoLink);
- done();
+ expectedUrl = url;
});
todoItem.click();
+
+ expect(expectedUrl).toEqual(todoLink);
});
describe('meta click', () => {
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 6fb03fa28fe..43c48617800 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
@@ -137,6 +137,16 @@ describe('BulkImportsHistoryApp', () => {
);
});
+ 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 });
+ await axios.waitForAll();
+
+ expect(wrapper.find('tbody tr a').attributes().href).toBe(
+ `/${DUMMY_RESPONSE[0].destination_namespace}/${DUMMY_RESPONSE[0].destination_name}`,
+ );
+ });
+
describe('details button', () => {
beforeEach(() => {
mock.onGet(API_URL).reply(200, DUMMY_RESPONSE, DEFAULT_HEADERS);
diff --git a/spec/frontend/pages/import/history/components/import_error_details_spec.js b/spec/frontend/pages/import/history/components/import_error_details_spec.js
new file mode 100644
index 00000000000..4ff3f0361cf
--- /dev/null
+++ b/spec/frontend/pages/import/history/components/import_error_details_spec.js
@@ -0,0 +1,66 @@
+import { GlLoadingIcon } from '@gitlab/ui';
+import { mount, shallowMount } from '@vue/test-utils';
+import MockAdapter from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
+import ImportErrorDetails from '~/pages/import/history/components/import_error_details.vue';
+
+describe('ImportErrorDetails', () => {
+ const FAKE_ID = 5;
+ const API_URL = `/api/v4/projects/${FAKE_ID}`;
+
+ let wrapper;
+ let mock;
+
+ function createComponent({ shallow = true } = {}) {
+ const mountFn = shallow ? shallowMount : mount;
+ wrapper = mountFn(ImportErrorDetails, {
+ propsData: {
+ id: FAKE_ID,
+ },
+ });
+ }
+
+ const originalApiVersion = gon.api_version;
+ beforeAll(() => {
+ gon.api_version = 'v4';
+ });
+
+ afterAll(() => {
+ gon.api_version = originalApiVersion;
+ });
+
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+ });
+
+ afterEach(() => {
+ mock.restore();
+ wrapper.destroy();
+ });
+
+ describe('general behavior', () => {
+ it('renders loading state when loading', () => {
+ createComponent();
+ expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
+ });
+
+ it('renders import_error if it is available', async () => {
+ const FAKE_IMPORT_ERROR = 'IMPORT ERROR';
+ mock.onGet(API_URL).reply(200, { import_error: FAKE_IMPORT_ERROR });
+ createComponent();
+ await axios.waitForAll();
+
+ expect(wrapper.find(GlLoadingIcon).exists()).toBe(false);
+ expect(wrapper.find('pre').text()).toBe(FAKE_IMPORT_ERROR);
+ });
+
+ it('renders default text if error is not available', async () => {
+ mock.onGet(API_URL).reply(200, { import_error: null });
+ createComponent();
+ await axios.waitForAll();
+
+ expect(wrapper.find(GlLoadingIcon).exists()).toBe(false);
+ expect(wrapper.find('pre').text()).toBe('No additional information provided.');
+ });
+ });
+});
diff --git a/spec/frontend/pages/import/history/components/import_history_app_spec.js b/spec/frontend/pages/import/history/components/import_history_app_spec.js
new file mode 100644
index 00000000000..0d821b114cf
--- /dev/null
+++ b/spec/frontend/pages/import/history/components/import_history_app_spec.js
@@ -0,0 +1,205 @@
+import { GlEmptyState, GlLoadingIcon, GlTable } from '@gitlab/ui';
+import { mount, shallowMount } from '@vue/test-utils';
+import MockAdapter from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
+import ImportErrorDetails from '~/pages/import/history/components/import_error_details.vue';
+import ImportHistoryApp from '~/pages/import/history/components/import_history_app.vue';
+import PaginationBar from '~/vue_shared/components/pagination_bar/pagination_bar.vue';
+import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import { stubComponent } from 'helpers/stub_component';
+
+describe('ImportHistoryApp', () => {
+ const API_URL = '/api/v4/projects.json';
+
+ const DEFAULT_HEADERS = {
+ 'x-page': 1,
+ 'x-per-page': 20,
+ 'x-next-page': 2,
+ 'x-total': 22,
+ 'x-total-pages': 2,
+ 'x-prev-page': null,
+ };
+ const DUMMY_RESPONSE = [
+ {
+ id: 1,
+ path_with_namespace: 'root/imported',
+ created_at: '2022-03-10T15:10:03.172Z',
+ import_url: null,
+ import_type: 'gitlab_project',
+ import_status: 'finished',
+ },
+ {
+ id: 2,
+ name_with_namespace: 'Administrator / Dummy',
+ path_with_namespace: 'root/dummy',
+ created_at: '2022-03-09T11:23:04.974Z',
+ import_url: 'https://dummy.github/url',
+ import_type: 'github',
+ import_status: 'failed',
+ },
+ {
+ id: 3,
+ name_with_namespace: 'Administrator / Dummy',
+ path_with_namespace: 'root/dummy2',
+ created_at: '2022-03-09T11:23:04.974Z',
+ import_url: 'git://non-http.url',
+ import_type: 'gi',
+ import_status: 'finished',
+ },
+ ];
+ let wrapper;
+ let mock;
+
+ function createComponent({ shallow = true } = {}) {
+ const mountFn = shallow ? shallowMount : mount;
+ wrapper = mountFn(ImportHistoryApp, {
+ provide: { assets: { gitlabLogo: 'http://dummy.host' } },
+ stubs: shallow ? { GlTable: { ...stubComponent(GlTable), props: ['items'] } } : {},
+ });
+ }
+
+ const originalApiVersion = gon.api_version;
+ beforeAll(() => {
+ gon.api_version = 'v4';
+ });
+
+ afterAll(() => {
+ gon.api_version = originalApiVersion;
+ });
+
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+ });
+
+ afterEach(() => {
+ mock.restore();
+ wrapper.destroy();
+ });
+
+ describe('general behavior', () => {
+ it('renders loading state when loading', () => {
+ createComponent();
+ expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
+ });
+
+ it('renders empty state when no data is available', async () => {
+ mock.onGet(API_URL).reply(200, [], DEFAULT_HEADERS);
+ createComponent();
+ await axios.waitForAll();
+
+ expect(wrapper.find(GlLoadingIcon).exists()).toBe(false);
+ expect(wrapper.find(GlEmptyState).exists()).toBe(true);
+ });
+
+ it('renders table with data when history is available', async () => {
+ mock.onGet(API_URL).reply(200, DUMMY_RESPONSE, DEFAULT_HEADERS);
+ createComponent();
+ await axios.waitForAll();
+
+ const table = wrapper.find(GlTable);
+ expect(table.exists()).toBe(true);
+ expect(table.props().items).toStrictEqual(DUMMY_RESPONSE);
+ });
+
+ it('changes page when requested by pagination bar', async () => {
+ const NEW_PAGE = 4;
+
+ mock.onGet(API_URL).reply(200, DUMMY_RESPONSE, DEFAULT_HEADERS);
+ createComponent();
+ await axios.waitForAll();
+ mock.resetHistory();
+
+ const FAKE_NEXT_PAGE_REPLY = [
+ {
+ id: 4,
+ path_with_namespace: 'root/some_other_project',
+ created_at: '2022-03-10T15:10:03.172Z',
+ import_url: null,
+ import_type: 'gitlab_project',
+ import_status: 'finished',
+ },
+ ];
+
+ mock.onGet(API_URL).reply(200, FAKE_NEXT_PAGE_REPLY, DEFAULT_HEADERS);
+
+ wrapper.findComponent(PaginationBar).vm.$emit('set-page', NEW_PAGE);
+ await axios.waitForAll();
+
+ expect(mock.history.get.length).toBe(1);
+ expect(mock.history.get[0].params).toStrictEqual(expect.objectContaining({ page: NEW_PAGE }));
+ expect(wrapper.find(GlTable).props().items).toStrictEqual(FAKE_NEXT_PAGE_REPLY);
+ });
+ });
+
+ it('changes page size when requested by pagination bar', async () => {
+ const NEW_PAGE_SIZE = 4;
+
+ mock.onGet(API_URL).reply(200, DUMMY_RESPONSE, DEFAULT_HEADERS);
+ createComponent();
+ await axios.waitForAll();
+ mock.resetHistory();
+
+ wrapper.findComponent(PaginationBar).vm.$emit('set-page-size', NEW_PAGE_SIZE);
+ await axios.waitForAll();
+
+ expect(mock.history.get.length).toBe(1);
+ expect(mock.history.get[0].params).toStrictEqual(
+ expect.objectContaining({ per_page: NEW_PAGE_SIZE }),
+ );
+ });
+
+ it('resets page to 1 when page size is changed', async () => {
+ const NEW_PAGE_SIZE = 4;
+
+ mock.onGet(API_URL).reply(200, DUMMY_RESPONSE, DEFAULT_HEADERS);
+ createComponent();
+ await axios.waitForAll();
+ wrapper.findComponent(PaginationBar).vm.$emit('set-page', 2);
+ await axios.waitForAll();
+ mock.resetHistory();
+
+ wrapper.findComponent(PaginationBar).vm.$emit('set-page-size', NEW_PAGE_SIZE);
+ await axios.waitForAll();
+
+ expect(mock.history.get.length).toBe(1);
+ expect(mock.history.get[0].params).toStrictEqual(
+ expect.objectContaining({ per_page: NEW_PAGE_SIZE, page: 1 }),
+ );
+ });
+
+ describe('details button', () => {
+ beforeEach(() => {
+ mock.onGet(API_URL).reply(200, DUMMY_RESPONSE, DEFAULT_HEADERS);
+ createComponent({ shallow: false });
+ return axios.waitForAll();
+ });
+
+ it('renders details button if relevant item has failed', async () => {
+ expect(
+ extendedWrapper(wrapper.find('tbody').findAll('tr').at(1)).findByText('Details').exists(),
+ ).toBe(true);
+ });
+
+ it('does not render details button if relevant item does not failed', () => {
+ expect(
+ extendedWrapper(wrapper.find('tbody').findAll('tr').at(0)).findByText('Details').exists(),
+ ).toBe(false);
+ });
+
+ it('expands details when details button is clicked', async () => {
+ const ORIGINAL_ROW_INDEX = 1;
+ await extendedWrapper(wrapper.find('tbody').findAll('tr').at(ORIGINAL_ROW_INDEX))
+ .findByText('Details')
+ .trigger('click');
+
+ const detailsRowContent = wrapper
+ .find('tbody')
+ .findAll('tr')
+ .at(ORIGINAL_ROW_INDEX + 1)
+ .findComponent(ImportErrorDetails);
+
+ expect(detailsRowContent.exists()).toBe(true);
+ expect(detailsRowContent.props().id).toBe(DUMMY_RESPONSE[1].id);
+ });
+ });
+});
diff --git a/spec/frontend/pages/profiles/show/emoji_menu_spec.js b/spec/frontend/pages/profiles/show/emoji_menu_spec.js
index f35fb57aec7..fa6e7e51a60 100644
--- a/spec/frontend/pages/profiles/show/emoji_menu_spec.js
+++ b/spec/frontend/pages/profiles/show/emoji_menu_spec.js
@@ -46,22 +46,18 @@ describe('EmojiMenu', () => {
const dummyEmoji = 'tropical_fish';
const dummyVotesBlock = () => $('<div />');
- it('calls selectEmojiCallback', (done) => {
+ it('calls selectEmojiCallback', async () => {
expect(dummySelectEmojiCallback).not.toHaveBeenCalled();
- emojiMenu.addAward(dummyVotesBlock(), dummyAwardUrl, dummyEmoji, false, () => {
- expect(dummySelectEmojiCallback).toHaveBeenCalledWith(dummyEmoji, dummyEmojiTag);
- done();
- });
+ await emojiMenu.addAward(dummyVotesBlock(), dummyAwardUrl, dummyEmoji, false);
+ expect(dummySelectEmojiCallback).toHaveBeenCalledWith(dummyEmoji, dummyEmojiTag);
});
- it('does not make an axios request', (done) => {
+ it('does not make an axios request', async () => {
jest.spyOn(axios, 'request').mockReturnValue();
- emojiMenu.addAward(dummyVotesBlock(), dummyAwardUrl, dummyEmoji, false, () => {
- expect(axios.request).not.toHaveBeenCalled();
- done();
- });
+ await emojiMenu.addAward(dummyVotesBlock(), dummyAwardUrl, dummyEmoji, false);
+ expect(axios.request).not.toHaveBeenCalled();
});
});
diff --git a/spec/frontend/pages/projects/learn_gitlab/components/__snapshots__/learn_gitlab_section_card_spec.js.snap b/spec/frontend/pages/projects/learn_gitlab/components/__snapshots__/learn_gitlab_section_card_spec.js.snap
index 9e00ace761c..83feb621478 100644
--- a/spec/frontend/pages/projects/learn_gitlab/components/__snapshots__/learn_gitlab_section_card_spec.js.snap
+++ b/spec/frontend/pages/projects/learn_gitlab/components/__snapshots__/learn_gitlab_section_card_spec.js.snap
@@ -2,31 +2,26 @@
exports[`Learn GitLab Section Card renders correctly 1`] = `
<gl-card-stub
- bodyclass=""
- class="gl-pt-0 learn-gitlab-section-card"
+ bodyclass="gl-pt-0"
+ class="gl-pt-0 h-100"
footerclass=""
- headerclass=""
+ headerclass="gl-bg-white gl-border-0 gl-pb-0"
>
- <div
- class="learn-gitlab-section-card-header"
+ <img
+ src="workspace.svg"
+ />
+
+ <h2
+ class="gl-font-lg gl-mb-3"
>
- <img
- src="workspace.svg"
- />
-
- <h2
- class="gl-font-lg gl-mb-3"
- >
- Set up your workspace
- </h2>
-
- <p
- class="gl-text-gray-700 gl-mb-6"
- >
- Complete these tasks first so you can enjoy GitLab's features to their fullest:
- </p>
- </div>
+ Set up your workspace
+ </h2>
+ <p
+ class="gl-text-gray-700 gl-mb-6"
+ >
+ Complete these tasks first so you can enjoy GitLab's features to their fullest:
+ </p>
<learn-gitlab-section-link-stub
action="userAdded"
value="[object Object]"
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 62cf769cffd..269c7467c8b 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
@@ -51,170 +51,204 @@ exports[`Learn GitLab renders correctly 1`] = `
</div>
<div
- class="row row-cols-1 row-cols-md-3 gl-mt-5"
+ class="row"
>
<div
- class="col gl-mb-6"
+ class="gl-mt-5 col-sm-12 col-mb-6 col-lg-4"
>
<div
- class="gl-card gl-pt-0 learn-gitlab-section-card"
+ class="gl-card gl-pt-0 h-100"
>
- <!---->
-
<div
- class="gl-card-body"
+ class="gl-card-header gl-bg-white gl-border-0 gl-pb-0"
>
- <div
- class="learn-gitlab-section-card-header"
+ <img
+ src="workspace.svg"
+ />
+
+ <h2
+ class="gl-font-lg gl-mb-3"
>
- <img
- src="workspace.svg"
- />
-
- <h2
- class="gl-font-lg gl-mb-3"
- >
- Set up your workspace
- </h2>
-
- <p
- class="gl-text-gray-700 gl-mb-6"
- >
- Complete these tasks first so you can enjoy GitLab's features to their fullest:
- </p>
- </div>
+ Set up your workspace
+ </h2>
+ <p
+ class="gl-text-gray-700 gl-mb-6"
+ >
+ Complete these tasks first so you can enjoy GitLab's features to their fullest:
+ </p>
+ </div>
+
+ <div
+ class="gl-card-body gl-pt-0"
+ >
<div
class="gl-mb-4"
>
- <span
- class="gl-text-green-500"
+ <!---->
+
+ <div
+ class="flex align-items-center"
>
- <svg
- aria-hidden="true"
- class="gl-icon s16"
- data-testid="completed-icon"
- role="img"
+ <span
+ class="gl-text-green-500"
>
- <use
- href="#check-circle-filled"
- />
- </svg>
-
- Invite your colleagues
-
- </span>
-
- <!---->
+ <svg
+ aria-hidden="true"
+ class="gl-icon s16"
+ data-testid="completed-icon"
+ role="img"
+ >
+ <use
+ href="#check-circle-filled"
+ />
+ </svg>
+
+ Invite your colleagues
+
+ </span>
+
+ <!---->
+ </div>
</div>
<div
class="gl-mb-4"
>
- <span
- class="gl-text-green-500"
+ <!---->
+
+ <div
+ class="flex align-items-center"
>
- <svg
- aria-hidden="true"
- class="gl-icon s16"
- data-testid="completed-icon"
- role="img"
+ <span
+ class="gl-text-green-500"
>
- <use
- href="#check-circle-filled"
- />
- </svg>
-
- Create or import a repository
-
- </span>
-
- <!---->
+ <svg
+ aria-hidden="true"
+ class="gl-icon s16"
+ data-testid="completed-icon"
+ role="img"
+ >
+ <use
+ href="#check-circle-filled"
+ />
+ </svg>
+
+ Create or import a repository
+
+ </span>
+
+ <!---->
+ </div>
</div>
<div
class="gl-mb-4"
>
- <a
- class="gl-link"
- data-testid="uncompleted-learn-gitlab-link"
- data-track-action="click_link"
- data-track-label="Set up CI/CD"
- href="http://example.com/"
- target="_self"
- >
-
- Set up CI/CD
-
- </a>
-
<!---->
+
+ <div
+ class="flex align-items-center"
+ >
+ <a
+ class="gl-link"
+ data-testid="uncompleted-learn-gitlab-link"
+ data-track-action="click_link"
+ data-track-label="Set up CI/CD"
+ href="http://example.com/"
+ target="_self"
+ >
+
+ Set up CI/CD
+
+ </a>
+
+ <!---->
+ </div>
</div>
<div
class="gl-mb-4"
>
- <a
- class="gl-link"
- data-testid="uncompleted-learn-gitlab-link"
- data-track-action="click_link"
- data-track-label="Start a free Ultimate trial"
- href="http://example.com/"
- target="_self"
- >
-
- Start a free Ultimate trial
-
- </a>
-
<!---->
+
+ <div
+ class="flex align-items-center"
+ >
+ <a
+ class="gl-link"
+ data-testid="uncompleted-learn-gitlab-link"
+ data-track-action="click_link"
+ data-track-label="Start a free Ultimate trial"
+ href="http://example.com/"
+ target="_self"
+ >
+
+ Start a free Ultimate trial
+
+ </a>
+
+ <!---->
+ </div>
</div>
<div
class="gl-mb-4"
>
- <a
- class="gl-link"
- data-testid="uncompleted-learn-gitlab-link"
- data-track-action="click_link"
- data-track-label="Add code owners"
- href="http://example.com/"
- target="_self"
- >
-
- Add code owners
-
- </a>
-
- <span
+ <div
class="gl-font-style-italic gl-text-gray-500"
data-testid="trial-only"
>
- - Trial only
+ Trial only
- </span>
+ </div>
+
+ <div
+ class="flex align-items-center"
+ >
+ <a
+ class="gl-link"
+ data-testid="uncompleted-learn-gitlab-link"
+ data-track-action="click_link"
+ data-track-label="Add code owners"
+ href="http://example.com/"
+ target="_self"
+ >
+
+ Add code owners
+
+ </a>
+
+ <!---->
+ </div>
</div>
<div
class="gl-mb-4"
>
- <a
- class="gl-link"
- data-testid="uncompleted-learn-gitlab-link"
- data-track-action="click_link"
- data-track-label="Add merge request approval"
- href="http://example.com/"
- target="_self"
- >
-
- Add merge request approval
-
- </a>
-
- <span
+ <div
class="gl-font-style-italic gl-text-gray-500"
data-testid="trial-only"
>
- - Trial only
+ Trial only
- </span>
+ </div>
+
+ <div
+ class="flex align-items-center"
+ >
+ <a
+ class="gl-link"
+ data-testid="uncompleted-learn-gitlab-link"
+ data-track-action="click_link"
+ data-track-label="Add merge request approval"
+ href="http://example.com/"
+ target="_self"
+ >
+
+ Add merge request approval
+
+ </a>
+
+ <!---->
+ </div>
</div>
</div>
@@ -222,71 +256,81 @@ exports[`Learn GitLab renders correctly 1`] = `
</div>
</div>
<div
- class="col gl-mb-6"
+ class="gl-mt-5 col-sm-12 col-mb-6 col-lg-4"
>
<div
- class="gl-card gl-pt-0 learn-gitlab-section-card"
+ class="gl-card gl-pt-0 h-100"
>
- <!---->
-
<div
- class="gl-card-body"
+ class="gl-card-header gl-bg-white gl-border-0 gl-pb-0"
>
- <div
- class="learn-gitlab-section-card-header"
+ <img
+ src="plan.svg"
+ />
+
+ <h2
+ class="gl-font-lg gl-mb-3"
>
- <img
- src="plan.svg"
- />
-
- <h2
- class="gl-font-lg gl-mb-3"
- >
- Plan and execute
- </h2>
-
- <p
- class="gl-text-gray-700 gl-mb-6"
- >
- Create a workflow for your new workspace, and learn how GitLab features work together:
- </p>
- </div>
+ Plan and execute
+ </h2>
+ <p
+ class="gl-text-gray-700 gl-mb-6"
+ >
+ Create a workflow for your new workspace, and learn how GitLab features work together:
+ </p>
+ </div>
+
+ <div
+ class="gl-card-body gl-pt-0"
+ >
<div
class="gl-mb-4"
>
- <a
- class="gl-link"
- data-testid="uncompleted-learn-gitlab-link"
- data-track-action="click_link"
- data-track-label="Create an issue"
- href="http://example.com/"
- target="_self"
- >
-
- Create an issue
-
- </a>
-
<!---->
+
+ <div
+ class="flex align-items-center"
+ >
+ <a
+ class="gl-link"
+ data-testid="uncompleted-learn-gitlab-link"
+ data-track-action="click_link"
+ data-track-label="Create an issue"
+ href="http://example.com/"
+ target="_self"
+ >
+
+ Create an issue
+
+ </a>
+
+ <!---->
+ </div>
</div>
<div
class="gl-mb-4"
>
- <a
- class="gl-link"
- data-testid="uncompleted-learn-gitlab-link"
- data-track-action="click_link"
- data-track-label="Submit a merge request"
- href="http://example.com/"
- target="_self"
- >
-
- Submit a merge request
-
- </a>
-
<!---->
+
+ <div
+ class="flex align-items-center"
+ >
+ <a
+ class="gl-link"
+ data-testid="uncompleted-learn-gitlab-link"
+ data-track-action="click_link"
+ data-track-label="Submit a merge request"
+ href="http://example.com/"
+ target="_self"
+ >
+
+ Submit a merge request
+
+ </a>
+
+ <!---->
+ </div>
</div>
</div>
@@ -294,54 +338,58 @@ exports[`Learn GitLab renders correctly 1`] = `
</div>
</div>
<div
- class="col gl-mb-6"
+ class="gl-mt-5 col-sm-12 col-mb-6 col-lg-4"
>
<div
- class="gl-card gl-pt-0 learn-gitlab-section-card"
+ class="gl-card gl-pt-0 h-100"
>
- <!---->
-
<div
- class="gl-card-body"
+ class="gl-card-header gl-bg-white gl-border-0 gl-pb-0"
>
- <div
- class="learn-gitlab-section-card-header"
+ <img
+ src="deploy.svg"
+ />
+
+ <h2
+ class="gl-font-lg gl-mb-3"
>
- <img
- src="deploy.svg"
- />
-
- <h2
- class="gl-font-lg gl-mb-3"
- >
- Deploy
- </h2>
-
- <p
- class="gl-text-gray-700 gl-mb-6"
- >
- Use your new GitLab workflow to deploy your application, monitor its health, and keep it secure:
- </p>
- </div>
+ Deploy
+ </h2>
+ <p
+ class="gl-text-gray-700 gl-mb-6"
+ >
+ Use your new GitLab workflow to deploy your application, monitor its health, and keep it secure:
+ </p>
+ </div>
+
+ <div
+ class="gl-card-body gl-pt-0"
+ >
<div
class="gl-mb-4"
>
- <a
- class="gl-link"
- data-testid="uncompleted-learn-gitlab-link"
- data-track-action="click_link"
- data-track-label="Run a Security scan using CI/CD"
- href="https://docs.gitlab.com/ee/foobar/"
- rel="noopener noreferrer"
- target="_blank"
- >
-
- Run a Security scan using CI/CD
-
- </a>
-
<!---->
+
+ <div
+ class="flex align-items-center"
+ >
+ <a
+ class="gl-link"
+ data-testid="uncompleted-learn-gitlab-link"
+ data-track-action="click_link"
+ data-track-label="Run a Security scan using CI/CD"
+ href="https://docs.gitlab.com/ee/foobar/"
+ rel="noopener noreferrer"
+ target="_blank"
+ >
+
+ Run a Security scan using CI/CD
+
+ </a>
+
+ <!---->
+ </div>
</div>
</div>
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 e21371123e8..b8ebf2a1430 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,4 +1,4 @@
-import { shallowMount } from '@vue/test-utils';
+import { mount } from '@vue/test-utils';
import { stubExperiments } from 'helpers/experimentation_helper';
import { mockTracking, triggerEvent, unmockTracking } from 'helpers/tracking_helper';
import eventHub from '~/invite_members/event_hub';
@@ -26,7 +26,7 @@ describe('Learn GitLab Section Link', () => {
});
const createWrapper = (action = defaultAction, props = {}) => {
- wrapper = shallowMount(LearnGitlabSectionLink, {
+ wrapper = mount(LearnGitlabSectionLink, {
propsData: { action, value: { ...defaultProps, ...props } },
});
};
@@ -36,6 +36,8 @@ describe('Learn GitLab Section Link', () => {
const findUncompletedLink = () => wrapper.find('[data-testid="uncompleted-learn-gitlab-link"]');
+ const videoTutorialLink = () => wrapper.find('[data-testid="video-tutorial-link"]');
+
it('renders no icon when not completed', () => {
createWrapper(undefined, { completed: false });
@@ -130,4 +132,51 @@ describe('Learn GitLab Section Link', () => {
unmockTracking();
});
});
+
+ describe('video_tutorials_continuous_onboarding experiment', () => {
+ describe('when control', () => {
+ beforeEach(() => {
+ stubExperiments({ video_tutorials_continuous_onboarding: 'control' });
+ createWrapper('codeOwnersEnabled');
+ });
+
+ it('renders no video link', () => {
+ expect(videoTutorialLink().exists()).toBe(false);
+ });
+ });
+
+ describe('when candidate', () => {
+ beforeEach(() => {
+ stubExperiments({ video_tutorials_continuous_onboarding: 'candidate' });
+ createWrapper('codeOwnersEnabled');
+ });
+
+ it('renders video link with blank target', () => {
+ const videoLinkElement = videoTutorialLink();
+
+ expect(videoLinkElement.exists()).toBe(true);
+ expect(videoLinkElement.attributes('target')).toEqual('_blank');
+ });
+
+ it('tracks the click', () => {
+ const trackingSpy = mockTracking('_category_', wrapper.element, jest.spyOn);
+
+ videoTutorialLink().trigger('click');
+
+ expect(trackingSpy).toHaveBeenCalledWith('_category_', 'click_video_link', {
+ label: 'Add code owners',
+ property: 'Growth::Conversion::Experiment::LearnGitLab',
+ context: {
+ data: {
+ experiment: 'video_tutorials_continuous_onboarding',
+ variant: 'candidate',
+ },
+ schema: 'iglu:com.gitlab/gitlab_experiment/jsonschema/1-0-0',
+ },
+ });
+
+ unmockTracking();
+ });
+ });
+ });
});
diff --git a/spec/frontend/pages/projects/shared/permissions/components/project_feature_settings_spec.js b/spec/frontend/pages/projects/shared/permissions/components/project_feature_settings_spec.js
index 0fffcf433a3..5771e1b88e8 100644
--- a/spec/frontend/pages/projects/shared/permissions/components/project_feature_settings_spec.js
+++ b/spec/frontend/pages/projects/shared/permissions/components/project_feature_settings_spec.js
@@ -3,15 +3,17 @@ import { shallowMount } from '@vue/test-utils';
import ProjectFeatureSetting from '~/pages/projects/shared/permissions/components/project_feature_setting.vue';
describe('Project Feature Settings', () => {
+ const defaultOptions = [
+ [1, 1],
+ [2, 2],
+ [3, 3],
+ [4, 4],
+ [5, 5],
+ ];
+
const defaultProps = {
name: 'Test',
- options: [
- [1, 1],
- [2, 2],
- [3, 3],
- [4, 4],
- [5, 5],
- ],
+ options: defaultOptions,
value: 1,
disabledInput: false,
showToggle: true,
@@ -110,15 +112,25 @@ describe('Project Feature Settings', () => {
},
);
- it('should emit the change when a new option is selected', () => {
+ it('should emit the change when a new option is selected', async () => {
wrapper = mountComponent();
expect(wrapper.emitted('change')).toBeUndefined();
- wrapper.findAll('option').at(1).trigger('change');
+ await wrapper.findAll('option').at(1).setSelected();
expect(wrapper.emitted('change')).toHaveLength(1);
expect(wrapper.emitted('change')[0]).toEqual([2]);
});
+
+ it('value of select matches prop `value` if options are modified', async () => {
+ wrapper = mountComponent();
+
+ await wrapper.setProps({ value: 0, options: [[0, 0]] });
+ expect(wrapper.find('select').element.selectedIndex).toBe(0);
+
+ await wrapper.setProps({ value: 2, options: defaultOptions });
+ expect(wrapper.find('select').element.selectedIndex).toBe(1);
+ });
});
});
diff --git a/spec/frontend/pages/projects/shared/permissions/components/settings_panel_spec.js b/spec/frontend/pages/projects/shared/permissions/components/settings_panel_spec.js
index 305dce51971..30d5f89d2f6 100644
--- a/spec/frontend/pages/projects/shared/permissions/components/settings_panel_spec.js
+++ b/spec/frontend/pages/projects/shared/permissions/components/settings_panel_spec.js
@@ -1,6 +1,6 @@
import { GlSprintf, GlToggle } from '@gitlab/ui';
import { shallowMount, mount } from '@vue/test-utils';
-import projectFeatureSetting from '~/pages/projects/shared/permissions/components/project_feature_setting.vue';
+import ProjectFeatureSetting from '~/pages/projects/shared/permissions/components/project_feature_setting.vue';
import settingsPanel from '~/pages/projects/shared/permissions/components/settings_panel.vue';
import {
featureAccessLevel,
@@ -21,6 +21,7 @@ const defaultProps = {
wikiAccessLevel: 20,
snippetsAccessLevel: 20,
operationsAccessLevel: 20,
+ metricsDashboardAccessLevel: 20,
pagesAccessLevel: 10,
analyticsAccessLevel: 20,
containerRegistryAccessLevel: 20,
@@ -75,7 +76,7 @@ describe('Settings Panel', () => {
const findLFSFeatureToggle = () => findLFSSettingsRow().find(GlToggle);
const findRepositoryFeatureProjectRow = () => wrapper.find({ ref: 'repository-settings' });
const findRepositoryFeatureSetting = () =>
- findRepositoryFeatureProjectRow().find(projectFeatureSetting);
+ findRepositoryFeatureProjectRow().find(ProjectFeatureSetting);
const findProjectVisibilitySettings = () => wrapper.find({ ref: 'project-visibility-settings' });
const findIssuesSettingsRow = () => wrapper.find({ ref: 'issues-settings' });
const findAnalyticsRow = () => wrapper.find({ ref: 'analytics-settings' });
@@ -106,7 +107,11 @@ describe('Settings Panel', () => {
'input[name="project[project_setting_attributes][warn_about_potentially_unwanted_characters]"]',
);
const findMetricsVisibilitySettings = () => wrapper.find({ ref: 'metrics-visibility-settings' });
+ const findMetricsVisibilityInput = () =>
+ findMetricsVisibilitySettings().findComponent(ProjectFeatureSetting);
const findOperationsSettings = () => wrapper.find({ ref: 'operations-settings' });
+ const findOperationsVisibilityInput = () =>
+ findOperationsSettings().findComponent(ProjectFeatureSetting);
const findConfirmDangerButton = () => wrapper.findComponent(ConfirmDanger);
afterEach(() => {
@@ -595,7 +600,7 @@ describe('Settings Panel', () => {
});
describe('Metrics dashboard', () => {
- it('should show the metrics dashboard access toggle', () => {
+ it('should show the metrics dashboard access select', () => {
wrapper = mountComponent();
expect(findMetricsVisibilitySettings().exists()).toBe(true);
@@ -610,23 +615,51 @@ describe('Settings Panel', () => {
});
it.each`
- scenario | selectedOption | selectedOptionLabel
- ${{ currentSettings: { visibilityLevel: visibilityOptions.PRIVATE } }} | ${String(featureAccessLevel.PROJECT_MEMBERS)} | ${'Only Project Members'}
- ${{ currentSettings: { operationsAccessLevel: featureAccessLevel.NOT_ENABLED } }} | ${String(featureAccessLevel.NOT_ENABLED)} | ${'Enable feature to choose access level'}
+ before | after
+ ${featureAccessLevel.NOT_ENABLED} | ${featureAccessLevel.EVERYONE}
+ ${featureAccessLevel.NOT_ENABLED} | ${featureAccessLevel.PROJECT_MEMBERS}
+ ${featureAccessLevel.EVERYONE} | ${featureAccessLevel.PROJECT_MEMBERS}
+ ${featureAccessLevel.EVERYONE} | ${featureAccessLevel.NOT_ENABLED}
+ ${featureAccessLevel.PROJECT_MEMBERS} | ${featureAccessLevel.NOT_ENABLED}
`(
- 'should disable the metrics visibility dropdown when #scenario',
- ({ scenario, selectedOption, selectedOptionLabel }) => {
- wrapper = mountComponent(scenario, mount);
+ 'when updating Operations Settings access level from `$before` to `$after`, Metric Dashboard access is updated to `$after` as well',
+ async ({ before, after }) => {
+ wrapper = mountComponent({
+ currentSettings: { operationsAccessLevel: before, metricsDashboardAccessLevel: before },
+ });
- const select = findMetricsVisibilitySettings().find('select');
- const option = select.find('option');
+ await findOperationsVisibilityInput().vm.$emit('change', after);
- expect(select.attributes('disabled')).toBe('disabled');
- expect(select.element.value).toBe(selectedOption);
- expect(option.attributes('value')).toBe(selectedOption);
- expect(option.text()).toBe(selectedOptionLabel);
+ expect(findMetricsVisibilityInput().props('value')).toBe(after);
},
);
+
+ it('when updating Operations Settings access level from `10` to `20`, Metric Dashboard access is not increased', async () => {
+ wrapper = mountComponent({
+ currentSettings: {
+ operationsAccessLevel: featureAccessLevel.PROJECT_MEMBERS,
+ metricsDashboardAccessLevel: featureAccessLevel.PROJECT_MEMBERS,
+ },
+ });
+
+ await findOperationsVisibilityInput().vm.$emit('change', featureAccessLevel.EVERYONE);
+
+ expect(findMetricsVisibilityInput().props('value')).toBe(featureAccessLevel.PROJECT_MEMBERS);
+ });
+
+ it('should reduce Metrics visibility level when visibility is set to private', async () => {
+ wrapper = mountComponent({
+ currentSettings: {
+ visibilityLevel: visibilityOptions.PUBLIC,
+ operationsAccessLevel: featureAccessLevel.EVERYONE,
+ metricsDashboardAccessLevel: featureAccessLevel.EVERYONE,
+ },
+ });
+
+ await findProjectVisibilityLevelInput().setValue(visibilityOptions.PRIVATE);
+
+ expect(findMetricsVisibilityInput().props('value')).toBe(featureAccessLevel.PROJECT_MEMBERS);
+ });
});
describe('Analytics', () => {
diff --git a/spec/frontend/pages/shared/wikis/components/wiki_content_spec.js b/spec/frontend/pages/shared/wikis/components/wiki_content_spec.js
new file mode 100644
index 00000000000..365bb878485
--- /dev/null
+++ b/spec/frontend/pages/shared/wikis/components/wiki_content_spec.js
@@ -0,0 +1,97 @@
+import { GlSkeletonLoader, GlAlert } from '@gitlab/ui';
+import { nextTick } from 'vue';
+import { shallowMount } from '@vue/test-utils';
+import MockAdapter from 'axios-mock-adapter';
+import WikiContent from '~/pages/shared/wikis/components/wiki_content.vue';
+import { renderGFM } from '~/pages/shared/wikis/render_gfm_facade';
+import axios from '~/lib/utils/axios_utils';
+import httpStatus from '~/lib/utils/http_status';
+import waitForPromises from 'helpers/wait_for_promises';
+
+jest.mock('~/pages/shared/wikis/render_gfm_facade');
+
+describe('pages/shared/wikis/components/wiki_content', () => {
+ const PATH = '/test';
+ let wrapper;
+ let mock;
+
+ function buildWrapper(propsData = {}) {
+ wrapper = shallowMount(WikiContent, {
+ propsData: { getWikiContentUrl: PATH, ...propsData },
+ stubs: {
+ GlSkeletonLoader,
+ GlAlert,
+ },
+ });
+ }
+
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ const findGlAlert = () => wrapper.findComponent(GlAlert);
+ const findGlSkeletonLoader = () => wrapper.findComponent(GlSkeletonLoader);
+ const findContent = () => wrapper.find('[data-testid="wiki_page_content"]');
+
+ describe('when loading content', () => {
+ beforeEach(() => {
+ buildWrapper();
+ });
+
+ it('renders skeleton loader', () => {
+ expect(findGlSkeletonLoader().exists()).toBe(true);
+ });
+
+ it('does not render content container or error alert', () => {
+ expect(findGlAlert().exists()).toBe(false);
+ expect(findContent().exists()).toBe(false);
+ });
+ });
+
+ describe('when content loads successfully', () => {
+ const content = 'content';
+
+ beforeEach(() => {
+ mock.onGet(PATH, { params: { render_html: true } }).replyOnce(httpStatus.OK, { content });
+ buildWrapper();
+ return waitForPromises();
+ });
+
+ it('renders content container', () => {
+ expect(findContent().text()).toBe(content);
+ });
+
+ it('does not render skeleton loader or error alert', () => {
+ expect(findGlAlert().exists()).toBe(false);
+ expect(findGlSkeletonLoader().exists()).toBe(false);
+ });
+
+ it('calls renderGFM after nextTick', async () => {
+ await nextTick();
+
+ expect(renderGFM).toHaveBeenCalledWith(wrapper.element);
+ });
+ });
+
+ describe('when loading content fails', () => {
+ beforeEach(() => {
+ mock.onGet(PATH).replyOnce(httpStatus.INTERNAL_SERVER_ERROR, '');
+ buildWrapper();
+ return waitForPromises();
+ });
+
+ it('renders error alert', () => {
+ expect(findGlAlert().exists()).toBe(true);
+ });
+
+ it('does not render skeleton loader or content container', () => {
+ expect(findContent().exists()).toBe(false);
+ expect(findGlSkeletonLoader().exists()).toBe(false);
+ });
+ });
+});
diff --git a/spec/frontend/pages/shared/wikis/components/wiki_form_spec.js b/spec/frontend/pages/shared/wikis/components/wiki_form_spec.js
index e118a35804f..d7f8dc3c98e 100644
--- a/spec/frontend/pages/shared/wikis/components/wiki_form_spec.js
+++ b/spec/frontend/pages/shared/wikis/components/wiki_form_spec.js
@@ -1,5 +1,5 @@
import { nextTick } from 'vue';
-import { GlLoadingIcon, GlModal, GlAlert, GlButton } from '@gitlab/ui';
+import { GlAlert, GlButton } from '@gitlab/ui';
import { mount, shallowMount } from '@vue/test-utils';
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
@@ -32,11 +32,7 @@ describe('WikiForm', () => {
const findMessage = () => wrapper.find('#wiki_message');
const findSubmitButton = () => wrapper.findByTestId('wiki-submit-button');
const findCancelButton = () => wrapper.findByTestId('wiki-cancel-button');
- const findUseNewEditorButton = () => wrapper.findByText('Use the new editor');
const findToggleEditingModeButton = () => wrapper.findByTestId('toggle-editing-mode-button');
- const findDismissContentEditorAlertButton = () => wrapper.findByText('Try this later');
- const findSwitchToOldEditorButton = () =>
- wrapper.findByRole('button', { name: 'Switch me back to the classic editor.' });
const findTitleHelpLink = () => wrapper.findByText('Learn more.');
const findMarkdownHelpLink = () => wrapper.findByTestId('wiki-markdown-help-link');
const findContentEditor = () => wrapper.findComponent(ContentEditor);
@@ -293,27 +289,21 @@ describe('WikiForm', () => {
);
});
- describe('when wikiSwitchBetweenContentEditorRawMarkdown feature flag is not enabled', () => {
+ describe('toggle editing mode control', () => {
beforeEach(() => {
- createWrapper({
- glFeatures: { wikiSwitchBetweenContentEditorRawMarkdown: false },
- });
- });
-
- it('hides toggle editing mode button', () => {
- expect(findToggleEditingModeButton().exists()).toBe(false);
- });
- });
-
- describe('when wikiSwitchBetweenContentEditorRawMarkdown feature flag is enabled', () => {
- beforeEach(() => {
- createWrapper({
- glFeatures: { wikiSwitchBetweenContentEditorRawMarkdown: true },
- });
+ createWrapper();
});
- it('hides gl-alert containing "use new editor" button', () => {
- expect(findUseNewEditorButton().exists()).toBe(false);
+ it.each`
+ format | enabled | action
+ ${'markdown'} | ${true} | ${'displays'}
+ ${'rdoc'} | ${false} | ${'hides'}
+ ${'asciidoc'} | ${false} | ${'hides'}
+ ${'org'} | ${false} | ${'hides'}
+ `('$action toggle editing mode button when format is $format', async ({ format, enabled }) => {
+ await setFormat(format);
+
+ expect(findToggleEditingModeButton().exists()).toBe(enabled);
});
it('displays toggle editing mode button', () => {
@@ -326,8 +316,8 @@ describe('WikiForm', () => {
});
describe('when clicking the toggle editing mode button', () => {
- beforeEach(() => {
- findToggleEditingModeButton().vm.$emit('click');
+ beforeEach(async () => {
+ await findToggleEditingModeButton().trigger('click');
});
it('hides the classic editor', () => {
@@ -343,17 +333,13 @@ describe('WikiForm', () => {
describe('when content editor is active', () => {
let mockContentEditor;
- beforeEach(() => {
+ beforeEach(async () => {
mockContentEditor = {
getSerializedContent: jest.fn(),
setSerializedContent: jest.fn(),
};
- findToggleEditingModeButton().vm.$emit('click');
- });
-
- it('hides switch to old editor button', () => {
- expect(findSwitchToOldEditorButton().exists()).toBe(false);
+ await findToggleEditingModeButton().trigger('click');
});
it('displays "Edit source" label in the toggle editing mode button', () => {
@@ -363,13 +349,13 @@ describe('WikiForm', () => {
describe('when clicking the toggle editing mode button', () => {
const contentEditorFakeSerializedContent = 'fake content';
- beforeEach(() => {
+ beforeEach(async () => {
mockContentEditor.getSerializedContent.mockReturnValueOnce(
contentEditorFakeSerializedContent,
);
findContentEditor().vm.$emit('initialized', mockContentEditor);
- findToggleEditingModeButton().vm.$emit('click');
+ await findToggleEditingModeButton().trigger('click');
});
it('hides the content editor', () => {
@@ -388,75 +374,12 @@ describe('WikiForm', () => {
});
describe('wiki content editor', () => {
- it.each`
- format | buttonExists
- ${'markdown'} | ${true}
- ${'rdoc'} | ${false}
- `(
- 'gl-alert containing "use new editor" button exists: $buttonExists if format is $format',
- async ({ format, buttonExists }) => {
- createWrapper();
-
- await setFormat(format);
-
- expect(findUseNewEditorButton().exists()).toBe(buttonExists);
- },
- );
-
- it('gl-alert containing "use new editor" button is dismissed on clicking dismiss button', async () => {
- createWrapper();
-
- await findDismissContentEditorAlertButton().trigger('click');
-
- expect(findUseNewEditorButton().exists()).toBe(false);
- });
-
- const assertOldEditorIsVisible = () => {
- expect(findContentEditor().exists()).toBe(false);
- expect(findClassicEditor().exists()).toBe(true);
- expect(findSubmitButton().props('disabled')).toBe(false);
-
- expect(wrapper.text()).not.toContain(
- "Switching will discard any changes you've made in the new editor.",
- );
- expect(wrapper.text()).not.toContain(
- "This editor is in beta and may not display the page's contents properly.",
- );
- };
-
- it('shows classic editor by default', () => {
- createWrapper({ persisted: true });
-
- assertOldEditorIsVisible();
- });
-
- describe('switch format to rdoc', () => {
- beforeEach(async () => {
- createWrapper({ persisted: true });
-
- await setFormat('rdoc');
- });
-
- it('continues to show the classic editor', assertOldEditorIsVisible);
-
- describe('switch format back to markdown', () => {
- beforeEach(async () => {
- await setFormat('markdown');
- });
-
- it(
- 'still shows the classic editor and does not automatically switch to the content editor ',
- assertOldEditorIsVisible,
- );
- });
- });
-
describe('clicking "use new editor": editor fails to load', () => {
beforeEach(async () => {
createWrapper({ mountFn: mount });
mock.onPost(/preview-markdown/).reply(400);
- await findUseNewEditorButton().trigger('click');
+ await findToggleEditingModeButton().trigger('click');
// try waiting for content editor to load (but it will never actually load)
await waitForPromises();
@@ -466,14 +389,14 @@ describe('WikiForm', () => {
expect(findSubmitButton().props('disabled')).toBe(true);
});
- describe('clicking "switch to classic editor"', () => {
+ describe('toggling editing modes to the classic editor', () => {
beforeEach(() => {
- return findSwitchToOldEditorButton().trigger('click');
+ return findToggleEditingModeButton().trigger('click');
});
- it('switches to classic editor directly without showing a modal', () => {
- expect(wrapper.findComponent(ContentEditor).exists()).toBe(false);
- expect(wrapper.findComponent(MarkdownField).exists()).toBe(true);
+ it('switches to classic editor', () => {
+ expect(findContentEditor().exists()).toBe(false);
+ expect(findClassicEditor().exists()).toBe(true);
});
});
});
@@ -484,31 +407,15 @@ describe('WikiForm', () => {
mock.onPost(/preview-markdown/).reply(200, { body: '<p>hello <strong>world</strong></p>' });
- await findUseNewEditorButton().trigger('click');
- });
-
- it('shows a tip to send feedback', () => {
- expect(wrapper.text()).toContain('Tell us your experiences with the new Markdown editor');
- });
-
- it('shows warnings that the rich text editor is in beta and may not work properly', () => {
- expect(wrapper.text()).toContain(
- "This editor is in beta and may not display the page's contents properly.",
- );
+ await findToggleEditingModeButton().trigger('click');
+ await waitForPromises();
});
it('shows the rich text editor when loading finishes', async () => {
- // wait for content editor to load
- await waitForPromises();
-
- expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(false);
- expect(wrapper.findComponent(ContentEditor).exists()).toBe(true);
+ expect(findContentEditor().exists()).toBe(true);
});
it('sends tracking event when editor loads', async () => {
- // wait for content editor to load
- await waitForPromises();
-
expect(trackingSpy).toHaveBeenCalledWith(undefined, CONTENT_EDITOR_LOADED_ACTION, {
label: WIKI_CONTENT_EDITOR_TRACKING_LABEL,
});
@@ -564,49 +471,6 @@ describe('WikiForm', () => {
expect(findContent().element.value).toBe('hello **world**');
});
});
-
- describe('clicking "switch to classic editor"', () => {
- let modal;
-
- beforeEach(async () => {
- modal = wrapper.findComponent(GlModal);
- jest.spyOn(modal.vm, 'show');
-
- findSwitchToOldEditorButton().trigger('click');
- });
-
- it('shows a modal confirming the change', () => {
- expect(modal.vm.show).toHaveBeenCalled();
- });
-
- describe('confirming "switch to classic editor" in the modal', () => {
- beforeEach(async () => {
- wrapper.vm.contentEditor.tiptapEditor.commands.setContent(
- '<p>hello __world__ from content editor</p>',
- true,
- );
-
- wrapper.findComponent(GlModal).vm.$emit('primary');
-
- await nextTick();
- });
-
- it('switches to classic editor', () => {
- expect(wrapper.findComponent(ContentEditor).exists()).toBe(false);
- expect(wrapper.findComponent(MarkdownField).exists()).toBe(true);
- });
-
- it('does not show a warning about content editor', () => {
- expect(wrapper.text()).not.toContain(
- "This editor is in beta and may not display the page's contents properly.",
- );
- });
-
- it('the classic editor retains its old value and does not use the content from the content editor', () => {
- expect(findContent().element.value).toBe(' My page content ');
- });
- });
- });
});
});
});
diff --git a/spec/frontend/pdf/page_spec.js b/spec/frontend/pdf/page_spec.js
index 4e0a6f78b63..07a7f1bb2ff 100644
--- a/spec/frontend/pdf/page_spec.js
+++ b/spec/frontend/pdf/page_spec.js
@@ -1,4 +1,4 @@
-import Vue from 'vue';
+import Vue, { nextTick } from 'vue';
import mountComponent from 'helpers/vue_mount_component_helper';
import PageComponent from '~/pdf/page/index.vue';
@@ -14,8 +14,7 @@ describe('Page component', () => {
vm.$destroy();
});
- it('renders the page when mounting', (done) => {
- const promise = Promise.resolve();
+ it('renders the page when mounting', async () => {
const testPage = {
render: jest.fn().mockReturnValue({ promise: Promise.resolve() }),
getViewport: jest.fn().mockReturnValue({}),
@@ -28,12 +27,9 @@ describe('Page component', () => {
expect(vm.rendering).toBe(true);
- promise
- .then(() => {
- expect(testPage.render).toHaveBeenCalledWith(vm.renderContext);
- expect(vm.rendering).toBe(false);
- })
- .then(done)
- .catch(done.fail);
+ await nextTick();
+
+ expect(testPage.render).toHaveBeenCalledWith(vm.renderContext);
+ expect(vm.rendering).toBe(false);
});
});
diff --git a/spec/frontend/pipeline_editor/components/drawer/pipeline_editor_drawer_spec.js b/spec/frontend/pipeline_editor/components/drawer/pipeline_editor_drawer_spec.js
index ba06f113120..33b53bf6a56 100644
--- a/spec/frontend/pipeline_editor/components/drawer/pipeline_editor_drawer_spec.js
+++ b/spec/frontend/pipeline_editor/components/drawer/pipeline_editor_drawer_spec.js
@@ -1,146 +1,27 @@
-import { GlButton } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import { nextTick } from 'vue';
-import { useLocalStorageSpy } from 'helpers/local_storage_helper';
-import FirstPipelineCard from '~/pipeline_editor/components/drawer/cards/first_pipeline_card.vue';
-import GettingStartedCard from '~/pipeline_editor/components/drawer/cards/getting_started_card.vue';
-import PipelineConfigReferenceCard from '~/pipeline_editor/components/drawer/cards/pipeline_config_reference_card.vue';
-import VisualizeAndLintCard from '~/pipeline_editor/components/drawer/cards/visualize_and_lint_card.vue';
+import { GlDrawer } from '@gitlab/ui';
import PipelineEditorDrawer from '~/pipeline_editor/components/drawer/pipeline_editor_drawer.vue';
-import { DRAWER_EXPANDED_KEY } from '~/pipeline_editor/constants';
-import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
describe('Pipeline editor drawer', () => {
- useLocalStorageSpy();
-
let wrapper;
+ const findDrawer = () => wrapper.findComponent(GlDrawer);
+
const createComponent = () => {
- wrapper = shallowMount(PipelineEditorDrawer, {
- stubs: { LocalStorageSync },
- });
+ wrapper = shallowMount(PipelineEditorDrawer);
};
- const findFirstPipelineCard = () => wrapper.findComponent(FirstPipelineCard);
- const findGettingStartedCard = () => wrapper.findComponent(GettingStartedCard);
- const findPipelineConfigReferenceCard = () => wrapper.findComponent(PipelineConfigReferenceCard);
- const findToggleBtn = () => wrapper.findComponent(GlButton);
- const findVisualizeAndLintCard = () => wrapper.findComponent(VisualizeAndLintCard);
-
- const findArrowIcon = () => wrapper.find('[data-testid="toggle-icon"]');
- const findCollapseText = () => wrapper.find('[data-testid="collapse-text"]');
- const findDrawerContent = () => wrapper.find('[data-testid="drawer-content"]');
-
- const clickToggleBtn = async () => findToggleBtn().vm.$emit('click');
-
- const originalObjects = [];
-
- beforeEach(() => {
- originalObjects.push(window.gon, window.gl);
- });
-
afterEach(() => {
wrapper.destroy();
- localStorage.clear();
- [window.gon, window.gl] = originalObjects;
- });
-
- describe('default expanded state', () => {
- it('sets the drawer to be closed by default', async () => {
- createComponent();
- expect(findDrawerContent().exists()).toBe(false);
- });
- });
-
- describe('when the drawer is collapsed', () => {
- beforeEach(async () => {
- createComponent();
- });
-
- it('shows the left facing arrow icon', () => {
- expect(findArrowIcon().props('name')).toBe('chevron-double-lg-left');
- });
-
- it('does not show the collapse text', () => {
- expect(findCollapseText().exists()).toBe(false);
- });
-
- it('does not show the drawer content', () => {
- expect(findDrawerContent().exists()).toBe(false);
- });
-
- it('can open the drawer by clicking on the toggle button', async () => {
- expect(findDrawerContent().exists()).toBe(false);
-
- await clickToggleBtn();
-
- expect(findDrawerContent().exists()).toBe(true);
- });
- });
-
- describe('when the drawer is expanded', () => {
- beforeEach(async () => {
- createComponent();
- await clickToggleBtn();
- });
-
- it('shows the right facing arrow icon', () => {
- expect(findArrowIcon().props('name')).toBe('chevron-double-lg-right');
- });
-
- it('shows the collapse text', () => {
- expect(findCollapseText().exists()).toBe(true);
- });
-
- it('shows the drawer content', () => {
- expect(findDrawerContent().exists()).toBe(true);
- });
-
- it('shows all the introduction cards', () => {
- expect(findFirstPipelineCard().exists()).toBe(true);
- expect(findGettingStartedCard().exists()).toBe(true);
- expect(findPipelineConfigReferenceCard().exists()).toBe(true);
- expect(findVisualizeAndLintCard().exists()).toBe(true);
- });
-
- it('can close the drawer by clicking on the toggle button', async () => {
- expect(findDrawerContent().exists()).toBe(true);
-
- await clickToggleBtn();
-
- expect(findDrawerContent().exists()).toBe(false);
- });
});
- describe('local storage', () => {
- it('saves the drawer expanded value to local storage', async () => {
- localStorage.setItem(DRAWER_EXPANDED_KEY, 'false');
-
- createComponent();
- await clickToggleBtn();
-
- expect(localStorage.setItem.mock.calls).toEqual([
- [DRAWER_EXPANDED_KEY, 'false'],
- [DRAWER_EXPANDED_KEY, 'true'],
- ]);
- });
-
- it('loads the drawer collapsed when local storage is set to `false`, ', async () => {
- localStorage.setItem(DRAWER_EXPANDED_KEY, false);
- createComponent();
-
- await nextTick();
-
- expect(findDrawerContent().exists()).toBe(false);
- });
+ it('emits close event when closing the drawer', () => {
+ createComponent();
- it('loads the drawer expanded when local storage is set to `true`, ', async () => {
- localStorage.setItem(DRAWER_EXPANDED_KEY, true);
- createComponent();
+ expect(wrapper.emitted('close-drawer')).toBeUndefined();
- await nextTick();
+ findDrawer().vm.$emit('close');
- expect(findDrawerContent().exists()).toBe(true);
- });
+ expect(wrapper.emitted('close-drawer')).toHaveLength(1);
});
});
diff --git a/spec/frontend/pipeline_editor/components/editor/ci_editor_header_spec.js b/spec/frontend/pipeline_editor/components/editor/ci_editor_header_spec.js
index 3ee53d4a055..8f50325295e 100644
--- a/spec/frontend/pipeline_editor/components/editor/ci_editor_header_spec.js
+++ b/spec/frontend/pipeline_editor/components/editor/ci_editor_header_spec.js
@@ -1,5 +1,5 @@
-import { GlButton } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
+import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
import CiEditorHeader from '~/pipeline_editor/components/editor/ci_editor_header.vue';
import {
@@ -11,11 +11,18 @@ describe('CI Editor Header', () => {
let wrapper;
let trackingSpy = null;
- const createComponent = () => {
- wrapper = shallowMount(CiEditorHeader, {});
+ const createComponent = ({ showDrawer = false } = {}) => {
+ wrapper = extendedWrapper(
+ shallowMount(CiEditorHeader, {
+ propsData: {
+ showDrawer,
+ },
+ }),
+ );
};
- const findLinkBtn = () => wrapper.findComponent(GlButton);
+ const findLinkBtn = () => wrapper.findByTestId('template-repo-link');
+ const findHelpBtn = () => wrapper.findByTestId('drawer-toggle');
afterEach(() => {
wrapper.destroy();
@@ -50,4 +57,42 @@ describe('CI Editor Header', () => {
});
});
});
+
+ describe('help button', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('finds the help button', () => {
+ expect(findHelpBtn().exists()).toBe(true);
+ });
+
+ it('has the information-o icon', () => {
+ expect(findHelpBtn().props('icon')).toBe('information-o');
+ });
+
+ describe('when pipeline editor drawer is closed', () => {
+ it('emits open drawer event when clicked', () => {
+ createComponent({ showDrawer: false });
+
+ expect(wrapper.emitted('open-drawer')).toBeUndefined();
+
+ findHelpBtn().vm.$emit('click');
+
+ expect(wrapper.emitted('open-drawer')).toHaveLength(1);
+ });
+ });
+
+ describe('when pipeline editor drawer is open', () => {
+ it('emits close drawer event when clicked', () => {
+ createComponent({ showDrawer: true });
+
+ expect(wrapper.emitted('close-drawer')).toBeUndefined();
+
+ findHelpBtn().vm.$emit('click');
+
+ expect(wrapper.emitted('close-drawer')).toHaveLength(1);
+ });
+ });
+ });
});
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 fee52db9b64..6dffb7e5470 100644
--- a/spec/frontend/pipeline_editor/components/pipeline_editor_tabs_spec.js
+++ b/spec/frontend/pipeline_editor/components/pipeline_editor_tabs_spec.js
@@ -40,6 +40,7 @@ describe('Pipeline editor tabs component', () => {
ciConfigData: mockLintResponse,
ciFileContent: mockCiYml,
isNewCiConfigFile: true,
+ showDrawer: false,
...props,
},
data() {
diff --git a/spec/frontend/pipeline_editor/pipeline_editor_home_spec.js b/spec/frontend/pipeline_editor/pipeline_editor_home_spec.js
index 6f969546171..98e2c17967c 100644
--- a/spec/frontend/pipeline_editor/pipeline_editor_home_spec.js
+++ b/spec/frontend/pipeline_editor/pipeline_editor_home_spec.js
@@ -1,6 +1,8 @@
import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
-import { GlModal } from '@gitlab/ui';
+import { GlButton, GlDrawer, GlModal } from '@gitlab/ui';
+import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import CiEditorHeader from '~/pipeline_editor/components/editor/ci_editor_header.vue';
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';
@@ -18,24 +20,26 @@ describe('Pipeline editor home wrapper', () => {
let wrapper;
const createComponent = ({ props = {}, glFeatures = {}, data = {}, stubs = {} } = {}) => {
- wrapper = shallowMount(PipelineEditorHome, {
- data: () => data,
- propsData: {
- ciConfigData: mockLintResponse,
- ciFileContent: mockCiYml,
- isCiConfigDataLoading: false,
- isNewCiConfigFile: false,
- ...props,
- },
- provide: {
- projectFullPath: '',
- totalBranches: 19,
- glFeatures: {
- ...glFeatures,
+ wrapper = extendedWrapper(
+ shallowMount(PipelineEditorHome, {
+ data: () => data,
+ propsData: {
+ ciConfigData: mockLintResponse,
+ ciFileContent: mockCiYml,
+ isCiConfigDataLoading: false,
+ isNewCiConfigFile: false,
+ ...props,
},
- },
- stubs,
- });
+ provide: {
+ projectFullPath: '',
+ totalBranches: 19,
+ glFeatures: {
+ ...glFeatures,
+ },
+ },
+ stubs,
+ }),
+ );
};
const findBranchSwitcher = () => wrapper.findComponent(BranchSwitcher);
@@ -45,6 +49,7 @@ describe('Pipeline editor home wrapper', () => {
const findPipelineEditorDrawer = () => wrapper.findComponent(PipelineEditorDrawer);
const findPipelineEditorHeader = () => wrapper.findComponent(PipelineEditorHeader);
const findPipelineEditorTabs = () => wrapper.findComponent(PipelineEditorTabs);
+ const findHelpBtn = () => wrapper.findByTestId('drawer-toggle');
afterEach(() => {
wrapper.destroy();
@@ -70,10 +75,6 @@ describe('Pipeline editor home wrapper', () => {
it('shows the commit section by default', () => {
expect(findCommitSection().exists()).toBe(true);
});
-
- it('show the pipeline drawer', () => {
- expect(findPipelineEditorDrawer().exists()).toBe(true);
- });
});
describe('modal when switching branch', () => {
@@ -175,4 +176,58 @@ describe('Pipeline editor home wrapper', () => {
});
});
});
+
+ describe('help drawer', () => {
+ const clickHelpBtn = async () => {
+ findHelpBtn().vm.$emit('click');
+ await nextTick();
+ };
+
+ it('hides the drawer by default', () => {
+ createComponent();
+
+ expect(findPipelineEditorDrawer().props('isVisible')).toBe(false);
+ });
+
+ it('toggles the drawer on button click', async () => {
+ createComponent({
+ stubs: {
+ CiEditorHeader,
+ GlButton,
+ GlDrawer,
+ PipelineEditorTabs,
+ PipelineEditorDrawer,
+ },
+ });
+
+ await clickHelpBtn();
+
+ expect(findPipelineEditorDrawer().props('isVisible')).toBe(true);
+
+ await clickHelpBtn();
+
+ expect(findPipelineEditorDrawer().props('isVisible')).toBe(false);
+ });
+
+ it("closes the drawer through the drawer's close button", async () => {
+ createComponent({
+ stubs: {
+ CiEditorHeader,
+ GlButton,
+ GlDrawer,
+ PipelineEditorTabs,
+ PipelineEditorDrawer,
+ },
+ });
+
+ await clickHelpBtn();
+
+ expect(findPipelineEditorDrawer().props('isVisible')).toBe(true);
+
+ findPipelineEditorDrawer().find(GlDrawer).vm.$emit('close');
+ await nextTick();
+
+ expect(findPipelineEditorDrawer().props('isVisible')).toBe(false);
+ });
+ });
});
diff --git a/spec/frontend/pipeline_wizard/components/wrapper_spec.js b/spec/frontend/pipeline_wizard/components/wrapper_spec.js
index bd1679baf48..357a9d21723 100644
--- a/spec/frontend/pipeline_wizard/components/wrapper_spec.js
+++ b/spec/frontend/pipeline_wizard/components/wrapper_spec.js
@@ -1,21 +1,26 @@
import { Document, parseDocument } from 'yaml';
import { GlProgressBar } from '@gitlab/ui';
import { nextTick } from 'vue';
-import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { shallowMountExtended, mountExtended } from 'helpers/vue_test_utils_helper';
import PipelineWizardWrapper, { i18n } from '~/pipeline_wizard/components/wrapper.vue';
import WizardStep from '~/pipeline_wizard/components/step.vue';
import CommitStep from '~/pipeline_wizard/components/commit.vue';
import YamlEditor from '~/pipeline_wizard/components/editor.vue';
import { sprintf } from '~/locale';
-import { steps as stepsYaml } from '../mock/yaml';
+import {
+ steps as stepsYaml,
+ compiledScenario1,
+ compiledScenario2,
+ compiledScenario3,
+} from '../mock/yaml';
describe('Pipeline Wizard - wrapper.vue', () => {
let wrapper;
const steps = parseDocument(stepsYaml).toJS();
const getAsYamlNode = (value) => new Document(value).contents;
- const createComponent = (props = {}) => {
- wrapper = shallowMountExtended(PipelineWizardWrapper, {
+ const createComponent = (props = {}, mountFn = shallowMountExtended) => {
+ wrapper = mountFn(PipelineWizardWrapper, {
propsData: {
projectPath: '/user/repo',
defaultBranch: 'main',
@@ -23,13 +28,21 @@ describe('Pipeline Wizard - wrapper.vue', () => {
steps: getAsYamlNode(steps),
...props,
},
+ stubs: {
+ CommitStep: true,
+ },
});
};
const getEditorContent = () => {
- return wrapper.getComponent(YamlEditor).attributes().doc.toString();
+ return wrapper.getComponent(YamlEditor).props().doc.toString();
};
- const getStepWrapper = () => wrapper.getComponent(WizardStep);
+ const getStepWrapper = () =>
+ wrapper.findAllComponents(WizardStep).wrappers.find((w) => w.isVisible());
const getGlProgressBarWrapper = () => wrapper.getComponent(GlProgressBar);
+ const findFirstVisibleStep = () =>
+ wrapper.findAllComponents('[data-testid="step"]').wrappers.find((w) => w.isVisible());
+ const findFirstInputFieldForTarget = (target) =>
+ wrapper.find(`[data-input-target="${target}"]`).find('input');
describe('display', () => {
afterEach(() => {
@@ -118,8 +131,9 @@ describe('Pipeline Wizard - wrapper.vue', () => {
}) => {
beforeAll(async () => {
createComponent();
+
for (const emittedValue of navigationEventChain) {
- wrapper.findComponent({ ref: 'step' }).vm.$emit(emittedValue);
+ findFirstVisibleStep().vm.$emit(emittedValue);
// We have to wait for the next step to be mounted
// before we can emit the next event, so we have to await
// inside the loop.
@@ -134,11 +148,11 @@ describe('Pipeline Wizard - wrapper.vue', () => {
if (expectCommitStepShown) {
it('does not show the step wrapper', async () => {
- expect(wrapper.findComponent(WizardStep).exists()).toBe(false);
+ expect(wrapper.findComponent(WizardStep).isVisible()).toBe(false);
});
it('shows the commit step page', () => {
- expect(wrapper.findComponent(CommitStep).exists()).toBe(true);
+ expect(wrapper.findComponent(CommitStep).isVisible()).toBe(true);
});
} else {
it('passes the correct step config to the step component', async () => {
@@ -146,7 +160,7 @@ describe('Pipeline Wizard - wrapper.vue', () => {
});
it('does not show the commit step page', () => {
- expect(wrapper.findComponent(CommitStep).exists()).toBe(false);
+ expect(wrapper.findComponent(CommitStep).isVisible()).toBe(false);
});
}
@@ -247,4 +261,54 @@ describe('Pipeline Wizard - wrapper.vue', () => {
expect(wrapper.getComponent(YamlEditor).props('highlight')).toBe(null);
});
});
+
+ describe('integration test', () => {
+ beforeAll(async () => {
+ createComponent({}, mountExtended);
+ });
+
+ it('updates the editor content after input on step 1', async () => {
+ findFirstInputFieldForTarget('$FOO').setValue('fooVal');
+ await nextTick();
+
+ expect(getEditorContent()).toBe(compiledScenario1);
+ });
+
+ it('updates the editor content after input on step 2', async () => {
+ findFirstVisibleStep().vm.$emit('next');
+ await nextTick();
+
+ findFirstInputFieldForTarget('$BAR').setValue('barVal');
+ await nextTick();
+
+ expect(getEditorContent()).toBe(compiledScenario2);
+ });
+
+ describe('navigating back', () => {
+ let inputField;
+
+ beforeAll(async () => {
+ findFirstVisibleStep().vm.$emit('back');
+ await nextTick();
+
+ inputField = findFirstInputFieldForTarget('$FOO');
+ });
+
+ afterAll(() => {
+ wrapper.destroy();
+ inputField = undefined;
+ });
+
+ it('still shows the input values from the former visit', () => {
+ expect(inputField.element.value).toBe('fooVal');
+ });
+
+ it('updates the editor content without modifying input that came from a later step', async () => {
+ inputField.setValue('newFooVal');
+ await nextTick();
+
+ expect(getEditorContent()).toBe(compiledScenario3);
+ });
+ });
+ });
});
diff --git a/spec/frontend/pipeline_wizard/mock/yaml.js b/spec/frontend/pipeline_wizard/mock/yaml.js
index 5eaeaa32a8c..e7087b59ce7 100644
--- a/spec/frontend/pipeline_wizard/mock/yaml.js
+++ b/spec/frontend/pipeline_wizard/mock/yaml.js
@@ -59,6 +59,17 @@ export const steps = `
bar: $BAR
`;
+export const compiledScenario1 = `foo: fooVal
+`;
+
+export const compiledScenario2 = `foo: fooVal
+bar: barVal
+`;
+
+export const compiledScenario3 = `foo: newFooVal
+bar: barVal
+`;
+
export const fullTemplate = `
title: some title
description: some description
diff --git a/spec/frontend/pipelines/components/pipeline_tabs_spec.js b/spec/frontend/pipelines/components/pipeline_tabs_spec.js
new file mode 100644
index 00000000000..e18c3edbad9
--- /dev/null
+++ b/spec/frontend/pipelines/components/pipeline_tabs_spec.js
@@ -0,0 +1,61 @@
+import { shallowMount } from '@vue/test-utils';
+import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import PipelineTabs from '~/pipelines/components/pipeline_tabs.vue';
+import PipelineGraphWrapper from '~/pipelines/components/graph/graph_component_wrapper.vue';
+import Dag from '~/pipelines/components/dag/dag.vue';
+import JobsApp from '~/pipelines/components/jobs/jobs_app.vue';
+import TestReports from '~/pipelines/components/test_reports/test_reports.vue';
+
+describe('The Pipeline Tabs', () => {
+ let wrapper;
+
+ const findDagTab = () => wrapper.findByTestId('dag-tab');
+ const findFailedJobsTab = () => wrapper.findByTestId('failed-jobs-tab');
+ const findJobsTab = () => wrapper.findByTestId('jobs-tab');
+ const findPipelineTab = () => wrapper.findByTestId('pipeline-tab');
+ const findTestsTab = () => wrapper.findByTestId('tests-tab');
+
+ const findDagApp = () => wrapper.findComponent(Dag);
+ const findFailedJobsApp = () => wrapper.findComponent(JobsApp);
+ const findJobsApp = () => wrapper.findComponent(JobsApp);
+ const findPipelineApp = () => wrapper.findComponent(PipelineGraphWrapper);
+ const findTestsApp = () => wrapper.findComponent(TestReports);
+
+ const createComponent = (propsData = {}) => {
+ wrapper = extendedWrapper(
+ shallowMount(PipelineTabs, {
+ propsData,
+ stubs: {
+ Dag: { template: '<div id="dag"/>' },
+ JobsApp: { template: '<div class="jobs" />' },
+ PipelineGraph: { template: '<div id="graph" />' },
+ TestReports: { template: '<div id="tests" />' },
+ },
+ }),
+ );
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ // The failed jobs MUST be removed from here and tested individually once
+ // the logic for the tab is implemented.
+ describe('Tabs', () => {
+ it.each`
+ tabName | tabComponent | appComponent
+ ${'Pipeline'} | ${findPipelineTab} | ${findPipelineApp}
+ ${'Dag'} | ${findDagTab} | ${findDagApp}
+ ${'Jobs'} | ${findJobsTab} | ${findJobsApp}
+ ${'Failed Jobs'} | ${findFailedJobsTab} | ${findFailedJobsApp}
+ ${'Tests'} | ${findTestsTab} | ${findTestsApp}
+ `('shows $tabName tab and its associated component', ({ appComponent, tabComponent }) => {
+ expect(tabComponent().exists()).toBe(true);
+ expect(appComponent().exists()).toBe(true);
+ });
+ });
+});
diff --git a/spec/frontend/pipelines/components/pipelines_filtered_search_spec.js b/spec/frontend/pipelines/components/pipelines_filtered_search_spec.js
index 0822b293f75..6c743f92116 100644
--- a/spec/frontend/pipelines/components/pipelines_filtered_search_spec.js
+++ b/spec/frontend/pipelines/components/pipelines_filtered_search_spec.js
@@ -173,7 +173,7 @@ describe('Pipelines filtered search', () => {
{ type: 'filtered-search-term', value: { data: '' } },
];
- expect(findFilteredSearch().props('value')).toEqual(expectedValueProp);
+ expect(findFilteredSearch().props('value')).toMatchObject(expectedValueProp);
expect(findFilteredSearch().props('value')).toHaveLength(expectedValueProp.length);
});
});
diff --git a/spec/frontend/pipelines/empty_state/ci_templates_spec.js b/spec/frontend/pipelines/empty_state/ci_templates_spec.js
new file mode 100644
index 00000000000..606fdc9cac1
--- /dev/null
+++ b/spec/frontend/pipelines/empty_state/ci_templates_spec.js
@@ -0,0 +1,85 @@
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
+import CiTemplates from '~/pipelines/components/pipelines_list/empty_state/ci_templates.vue';
+
+const pipelineEditorPath = '/-/ci/editor';
+const suggestedCiTemplates = [
+ { name: 'Android', logo: '/assets/illustrations/logos/android.svg' },
+ { name: 'Bash', logo: '/assets/illustrations/logos/bash.svg' },
+ { name: 'C++', logo: '/assets/illustrations/logos/c_plus_plus.svg' },
+];
+
+describe('CI Templates', () => {
+ let wrapper;
+ let trackingSpy;
+
+ const createWrapper = () => {
+ return shallowMountExtended(CiTemplates, {
+ provide: {
+ pipelineEditorPath,
+ suggestedCiTemplates,
+ },
+ });
+ };
+
+ const findTemplateDescription = () => wrapper.findByTestId('template-description');
+ const findTemplateLink = () => wrapper.findByTestId('template-link');
+ 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();
+
+ expect(content).toContain('Android', 'Bash', 'C++');
+ });
+
+ it('has the correct template name', () => {
+ expect(findTemplateName().text()).toBe('Android');
+ });
+
+ it('links to the correct template', () => {
+ expect(findTemplateLink().attributes('href')).toBe(
+ pipelineEditorPath.concat('?template=Android'),
+ );
+ });
+
+ it('has the description of the template', () => {
+ expect(findTemplateDescription().text()).toBe(
+ 'CI/CD template to test and deploy your Android project.',
+ );
+ });
+
+ it('has the right logo of the template', () => {
+ expect(findTemplateLogo().attributes('src')).toBe('/assets/illustrations/logos/android.svg');
+ });
+ });
+
+ describe('tracking', () => {
+ beforeEach(() => {
+ trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
+ });
+
+ afterEach(() => {
+ unmockTracking();
+ });
+
+ it('sends an event when template is clicked', () => {
+ findTemplateLink().vm.$emit('click');
+
+ expect(trackingSpy).toHaveBeenCalledTimes(1);
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, 'template_clicked', {
+ label: 'Android',
+ });
+ });
+ });
+});
diff --git a/spec/frontend/pipelines/pipelines_ci_templates_spec.js b/spec/frontend/pipelines/empty_state/pipelines_ci_templates_spec.js
index 7064f7448ec..14860f20317 100644
--- a/spec/frontend/pipelines/pipelines_ci_templates_spec.js
+++ b/spec/frontend/pipelines/empty_state/pipelines_ci_templates_spec.js
@@ -1,12 +1,12 @@
import '~/commons';
import { GlButton, GlSprintf } from '@gitlab/ui';
-import { sprintf } from '~/locale';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import { mockTracking } from 'helpers/tracking_helper';
+import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
import { stubExperiments } from 'helpers/experimentation_helper';
import GitlabExperiment from '~/experimentation/components/gitlab_experiment.vue';
import ExperimentTracking from '~/experimentation/experiment_tracking';
-import PipelinesCiTemplate from '~/pipelines/components/pipelines_list/pipelines_ci_templates.vue';
+import PipelinesCiTemplates from '~/pipelines/components/pipelines_list/empty_state/pipelines_ci_templates.vue';
+import CiTemplates from '~/pipelines/components/pipelines_list/empty_state/ci_templates.vue';
import {
RUNNERS_AVAILABILITY_SECTION_EXPERIMENT_NAME,
RUNNERS_SETTINGS_LINK_CLICKED_EVENT,
@@ -16,11 +16,6 @@ import {
} from '~/pipeline_editor/constants';
const pipelineEditorPath = '/-/ci/editor';
-const suggestedCiTemplates = [
- { name: 'Android', logo: '/assets/illustrations/logos/android.svg' },
- { name: 'Bash', logo: '/assets/illustrations/logos/bash.svg' },
- { name: 'C++', logo: '/assets/illustrations/logos/c_plus_plus.svg' },
-];
jest.mock('~/experimentation/experiment_tracking');
@@ -29,21 +24,17 @@ describe('Pipelines CI Templates', () => {
let trackingSpy;
const createWrapper = (propsData = {}, stubs = {}) => {
- return shallowMountExtended(PipelinesCiTemplate, {
+ return shallowMountExtended(PipelinesCiTemplates, {
provide: {
pipelineEditorPath,
- suggestedCiTemplates,
},
propsData,
stubs,
});
};
- const findTestTemplateLinks = () => wrapper.findAll('[data-testid="test-template-link"]');
- const findTemplateDescriptions = () => wrapper.findAll('[data-testid="template-description"]');
- const findTemplateLinks = () => wrapper.findAll('[data-testid="template-link"]');
- const findTemplateNames = () => wrapper.findAll('[data-testid="template-name"]');
- const findTemplateLogos = () => wrapper.findAll('[data-testid="template-logo"]');
+ const findTestTemplateLink = () => wrapper.findByTestId('test-template-link');
+ const findCiTemplates = () => wrapper.findComponent(CiTemplates);
const findSettingsLink = () => wrapper.findByTestId('settings-link');
const findDocumentationLink = () => wrapper.findByTestId('documentation-link');
const findSettingsButton = () => wrapper.findByTestId('settings-button');
@@ -59,63 +50,24 @@ describe('Pipelines CI Templates', () => {
});
it('links to the getting started template', () => {
- expect(findTestTemplateLinks().at(0).attributes('href')).toBe(
+ expect(findTestTemplateLink().attributes('href')).toBe(
pipelineEditorPath.concat('?template=Getting-Started'),
);
});
});
- describe('renders template list', () => {
- beforeEach(() => {
- wrapper = createWrapper();
- });
-
- it('renders all suggested templates', () => {
- const content = wrapper.text();
-
- expect(content).toContain('Android', 'Bash', 'C++');
- });
-
- it('has the correct template name', () => {
- expect(findTemplateNames().at(0).text()).toBe('Android');
- });
-
- it('links to the correct template', () => {
- expect(findTemplateLinks().at(0).attributes('href')).toBe(
- pipelineEditorPath.concat('?template=Android'),
- );
- });
-
- it('has the description of the template', () => {
- expect(findTemplateDescriptions().at(0).text()).toBe(
- sprintf(I18N.templates.description, { name: 'Android' }),
- );
- });
-
- it('has the right logo of the template', () => {
- expect(findTemplateLogos().at(0).attributes('src')).toBe(
- '/assets/illustrations/logos/android.svg',
- );
- });
- });
-
describe('tracking', () => {
beforeEach(() => {
wrapper = createWrapper();
trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
});
- it('sends an event when template is clicked', () => {
- findTemplateLinks().at(0).vm.$emit('click');
-
- expect(trackingSpy).toHaveBeenCalledTimes(1);
- expect(trackingSpy).toHaveBeenCalledWith(undefined, 'template_clicked', {
- label: 'Android',
- });
+ afterEach(() => {
+ unmockTracking();
});
it('sends an event when Getting-Started template is clicked', () => {
- findTestTemplateLinks().at(0).vm.$emit('click');
+ findTestTemplateLink().vm.$emit('click');
expect(trackingSpy).toHaveBeenCalledTimes(1);
expect(trackingSpy).toHaveBeenCalledWith(undefined, 'template_clicked', {
@@ -198,8 +150,8 @@ describe('Pipelines CI Templates', () => {
});
it(`renders the templates: ${templatesRendered}`, () => {
- expect(findTestTemplateLinks().exists()).toBe(templatesRendered);
- expect(findTemplateLinks().exists()).toBe(templatesRendered);
+ expect(findTestTemplateLink().exists()).toBe(templatesRendered);
+ expect(findCiTemplates().exists()).toBe(templatesRendered);
});
},
);
diff --git a/spec/frontend/pipelines/empty_state_spec.js b/spec/frontend/pipelines/empty_state_spec.js
index 31b74a06efd..46dad4a035c 100644
--- a/spec/frontend/pipelines/empty_state_spec.js
+++ b/spec/frontend/pipelines/empty_state_spec.js
@@ -1,7 +1,7 @@
import '~/commons';
import { mount } from '@vue/test-utils';
import EmptyState from '~/pipelines/components/pipelines_list/empty_state.vue';
-import PipelinesCiTemplates from '~/pipelines/components/pipelines_list/pipelines_ci_templates.vue';
+import PipelinesCiTemplates from '~/pipelines/components/pipelines_list/empty_state/pipelines_ci_templates.vue';
describe('Pipelines Empty State', () => {
let wrapper;
diff --git a/spec/frontend/pipelines/graph/action_component_spec.js b/spec/frontend/pipelines/graph/action_component_spec.js
index fab6e6887b7..6e5aa572ec0 100644
--- a/spec/frontend/pipelines/graph/action_component_spec.js
+++ b/spec/frontend/pipelines/graph/action_component_spec.js
@@ -48,17 +48,14 @@ describe('pipeline graph action component', () => {
});
describe('on click', () => {
- it('emits `pipelineActionRequestComplete` after a successful request', (done) => {
+ it('emits `pipelineActionRequestComplete` after a successful request', async () => {
jest.spyOn(wrapper.vm, '$emit');
findButton().trigger('click');
- waitForPromises()
- .then(() => {
- expect(wrapper.vm.$emit).toHaveBeenCalledWith('pipelineActionRequestComplete');
- done();
- })
- .catch(done.fail);
+ await waitForPromises();
+
+ expect(wrapper.vm.$emit).toHaveBeenCalledWith('pipelineActionRequestComplete');
});
it('renders a loading icon while waiting for request', async () => {
diff --git a/spec/frontend/pipelines/graph/graph_component_wrapper_spec.js b/spec/frontend/pipelines/graph/graph_component_wrapper_spec.js
index 8bc6c086b9d..cb7073fb5f5 100644
--- a/spec/frontend/pipelines/graph/graph_component_wrapper_spec.js
+++ b/spec/frontend/pipelines/graph/graph_component_wrapper_spec.js
@@ -30,6 +30,7 @@ import LinksLayer from '~/pipelines/components/graph_shared/links_layer.vue';
import * as parsingUtils from '~/pipelines/components/parsing_utils';
import getPipelineHeaderData from '~/pipelines/graphql/queries/get_pipeline_header_data.query.graphql';
import * as sentryUtils from '~/pipelines/utils';
+import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
import { mockRunningPipelineHeaderData } from '../mock_data';
import { mapCallouts, mockCalloutsResponse, mockPipelineResponse } from './mock_data';
@@ -55,6 +56,7 @@ describe('Pipeline graph wrapper', () => {
wrapper.find(StageColumnComponent).findAll('[data-testid="stage-column-group"]');
const getViewSelector = () => wrapper.find(GraphViewSelector);
const getViewSelectorTrip = () => getViewSelector().findComponent(GlAlert);
+ const getLocalStorageSync = () => wrapper.findComponent(LocalStorageSync);
const createComponent = ({
apolloProvider,
@@ -376,6 +378,10 @@ describe('Pipeline graph wrapper', () => {
localStorage.clear();
});
+ it('sets the asString prop on the LocalStorageSync component', () => {
+ expect(getLocalStorageSync().props('asString')).toBe(true);
+ });
+
it('reads the view type from localStorage when available', () => {
const viewSelectorNeedsSegment = wrapper
.find(GlButtonGroup)
diff --git a/spec/frontend/pipelines/pipeline_triggerer_spec.js b/spec/frontend/pipelines/pipeline_triggerer_spec.js
index 701b1691c7b..58bfb68e85c 100644
--- a/spec/frontend/pipelines/pipeline_triggerer_spec.js
+++ b/spec/frontend/pipelines/pipeline_triggerer_spec.js
@@ -1,17 +1,11 @@
-import { shallowMount } from '@vue/test-utils';
-import { nextTick } from 'vue';
+import { GlAvatar, GlAvatarLink } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import pipelineTriggerer from '~/pipelines/components/pipelines_list/pipeline_triggerer.vue';
-import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
+import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
describe('Pipelines Triggerer', () => {
let wrapper;
- const expectComponentWithProps = (Component, props = {}) => {
- const componentWrapper = wrapper.find(Component);
- expect(componentWrapper.isVisible()).toBe(true);
- expect(componentWrapper.props()).toEqual(expect.objectContaining(props));
- };
-
const mockData = {
pipeline: {
user: {
@@ -22,40 +16,65 @@ describe('Pipelines Triggerer', () => {
},
};
- const createComponent = () => {
- wrapper = shallowMount(pipelineTriggerer, {
- propsData: mockData,
+ const createComponent = (props) => {
+ wrapper = shallowMountExtended(pipelineTriggerer, {
+ propsData: {
+ ...props,
+ },
+ directives: {
+ GlTooltip: createMockDirective(),
+ },
});
};
- beforeEach(() => {
- createComponent();
- });
-
afterEach(() => {
wrapper.destroy();
});
- it('should render pipeline triggerer table cell', () => {
- expect(wrapper.find('[data-testid="pipeline-triggerer"]').exists()).toBe(true);
- });
+ const findAvatarLink = () => wrapper.findComponent(GlAvatarLink);
+ const findAvatar = () => wrapper.findComponent(GlAvatar);
+ const findTriggerer = () => wrapper.findByText('API');
+
+ describe('when user was a triggerer', () => {
+ beforeEach(() => {
+ createComponent(mockData);
+ });
+
+ it('should render pipeline triggerer table cell', () => {
+ expect(wrapper.find('[data-testid="pipeline-triggerer"]').exists()).toBe(true);
+ });
+
+ it('should render only user avatar', () => {
+ expect(findAvatarLink().exists()).toBe(true);
+ expect(findTriggerer().exists()).toBe(false);
+ });
+
+ it('should set correct props on avatar link component', () => {
+ expect(findAvatarLink().attributes()).toMatchObject({
+ title: mockData.pipeline.user.name,
+ href: mockData.pipeline.user.path,
+ });
+ });
- it('should pass triggerer information when triggerer is provided', () => {
- expectComponentWithProps(UserAvatarLink, {
- linkHref: mockData.pipeline.user.path,
- tooltipText: mockData.pipeline.user.name,
- imgSrc: mockData.pipeline.user.avatar_url,
+ it('should add tooltip to avatar link', () => {
+ const tooltip = getBinding(findAvatarLink().element, 'gl-tooltip');
+
+ expect(tooltip).toBeDefined();
+ });
+
+ it('should set correct props on avatar component', () => {
+ expect(findAvatar().attributes().src).toBe(mockData.pipeline.user.avatar_url);
});
});
- it('should render "API" when no triggerer is provided', async () => {
- wrapper.setProps({
- pipeline: {
- user: null,
- },
+ describe('when API was a triggerer', () => {
+ beforeEach(() => {
+ createComponent({ pipeline: {} });
});
- await nextTick();
- expect(wrapper.find('.js-pipeline-url-api').text()).toEqual('API');
+ it('should render label only', () => {
+ expect(findAvatarLink().exists()).toBe(false);
+ expect(findTriggerer().exists()).toBe(true);
+ });
});
});
diff --git a/spec/frontend/pipelines/pipeline_url_spec.js b/spec/frontend/pipelines/pipeline_url_spec.js
index 2a0aeed917c..c6104a13216 100644
--- a/spec/frontend/pipelines/pipeline_url_spec.js
+++ b/spec/frontend/pipelines/pipeline_url_spec.js
@@ -1,5 +1,6 @@
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import PipelineUrlComponent from '~/pipelines/components/pipelines_list/pipeline_url.vue';
+import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
import { mockPipeline, mockPipelineBranch, mockPipelineTag } from './mock_data';
const projectPath = 'test/test';
@@ -57,6 +58,30 @@ describe('Pipeline Url Component', () => {
expect(findCommitShortSha().exists()).toBe(true);
});
+ describe('commit user avatar', () => {
+ it('renders when commit author exists', () => {
+ const pipelineBranch = mockPipelineBranch();
+ const { avatar_url, name, path } = pipelineBranch.pipeline.commit.author;
+ createComponent(pipelineBranch);
+
+ const component = wrapper.findComponent(UserAvatarLink);
+ expect(component.exists()).toBe(true);
+ expect(component.props()).toMatchObject({
+ imgSize: 16,
+ imgSrc: avatar_url,
+ imgAlt: name,
+ linkHref: path,
+ tooltipText: name,
+ });
+ });
+
+ it('does not render when commit author does not exist', () => {
+ createComponent();
+
+ expect(wrapper.findComponent(UserAvatarLink).exists()).toBe(false);
+ });
+ });
+
it('should render commit icon tooltip', () => {
createComponent({}, true);
diff --git a/spec/frontend/pipelines/pipelines_spec.js b/spec/frontend/pipelines/pipelines_spec.js
index 20ed12cd1f5..d2b30c93746 100644
--- a/spec/frontend/pipelines/pipelines_spec.js
+++ b/spec/frontend/pipelines/pipelines_spec.js
@@ -14,7 +14,7 @@ import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import NavigationControls from '~/pipelines/components/pipelines_list/nav_controls.vue';
import PipelinesComponent from '~/pipelines/components/pipelines_list/pipelines.vue';
-import PipelinesCiTemplates from '~/pipelines/components/pipelines_list/pipelines_ci_templates.vue';
+import PipelinesCiTemplates from '~/pipelines/components/pipelines_list/empty_state/pipelines_ci_templates.vue';
import PipelinesTableComponent from '~/pipelines/components/pipelines_list/pipelines_table.vue';
import { RAW_TEXT_WARNING } from '~/pipelines/constants';
import Store from '~/pipelines/stores/pipelines_store';
diff --git a/spec/frontend/pipelines/test_reports/stores/actions_spec.js b/spec/frontend/pipelines/test_reports/stores/actions_spec.js
index 84a9f4776b9..d5acb115bc1 100644
--- a/spec/frontend/pipelines/test_reports/stores/actions_spec.js
+++ b/spec/frontend/pipelines/test_reports/stores/actions_spec.js
@@ -38,29 +38,25 @@ describe('Actions TestReports Store', () => {
mock.onGet(summaryEndpoint).replyOnce(200, summary, {});
});
- it('sets testReports and shows tests', (done) => {
- testAction(
+ it('sets testReports and shows tests', () => {
+ return testAction(
actions.fetchSummary,
null,
state,
[{ type: types.SET_SUMMARY, payload: summary }],
[{ type: 'toggleLoading' }, { type: 'toggleLoading' }],
- done,
);
});
- it('should create flash on API error', (done) => {
- testAction(
+ it('should create flash on API error', async () => {
+ await testAction(
actions.fetchSummary,
null,
{ summaryEndpoint: null },
[],
[{ type: 'toggleLoading' }, { type: 'toggleLoading' }],
- () => {
- expect(createFlash).toHaveBeenCalled();
- done();
- },
);
+ expect(createFlash).toHaveBeenCalled();
});
});
@@ -73,87 +69,80 @@ describe('Actions TestReports Store', () => {
.replyOnce(200, testReports.test_suites[0], {});
});
- it('sets test suite and shows tests', (done) => {
+ it('sets test suite and shows tests', () => {
const suite = testReports.test_suites[0];
const index = 0;
- testAction(
+ return testAction(
actions.fetchTestSuite,
index,
{ ...state, testReports },
[{ type: types.SET_SUITE, payload: { suite, index } }],
[{ type: 'toggleLoading' }, { type: 'toggleLoading' }],
- done,
);
});
- it('should create flash on API error', (done) => {
+ it('should create flash on API error', async () => {
const index = 0;
- testAction(
+ await testAction(
actions.fetchTestSuite,
index,
{ ...state, testReports, suiteEndpoint: null },
[],
[{ type: 'toggleLoading' }, { type: 'toggleLoading' }],
- () => {
- expect(createFlash).toHaveBeenCalled();
- done();
- },
);
+ expect(createFlash).toHaveBeenCalled();
});
describe('when we already have the suite data', () => {
- it('should not fetch suite', (done) => {
+ it('should not fetch suite', () => {
const index = 0;
testReports.test_suites[0].hasFullSuite = true;
- testAction(actions.fetchTestSuite, index, { ...state, testReports }, [], [], done);
+ return testAction(actions.fetchTestSuite, index, { ...state, testReports }, [], []);
});
});
});
describe('set selected suite index', () => {
- it('sets selectedSuiteIndex', (done) => {
+ it('sets selectedSuiteIndex', () => {
const selectedSuiteIndex = 0;
- testAction(
+ return testAction(
actions.setSelectedSuiteIndex,
selectedSuiteIndex,
{ ...state, hasFullReport: true },
[{ type: types.SET_SELECTED_SUITE_INDEX, payload: selectedSuiteIndex }],
[],
- done,
);
});
});
describe('remove selected suite index', () => {
- it('sets selectedSuiteIndex to null', (done) => {
- testAction(
+ it('sets selectedSuiteIndex to null', () => {
+ return testAction(
actions.removeSelectedSuiteIndex,
{},
state,
[{ type: types.SET_SELECTED_SUITE_INDEX, payload: null }],
[],
- done,
);
});
});
describe('toggles loading', () => {
- it('sets isLoading to true', (done) => {
- testAction(actions.toggleLoading, {}, state, [{ type: types.TOGGLE_LOADING }], [], done);
+ it('sets isLoading to true', () => {
+ return testAction(actions.toggleLoading, {}, state, [{ type: types.TOGGLE_LOADING }], []);
});
- it('toggles isLoading to false', (done) => {
- testAction(
+ it('toggles isLoading to false', () => {
+ return testAction(
actions.toggleLoading,
{},
{ ...state, isLoading: true },
[{ type: types.TOGGLE_LOADING }],
[],
- done,
);
});
});
diff --git a/spec/frontend/profile/add_ssh_key_validation_spec.js b/spec/frontend/profile/add_ssh_key_validation_spec.js
index a6bcca0ccb3..730a94592a7 100644
--- a/spec/frontend/profile/add_ssh_key_validation_spec.js
+++ b/spec/frontend/profile/add_ssh_key_validation_spec.js
@@ -1,4 +1,4 @@
-import AddSshKeyValidation from '../../../app/assets/javascripts/profile/add_ssh_key_validation';
+import AddSshKeyValidation from '~/profile/add_ssh_key_validation';
describe('AddSshKeyValidation', () => {
describe('submit', () => {
diff --git a/spec/frontend/profile/preferences/components/__snapshots__/diffs_colors_preview_spec.js.snap b/spec/frontend/profile/preferences/components/__snapshots__/diffs_colors_preview_spec.js.snap
new file mode 100644
index 00000000000..3025a2f87ae
--- /dev/null
+++ b/spec/frontend/profile/preferences/components/__snapshots__/diffs_colors_preview_spec.js.snap
@@ -0,0 +1,915 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`DiffsColorsPreview component renders diff colors preview 1`] = `
+<div
+ class="form-group"
+>
+ <label>
+ Preview
+ </label>
+
+ <table
+ class="code"
+ >
+ <tbody>
+ <tr
+ class="line_holder parallel"
+ >
+ <td
+ class="old_line diff-line-num old"
+ >
+ <a
+ data-linenumber="1"
+ />
+ </td>
+
+ <td
+ class="line_content parallel left-side old"
+ >
+ <span>
+ <span
+ class="c1"
+ >
+ #
+ <span
+ class="idiff deletion"
+ >
+ Removed
+ </span>
+ content
+ </span>
+ </span>
+ </td>
+
+ <td
+ class="new_line diff-line-num new"
+ >
+ <a
+ data-linenumber="1"
+ />
+ </td>
+
+ <td
+ class="line_content parallel right-side new"
+ >
+ <span>
+ <span
+ class="c1"
+ >
+ #
+ <span
+ class="idiff addition"
+ >
+ Added
+ </span>
+ content
+ </span>
+ </span>
+ </td>
+ </tr>
+
+ <tr
+ class="line_holder parallel"
+ >
+ <td
+ class="old_line diff-line-num old"
+ >
+ <a
+ data-linenumber="2"
+ />
+ </td>
+
+ <td
+ class="line_content parallel left-side old"
+ >
+ <span>
+ <span
+ class="n"
+ >
+ v
+ </span>
+
+ <span
+ class="o"
+ >
+ =
+ </span>
+
+ <span
+ class="mi"
+ >
+ 1
+ </span>
+ </span>
+ </td>
+
+ <td
+ class="new_line diff-line-num new"
+ >
+ <a
+ data-linenumber="2"
+ />
+ </td>
+
+ <td
+ class="line_content parallel right-side new"
+ >
+ <span>
+ <span
+ class="n"
+ >
+ v
+ </span>
+
+ <span
+ class="o"
+ >
+ =
+ </span>
+
+ <span
+ class="mi"
+ >
+ 1
+ </span>
+ </span>
+ </td>
+ </tr>
+
+ <tr
+ class="line_holder parallel"
+ >
+ <td
+ class="old_line diff-line-num old"
+ >
+ <a
+ data-linenumber="3"
+ />
+ </td>
+
+ <td
+ class="line_content parallel left-side old"
+ >
+ <span>
+ <span
+ class="n"
+ >
+ s
+ </span>
+
+ <span
+ class="o"
+ >
+ =
+ </span>
+
+ <span
+ class="s"
+ >
+ "string"
+ </span>
+ </span>
+ </td>
+
+ <td
+ class="new_line diff-line-num new"
+ >
+ <a
+ data-linenumber="3"
+ />
+ </td>
+
+ <td
+ class="line_content parallel right-side new"
+ >
+ <span>
+ <span
+ class="n"
+ >
+ s
+ </span>
+
+ <span
+ class="o"
+ >
+ =
+ </span>
+
+ <span
+ class="s"
+ >
+ "string"
+ </span>
+ </span>
+ </td>
+ </tr>
+
+ <tr
+ class="line_holder parallel"
+ >
+ <td
+ class="old_line diff-line-num old"
+ >
+ <a
+ data-linenumber="4"
+ />
+ </td>
+
+ <td
+ class="line_content parallel left-side old"
+ >
+ <span />
+ </td>
+
+ <td
+ class="new_line diff-line-num new"
+ >
+ <a
+ data-linenumber="4"
+ />
+ </td>
+
+ <td
+ class="line_content parallel right-side new"
+ >
+ <span />
+ </td>
+ </tr>
+
+ <tr
+ class="line_holder parallel"
+ >
+ <td
+ class="old_line diff-line-num old"
+ >
+ <a
+ data-linenumber="5"
+ />
+ </td>
+
+ <td
+ class="line_content parallel left-side old"
+ >
+ <span>
+ <span
+ class="k"
+ >
+ for
+ </span>
+
+ <span
+ class="n"
+ >
+ i
+ </span>
+
+ <span
+ class="ow"
+ >
+ in
+ </span>
+
+ <span
+ class="nb"
+ >
+ range
+ </span>
+ <span
+ class="p"
+ >
+ (
+ </span>
+ <span
+ class="o"
+ >
+ -
+ </span>
+ <span
+ class="mi"
+ >
+ 10
+ </span>
+ <span
+ class="p"
+ >
+ ,
+ </span>
+
+ <span
+ class="mi"
+ >
+ 10
+ </span>
+ <span
+ class="p"
+ >
+ ):
+ </span>
+ </span>
+ </td>
+
+ <td
+ class="new_line diff-line-num new"
+ >
+ <a
+ data-linenumber="5"
+ />
+ </td>
+
+ <td
+ class="line_content parallel right-side new"
+ >
+ <span>
+ <span
+ class="k"
+ >
+ for
+ </span>
+
+ <span
+ class="n"
+ >
+ i
+ </span>
+
+ <span
+ class="ow"
+ >
+ in
+ </span>
+
+ <span
+ class="nb"
+ >
+ range
+ </span>
+ <span
+ class="p"
+ >
+ (
+ </span>
+ <span
+ class="o"
+ >
+ -
+ </span>
+ <span
+ class="mi"
+ >
+ 10
+ </span>
+ <span
+ class="p"
+ >
+ ,
+ </span>
+
+ <span
+ class="mi"
+ >
+ 10
+ </span>
+ <span
+ class="p"
+ >
+ ):
+ </span>
+ </span>
+ </td>
+ </tr>
+
+ <tr
+ class="line_holder parallel"
+ >
+ <td
+ class="old_line diff-line-num old"
+ >
+ <a
+ data-linenumber="6"
+ />
+ </td>
+
+ <td
+ class="line_content parallel left-side old"
+ >
+ <span>
+ <span>
+
+ </span>
+
+ <span
+ class="k"
+ >
+ print
+ </span>
+ <span
+ class="p"
+ >
+ (
+ </span>
+ <span
+ class="n"
+ >
+ i
+ </span>
+
+ <span
+ class="o"
+ >
+ +
+ </span>
+
+ <span
+ class="mi"
+ >
+ 1
+ </span>
+ <span
+ class="p"
+ >
+ )
+ </span>
+ </span>
+ </td>
+
+ <td
+ class="new_line diff-line-num new"
+ >
+ <a
+ data-linenumber="6"
+ />
+ </td>
+
+ <td
+ class="line_content parallel right-side new"
+ >
+ <span>
+ <span>
+
+ </span>
+
+ <span
+ class="k"
+ >
+ print
+ </span>
+ <span
+ class="p"
+ >
+ (
+ </span>
+ <span
+ class="n"
+ >
+ i
+ </span>
+
+ <span
+ class="o"
+ >
+ +
+ </span>
+
+ <span
+ class="mi"
+ >
+ 1
+ </span>
+ <span
+ class="p"
+ >
+ )
+ </span>
+ </span>
+ </td>
+ </tr>
+
+ <tr
+ class="line_holder parallel"
+ >
+ <td
+ class="old_line diff-line-num old"
+ >
+ <a
+ data-linenumber="7"
+ />
+ </td>
+
+ <td
+ class="line_content parallel left-side old"
+ >
+ <span />
+ </td>
+
+ <td
+ class="new_line diff-line-num new"
+ >
+ <a
+ data-linenumber="7"
+ />
+ </td>
+
+ <td
+ class="line_content parallel right-side new"
+ >
+ <span />
+ </td>
+ </tr>
+
+ <tr
+ class="line_holder parallel"
+ >
+ <td
+ class="old_line diff-line-num old"
+ >
+ <a
+ data-linenumber="8"
+ />
+ </td>
+
+ <td
+ class="line_content parallel left-side old"
+ >
+ <span>
+ <span
+ class="k"
+ >
+ class
+ </span>
+
+ <span
+ class="nc"
+ >
+ LinkedList
+ </span>
+ <span
+ class="p"
+ >
+ (
+ </span>
+ <span
+ class="nb"
+ >
+ object
+ </span>
+ <span
+ class="p"
+ >
+ ):
+ </span>
+ </span>
+ </td>
+
+ <td
+ class="new_line diff-line-num new"
+ >
+ <a
+ data-linenumber="8"
+ />
+ </td>
+
+ <td
+ class="line_content parallel right-side new"
+ >
+ <span>
+ <span
+ class="k"
+ >
+ class
+ </span>
+
+ <span
+ class="nc"
+ >
+ LinkedList
+ </span>
+ <span
+ class="p"
+ >
+ (
+ </span>
+ <span
+ class="nb"
+ >
+ object
+ </span>
+ <span
+ class="p"
+ >
+ ):
+ </span>
+ </span>
+ </td>
+ </tr>
+
+ <tr
+ class="line_holder parallel"
+ >
+ <td
+ class="old_line diff-line-num old"
+ >
+ <a
+ data-linenumber="9"
+ />
+ </td>
+
+ <td
+ class="line_content parallel left-side old"
+ >
+ <span>
+ <span>
+
+ </span>
+
+ <span
+ class="k"
+ >
+ def
+ </span>
+
+ <span
+ class="nf"
+ >
+ __init__
+ </span>
+ <span
+ class="p"
+ >
+ (
+ </span>
+ <span
+ class="bp"
+ >
+ self
+ </span>
+ <span
+ class="p"
+ >
+ ,
+ </span>
+
+ <span
+ class="n"
+ >
+ x
+ </span>
+ <span
+ class="p"
+ >
+ ):
+ </span>
+ </span>
+ </td>
+
+ <td
+ class="new_line diff-line-num new"
+ >
+ <a
+ data-linenumber="9"
+ />
+ </td>
+
+ <td
+ class="line_content parallel right-side new"
+ >
+ <span>
+ <span>
+
+ </span>
+
+ <span
+ class="k"
+ >
+ def
+ </span>
+
+ <span
+ class="nf"
+ >
+ __init__
+ </span>
+ <span
+ class="p"
+ >
+ (
+ </span>
+ <span
+ class="bp"
+ >
+ self
+ </span>
+ <span
+ class="p"
+ >
+ ,
+ </span>
+
+ <span
+ class="n"
+ >
+ x
+ </span>
+ <span
+ class="p"
+ >
+ ):
+ </span>
+ </span>
+ </td>
+ </tr>
+
+ <tr
+ class="line_holder parallel"
+ >
+ <td
+ class="old_line diff-line-num old"
+ >
+ <a
+ data-linenumber="10"
+ />
+ </td>
+
+ <td
+ class="line_content parallel left-side old"
+ >
+ <span>
+ <span>
+
+ </span>
+
+ <span
+ class="bp"
+ >
+ self
+ </span>
+ <span
+ class="p"
+ >
+ .
+ </span>
+ <span
+ class="n"
+ >
+ val
+ </span>
+
+ <span
+ class="o"
+ >
+ =
+ </span>
+
+ <span
+ class="n"
+ >
+ x
+ </span>
+ </span>
+ </td>
+
+ <td
+ class="new_line diff-line-num new"
+ >
+ <a
+ data-linenumber="10"
+ />
+ </td>
+
+ <td
+ class="line_content parallel right-side new"
+ >
+ <span>
+ <span>
+
+ </span>
+
+ <span
+ class="bp"
+ >
+ self
+ </span>
+ <span
+ class="p"
+ >
+ .
+ </span>
+ <span
+ class="n"
+ >
+ val
+ </span>
+
+ <span
+ class="o"
+ >
+ =
+ </span>
+
+ <span
+ class="n"
+ >
+ x
+ </span>
+ </span>
+ </td>
+ </tr>
+
+ <tr
+ class="line_holder parallel"
+ >
+ <td
+ class="old_line diff-line-num old"
+ >
+ <a
+ data-linenumber="11"
+ />
+ </td>
+
+ <td
+ class="line_content parallel left-side old"
+ >
+ <span>
+ <span>
+
+ </span>
+
+ <span
+ class="bp"
+ >
+ self
+ </span>
+ <span
+ class="p"
+ >
+ .
+ </span>
+ <span
+ class="nb"
+ >
+ next
+ </span>
+
+ <span
+ class="o"
+ >
+ =
+ </span>
+
+ <span
+ class="bp"
+ >
+ None
+ </span>
+ </span>
+ </td>
+
+ <td
+ class="new_line diff-line-num new"
+ >
+ <a
+ data-linenumber="11"
+ />
+ </td>
+
+ <td
+ class="line_content parallel right-side new"
+ >
+ <span>
+ <span>
+
+ </span>
+
+ <span
+ class="bp"
+ >
+ self
+ </span>
+ <span
+ class="p"
+ >
+ .
+ </span>
+ <span
+ class="nb"
+ >
+ next
+ </span>
+
+ <span
+ class="o"
+ >
+ =
+ </span>
+
+ <span
+ class="bp"
+ >
+ None
+ </span>
+ </span>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+</div>
+`;
diff --git a/spec/frontend/profile/preferences/components/diffs_colors_preview_spec.js b/spec/frontend/profile/preferences/components/diffs_colors_preview_spec.js
new file mode 100644
index 00000000000..e60602ab336
--- /dev/null
+++ b/spec/frontend/profile/preferences/components/diffs_colors_preview_spec.js
@@ -0,0 +1,23 @@
+import { shallowMount } from '@vue/test-utils';
+import DiffsColorsPreview from '~/profile/preferences/components/diffs_colors_preview.vue';
+
+describe('DiffsColorsPreview component', () => {
+ let wrapper;
+
+ function createComponent() {
+ wrapper = shallowMount(DiffsColorsPreview);
+ }
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ it('renders diff colors preview', () => {
+ expect(wrapper.element).toMatchSnapshot();
+ });
+});
diff --git a/spec/frontend/profile/preferences/components/diffs_colors_spec.js b/spec/frontend/profile/preferences/components/diffs_colors_spec.js
new file mode 100644
index 00000000000..02f501a0b06
--- /dev/null
+++ b/spec/frontend/profile/preferences/components/diffs_colors_spec.js
@@ -0,0 +1,153 @@
+import { shallowMount } from '@vue/test-utils';
+import { s__ } from '~/locale';
+import ColorPicker from '~/vue_shared/components/color_picker/color_picker.vue';
+import DiffsColors from '~/profile/preferences/components/diffs_colors.vue';
+import DiffsColorsPreview from '~/profile/preferences/components/diffs_colors_preview.vue';
+import * as CssUtils from '~/lib/utils/css_utils';
+
+describe('DiffsColors component', () => {
+ let wrapper;
+
+ const defaultInjectedProps = {
+ addition: '#00ff00',
+ deletion: '#ff0000',
+ };
+
+ const initialSuggestedColors = {
+ '#d99530': s__('SuggestedColors|Orange'),
+ '#1f75cb': s__('SuggestedColors|Blue'),
+ };
+
+ const findColorPickers = () => wrapper.findAllComponents(ColorPicker);
+
+ function createComponent(provide = {}) {
+ wrapper = shallowMount(DiffsColors, {
+ provide: {
+ ...defaultInjectedProps,
+ ...provide,
+ },
+ });
+ }
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ it('mounts', () => {
+ createComponent();
+
+ expect(wrapper.exists()).toBe(true);
+ });
+
+ describe('preview', () => {
+ it('should render preview', () => {
+ createComponent();
+
+ expect(wrapper.findComponent(DiffsColorsPreview).exists()).toBe(true);
+ });
+
+ it('should set preview classes', () => {
+ createComponent();
+
+ expect(wrapper.attributes('class')).toBe(
+ 'diff-custom-addition-color diff-custom-deletion-color',
+ );
+ });
+
+ it.each([
+ [{ addition: null }, 'diff-custom-deletion-color'],
+ [{ deletion: null }, 'diff-custom-addition-color'],
+ ])('should not set preview class if color not set', (provide, expectedClass) => {
+ createComponent(provide);
+
+ expect(wrapper.attributes('class')).toBe(expectedClass);
+ });
+
+ it.each([
+ [{}, '--diff-deletion-color: rgba(255,0,0,0.2); --diff-addition-color: rgba(0,255,0,0.2);'],
+ [{ addition: null }, '--diff-deletion-color: rgba(255,0,0,0.2);'],
+ [{ deletion: null }, '--diff-addition-color: rgba(0,255,0,0.2);'],
+ ])('should set correct CSS variables', (provide, expectedStyle) => {
+ createComponent(provide);
+
+ expect(wrapper.attributes('style')).toBe(expectedStyle);
+ });
+ });
+
+ describe('color pickers', () => {
+ it('should render both color pickers', () => {
+ createComponent();
+
+ const colorPickers = findColorPickers();
+
+ expect(colorPickers.length).toBe(2);
+ expect(colorPickers.at(0).props()).toMatchObject({
+ label: s__('Preferences|Color for removed lines'),
+ value: '#ff0000',
+ state: true,
+ });
+ expect(colorPickers.at(1).props()).toMatchObject({
+ label: s__('Preferences|Color for added lines'),
+ value: '#00ff00',
+ state: true,
+ });
+ });
+
+ describe('suggested colors', () => {
+ const suggestedColors = () => findColorPickers().at(0).props('suggestedColors');
+
+ it('contains initial suggested colors', () => {
+ createComponent();
+
+ expect(suggestedColors()).toMatchObject(initialSuggestedColors);
+ });
+
+ it('contains default diff colors of theme', () => {
+ jest.spyOn(CssUtils, 'getCssVariable').mockImplementation((variable) => {
+ if (variable === '--default-diff-color-addition') return '#111111';
+ if (variable === '--default-diff-color-deletion') return '#222222';
+ return '#000000';
+ });
+
+ createComponent();
+
+ expect(suggestedColors()).toMatchObject({
+ '#111111': s__('SuggestedColors|Default addition color'),
+ '#222222': s__('SuggestedColors|Default removal color'),
+ });
+ });
+
+ it('contains current diff colors if set', () => {
+ createComponent();
+
+ expect(suggestedColors()).toMatchObject({
+ [defaultInjectedProps.addition]: s__('SuggestedColors|Current addition color'),
+ [defaultInjectedProps.deletion]: s__('SuggestedColors|Current removal color'),
+ });
+ });
+
+ it.each([
+ [
+ { addition: null },
+ s__('SuggestedColors|Current removal color'),
+ s__('SuggestedColors|Current addition color'),
+ ],
+ [
+ { deletion: null },
+ s__('SuggestedColors|Current addition color'),
+ s__('SuggestedColors|Current removal color'),
+ ],
+ ])(
+ 'does not contain current diff color if not set %p',
+ (provide, expectedToContain, expectNotToContain) => {
+ createComponent(provide);
+
+ const suggestedColorsLabels = Object.values(suggestedColors());
+ expect(suggestedColorsLabels).toContain(expectedToContain);
+ expect(suggestedColorsLabels).not.toContain(expectNotToContain);
+ },
+ );
+ });
+ });
+});
diff --git a/spec/frontend/profile/preferences/components/integration_view_spec.js b/spec/frontend/profile/preferences/components/integration_view_spec.js
index 6ab0c70298c..92c53b8c91b 100644
--- a/spec/frontend/profile/preferences/components/integration_view_spec.js
+++ b/spec/frontend/profile/preferences/components/integration_view_spec.js
@@ -1,5 +1,5 @@
-import { GlFormText } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
+import { GlFormGroup } from '@gitlab/ui';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import IntegrationView from '~/profile/preferences/components/integration_view.vue';
@@ -21,7 +21,7 @@ describe('IntegrationView component', () => {
function createComponent(options = {}) {
const { props = {}, provide = {} } = options;
- return shallowMount(IntegrationView, {
+ return mountExtended(IntegrationView, {
provide: {
userFields,
...provide,
@@ -33,28 +33,20 @@ describe('IntegrationView component', () => {
});
}
- function findCheckbox() {
- return wrapper.find('[data-testid="profile-preferences-integration-checkbox"]');
- }
- function findFormGroup() {
- return wrapper.find('[data-testid="profile-preferences-integration-form-group"]');
- }
- function findHiddenField() {
- return wrapper.find('[data-testid="profile-preferences-integration-hidden-field"]');
- }
- function findFormGroupLabel() {
- return wrapper.find('[data-testid="profile-preferences-integration-form-group"] label');
- }
+ const findCheckbox = () => wrapper.findByLabelText(new RegExp(defaultProps.config.label));
+ const findFormGroup = () => wrapper.findComponent(GlFormGroup);
+ const findHiddenField = () =>
+ wrapper.findByTestId('profile-preferences-integration-hidden-field');
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
- it('should render the title correctly', () => {
+ it('should render the form group legend correctly', () => {
wrapper = createComponent();
- expect(wrapper.find('label.label-bold').text()).toBe('Foo');
+ expect(wrapper.findByText(defaultProps.config.title).exists()).toBe(true);
});
it('should render the form correctly', () => {
@@ -106,13 +98,6 @@ describe('IntegrationView component', () => {
it('should render the help text', () => {
wrapper = createComponent();
- expect(wrapper.find(GlFormText).exists()).toBe(true);
expect(wrapper.find(IntegrationHelpText).exists()).toBe(true);
});
-
- it('should render the label correctly', () => {
- wrapper = createComponent();
-
- expect(findFormGroupLabel().text()).toBe('Enable foo');
- });
});
diff --git a/spec/frontend/projects/commit/store/actions_spec.js b/spec/frontend/projects/commit/store/actions_spec.js
index 305257c9ca5..56dffcbd48e 100644
--- a/spec/frontend/projects/commit/store/actions_spec.js
+++ b/spec/frontend/projects/commit/store/actions_spec.js
@@ -44,12 +44,12 @@ describe('Commit form modal store actions', () => {
});
describe('fetchBranches', () => {
- it('dispatch correct actions on fetchBranches', (done) => {
+ it('dispatch correct actions on fetchBranches', () => {
jest
.spyOn(axios, 'get')
.mockImplementation(() => Promise.resolve({ data: { Branches: mockData.mockBranches } }));
- testAction(
+ return testAction(
actions.fetchBranches,
{},
state,
@@ -60,19 +60,15 @@ describe('Commit form modal store actions', () => {
},
],
[{ type: 'requestBranches' }],
- () => {
- done();
- },
);
});
- it('should show flash error and set error in state on fetchBranches failure', (done) => {
+ it('should show flash error and set error in state on fetchBranches failure', async () => {
jest.spyOn(axios, 'get').mockRejectedValue();
- testAction(actions.fetchBranches, {}, state, [], [{ type: 'requestBranches' }], () => {
- expect(createFlash).toHaveBeenCalledWith({ message: PROJECT_BRANCHES_ERROR });
- done();
- });
+ await testAction(actions.fetchBranches, {}, state, [], [{ type: 'requestBranches' }]);
+
+ expect(createFlash).toHaveBeenCalledWith({ message: PROJECT_BRANCHES_ERROR });
});
});
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 b8f9951bbfc..26a3b27d958 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
@@ -36,7 +36,7 @@ exports[`Project remove modal initialized matches the snapshot 1`] = `
modalclass=""
modalid="fakeUniqueId"
ok-variant="danger"
- size="sm"
+ size="md"
title-class="gl-text-red-500"
titletag="h4"
>
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 8fe4c5f1230..1c443879dc3 100644
--- a/spec/frontend/projects/new/components/deployment_target_select_spec.js
+++ b/spec/frontend/projects/new/components/deployment_target_select_spec.js
@@ -1,4 +1,5 @@
-import { GlFormGroup, GlFormSelect } from '@gitlab/ui';
+import { GlFormGroup, GlFormSelect, GlFormText, GlLink, GlSprintf } from '@gitlab/ui';
+import { nextTick } from 'vue';
import { shallowMount } from '@vue/test-utils';
import { mockTracking } from 'helpers/tracking_helper';
import DeploymentTargetSelect from '~/projects/new/components/deployment_target_select.vue';
@@ -6,7 +7,9 @@ import {
DEPLOYMENT_TARGET_SELECTIONS,
DEPLOYMENT_TARGET_LABEL,
DEPLOYMENT_TARGET_EVENT,
+ VISIT_DOCS_EVENT,
NEW_PROJECT_FORM,
+ K8S_OPTION,
} from '~/projects/new/constants';
describe('Deployment target select', () => {
@@ -15,11 +18,15 @@ describe('Deployment target select', () => {
const findFormGroup = () => wrapper.findComponent(GlFormGroup);
const findSelect = () => wrapper.findComponent(GlFormSelect);
+ const findText = () => wrapper.findComponent(GlFormText);
+ const findLink = () => wrapper.findComponent(GlLink);
const createdWrapper = () => {
wrapper = shallowMount(DeploymentTargetSelect, {
stubs: {
GlFormSelect,
+ GlFormText,
+ GlSprintf,
},
});
};
@@ -79,4 +86,34 @@ describe('Deployment target select', () => {
});
}
});
+
+ describe.each`
+ selectedTarget | isTextShown
+ ${null} | ${false}
+ ${DEPLOYMENT_TARGET_SELECTIONS[0]} | ${true}
+ ${DEPLOYMENT_TARGET_SELECTIONS[1]} | ${false}
+ `('K8s education text', ({ selectedTarget, isTextShown }) => {
+ beforeEach(() => {
+ findSelect().vm.$emit('input', selectedTarget);
+ });
+
+ it(`is ${!isTextShown ? 'not ' : ''}shown when selected option is ${selectedTarget}`, () => {
+ expect(findText().exists()).toBe(isTextShown);
+ });
+ });
+
+ describe('when user clicks on the docs link', () => {
+ beforeEach(async () => {
+ findSelect().vm.$emit('input', K8S_OPTION);
+ await nextTick();
+
+ findLink().trigger('click');
+ });
+
+ it('sends the snowplow tracking event', () => {
+ expect(trackingSpy).toHaveBeenCalledWith('_category_', VISIT_DOCS_EVENT, {
+ label: DEPLOYMENT_TARGET_LABEL,
+ });
+ });
+ });
});
diff --git a/spec/frontend/projects/new/components/new_project_url_select_spec.js b/spec/frontend/projects/new/components/new_project_url_select_spec.js
index 921f5b74278..ba22622e1f7 100644
--- a/spec/frontend/projects/new/components/new_project_url_select_spec.js
+++ b/spec/frontend/projects/new/components/new_project_url_select_spec.js
@@ -15,6 +15,7 @@ import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import eventHub from '~/projects/new/event_hub';
import NewProjectUrlSelect from '~/projects/new/components/new_project_url_select.vue';
import searchQuery from '~/projects/new/queries/search_namespaces_where_user_can_create_projects.query.graphql';
+import { s__ } from '~/locale';
describe('NewProjectUrlSelect component', () => {
let wrapper;
@@ -61,7 +62,6 @@ describe('NewProjectUrlSelect component', () => {
namespaceId: '28',
rootUrl: 'https://gitlab.com/',
trackLabel: 'blank_project',
- userNamespaceFullPath: 'root',
userNamespaceId: '1',
};
@@ -91,7 +91,10 @@ describe('NewProjectUrlSelect component', () => {
const findButtonLabel = () => wrapper.findComponent(GlButton);
const findDropdown = () => wrapper.findComponent(GlDropdown);
const findInput = () => wrapper.findComponent(GlSearchBoxByType);
- const findHiddenInput = () => wrapper.find('[name="project[namespace_id]"]');
+ const findHiddenNamespaceInput = () => wrapper.find('[name="project[namespace_id]"]');
+
+ const findHiddenSelectedNamespaceInput = () =>
+ wrapper.find('[name="project[selected_namespace_id]"]');
const clickDropdownItem = async () => {
wrapper.findComponent(GlDropdownItem).vm.$emit('click');
@@ -122,11 +125,20 @@ describe('NewProjectUrlSelect component', () => {
});
it('renders a dropdown with the given namespace full path as the text', () => {
- expect(findDropdown().props('text')).toBe(defaultProvide.namespaceFullPath);
+ const dropdownProps = findDropdown().props();
+
+ expect(dropdownProps.text).toBe(defaultProvide.namespaceFullPath);
+ expect(dropdownProps.toggleClass).not.toContain('gl-text-gray-500!');
+ });
+
+ it('renders a hidden input with the given namespace id', () => {
+ expect(findHiddenNamespaceInput().attributes('value')).toBe(defaultProvide.namespaceId);
});
- it('renders a dropdown with the given namespace id in the hidden input', () => {
- expect(findHiddenInput().attributes('value')).toBe(defaultProvide.namespaceId);
+ it('renders a hidden input with the selected namespace id', () => {
+ expect(findHiddenSelectedNamespaceInput().attributes('value')).toBe(
+ defaultProvide.namespaceId,
+ );
});
});
@@ -142,11 +154,18 @@ describe('NewProjectUrlSelect component', () => {
});
it("renders a dropdown with the user's namespace full path as the text", () => {
- expect(findDropdown().props('text')).toBe(defaultProvide.userNamespaceFullPath);
+ const dropdownProps = findDropdown().props();
+
+ expect(dropdownProps.text).toBe(s__('ProjectsNew|Pick a group or namespace'));
+ expect(dropdownProps.toggleClass).toContain('gl-text-gray-500!');
+ });
+
+ it("renders a hidden input with the user's namespace id", () => {
+ expect(findHiddenNamespaceInput().attributes('value')).toBe(defaultProvide.userNamespaceId);
});
- it("renders a dropdown with the user's namespace id in the hidden input", () => {
- expect(findHiddenInput().attributes('value')).toBe(defaultProvide.userNamespaceId);
+ it('renders a hidden input with the selected namespace id', () => {
+ expect(findHiddenSelectedNamespaceInput().attributes('value')).toBe(undefined);
});
});
@@ -270,7 +289,7 @@ describe('NewProjectUrlSelect component', () => {
await clickDropdownItem();
- expect(findHiddenInput().attributes('value')).toBe(
+ expect(findHiddenNamespaceInput().attributes('value')).toBe(
getIdFromGraphQLId(data.currentUser.groups.nodes[0].id).toString(),
);
});
diff --git a/spec/frontend/releases/components/app_index_apollo_client_spec.js b/spec/frontend/releases/components/app_index_apollo_client_spec.js
deleted file mode 100644
index 9881ef9bc9f..00000000000
--- a/spec/frontend/releases/components/app_index_apollo_client_spec.js
+++ /dev/null
@@ -1,398 +0,0 @@
-import { cloneDeep } from 'lodash';
-import Vue, { nextTick } from 'vue';
-import VueApollo from 'vue-apollo';
-import originalAllReleasesQueryResponse from 'test_fixtures/graphql/releases/graphql/queries/all_releases.query.graphql.json';
-import createMockApollo from 'helpers/mock_apollo_helper';
-import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import waitForPromises from 'helpers/wait_for_promises';
-import allReleasesQuery from 'shared_queries/releases/all_releases.query.graphql';
-import createFlash from '~/flash';
-import { historyPushState } from '~/lib/utils/common_utils';
-import ReleasesIndexApolloClientApp from '~/releases/components/app_index_apollo_client.vue';
-import ReleaseBlock from '~/releases/components/release_block.vue';
-import ReleaseSkeletonLoader from '~/releases/components/release_skeleton_loader.vue';
-import ReleasesEmptyState from '~/releases/components/releases_empty_state.vue';
-import ReleasesPaginationApolloClient from '~/releases/components/releases_pagination_apollo_client.vue';
-import ReleasesSortApolloClient from '~/releases/components/releases_sort_apollo_client.vue';
-import { PAGE_SIZE, CREATED_ASC, DEFAULT_SORT } from '~/releases/constants';
-
-Vue.use(VueApollo);
-
-jest.mock('~/flash');
-
-let mockQueryParams;
-jest.mock('~/lib/utils/common_utils', () => ({
- ...jest.requireActual('~/lib/utils/common_utils'),
- historyPushState: jest.fn(),
-}));
-
-jest.mock('~/lib/utils/url_utility', () => ({
- ...jest.requireActual('~/lib/utils/url_utility'),
- getParameterByName: jest
- .fn()
- .mockImplementation((parameterName) => mockQueryParams[parameterName]),
-}));
-
-describe('app_index_apollo_client.vue', () => {
- const projectPath = 'project/path';
- const newReleasePath = 'path/to/new/release/page';
- const before = 'beforeCursor';
- const after = 'afterCursor';
-
- let wrapper;
- let allReleases;
- let singleRelease;
- let noReleases;
- let queryMock;
-
- const createComponent = ({
- singleResponse = Promise.resolve(singleRelease),
- fullResponse = Promise.resolve(allReleases),
- } = {}) => {
- const apolloProvider = createMockApollo([
- [
- allReleasesQuery,
- queryMock.mockImplementation((vars) => {
- return vars.first === 1 ? singleResponse : fullResponse;
- }),
- ],
- ]);
-
- wrapper = shallowMountExtended(ReleasesIndexApolloClientApp, {
- apolloProvider,
- provide: {
- newReleasePath,
- projectPath,
- },
- });
- };
-
- beforeEach(() => {
- mockQueryParams = {};
-
- allReleases = cloneDeep(originalAllReleasesQueryResponse);
-
- singleRelease = cloneDeep(originalAllReleasesQueryResponse);
- singleRelease.data.project.releases.nodes.splice(
- 1,
- singleRelease.data.project.releases.nodes.length,
- );
-
- noReleases = cloneDeep(originalAllReleasesQueryResponse);
- noReleases.data.project.releases.nodes = [];
-
- queryMock = jest.fn();
- });
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- // Finders
- const findLoadingIndicator = () => wrapper.findComponent(ReleaseSkeletonLoader);
- const findEmptyState = () => wrapper.findComponent(ReleasesEmptyState);
- const findNewReleaseButton = () =>
- wrapper.findByText(ReleasesIndexApolloClientApp.i18n.newRelease);
- const findAllReleaseBlocks = () => wrapper.findAllComponents(ReleaseBlock);
- const findPagination = () => wrapper.findComponent(ReleasesPaginationApolloClient);
- const findSort = () => wrapper.findComponent(ReleasesSortApolloClient);
-
- // Tests
- describe('component states', () => {
- // These need to be defined as functions, since `singleRelease` and
- // `allReleases` are generated in a `beforeEach`, and therefore
- // aren't available at test definition time.
- const getInProgressResponse = () => new Promise(() => {});
- const getErrorResponse = () => Promise.reject(new Error('Oops!'));
- const getSingleRequestLoadedResponse = () => Promise.resolve(singleRelease);
- const getFullRequestLoadedResponse = () => Promise.resolve(allReleases);
- const getLoadedEmptyResponse = () => Promise.resolve(noReleases);
-
- const toDescription = (bool) => (bool ? 'does' : 'does not');
-
- describe.each`
- description | singleResponseFn | fullResponseFn | loadingIndicator | emptyState | flashMessage | releaseCount | pagination
- ${'both requests loading'} | ${getInProgressResponse} | ${getInProgressResponse} | ${true} | ${false} | ${false} | ${0} | ${false}
- ${'both requests failed'} | ${getErrorResponse} | ${getErrorResponse} | ${false} | ${false} | ${true} | ${0} | ${false}
- ${'both requests loaded'} | ${getSingleRequestLoadedResponse} | ${getFullRequestLoadedResponse} | ${false} | ${false} | ${false} | ${2} | ${true}
- ${'both requests loaded with no results'} | ${getLoadedEmptyResponse} | ${getLoadedEmptyResponse} | ${false} | ${true} | ${false} | ${0} | ${false}
- ${'single request loading, full request loaded'} | ${getInProgressResponse} | ${getFullRequestLoadedResponse} | ${false} | ${false} | ${false} | ${2} | ${true}
- ${'single request loading, full request failed'} | ${getInProgressResponse} | ${getErrorResponse} | ${true} | ${false} | ${true} | ${0} | ${false}
- ${'single request loaded, full request loading'} | ${getSingleRequestLoadedResponse} | ${getInProgressResponse} | ${true} | ${false} | ${false} | ${1} | ${false}
- ${'single request loaded, full request failed'} | ${getSingleRequestLoadedResponse} | ${getErrorResponse} | ${false} | ${false} | ${true} | ${1} | ${false}
- ${'single request failed, full request loading'} | ${getErrorResponse} | ${getInProgressResponse} | ${true} | ${false} | ${false} | ${0} | ${false}
- ${'single request failed, full request loaded'} | ${getErrorResponse} | ${getFullRequestLoadedResponse} | ${false} | ${false} | ${false} | ${2} | ${true}
- ${'single request loaded with no results, full request loading'} | ${getLoadedEmptyResponse} | ${getInProgressResponse} | ${true} | ${false} | ${false} | ${0} | ${false}
- ${'single request loading, full request loadied with no results'} | ${getInProgressResponse} | ${getLoadedEmptyResponse} | ${false} | ${true} | ${false} | ${0} | ${false}
- `(
- '$description',
- ({
- singleResponseFn,
- fullResponseFn,
- loadingIndicator,
- emptyState,
- flashMessage,
- releaseCount,
- pagination,
- }) => {
- beforeEach(() => {
- createComponent({
- singleResponse: singleResponseFn(),
- fullResponse: fullResponseFn(),
- });
- });
-
- it(`${toDescription(loadingIndicator)} render a loading indicator`, async () => {
- await waitForPromises();
- expect(findLoadingIndicator().exists()).toBe(loadingIndicator);
- });
-
- it(`${toDescription(emptyState)} render an empty state`, () => {
- expect(findEmptyState().exists()).toBe(emptyState);
- });
-
- it(`${toDescription(flashMessage)} show a flash message`, () => {
- if (flashMessage) {
- expect(createFlash).toHaveBeenCalledWith({
- message: ReleasesIndexApolloClientApp.i18n.errorMessage,
- captureError: true,
- error: expect.any(Error),
- });
- } else {
- expect(createFlash).not.toHaveBeenCalled();
- }
- });
-
- it(`renders ${releaseCount} release(s)`, () => {
- expect(findAllReleaseBlocks()).toHaveLength(releaseCount);
- });
-
- it(`${toDescription(pagination)} render the pagination controls`, () => {
- expect(findPagination().exists()).toBe(pagination);
- });
-
- it('does render the "New release" button', () => {
- expect(findNewReleaseButton().exists()).toBe(true);
- });
-
- it('does render the sort controls', () => {
- expect(findSort().exists()).toBe(true);
- });
- },
- );
- });
-
- describe('URL parameters', () => {
- describe('when the URL contains no query parameters', () => {
- beforeEach(() => {
- createComponent();
- });
-
- it('makes a request with the correct GraphQL query parameters', () => {
- expect(queryMock).toHaveBeenCalledTimes(2);
-
- expect(queryMock).toHaveBeenCalledWith({
- first: 1,
- fullPath: projectPath,
- sort: DEFAULT_SORT,
- });
-
- expect(queryMock).toHaveBeenCalledWith({
- first: PAGE_SIZE,
- fullPath: projectPath,
- sort: DEFAULT_SORT,
- });
- });
- });
-
- describe('when the URL contains a "before" query parameter', () => {
- beforeEach(() => {
- mockQueryParams = { before };
- createComponent();
- });
-
- it('makes a request with the correct GraphQL query parameters', () => {
- expect(queryMock).toHaveBeenCalledTimes(1);
-
- expect(queryMock).toHaveBeenCalledWith({
- before,
- last: PAGE_SIZE,
- fullPath: projectPath,
- sort: DEFAULT_SORT,
- });
- });
- });
-
- describe('when the URL contains an "after" query parameter', () => {
- beforeEach(() => {
- mockQueryParams = { after };
- createComponent();
- });
-
- it('makes a request with the correct GraphQL query parameters', () => {
- expect(queryMock).toHaveBeenCalledTimes(2);
-
- expect(queryMock).toHaveBeenCalledWith({
- after,
- first: 1,
- fullPath: projectPath,
- sort: DEFAULT_SORT,
- });
-
- expect(queryMock).toHaveBeenCalledWith({
- after,
- first: PAGE_SIZE,
- fullPath: projectPath,
- sort: DEFAULT_SORT,
- });
- });
- });
-
- describe('when the URL contains both "before" and "after" query parameters', () => {
- beforeEach(() => {
- mockQueryParams = { before, after };
- createComponent();
- });
-
- it('ignores the "before" parameter and behaves as if only the "after" parameter was provided', () => {
- expect(queryMock).toHaveBeenCalledTimes(2);
-
- expect(queryMock).toHaveBeenCalledWith({
- after,
- first: 1,
- fullPath: projectPath,
- sort: DEFAULT_SORT,
- });
-
- expect(queryMock).toHaveBeenCalledWith({
- after,
- first: PAGE_SIZE,
- fullPath: projectPath,
- sort: DEFAULT_SORT,
- });
- });
- });
- });
-
- describe('New release button', () => {
- beforeEach(() => {
- createComponent();
- });
-
- it('renders the new release button with the correct href', () => {
- expect(findNewReleaseButton().attributes().href).toBe(newReleasePath);
- });
- });
-
- describe('pagination', () => {
- beforeEach(() => {
- mockQueryParams = { before };
- createComponent();
- });
-
- it('requeries the GraphQL endpoint when a pagination button is clicked', async () => {
- expect(queryMock.mock.calls).toEqual([[expect.objectContaining({ before })]]);
-
- mockQueryParams = { after };
- findPagination().vm.$emit('next', after);
-
- await nextTick();
-
- expect(queryMock.mock.calls).toEqual([
- [expect.objectContaining({ before })],
- [expect.objectContaining({ after })],
- [expect.objectContaining({ after })],
- ]);
- });
- });
-
- describe('sorting', () => {
- beforeEach(() => {
- createComponent();
- });
-
- it(`sorts by ${DEFAULT_SORT} by default`, () => {
- expect(queryMock.mock.calls).toEqual([
- [expect.objectContaining({ sort: DEFAULT_SORT })],
- [expect.objectContaining({ sort: DEFAULT_SORT })],
- ]);
- });
-
- it('requeries the GraphQL endpoint and updates the URL when the sort is changed', async () => {
- findSort().vm.$emit('input', CREATED_ASC);
-
- await nextTick();
-
- expect(queryMock.mock.calls).toEqual([
- [expect.objectContaining({ sort: DEFAULT_SORT })],
- [expect.objectContaining({ sort: DEFAULT_SORT })],
- [expect.objectContaining({ sort: CREATED_ASC })],
- [expect.objectContaining({ sort: CREATED_ASC })],
- ]);
-
- // URL manipulation is tested in more detail in the `describe` block below
- expect(historyPushState).toHaveBeenCalled();
- });
-
- it('does not requery the GraphQL endpoint or update the URL if the sort is updated to the same value', async () => {
- findSort().vm.$emit('input', DEFAULT_SORT);
-
- await nextTick();
-
- expect(queryMock.mock.calls).toEqual([
- [expect.objectContaining({ sort: DEFAULT_SORT })],
- [expect.objectContaining({ sort: DEFAULT_SORT })],
- ]);
-
- expect(historyPushState).not.toHaveBeenCalled();
- });
- });
-
- describe('sorting + pagination interaction', () => {
- const nonPaginationQueryParam = 'nonPaginationQueryParam';
-
- beforeEach(() => {
- historyPushState.mockImplementation((newUrl) => {
- mockQueryParams = Object.fromEntries(new URL(newUrl).searchParams);
- });
- });
-
- describe.each`
- queryParamsBefore | paramName | paramInitialValue
- ${{ before, nonPaginationQueryParam }} | ${'before'} | ${before}
- ${{ after, nonPaginationQueryParam }} | ${'after'} | ${after}
- `(
- 'when the URL contains a "$paramName" pagination cursor',
- ({ queryParamsBefore, paramName, paramInitialValue }) => {
- beforeEach(async () => {
- mockQueryParams = queryParamsBefore;
- createComponent();
-
- findSort().vm.$emit('input', CREATED_ASC);
-
- await nextTick();
- });
-
- it(`resets the page's "${paramName}" pagination cursor when the sort is changed`, () => {
- const firstRequestVariables = queryMock.mock.calls[0][0];
- // Might be request #2 or #3, depending on the pagination direction
- const mostRecentRequestVariables =
- queryMock.mock.calls[queryMock.mock.calls.length - 1][0];
-
- expect(firstRequestVariables[paramName]).toBe(paramInitialValue);
- expect(mostRecentRequestVariables[paramName]).toBeUndefined();
- });
-
- it(`updates the URL to not include the "${paramName}" URL query parameter`, () => {
- expect(historyPushState).toHaveBeenCalledTimes(1);
-
- const updatedUrlQueryParams = Object.fromEntries(
- new URL(historyPushState.mock.calls[0][0]).searchParams,
- );
-
- expect(updatedUrlQueryParams[paramName]).toBeUndefined();
- });
- },
- );
- });
-});
diff --git a/spec/frontend/releases/components/app_index_spec.js b/spec/frontend/releases/components/app_index_spec.js
index 43e88650ae3..63ce4c8bb17 100644
--- a/spec/frontend/releases/components/app_index_spec.js
+++ b/spec/frontend/releases/components/app_index_spec.js
@@ -1,50 +1,87 @@
-import { shallowMount } from '@vue/test-utils';
-import { merge } from 'lodash';
-import Vue from 'vue';
-import Vuex from 'vuex';
-import { getParameterByName } from '~/lib/utils/url_utility';
-import AppIndex from '~/releases/components/app_index.vue';
+import { cloneDeep } from 'lodash';
+import Vue, { nextTick } from 'vue';
+import VueApollo from 'vue-apollo';
+import originalAllReleasesQueryResponse from 'test_fixtures/graphql/releases/graphql/queries/all_releases.query.graphql.json';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import allReleasesQuery from '~/releases/graphql/queries/all_releases.query.graphql';
+import createFlash from '~/flash';
+import { historyPushState } from '~/lib/utils/common_utils';
+import ReleasesIndexApp from '~/releases/components/app_index.vue';
+import ReleaseBlock from '~/releases/components/release_block.vue';
import ReleaseSkeletonLoader from '~/releases/components/release_skeleton_loader.vue';
+import ReleasesEmptyState from '~/releases/components/releases_empty_state.vue';
import ReleasesPagination from '~/releases/components/releases_pagination.vue';
import ReleasesSort from '~/releases/components/releases_sort.vue';
+import { PAGE_SIZE, CREATED_ASC, DEFAULT_SORT } from '~/releases/constants';
+
+Vue.use(VueApollo);
+
+jest.mock('~/flash');
+
+let mockQueryParams;
+jest.mock('~/lib/utils/common_utils', () => ({
+ ...jest.requireActual('~/lib/utils/common_utils'),
+ historyPushState: jest.fn(),
+}));
jest.mock('~/lib/utils/url_utility', () => ({
...jest.requireActual('~/lib/utils/url_utility'),
- getParameterByName: jest.fn(),
+ getParameterByName: jest
+ .fn()
+ .mockImplementation((parameterName) => mockQueryParams[parameterName]),
}));
-Vue.use(Vuex);
-
describe('app_index.vue', () => {
+ const projectPath = 'project/path';
+ const newReleasePath = 'path/to/new/release/page';
+ const before = 'beforeCursor';
+ const after = 'afterCursor';
+
let wrapper;
- let fetchReleasesSpy;
- let urlParams;
-
- const createComponent = (storeUpdates) => {
- wrapper = shallowMount(AppIndex, {
- store: new Vuex.Store({
- modules: {
- index: merge(
- {
- namespaced: true,
- actions: {
- fetchReleases: fetchReleasesSpy,
- },
- state: {
- isLoading: true,
- releases: [],
- },
- },
- storeUpdates,
- ),
- },
- }),
+ let allReleases;
+ let singleRelease;
+ let noReleases;
+ let queryMock;
+
+ const createComponent = ({
+ singleResponse = Promise.resolve(singleRelease),
+ fullResponse = Promise.resolve(allReleases),
+ } = {}) => {
+ const apolloProvider = createMockApollo([
+ [
+ allReleasesQuery,
+ queryMock.mockImplementation((vars) => {
+ return vars.first === 1 ? singleResponse : fullResponse;
+ }),
+ ],
+ ]);
+
+ wrapper = shallowMountExtended(ReleasesIndexApp, {
+ apolloProvider,
+ provide: {
+ newReleasePath,
+ projectPath,
+ },
});
};
beforeEach(() => {
- fetchReleasesSpy = jest.fn();
- getParameterByName.mockImplementation((paramName) => urlParams[paramName]);
+ mockQueryParams = {};
+
+ allReleases = cloneDeep(originalAllReleasesQueryResponse);
+
+ singleRelease = cloneDeep(originalAllReleasesQueryResponse);
+ singleRelease.data.project.releases.nodes.splice(
+ 1,
+ singleRelease.data.project.releases.nodes.length,
+ );
+
+ noReleases = cloneDeep(originalAllReleasesQueryResponse);
+ noReleases.data.project.releases.nodes = [];
+
+ queryMock = jest.fn();
});
afterEach(() => {
@@ -52,120 +89,221 @@ describe('app_index.vue', () => {
});
// Finders
- const findLoadingIndicator = () => wrapper.find(ReleaseSkeletonLoader);
- const findEmptyState = () => wrapper.find('[data-testid="empty-state"]');
- const findSuccessState = () => wrapper.find('[data-testid="success-state"]');
- const findPagination = () => wrapper.find(ReleasesPagination);
- const findSortControls = () => wrapper.find(ReleasesSort);
- const findNewReleaseButton = () => wrapper.find('[data-testid="new-release-button"]');
-
- // Expectations
- const expectLoadingIndicator = (shouldExist) => {
- it(`${shouldExist ? 'renders' : 'does not render'} a loading indicator`, () => {
- expect(findLoadingIndicator().exists()).toBe(shouldExist);
- });
- };
+ const findLoadingIndicator = () => wrapper.findComponent(ReleaseSkeletonLoader);
+ const findEmptyState = () => wrapper.findComponent(ReleasesEmptyState);
+ const findNewReleaseButton = () => wrapper.findByText(ReleasesIndexApp.i18n.newRelease);
+ const findAllReleaseBlocks = () => wrapper.findAllComponents(ReleaseBlock);
+ const findPagination = () => wrapper.findComponent(ReleasesPagination);
+ const findSort = () => wrapper.findComponent(ReleasesSort);
- const expectEmptyState = (shouldExist) => {
- it(`${shouldExist ? 'renders' : 'does not render'} an empty state`, () => {
- expect(findEmptyState().exists()).toBe(shouldExist);
- });
- };
+ // Tests
+ describe('component states', () => {
+ // These need to be defined as functions, since `singleRelease` and
+ // `allReleases` are generated in a `beforeEach`, and therefore
+ // aren't available at test definition time.
+ const getInProgressResponse = () => new Promise(() => {});
+ const getErrorResponse = () => Promise.reject(new Error('Oops!'));
+ const getSingleRequestLoadedResponse = () => Promise.resolve(singleRelease);
+ const getFullRequestLoadedResponse = () => Promise.resolve(allReleases);
+ const getLoadedEmptyResponse = () => Promise.resolve(noReleases);
+
+ const toDescription = (bool) => (bool ? 'does' : 'does not');
+
+ describe.each`
+ description | singleResponseFn | fullResponseFn | loadingIndicator | emptyState | flashMessage | releaseCount | pagination
+ ${'both requests loading'} | ${getInProgressResponse} | ${getInProgressResponse} | ${true} | ${false} | ${false} | ${0} | ${false}
+ ${'both requests failed'} | ${getErrorResponse} | ${getErrorResponse} | ${false} | ${false} | ${true} | ${0} | ${false}
+ ${'both requests loaded'} | ${getSingleRequestLoadedResponse} | ${getFullRequestLoadedResponse} | ${false} | ${false} | ${false} | ${2} | ${true}
+ ${'both requests loaded with no results'} | ${getLoadedEmptyResponse} | ${getLoadedEmptyResponse} | ${false} | ${true} | ${false} | ${0} | ${false}
+ ${'single request loading, full request loaded'} | ${getInProgressResponse} | ${getFullRequestLoadedResponse} | ${false} | ${false} | ${false} | ${2} | ${true}
+ ${'single request loading, full request failed'} | ${getInProgressResponse} | ${getErrorResponse} | ${true} | ${false} | ${true} | ${0} | ${false}
+ ${'single request loaded, full request loading'} | ${getSingleRequestLoadedResponse} | ${getInProgressResponse} | ${true} | ${false} | ${false} | ${1} | ${false}
+ ${'single request loaded, full request failed'} | ${getSingleRequestLoadedResponse} | ${getErrorResponse} | ${false} | ${false} | ${true} | ${1} | ${false}
+ ${'single request failed, full request loading'} | ${getErrorResponse} | ${getInProgressResponse} | ${true} | ${false} | ${false} | ${0} | ${false}
+ ${'single request failed, full request loaded'} | ${getErrorResponse} | ${getFullRequestLoadedResponse} | ${false} | ${false} | ${false} | ${2} | ${true}
+ ${'single request loaded with no results, full request loading'} | ${getLoadedEmptyResponse} | ${getInProgressResponse} | ${true} | ${false} | ${false} | ${0} | ${false}
+ ${'single request loading, full request loadied with no results'} | ${getInProgressResponse} | ${getLoadedEmptyResponse} | ${false} | ${true} | ${false} | ${0} | ${false}
+ `(
+ '$description',
+ ({
+ singleResponseFn,
+ fullResponseFn,
+ loadingIndicator,
+ emptyState,
+ flashMessage,
+ releaseCount,
+ pagination,
+ }) => {
+ beforeEach(() => {
+ createComponent({
+ singleResponse: singleResponseFn(),
+ fullResponse: fullResponseFn(),
+ });
+ });
+
+ it(`${toDescription(loadingIndicator)} render a loading indicator`, async () => {
+ await waitForPromises();
+ expect(findLoadingIndicator().exists()).toBe(loadingIndicator);
+ });
+
+ it(`${toDescription(emptyState)} render an empty state`, () => {
+ expect(findEmptyState().exists()).toBe(emptyState);
+ });
+
+ it(`${toDescription(flashMessage)} show a flash message`, async () => {
+ await waitForPromises();
+ if (flashMessage) {
+ expect(createFlash).toHaveBeenCalledWith({
+ message: ReleasesIndexApp.i18n.errorMessage,
+ captureError: true,
+ error: expect.any(Error),
+ });
+ } else {
+ expect(createFlash).not.toHaveBeenCalled();
+ }
+ });
+
+ it(`renders ${releaseCount} release(s)`, () => {
+ expect(findAllReleaseBlocks()).toHaveLength(releaseCount);
+ });
+
+ it(`${toDescription(pagination)} render the pagination controls`, () => {
+ expect(findPagination().exists()).toBe(pagination);
+ });
+
+ it('does render the "New release" button', () => {
+ expect(findNewReleaseButton().exists()).toBe(true);
+ });
+
+ it('does render the sort controls', () => {
+ expect(findSort().exists()).toBe(true);
+ });
+ },
+ );
+ });
- const expectSuccessState = (shouldExist) => {
- it(`${shouldExist ? 'renders' : 'does not render'} the success state`, () => {
- expect(findSuccessState().exists()).toBe(shouldExist);
- });
- };
+ describe('URL parameters', () => {
+ describe('when the URL contains no query parameters', () => {
+ beforeEach(() => {
+ createComponent();
+ });
- const expectPagination = (shouldExist) => {
- it(`${shouldExist ? 'renders' : 'does not render'} the pagination controls`, () => {
- expect(findPagination().exists()).toBe(shouldExist);
- });
- };
+ it('makes a request with the correct GraphQL query parameters', () => {
+ expect(queryMock).toHaveBeenCalledTimes(2);
- const expectNewReleaseButton = (shouldExist) => {
- it(`${shouldExist ? 'renders' : 'does not render'} the "New release" button`, () => {
- expect(findNewReleaseButton().exists()).toBe(shouldExist);
- });
- };
+ expect(queryMock).toHaveBeenCalledWith({
+ first: 1,
+ fullPath: projectPath,
+ sort: DEFAULT_SORT,
+ });
- // Tests
- describe('on startup', () => {
- it.each`
- before | after
- ${null} | ${null}
- ${'before_param_value'} | ${null}
- ${null} | ${'after_param_value'}
- `(
- 'calls fetchRelease with the correct parameters based on the curent query parameters: before: $before, after: $after',
- ({ before, after }) => {
- urlParams = { before, after };
+ expect(queryMock).toHaveBeenCalledWith({
+ first: PAGE_SIZE,
+ fullPath: projectPath,
+ sort: DEFAULT_SORT,
+ });
+ });
+ });
+ describe('when the URL contains a "before" query parameter', () => {
+ beforeEach(() => {
+ mockQueryParams = { before };
createComponent();
+ });
- expect(fetchReleasesSpy).toHaveBeenCalledTimes(1);
- expect(fetchReleasesSpy).toHaveBeenCalledWith(expect.anything(), urlParams);
- },
- );
- });
+ it('makes a request with the correct GraphQL query parameters', () => {
+ expect(queryMock).toHaveBeenCalledTimes(1);
- describe('when the request to fetch releases has not yet completed', () => {
- beforeEach(() => {
- createComponent();
+ expect(queryMock).toHaveBeenCalledWith({
+ before,
+ last: PAGE_SIZE,
+ fullPath: projectPath,
+ sort: DEFAULT_SORT,
+ });
+ });
});
- expectLoadingIndicator(true);
- expectEmptyState(false);
- expectSuccessState(false);
- expectPagination(false);
- });
+ describe('when the URL contains an "after" query parameter', () => {
+ beforeEach(() => {
+ mockQueryParams = { after };
+ createComponent();
+ });
- describe('when the request fails', () => {
- beforeEach(() => {
- createComponent({
- state: {
- isLoading: false,
- hasError: true,
- },
+ it('makes a request with the correct GraphQL query parameters', () => {
+ expect(queryMock).toHaveBeenCalledTimes(2);
+
+ expect(queryMock).toHaveBeenCalledWith({
+ after,
+ first: 1,
+ fullPath: projectPath,
+ sort: DEFAULT_SORT,
+ });
+
+ expect(queryMock).toHaveBeenCalledWith({
+ after,
+ first: PAGE_SIZE,
+ fullPath: projectPath,
+ sort: DEFAULT_SORT,
+ });
});
});
- expectLoadingIndicator(false);
- expectEmptyState(false);
- expectSuccessState(false);
- expectPagination(true);
+ describe('when the URL contains both "before" and "after" query parameters', () => {
+ beforeEach(() => {
+ mockQueryParams = { before, after };
+ createComponent();
+ });
+
+ it('ignores the "before" parameter and behaves as if only the "after" parameter was provided', () => {
+ expect(queryMock).toHaveBeenCalledTimes(2);
+
+ expect(queryMock).toHaveBeenCalledWith({
+ after,
+ first: 1,
+ fullPath: projectPath,
+ sort: DEFAULT_SORT,
+ });
+
+ expect(queryMock).toHaveBeenCalledWith({
+ after,
+ first: PAGE_SIZE,
+ fullPath: projectPath,
+ sort: DEFAULT_SORT,
+ });
+ });
+ });
});
- describe('when the request succeeds but returns no releases', () => {
+ describe('New release button', () => {
beforeEach(() => {
- createComponent({
- state: {
- isLoading: false,
- },
- });
+ createComponent();
});
- expectLoadingIndicator(false);
- expectEmptyState(true);
- expectSuccessState(false);
- expectPagination(true);
+ it('renders the new release button with the correct href', () => {
+ expect(findNewReleaseButton().attributes().href).toBe(newReleasePath);
+ });
});
- describe('when the request succeeds and includes at least one release', () => {
+ describe('pagination', () => {
beforeEach(() => {
- createComponent({
- state: {
- isLoading: false,
- releases: [{}],
- },
- });
+ mockQueryParams = { before };
+ createComponent();
});
- expectLoadingIndicator(false);
- expectEmptyState(false);
- expectSuccessState(true);
- expectPagination(true);
+ it('requeries the GraphQL endpoint when a pagination button is clicked', async () => {
+ expect(queryMock.mock.calls).toEqual([[expect.objectContaining({ before })]]);
+
+ mockQueryParams = { after };
+ findPagination().vm.$emit('next', after);
+
+ await nextTick();
+
+ expect(queryMock.mock.calls).toEqual([
+ [expect.objectContaining({ before })],
+ [expect.objectContaining({ after })],
+ [expect.objectContaining({ after })],
+ ]);
+ });
});
describe('sorting', () => {
@@ -173,59 +311,88 @@ describe('app_index.vue', () => {
createComponent();
});
- it('renders the sort controls', () => {
- expect(findSortControls().exists()).toBe(true);
+ it(`sorts by ${DEFAULT_SORT} by default`, () => {
+ expect(queryMock.mock.calls).toEqual([
+ [expect.objectContaining({ sort: DEFAULT_SORT })],
+ [expect.objectContaining({ sort: DEFAULT_SORT })],
+ ]);
});
- it('calls the fetchReleases store method when the sort is updated', () => {
- fetchReleasesSpy.mockClear();
+ it('requeries the GraphQL endpoint and updates the URL when the sort is changed', async () => {
+ findSort().vm.$emit('input', CREATED_ASC);
+
+ await nextTick();
- findSortControls().vm.$emit('sort:changed');
+ expect(queryMock.mock.calls).toEqual([
+ [expect.objectContaining({ sort: DEFAULT_SORT })],
+ [expect.objectContaining({ sort: DEFAULT_SORT })],
+ [expect.objectContaining({ sort: CREATED_ASC })],
+ [expect.objectContaining({ sort: CREATED_ASC })],
+ ]);
- expect(fetchReleasesSpy).toHaveBeenCalledTimes(1);
+ // URL manipulation is tested in more detail in the `describe` block below
+ expect(historyPushState).toHaveBeenCalled();
});
- });
- describe('"New release" button', () => {
- describe('when the user is allowed to create releases', () => {
- const newReleasePath = 'path/to/new/release/page';
+ it('does not requery the GraphQL endpoint or update the URL if the sort is updated to the same value', async () => {
+ findSort().vm.$emit('input', DEFAULT_SORT);
- beforeEach(() => {
- createComponent({ state: { newReleasePath } });
- });
+ await nextTick();
- expectNewReleaseButton(true);
+ expect(queryMock.mock.calls).toEqual([
+ [expect.objectContaining({ sort: DEFAULT_SORT })],
+ [expect.objectContaining({ sort: DEFAULT_SORT })],
+ ]);
- it('renders the button with the correct href', () => {
- expect(findNewReleaseButton().attributes('href')).toBe(newReleasePath);
- });
+ expect(historyPushState).not.toHaveBeenCalled();
});
+ });
- describe('when the user is not allowed to create releases', () => {
- beforeEach(() => {
- createComponent();
- });
+ describe('sorting + pagination interaction', () => {
+ const nonPaginationQueryParam = 'nonPaginationQueryParam';
- expectNewReleaseButton(false);
+ beforeEach(() => {
+ historyPushState.mockImplementation((newUrl) => {
+ mockQueryParams = Object.fromEntries(new URL(newUrl).searchParams);
+ });
});
- });
- describe("when the browser's back button is pressed", () => {
- beforeEach(() => {
- urlParams = {
- before: 'before_param_value',
- };
+ describe.each`
+ queryParamsBefore | paramName | paramInitialValue
+ ${{ before, nonPaginationQueryParam }} | ${'before'} | ${before}
+ ${{ after, nonPaginationQueryParam }} | ${'after'} | ${after}
+ `(
+ 'when the URL contains a "$paramName" pagination cursor',
+ ({ queryParamsBefore, paramName, paramInitialValue }) => {
+ beforeEach(async () => {
+ mockQueryParams = queryParamsBefore;
+ createComponent();
- createComponent();
+ findSort().vm.$emit('input', CREATED_ASC);
- fetchReleasesSpy.mockClear();
+ await nextTick();
+ });
- window.dispatchEvent(new PopStateEvent('popstate'));
- });
+ it(`resets the page's "${paramName}" pagination cursor when the sort is changed`, () => {
+ const firstRequestVariables = queryMock.mock.calls[0][0];
+ // Might be request #2 or #3, depending on the pagination direction
+ const mostRecentRequestVariables =
+ queryMock.mock.calls[queryMock.mock.calls.length - 1][0];
- it('calls the fetchRelease store method with the parameters from the URL query', () => {
- expect(fetchReleasesSpy).toHaveBeenCalledTimes(1);
- expect(fetchReleasesSpy).toHaveBeenCalledWith(expect.anything(), urlParams);
- });
+ expect(firstRequestVariables[paramName]).toBe(paramInitialValue);
+ expect(mostRecentRequestVariables[paramName]).toBeUndefined();
+ });
+
+ it(`updates the URL to not include the "${paramName}" URL query parameter`, () => {
+ expect(historyPushState).toHaveBeenCalledTimes(1);
+
+ const updatedUrlQueryParams = Object.fromEntries(
+ new URL(historyPushState.mock.calls[0][0]).searchParams,
+ );
+
+ expect(updatedUrlQueryParams[paramName]).toBeUndefined();
+ });
+ },
+ );
});
});
diff --git a/spec/frontend/releases/components/app_show_spec.js b/spec/frontend/releases/components/app_show_spec.js
index 41c9746a363..c2ea6900d6e 100644
--- a/spec/frontend/releases/components/app_show_spec.js
+++ b/spec/frontend/releases/components/app_show_spec.js
@@ -143,6 +143,12 @@ describe('Release show component', () => {
describe('when the request succeeded, but the returned "project.release" key was null', () => {
beforeEach(async () => {
+ // As we return a release as `null`, Apollo also throws an error to the console
+ // about the missing field. We need to suppress console.error in order to check
+ // that flash message was called
+
+ // eslint-disable-next-line no-console
+ console.error = jest.fn();
const apolloProvider = createMockApollo([
[
oneReleaseQuery,
diff --git a/spec/frontend/releases/components/releases_pagination_apollo_client_spec.js b/spec/frontend/releases/components/releases_pagination_apollo_client_spec.js
deleted file mode 100644
index a538afd5d38..00000000000
--- a/spec/frontend/releases/components/releases_pagination_apollo_client_spec.js
+++ /dev/null
@@ -1,126 +0,0 @@
-import { mountExtended } from 'helpers/vue_test_utils_helper';
-import { historyPushState } from '~/lib/utils/common_utils';
-import ReleasesPaginationApolloClient from '~/releases/components/releases_pagination_apollo_client.vue';
-
-jest.mock('~/lib/utils/common_utils', () => ({
- ...jest.requireActual('~/lib/utils/common_utils'),
- historyPushState: jest.fn(),
-}));
-
-describe('releases_pagination_apollo_client.vue', () => {
- const startCursor = 'startCursor';
- const endCursor = 'endCursor';
- let wrapper;
- let onPrev;
- let onNext;
-
- const createComponent = (pageInfo) => {
- onPrev = jest.fn();
- onNext = jest.fn();
-
- wrapper = mountExtended(ReleasesPaginationApolloClient, {
- propsData: {
- pageInfo,
- },
- listeners: {
- prev: onPrev,
- next: onNext,
- },
- });
- };
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- const singlePageInfo = {
- hasPreviousPage: false,
- hasNextPage: false,
- startCursor,
- endCursor,
- };
-
- const onlyNextPageInfo = {
- hasPreviousPage: false,
- hasNextPage: true,
- startCursor,
- endCursor,
- };
-
- const onlyPrevPageInfo = {
- hasPreviousPage: true,
- hasNextPage: false,
- startCursor,
- endCursor,
- };
-
- const prevAndNextPageInfo = {
- hasPreviousPage: true,
- hasNextPage: true,
- startCursor,
- endCursor,
- };
-
- const findPrevButton = () => wrapper.findByTestId('prevButton');
- const findNextButton = () => wrapper.findByTestId('nextButton');
-
- describe.each`
- description | pageInfo | prevEnabled | nextEnabled
- ${'when there is only one page of results'} | ${singlePageInfo} | ${false} | ${false}
- ${'when there is a next page, but not a previous page'} | ${onlyNextPageInfo} | ${false} | ${true}
- ${'when there is a previous page, but not a next page'} | ${onlyPrevPageInfo} | ${true} | ${false}
- ${'when there is both a previous and next page'} | ${prevAndNextPageInfo} | ${true} | ${true}
- `('component states', ({ description, pageInfo, prevEnabled, nextEnabled }) => {
- describe(description, () => {
- beforeEach(() => {
- createComponent(pageInfo);
- });
-
- it(`renders the "Prev" button as ${prevEnabled ? 'enabled' : 'disabled'}`, () => {
- expect(findPrevButton().attributes().disabled).toBe(prevEnabled ? undefined : 'disabled');
- });
-
- it(`renders the "Next" button as ${nextEnabled ? 'enabled' : 'disabled'}`, () => {
- expect(findNextButton().attributes().disabled).toBe(nextEnabled ? undefined : 'disabled');
- });
- });
- });
-
- describe('button behavior', () => {
- beforeEach(() => {
- createComponent(prevAndNextPageInfo);
- });
-
- describe('next button behavior', () => {
- beforeEach(() => {
- findNextButton().trigger('click');
- });
-
- it('emits an "next" event with the "after" cursor', () => {
- expect(onNext.mock.calls).toEqual([[endCursor]]);
- });
-
- it('calls historyPushState with the new URL', () => {
- expect(historyPushState.mock.calls).toEqual([
- [expect.stringContaining(`?after=${endCursor}`)],
- ]);
- });
- });
-
- describe('prev button behavior', () => {
- beforeEach(() => {
- findPrevButton().trigger('click');
- });
-
- it('emits an "prev" event with the "before" cursor', () => {
- expect(onPrev.mock.calls).toEqual([[startCursor]]);
- });
-
- it('calls historyPushState with the new URL', () => {
- expect(historyPushState.mock.calls).toEqual([
- [expect.stringContaining(`?before=${startCursor}`)],
- ]);
- });
- });
- });
-});
diff --git a/spec/frontend/releases/components/releases_pagination_spec.js b/spec/frontend/releases/components/releases_pagination_spec.js
index b8c69b0ea70..59be808c802 100644
--- a/spec/frontend/releases/components/releases_pagination_spec.js
+++ b/spec/frontend/releases/components/releases_pagination_spec.js
@@ -1,140 +1,94 @@
-import { GlKeysetPagination } from '@gitlab/ui';
-import { mount } from '@vue/test-utils';
-import Vue from 'vue';
-import Vuex from 'vuex';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
import { historyPushState } from '~/lib/utils/common_utils';
import ReleasesPagination from '~/releases/components/releases_pagination.vue';
-import createStore from '~/releases/stores';
-import createIndexModule from '~/releases/stores/modules/index';
jest.mock('~/lib/utils/common_utils', () => ({
...jest.requireActual('~/lib/utils/common_utils'),
historyPushState: jest.fn(),
}));
-Vue.use(Vuex);
-
-describe('~/releases/components/releases_pagination.vue', () => {
+describe('releases_pagination.vue', () => {
+ const startCursor = 'startCursor';
+ const endCursor = 'endCursor';
let wrapper;
- let indexModule;
-
- const cursors = {
- startCursor: 'startCursor',
- endCursor: 'endCursor',
- };
-
- const projectPath = 'my/project';
+ let onPrev;
+ let onNext;
const createComponent = (pageInfo) => {
- indexModule = createIndexModule({ projectPath });
-
- indexModule.state.pageInfo = pageInfo;
-
- indexModule.actions.fetchReleases = jest.fn();
-
- wrapper = mount(ReleasesPagination, {
- store: createStore({
- modules: {
- index: indexModule,
- },
- featureFlags: {},
- }),
+ onPrev = jest.fn();
+ onNext = jest.fn();
+
+ wrapper = mountExtended(ReleasesPagination, {
+ propsData: {
+ pageInfo,
+ },
+ listeners: {
+ prev: onPrev,
+ next: onNext,
+ },
});
};
afterEach(() => {
wrapper.destroy();
- wrapper = null;
});
- const findGlKeysetPagination = () => wrapper.findComponent(GlKeysetPagination);
- const findPrevButton = () => findGlKeysetPagination().find('[data-testid="prevButton"]');
- const findNextButton = () => findGlKeysetPagination().find('[data-testid="nextButton"]');
-
- const expectDisabledPrev = () => {
- expect(findPrevButton().attributes().disabled).toBe('disabled');
+ const singlePageInfo = {
+ hasPreviousPage: false,
+ hasNextPage: false,
+ startCursor,
+ endCursor,
};
- const expectEnabledPrev = () => {
- expect(findPrevButton().attributes().disabled).toBe(undefined);
+
+ const onlyNextPageInfo = {
+ hasPreviousPage: false,
+ hasNextPage: true,
+ startCursor,
+ endCursor,
};
- const expectDisabledNext = () => {
- expect(findNextButton().attributes().disabled).toBe('disabled');
+
+ const onlyPrevPageInfo = {
+ hasPreviousPage: true,
+ hasNextPage: false,
+ startCursor,
+ endCursor,
};
- const expectEnabledNext = () => {
- expect(findNextButton().attributes().disabled).toBe(undefined);
+
+ const prevAndNextPageInfo = {
+ hasPreviousPage: true,
+ hasNextPage: true,
+ startCursor,
+ endCursor,
};
- describe('when there is only one page of results', () => {
- beforeEach(() => {
- createComponent({
- hasPreviousPage: false,
- hasNextPage: false,
+ const findPrevButton = () => wrapper.findByTestId('prevButton');
+ const findNextButton = () => wrapper.findByTestId('nextButton');
+
+ describe.each`
+ description | pageInfo | prevEnabled | nextEnabled
+ ${'when there is only one page of results'} | ${singlePageInfo} | ${false} | ${false}
+ ${'when there is a next page, but not a previous page'} | ${onlyNextPageInfo} | ${false} | ${true}
+ ${'when there is a previous page, but not a next page'} | ${onlyPrevPageInfo} | ${true} | ${false}
+ ${'when there is both a previous and next page'} | ${prevAndNextPageInfo} | ${true} | ${true}
+ `('component states', ({ description, pageInfo, prevEnabled, nextEnabled }) => {
+ describe(description, () => {
+ beforeEach(() => {
+ createComponent(pageInfo);
});
- });
-
- it('does not render a GlKeysetPagination', () => {
- expect(findGlKeysetPagination().exists()).toBe(false);
- });
- });
- describe('when there is a next page, but not a previous page', () => {
- beforeEach(() => {
- createComponent({
- hasPreviousPage: false,
- hasNextPage: true,
+ it(`renders the "Prev" button as ${prevEnabled ? 'enabled' : 'disabled'}`, () => {
+ expect(findPrevButton().attributes().disabled).toBe(prevEnabled ? undefined : 'disabled');
});
- });
-
- it('renders a disabled "Prev" button', () => {
- expectDisabledPrev();
- });
- it('renders an enabled "Next" button', () => {
- expectEnabledNext();
- });
- });
-
- describe('when there is a previous page, but not a next page', () => {
- beforeEach(() => {
- createComponent({
- hasPreviousPage: true,
- hasNextPage: false,
- });
- });
-
- it('renders a enabled "Prev" button', () => {
- expectEnabledPrev();
- });
-
- it('renders an disabled "Next" button', () => {
- expectDisabledNext();
- });
- });
-
- describe('when there is both a previous page and a next page', () => {
- beforeEach(() => {
- createComponent({
- hasPreviousPage: true,
- hasNextPage: true,
+ it(`renders the "Next" button as ${nextEnabled ? 'enabled' : 'disabled'}`, () => {
+ expect(findNextButton().attributes().disabled).toBe(nextEnabled ? undefined : 'disabled');
});
});
-
- it('renders a enabled "Prev" button', () => {
- expectEnabledPrev();
- });
-
- it('renders an enabled "Next" button', () => {
- expectEnabledNext();
- });
});
describe('button behavior', () => {
beforeEach(() => {
- createComponent({
- hasPreviousPage: true,
- hasNextPage: true,
- ...cursors,
- });
+ createComponent(prevAndNextPageInfo);
});
describe('next button behavior', () => {
@@ -142,33 +96,29 @@ describe('~/releases/components/releases_pagination.vue', () => {
findNextButton().trigger('click');
});
- it('calls fetchReleases with the correct after cursor', () => {
- expect(indexModule.actions.fetchReleases.mock.calls).toEqual([
- [expect.anything(), { after: cursors.endCursor }],
- ]);
+ it('emits an "next" event with the "after" cursor', () => {
+ expect(onNext.mock.calls).toEqual([[endCursor]]);
});
it('calls historyPushState with the new URL', () => {
expect(historyPushState.mock.calls).toEqual([
- [expect.stringContaining(`?after=${cursors.endCursor}`)],
+ [expect.stringContaining(`?after=${endCursor}`)],
]);
});
});
- describe('previous button behavior', () => {
+ describe('prev button behavior', () => {
beforeEach(() => {
findPrevButton().trigger('click');
});
- it('calls fetchReleases with the correct before cursor', () => {
- expect(indexModule.actions.fetchReleases.mock.calls).toEqual([
- [expect.anything(), { before: cursors.startCursor }],
- ]);
+ it('emits an "prev" event with the "before" cursor', () => {
+ expect(onPrev.mock.calls).toEqual([[startCursor]]);
});
it('calls historyPushState with the new URL', () => {
expect(historyPushState.mock.calls).toEqual([
- [expect.stringContaining(`?before=${cursors.startCursor}`)],
+ [expect.stringContaining(`?before=${startCursor}`)],
]);
});
});
diff --git a/spec/frontend/releases/components/releases_sort_apollo_client_spec.js b/spec/frontend/releases/components/releases_sort_apollo_client_spec.js
deleted file mode 100644
index d93a932af01..00000000000
--- a/spec/frontend/releases/components/releases_sort_apollo_client_spec.js
+++ /dev/null
@@ -1,103 +0,0 @@
-import { GlSorting, GlSortingItem } from '@gitlab/ui';
-import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import ReleasesSortApolloClient from '~/releases/components/releases_sort_apollo_client.vue';
-import { RELEASED_AT_ASC, RELEASED_AT_DESC, CREATED_ASC, CREATED_DESC } from '~/releases/constants';
-
-describe('releases_sort_apollo_client.vue', () => {
- let wrapper;
-
- const createComponent = (valueProp = RELEASED_AT_ASC) => {
- wrapper = shallowMountExtended(ReleasesSortApolloClient, {
- propsData: {
- value: valueProp,
- },
- stubs: {
- GlSortingItem,
- },
- });
- };
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- const findSorting = () => wrapper.findComponent(GlSorting);
- const findSortingItems = () => wrapper.findAllComponents(GlSortingItem);
- const findReleasedDateItem = () =>
- findSortingItems().wrappers.find((item) => item.text() === 'Released date');
- const findCreatedDateItem = () =>
- findSortingItems().wrappers.find((item) => item.text() === 'Created date');
- const getSortingItemsInfo = () =>
- findSortingItems().wrappers.map((item) => ({
- label: item.text(),
- active: item.attributes().active === 'true',
- }));
-
- describe.each`
- valueProp | text | isAscending | items
- ${RELEASED_AT_ASC} | ${'Released date'} | ${true} | ${[{ label: 'Released date', active: true }, { label: 'Created date', active: false }]}
- ${RELEASED_AT_DESC} | ${'Released date'} | ${false} | ${[{ label: 'Released date', active: true }, { label: 'Created date', active: false }]}
- ${CREATED_ASC} | ${'Created date'} | ${true} | ${[{ label: 'Released date', active: false }, { label: 'Created date', active: true }]}
- ${CREATED_DESC} | ${'Created date'} | ${false} | ${[{ label: 'Released date', active: false }, { label: 'Created date', active: true }]}
- `('component states', ({ valueProp, text, isAscending, items }) => {
- beforeEach(() => {
- createComponent(valueProp);
- });
-
- it(`when the sort is ${valueProp}, provides the GlSorting with the props text="${text}" and isAscending=${isAscending}`, () => {
- expect(findSorting().props()).toEqual(
- expect.objectContaining({
- text,
- isAscending,
- }),
- );
- });
-
- it(`when the sort is ${valueProp}, renders the expected dropdown items`, () => {
- expect(getSortingItemsInfo()).toEqual(items);
- });
- });
-
- const clickReleasedDateItem = () => findReleasedDateItem().vm.$emit('click');
- const clickCreatedDateItem = () => findCreatedDateItem().vm.$emit('click');
- const clickSortDirectionButton = () => findSorting().vm.$emit('sortDirectionChange');
-
- const releasedAtDropdownItemDescription = 'released at dropdown item';
- const createdAtDropdownItemDescription = 'created at dropdown item';
- const sortDirectionButtonDescription = 'sort direction button';
-
- describe.each`
- initialValueProp | itemClickFn | itemToClickDescription | emittedEvent
- ${RELEASED_AT_ASC} | ${clickReleasedDateItem} | ${releasedAtDropdownItemDescription} | ${undefined}
- ${RELEASED_AT_ASC} | ${clickCreatedDateItem} | ${createdAtDropdownItemDescription} | ${CREATED_ASC}
- ${RELEASED_AT_ASC} | ${clickSortDirectionButton} | ${sortDirectionButtonDescription} | ${RELEASED_AT_DESC}
- ${RELEASED_AT_DESC} | ${clickReleasedDateItem} | ${releasedAtDropdownItemDescription} | ${undefined}
- ${RELEASED_AT_DESC} | ${clickCreatedDateItem} | ${createdAtDropdownItemDescription} | ${CREATED_DESC}
- ${RELEASED_AT_DESC} | ${clickSortDirectionButton} | ${sortDirectionButtonDescription} | ${RELEASED_AT_ASC}
- ${CREATED_ASC} | ${clickReleasedDateItem} | ${releasedAtDropdownItemDescription} | ${RELEASED_AT_ASC}
- ${CREATED_ASC} | ${clickCreatedDateItem} | ${createdAtDropdownItemDescription} | ${undefined}
- ${CREATED_ASC} | ${clickSortDirectionButton} | ${sortDirectionButtonDescription} | ${CREATED_DESC}
- ${CREATED_DESC} | ${clickReleasedDateItem} | ${releasedAtDropdownItemDescription} | ${RELEASED_AT_DESC}
- ${CREATED_DESC} | ${clickCreatedDateItem} | ${createdAtDropdownItemDescription} | ${undefined}
- ${CREATED_DESC} | ${clickSortDirectionButton} | ${sortDirectionButtonDescription} | ${CREATED_ASC}
- `('input event', ({ initialValueProp, itemClickFn, itemToClickDescription, emittedEvent }) => {
- beforeEach(() => {
- createComponent(initialValueProp);
- itemClickFn();
- });
-
- it(`emits ${
- emittedEvent || 'nothing'
- } when value prop is ${initialValueProp} and the ${itemToClickDescription} is clicked`, () => {
- expect(wrapper.emitted().input?.[0]?.[0]).toEqual(emittedEvent);
- });
- });
-
- describe('prop validation', () => {
- it('validates that the `value` prop is one of the expected sort strings', () => {
- expect(() => {
- createComponent('not a valid value');
- }).toThrow('Invalid prop: custom validator check failed');
- });
- });
-});
diff --git a/spec/frontend/releases/components/releases_sort_spec.js b/spec/frontend/releases/components/releases_sort_spec.js
index 7774532bc12..c6e1846d252 100644
--- a/spec/frontend/releases/components/releases_sort_spec.js
+++ b/spec/frontend/releases/components/releases_sort_spec.js
@@ -1,65 +1,103 @@
import { GlSorting, GlSortingItem } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
-import Vue from 'vue';
-import Vuex from 'vuex';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import ReleasesSort from '~/releases/components/releases_sort.vue';
-import createStore from '~/releases/stores';
-import createIndexModule from '~/releases/stores/modules/index';
+import { RELEASED_AT_ASC, RELEASED_AT_DESC, CREATED_ASC, CREATED_DESC } from '~/releases/constants';
-Vue.use(Vuex);
-
-describe('~/releases/components/releases_sort.vue', () => {
+describe('releases_sort.vue', () => {
let wrapper;
- let store;
- let indexModule;
- const projectId = 8;
-
- const createComponent = () => {
- indexModule = createIndexModule({ projectId });
- store = createStore({
- modules: {
- index: indexModule,
+ const createComponent = (valueProp = RELEASED_AT_ASC) => {
+ wrapper = shallowMountExtended(ReleasesSort, {
+ propsData: {
+ value: valueProp,
},
- });
-
- store.dispatch = jest.fn();
-
- wrapper = shallowMount(ReleasesSort, {
- store,
stubs: {
GlSortingItem,
},
});
};
- const findReleasesSorting = () => wrapper.find(GlSorting);
- const findSortingItems = () => wrapper.findAll(GlSortingItem);
-
afterEach(() => {
wrapper.destroy();
- wrapper = null;
});
- beforeEach(() => {
- createComponent();
- });
+ const findSorting = () => wrapper.findComponent(GlSorting);
+ const findSortingItems = () => wrapper.findAllComponents(GlSortingItem);
+ const findReleasedDateItem = () =>
+ findSortingItems().wrappers.find((item) => item.text() === 'Released date');
+ const findCreatedDateItem = () =>
+ findSortingItems().wrappers.find((item) => item.text() === 'Created date');
+ const getSortingItemsInfo = () =>
+ findSortingItems().wrappers.map((item) => ({
+ label: item.text(),
+ active: item.attributes().active === 'true',
+ }));
+
+ describe.each`
+ valueProp | text | isAscending | items
+ ${RELEASED_AT_ASC} | ${'Released date'} | ${true} | ${[{ label: 'Released date', active: true }, { label: 'Created date', active: false }]}
+ ${RELEASED_AT_DESC} | ${'Released date'} | ${false} | ${[{ label: 'Released date', active: true }, { label: 'Created date', active: false }]}
+ ${CREATED_ASC} | ${'Created date'} | ${true} | ${[{ label: 'Released date', active: false }, { label: 'Created date', active: true }]}
+ ${CREATED_DESC} | ${'Created date'} | ${false} | ${[{ label: 'Released date', active: false }, { label: 'Created date', active: true }]}
+ `('component states', ({ valueProp, text, isAscending, items }) => {
+ beforeEach(() => {
+ createComponent(valueProp);
+ });
- it('has all the sortable items', () => {
- expect(findSortingItems()).toHaveLength(wrapper.vm.sortOptions.length);
+ it(`when the sort is ${valueProp}, provides the GlSorting with the props text="${text}" and isAscending=${isAscending}`, () => {
+ expect(findSorting().props()).toEqual(
+ expect.objectContaining({
+ text,
+ isAscending,
+ }),
+ );
+ });
+
+ it(`when the sort is ${valueProp}, renders the expected dropdown items`, () => {
+ expect(getSortingItemsInfo()).toEqual(items);
+ });
});
- it('on sort change set sorting in vuex and emit event', () => {
- findReleasesSorting().vm.$emit('sortDirectionChange');
- expect(store.dispatch).toHaveBeenCalledWith('index/setSorting', { sort: 'asc' });
- expect(wrapper.emitted('sort:changed')).toBeTruthy();
+ const clickReleasedDateItem = () => findReleasedDateItem().vm.$emit('click');
+ const clickCreatedDateItem = () => findCreatedDateItem().vm.$emit('click');
+ const clickSortDirectionButton = () => findSorting().vm.$emit('sortDirectionChange');
+
+ const releasedAtDropdownItemDescription = 'released at dropdown item';
+ const createdAtDropdownItemDescription = 'created at dropdown item';
+ const sortDirectionButtonDescription = 'sort direction button';
+
+ describe.each`
+ initialValueProp | itemClickFn | itemToClickDescription | emittedEvent
+ ${RELEASED_AT_ASC} | ${clickReleasedDateItem} | ${releasedAtDropdownItemDescription} | ${undefined}
+ ${RELEASED_AT_ASC} | ${clickCreatedDateItem} | ${createdAtDropdownItemDescription} | ${CREATED_ASC}
+ ${RELEASED_AT_ASC} | ${clickSortDirectionButton} | ${sortDirectionButtonDescription} | ${RELEASED_AT_DESC}
+ ${RELEASED_AT_DESC} | ${clickReleasedDateItem} | ${releasedAtDropdownItemDescription} | ${undefined}
+ ${RELEASED_AT_DESC} | ${clickCreatedDateItem} | ${createdAtDropdownItemDescription} | ${CREATED_DESC}
+ ${RELEASED_AT_DESC} | ${clickSortDirectionButton} | ${sortDirectionButtonDescription} | ${RELEASED_AT_ASC}
+ ${CREATED_ASC} | ${clickReleasedDateItem} | ${releasedAtDropdownItemDescription} | ${RELEASED_AT_ASC}
+ ${CREATED_ASC} | ${clickCreatedDateItem} | ${createdAtDropdownItemDescription} | ${undefined}
+ ${CREATED_ASC} | ${clickSortDirectionButton} | ${sortDirectionButtonDescription} | ${CREATED_DESC}
+ ${CREATED_DESC} | ${clickReleasedDateItem} | ${releasedAtDropdownItemDescription} | ${RELEASED_AT_DESC}
+ ${CREATED_DESC} | ${clickCreatedDateItem} | ${createdAtDropdownItemDescription} | ${undefined}
+ ${CREATED_DESC} | ${clickSortDirectionButton} | ${sortDirectionButtonDescription} | ${CREATED_ASC}
+ `('input event', ({ initialValueProp, itemClickFn, itemToClickDescription, emittedEvent }) => {
+ beforeEach(() => {
+ createComponent(initialValueProp);
+ itemClickFn();
+ });
+
+ it(`emits ${
+ emittedEvent || 'nothing'
+ } when value prop is ${initialValueProp} and the ${itemToClickDescription} is clicked`, () => {
+ expect(wrapper.emitted().input?.[0]?.[0]).toEqual(emittedEvent);
+ });
});
- it('on sort item click set sorting and emit event', () => {
- const item = findSortingItems().at(0);
- const { orderBy } = wrapper.vm.sortOptions[0];
- item.vm.$emit('click');
- expect(store.dispatch).toHaveBeenCalledWith('index/setSorting', { orderBy });
- expect(wrapper.emitted('sort:changed')).toBeTruthy();
+ describe('prop validation', () => {
+ it('validates that the `value` prop is one of the expected sort strings', () => {
+ expect(() => {
+ createComponent('not a valid value');
+ }).toThrow('Invalid prop: custom validator check failed');
+ });
});
});
diff --git a/spec/frontend/releases/stores/modules/list/actions_spec.js b/spec/frontend/releases/stores/modules/list/actions_spec.js
deleted file mode 100644
index 91406f7e2f4..00000000000
--- a/spec/frontend/releases/stores/modules/list/actions_spec.js
+++ /dev/null
@@ -1,197 +0,0 @@
-import { cloneDeep } from 'lodash';
-import originalGraphqlReleasesResponse from 'test_fixtures/graphql/releases/graphql/queries/all_releases.query.graphql.json';
-import testAction from 'helpers/vuex_action_helper';
-import { PAGE_SIZE } from '~/releases/constants';
-import allReleasesQuery from '~/releases/graphql/queries/all_releases.query.graphql';
-import {
- fetchReleases,
- receiveReleasesError,
- setSorting,
-} from '~/releases/stores/modules/index/actions';
-import * as types from '~/releases/stores/modules/index/mutation_types';
-import createState from '~/releases/stores/modules/index/state';
-import { gqClient, convertAllReleasesGraphQLResponse } from '~/releases/util';
-
-describe('Releases State actions', () => {
- let mockedState;
- let graphqlReleasesResponse;
-
- const projectPath = 'root/test-project';
- const projectId = 19;
- const before = 'testBeforeCursor';
- const after = 'testAfterCursor';
-
- beforeEach(() => {
- mockedState = {
- ...createState({
- projectId,
- projectPath,
- }),
- };
-
- graphqlReleasesResponse = cloneDeep(originalGraphqlReleasesResponse);
- });
-
- describe('fetchReleases', () => {
- describe('GraphQL query variables', () => {
- let vuexParams;
-
- beforeEach(() => {
- jest.spyOn(gqClient, 'query');
-
- vuexParams = { dispatch: jest.fn(), commit: jest.fn(), state: mockedState };
- });
-
- describe('when neither a before nor an after parameter is provided', () => {
- beforeEach(() => {
- fetchReleases(vuexParams, { before: undefined, after: undefined });
- });
-
- it('makes a GraphQl query with a first variable', () => {
- expect(gqClient.query).toHaveBeenCalledWith({
- query: allReleasesQuery,
- variables: { fullPath: projectPath, first: PAGE_SIZE, sort: 'RELEASED_AT_DESC' },
- });
- });
- });
-
- describe('when only a before parameter is provided', () => {
- beforeEach(() => {
- fetchReleases(vuexParams, { before, after: undefined });
- });
-
- it('makes a GraphQl query with last and before variables', () => {
- expect(gqClient.query).toHaveBeenCalledWith({
- query: allReleasesQuery,
- variables: { fullPath: projectPath, last: PAGE_SIZE, before, sort: 'RELEASED_AT_DESC' },
- });
- });
- });
-
- describe('when only an after parameter is provided', () => {
- beforeEach(() => {
- fetchReleases(vuexParams, { before: undefined, after });
- });
-
- it('makes a GraphQl query with first and after variables', () => {
- expect(gqClient.query).toHaveBeenCalledWith({
- query: allReleasesQuery,
- variables: { fullPath: projectPath, first: PAGE_SIZE, after, sort: 'RELEASED_AT_DESC' },
- });
- });
- });
-
- describe('when both before and after parameters are provided', () => {
- it('throws an error', () => {
- const callFetchReleases = () => {
- fetchReleases(vuexParams, { before, after });
- };
-
- expect(callFetchReleases).toThrowError(
- 'Both a `before` and an `after` parameter were provided to fetchReleases. These parameters cannot be used together.',
- );
- });
- });
-
- describe('when the sort parameters are provided', () => {
- it.each`
- sort | orderBy | ReleaseSort
- ${'asc'} | ${'released_at'} | ${'RELEASED_AT_ASC'}
- ${'desc'} | ${'released_at'} | ${'RELEASED_AT_DESC'}
- ${'asc'} | ${'created_at'} | ${'CREATED_ASC'}
- ${'desc'} | ${'created_at'} | ${'CREATED_DESC'}
- `(
- 'correctly sets $ReleaseSort based on $sort and $orderBy',
- ({ sort, orderBy, ReleaseSort }) => {
- mockedState.sorting.sort = sort;
- mockedState.sorting.orderBy = orderBy;
-
- fetchReleases(vuexParams, { before: undefined, after: undefined });
-
- expect(gqClient.query).toHaveBeenCalledWith({
- query: allReleasesQuery,
- variables: { fullPath: projectPath, first: PAGE_SIZE, sort: ReleaseSort },
- });
- },
- );
- });
- });
-
- describe('when the request is successful', () => {
- beforeEach(() => {
- jest.spyOn(gqClient, 'query').mockResolvedValue(graphqlReleasesResponse);
- });
-
- it(`commits ${types.REQUEST_RELEASES} and ${types.RECEIVE_RELEASES_SUCCESS}`, () => {
- const convertedResponse = convertAllReleasesGraphQLResponse(graphqlReleasesResponse);
-
- return testAction(
- fetchReleases,
- {},
- mockedState,
- [
- {
- type: types.REQUEST_RELEASES,
- },
- {
- type: types.RECEIVE_RELEASES_SUCCESS,
- payload: {
- data: convertedResponse.data,
- pageInfo: convertedResponse.paginationInfo,
- },
- },
- ],
- [],
- );
- });
- });
-
- describe('when the request fails', () => {
- beforeEach(() => {
- jest.spyOn(gqClient, 'query').mockRejectedValue(new Error('Something went wrong!'));
- });
-
- it(`commits ${types.REQUEST_RELEASES} and dispatch receiveReleasesError`, () => {
- return testAction(
- fetchReleases,
- {},
- mockedState,
- [
- {
- type: types.REQUEST_RELEASES,
- },
- ],
- [
- {
- type: 'receiveReleasesError',
- },
- ],
- );
- });
- });
- });
-
- describe('receiveReleasesError', () => {
- it('should commit RECEIVE_RELEASES_ERROR mutation', () => {
- return testAction(
- receiveReleasesError,
- null,
- mockedState,
- [{ type: types.RECEIVE_RELEASES_ERROR }],
- [],
- );
- });
- });
-
- describe('setSorting', () => {
- it('should commit SET_SORTING', () => {
- return testAction(
- setSorting,
- { orderBy: 'released_at', sort: 'asc' },
- null,
- [{ type: types.SET_SORTING, payload: { orderBy: 'released_at', sort: 'asc' } }],
- [],
- );
- });
- });
-});
diff --git a/spec/frontend/releases/stores/modules/list/helpers.js b/spec/frontend/releases/stores/modules/list/helpers.js
deleted file mode 100644
index 6669f44aa95..00000000000
--- a/spec/frontend/releases/stores/modules/list/helpers.js
+++ /dev/null
@@ -1,5 +0,0 @@
-import state from '~/releases/stores/modules/index/state';
-
-export const resetStore = (store) => {
- store.replaceState(state());
-};
diff --git a/spec/frontend/releases/stores/modules/list/mutations_spec.js b/spec/frontend/releases/stores/modules/list/mutations_spec.js
deleted file mode 100644
index 49e324c28a5..00000000000
--- a/spec/frontend/releases/stores/modules/list/mutations_spec.js
+++ /dev/null
@@ -1,81 +0,0 @@
-import originalRelease from 'test_fixtures/api/releases/release.json';
-import graphqlReleasesResponse from 'test_fixtures/graphql/releases/graphql/queries/all_releases.query.graphql.json';
-import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
-import * as types from '~/releases/stores/modules/index/mutation_types';
-import mutations from '~/releases/stores/modules/index/mutations';
-import createState from '~/releases/stores/modules/index/state';
-import { convertAllReleasesGraphQLResponse } from '~/releases/util';
-
-const originalReleases = [originalRelease];
-
-describe('Releases Store Mutations', () => {
- let stateCopy;
- let pageInfo;
- let releases;
-
- beforeEach(() => {
- stateCopy = createState({});
- pageInfo = convertAllReleasesGraphQLResponse(graphqlReleasesResponse).paginationInfo;
- releases = convertObjectPropsToCamelCase(originalReleases, { deep: true });
- });
-
- describe('REQUEST_RELEASES', () => {
- it('sets isLoading to true', () => {
- mutations[types.REQUEST_RELEASES](stateCopy);
-
- expect(stateCopy.isLoading).toEqual(true);
- });
- });
-
- describe('RECEIVE_RELEASES_SUCCESS', () => {
- beforeEach(() => {
- mutations[types.RECEIVE_RELEASES_SUCCESS](stateCopy, {
- pageInfo,
- data: releases,
- });
- });
-
- it('sets is loading to false', () => {
- expect(stateCopy.isLoading).toEqual(false);
- });
-
- it('sets hasError to false', () => {
- expect(stateCopy.hasError).toEqual(false);
- });
-
- it('sets data', () => {
- expect(stateCopy.releases).toEqual(releases);
- });
-
- it('sets pageInfo', () => {
- expect(stateCopy.pageInfo).toEqual(pageInfo);
- });
- });
-
- describe('RECEIVE_RELEASES_ERROR', () => {
- it('resets data', () => {
- mutations[types.RECEIVE_RELEASES_SUCCESS](stateCopy, {
- pageInfo,
- data: releases,
- });
-
- mutations[types.RECEIVE_RELEASES_ERROR](stateCopy);
-
- expect(stateCopy.isLoading).toEqual(false);
- expect(stateCopy.releases).toEqual([]);
- expect(stateCopy.pageInfo).toEqual({});
- });
- });
-
- describe('SET_SORTING', () => {
- it('should merge the sorting object with sort value', () => {
- mutations[types.SET_SORTING](stateCopy, { sort: 'asc' });
- expect(stateCopy.sorting).toEqual({ ...stateCopy.sorting, sort: 'asc' });
- });
-
- it('should merge the sorting object with order_by value', () => {
- mutations[types.SET_SORTING](stateCopy, { orderBy: 'created_at' });
- expect(stateCopy.sorting).toEqual({ ...stateCopy.sorting, orderBy: 'created_at' });
- });
- });
-});
diff --git a/spec/frontend/reports/accessibility_report/store/actions_spec.js b/spec/frontend/reports/accessibility_report/store/actions_spec.js
index 46dbe1ff7a1..bab6c4905a7 100644
--- a/spec/frontend/reports/accessibility_report/store/actions_spec.js
+++ b/spec/frontend/reports/accessibility_report/store/actions_spec.js
@@ -17,16 +17,15 @@ describe('Accessibility Reports actions', () => {
});
describe('setEndpoints', () => {
- it('should commit SET_ENDPOINTS mutation', (done) => {
+ it('should commit SET_ENDPOINTS mutation', () => {
const endpoint = 'endpoint.json';
- testAction(
+ return testAction(
actions.setEndpoint,
endpoint,
localState,
[{ type: types.SET_ENDPOINT, payload: endpoint }],
[],
- done,
);
});
});
@@ -46,11 +45,11 @@ describe('Accessibility Reports actions', () => {
});
describe('success', () => {
- it('should commit REQUEST_REPORT mutation and dispatch receiveReportSuccess', (done) => {
+ it('should commit REQUEST_REPORT mutation and dispatch receiveReportSuccess', () => {
const data = { report: { summary: {} } };
mock.onGet(`${TEST_HOST}/endpoint.json`).reply(200, data);
- testAction(
+ return testAction(
actions.fetchReport,
null,
localState,
@@ -61,60 +60,55 @@ describe('Accessibility Reports actions', () => {
type: 'receiveReportSuccess',
},
],
- done,
);
});
});
describe('error', () => {
- it('should commit REQUEST_REPORT and RECEIVE_REPORT_ERROR mutations', (done) => {
+ it('should commit REQUEST_REPORT and RECEIVE_REPORT_ERROR mutations', () => {
mock.onGet(`${TEST_HOST}/endpoint.json`).reply(500);
- testAction(
+ return testAction(
actions.fetchReport,
null,
localState,
[{ type: types.REQUEST_REPORT }],
[{ type: 'receiveReportError' }],
- done,
);
});
});
});
describe('receiveReportSuccess', () => {
- it('should commit RECEIVE_REPORT_SUCCESS mutation with 200', (done) => {
- testAction(
+ it('should commit RECEIVE_REPORT_SUCCESS mutation with 200', () => {
+ return testAction(
actions.receiveReportSuccess,
{ status: 200, data: mockReport },
localState,
[{ type: types.RECEIVE_REPORT_SUCCESS, payload: mockReport }],
[{ type: 'stopPolling' }],
- done,
);
});
- it('should not commit RECEIVE_REPORTS_SUCCESS mutation with 204', (done) => {
- testAction(
+ it('should not commit RECEIVE_REPORTS_SUCCESS mutation with 204', () => {
+ return testAction(
actions.receiveReportSuccess,
{ status: 204, data: mockReport },
localState,
[],
[],
- done,
);
});
});
describe('receiveReportError', () => {
- it('should commit RECEIVE_REPORT_ERROR mutation', (done) => {
- testAction(
+ it('should commit RECEIVE_REPORT_ERROR mutation', () => {
+ return testAction(
actions.receiveReportError,
null,
localState,
[{ type: types.RECEIVE_REPORT_ERROR }],
[{ type: 'stopPolling' }],
- done,
);
});
});
diff --git a/spec/frontend/reports/codequality_report/components/codequality_issue_body_spec.js b/spec/frontend/reports/codequality_report/components/codequality_issue_body_spec.js
index c548007a8a6..17f07ac2b8f 100644
--- a/spec/frontend/reports/codequality_report/components/codequality_issue_body_spec.js
+++ b/spec/frontend/reports/codequality_report/components/codequality_issue_body_spec.js
@@ -51,6 +51,7 @@ describe('code quality issue body issue body', () => {
${'blocker'} | ${'text-danger-800'} | ${'severity-critical'}
${'unknown'} | ${'text-secondary-400'} | ${'severity-unknown'}
${'invalid'} | ${'text-secondary-400'} | ${'severity-unknown'}
+ ${undefined} | ${'text-secondary-400'} | ${'severity-unknown'}
`(
'renders correct icon for "$severity" severity rating',
({ severity, iconClass, iconName }) => {
diff --git a/spec/frontend/reports/codequality_report/grouped_codequality_reports_app_spec.js b/spec/frontend/reports/codequality_report/grouped_codequality_reports_app_spec.js
index 1f923f41274..b61b65c2713 100644
--- a/spec/frontend/reports/codequality_report/grouped_codequality_reports_app_spec.js
+++ b/spec/frontend/reports/codequality_report/grouped_codequality_reports_app_spec.js
@@ -135,7 +135,7 @@ describe('Grouped code quality reports app', () => {
});
it('does not render a help icon', () => {
- expect(findWidget().find('[data-testid="question-icon"]').exists()).toBe(false);
+ expect(findWidget().find('[data-testid="question-o-icon"]').exists()).toBe(false);
});
describe('when base report was not found', () => {
@@ -144,7 +144,7 @@ describe('Grouped code quality reports app', () => {
});
it('renders a help icon with more information', () => {
- expect(findWidget().find('[data-testid="question-icon"]').exists()).toBe(true);
+ expect(findWidget().find('[data-testid="question-o-icon"]').exists()).toBe(true);
});
});
});
diff --git a/spec/frontend/reports/codequality_report/store/actions_spec.js b/spec/frontend/reports/codequality_report/store/actions_spec.js
index 1821390786b..71f1a0f4de0 100644
--- a/spec/frontend/reports/codequality_report/store/actions_spec.js
+++ b/spec/frontend/reports/codequality_report/store/actions_spec.js
@@ -23,7 +23,7 @@ describe('Codequality Reports actions', () => {
});
describe('setPaths', () => {
- it('should commit SET_PATHS mutation', (done) => {
+ it('should commit SET_PATHS mutation', () => {
const paths = {
baseBlobPath: 'baseBlobPath',
headBlobPath: 'headBlobPath',
@@ -31,13 +31,12 @@ describe('Codequality Reports actions', () => {
helpPath: 'codequalityHelpPath',
};
- testAction(
+ return testAction(
actions.setPaths,
paths,
localState,
[{ type: types.SET_PATHS, payload: paths }],
[],
- done,
);
});
});
@@ -56,10 +55,10 @@ describe('Codequality Reports actions', () => {
});
describe('on success', () => {
- it('commits REQUEST_REPORTS and dispatches receiveReportsSuccess', (done) => {
+ it('commits REQUEST_REPORTS and dispatches receiveReportsSuccess', () => {
mock.onGet(endpoint).reply(200, reportIssues);
- testAction(
+ return testAction(
actions.fetchReports,
null,
localState,
@@ -70,51 +69,48 @@ describe('Codequality Reports actions', () => {
type: 'receiveReportsSuccess',
},
],
- done,
);
});
});
describe('on error', () => {
- it('commits REQUEST_REPORTS and dispatches receiveReportsError', (done) => {
+ it('commits REQUEST_REPORTS and dispatches receiveReportsError', () => {
mock.onGet(endpoint).reply(500);
- testAction(
+ return testAction(
actions.fetchReports,
null,
localState,
[{ type: types.REQUEST_REPORTS }],
[{ type: 'receiveReportsError', payload: expect.any(Error) }],
- done,
);
});
});
describe('when base report is not found', () => {
- it('commits REQUEST_REPORTS and dispatches receiveReportsError', (done) => {
+ it('commits REQUEST_REPORTS and dispatches receiveReportsError', () => {
const data = { status: STATUS_NOT_FOUND };
mock.onGet(`${TEST_HOST}/codequality_reports.json`).reply(200, data);
- testAction(
+ return testAction(
actions.fetchReports,
null,
localState,
[{ type: types.REQUEST_REPORTS }],
[{ type: 'receiveReportsError', payload: data }],
- done,
);
});
});
describe('while waiting for report results', () => {
- it('continues polling until it receives data', (done) => {
+ it('continues polling until it receives data', () => {
mock
.onGet(endpoint)
.replyOnce(204, undefined, pollIntervalHeader)
.onGet(endpoint)
.reply(200, reportIssues);
- Promise.all([
+ return Promise.all([
testAction(
actions.fetchReports,
null,
@@ -126,7 +122,6 @@ describe('Codequality Reports actions', () => {
type: 'receiveReportsSuccess',
},
],
- done,
),
axios
// wait for initial NO_CONTENT response to be fulfilled
@@ -134,24 +129,23 @@ describe('Codequality Reports actions', () => {
.then(() => {
jest.advanceTimersByTime(pollInterval);
}),
- ]).catch(done.fail);
+ ]);
});
- it('continues polling until it receives an error', (done) => {
+ it('continues polling until it receives an error', () => {
mock
.onGet(endpoint)
.replyOnce(204, undefined, pollIntervalHeader)
.onGet(endpoint)
.reply(500);
- Promise.all([
+ return Promise.all([
testAction(
actions.fetchReports,
null,
localState,
[{ type: types.REQUEST_REPORTS }],
[{ type: 'receiveReportsError', payload: expect.any(Error) }],
- done,
),
axios
// wait for initial NO_CONTENT response to be fulfilled
@@ -159,35 +153,33 @@ describe('Codequality Reports actions', () => {
.then(() => {
jest.advanceTimersByTime(pollInterval);
}),
- ]).catch(done.fail);
+ ]);
});
});
});
describe('receiveReportsSuccess', () => {
- it('commits RECEIVE_REPORTS_SUCCESS', (done) => {
+ it('commits RECEIVE_REPORTS_SUCCESS', () => {
const data = { issues: [] };
- testAction(
+ return testAction(
actions.receiveReportsSuccess,
data,
localState,
[{ type: types.RECEIVE_REPORTS_SUCCESS, payload: data }],
[],
- done,
);
});
});
describe('receiveReportsError', () => {
- it('commits RECEIVE_REPORTS_ERROR', (done) => {
- testAction(
+ it('commits RECEIVE_REPORTS_ERROR', () => {
+ return testAction(
actions.receiveReportsError,
null,
localState,
[{ type: types.RECEIVE_REPORTS_ERROR, payload: null }],
[],
- done,
);
});
});
diff --git a/spec/frontend/reports/components/report_section_spec.js b/spec/frontend/reports/components/report_section_spec.js
index f9eb6dd05f3..888b49f3e0c 100644
--- a/spec/frontend/reports/components/report_section_spec.js
+++ b/spec/frontend/reports/components/report_section_spec.js
@@ -2,6 +2,7 @@ import { mount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import mountComponent, { mountComponentWithSlots } from 'helpers/vue_mount_component_helper';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import HelpPopover from '~/vue_shared/components/help_popover.vue';
import reportSection from '~/reports/components/report_section.vue';
describe('Report section', () => {
@@ -9,6 +10,7 @@ describe('Report section', () => {
let wrapper;
const ReportSection = Vue.extend(reportSection);
const findCollapseButton = () => wrapper.findByTestId('report-section-expand-button');
+ const findPopover = () => wrapper.findComponent(HelpPopover);
const resolvedIssues = [
{
@@ -269,4 +271,33 @@ describe('Report section', () => {
expect(vm.$el.textContent.trim()).not.toContain('This is a success');
});
});
+
+ describe('help popover', () => {
+ describe('when popover options are defined', () => {
+ const options = {
+ title: 'foo',
+ content: 'bar',
+ };
+
+ beforeEach(() => {
+ createComponent({
+ popoverOptions: options,
+ });
+ });
+
+ it('popover is shown with options', () => {
+ expect(findPopover().props('options')).toEqual(options);
+ });
+ });
+
+ describe('when popover options are not defined', () => {
+ beforeEach(() => {
+ createComponent({ popoverOptions: {} });
+ });
+
+ it('popover is not shown', () => {
+ expect(findPopover().exists()).toBe(false);
+ });
+ });
+ });
});
diff --git a/spec/frontend/reports/components/summary_row_spec.js b/spec/frontend/reports/components/summary_row_spec.js
index 04d9d10dcd2..778660d9e44 100644
--- a/spec/frontend/reports/components/summary_row_spec.js
+++ b/spec/frontend/reports/components/summary_row_spec.js
@@ -1,25 +1,26 @@
import { mount } from '@vue/test-utils';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import HelpPopover from '~/vue_shared/components/help_popover.vue';
import SummaryRow from '~/reports/components/summary_row.vue';
describe('Summary row', () => {
let wrapper;
- const props = {
- summary: 'SAST detected 1 new vulnerability and 1 fixed vulnerability',
- popoverOptions: {
- title: 'Static Application Security Testing (SAST)',
- content: '<a>Learn more about SAST</a>',
- },
- statusIcon: 'warning',
+ const summary = 'SAST detected 1 new vulnerability and 1 fixed vulnerability';
+ const popoverOptions = {
+ title: 'Static Application Security Testing (SAST)',
+ content: '<a>Learn more about SAST</a>',
};
+ const statusIcon = 'warning';
- const createComponent = ({ propsData = {}, slots = {} } = {}) => {
+ const createComponent = ({ props = {}, slots = {} } = {}) => {
wrapper = extendedWrapper(
mount(SummaryRow, {
propsData: {
+ summary,
+ popoverOptions,
+ statusIcon,
...props,
- ...propsData,
},
slots,
}),
@@ -28,6 +29,7 @@ describe('Summary row', () => {
const findSummary = () => wrapper.findByTestId('summary-row-description');
const findStatusIcon = () => wrapper.findByTestId('summary-row-icon');
+ const findHelpPopover = () => wrapper.findComponent(HelpPopover);
afterEach(() => {
wrapper.destroy();
@@ -36,7 +38,7 @@ describe('Summary row', () => {
it('renders provided summary', () => {
createComponent();
- expect(findSummary().text()).toContain(props.summary);
+ expect(findSummary().text()).toContain(summary);
});
it('renders provided icon', () => {
@@ -44,12 +46,22 @@ describe('Summary row', () => {
expect(findStatusIcon().classes()).toContain('js-ci-status-icon-warning');
});
+ it('renders help popover if popoverOptions are provided', () => {
+ createComponent();
+ expect(findHelpPopover().props('options')).toEqual(popoverOptions);
+ });
+
+ it('does not render help popover if popoverOptions are not provided', () => {
+ createComponent({ props: { popoverOptions: null } });
+ expect(findHelpPopover().exists()).toBe(false);
+ });
+
describe('summary slot', () => {
it('replaces the summary prop', () => {
const summarySlotContent = 'Summary slot content';
createComponent({ slots: { summary: summarySlotContent } });
- expect(wrapper.text()).not.toContain(props.summary);
+ expect(wrapper.text()).not.toContain(summary);
expect(findSummary().text()).toContain(summarySlotContent);
});
});
diff --git a/spec/frontend/reports/grouped_test_report/store/actions_spec.js b/spec/frontend/reports/grouped_test_report/store/actions_spec.js
index bbc3a5dbba5..5876827c548 100644
--- a/spec/frontend/reports/grouped_test_report/store/actions_spec.js
+++ b/spec/frontend/reports/grouped_test_report/store/actions_spec.js
@@ -24,8 +24,8 @@ describe('Reports Store Actions', () => {
});
describe('setPaths', () => {
- it('should commit SET_PATHS mutation', (done) => {
- testAction(
+ it('should commit SET_PATHS mutation', () => {
+ return testAction(
setPaths,
{ endpoint: 'endpoint.json', headBlobPath: '/blob/path' },
mockedState,
@@ -36,14 +36,13 @@ describe('Reports Store Actions', () => {
},
],
[],
- done,
);
});
});
describe('requestReports', () => {
- it('should commit REQUEST_REPORTS mutation', (done) => {
- testAction(requestReports, null, mockedState, [{ type: types.REQUEST_REPORTS }], [], done);
+ it('should commit REQUEST_REPORTS mutation', () => {
+ return testAction(requestReports, null, mockedState, [{ type: types.REQUEST_REPORTS }], []);
});
});
@@ -62,12 +61,12 @@ describe('Reports Store Actions', () => {
});
describe('success', () => {
- it('dispatches requestReports and receiveReportsSuccess ', (done) => {
+ it('dispatches requestReports and receiveReportsSuccess ', () => {
mock
.onGet(`${TEST_HOST}/endpoint.json`)
.replyOnce(200, { summary: {}, suites: [{ name: 'rspec' }] });
- testAction(
+ return testAction(
fetchReports,
null,
mockedState,
@@ -81,7 +80,6 @@ describe('Reports Store Actions', () => {
type: 'receiveReportsSuccess',
},
],
- done,
);
});
});
@@ -91,8 +89,8 @@ describe('Reports Store Actions', () => {
mock.onGet(`${TEST_HOST}/endpoint.json`).reply(500);
});
- it('dispatches requestReports and receiveReportsError ', (done) => {
- testAction(
+ it('dispatches requestReports and receiveReportsError ', () => {
+ return testAction(
fetchReports,
null,
mockedState,
@@ -105,71 +103,65 @@ describe('Reports Store Actions', () => {
type: 'receiveReportsError',
},
],
- done,
);
});
});
});
describe('receiveReportsSuccess', () => {
- it('should commit RECEIVE_REPORTS_SUCCESS mutation with 200', (done) => {
- testAction(
+ it('should commit RECEIVE_REPORTS_SUCCESS mutation with 200', () => {
+ return testAction(
receiveReportsSuccess,
{ data: { summary: {} }, status: 200 },
mockedState,
[{ type: types.RECEIVE_REPORTS_SUCCESS, payload: { summary: {} } }],
[],
- done,
);
});
- it('should not commit RECEIVE_REPORTS_SUCCESS mutation with 204', (done) => {
- testAction(
+ it('should not commit RECEIVE_REPORTS_SUCCESS mutation with 204', () => {
+ return testAction(
receiveReportsSuccess,
{ data: { summary: {} }, status: 204 },
mockedState,
[],
[],
- done,
);
});
});
describe('receiveReportsError', () => {
- it('should commit RECEIVE_REPORTS_ERROR mutation', (done) => {
- testAction(
+ it('should commit RECEIVE_REPORTS_ERROR mutation', () => {
+ return testAction(
receiveReportsError,
null,
mockedState,
[{ type: types.RECEIVE_REPORTS_ERROR }],
[],
- done,
);
});
});
describe('openModal', () => {
- it('should commit SET_ISSUE_MODAL_DATA', (done) => {
- testAction(
+ it('should commit SET_ISSUE_MODAL_DATA', () => {
+ return testAction(
openModal,
{ name: 'foo' },
mockedState,
[{ type: types.SET_ISSUE_MODAL_DATA, payload: { name: 'foo' } }],
[],
- done,
);
});
});
describe('closeModal', () => {
- it('should commit RESET_ISSUE_MODAL_DATA', (done) => {
- testAction(
+ it('should commit RESET_ISSUE_MODAL_DATA', () => {
+ return testAction(
closeModal,
{},
mockedState,
[{ type: types.RESET_ISSUE_MODAL_DATA, payload: {} }],
[],
- done,
);
});
});
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 7854325e4ed..fea937b905f 100644
--- a/spec/frontend/repository/components/__snapshots__/last_commit_spec.js.snap
+++ b/spec/frontend/repository/components/__snapshots__/last_commit_spec.js.snap
@@ -106,114 +106,3 @@ exports[`Repository last commit component renders commit widget 1`] = `
</div>
</div>
`;
-
-exports[`Repository last commit component renders the signature HTML as returned by the backend 1`] = `
-<div
- class="well-segment commit gl-p-5 gl-w-full"
->
- <user-avatar-link-stub
- class="avatar-cell"
- imgalt=""
- imgcssclasses=""
- imgsize="40"
- imgsrc="https://test.com"
- linkhref="/test"
- tooltipplacement="top"
- tooltiptext=""
- username=""
- />
-
- <div
- class="commit-detail flex-list"
- >
- <div
- class="commit-content qa-commit-content"
- >
- <gl-link-stub
- class="commit-row-message item-title"
- href="/commit/123"
- >
- Commit title
- </gl-link-stub>
-
- <!---->
-
- <div
- class="committer"
- >
- <gl-link-stub
- class="commit-author-link js-user-link"
- href="/test"
- >
-
- Test
- </gl-link-stub>
-
- authored
-
- <timeago-tooltip-stub
- cssclass=""
- time="2019-01-01"
- tooltipplacement="bottom"
- />
- </div>
-
- <!---->
- </div>
-
- <div
- class="commit-actions flex-row"
- >
- <div>
- <button>
- Verified
- </button>
- </div>
-
- <div
- class="ci-status-link"
- >
- <gl-link-stub
- class="js-commit-pipeline"
- href="https://test.com/pipeline"
- title="Pipeline: failed"
- >
- <ci-icon-stub
- aria-label="Pipeline: failed"
- cssclasses=""
- size="24"
- status="[object Object]"
- />
- </gl-link-stub>
- </div>
-
- <gl-button-group-stub
- class="gl-ml-4 js-commit-sha-group"
- >
- <gl-button-stub
- buttontextclasses=""
- category="primary"
- class="gl-font-monospace"
- data-testid="last-commit-id-label"
- icon=""
- label="true"
- size="medium"
- variant="default"
- >
- 12345678
- </gl-button-stub>
-
- <clipboard-button-stub
- category="secondary"
- class="input-group-text"
- size="medium"
- text="123456789"
- title="Copy commit SHA"
- tooltipplacement="top"
- variant="default"
- />
- </gl-button-group-stub>
- </div>
- </div>
-</div>
-`;
diff --git a/spec/frontend/repository/components/blob_content_viewer_spec.js b/spec/frontend/repository/components/blob_content_viewer_spec.js
index 96c03419dd6..2f6de03b73d 100644
--- a/spec/frontend/repository/components/blob_content_viewer_spec.js
+++ b/spec/frontend/repository/components/blob_content_viewer_spec.js
@@ -25,6 +25,7 @@ import { redirectTo } from '~/lib/utils/url_utility';
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 {
simpleViewerMock,
richViewerMock,
@@ -39,6 +40,7 @@ import {
jest.mock('~/repository/components/blob_viewers');
jest.mock('~/lib/utils/url_utility');
jest.mock('~/lib/utils/common_utils');
+jest.mock('~/blob/line_highlighter');
let wrapper;
let mockResolver;
@@ -173,20 +175,30 @@ describe('Blob content viewer component', () => {
});
describe('legacy viewers', () => {
+ const legacyViewerUrl = 'some_file.js?format=json&viewer=simple';
+ const fileType = 'text';
+ const highlightJs = false;
+
it('loads a legacy viewer when a the fileType is text and the highlightJs feature is turned off', async () => {
await createComponent({
- blob: { ...simpleViewerMock, fileType: 'text', highlightJs: false },
+ blob: { ...simpleViewerMock, fileType, highlightJs },
});
expect(mockAxios.history.get).toHaveLength(1);
- expect(mockAxios.history.get[0].url).toEqual('some_file.js?format=json&viewer=simple');
+ expect(mockAxios.history.get[0].url).toBe(legacyViewerUrl);
});
it('loads a legacy viewer when a viewer component is not available', async () => {
await createComponent({ blob: { ...simpleViewerMock, fileType: 'unknown' } });
expect(mockAxios.history.get).toHaveLength(1);
- expect(mockAxios.history.get[0].url).toEqual('some_file.js?format=json&viewer=simple');
+ expect(mockAxios.history.get[0].url).toBe(legacyViewerUrl);
+ });
+
+ it('loads the LineHighlighter', async () => {
+ mockAxios.onGet(legacyViewerUrl).replyOnce(httpStatusCodes.OK, 'test');
+ await createComponent({ blob: { ...simpleViewerMock, fileType, highlightJs } });
+ expect(LineHighlighter).toHaveBeenCalled();
});
});
});
@@ -258,6 +270,7 @@ describe('Blob content viewer component', () => {
codeNavigationPath: simpleViewerMock.codeNavigationPath,
blobPath: simpleViewerMock.path,
pathPrefix: simpleViewerMock.projectBlobPathRoot,
+ wrapTextNodes: true,
});
});
diff --git a/spec/frontend/repository/components/breadcrumbs_spec.js b/spec/frontend/repository/components/breadcrumbs_spec.js
index 0e3e7075e99..eef66045573 100644
--- a/spec/frontend/repository/components/breadcrumbs_spec.js
+++ b/spec/frontend/repository/components/breadcrumbs_spec.js
@@ -12,7 +12,7 @@ const defaultMockRoute = {
describe('Repository breadcrumbs component', () => {
let wrapper;
- const factory = (currentPath, extraProps = {}, mockRoute = {}, newDirModal = true) => {
+ const factory = (currentPath, extraProps = {}, mockRoute = {}) => {
const $apollo = {
queries: {
userPermissions: {
@@ -36,7 +36,6 @@ describe('Repository breadcrumbs component', () => {
},
$apollo,
},
- provide: { glFeatures: { newDirModal } },
});
};
@@ -147,37 +146,21 @@ describe('Repository breadcrumbs component', () => {
});
describe('renders the new directory modal', () => {
- describe('with the feature flag enabled', () => {
- beforeEach(() => {
- window.gon.features = {
- newDirModal: true,
- };
- factory('/', { canEditTree: true });
- });
-
- it('does not render the modal while loading', () => {
- expect(findNewDirectoryModal().exists()).toBe(false);
- });
-
- it('renders the modal once loaded', async () => {
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({ $apollo: { queries: { userPermissions: { loading: false } } } });
-
- await nextTick();
-
- expect(findNewDirectoryModal().exists()).toBe(true);
- });
+ beforeEach(() => {
+ factory('/', { canEditTree: true });
+ });
+ it('does not render the modal while loading', () => {
+ expect(findNewDirectoryModal().exists()).toBe(false);
});
- describe('with the feature flag disabled', () => {
- it('does not render the modal', () => {
- window.gon.features = {
- newDirModal: false,
- };
- factory('/', { canEditTree: true }, {}, {}, false);
- expect(findNewDirectoryModal().exists()).toBe(false);
- });
+ it('renders the modal once loaded', async () => {
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
+ wrapper.setData({ $apollo: { queries: { userPermissions: { loading: false } } } });
+
+ await nextTick();
+
+ expect(findNewDirectoryModal().exists()).toBe(true);
});
});
});
diff --git a/spec/frontend/repository/components/last_commit_spec.js b/spec/frontend/repository/components/last_commit_spec.js
index bb710c3a96c..cfbf74e34aa 100644
--- a/spec/frontend/repository/components/last_commit_spec.js
+++ b/spec/frontend/repository/components/last_commit_spec.js
@@ -143,11 +143,30 @@ describe('Repository last commit component', () => {
});
it('renders the signature HTML as returned by the backend', async () => {
- factory(createCommitData({ signatureHtml: '<button>Verified</button>' }));
+ factory(
+ createCommitData({
+ signatureHtml: `<a
+ class="btn gpg-status-box valid"
+ data-content="signature-content"
+ data-html="true"
+ data-placement="top"
+ data-title="signature-title"
+ data-toggle="popover"
+ role="button"
+ tabindex="0"
+ >
+ Verified
+ </a>`,
+ }),
+ );
await nextTick();
- expect(vm.element).toMatchSnapshot();
+ expect(vm.find('.gpg-status-box').html()).toBe(
+ `<a class="btn gpg-status-box valid" data-content="signature-content" data-html="true" data-placement="top" data-title="signature-title" data-toggle="popover" role="button" tabindex="0">
+ Verified
+</a>`,
+ );
});
it('sets correct CSS class if the commit message is empty', async () => {
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 cdaec0a3a8b..2ef856c90ab 100644
--- a/spec/frontend/runner/admin_runners/admin_runners_app_spec.js
+++ b/spec/frontend/runner/admin_runners/admin_runners_app_spec.js
@@ -13,6 +13,7 @@ import { createAlert } from '~/flash';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { updateHistory } from '~/lib/utils/url_utility';
+import { createLocalState } from '~/runner/graphql/list/local_state';
import AdminRunnersApp from '~/runner/admin_runners/admin_runners_app.vue';
import RunnerTypeTabs from '~/runner/components/runner_type_tabs.vue';
import RunnerFilteredSearchBar from '~/runner/components/runner_filtered_search_bar.vue';
@@ -30,9 +31,10 @@ import {
INSTANCE_TYPE,
GROUP_TYPE,
PROJECT_TYPE,
+ PARAM_KEY_PAUSED,
PARAM_KEY_STATUS,
PARAM_KEY_TAG,
- STATUS_ACTIVE,
+ STATUS_ONLINE,
RUNNER_PAGE_SIZE,
} from '~/runner/constants';
import adminRunnersQuery from '~/runner/graphql/list/admin_runners.query.graphql';
@@ -40,9 +42,16 @@ import adminRunnersCountQuery from '~/runner/graphql/list/admin_runners_count.qu
import { captureException } from '~/runner/sentry_utils';
import FilteredSearch from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
-import { runnersData, runnersCountData, runnersDataPaginated } from '../mock_data';
+import {
+ runnersData,
+ runnersCountData,
+ runnersDataPaginated,
+ onlineContactTimeoutSecs,
+ staleTimeoutSecs,
+} from '../mock_data';
const mockRegistrationToken = 'MOCK_REGISTRATION_TOKEN';
+const mockRunners = runnersData.data.runners.nodes;
jest.mock('~/flash');
jest.mock('~/runner/sentry_utils');
@@ -58,6 +67,8 @@ describe('AdminRunnersApp', () => {
let wrapper;
let mockRunnersQuery;
let mockRunnersCountQuery;
+ let cacheConfig;
+ let localMutations;
const findRunnerStats = () => wrapper.findComponent(RunnerStats);
const findRunnerActionsCell = () => wrapper.findComponent(RunnerActionsCell);
@@ -69,18 +80,32 @@ describe('AdminRunnersApp', () => {
const findRunnerFilteredSearchBar = () => wrapper.findComponent(RunnerFilteredSearchBar);
const findFilteredSearch = () => wrapper.findComponent(FilteredSearch);
- const createComponent = ({ props = {}, mountFn = shallowMountExtended } = {}) => {
+ const createComponent = ({
+ props = {},
+ mountFn = shallowMountExtended,
+ provide,
+ ...options
+ } = {}) => {
+ ({ cacheConfig, localMutations } = createLocalState());
+
const handlers = [
[adminRunnersQuery, mockRunnersQuery],
[adminRunnersCountQuery, mockRunnersCountQuery],
];
wrapper = mountFn(AdminRunnersApp, {
- apolloProvider: createMockApollo(handlers),
+ apolloProvider: createMockApollo(handlers, {}, cacheConfig),
propsData: {
registrationToken: mockRegistrationToken,
...props,
},
+ provide: {
+ localMutations,
+ onlineContactTimeoutSecs,
+ staleTimeoutSecs,
+ ...provide,
+ },
+ ...options,
});
};
@@ -173,7 +198,7 @@ describe('AdminRunnersApp', () => {
});
it('shows the runners list', () => {
- expect(findRunnerList().props('runners')).toEqual(runnersData.data.runners.nodes);
+ expect(findRunnerList().props('runners')).toEqual(mockRunners);
});
it('runner item links to the runner admin page', async () => {
@@ -181,7 +206,7 @@ describe('AdminRunnersApp', () => {
await waitForPromises();
- const { id, shortSha } = runnersData.data.runners.nodes[0];
+ const { id, shortSha } = mockRunners[0];
const numericId = getIdFromGraphQLId(id);
const runnerLink = wrapper.find('tr [data-testid="td-summary"]').find(GlLink);
@@ -197,7 +222,7 @@ describe('AdminRunnersApp', () => {
const runnerActions = wrapper.find('tr [data-testid="td-actions"]').find(RunnerActionsCell);
- const runner = runnersData.data.runners.nodes[0];
+ const runner = mockRunners[0];
expect(runnerActions.props()).toEqual({
runner,
@@ -219,6 +244,10 @@ describe('AdminRunnersApp', () => {
expect(findFilteredSearch().props('tokens')).toEqual([
expect.objectContaining({
+ type: PARAM_KEY_PAUSED,
+ options: expect.any(Array),
+ }),
+ expect.objectContaining({
type: PARAM_KEY_STATUS,
options: expect.any(Array),
}),
@@ -232,12 +261,13 @@ describe('AdminRunnersApp', () => {
describe('Single runner row', () => {
let showToast;
- const mockRunner = runnersData.data.runners.nodes[0];
- const { id: graphqlId, shortSha } = mockRunner;
+ const { id: graphqlId, shortSha } = mockRunners[0];
const id = getIdFromGraphQLId(graphqlId);
+ const COUNT_QUERIES = 7; // Smart queries that display a filtered count of runners
+ const FILTERED_COUNT_QUERIES = 4; // Smart queries that display a count of runners in tabs
beforeEach(async () => {
- mockRunnersQuery.mockClear();
+ mockRunnersCountQuery.mockClear();
createComponent({ mountFn: mountExtended });
showToast = jest.spyOn(wrapper.vm.$root.$toast, 'show');
@@ -252,12 +282,18 @@ describe('AdminRunnersApp', () => {
expect(runnerLink.attributes('href')).toBe(`http://localhost/admin/runners/${id}`);
});
- it('When runner is deleted, data is refetched and a toast message is shown', async () => {
- expect(mockRunnersQuery).toHaveBeenCalledTimes(1);
+ it('When runner is paused or unpaused, some data is refetched', async () => {
+ expect(mockRunnersCountQuery).toHaveBeenCalledTimes(COUNT_QUERIES);
- findRunnerActionsCell().vm.$emit('deleted', { message: 'Runner deleted' });
+ findRunnerActionsCell().vm.$emit('toggledPaused');
- expect(mockRunnersQuery).toHaveBeenCalledTimes(2);
+ expect(mockRunnersCountQuery).toHaveBeenCalledTimes(COUNT_QUERIES + FILTERED_COUNT_QUERIES);
+
+ expect(showToast).toHaveBeenCalledTimes(0);
+ });
+
+ it('When runner is deleted, data is refetched and a toast message is shown', async () => {
+ findRunnerActionsCell().vm.$emit('deleted', { message: 'Runner deleted' });
expect(showToast).toHaveBeenCalledTimes(1);
expect(showToast).toHaveBeenCalledWith('Runner deleted');
@@ -266,7 +302,7 @@ describe('AdminRunnersApp', () => {
describe('when a filter is preselected', () => {
beforeEach(async () => {
- setWindowLocation(`?status[]=${STATUS_ACTIVE}&runner_type[]=${INSTANCE_TYPE}&tag[]=tag1`);
+ setWindowLocation(`?status[]=${STATUS_ONLINE}&runner_type[]=${INSTANCE_TYPE}&tag[]=tag1`);
createComponent();
await waitForPromises();
@@ -276,7 +312,7 @@ describe('AdminRunnersApp', () => {
expect(findRunnerFilteredSearchBar().props('value')).toEqual({
runnerType: INSTANCE_TYPE,
filters: [
- { type: 'status', value: { data: STATUS_ACTIVE, operator: '=' } },
+ { type: 'status', value: { data: STATUS_ONLINE, operator: '=' } },
{ type: 'tag', value: { data: 'tag1', operator: '=' } },
],
sort: 'CREATED_DESC',
@@ -286,7 +322,7 @@ describe('AdminRunnersApp', () => {
it('requests the runners with filter parameters', () => {
expect(mockRunnersQuery).toHaveBeenLastCalledWith({
- status: STATUS_ACTIVE,
+ status: STATUS_ONLINE,
type: INSTANCE_TYPE,
tagList: ['tag1'],
sort: DEFAULT_SORT,
@@ -299,7 +335,7 @@ describe('AdminRunnersApp', () => {
beforeEach(() => {
findRunnerFilteredSearchBar().vm.$emit('input', {
runnerType: null,
- filters: [{ type: PARAM_KEY_STATUS, value: { data: STATUS_ACTIVE, operator: '=' } }],
+ filters: [{ type: PARAM_KEY_STATUS, value: { data: STATUS_ONLINE, operator: '=' } }],
sort: CREATED_ASC,
});
});
@@ -307,13 +343,13 @@ describe('AdminRunnersApp', () => {
it('updates the browser url', () => {
expect(updateHistory).toHaveBeenLastCalledWith({
title: expect.any(String),
- url: 'http://test.host/admin/runners?status[]=ACTIVE&sort=CREATED_ASC',
+ url: 'http://test.host/admin/runners?status[]=ONLINE&sort=CREATED_ASC',
});
});
it('requests the runners with filters', () => {
expect(mockRunnersQuery).toHaveBeenLastCalledWith({
- status: STATUS_ACTIVE,
+ status: STATUS_ONLINE,
sort: CREATED_ASC,
first: RUNNER_PAGE_SIZE,
});
@@ -325,6 +361,41 @@ describe('AdminRunnersApp', () => {
expect(findRunnerList().props('loading')).toBe(true);
});
+ describe('when bulk delete is enabled', () => {
+ beforeEach(() => {
+ createComponent({
+ provide: {
+ glFeatures: { adminRunnersBulkDelete: true },
+ },
+ });
+ });
+
+ it('runner list is checkable', () => {
+ expect(findRunnerList().props('checkable')).toBe(true);
+ });
+
+ it('responds to checked items by updating the local cache', () => {
+ const setRunnerCheckedMock = jest
+ .spyOn(localMutations, 'setRunnerChecked')
+ .mockImplementation(() => {});
+
+ const runner = mockRunners[0];
+
+ expect(setRunnerCheckedMock).toHaveBeenCalledTimes(0);
+
+ findRunnerList().vm.$emit('checked', {
+ runner,
+ isChecked: true,
+ });
+
+ expect(setRunnerCheckedMock).toHaveBeenCalledTimes(1);
+ expect(setRunnerCheckedMock).toHaveBeenCalledWith({
+ runner,
+ isChecked: true,
+ });
+ });
+ });
+
describe('when no runners are found', () => {
beforeEach(async () => {
mockRunnersQuery = jest.fn().mockResolvedValue({
diff --git a/spec/frontend/runner/components/__snapshots__/runner_status_popover_spec.js.snap b/spec/frontend/runner/components/__snapshots__/runner_status_popover_spec.js.snap
new file mode 100644
index 00000000000..80a04401760
--- /dev/null
+++ b/spec/frontend/runner/components/__snapshots__/runner_status_popover_spec.js.snap
@@ -0,0 +1,3 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`RunnerStatusPopover renders complete text 1`] = `"Never contacted: Runner has never contacted GitLab (when you register a runner, use gitlab-runner run to bring it online) Online: Runner has contacted GitLab within the last 2 hours Offline: Runner has not contacted GitLab in more than 2 hours Stale: Runner has not contacted GitLab in more than 2 months"`;
diff --git a/spec/frontend/runner/components/cells/runner_actions_cell_spec.js b/spec/frontend/runner/components/cells/runner_actions_cell_spec.js
index 0d579106860..7a949cb6505 100644
--- a/spec/frontend/runner/components/cells/runner_actions_cell_spec.js
+++ b/spec/frontend/runner/components/cells/runner_actions_cell_spec.js
@@ -92,6 +92,24 @@ describe('RunnerActionsCell', () => {
expect(findDeleteBtn().props('compact')).toBe(true);
});
+ it('Passes runner data to delete button', () => {
+ createComponent({
+ runner: mockRunner,
+ });
+
+ expect(findDeleteBtn().props('runner')).toEqual(mockRunner);
+ });
+
+ it('Emits toggledPaused events', () => {
+ createComponent();
+
+ expect(wrapper.emitted('toggledPaused')).toBe(undefined);
+
+ findRunnerPauseBtn().vm.$emit('toggledPaused');
+
+ expect(wrapper.emitted('toggledPaused')).toHaveLength(1);
+ });
+
it('Emits delete events', () => {
const value = { name: 'Runner' };
@@ -104,7 +122,7 @@ describe('RunnerActionsCell', () => {
expect(wrapper.emitted('deleted')).toEqual([[value]]);
});
- it('Does not render the runner delete button when user cannot delete', () => {
+ it('Renders the runner delete disabled button when user cannot delete', () => {
createComponent({
runner: {
userPermissions: {
@@ -114,7 +132,7 @@ describe('RunnerActionsCell', () => {
},
});
- expect(findDeleteBtn().exists()).toBe(false);
+ expect(findDeleteBtn().props('disabled')).toBe(true);
});
});
});
diff --git a/spec/frontend/runner/components/cells/runner_summary_cell_spec.js b/spec/frontend/runner/components/cells/runner_summary_cell_spec.js
index b6d957d27ea..b2e8c5a3ad9 100644
--- a/spec/frontend/runner/components/cells/runner_summary_cell_spec.js
+++ b/spec/frontend/runner/components/cells/runner_summary_cell_spec.js
@@ -5,6 +5,7 @@ import { INSTANCE_TYPE, PROJECT_TYPE } from '~/runner/constants';
const mockId = '1';
const mockShortSha = '2P6oDVDm';
const mockDescription = 'runner-1';
+const mockIpAddress = '0.0.0.0';
describe('RunnerTypeCell', () => {
let wrapper;
@@ -18,6 +19,7 @@ describe('RunnerTypeCell', () => {
id: `gid://gitlab/Ci::Runner/${mockId}`,
shortSha: mockShortSha,
description: mockDescription,
+ ipAddress: mockIpAddress,
runnerType: INSTANCE_TYPE,
...runner,
},
@@ -59,6 +61,10 @@ describe('RunnerTypeCell', () => {
expect(wrapper.text()).toContain(mockDescription);
});
+ it('Displays the runner ip address', () => {
+ expect(wrapper.text()).toContain(mockIpAddress);
+ });
+
it('Displays a custom slot', () => {
const slotContent = 'My custom runner summary';
diff --git a/spec/frontend/runner/components/registration/registration_dropdown_spec.js b/spec/frontend/runner/components/registration/registration_dropdown_spec.js
index da8ef7c3af0..5cd93df9967 100644
--- a/spec/frontend/runner/components/registration/registration_dropdown_spec.js
+++ b/spec/frontend/runner/components/registration/registration_dropdown_spec.js
@@ -8,6 +8,7 @@ import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import RegistrationDropdown from '~/runner/components/registration/registration_dropdown.vue';
+import RegistrationToken from '~/runner/components/registration/registration_token.vue';
import RegistrationTokenResetDropdownItem from '~/runner/components/registration/registration_token_reset_dropdown_item.vue';
import { INSTANCE_TYPE, GROUP_TYPE, PROJECT_TYPE } from '~/runner/constants';
@@ -30,11 +31,11 @@ describe('RegistrationDropdown', () => {
const findRegistrationInstructionsDropdownItem = () => wrapper.findComponent(GlDropdownItem);
const findTokenDropdownItem = () => wrapper.findComponent(GlDropdownForm);
+ const findRegistrationToken = () => wrapper.findComponent(RegistrationToken);
+ const findRegistrationTokenInput = () => wrapper.findByTestId('token-value').find('input');
const findTokenResetDropdownItem = () =>
wrapper.findComponent(RegistrationTokenResetDropdownItem);
- const findToggleMaskButton = () => wrapper.findByTestId('toggle-masked');
-
const createComponent = ({ props = {}, ...options } = {}, mountFn = shallowMount) => {
wrapper = extendedWrapper(
mountFn(RegistrationDropdown, {
@@ -134,9 +135,7 @@ describe('RegistrationDropdown', () => {
it('Displays masked value by default', () => {
createComponent({}, mount);
- expect(findTokenDropdownItem().text()).toMatchInterpolatedText(
- `Registration token ${maskToken}`,
- );
+ expect(findRegistrationTokenInput().element.value).toBe(maskToken);
});
});
@@ -155,16 +154,14 @@ describe('RegistrationDropdown', () => {
});
it('Updates the token when it gets reset', async () => {
+ const newToken = 'mock1';
createComponent({}, mount);
- const newToken = 'mock1';
+ expect(findRegistrationTokenInput().props('value')).not.toBe(newToken);
findTokenResetDropdownItem().vm.$emit('tokenReset', newToken);
- findToggleMaskButton().vm.$emit('click', { stopPropagation: jest.fn() });
await nextTick();
- expect(findTokenDropdownItem().text()).toMatchInterpolatedText(
- `Registration token ${newToken}`,
- );
+ expect(findRegistrationToken().props('value')).toBe(newToken);
});
});
diff --git a/spec/frontend/runner/components/registration/registration_token_spec.js b/spec/frontend/runner/components/registration/registration_token_spec.js
index 6b9708cc525..cb42c7c8493 100644
--- a/spec/frontend/runner/components/registration/registration_token_spec.js
+++ b/spec/frontend/runner/components/registration/registration_token_spec.js
@@ -1,20 +1,17 @@
-import { nextTick } from 'vue';
import { GlToast } from '@gitlab/ui';
import { createLocalVue } from '@vue/test-utils';
-import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
import RegistrationToken from '~/runner/components/registration/registration_token.vue';
-import ModalCopyButton from '~/vue_shared/components/modal_copy_button.vue';
+import InputCopyToggleVisibility from '~/vue_shared/components/form/input_copy_toggle_visibility.vue';
const mockToken = '01234567890';
const mockMasked = '***********';
describe('RegistrationToken', () => {
let wrapper;
- let stopPropagation;
let showToast;
- const findToggleMaskButton = () => wrapper.findByTestId('toggle-masked');
- const findCopyButton = () => wrapper.findComponent(ModalCopyButton);
+ const findInputCopyToggleVisibility = () => wrapper.findComponent(InputCopyToggleVisibility);
const vueWithGlToast = () => {
const localVue = createLocalVue();
@@ -22,10 +19,14 @@ describe('RegistrationToken', () => {
return localVue;
};
- const createComponent = ({ props = {}, withGlToast = true } = {}) => {
+ const createComponent = ({
+ props = {},
+ withGlToast = true,
+ mountFn = shallowMountExtended,
+ } = {}) => {
const localVue = withGlToast ? vueWithGlToast() : undefined;
- wrapper = shallowMountExtended(RegistrationToken, {
+ wrapper = mountFn(RegistrationToken, {
propsData: {
value: mockToken,
...props,
@@ -36,61 +37,33 @@ describe('RegistrationToken', () => {
showToast = wrapper.vm.$toast ? jest.spyOn(wrapper.vm.$toast, 'show') : null;
};
- beforeEach(() => {
- stopPropagation = jest.fn();
-
- createComponent();
- });
-
afterEach(() => {
wrapper.destroy();
});
- it('Displays masked value by default', () => {
- expect(wrapper.text()).toBe(mockMasked);
- });
+ it('Displays value and copy button', () => {
+ createComponent();
- it('Displays button to reveal token', () => {
- expect(findToggleMaskButton().attributes('aria-label')).toBe('Click to reveal');
+ expect(findInputCopyToggleVisibility().props('value')).toBe(mockToken);
+ expect(findInputCopyToggleVisibility().props('copyButtonTitle')).toBe(
+ 'Copy registration token',
+ );
});
- it('Can copy the original token value', () => {
- expect(findCopyButton().props('text')).toBe(mockToken);
+ // Component integration test to ensure secure masking
+ it('Displays masked value by default', () => {
+ createComponent({ mountFn: mountExtended });
+
+ expect(wrapper.find('input').element.value).toBe(mockMasked);
});
- describe('When the reveal icon is clicked', () => {
+ describe('When the copy to clipboard button is clicked', () => {
beforeEach(() => {
- findToggleMaskButton().vm.$emit('click', { stopPropagation });
- });
-
- it('Click event is not propagated', async () => {
- expect(stopPropagation).toHaveBeenCalledTimes(1);
+ createComponent();
});
- it('Displays the actual value', () => {
- expect(wrapper.text()).toBe(mockToken);
- });
-
- it('Can copy the original token value', () => {
- expect(findCopyButton().props('text')).toBe(mockToken);
- });
-
- it('Displays button to mask token', () => {
- expect(findToggleMaskButton().attributes('aria-label')).toBe('Click to hide');
- });
-
- it('When user clicks again, displays masked value', async () => {
- findToggleMaskButton().vm.$emit('click', { stopPropagation });
- await nextTick();
-
- expect(wrapper.text()).toBe(mockMasked);
- expect(findToggleMaskButton().attributes('aria-label')).toBe('Click to reveal');
- });
- });
-
- describe('When the copy to clipboard button is clicked', () => {
it('shows a copied message', () => {
- findCopyButton().vm.$emit('success');
+ findInputCopyToggleVisibility().vm.$emit('copy');
expect(showToast).toHaveBeenCalledTimes(1);
expect(showToast).toHaveBeenCalledWith('Registration token copied!');
@@ -98,7 +71,7 @@ describe('RegistrationToken', () => {
it('does not fail when toast is not defined', () => {
createComponent({ withGlToast: false });
- findCopyButton().vm.$emit('success');
+ findInputCopyToggleVisibility().vm.$emit('copy');
// This block also tests for unhandled errors
expect(showToast).toBeNull();
diff --git a/spec/frontend/runner/components/runner_assigned_item_spec.js b/spec/frontend/runner/components/runner_assigned_item_spec.js
index c6156c16d4a..1ff6983fbe7 100644
--- a/spec/frontend/runner/components/runner_assigned_item_spec.js
+++ b/spec/frontend/runner/components/runner_assigned_item_spec.js
@@ -1,6 +1,7 @@
import { GlAvatar } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import RunnerAssignedItem from '~/runner/components/runner_assigned_item.vue';
+import { AVATAR_SHAPE_OPTION_RECT } from '~/vue_shared/constants';
const mockHref = '/group/project';
const mockName = 'Project';
@@ -40,7 +41,7 @@ describe('RunnerAssignedItem', () => {
alt: mockName,
entityName: mockName,
src: mockAvatarUrl,
- shape: 'rect',
+ shape: AVATAR_SHAPE_OPTION_RECT,
size: 48,
});
});
diff --git a/spec/frontend/runner/components/runner_bulk_delete_spec.js b/spec/frontend/runner/components/runner_bulk_delete_spec.js
new file mode 100644
index 00000000000..f5b56396cf1
--- /dev/null
+++ b/spec/frontend/runner/components/runner_bulk_delete_spec.js
@@ -0,0 +1,103 @@
+import Vue from 'vue';
+import { GlSprintf } from '@gitlab/ui';
+import VueApollo from 'vue-apollo';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
+import RunnerBulkDelete from '~/runner/components/runner_bulk_delete.vue';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import { createLocalState } from '~/runner/graphql/list/local_state';
+import waitForPromises from 'helpers/wait_for_promises';
+
+Vue.use(VueApollo);
+
+jest.mock('~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal');
+
+describe('RunnerBulkDelete', () => {
+ let wrapper;
+ let mockState;
+ let mockCheckedRunnerIds;
+
+ const findClearBtn = () => wrapper.findByTestId('clear-btn');
+ const findDeleteBtn = () => wrapper.findByTestId('delete-btn');
+
+ const createComponent = () => {
+ const { cacheConfig, localMutations } = mockState;
+
+ wrapper = shallowMountExtended(RunnerBulkDelete, {
+ apolloProvider: createMockApollo(undefined, undefined, cacheConfig),
+ provide: {
+ localMutations,
+ },
+ stubs: {
+ GlSprintf,
+ },
+ });
+ };
+
+ beforeEach(() => {
+ mockState = createLocalState();
+
+ jest
+ .spyOn(mockState.cacheConfig.typePolicies.Query.fields, 'checkedRunnerIds')
+ .mockImplementation(() => mockCheckedRunnerIds);
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('When no runners are checked', () => {
+ beforeEach(async () => {
+ mockCheckedRunnerIds = [];
+
+ createComponent();
+
+ await waitForPromises();
+ });
+
+ it('shows no contents', () => {
+ expect(wrapper.html()).toBe('');
+ });
+ });
+
+ describe.each`
+ count | ids | text
+ ${1} | ${['gid:Runner/1']} | ${'1 runner'}
+ ${2} | ${['gid:Runner/1', 'gid:Runner/2']} | ${'2 runners'}
+ `('When $count runner(s) are checked', ({ count, ids, text }) => {
+ beforeEach(() => {
+ mockCheckedRunnerIds = ids;
+
+ createComponent();
+
+ jest.spyOn(mockState.localMutations, 'clearChecked').mockImplementation(() => {});
+ });
+
+ it(`shows "${text}"`, () => {
+ expect(wrapper.text()).toContain(text);
+ });
+
+ it('clears selection', () => {
+ expect(mockState.localMutations.clearChecked).toHaveBeenCalledTimes(0);
+
+ findClearBtn().vm.$emit('click');
+
+ expect(mockState.localMutations.clearChecked).toHaveBeenCalledTimes(1);
+ });
+
+ it('shows confirmation modal', () => {
+ expect(confirmAction).toHaveBeenCalledTimes(0);
+
+ findDeleteBtn().vm.$emit('click');
+
+ expect(confirmAction).toHaveBeenCalledTimes(1);
+
+ const [, confirmOptions] = confirmAction.mock.calls[0];
+ const { title, modalHtmlMessage, primaryBtnText } = confirmOptions;
+
+ expect(title).toMatch(text);
+ expect(primaryBtnText).toMatch(text);
+ expect(modalHtmlMessage).toMatch(`<strong>${count}</strong>`);
+ });
+ });
+});
diff --git a/spec/frontend/runner/components/runner_delete_button_spec.js b/spec/frontend/runner/components/runner_delete_button_spec.js
index 81c870f23cf..3eb257607b4 100644
--- a/spec/frontend/runner/components/runner_delete_button_spec.js
+++ b/spec/frontend/runner/components/runner_delete_button_spec.js
@@ -9,7 +9,11 @@ import waitForPromises from 'helpers/wait_for_promises';
import { captureException } from '~/runner/sentry_utils';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { createAlert } from '~/flash';
-import { I18N_DELETE_RUNNER } from '~/runner/constants';
+import {
+ I18N_DELETE_RUNNER,
+ I18N_DELETE_DISABLED_MANY_PROJECTS,
+ I18N_DELETE_DISABLED_UNKNOWN_REASON,
+} from '~/runner/constants';
import RunnerDeleteButton from '~/runner/components/runner_delete_button.vue';
import RunnerDeleteModal from '~/runner/components/runner_delete_modal.vue';
@@ -25,26 +29,32 @@ jest.mock('~/runner/sentry_utils');
describe('RunnerDeleteButton', () => {
let wrapper;
+ let apolloProvider;
+ let apolloCache;
let runnerDeleteHandler;
- const getTooltip = () => getBinding(wrapper.element, 'gl-tooltip').value;
- const getModal = () => getBinding(wrapper.element, 'gl-modal').value;
const findBtn = () => wrapper.findComponent(GlButton);
const findModal = () => wrapper.findComponent(RunnerDeleteModal);
+ const getTooltip = () => getBinding(wrapper.element, 'gl-tooltip').value;
+ const getModal = () => getBinding(findBtn().element, 'gl-modal').value;
+
const createComponent = ({ props = {}, mountFn = shallowMountExtended } = {}) => {
const { runner, ...propsData } = props;
wrapper = mountFn(RunnerDeleteButton, {
propsData: {
runner: {
+ // We need typename so that cache.identify works
+ // eslint-disable-next-line no-underscore-dangle
+ __typename: mockRunner.__typename,
id: mockRunner.id,
shortSha: mockRunner.shortSha,
...runner,
},
...propsData,
},
- apolloProvider: createMockApollo([[runnerDeleteMutation, runnerDeleteHandler]]),
+ apolloProvider,
directives: {
GlTooltip: createMockDirective(),
GlModal: createMockDirective(),
@@ -67,6 +77,11 @@ describe('RunnerDeleteButton', () => {
},
});
});
+ apolloProvider = createMockApollo([[runnerDeleteMutation, runnerDeleteHandler]]);
+ apolloCache = apolloProvider.defaultClient.cache;
+
+ jest.spyOn(apolloCache, 'evict');
+ jest.spyOn(apolloCache, 'gc');
createComponent();
});
@@ -88,6 +103,10 @@ describe('RunnerDeleteButton', () => {
expect(findModal().props('runnerName')).toBe(`#${mockRunnerId} (${mockRunner.shortSha})`);
});
+ it('Does not have tabindex when button is enabled', () => {
+ expect(wrapper.attributes('tabindex')).toBeUndefined();
+ });
+
it('Displays a modal when clicked', () => {
const modalId = `delete-runner-modal-${mockRunnerId}`;
@@ -140,6 +159,13 @@ describe('RunnerDeleteButton', () => {
expect(deleted[0][0].message).toMatch(`#${mockRunnerId}`);
expect(deleted[0][0].message).toMatch(`${mockRunner.shortSha}`);
});
+
+ it('evicts runner from apollo cache', () => {
+ expect(apolloCache.evict).toHaveBeenCalledWith({
+ id: apolloCache.identify(mockRunner),
+ });
+ expect(apolloCache.gc).toHaveBeenCalled();
+ });
});
describe('When update fails', () => {
@@ -190,6 +216,11 @@ describe('RunnerDeleteButton', () => {
it('error is shown to the user', () => {
expect(createAlert).toHaveBeenCalledTimes(1);
});
+
+ it('does not evict runner from apollo cache', () => {
+ expect(apolloCache.evict).not.toHaveBeenCalled();
+ expect(apolloCache.gc).not.toHaveBeenCalled();
+ });
});
});
@@ -230,4 +261,29 @@ describe('RunnerDeleteButton', () => {
});
});
});
+
+ describe.each`
+ reason | runner | tooltip
+ ${'runner belongs to more than 1 project'} | ${{ projectCount: 2 }} | ${I18N_DELETE_DISABLED_MANY_PROJECTS}
+ ${'unknown reason'} | ${{}} | ${I18N_DELETE_DISABLED_UNKNOWN_REASON}
+ `('When button is disabled because $reason', ({ runner, tooltip }) => {
+ beforeEach(() => {
+ createComponent({
+ props: {
+ disabled: true,
+ runner,
+ },
+ });
+ });
+
+ it('Displays a disabled delete button', () => {
+ expect(findBtn().props('disabled')).toBe(true);
+ });
+
+ it(`Tooltip "${tooltip}" is shown`, () => {
+ // tabindex is required for a11y
+ expect(wrapper.attributes('tabindex')).toBe('0');
+ expect(getTooltip()).toBe(tooltip);
+ });
+ });
});
diff --git a/spec/frontend/runner/components/runner_filtered_search_bar_spec.js b/spec/frontend/runner/components/runner_filtered_search_bar_spec.js
index fda96e5918e..b1b436e5443 100644
--- a/spec/frontend/runner/components/runner_filtered_search_bar_spec.js
+++ b/spec/frontend/runner/components/runner_filtered_search_bar_spec.js
@@ -4,7 +4,7 @@ import RunnerFilteredSearchBar from '~/runner/components/runner_filtered_search_
import { statusTokenConfig } from '~/runner/components/search_tokens/status_token_config';
import TagToken from '~/runner/components/search_tokens/tag_token.vue';
import { tagTokenConfig } from '~/runner/components/search_tokens/tag_token_config';
-import { PARAM_KEY_STATUS, PARAM_KEY_TAG, STATUS_ACTIVE, INSTANCE_TYPE } from '~/runner/constants';
+import { PARAM_KEY_STATUS, PARAM_KEY_TAG, STATUS_ONLINE, INSTANCE_TYPE } from '~/runner/constants';
import FilteredSearch from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_token.vue';
@@ -18,7 +18,7 @@ describe('RunnerList', () => {
const mockDefaultSort = 'CREATED_DESC';
const mockOtherSort = 'CONTACTED_DESC';
const mockFilters = [
- { type: PARAM_KEY_STATUS, value: { data: STATUS_ACTIVE, operator: '=' } },
+ { type: PARAM_KEY_STATUS, value: { data: STATUS_ONLINE, operator: '=' } },
{ type: 'filtered-search-term', value: { data: '' } },
];
@@ -113,7 +113,7 @@ describe('RunnerList', () => {
});
it('filter values are shown', () => {
- expect(findGlFilteredSearch().props('value')).toEqual(mockFilters);
+ expect(findGlFilteredSearch().props('value')).toMatchObject(mockFilters);
});
it('sort option is selected', () => {
diff --git a/spec/frontend/runner/components/runner_jobs_spec.js b/spec/frontend/runner/components/runner_jobs_spec.js
index 9abb2861005..9e40e911448 100644
--- a/spec/frontend/runner/components/runner_jobs_spec.js
+++ b/spec/frontend/runner/components/runner_jobs_spec.js
@@ -1,4 +1,4 @@
-import { GlSkeletonLoading } from '@gitlab/ui';
+import { GlDeprecatedSkeletonLoading as GlSkeletonLoading } from '@gitlab/ui';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
diff --git a/spec/frontend/runner/components/runner_list_spec.js b/spec/frontend/runner/components/runner_list_spec.js
index a0f42738d2c..872394430ae 100644
--- a/spec/frontend/runner/components/runner_list_spec.js
+++ b/spec/frontend/runner/components/runner_list_spec.js
@@ -6,7 +6,8 @@ import {
} from 'helpers/vue_test_utils_helper';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import RunnerList from '~/runner/components/runner_list.vue';
-import { runnersData } from '../mock_data';
+import RunnerStatusPopover from '~/runner/components/runner_status_popover.vue';
+import { runnersData, onlineContactTimeoutSecs, staleTimeoutSecs } from '../mock_data';
const mockRunners = runnersData.data.runners.nodes;
const mockActiveRunnersCount = mockRunners.length;
@@ -28,26 +29,38 @@ describe('RunnerList', () => {
activeRunnersCount: mockActiveRunnersCount,
...props,
},
+ provide: {
+ onlineContactTimeoutSecs,
+ staleTimeoutSecs,
+ },
...options,
});
};
- beforeEach(() => {
- createComponent({}, mountExtended);
- });
-
afterEach(() => {
wrapper.destroy();
});
it('Displays headers', () => {
+ createComponent(
+ {
+ stubs: {
+ RunnerStatusPopover: {
+ template: '<div/>',
+ },
+ },
+ },
+ mountExtended,
+ );
+
const headerLabels = findHeaders().wrappers.map((w) => w.text());
+ expect(findHeaders().at(0).findComponent(RunnerStatusPopover).exists()).toBe(true);
+
expect(headerLabels).toEqual([
'Status',
'Runner',
'Version',
- 'IP',
'Jobs',
'Tags',
'Last contact',
@@ -56,19 +69,23 @@ describe('RunnerList', () => {
});
it('Sets runner id as a row key', () => {
- createComponent({});
+ createComponent();
expect(findTable().attributes('primary-key')).toBe('id');
});
it('Displays a list of runners', () => {
+ createComponent({}, mountExtended);
+
expect(findRows()).toHaveLength(4);
expect(findSkeletonLoader().exists()).toBe(false);
});
it('Displays details of a runner', () => {
- const { id, description, version, ipAddress, shortSha } = mockRunners[0];
+ const { id, description, version, shortSha } = mockRunners[0];
+
+ createComponent({}, mountExtended);
// Badges
expect(findCell({ fieldKey: 'status' }).text()).toMatchInterpolatedText(
@@ -83,7 +100,6 @@ describe('RunnerList', () => {
// Other fields
expect(findCell({ fieldKey: 'version' }).text()).toBe(version);
- expect(findCell({ fieldKey: 'ipAddress' }).text()).toBe(ipAddress);
expect(findCell({ fieldKey: 'jobCount' }).text()).toBe('0');
expect(findCell({ fieldKey: 'tagList' }).text()).toBe('');
expect(findCell({ fieldKey: 'contactedAt' }).text()).toEqual(expect.any(String));
@@ -92,6 +108,35 @@ describe('RunnerList', () => {
expect(findCell({ fieldKey: 'actions' }).exists()).toBe(true);
});
+ describe('When the list is checkable', () => {
+ beforeEach(() => {
+ createComponent(
+ {
+ props: {
+ checkable: true,
+ },
+ },
+ mountExtended,
+ );
+ });
+
+ it('Displays a checkbox field', () => {
+ expect(findCell({ fieldKey: 'checkbox' }).find('input').exists()).toBe(true);
+ });
+
+ it('Emits a checked event', () => {
+ const checkbox = findCell({ fieldKey: 'checkbox' }).find('input');
+
+ checkbox.setChecked();
+
+ expect(wrapper.emitted('checked')).toHaveLength(1);
+ expect(wrapper.emitted('checked')[0][0]).toEqual({
+ isChecked: true,
+ runner: mockRunners[0],
+ });
+ });
+ });
+
describe('Scoped cell slots', () => {
it('Render #runner-name slot in "summary" cell', () => {
createComponent(
@@ -156,6 +201,8 @@ describe('RunnerList', () => {
const { id, shortSha } = mockRunners[0];
const numericId = getIdFromGraphQLId(id);
+ createComponent({}, mountExtended);
+
expect(findCell({ fieldKey: 'summary' }).text()).toContain(`#${numericId} (${shortSha})`);
});
diff --git a/spec/frontend/runner/components/runner_pause_button_spec.js b/spec/frontend/runner/components/runner_pause_button_spec.js
index 3d9df03977e..9ebb30b6ed7 100644
--- a/spec/frontend/runner/components/runner_pause_button_spec.js
+++ b/spec/frontend/runner/components/runner_pause_button_spec.js
@@ -146,6 +146,10 @@ describe('RunnerPauseButton', () => {
it('The button does not have a loading state', () => {
expect(findBtn().props('loading')).toBe(false);
});
+
+ it('The button emits toggledPaused', () => {
+ expect(wrapper.emitted('toggledPaused')).toHaveLength(1);
+ });
});
describe('When update fails', () => {
diff --git a/spec/frontend/runner/components/runner_projects_spec.js b/spec/frontend/runner/components/runner_projects_spec.js
index 96de8d11bca..62ebc6539e2 100644
--- a/spec/frontend/runner/components/runner_projects_spec.js
+++ b/spec/frontend/runner/components/runner_projects_spec.js
@@ -1,4 +1,4 @@
-import { GlSkeletonLoading } from '@gitlab/ui';
+import { GlDeprecatedSkeletonLoading as GlSkeletonLoading } from '@gitlab/ui';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
diff --git a/spec/frontend/runner/components/runner_status_badge_spec.js b/spec/frontend/runner/components/runner_status_badge_spec.js
index c470c6bb989..bb833bd7d5a 100644
--- a/spec/frontend/runner/components/runner_status_badge_spec.js
+++ b/spec/frontend/runner/components/runner_status_badge_spec.js
@@ -7,6 +7,8 @@ import {
STATUS_OFFLINE,
STATUS_STALE,
STATUS_NEVER_CONTACTED,
+ I18N_NEVER_CONTACTED_TOOLTIP,
+ I18N_STALE_NEVER_CONTACTED_TOOLTIP,
} from '~/runner/constants';
describe('RunnerTypeBadge', () => {
@@ -59,7 +61,7 @@ describe('RunnerTypeBadge', () => {
expect(wrapper.text()).toBe('never contacted');
expect(findBadge().props('variant')).toBe('muted');
- expect(getTooltip().value).toMatch('This runner has never contacted');
+ expect(getTooltip().value).toBe(I18N_NEVER_CONTACTED_TOOLTIP);
});
it('renders offline state', () => {
@@ -72,9 +74,7 @@ describe('RunnerTypeBadge', () => {
expect(wrapper.text()).toBe('offline');
expect(findBadge().props('variant')).toBe('muted');
- expect(getTooltip().value).toBe(
- 'No recent contact from this runner; last contact was 1 day ago',
- );
+ expect(getTooltip().value).toBe('Runner is offline; last contact was 1 day ago');
});
it('renders stale state', () => {
@@ -87,7 +87,20 @@ describe('RunnerTypeBadge', () => {
expect(wrapper.text()).toBe('stale');
expect(findBadge().props('variant')).toBe('warning');
- expect(getTooltip().value).toBe('No contact from this runner in over 3 months');
+ expect(getTooltip().value).toBe('Runner is stale; last contact was 1 year ago');
+ });
+
+ it('renders stale state with no contact time', () => {
+ createComponent({
+ runner: {
+ contactedAt: null,
+ status: STATUS_STALE,
+ },
+ });
+
+ expect(wrapper.text()).toBe('stale');
+ expect(findBadge().props('variant')).toBe('warning');
+ expect(getTooltip().value).toBe(I18N_STALE_NEVER_CONTACTED_TOOLTIP);
});
describe('does not fail when data is missing', () => {
@@ -100,7 +113,7 @@ describe('RunnerTypeBadge', () => {
});
expect(wrapper.text()).toBe('online');
- expect(getTooltip().value).toBe('Runner is online; last contact was n/a');
+ expect(getTooltip().value).toBe('Runner is online; last contact was never');
});
it('status is missing', () => {
diff --git a/spec/frontend/runner/components/runner_status_popover_spec.js b/spec/frontend/runner/components/runner_status_popover_spec.js
new file mode 100644
index 00000000000..789283d1245
--- /dev/null
+++ b/spec/frontend/runner/components/runner_status_popover_spec.js
@@ -0,0 +1,36 @@
+import { GlSprintf } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import RunnerStatusPopover from '~/runner/components/runner_status_popover.vue';
+import HelpPopover from '~/vue_shared/components/help_popover.vue';
+import { onlineContactTimeoutSecs, staleTimeoutSecs } from '../mock_data';
+
+describe('RunnerStatusPopover', () => {
+ let wrapper;
+
+ const createComponent = ({ provide = {} } = {}) => {
+ wrapper = shallowMountExtended(RunnerStatusPopover, {
+ provide: {
+ onlineContactTimeoutSecs,
+ staleTimeoutSecs,
+ ...provide,
+ },
+ stubs: {
+ GlSprintf,
+ },
+ });
+ };
+
+ const findHelpPopover = () => wrapper.findComponent(HelpPopover);
+
+ it('renders popoover', () => {
+ createComponent();
+
+ expect(findHelpPopover().exists()).toBe(true);
+ });
+
+ it('renders complete text', () => {
+ createComponent();
+
+ expect(findHelpPopover().text()).toMatchSnapshot();
+ });
+});
diff --git a/spec/frontend/runner/graphql/local_state_spec.js b/spec/frontend/runner/graphql/local_state_spec.js
new file mode 100644
index 00000000000..5c4302e4aa2
--- /dev/null
+++ b/spec/frontend/runner/graphql/local_state_spec.js
@@ -0,0 +1,72 @@
+import createApolloClient from '~/lib/graphql';
+import { createLocalState } from '~/runner/graphql/list/local_state';
+import getCheckedRunnerIdsQuery from '~/runner/graphql/list/checked_runner_ids.query.graphql';
+
+describe('~/runner/graphql/list/local_state', () => {
+ let localState;
+ let apolloClient;
+
+ const createSubject = () => {
+ if (apolloClient) {
+ throw new Error('test subject already exists!');
+ }
+
+ localState = createLocalState();
+
+ const { cacheConfig, typeDefs } = localState;
+
+ apolloClient = createApolloClient({}, { cacheConfig, typeDefs });
+ };
+
+ const queryCheckedRunnerIds = () => {
+ const { checkedRunnerIds } = apolloClient.readQuery({
+ query: getCheckedRunnerIdsQuery,
+ });
+ return checkedRunnerIds;
+ };
+
+ beforeEach(() => {
+ createSubject();
+ });
+
+ afterEach(() => {
+ localState = null;
+ apolloClient = null;
+ });
+
+ describe('default', () => {
+ it('has empty checked list', () => {
+ expect(queryCheckedRunnerIds()).toEqual([]);
+ });
+ });
+
+ describe.each`
+ inputs | expected
+ ${[['a', true], ['b', true], ['b', true]]} | ${['a', 'b']}
+ ${[['a', true], ['b', true], ['a', false]]} | ${['b']}
+ ${[['c', true], ['b', true], ['a', true], ['d', false]]} | ${['c', 'b', 'a']}
+ `('setRunnerChecked', ({ inputs, expected }) => {
+ beforeEach(() => {
+ inputs.forEach(([id, isChecked]) => {
+ localState.localMutations.setRunnerChecked({ runner: { id }, isChecked });
+ });
+ });
+ it(`for inputs="${inputs}" has a ids="[${expected}]"`, () => {
+ expect(queryCheckedRunnerIds()).toEqual(expected);
+ });
+ });
+
+ describe('clearChecked', () => {
+ it('clears all checked items', () => {
+ ['a', 'b', 'c'].forEach((id) => {
+ localState.localMutations.setRunnerChecked({ runner: { id }, isChecked: true });
+ });
+
+ expect(queryCheckedRunnerIds()).toEqual(['a', 'b', 'c']);
+
+ localState.localMutations.clearChecked();
+
+ expect(queryCheckedRunnerIds()).toEqual([]);
+ });
+ });
+});
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 70e303e8626..02348bf737a 100644
--- a/spec/frontend/runner/group_runners/group_runners_app_spec.js
+++ b/spec/frontend/runner/group_runners/group_runners_app_spec.js
@@ -28,8 +28,9 @@ import {
INSTANCE_TYPE,
GROUP_TYPE,
PROJECT_TYPE,
+ PARAM_KEY_PAUSED,
PARAM_KEY_STATUS,
- STATUS_ACTIVE,
+ STATUS_ONLINE,
RUNNER_PAGE_SIZE,
I18N_EDIT,
} from '~/runner/constants';
@@ -38,7 +39,13 @@ import getGroupRunnersCountQuery from '~/runner/graphql/list/group_runners_count
import GroupRunnersApp from '~/runner/group_runners/group_runners_app.vue';
import { captureException } from '~/runner/sentry_utils';
import FilteredSearch from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
-import { groupRunnersData, groupRunnersDataPaginated, groupRunnersCountData } from '../mock_data';
+import {
+ groupRunnersData,
+ groupRunnersDataPaginated,
+ groupRunnersCountData,
+ onlineContactTimeoutSecs,
+ staleTimeoutSecs,
+} from '../mock_data';
Vue.use(VueApollo);
Vue.use(GlToast);
@@ -90,6 +97,10 @@ describe('GroupRunnersApp', () => {
groupRunnersLimitedCount: mockGroupRunnersLimitedCount,
...props,
},
+ provide: {
+ onlineContactTimeoutSecs,
+ staleTimeoutSecs,
+ },
});
};
@@ -178,13 +189,16 @@ describe('GroupRunnersApp', () => {
const tokens = findFilteredSearch().props('tokens');
- expect(tokens).toHaveLength(1);
- expect(tokens[0]).toEqual(
+ expect(tokens).toEqual([
+ expect.objectContaining({
+ type: PARAM_KEY_PAUSED,
+ options: expect.any(Array),
+ }),
expect.objectContaining({
type: PARAM_KEY_STATUS,
options: expect.any(Array),
}),
- );
+ ]);
});
describe('Single runner row', () => {
@@ -193,9 +207,11 @@ describe('GroupRunnersApp', () => {
const { webUrl, editUrl, node } = mockGroupRunnersEdges[0];
const { id: graphqlId, shortSha } = node;
const id = getIdFromGraphQLId(graphqlId);
+ const COUNT_QUERIES = 6; // Smart queries that display a filtered count of runners
+ const FILTERED_COUNT_QUERIES = 3; // Smart queries that display a count of runners in tabs
beforeEach(async () => {
- mockGroupRunnersQuery.mockClear();
+ mockGroupRunnersCountQuery.mockClear();
createComponent({ mountFn: mountExtended });
showToast = jest.spyOn(wrapper.vm.$root.$toast, 'show');
@@ -219,12 +235,20 @@ describe('GroupRunnersApp', () => {
});
});
- it('When runner is deleted, data is refetched and a toast is shown', async () => {
- expect(mockGroupRunnersQuery).toHaveBeenCalledTimes(1);
+ it('When runner is paused or unpaused, some data is refetched', async () => {
+ expect(mockGroupRunnersCountQuery).toHaveBeenCalledTimes(COUNT_QUERIES);
- findRunnerActionsCell().vm.$emit('deleted', { message: 'Runner deleted' });
+ findRunnerActionsCell().vm.$emit('toggledPaused');
+
+ expect(mockGroupRunnersCountQuery).toHaveBeenCalledTimes(
+ COUNT_QUERIES + FILTERED_COUNT_QUERIES,
+ );
- expect(mockGroupRunnersQuery).toHaveBeenCalledTimes(2);
+ expect(showToast).toHaveBeenCalledTimes(0);
+ });
+
+ it('When runner is deleted, data is refetched and a toast message is shown', async () => {
+ findRunnerActionsCell().vm.$emit('deleted', { message: 'Runner deleted' });
expect(showToast).toHaveBeenCalledTimes(1);
expect(showToast).toHaveBeenCalledWith('Runner deleted');
@@ -233,7 +257,7 @@ describe('GroupRunnersApp', () => {
describe('when a filter is preselected', () => {
beforeEach(async () => {
- setWindowLocation(`?status[]=${STATUS_ACTIVE}&runner_type[]=${INSTANCE_TYPE}`);
+ setWindowLocation(`?status[]=${STATUS_ONLINE}&runner_type[]=${INSTANCE_TYPE}`);
createComponent();
await waitForPromises();
@@ -242,7 +266,7 @@ describe('GroupRunnersApp', () => {
it('sets the filters in the search bar', () => {
expect(findRunnerFilteredSearchBar().props('value')).toEqual({
runnerType: INSTANCE_TYPE,
- filters: [{ type: 'status', value: { data: STATUS_ACTIVE, operator: '=' } }],
+ filters: [{ type: 'status', value: { data: STATUS_ONLINE, operator: '=' } }],
sort: 'CREATED_DESC',
pagination: { page: 1 },
});
@@ -251,7 +275,7 @@ describe('GroupRunnersApp', () => {
it('requests the runners with filter parameters', () => {
expect(mockGroupRunnersQuery).toHaveBeenLastCalledWith({
groupFullPath: mockGroupFullPath,
- status: STATUS_ACTIVE,
+ status: STATUS_ONLINE,
type: INSTANCE_TYPE,
sort: DEFAULT_SORT,
first: RUNNER_PAGE_SIZE,
@@ -263,7 +287,7 @@ describe('GroupRunnersApp', () => {
beforeEach(async () => {
findRunnerFilteredSearchBar().vm.$emit('input', {
runnerType: null,
- filters: [{ type: PARAM_KEY_STATUS, value: { data: STATUS_ACTIVE, operator: '=' } }],
+ filters: [{ type: PARAM_KEY_STATUS, value: { data: STATUS_ONLINE, operator: '=' } }],
sort: CREATED_ASC,
});
@@ -273,14 +297,14 @@ describe('GroupRunnersApp', () => {
it('updates the browser url', () => {
expect(updateHistory).toHaveBeenLastCalledWith({
title: expect.any(String),
- url: 'http://test.host/groups/group1/-/runners?status[]=ACTIVE&sort=CREATED_ASC',
+ url: 'http://test.host/groups/group1/-/runners?status[]=ONLINE&sort=CREATED_ASC',
});
});
it('requests the runners with filters', () => {
expect(mockGroupRunnersQuery).toHaveBeenLastCalledWith({
groupFullPath: mockGroupFullPath,
- status: STATUS_ACTIVE,
+ status: STATUS_ONLINE,
sort: CREATED_ASC,
first: RUNNER_PAGE_SIZE,
});
diff --git a/spec/frontend/runner/mock_data.js b/spec/frontend/runner/mock_data.js
index 49c25039719..fbe8926124c 100644
--- a/spec/frontend/runner/mock_data.js
+++ b/spec/frontend/runner/mock_data.js
@@ -14,6 +14,10 @@ import runnerWithGroupData from 'test_fixtures/graphql/runner/details/runner.que
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,
diff --git a/spec/frontend/runner/runner_search_utils_spec.js b/spec/frontend/runner/runner_search_utils_spec.js
index aff1ec882bb..7834e76fe48 100644
--- a/spec/frontend/runner/runner_search_utils_spec.js
+++ b/spec/frontend/runner/runner_search_utils_spec.js
@@ -181,6 +181,28 @@ describe('search_params.js', () => {
first: RUNNER_PAGE_SIZE,
},
},
+ {
+ name: 'paused runners',
+ urlQuery: '?paused[]=true',
+ search: {
+ runnerType: null,
+ filters: [{ type: 'paused', value: { data: 'true', operator: '=' } }],
+ pagination: { page: 1 },
+ sort: 'CREATED_DESC',
+ },
+ graphqlVariables: { paused: true, sort: 'CREATED_DESC', first: RUNNER_PAGE_SIZE },
+ },
+ {
+ name: 'active runners',
+ urlQuery: '?paused[]=false',
+ search: {
+ runnerType: null,
+ filters: [{ type: 'paused', value: { data: 'false', operator: '=' } }],
+ pagination: { page: 1 },
+ sort: 'CREATED_DESC',
+ },
+ graphqlVariables: { paused: false, sort: 'CREATED_DESC', first: RUNNER_PAGE_SIZE },
+ },
];
describe('searchValidator', () => {
@@ -197,14 +219,18 @@ describe('search_params.js', () => {
expect(updateOutdatedUrl('http://test.host/?a=b')).toBe(null);
});
- it('returns updated url for updating NOT_CONNECTED to NEVER_CONTACTED', () => {
- expect(updateOutdatedUrl('http://test.host/admin/runners?status[]=NOT_CONNECTED')).toBe(
- 'http://test.host/admin/runners?status[]=NEVER_CONTACTED',
- );
+ 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'}
+ `('updates "$query" to "$updatedQuery"', ({ query, updatedQuery }) => {
+ const mockUrl = 'http://test.host/admin/runners?';
- expect(updateOutdatedUrl('http://test.host/admin/runners?status[]=NOT_CONNECTED&a=b')).toBe(
- 'http://test.host/admin/runners?status[]=NEVER_CONTACTED&a=b',
- );
+ expect(updateOutdatedUrl(`${mockUrl}${query}`)).toBe(`${mockUrl}${updatedQuery}`);
});
});
diff --git a/spec/frontend/runner/utils_spec.js b/spec/frontend/runner/utils_spec.js
index 3fa9784ecdf..1db9815dfd8 100644
--- a/spec/frontend/runner/utils_spec.js
+++ b/spec/frontend/runner/utils_spec.js
@@ -44,6 +44,10 @@ describe('~/runner/utils', () => {
thClass: expect.arrayContaining(mockClasses),
});
});
+
+ it('a field with custom options', () => {
+ expect(tableField({ foo: 'bar' })).toMatchObject({ foo: 'bar' });
+ });
});
describe('getPaginationVariables', () => {
diff --git a/spec/frontend/search/store/actions_spec.js b/spec/frontend/search/store/actions_spec.js
index 5f8cee8160f..67bd3194f20 100644
--- a/spec/frontend/search/store/actions_spec.js
+++ b/spec/frontend/search/store/actions_spec.js
@@ -56,7 +56,7 @@ describe('Global Search Store Actions', () => {
${actions.fetchGroups} | ${{ method: 'onGet', code: 200, res: MOCK_GROUPS }} | ${'success'} | ${[{ type: types.REQUEST_GROUPS }, { type: types.RECEIVE_GROUPS_SUCCESS, payload: MOCK_GROUPS }]} | ${0}
${actions.fetchGroups} | ${{ method: 'onGet', code: 500, res: null }} | ${'error'} | ${[{ type: types.REQUEST_GROUPS }, { type: types.RECEIVE_GROUPS_ERROR }]} | ${1}
${actions.fetchProjects} | ${{ method: 'onGet', code: 200, res: MOCK_PROJECTS }} | ${'success'} | ${[{ type: types.REQUEST_PROJECTS }, { type: types.RECEIVE_PROJECTS_SUCCESS, payload: MOCK_PROJECTS }]} | ${0}
- ${actions.fetchProjects} | ${{ method: 'onGet', code: 500, res: null }} | ${'error'} | ${[{ type: types.REQUEST_PROJECTS }, { type: types.RECEIVE_PROJECTS_ERROR }]} | ${2}
+ ${actions.fetchProjects} | ${{ method: 'onGet', code: 500, res: null }} | ${'error'} | ${[{ type: types.REQUEST_PROJECTS }, { type: types.RECEIVE_PROJECTS_ERROR }]} | ${1}
`(`axios calls`, ({ action, axiosMock, type, expectedMutations, flashCallCount }) => {
describe(action.name, () => {
describe(`on ${type}`, () => {
@@ -121,8 +121,8 @@ describe('Global Search Store Actions', () => {
describe('when groupId is set', () => {
it('calls Api.groupProjects with expected parameters', () => {
- actions.fetchProjects({ commit: mockCommit, state });
-
+ const callbackTest = jest.fn();
+ actions.fetchProjects({ commit: mockCommit, state }, undefined, callbackTest);
expect(Api.groupProjects).toHaveBeenCalledWith(
state.query.group_id,
state.query.search,
@@ -131,7 +131,8 @@ describe('Global Search Store Actions', () => {
include_subgroups: true,
with_shared: false,
},
- expect.any(Function),
+ callbackTest,
+ true,
);
expect(Api.projects).not.toHaveBeenCalled();
});
@@ -144,15 +145,10 @@ describe('Global Search Store Actions', () => {
it('calls Api.projects', () => {
actions.fetchProjects({ commit: mockCommit, state });
-
expect(Api.groupProjects).not.toHaveBeenCalled();
- expect(Api.projects).toHaveBeenCalledWith(
- state.query.search,
- {
- order_by: 'similarity',
- },
- expect.any(Function),
- );
+ expect(Api.projects).toHaveBeenCalledWith(state.query.search, {
+ order_by: 'similarity',
+ });
});
});
});
diff --git a/spec/frontend/search_autocomplete_spec.js b/spec/frontend/search_autocomplete_spec.js
index c643cf6557d..190f2803324 100644
--- a/spec/frontend/search_autocomplete_spec.js
+++ b/spec/frontend/search_autocomplete_spec.js
@@ -223,34 +223,22 @@ describe('Search autocomplete dropdown', () => {
});
}
- it('suggest Projects', (done) => {
- // eslint-disable-next-line promise/catch-or-return
- triggerAutocomplete().finally(() => {
- const list = widget.wrap.find('.dropdown-menu').find('ul');
- const link = "a[href$='/gitlab-org/gitlab-test']";
+ it('suggest Projects', async () => {
+ await triggerAutocomplete();
- expect(list.find(link).length).toBe(1);
+ const list = widget.wrap.find('.dropdown-menu').find('ul');
+ const link = "a[href$='/gitlab-org/gitlab-test']";
- done();
- });
-
- // Make sure jest properly acknowledge the `done` invocation
- jest.runOnlyPendingTimers();
+ expect(list.find(link).length).toBe(1);
});
- it('suggest Groups', (done) => {
- // eslint-disable-next-line promise/catch-or-return
- triggerAutocomplete().finally(() => {
- const list = widget.wrap.find('.dropdown-menu').find('ul');
- const link = "a[href$='/gitlab-org']";
+ it('suggest Groups', async () => {
+ await triggerAutocomplete();
- expect(list.find(link).length).toBe(1);
-
- done();
- });
+ const list = widget.wrap.find('.dropdown-menu').find('ul');
+ const link = "a[href$='/gitlab-org']";
- // Make sure jest properly acknowledge the `done` invocation
- jest.runOnlyPendingTimers();
+ expect(list.find(link).length).toBe(1);
});
});
diff --git a/spec/frontend/search_settings/components/search_settings_spec.js b/spec/frontend/search_settings/components/search_settings_spec.js
index 6beaea8dba5..d0a2018c7f0 100644
--- a/spec/frontend/search_settings/components/search_settings_spec.js
+++ b/spec/frontend/search_settings/components/search_settings_spec.js
@@ -1,5 +1,6 @@
import { GlSearchBoxByType } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
+import { setHTMLFixture } from 'helpers/fixtures';
import SearchSettings from '~/search_settings/components/search_settings.vue';
import { HIGHLIGHT_CLASS, HIDE_CLASS } from '~/search_settings/constants';
import { isExpanded, expandSection, closeSection } from '~/settings_panels';
@@ -11,7 +12,8 @@ describe('search_settings/components/search_settings.vue', () => {
const GENERAL_SETTINGS_ID = 'js-general-settings';
const ADVANCED_SETTINGS_ID = 'js-advanced-settings';
const EXTRA_SETTINGS_ID = 'js-extra-settings';
- const TEXT_CONTAIN_SEARCH_TERM = `This text contain ${SEARCH_TERM} and <script>alert("111")</script> others.`;
+ const TEXT_CONTAIN_SEARCH_TERM = `This text contain ${SEARCH_TERM}.`;
+ const TEXT_WITH_SIBLING_ELEMENTS = `${SEARCH_TERM} <a data-testid="sibling" href="#">Learn more</a>.`;
let wrapper;
@@ -42,13 +44,7 @@ describe('search_settings/components/search_settings.vue', () => {
});
};
- const matchParentElement = () => {
- const highlightedList = Array.from(document.querySelectorAll(`.${HIGHLIGHT_CLASS}`));
- return highlightedList.map((element) => {
- return element.parentNode;
- });
- };
-
+ const findMatchSiblingElement = () => document.querySelector(`[data-testid="sibling"]`);
const findSearchBox = () => wrapper.find(GlSearchBoxByType);
const search = (term) => {
findSearchBox().vm.$emit('input', term);
@@ -56,7 +52,7 @@ describe('search_settings/components/search_settings.vue', () => {
const clearSearch = () => search('');
beforeEach(() => {
- setFixtures(`
+ setHTMLFixture(`
<div>
<div class="js-search-app"></div>
<div id="${ROOT_ID}">
@@ -69,6 +65,7 @@ describe('search_settings/components/search_settings.vue', () => {
<section id="${EXTRA_SETTINGS_ID}" class="settings">
<span>${SEARCH_TERM}</span>
<span>${TEXT_CONTAIN_SEARCH_TERM}</span>
+ <span>${TEXT_WITH_SIBLING_ELEMENTS}</span>
</section>
</div>
</div>
@@ -99,7 +96,7 @@ describe('search_settings/components/search_settings.vue', () => {
it('highlight elements that match the search term', () => {
search(SEARCH_TERM);
- expect(highlightedElementsCount()).toBe(2);
+ expect(highlightedElementsCount()).toBe(3);
});
it('highlight only search term and not the whole line', () => {
@@ -108,14 +105,26 @@ describe('search_settings/components/search_settings.vue', () => {
expect(highlightedTextNodes()).toBe(true);
});
- it('prevents search xss', () => {
+ // Regression test for https://gitlab.com/gitlab-org/gitlab/-/issues/350494
+ it('preserves elements that are siblings of matches', () => {
+ const snapshot = `
+ <a
+ data-testid="sibling"
+ href="#"
+ >
+ Learn more
+ </a>
+ `;
+
+ expect(findMatchSiblingElement()).toMatchInlineSnapshot(snapshot);
+
search(SEARCH_TERM);
- const parentNodeList = matchParentElement();
- parentNodeList.forEach((element) => {
- const scriptElement = element.getElementsByTagName('script');
- expect(scriptElement.length).toBe(0);
- });
+ expect(findMatchSiblingElement()).toMatchInlineSnapshot(snapshot);
+
+ clearSearch();
+
+ expect(findMatchSiblingElement()).toMatchInlineSnapshot(snapshot);
});
describe('default', () => {
diff --git a/spec/frontend/security_configuration/components/app_spec.js b/spec/frontend/security_configuration/components/app_spec.js
index 963577fa763..9a18cb636b2 100644
--- a/spec/frontend/security_configuration/components/app_spec.js
+++ b/spec/frontend/security_configuration/components/app_spec.js
@@ -1,4 +1,4 @@
-import { GlTab, GlTabs } from '@gitlab/ui';
+import { GlTab, GlTabs, GlLink } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import { nextTick } from 'vue';
import { useLocalStorageSpy } from 'helpers/local_storage_helper';
@@ -33,6 +33,7 @@ const autoDevopsHelpPagePath = '/autoDevopsHelpPagePath';
const autoDevopsPath = '/autoDevopsPath';
const gitlabCiHistoryPath = 'test/historyPath';
const projectFullPath = 'namespace/project';
+const vulnerabilityTrainingDocsPath = 'user/application_security/vulnerabilities/index';
useLocalStorageSpy();
@@ -55,6 +56,7 @@ describe('App component', () => {
autoDevopsHelpPagePath,
autoDevopsPath,
projectFullPath,
+ vulnerabilityTrainingDocsPath,
glFeatures: {
secureVulnerabilityTraining,
},
@@ -107,6 +109,7 @@ describe('App component', () => {
const findUpgradeBanner = () => wrapper.findComponent(UpgradeBanner);
const findAutoDevopsAlert = () => wrapper.findComponent(AutoDevopsAlert);
const findAutoDevopsEnabledAlert = () => wrapper.findComponent(AutoDevopsEnabledAlert);
+ const findVulnerabilityManagementTab = () => wrapper.findByTestId('vulnerability-management-tab');
const securityFeaturesMock = [
{
@@ -454,9 +457,14 @@ describe('App component', () => {
});
it('renders security training description', () => {
- const vulnerabilityManagementTab = wrapper.findByTestId('vulnerability-management-tab');
+ expect(findVulnerabilityManagementTab().text()).toContain(i18n.securityTrainingDescription);
+ });
+
+ it('renders link to help docs', () => {
+ const trainingLink = findVulnerabilityManagementTab().findComponent(GlLink);
- expect(vulnerabilityManagementTab.text()).toContain(i18n.securityTrainingDescription);
+ expect(trainingLink.text()).toBe('Learn more about vulnerability training');
+ expect(trainingLink.attributes('href')).toBe(vulnerabilityTrainingDocsPath);
});
});
diff --git a/spec/frontend/security_configuration/components/feature_card_badge_spec.js b/spec/frontend/security_configuration/components/feature_card_badge_spec.js
new file mode 100644
index 00000000000..dcde0808fa4
--- /dev/null
+++ b/spec/frontend/security_configuration/components/feature_card_badge_spec.js
@@ -0,0 +1,40 @@
+import { mount } from '@vue/test-utils';
+import { GlBadge, GlTooltip } from '@gitlab/ui';
+import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import FeatureCardBadge from '~/security_configuration/components/feature_card_badge.vue';
+
+describe('Feature card badge component', () => {
+ let wrapper;
+
+ const createComponent = (propsData) => {
+ wrapper = extendedWrapper(
+ mount(FeatureCardBadge, {
+ propsData,
+ }),
+ );
+ };
+
+ const findTooltip = () => wrapper.findComponent(GlTooltip);
+ const findBadge = () => wrapper.findComponent(GlBadge);
+
+ describe('tooltip render', () => {
+ describe.each`
+ context | badge | badgeHref
+ ${'href on a badge object'} | ${{ tooltipText: 'test', badgeHref: 'href' }} | ${undefined}
+ ${'href as property '} | ${{ tooltipText: null, badgeHref: '' }} | ${'link'}
+ ${'default href no property on badge or component'} | ${{ tooltipText: null, badgeHref: '' }} | ${undefined}
+ `('given $context', ({ badge, badgeHref }) => {
+ beforeEach(() => {
+ createComponent({ badge, badgeHref });
+ });
+
+ it('should show badge when badge given in configuration and available', () => {
+ expect(findTooltip().exists()).toBe(Boolean(badge && badge.tooltipText));
+ });
+
+ it('should render correct link if link is provided', () => {
+ expect(findBadge().attributes().href).toEqual(badgeHref);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/security_configuration/components/feature_card_spec.js b/spec/frontend/security_configuration/components/feature_card_spec.js
index f0d902bf9fe..d10722be8ea 100644
--- a/spec/frontend/security_configuration/components/feature_card_spec.js
+++ b/spec/frontend/security_configuration/components/feature_card_spec.js
@@ -2,6 +2,7 @@ import { GlIcon } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import FeatureCard from '~/security_configuration/components/feature_card.vue';
+import FeatureCardBadge from '~/security_configuration/components/feature_card_badge.vue';
import ManageViaMr from '~/vue_shared/security_configuration/components/manage_via_mr.vue';
import { REPORT_TYPE_SAST } from '~/vue_shared/security_reports/constants';
import { makeFeature } from './utils';
@@ -16,6 +17,7 @@ describe('FeatureCard component', () => {
propsData,
stubs: {
ManageViaMr: true,
+ FeatureCardBadge: true,
},
}),
);
@@ -24,6 +26,8 @@ describe('FeatureCard component', () => {
const findLinks = ({ text, href }) =>
wrapper.findAll(`a[href="${href}"]`).filter((link) => link.text() === text);
+ const findBadge = () => wrapper.findComponent(FeatureCardBadge);
+
const findEnableLinks = () =>
findLinks({
text: `Enable ${feature.shortName ?? feature.name}`,
@@ -262,5 +266,28 @@ describe('FeatureCard component', () => {
});
});
});
+
+ describe('information badge', () => {
+ describe.each`
+ context | available | badge
+ ${'available feature with badge'} | ${true} | ${{ text: 'test' }}
+ ${'unavailable feature without badge'} | ${false} | ${null}
+ ${'available feature without badge'} | ${true} | ${null}
+ ${'unavailable feature with badge'} | ${false} | ${{ text: 'test' }}
+ ${'available feature with empty badge'} | ${false} | ${{}}
+ `('given $context', ({ available, badge }) => {
+ beforeEach(() => {
+ feature = makeFeature({
+ available,
+ badge,
+ });
+ createComponent({ feature });
+ });
+
+ it('should show badge when badge given in configuration and available', () => {
+ expect(findBadge().exists()).toBe(Boolean(available && badge && badge.text));
+ });
+ });
+ });
});
});
diff --git a/spec/frontend/security_configuration/components/training_provider_list_spec.js b/spec/frontend/security_configuration/components/training_provider_list_spec.js
index b8c1bef0ddd..309a9cd4cd6 100644
--- a/spec/frontend/security_configuration/components/training_provider_list_spec.js
+++ b/spec/frontend/security_configuration/components/training_provider_list_spec.js
@@ -1,5 +1,13 @@
import * as Sentry from '@sentry/browser';
-import { GlAlert, GlLink, GlToggle, GlCard, GlSkeletonLoader, GlIcon } from '@gitlab/ui';
+import {
+ GlAlert,
+ GlLink,
+ GlFormRadio,
+ GlToggle,
+ GlCard,
+ GlSkeletonLoader,
+ GlIcon,
+} from '@gitlab/ui';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
@@ -87,7 +95,7 @@ describe('TrainingProviderList component', () => {
const findLinks = () => wrapper.findAllComponents(GlLink);
const findToggles = () => wrapper.findAllComponents(GlToggle);
const findFirstToggle = () => findToggles().at(0);
- const findPrimaryProviderRadios = () => wrapper.findAllByTestId('primary-provider-radio');
+ const findPrimaryProviderRadios = () => wrapper.findAllComponents(GlFormRadio);
const findLoader = () => wrapper.findComponent(GlSkeletonLoader);
const findErrorAlert = () => wrapper.findComponent(GlAlert);
const findLogos = () => wrapper.findAllByTestId('provider-logo');
@@ -177,8 +185,8 @@ describe('TrainingProviderList component', () => {
const primaryProviderRadioForCurrentCard = findPrimaryProviderRadios().at(index);
// if the given provider is not enabled it should not be possible select it as primary
- expect(primaryProviderRadioForCurrentCard.find('input').attributes('disabled')).toBe(
- isEnabled ? undefined : 'disabled',
+ expect(primaryProviderRadioForCurrentCard.attributes('disabled')).toBe(
+ isEnabled ? undefined : 'true',
);
expect(primaryProviderRadioForCurrentCard.text()).toBe(
diff --git a/spec/frontend/self_monitor/store/actions_spec.js b/spec/frontend/self_monitor/store/actions_spec.js
index 6bcb2a713ea..59ee87c4a02 100644
--- a/spec/frontend/self_monitor/store/actions_spec.js
+++ b/spec/frontend/self_monitor/store/actions_spec.js
@@ -16,27 +16,25 @@ describe('self monitor actions', () => {
});
describe('setSelfMonitor', () => {
- it('commits the SET_ENABLED mutation', (done) => {
- testAction(
+ it('commits the SET_ENABLED mutation', () => {
+ return testAction(
actions.setSelfMonitor,
null,
state,
[{ type: types.SET_ENABLED, payload: null }],
[],
- done,
);
});
});
describe('resetAlert', () => {
- it('commits the SET_ENABLED mutation', (done) => {
- testAction(
+ it('commits the SET_ENABLED mutation', () => {
+ return testAction(
actions.resetAlert,
null,
state,
[{ type: types.SET_SHOW_ALERT, payload: false }],
[],
- done,
);
});
});
@@ -54,8 +52,8 @@ describe('self monitor actions', () => {
});
});
- it('dispatches status request with job data', (done) => {
- testAction(
+ it('dispatches status request with job data', () => {
+ return testAction(
actions.requestCreateProject,
null,
state,
@@ -71,12 +69,11 @@ describe('self monitor actions', () => {
payload: '123',
},
],
- done,
);
});
- it('dispatches success with project path', (done) => {
- testAction(
+ it('dispatches success with project path', () => {
+ return testAction(
actions.requestCreateProjectStatus,
null,
state,
@@ -87,7 +84,6 @@ describe('self monitor actions', () => {
payload: { project_full_path: '/self-monitor-url' },
},
],
- done,
);
});
});
@@ -98,8 +94,8 @@ describe('self monitor actions', () => {
mock.onPost(state.createProjectEndpoint).reply(500);
});
- it('dispatches error', (done) => {
- testAction(
+ it('dispatches error', () => {
+ return testAction(
actions.requestCreateProject,
null,
state,
@@ -115,14 +111,13 @@ describe('self monitor actions', () => {
payload: new Error('Request failed with status code 500'),
},
],
- done,
);
});
});
describe('requestCreateProjectSuccess', () => {
- it('should commit the received data', (done) => {
- testAction(
+ it('should commit the received data', () => {
+ return testAction(
actions.requestCreateProjectSuccess,
{ project_full_path: '/self-monitor-url' },
state,
@@ -146,7 +141,6 @@ describe('self monitor actions', () => {
type: 'setSelfMonitor',
},
],
- done,
);
});
});
@@ -165,8 +159,8 @@ describe('self monitor actions', () => {
});
});
- it('dispatches status request with job data', (done) => {
- testAction(
+ it('dispatches status request with job data', () => {
+ return testAction(
actions.requestDeleteProject,
null,
state,
@@ -182,12 +176,11 @@ describe('self monitor actions', () => {
payload: '456',
},
],
- done,
);
});
- it('dispatches success with status', (done) => {
- testAction(
+ it('dispatches success with status', () => {
+ return testAction(
actions.requestDeleteProjectStatus,
null,
state,
@@ -198,7 +191,6 @@ describe('self monitor actions', () => {
payload: { status: 'success' },
},
],
- done,
);
});
});
@@ -209,8 +201,8 @@ describe('self monitor actions', () => {
mock.onDelete(state.deleteProjectEndpoint).reply(500);
});
- it('dispatches error', (done) => {
- testAction(
+ it('dispatches error', () => {
+ return testAction(
actions.requestDeleteProject,
null,
state,
@@ -226,14 +218,13 @@ describe('self monitor actions', () => {
payload: new Error('Request failed with status code 500'),
},
],
- done,
);
});
});
describe('requestDeleteProjectSuccess', () => {
- it('should commit mutations to remove previously set data', (done) => {
- testAction(
+ it('should commit mutations to remove previously set data', () => {
+ return testAction(
actions.requestDeleteProjectSuccess,
null,
state,
@@ -252,7 +243,6 @@ describe('self monitor actions', () => {
{ type: types.SET_LOADING, payload: false },
],
[],
- done,
);
});
});
diff --git a/spec/frontend/serverless/components/__snapshots__/empty_state_spec.js.snap b/spec/frontend/serverless/components/__snapshots__/empty_state_spec.js.snap
index f57b9418be5..0f4dfdf8a75 100644
--- a/spec/frontend/serverless/components/__snapshots__/empty_state_spec.js.snap
+++ b/spec/frontend/serverless/components/__snapshots__/empty_state_spec.js.snap
@@ -3,7 +3,7 @@
exports[`EmptyStateComponent should render content 1`] = `
"<section class=\\"gl-display-flex empty-state gl-text-center gl-flex-direction-column\\">
<div class=\\"gl-max-w-full\\">
- <div class=\\"svg-250 svg-content\\"><img src=\\"/image.svg\\" alt=\\"\\" role=\\"img\\" class=\\"gl-max-w-full\\"></div>
+ <div 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\\">
diff --git a/spec/frontend/serverless/store/actions_spec.js b/spec/frontend/serverless/store/actions_spec.js
index 61b9bd121af..5fbecf081a6 100644
--- a/spec/frontend/serverless/store/actions_spec.js
+++ b/spec/frontend/serverless/store/actions_spec.js
@@ -7,13 +7,22 @@ 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', (done) => {
+ it('should successfully fetch functions', () => {
const endpoint = '/functions';
- const mock = new MockAdapter(axios);
mock.onGet(endpoint).reply(statusCodes.OK, JSON.stringify(mockServerlessFunctions));
- testAction(
+ return testAction(
fetchFunctions,
{ functionsPath: endpoint },
{},
@@ -22,68 +31,49 @@ describe('ServerlessActions', () => {
{ type: 'requestFunctionsLoading' },
{ type: 'receiveFunctionsSuccess', payload: mockServerlessFunctions },
],
- () => {
- mock.restore();
- done();
- },
);
});
- it('should successfully retry', (done) => {
+ it('should successfully retry', () => {
const endpoint = '/functions';
- const mock = new MockAdapter(axios);
mock
.onGet(endpoint)
.reply(() => new Promise((resolve) => setTimeout(() => resolve(200), Infinity)));
- testAction(
+ return testAction(
fetchFunctions,
{ functionsPath: endpoint },
{},
[],
[{ type: 'requestFunctionsLoading' }],
- () => {
- mock.restore();
- done();
- },
);
});
});
describe('fetchMetrics', () => {
- it('should return no prometheus', (done) => {
+ it('should return no prometheus', () => {
const endpoint = '/metrics';
- const mock = new MockAdapter(axios);
mock.onGet(endpoint).reply(statusCodes.NO_CONTENT);
- testAction(
+ return testAction(
fetchMetrics,
{ metricsPath: endpoint, hasPrometheus: false },
{},
[],
[{ type: 'receiveMetricsNoPrometheus' }],
- () => {
- mock.restore();
- done();
- },
);
});
- it('should successfully fetch metrics', (done) => {
+ it('should successfully fetch metrics', () => {
const endpoint = '/metrics';
- const mock = new MockAdapter(axios);
mock.onGet(endpoint).reply(statusCodes.OK, JSON.stringify(mockMetrics));
- testAction(
+ return testAction(
fetchMetrics,
{ metricsPath: endpoint, hasPrometheus: true },
{},
[],
[{ type: 'receiveMetricsSuccess', payload: adjustMetricQuery(mockMetrics) }],
- () => {
- mock.restore();
- done();
- },
);
});
});
diff --git a/spec/frontend/set_status_modal/set_status_modal_wrapper_spec.js b/spec/frontend/set_status_modal/set_status_modal_wrapper_spec.js
index c105810e11c..0b672cbc93e 100644
--- a/spec/frontend/set_status_modal/set_status_modal_wrapper_spec.js
+++ b/spec/frontend/set_status_modal/set_status_modal_wrapper_spec.js
@@ -26,7 +26,7 @@ describe('SetStatusModalWrapper', () => {
defaultEmoji,
};
- const createComponent = (props = {}, improvedEmojiPicker = false) => {
+ const createComponent = (props = {}) => {
return shallowMount(SetStatusModalWrapper, {
propsData: {
...defaultProps,
@@ -35,19 +35,15 @@ describe('SetStatusModalWrapper', () => {
mocks: {
$toast,
},
- provide: {
- glFeatures: { improvedEmojiPicker },
- },
});
};
const findModal = () => wrapper.find(GlModal);
const findFormField = (field) => wrapper.find(`[name="user[status][${field}]"]`);
const findClearStatusButton = () => wrapper.find('.js-clear-user-status-button');
- const findNoEmojiPlaceholder = () => wrapper.find('.js-no-emoji-placeholder');
- const findToggleEmojiButton = () => wrapper.find('.js-toggle-emoji-menu');
const findAvailabilityCheckbox = () => wrapper.find(GlFormCheckbox);
const findClearStatusAtMessage = () => wrapper.find('[data-testid="clear-status-at-message"]');
+ const getEmojiPicker = () => wrapper.findComponent(EmojiPicker);
const initModal = async ({ mockOnUpdateSuccess = true, mockOnUpdateFailure = true } = {}) => {
const modal = findModal();
@@ -95,12 +91,6 @@ describe('SetStatusModalWrapper', () => {
expect(findClearStatusButton().isVisible()).toBe(true);
});
- it('clicking the toggle emoji button displays the emoji list', () => {
- expect(wrapper.vm.showEmojiMenu).not.toHaveBeenCalled();
- findToggleEmojiButton().trigger('click');
- expect(wrapper.vm.showEmojiMenu).toHaveBeenCalled();
- });
-
it('displays the clear status at dropdown', () => {
expect(wrapper.find('[data-testid="clear-status-at-dropdown"]').exists()).toBe(true);
});
@@ -108,16 +98,6 @@ describe('SetStatusModalWrapper', () => {
it('does not display the clear status at message', () => {
expect(findClearStatusAtMessage().exists()).toBe(false);
});
- });
-
- describe('improvedEmojiPicker is true', () => {
- const getEmojiPicker = () => wrapper.findComponent(EmojiPicker);
-
- beforeEach(async () => {
- await initEmojiMock();
- wrapper = createComponent({}, true);
- return initModal();
- });
it('renders emoji picker dropdown with custom positioning', () => {
expect(getEmojiPicker().props()).toMatchObject({
@@ -147,10 +127,6 @@ describe('SetStatusModalWrapper', () => {
it('hides the clear status button', () => {
expect(findClearStatusButton().isVisible()).toBe(false);
});
-
- it('shows the placeholder emoji', () => {
- expect(findNoEmojiPlaceholder().isVisible()).toBe(true);
- });
});
describe('with no currentEmoji set', () => {
@@ -163,22 +139,6 @@ describe('SetStatusModalWrapper', () => {
it('does not set the hidden status emoji field', () => {
expect(findFormField('emoji').element.value).toBe('');
});
-
- it('hides the placeholder emoji', () => {
- expect(findNoEmojiPlaceholder().isVisible()).toBe(false);
- });
-
- describe('with no currentMessage set', () => {
- beforeEach(async () => {
- await initEmojiMock();
- wrapper = createComponent({ currentEmoji: '', currentMessage: '' });
- return initModal();
- });
-
- it('shows the placeholder emoji', () => {
- expect(findNoEmojiPlaceholder().isVisible()).toBe(true);
- });
- });
});
describe('with currentClearStatusAfter set', () => {
diff --git a/spec/frontend/shortcuts_spec.js b/spec/frontend/shortcuts_spec.js
index 49148123a1c..8b9a11056f2 100644
--- a/spec/frontend/shortcuts_spec.js
+++ b/spec/frontend/shortcuts_spec.js
@@ -41,7 +41,7 @@ describe('Shortcuts', () => {
).toHaveBeenCalled();
});
- it('focues preview button inside edit comment form', () => {
+ it('focuses preview button inside edit comment form', () => {
document.querySelector('.js-note-edit').click();
Shortcuts.toggleMarkdownPreview(
diff --git a/spec/frontend/sidebar/assignees_realtime_spec.js b/spec/frontend/sidebar/assignees_realtime_spec.js
index 2249a1c08b8..ae8f07bf901 100644
--- a/spec/frontend/sidebar/assignees_realtime_spec.js
+++ b/spec/frontend/sidebar/assignees_realtime_spec.js
@@ -2,11 +2,16 @@ 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 AssigneesRealtime from '~/sidebar/components/assignees/assignees_realtime.vue';
import issuableAssigneesSubscription from '~/sidebar/queries/issuable_assignees.subscription.graphql';
import SidebarMediator from '~/sidebar/sidebar_mediator';
import getIssueAssigneesQuery from '~/vue_shared/components/sidebar/queries/get_issue_assignees.query.graphql';
-import Mock, { issuableQueryResponse, subscriptionNullResponse } from './mock_data';
+import Mock, {
+ issuableQueryResponse,
+ subscriptionNullResponse,
+ subscriptionResponse,
+} from './mock_data';
Vue.use(VueApollo);
@@ -20,7 +25,6 @@ describe('Assignees Realtime', () => {
const createComponent = ({
issuableType = 'issue',
- issuableId = 1,
subscriptionHandler = subscriptionInitialHandler,
} = {}) => {
fakeApollo = createMockApollo([
@@ -30,7 +34,6 @@ describe('Assignees Realtime', () => {
wrapper = shallowMount(AssigneesRealtime, {
propsData: {
issuableType,
- issuableId,
queryVariables: {
issuableIid: '1',
projectPath: 'path/to/project',
@@ -60,11 +63,23 @@ describe('Assignees Realtime', () => {
});
});
- it('calls the subscription with correct variable for issue', () => {
+ it('calls the subscription with correct variable for issue', async () => {
createComponent();
+ await waitForPromises();
expect(subscriptionInitialHandler).toHaveBeenCalledWith({
issuableId: 'gid://gitlab/Issue/1',
});
});
+
+ it('emits an `assigneesUpdated` event on subscription response', async () => {
+ createComponent({
+ subscriptionHandler: jest.fn().mockResolvedValue(subscriptionResponse),
+ });
+ await waitForPromises();
+
+ expect(wrapper.emitted('assigneesUpdated')).toEqual([
+ [{ id: '1', assignees: subscriptionResponse.data.issuableAssigneesUpdated.assignees.nodes }],
+ ]);
+ });
});
diff --git a/spec/frontend/sidebar/components/incidents/escalation_status_spec.js b/spec/frontend/sidebar/components/incidents/escalation_status_spec.js
index 7a736624fc0..8d8c10d10f1 100644
--- a/spec/frontend/sidebar/components/incidents/escalation_status_spec.js
+++ b/spec/frontend/sidebar/components/incidents/escalation_status_spec.js
@@ -1,4 +1,5 @@
import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
+import waitForPromises from 'helpers/wait_for_promises';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import EscalationStatus from '~/sidebar/components/incidents/escalation_status.vue';
import {
@@ -25,6 +26,11 @@ describe('EscalationStatus', () => {
const findDropdownComponent = () => wrapper.findComponent(GlDropdown);
const findDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
+ const findDropdownMenu = () => findDropdownComponent().find('.dropdown-menu');
+ const toggleDropdown = async () => {
+ await findDropdownComponent().findComponent('button').trigger('click');
+ await waitForPromises();
+ };
describe('status', () => {
it('shows the current status', () => {
@@ -49,4 +55,32 @@ describe('EscalationStatus', () => {
expect(wrapper.emitted().input[0][0]).toBe(STATUS_ACKNOWLEDGED);
});
});
+
+ describe('close behavior', () => {
+ it('allows the dropdown to be closed by default', async () => {
+ createComponent();
+ // Open dropdown
+ await toggleDropdown();
+
+ expect(findDropdownMenu().classes('show')).toBe(true);
+
+ // Attempt to close dropdown
+ await toggleDropdown();
+
+ expect(findDropdownMenu().classes('show')).toBe(false);
+ });
+
+ it('preventDropdownClose prevents the dropdown from closing', async () => {
+ createComponent({ preventDropdownClose: true });
+ // Open dropdown
+ await toggleDropdown();
+
+ expect(findDropdownMenu().classes('show')).toBe(true);
+
+ // Attempt to close dropdown
+ await toggleDropdown();
+
+ expect(findDropdownMenu().classes('show')).toBe(true);
+ });
+ });
});
diff --git a/spec/frontend/sidebar/mock_data.js b/spec/frontend/sidebar/mock_data.js
index fbca00636b6..2b421037339 100644
--- a/spec/frontend/sidebar/mock_data.js
+++ b/spec/frontend/sidebar/mock_data.js
@@ -415,6 +415,28 @@ export const subscriptionNullResponse = {
},
};
+export const subscriptionResponse = {
+ data: {
+ issuableAssigneesUpdated: {
+ id: '1',
+ assignees: {
+ nodes: [
+ {
+ __typename: 'UserCore',
+ id: 'gid://gitlab/User/1',
+ avatarUrl:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon',
+ name: 'Administrator',
+ username: 'root',
+ webUrl: '/root',
+ status: null,
+ },
+ ],
+ },
+ },
+ },
+};
+
const mockUser1 = {
__typename: 'UserCore',
id: 'gid://gitlab/User/1',
diff --git a/spec/frontend/sidebar/participants_spec.js b/spec/frontend/sidebar/participants_spec.js
index 356628849d9..2517b625225 100644
--- a/spec/frontend/sidebar/participants_spec.js
+++ b/spec/frontend/sidebar/participants_spec.js
@@ -17,8 +17,7 @@ const PARTICIPANT_LIST = [PARTICIPANT, { ...PARTICIPANT, id: 2 }, { ...PARTICIPA
describe('Participants', () => {
let wrapper;
- const getMoreParticipantsButton = () => wrapper.find('button');
-
+ const getMoreParticipantsButton = () => wrapper.find('[data-testid="more-participants"]');
const getCollapsedParticipantsCount = () => wrapper.find('[data-testid="collapsed-count"]');
const mountComponent = (propsData) =>
@@ -167,7 +166,7 @@ describe('Participants', () => {
expect(wrapper.vm.isShowingMoreParticipants).toBe(false);
- getMoreParticipantsButton().trigger('click');
+ getMoreParticipantsButton().vm.$emit('click');
expect(wrapper.vm.isShowingMoreParticipants).toBe(true);
});
diff --git a/spec/frontend/snippets/components/edit_spec.js b/spec/frontend/snippets/components/edit_spec.js
index 61424fa1eb2..9cfe136129a 100644
--- a/spec/frontend/snippets/components/edit_spec.js
+++ b/spec/frontend/snippets/components/edit_spec.js
@@ -19,8 +19,8 @@ import {
SNIPPET_VISIBILITY_INTERNAL,
SNIPPET_VISIBILITY_PUBLIC,
} from '~/snippets/constants';
-import CreateSnippetMutation from '~/snippets/mutations/createSnippet.mutation.graphql';
-import UpdateSnippetMutation from '~/snippets/mutations/updateSnippet.mutation.graphql';
+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';
diff --git a/spec/frontend/snippets/components/snippet_header_spec.js b/spec/frontend/snippets/components/snippet_header_spec.js
index 1b9d170556b..b750225a383 100644
--- a/spec/frontend/snippets/components/snippet_header_spec.js
+++ b/spec/frontend/snippets/components/snippet_header_spec.js
@@ -8,7 +8,7 @@ import waitForPromises from 'helpers/wait_for_promises';
import { Blob, BinaryBlob } from 'jest/blob/components/mock_data';
import { differenceInMilliseconds } from '~/lib/utils/datetime_utility';
import SnippetHeader, { i18n } from '~/snippets/components/snippet_header.vue';
-import DeleteSnippetMutation from '~/snippets/mutations/deleteSnippet.mutation.graphql';
+import DeleteSnippetMutation from '~/snippets/mutations/delete_snippet.mutation.graphql';
import axios from '~/lib/utils/axios_utils';
import createFlash, { FLASH_TYPES } from '~/flash';
diff --git a/spec/frontend/task_list_spec.js b/spec/frontend/task_list_spec.js
index bf470e7e126..fbdb73ae6de 100644
--- a/spec/frontend/task_list_spec.js
+++ b/spec/frontend/task_list_spec.js
@@ -121,7 +121,7 @@ describe('TaskList', () => {
});
describe('update', () => {
- it('should disable task list items and make a patch request then enable them again', (done) => {
+ it('should disable task list items and make a patch request then enable them again', () => {
const response = { data: { lock_version: 3 } };
jest.spyOn(taskList, 'enableTaskListItems').mockImplementation(() => {});
jest.spyOn(taskList, 'disableTaskListItems').mockImplementation(() => {});
@@ -156,20 +156,17 @@ describe('TaskList', () => {
expect(taskList.onUpdate).toHaveBeenCalled();
- update
- .then(() => {
- expect(taskList.disableTaskListItems).toHaveBeenCalledWith(event);
- expect(axios.patch).toHaveBeenCalledWith(endpoint, patchData);
- expect(taskList.enableTaskListItems).toHaveBeenCalledWith(event);
- expect(taskList.onSuccess).toHaveBeenCalledWith(response.data);
- expect(taskList.lockVersion).toEqual(response.data.lock_version);
- })
- .then(done)
- .catch(done.fail);
+ return update.then(() => {
+ expect(taskList.disableTaskListItems).toHaveBeenCalledWith(event);
+ expect(axios.patch).toHaveBeenCalledWith(endpoint, patchData);
+ expect(taskList.enableTaskListItems).toHaveBeenCalledWith(event);
+ expect(taskList.onSuccess).toHaveBeenCalledWith(response.data);
+ expect(taskList.lockVersion).toEqual(response.data.lock_version);
+ });
});
});
- it('should handle request error and enable task list items', (done) => {
+ it('should handle request error and enable task list items', () => {
const response = { data: { error: 1 } };
jest.spyOn(taskList, 'enableTaskListItems').mockImplementation(() => {});
jest.spyOn(taskList, 'onUpdate').mockImplementation(() => {});
@@ -182,12 +179,9 @@ describe('TaskList', () => {
expect(taskList.onUpdate).toHaveBeenCalled();
- update
- .then(() => {
- expect(taskList.enableTaskListItems).toHaveBeenCalledWith(event);
- expect(taskList.onError).toHaveBeenCalledWith(response.data);
- })
- .then(done)
- .catch(done.fail);
+ return update.then(() => {
+ expect(taskList.enableTaskListItems).toHaveBeenCalledWith(event);
+ expect(taskList.onError).toHaveBeenCalledWith(response.data);
+ });
});
});
diff --git a/spec/frontend/terraform/components/empty_state_spec.js b/spec/frontend/terraform/components/empty_state_spec.js
index b1303cf2b5e..21bfff5f1be 100644
--- a/spec/frontend/terraform/components/empty_state_spec.js
+++ b/spec/frontend/terraform/components/empty_state_spec.js
@@ -13,15 +13,20 @@ describe('EmptyStateComponent', () => {
const findLink = () => wrapper.findComponent(GlLink);
beforeEach(() => {
- wrapper = shallowMount(EmptyState, { propsData, stubs: { GlEmptyState, GlLink } });
+ wrapper = shallowMount(EmptyState, { propsData });
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
});
it('should render content', () => {
- expect(findEmptyState().exists()).toBe(true);
- expect(wrapper.text()).toContain('Get started with Terraform');
+ expect(findEmptyState().props('title')).toBe(
+ "Your project doesn't have any Terraform state files",
+ );
});
- it('should have a link to the GitLab managed Terraform States docs', () => {
+ it('should have a link to the GitLab managed Terraform states docs', () => {
expect(findLink().attributes('href')).toBe(docsUrl);
});
});
diff --git a/spec/frontend/terraform/components/mock_data.js b/spec/frontend/terraform/components/mock_data.js
new file mode 100644
index 00000000000..f0109047d4c
--- /dev/null
+++ b/spec/frontend/terraform/components/mock_data.js
@@ -0,0 +1,35 @@
+export const getStatesResponse = {
+ data: {
+ project: {
+ id: 'project-1',
+ terraformStates: {
+ count: 1,
+ nodes: {
+ _showDetails: true,
+ errorMessages: [],
+ loadingLock: false,
+ loadingRemove: false,
+ id: 'state-1',
+ name: 'state',
+ lockedAt: '01-01-2022',
+ updatedAt: '01-01-2022',
+ lockedByUser: {
+ id: 'user-1',
+ avatarUrl: 'avatar',
+ name: 'User 1',
+ username: 'user-1',
+ webUrl: 'web',
+ },
+ latestVersion: null,
+ },
+ pageInfo: {
+ __typename: 'PageInfo',
+ hasNextPage: true,
+ hasPreviousPage: false,
+ startCursor: 'prev',
+ endCursor: 'next',
+ },
+ },
+ },
+ },
+};
diff --git a/spec/frontend/terraform/components/states_table_actions_spec.js b/spec/frontend/terraform/components/states_table_actions_spec.js
index a6c80b95af4..d01f6af9023 100644
--- a/spec/frontend/terraform/components/states_table_actions_spec.js
+++ b/spec/frontend/terraform/components/states_table_actions_spec.js
@@ -9,6 +9,8 @@ import StateActions from '~/terraform/components/states_table_actions.vue';
import lockStateMutation from '~/terraform/graphql/mutations/lock_state.mutation.graphql';
import removeStateMutation from '~/terraform/graphql/mutations/remove_state.mutation.graphql';
import unlockStateMutation from '~/terraform/graphql/mutations/unlock_state.mutation.graphql';
+import getStatesQuery from '~/terraform/graphql/queries/get_states.query.graphql';
+import { getStatesResponse } from './mock_data';
Vue.use(VueApollo);
@@ -49,6 +51,7 @@ describe('StatesTableActions', () => {
[lockStateMutation, lockResponse],
[removeStateMutation, removeResponse],
[unlockStateMutation, unlockResponse],
+ [getStatesQuery, jest.fn().mockResolvedValue(getStatesResponse)],
],
{
Mutation: {
diff --git a/spec/frontend/tracking/tracking_spec.js b/spec/frontend/tracking/tracking_spec.js
index d85299cdfc3..665bf44fc77 100644
--- a/spec/frontend/tracking/tracking_spec.js
+++ b/spec/frontend/tracking/tracking_spec.js
@@ -129,6 +129,72 @@ describe('Tracking', () => {
});
});
+ describe('.definition', () => {
+ const TEST_VALID_BASENAME = '202108302307_default_click_button';
+ const TEST_EVENT_DATA = { category: undefined, action: 'click_button' };
+ let eventSpy;
+ let dispatcherSpy;
+
+ beforeAll(() => {
+ Tracking.definitionsManifest = {
+ '202108302307_default_click_button': 'config/events/202108302307_default_click_button.yml',
+ };
+ });
+
+ beforeEach(() => {
+ eventSpy = jest.spyOn(Tracking, 'event');
+ dispatcherSpy = jest.spyOn(Tracking, 'dispatchFromDefinition');
+ });
+
+ it('throws an error if the definition does not exists', () => {
+ const basename = '20220230_default_missing_definition';
+ const expectedError = new Error(`Missing Snowplow event definition "${basename}"`);
+
+ expect(() => Tracking.definition(basename)).toThrow(expectedError);
+ });
+
+ it('dispatches an event from a definition present in the manifest', () => {
+ Tracking.definition(TEST_VALID_BASENAME);
+
+ expect(dispatcherSpy).toHaveBeenCalledWith(TEST_VALID_BASENAME, {});
+ });
+
+ it('push events to the queue if not loaded', () => {
+ Tracking.definitionsLoaded = false;
+ Tracking.definitionsEventsQueue = [];
+
+ const dispatched = Tracking.definition(TEST_VALID_BASENAME);
+
+ expect(dispatched).toBe(false);
+ expect(Tracking.definitionsEventsQueue[0]).toStrictEqual([TEST_VALID_BASENAME, {}]);
+ expect(eventSpy).not.toHaveBeenCalled();
+ });
+
+ it('dispatch events when the definition is loaded', () => {
+ const definition = { key: TEST_VALID_BASENAME, ...TEST_EVENT_DATA };
+ Tracking.definitions = [{ ...definition }];
+ Tracking.definitionsEventsQueue = [];
+ Tracking.definitionsLoaded = true;
+
+ const dispatched = Tracking.definition(TEST_VALID_BASENAME);
+
+ expect(dispatched).not.toBe(false);
+ expect(Tracking.definitionsEventsQueue).toEqual([]);
+ expect(eventSpy).toHaveBeenCalledWith(definition.category, definition.action, {});
+ });
+
+ it('lets defined event data takes precedence', () => {
+ const definition = { key: TEST_VALID_BASENAME, category: undefined, action: 'click_button' };
+ const eventData = { category: TEST_CATEGORY };
+ Tracking.definitions = [{ ...definition }];
+ Tracking.definitionsLoaded = true;
+
+ Tracking.definition(TEST_VALID_BASENAME, eventData);
+
+ expect(eventSpy).toHaveBeenCalledWith(TEST_CATEGORY, definition.action, eventData);
+ });
+ });
+
describe('.enableFormTracking', () => {
it('tells snowplow to enable form tracking, with only explicit contexts', () => {
const config = { forms: { allow: ['form-class1'] }, fields: { allow: ['input-class1'] } };
diff --git a/spec/frontend/user_lists/components/edit_user_list_spec.js b/spec/frontend/user_lists/components/edit_user_list_spec.js
index 7cafe5e1f56..941c8244247 100644
--- a/spec/frontend/user_lists/components/edit_user_list_spec.js
+++ b/spec/frontend/user_lists/components/edit_user_list_spec.js
@@ -8,7 +8,7 @@ import { redirectTo } from '~/lib/utils/url_utility';
import EditUserList from '~/user_lists/components/edit_user_list.vue';
import UserListForm from '~/user_lists/components/user_list_form.vue';
import createStore from '~/user_lists/store/edit';
-import { userList } from '../../feature_flags/mock_data';
+import { userList } from 'jest/feature_flags/mock_data';
jest.mock('~/api');
jest.mock('~/lib/utils/url_utility');
diff --git a/spec/frontend/user_lists/components/new_user_list_spec.js b/spec/frontend/user_lists/components/new_user_list_spec.js
index 5eb44970fe4..ace4a284347 100644
--- a/spec/frontend/user_lists/components/new_user_list_spec.js
+++ b/spec/frontend/user_lists/components/new_user_list_spec.js
@@ -7,7 +7,7 @@ import Api from '~/api';
import { redirectTo } from '~/lib/utils/url_utility';
import NewUserList from '~/user_lists/components/new_user_list.vue';
import createStore from '~/user_lists/store/new';
-import { userList } from '../../feature_flags/mock_data';
+import { userList } from 'jest/feature_flags/mock_data';
jest.mock('~/api');
jest.mock('~/lib/utils/url_utility');
diff --git a/spec/frontend/user_lists/components/user_list_form_spec.js b/spec/frontend/user_lists/components/user_list_form_spec.js
index 42f7659600e..e09d8eac32f 100644
--- a/spec/frontend/user_lists/components/user_list_form_spec.js
+++ b/spec/frontend/user_lists/components/user_list_form_spec.js
@@ -1,6 +1,6 @@
import { mount } from '@vue/test-utils';
import Form from '~/user_lists/components/user_list_form.vue';
-import { userList } from '../../feature_flags/mock_data';
+import { userList } from 'jest/feature_flags/mock_data';
describe('user_lists/components/user_list_form', () => {
let wrapper;
diff --git a/spec/frontend/user_lists/components/user_list_spec.js b/spec/frontend/user_lists/components/user_list_spec.js
index 88dad06938b..f126c733dd5 100644
--- a/spec/frontend/user_lists/components/user_list_spec.js
+++ b/spec/frontend/user_lists/components/user_list_spec.js
@@ -7,7 +7,7 @@ import Api from '~/api';
import UserList from '~/user_lists/components/user_list.vue';
import createStore from '~/user_lists/store/show';
import { parseUserIds, stringifyUserIds } from '~/user_lists/store/utils';
-import { userList } from '../../feature_flags/mock_data';
+import { userList } from 'jest/feature_flags/mock_data';
jest.mock('~/api');
diff --git a/spec/frontend/user_lists/components/user_lists_spec.js b/spec/frontend/user_lists/components/user_lists_spec.js
index 10742c029c1..161eb036361 100644
--- a/spec/frontend/user_lists/components/user_lists_spec.js
+++ b/spec/frontend/user_lists/components/user_lists_spec.js
@@ -9,7 +9,7 @@ import UserListsComponent from '~/user_lists/components/user_lists.vue';
import UserListsTable from '~/user_lists/components/user_lists_table.vue';
import createStore from '~/user_lists/store/index';
import TablePagination from '~/vue_shared/components/pagination/table_pagination.vue';
-import { userList } from '../../feature_flags/mock_data';
+import { userList } from 'jest/feature_flags/mock_data';
jest.mock('~/api');
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 63587703392..08eb8ae0843 100644
--- a/spec/frontend/user_lists/components/user_lists_table_spec.js
+++ b/spec/frontend/user_lists/components/user_lists_table_spec.js
@@ -3,7 +3,7 @@ import { mount } from '@vue/test-utils';
import * as timeago from 'timeago.js';
import { nextTick } from 'vue';
import UserListsTable from '~/user_lists/components/user_lists_table.vue';
-import { userList } from '../../feature_flags/mock_data';
+import { userList } from 'jest/feature_flags/mock_data';
jest.mock('timeago.js', () => ({
format: jest.fn().mockReturnValue('2 weeks ago'),
diff --git a/spec/frontend/user_lists/store/edit/actions_spec.js b/spec/frontend/user_lists/store/edit/actions_spec.js
index c4b0f888d3e..ca56c935ea5 100644
--- a/spec/frontend/user_lists/store/edit/actions_spec.js
+++ b/spec/frontend/user_lists/store/edit/actions_spec.js
@@ -4,7 +4,7 @@ import { redirectTo } from '~/lib/utils/url_utility';
import * as actions from '~/user_lists/store/edit/actions';
import * as types from '~/user_lists/store/edit/mutation_types';
import createState from '~/user_lists/store/edit/state';
-import { userList } from '../../../feature_flags/mock_data';
+import { userList } from 'jest/feature_flags/mock_data';
jest.mock('~/api');
jest.mock('~/lib/utils/url_utility');
diff --git a/spec/frontend/user_lists/store/edit/mutations_spec.js b/spec/frontend/user_lists/store/edit/mutations_spec.js
index 0943c64e934..7971906429b 100644
--- a/spec/frontend/user_lists/store/edit/mutations_spec.js
+++ b/spec/frontend/user_lists/store/edit/mutations_spec.js
@@ -2,7 +2,7 @@ import statuses from '~/user_lists/constants/edit';
import * as types from '~/user_lists/store/edit/mutation_types';
import mutations from '~/user_lists/store/edit/mutations';
import createState from '~/user_lists/store/edit/state';
-import { userList } from '../../../feature_flags/mock_data';
+import { userList } from 'jest/feature_flags/mock_data';
describe('User List Edit Mutations', () => {
let state;
diff --git a/spec/frontend/user_lists/store/index/actions_spec.js b/spec/frontend/user_lists/store/index/actions_spec.js
index c5d7d557de9..4a8d0afb963 100644
--- a/spec/frontend/user_lists/store/index/actions_spec.js
+++ b/spec/frontend/user_lists/store/index/actions_spec.js
@@ -12,7 +12,7 @@ import {
} from '~/user_lists/store/index/actions';
import * as types from '~/user_lists/store/index/mutation_types';
import createState from '~/user_lists/store/index/state';
-import { userList } from '../../../feature_flags/mock_data';
+import { userList } from 'jest/feature_flags/mock_data';
jest.mock('~/api.js');
@@ -24,14 +24,13 @@ describe('~/user_lists/store/index/actions', () => {
});
describe('setUserListsOptions', () => {
- it('should commit SET_USER_LISTS_OPTIONS mutation', (done) => {
- testAction(
+ it('should commit SET_USER_LISTS_OPTIONS mutation', () => {
+ return testAction(
setUserListsOptions,
{ page: '1', scope: 'all' },
state,
[{ type: types.SET_USER_LISTS_OPTIONS, payload: { page: '1', scope: 'all' } }],
[],
- done,
);
});
});
@@ -42,8 +41,8 @@ describe('~/user_lists/store/index/actions', () => {
});
describe('success', () => {
- it('dispatches requestUserLists and receiveUserListsSuccess ', (done) => {
- testAction(
+ it('dispatches requestUserLists and receiveUserListsSuccess ', () => {
+ return testAction(
fetchUserLists,
null,
state,
@@ -57,16 +56,15 @@ describe('~/user_lists/store/index/actions', () => {
type: 'receiveUserListsSuccess',
},
],
- done,
);
});
});
describe('error', () => {
- it('dispatches requestUserLists and receiveUserListsError ', (done) => {
+ it('dispatches requestUserLists and receiveUserListsError ', () => {
Api.fetchFeatureFlagUserLists.mockRejectedValue();
- testAction(
+ return testAction(
fetchUserLists,
null,
state,
@@ -79,21 +77,20 @@ describe('~/user_lists/store/index/actions', () => {
type: 'receiveUserListsError',
},
],
- done,
);
});
});
});
describe('requestUserLists', () => {
- it('should commit RECEIVE_USER_LISTS_SUCCESS mutation', (done) => {
- testAction(requestUserLists, null, state, [{ type: types.REQUEST_USER_LISTS }], [], done);
+ it('should commit RECEIVE_USER_LISTS_SUCCESS mutation', () => {
+ return testAction(requestUserLists, null, state, [{ type: types.REQUEST_USER_LISTS }], []);
});
});
describe('receiveUserListsSuccess', () => {
- it('should commit RECEIVE_USER_LISTS_SUCCESS mutation', (done) => {
- testAction(
+ it('should commit RECEIVE_USER_LISTS_SUCCESS mutation', () => {
+ return testAction(
receiveUserListsSuccess,
{ data: [userList], headers: {} },
state,
@@ -104,20 +101,18 @@ describe('~/user_lists/store/index/actions', () => {
},
],
[],
- done,
);
});
});
describe('receiveUserListsError', () => {
- it('should commit RECEIVE_USER_LISTS_ERROR mutation', (done) => {
- testAction(
+ it('should commit RECEIVE_USER_LISTS_ERROR mutation', () => {
+ return testAction(
receiveUserListsError,
null,
state,
[{ type: types.RECEIVE_USER_LISTS_ERROR }],
[],
- done,
);
});
});
@@ -132,14 +127,13 @@ describe('~/user_lists/store/index/actions', () => {
Api.deleteFeatureFlagUserList.mockResolvedValue();
});
- it('should refresh the user lists', (done) => {
- testAction(
+ it('should refresh the user lists', () => {
+ return testAction(
deleteUserList,
userList,
state,
[],
[{ type: 'requestDeleteUserList', payload: userList }, { type: 'fetchUserLists' }],
- done,
);
});
});
@@ -149,8 +143,8 @@ describe('~/user_lists/store/index/actions', () => {
Api.deleteFeatureFlagUserList.mockRejectedValue({ response: { data: 'some error' } });
});
- it('should dispatch receiveDeleteUserListError', (done) => {
- testAction(
+ it('should dispatch receiveDeleteUserListError', () => {
+ return testAction(
deleteUserList,
userList,
state,
@@ -162,15 +156,14 @@ describe('~/user_lists/store/index/actions', () => {
payload: { list: userList, error: 'some error' },
},
],
- done,
);
});
});
});
describe('receiveDeleteUserListError', () => {
- it('should commit RECEIVE_DELETE_USER_LIST_ERROR with the given list', (done) => {
- testAction(
+ it('should commit RECEIVE_DELETE_USER_LIST_ERROR with the given list', () => {
+ return testAction(
receiveDeleteUserListError,
{ list: userList, error: 'mock error' },
state,
@@ -181,22 +174,20 @@ describe('~/user_lists/store/index/actions', () => {
},
],
[],
- done,
);
});
});
describe('clearAlert', () => {
- it('should commit RECEIVE_CLEAR_ALERT', (done) => {
+ it('should commit RECEIVE_CLEAR_ALERT', () => {
const alertIndex = 3;
- testAction(
+ return testAction(
clearAlert,
alertIndex,
state,
[{ type: 'RECEIVE_CLEAR_ALERT', payload: alertIndex }],
[],
- done,
);
});
});
diff --git a/spec/frontend/user_lists/store/index/mutations_spec.js b/spec/frontend/user_lists/store/index/mutations_spec.js
index 370838ae5fb..18d6a9b8f38 100644
--- a/spec/frontend/user_lists/store/index/mutations_spec.js
+++ b/spec/frontend/user_lists/store/index/mutations_spec.js
@@ -2,7 +2,7 @@ import { parseIntPagination, normalizeHeaders } from '~/lib/utils/common_utils';
import * as types from '~/user_lists/store/index/mutation_types';
import mutations from '~/user_lists/store/index/mutations';
import createState from '~/user_lists/store/index/state';
-import { userList } from '../../../feature_flags/mock_data';
+import { userList } from 'jest/feature_flags/mock_data';
describe('~/user_lists/store/index/mutations', () => {
let state;
diff --git a/spec/frontend/user_lists/store/new/actions_spec.js b/spec/frontend/user_lists/store/new/actions_spec.js
index 916ec2e6da7..fa69fa7fa66 100644
--- a/spec/frontend/user_lists/store/new/actions_spec.js
+++ b/spec/frontend/user_lists/store/new/actions_spec.js
@@ -4,7 +4,7 @@ import { redirectTo } from '~/lib/utils/url_utility';
import * as actions from '~/user_lists/store/new/actions';
import * as types from '~/user_lists/store/new/mutation_types';
import createState from '~/user_lists/store/new/state';
-import { userList } from '../../../feature_flags/mock_data';
+import { userList } from 'jest/feature_flags/mock_data';
jest.mock('~/api');
jest.mock('~/lib/utils/url_utility');
diff --git a/spec/frontend/vue_mr_widget/components/approvals/approvals_spec.js b/spec/frontend/vue_mr_widget/components/approvals/approvals_spec.js
index 36850e623c7..4985417ad99 100644
--- a/spec/frontend/vue_mr_widget/components/approvals/approvals_spec.js
+++ b/spec/frontend/vue_mr_widget/components/approvals/approvals_spec.js
@@ -1,3 +1,4 @@
+import { nextTick } from 'vue';
import { GlButton } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import createFlash from '~/flash';
@@ -28,11 +29,6 @@ const testApprovals = () => ({
});
const testApprovalRulesResponse = () => ({ rules: [{ id: 2 }] });
-// For some reason, the `Promise.resolve()` needs to be deferred
-// or the timing doesn't work.
-const tick = () => Promise.resolve();
-const waitForTick = (done) => tick().then(done).catch(done.fail);
-
describe('MRWidget approvals', () => {
let wrapper;
let service;
@@ -105,7 +101,7 @@ describe('MRWidget approvals', () => {
// eslint-disable-next-line no-restricted-syntax
wrapper.setData({ fetchingApprovals: true });
- return tick().then(() => {
+ return nextTick().then(() => {
expect(wrapper.text()).toContain(FETCH_LOADING);
});
});
@@ -116,10 +112,10 @@ describe('MRWidget approvals', () => {
});
describe('when fetch approvals error', () => {
- beforeEach((done) => {
+ beforeEach(() => {
jest.spyOn(service, 'fetchApprovals').mockReturnValue(Promise.reject());
createComponent();
- waitForTick(done);
+ return nextTick();
});
it('still shows loading message', () => {
@@ -133,13 +129,13 @@ describe('MRWidget approvals', () => {
describe('action button', () => {
describe('when mr is closed', () => {
- beforeEach((done) => {
+ beforeEach(() => {
mr.isOpen = false;
mr.approvals.user_has_approved = false;
mr.approvals.user_can_approve = true;
createComponent();
- waitForTick(done);
+ return nextTick();
});
it('action is not rendered', () => {
@@ -148,12 +144,12 @@ describe('MRWidget approvals', () => {
});
describe('when user cannot approve', () => {
- beforeEach((done) => {
+ beforeEach(() => {
mr.approvals.user_has_approved = false;
mr.approvals.user_can_approve = false;
createComponent();
- waitForTick(done);
+ return nextTick();
});
it('action is not rendered', () => {
@@ -168,9 +164,9 @@ describe('MRWidget approvals', () => {
});
describe('and MR is unapproved', () => {
- beforeEach((done) => {
+ beforeEach(() => {
createComponent();
- waitForTick(done);
+ return nextTick();
});
it('approve action is rendered', () => {
@@ -188,10 +184,10 @@ describe('MRWidget approvals', () => {
});
describe('with no approvers', () => {
- beforeEach((done) => {
+ beforeEach(() => {
mr.approvals.approved_by = [];
createComponent();
- waitForTick(done);
+ return nextTick();
});
it('approve action (with inverted style) is rendered', () => {
@@ -204,10 +200,10 @@ describe('MRWidget approvals', () => {
});
describe('with approvers', () => {
- beforeEach((done) => {
+ beforeEach(() => {
mr.approvals.approved_by = [{ user: { id: 7 } }];
createComponent();
- waitForTick(done);
+ return nextTick();
});
it('approve additionally action is rendered', () => {
@@ -221,9 +217,9 @@ describe('MRWidget approvals', () => {
});
describe('when approve action is clicked', () => {
- beforeEach((done) => {
+ beforeEach(() => {
createComponent();
- waitForTick(done);
+ return nextTick();
});
it('shows loading icon', () => {
@@ -234,15 +230,15 @@ describe('MRWidget approvals', () => {
action.vm.$emit('click');
- return tick().then(() => {
+ return nextTick().then(() => {
expect(action.props('loading')).toBe(true);
});
});
describe('and after loading', () => {
- beforeEach((done) => {
+ beforeEach(() => {
findAction().vm.$emit('click');
- waitForTick(done);
+ return nextTick();
});
it('calls service approve', () => {
@@ -259,10 +255,10 @@ describe('MRWidget approvals', () => {
});
describe('and error', () => {
- beforeEach((done) => {
+ beforeEach(() => {
jest.spyOn(service, 'approveMergeRequest').mockReturnValue(Promise.reject());
findAction().vm.$emit('click');
- waitForTick(done);
+ return nextTick();
});
it('flashes error message', () => {
@@ -273,12 +269,12 @@ describe('MRWidget approvals', () => {
});
describe('when user has approved', () => {
- beforeEach((done) => {
+ beforeEach(() => {
mr.approvals.user_has_approved = true;
mr.approvals.user_can_approve = false;
createComponent();
- waitForTick(done);
+ return nextTick();
});
it('revoke action is rendered', () => {
@@ -291,9 +287,9 @@ describe('MRWidget approvals', () => {
describe('when revoke action is clicked', () => {
describe('and successful', () => {
- beforeEach((done) => {
+ beforeEach(() => {
findAction().vm.$emit('click');
- waitForTick(done);
+ return nextTick();
});
it('calls service unapprove', () => {
@@ -310,10 +306,10 @@ describe('MRWidget approvals', () => {
});
describe('and error', () => {
- beforeEach((done) => {
+ beforeEach(() => {
jest.spyOn(service, 'unapproveMergeRequest').mockReturnValue(Promise.reject());
findAction().vm.$emit('click');
- waitForTick(done);
+ return nextTick();
});
it('flashes error message', () => {
@@ -333,11 +329,11 @@ describe('MRWidget approvals', () => {
});
describe('and can approve', () => {
- beforeEach((done) => {
+ beforeEach(() => {
mr.approvals.user_can_approve = true;
createComponent();
- waitForTick(done);
+ return nextTick();
});
it('is shown', () => {
@@ -350,11 +346,11 @@ describe('MRWidget approvals', () => {
});
describe('and cannot approve', () => {
- beforeEach((done) => {
+ beforeEach(() => {
mr.approvals.user_can_approve = false;
createComponent();
- waitForTick(done);
+ return nextTick();
});
it('is shown', () => {
@@ -369,9 +365,9 @@ describe('MRWidget approvals', () => {
});
describe('approvals summary', () => {
- beforeEach((done) => {
+ beforeEach(() => {
createComponent();
- waitForTick(done);
+ return nextTick();
});
it('is rendered with props', () => {
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 64e802c4fa5..98cfc04eb25 100644
--- a/spec/frontend/vue_mr_widget/components/extensions/utils_spec.js
+++ b/spec/frontend/vue_mr_widget/components/extensions/utils_spec.js
@@ -8,7 +8,7 @@ describe('generateText', () => {
${'%{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">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}
diff --git a/spec/frontend/vue_mr_widget/components/mr_widget_memory_usage_spec.js b/spec/frontend/vue_mr_widget/components/mr_widget_memory_usage_spec.js
index c0a30a5093d..f0106914674 100644
--- a/spec/frontend/vue_mr_widget/components/mr_widget_memory_usage_spec.js
+++ b/spec/frontend/vue_mr_widget/components/mr_widget_memory_usage_spec.js
@@ -175,22 +175,19 @@ describe('MemoryUsage', () => {
expect(el.querySelector('.js-usage-info')).toBeDefined();
});
- it('should show loading metrics message while metrics are being loaded', (done) => {
+ it('should show loading metrics message while metrics are being loaded', async () => {
vm.loadingMetrics = true;
vm.hasMetrics = false;
vm.loadFailed = false;
- nextTick(() => {
- expect(el.querySelector('.js-usage-info.usage-info-loading')).toBeDefined();
+ await nextTick();
- expect(el.querySelector('.js-usage-info .usage-info-load-spinner')).toBeDefined();
-
- expect(el.querySelector('.js-usage-info').innerText).toContain(messages.loadingMetrics);
- done();
- });
+ expect(el.querySelector('.js-usage-info.usage-info-loading')).toBeDefined();
+ expect(el.querySelector('.js-usage-info .usage-info-load-spinner')).toBeDefined();
+ expect(el.querySelector('.js-usage-info').innerText).toContain(messages.loadingMetrics);
});
- it('should show deployment memory usage when metrics are loaded', (done) => {
+ it('should show deployment memory usage when metrics are loaded', async () => {
// ignore BoostrapVue warnings
jest.spyOn(console, 'warn').mockImplementation();
@@ -199,37 +196,32 @@ describe('MemoryUsage', () => {
vm.loadFailed = false;
vm.memoryMetrics = metricsMockData.metrics.memory_values[0].values;
- nextTick(() => {
- expect(el.querySelector('.memory-graph-container')).toBeDefined();
- expect(el.querySelector('.js-usage-info').innerText).toContain(messages.hasMetrics);
- done();
- });
+ await nextTick();
+
+ expect(el.querySelector('.memory-graph-container')).toBeDefined();
+ expect(el.querySelector('.js-usage-info').innerText).toContain(messages.hasMetrics);
});
- it('should show failure message when metrics loading failed', (done) => {
+ it('should show failure message when metrics loading failed', async () => {
vm.loadingMetrics = false;
vm.hasMetrics = false;
vm.loadFailed = true;
- nextTick(() => {
- expect(el.querySelector('.js-usage-info.usage-info-failed')).toBeDefined();
+ await nextTick();
- expect(el.querySelector('.js-usage-info').innerText).toContain(messages.loadFailed);
- done();
- });
+ expect(el.querySelector('.js-usage-info.usage-info-failed')).toBeDefined();
+ expect(el.querySelector('.js-usage-info').innerText).toContain(messages.loadFailed);
});
- it('should show metrics unavailable message when metrics loading failed', (done) => {
+ it('should show metrics unavailable message when metrics loading failed', async () => {
vm.loadingMetrics = false;
vm.hasMetrics = false;
vm.loadFailed = false;
- nextTick(() => {
- expect(el.querySelector('.js-usage-info.usage-info-unavailable')).toBeDefined();
+ await nextTick();
- expect(el.querySelector('.js-usage-info').innerText).toContain(messages.metricsUnavailable);
- done();
- });
+ expect(el.querySelector('.js-usage-info.usage-info-unavailable')).toBeDefined();
+ expect(el.querySelector('.js-usage-info').innerText).toContain(messages.metricsUnavailable);
});
});
});
diff --git a/spec/frontend/vue_mr_widget/components/states/mr_widget_merged_spec.js b/spec/frontend/vue_mr_widget/components/states/mr_widget_merged_spec.js
index 7d86e453bc7..8efc4d84624 100644
--- a/spec/frontend/vue_mr_widget/components/states/mr_widget_merged_spec.js
+++ b/spec/frontend/vue_mr_widget/components/states/mr_widget_merged_spec.js
@@ -198,14 +198,13 @@ describe('MRWidgetMerged', () => {
);
});
- it('hides button to copy commit SHA if SHA does not exist', (done) => {
+ it('hides button to copy commit SHA if SHA does not exist', async () => {
vm.mr.mergeCommitSha = null;
- nextTick(() => {
- expect(selectors.copyMergeShaButton).toBe(null);
- expect(vm.$el.querySelector('.mr-info-list').innerText).not.toContain('with');
- done();
- });
+ await nextTick();
+
+ expect(selectors.copyMergeShaButton).toBe(null);
+ expect(vm.$el.querySelector('.mr-info-list').innerText).not.toContain('with');
});
it('shows merge commit SHA link', () => {
@@ -214,24 +213,22 @@ describe('MRWidgetMerged', () => {
expect(selectors.mergeCommitShaLink.href).toBe(vm.mr.mergeCommitPath);
});
- it('should not show source branch deleted text', (done) => {
+ it('should not show source branch deleted text', async () => {
vm.mr.sourceBranchRemoved = false;
- nextTick(() => {
- expect(vm.$el.innerText).not.toContain('The source branch has been deleted');
- done();
- });
+ await nextTick();
+
+ expect(vm.$el.innerText).not.toContain('The source branch has been deleted');
});
- it('should show source branch deleting text', (done) => {
+ it('should show source branch deleting text', async () => {
vm.mr.isRemovingSourceBranch = true;
vm.mr.sourceBranchRemoved = false;
- nextTick(() => {
- expect(vm.$el.innerText).toContain('The source branch is being deleted');
- expect(vm.$el.innerText).not.toContain('The source branch has been deleted');
- done();
- });
+ await nextTick();
+
+ expect(vm.$el.innerText).toContain('The source branch is being deleted');
+ expect(vm.$el.innerText).not.toContain('The source branch has been deleted');
});
it('should use mergedEvent mergedAt as tooltip title', () => {
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
new file mode 100644
index 00000000000..88b8e32bd5d
--- /dev/null
+++ b/spec/frontend/vue_mr_widget/extensions/test_report/index_spec.js
@@ -0,0 +1,149 @@
+import { GlButton } from '@gitlab/ui';
+import MockAdapter from 'axios-mock-adapter';
+import testReportExtension from '~/vue_merge_request_widget/extensions/test_report';
+import { i18n } from '~/vue_merge_request_widget/extensions/test_report/constants';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import { trimText } from 'helpers/text_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import axios from '~/lib/utils/axios_utils';
+import extensionsContainer from '~/vue_merge_request_widget/components/extensions/container';
+import { registerExtension } from '~/vue_merge_request_widget/components/extensions';
+import httpStatusCodes from '~/lib/utils/http_status';
+
+import { failedReport } from 'jest/reports/mock_data/mock_data';
+import mixedResultsTestReports from 'jest/reports/mock_data/new_and_fixed_failures_report.json';
+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';
+
+const reportWithParsingErrors = failedReport;
+reportWithParsingErrors.suites[0].suite_errors = {
+ head: 'JUnit XML parsing failed: 2:24: FATAL: attributes construct error',
+ base: 'JUnit data parsing failed: string not matched',
+};
+
+describe('Test report extension', () => {
+ let wrapper;
+ let mock;
+
+ registerExtension(testReportExtension);
+
+ const endpoint = '/root/repo/-/merge_requests/4/test_reports.json';
+
+ const mockApi = (statusCode, data = mixedResultsTestReports) => {
+ mock.onGet(endpoint).reply(statusCode, data);
+ };
+
+ const findToggleCollapsedButton = () => wrapper.findByTestId('toggle-button');
+ const findTertiaryButton = () => wrapper.find(GlButton);
+ const findAllExtensionListItems = () => wrapper.findAllByTestId('extension-list-item');
+
+ const createComponent = () => {
+ wrapper = mountExtended(extensionsContainer, {
+ propsData: {
+ mr: {
+ testResultsPath: endpoint,
+ headBlobPath: 'head/blob/path',
+ pipeline: { path: 'pipeline/path' },
+ },
+ },
+ });
+ };
+
+ const createExpandedWidgetWithData = async (data = mixedResultsTestReports) => {
+ mockApi(httpStatusCodes.OK, data);
+ createComponent();
+ await waitForPromises();
+ findToggleCollapsedButton().trigger('click');
+ await waitForPromises();
+ };
+
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ mock.restore();
+ });
+
+ describe('summary', () => {
+ it('displays loading text', () => {
+ mockApi(httpStatusCodes.OK);
+ createComponent();
+
+ expect(wrapper.text()).toContain(i18n.loading);
+ });
+
+ it('displays failed loading text', async () => {
+ mockApi(httpStatusCodes.INTERNAL_SERVER_ERROR);
+ createComponent();
+
+ await waitForPromises();
+
+ expect(wrapper.text()).toContain(i18n.error);
+ });
+
+ it.each`
+ description | mockData | expectedResult
+ ${'mixed test results'} | ${mixedResultsTestReports} | ${'Test summary: 2 failed and 2 fixed test results, 11 total tests'}
+ ${'unchanged test results'} | ${successTestReports} | ${'Test summary: no changed test results, 11 total tests'}
+ ${'tests with errors'} | ${newErrorsTestReports} | ${'Test summary: 2 errors, 11 total tests'}
+ ${'failed test results'} | ${newFailedTestReports} | ${'Test summary: 2 failed, 11 total tests'}
+ ${'resolved failures'} | ${resolvedFailures} | ${'Test summary: 4 fixed test results, 11 total tests'}
+ `('displays summary text for $description', async ({ mockData, expectedResult }) => {
+ mockApi(httpStatusCodes.OK, mockData);
+ createComponent();
+
+ await waitForPromises();
+
+ expect(wrapper.text()).toContain(expectedResult);
+ });
+
+ it('displays a link to the full report', async () => {
+ mockApi(httpStatusCodes.OK);
+ createComponent();
+
+ await waitForPromises();
+
+ expect(findTertiaryButton().text()).toBe('Full report');
+ expect(findTertiaryButton().attributes('href')).toBe('pipeline/path/test_report');
+ });
+
+ it('shows an error when a suite has a parsing error', async () => {
+ mockApi(httpStatusCodes.OK, reportWithParsingErrors);
+ createComponent();
+
+ await waitForPromises();
+
+ expect(wrapper.text()).toContain(i18n.error);
+ });
+ });
+
+ describe('expanded data', () => {
+ it('displays summary for each suite', async () => {
+ await createExpandedWidgetWithData();
+
+ expect(trimText(findAllExtensionListItems().at(0).text())).toBe(
+ 'rspec:pg: 1 failed and 2 fixed test results, 8 total tests',
+ );
+ expect(trimText(findAllExtensionListItems().at(1).text())).toBe(
+ 'java ant: 1 failed, 3 total tests',
+ );
+ });
+
+ it('displays suite parsing errors', async () => {
+ await createExpandedWidgetWithData(reportWithParsingErrors);
+
+ const suiteText = trimText(findAllExtensionListItems().at(0).text());
+
+ expect(suiteText).toContain(
+ 'Head report parsing error: JUnit XML parsing failed: 2:24: FATAL: attributes construct error',
+ );
+ expect(suiteText).toContain(
+ 'Base report parsing error: JUnit data parsing failed: string not matched',
+ );
+ });
+ });
+});
diff --git a/spec/frontend/vue_mr_widget/mr_widget_options_spec.js b/spec/frontend/vue_mr_widget/mr_widget_options_spec.js
index 0540107ea5f..9719e81fe12 100644
--- a/spec/frontend/vue_mr_widget/mr_widget_options_spec.js
+++ b/spec/frontend/vue_mr_widget/mr_widget_options_spec.js
@@ -46,6 +46,8 @@ describe('MrWidgetOptions', () => {
let mock;
const COLLABORATION_MESSAGE = 'Members who can merge are allowed to add commits';
+ const findExtensionToggleButton = () =>
+ wrapper.find('[data-testid="widget-extension"] [data-testid="toggle-button"]');
beforeEach(() => {
gl.mrWidgetData = { ...mockData };
@@ -187,9 +189,9 @@ describe('MrWidgetOptions', () => {
});
describe('when merge request is opened', () => {
- beforeEach((done) => {
+ beforeEach(() => {
wrapper.vm.mr.isOpen = true;
- nextTick(done);
+ return nextTick();
});
it('should render collaboration status', () => {
@@ -198,9 +200,9 @@ describe('MrWidgetOptions', () => {
});
describe('when merge request is not opened', () => {
- beforeEach((done) => {
+ beforeEach(() => {
wrapper.vm.mr.isOpen = false;
- nextTick(done);
+ return nextTick();
});
it('should not render collaboration status', () => {
@@ -215,9 +217,9 @@ describe('MrWidgetOptions', () => {
});
describe('when merge request is opened', () => {
- beforeEach((done) => {
+ beforeEach(() => {
wrapper.vm.mr.isOpen = true;
- nextTick(done);
+ return nextTick();
});
it('should not render collaboration status', () => {
@@ -229,11 +231,11 @@ describe('MrWidgetOptions', () => {
describe('showMergePipelineForkWarning', () => {
describe('when the source project and target project are the same', () => {
- beforeEach((done) => {
+ beforeEach(() => {
Vue.set(wrapper.vm.mr, 'mergePipelinesEnabled', true);
Vue.set(wrapper.vm.mr, 'sourceProjectId', 1);
Vue.set(wrapper.vm.mr, 'targetProjectId', 1);
- nextTick(done);
+ return nextTick();
});
it('should be false', () => {
@@ -242,11 +244,11 @@ describe('MrWidgetOptions', () => {
});
describe('when merge pipelines are not enabled', () => {
- beforeEach((done) => {
+ beforeEach(() => {
Vue.set(wrapper.vm.mr, 'mergePipelinesEnabled', false);
Vue.set(wrapper.vm.mr, 'sourceProjectId', 1);
Vue.set(wrapper.vm.mr, 'targetProjectId', 2);
- nextTick(done);
+ return nextTick();
});
it('should be false', () => {
@@ -255,11 +257,11 @@ describe('MrWidgetOptions', () => {
});
describe('when merge pipelines are enabled _and_ the source project and target project are different', () => {
- beforeEach((done) => {
+ beforeEach(() => {
Vue.set(wrapper.vm.mr, 'mergePipelinesEnabled', true);
Vue.set(wrapper.vm.mr, 'sourceProjectId', 1);
Vue.set(wrapper.vm.mr, 'targetProjectId', 2);
- nextTick(done);
+ return nextTick();
});
it('should be true', () => {
@@ -439,15 +441,10 @@ describe('MrWidgetOptions', () => {
expect(setFaviconOverlay).toHaveBeenCalledWith(overlayDataUrl);
});
- it('should not call setFavicon when there is no ciStatusFaviconPath', (done) => {
+ it('should not call setFavicon when there is no ciStatusFaviconPath', async () => {
wrapper.vm.mr.ciStatusFaviconPath = null;
- wrapper.vm
- .setFaviconHelper()
- .then(() => {
- expect(faviconElement.getAttribute('href')).toEqual(null);
- done();
- })
- .catch(done.fail);
+ await wrapper.vm.setFaviconHelper();
+ expect(faviconElement.getAttribute('href')).toEqual(null);
});
});
@@ -534,44 +531,36 @@ describe('MrWidgetOptions', () => {
expect(wrapper.find('.close-related-link').exists()).toBe(true);
});
- it('does not render if state is nothingToMerge', (done) => {
+ it('does not render if state is nothingToMerge', async () => {
wrapper.vm.mr.state = stateKey.nothingToMerge;
- nextTick(() => {
- expect(wrapper.find('.close-related-link').exists()).toBe(false);
- done();
- });
+ await nextTick();
+ expect(wrapper.find('.close-related-link').exists()).toBe(false);
});
});
describe('rendering source branch removal status', () => {
- it('renders when user cannot remove branch and branch should be removed', (done) => {
+ it('renders when user cannot remove branch and branch should be removed', async () => {
wrapper.vm.mr.canRemoveSourceBranch = false;
wrapper.vm.mr.shouldRemoveSourceBranch = true;
wrapper.vm.mr.state = 'readyToMerge';
- nextTick(() => {
- const tooltip = wrapper.find('[data-testid="question-o-icon"]');
-
- expect(wrapper.text()).toContain('Deletes the source branch');
- expect(tooltip.attributes('title')).toBe(
- 'A user with write access to the source branch selected this option',
- );
+ await nextTick();
+ const tooltip = wrapper.find('[data-testid="question-o-icon"]');
- done();
- });
+ expect(wrapper.text()).toContain('Deletes the source branch');
+ expect(tooltip.attributes('title')).toBe(
+ 'A user with write access to the source branch selected this option',
+ );
});
- it('does not render in merged state', (done) => {
+ it('does not render in merged state', async () => {
wrapper.vm.mr.canRemoveSourceBranch = false;
wrapper.vm.mr.shouldRemoveSourceBranch = true;
wrapper.vm.mr.state = 'merged';
- nextTick(() => {
- expect(wrapper.text()).toContain('The source branch has been deleted');
- expect(wrapper.text()).not.toContain('Deletes the source branch');
-
- done();
- });
+ await nextTick();
+ expect(wrapper.text()).toContain('The source branch has been deleted');
+ expect(wrapper.text()).not.toContain('Deletes the source branch');
});
});
@@ -605,7 +594,7 @@ describe('MrWidgetOptions', () => {
status: SUCCESS,
};
- beforeEach((done) => {
+ beforeEach(() => {
wrapper.vm.mr.deployments.push(
{
...deploymentMockData,
@@ -616,7 +605,7 @@ describe('MrWidgetOptions', () => {
},
);
- nextTick(done);
+ return nextTick();
});
it('renders multiple deployments', () => {
@@ -639,7 +628,7 @@ describe('MrWidgetOptions', () => {
describe('pipeline for target branch after merge', () => {
describe('with information for target branch pipeline', () => {
- beforeEach((done) => {
+ beforeEach(() => {
wrapper.vm.mr.state = 'merged';
wrapper.vm.mr.mergePipeline = {
id: 127,
@@ -747,7 +736,7 @@ describe('MrWidgetOptions', () => {
},
cancel_path: '/root/ci-web-terminal/pipelines/127/cancel',
};
- nextTick(done);
+ return nextTick();
});
it('renders pipeline block', () => {
@@ -755,7 +744,7 @@ describe('MrWidgetOptions', () => {
});
describe('with post merge deployments', () => {
- beforeEach((done) => {
+ beforeEach(() => {
wrapper.vm.mr.postMergeDeployments = [
{
id: 15,
@@ -787,7 +776,7 @@ describe('MrWidgetOptions', () => {
},
];
- nextTick(done);
+ return nextTick();
});
it('renders post deployment information', () => {
@@ -797,10 +786,10 @@ describe('MrWidgetOptions', () => {
});
describe('without information for target branch pipeline', () => {
- beforeEach((done) => {
+ beforeEach(() => {
wrapper.vm.mr.state = 'merged';
- nextTick(done);
+ return nextTick();
});
it('does not render pipeline block', () => {
@@ -809,10 +798,10 @@ describe('MrWidgetOptions', () => {
});
describe('when state is not merged', () => {
- beforeEach((done) => {
+ beforeEach(() => {
wrapper.vm.mr.state = 'archived';
- nextTick(done);
+ return nextTick();
});
it('does not render pipeline block', () => {
@@ -905,7 +894,7 @@ describe('MrWidgetOptions', () => {
beforeEach(() => {
pollRequest = jest.spyOn(Poll.prototype, 'makeRequest');
- registerExtension(workingExtension);
+ registerExtension(workingExtension());
createComponent();
});
@@ -937,9 +926,7 @@ describe('MrWidgetOptions', () => {
it('renders full data', async () => {
await waitForPromises();
- wrapper
- .find('[data-testid="widget-extension"] [data-testid="toggle-button"]')
- .trigger('click');
+ findExtensionToggleButton().trigger('click');
await nextTick();
@@ -975,6 +962,24 @@ describe('MrWidgetOptions', () => {
});
});
+ describe('expansion', () => {
+ it('hides collapse button', async () => {
+ registerExtension(workingExtension(false));
+ createComponent();
+ await waitForPromises();
+
+ expect(findExtensionToggleButton().exists()).toBe(false);
+ });
+
+ it('shows collapse button', async () => {
+ registerExtension(workingExtension(true));
+ createComponent();
+ await waitForPromises();
+
+ expect(findExtensionToggleButton().exists()).toBe(true);
+ });
+ });
+
describe('mock polling extension', () => {
let pollRequest;
let pollStop;
@@ -1025,7 +1030,7 @@ describe('MrWidgetOptions', () => {
it('captures sentry error and displays error when poll has failed', () => {
expect(captureException).toHaveBeenCalledTimes(1);
expect(captureException).toHaveBeenCalledWith(new Error('Fetch error'));
- expect(wrapper.findComponent(StatusIcon).props('iconName')).toBe('error');
+ expect(wrapper.findComponent(StatusIcon).props('iconName')).toBe('failed');
});
});
});
@@ -1036,7 +1041,7 @@ describe('MrWidgetOptions', () => {
const itHandlesTheException = () => {
expect(captureException).toHaveBeenCalledTimes(1);
expect(captureException).toHaveBeenCalledWith(new Error('Fetch error'));
- expect(wrapper.findComponent(StatusIcon).props('iconName')).toBe('error');
+ expect(wrapper.findComponent(StatusIcon).props('iconName')).toBe('failed');
};
beforeEach(() => {
diff --git a/spec/frontend/vue_mr_widget/stores/artifacts_list/actions_spec.js b/spec/frontend/vue_mr_widget/stores/artifacts_list/actions_spec.js
index 9423fa17c44..22562bb4ddb 100644
--- a/spec/frontend/vue_mr_widget/stores/artifacts_list/actions_spec.js
+++ b/spec/frontend/vue_mr_widget/stores/artifacts_list/actions_spec.js
@@ -22,27 +22,25 @@ describe('Artifacts App Store Actions', () => {
});
describe('setEndpoint', () => {
- it('should commit SET_ENDPOINT mutation', (done) => {
- testAction(
+ it('should commit SET_ENDPOINT mutation', () => {
+ return testAction(
setEndpoint,
'endpoint.json',
mockedState,
[{ type: types.SET_ENDPOINT, payload: 'endpoint.json' }],
[],
- done,
);
});
});
describe('requestArtifacts', () => {
- it('should commit REQUEST_ARTIFACTS mutation', (done) => {
- testAction(
+ it('should commit REQUEST_ARTIFACTS mutation', () => {
+ return testAction(
requestArtifacts,
null,
mockedState,
[{ type: types.REQUEST_ARTIFACTS }],
[],
- done,
);
});
});
@@ -62,7 +60,7 @@ describe('Artifacts App Store Actions', () => {
});
describe('success', () => {
- it('dispatches requestArtifacts and receiveArtifactsSuccess ', (done) => {
+ it('dispatches requestArtifacts and receiveArtifactsSuccess ', () => {
mock.onGet(`${TEST_HOST}/endpoint.json`).replyOnce(200, [
{
text: 'result.txt',
@@ -72,7 +70,7 @@ describe('Artifacts App Store Actions', () => {
},
]);
- testAction(
+ return testAction(
fetchArtifacts,
null,
mockedState,
@@ -96,7 +94,6 @@ describe('Artifacts App Store Actions', () => {
type: 'receiveArtifactsSuccess',
},
],
- done,
);
});
});
@@ -106,8 +103,8 @@ describe('Artifacts App Store Actions', () => {
mock.onGet(`${TEST_HOST}/endpoint.json`).reply(500);
});
- it('dispatches requestArtifacts and receiveArtifactsError ', (done) => {
- testAction(
+ it('dispatches requestArtifacts and receiveArtifactsError ', () => {
+ return testAction(
fetchArtifacts,
null,
mockedState,
@@ -120,45 +117,41 @@ describe('Artifacts App Store Actions', () => {
type: 'receiveArtifactsError',
},
],
- done,
);
});
});
});
describe('receiveArtifactsSuccess', () => {
- it('should commit RECEIVE_ARTIFACTS_SUCCESS mutation with 200', (done) => {
- testAction(
+ it('should commit RECEIVE_ARTIFACTS_SUCCESS mutation with 200', () => {
+ return testAction(
receiveArtifactsSuccess,
{ data: { summary: {} }, status: 200 },
mockedState,
[{ type: types.RECEIVE_ARTIFACTS_SUCCESS, payload: { summary: {} } }],
[],
- done,
);
});
- it('should not commit RECEIVE_ARTIFACTS_SUCCESS mutation with 204', (done) => {
- testAction(
+ it('should not commit RECEIVE_ARTIFACTS_SUCCESS mutation with 204', () => {
+ return testAction(
receiveArtifactsSuccess,
{ data: { summary: {} }, status: 204 },
mockedState,
[],
[],
- done,
);
});
});
describe('receiveArtifactsError', () => {
- it('should commit RECEIVE_ARTIFACTS_ERROR mutation', (done) => {
- testAction(
+ it('should commit RECEIVE_ARTIFACTS_ERROR mutation', () => {
+ return testAction(
receiveArtifactsError,
null,
mockedState,
[{ type: types.RECEIVE_ARTIFACTS_ERROR }],
[],
- done,
);
});
});
diff --git a/spec/frontend/vue_mr_widget/test_extensions.js b/spec/frontend/vue_mr_widget/test_extensions.js
index 986c1d6545a..6344636873f 100644
--- a/spec/frontend/vue_mr_widget/test_extensions.js
+++ b/spec/frontend/vue_mr_widget/test_extensions.js
@@ -1,6 +1,6 @@
import { EXTENSION_ICONS } from '~/vue_merge_request_widget/constants';
-export const workingExtension = {
+export const workingExtension = (shouldCollapse = true) => ({
name: 'WidgetTestExtension',
props: ['targetProjectFullPath'],
expandEvent: 'test_expand_event',
@@ -11,6 +11,9 @@ export const workingExtension = {
statusIcon({ count }) {
return count > 0 ? EXTENSION_ICONS.warning : EXTENSION_ICONS.success;
},
+ shouldCollapse() {
+ return shouldCollapse;
+ },
},
methods: {
fetchCollapsedData({ targetProjectFullPath }) {
@@ -36,7 +39,7 @@ export const workingExtension = {
]);
},
},
-};
+});
export const collapsedDataErrorExtension = {
name: 'WidgetTestCollapsedErrorExtension',
@@ -99,7 +102,7 @@ export const fullDataErrorExtension = {
};
export const pollingExtension = {
- ...workingExtension,
+ ...workingExtension(),
enablePolling: true,
};
diff --git a/spec/frontend/vue_shared/alert_details/alert_details_spec.js b/spec/frontend/vue_shared/alert_details/alert_details_spec.js
index 7ee6e29e6de..7aa54a1c55a 100644
--- a/spec/frontend/vue_shared/alert_details/alert_details_spec.js
+++ b/spec/frontend/vue_shared/alert_details/alert_details_spec.js
@@ -12,12 +12,17 @@ import AlertSummaryRow from '~/vue_shared/alert_details/components/alert_summary
import { PAGE_CONFIG, SEVERITY_LEVELS } from '~/vue_shared/alert_details/constants';
import createIssueMutation from '~/vue_shared/alert_details/graphql/mutations/alert_issue_create.mutation.graphql';
import AlertDetailsTable from '~/vue_shared/components/alert_details_table.vue';
+import MetricImagesTab from '~/vue_shared/components/metric_images/metric_images_tab.vue';
+import createStore from '~/vue_shared/components/metric_images/store/';
+import service from '~/vue_shared/alert_details/service';
import mockAlerts from './mocks/alerts.json';
const mockAlert = mockAlerts[0];
const environmentName = 'Production';
const environmentPath = '/fake/path';
+jest.mock('~/vue_shared/alert_details/service');
+
describe('AlertDetails', () => {
let environmentData = { name: environmentName, path: environmentPath };
let mock;
@@ -67,9 +72,11 @@ describe('AlertDetails', () => {
$route: { params: {} },
},
stubs: {
- ...stubs,
AlertSummaryRow,
+ 'metric-images-tab': true,
+ ...stubs,
},
+ store: createStore({}, service),
}),
);
}
@@ -91,7 +98,7 @@ describe('AlertDetails', () => {
const findEnvironmentName = () => wrapper.findByTestId('environmentName');
const findEnvironmentPath = () => wrapper.findByTestId('environmentPath');
const findDetailsTable = () => wrapper.findComponent(AlertDetailsTable);
- const findMetricsTab = () => wrapper.findByTestId('metrics');
+ const findMetricsTab = () => wrapper.findComponent(MetricImagesTab);
describe('Alert details', () => {
describe('when alert is null', () => {
@@ -129,8 +136,21 @@ describe('AlertDetails', () => {
expect(wrapper.findByTestId('startTimeItem').exists()).toBe(true);
expect(wrapper.findByTestId('startTimeItem').props('time')).toBe(mockAlert.startedAt);
});
+ });
+
+ describe('Metrics tab', () => {
+ it('should mount without errors', () => {
+ mountComponent({
+ mountMethod: mount,
+ provide: {
+ canUpdate: true,
+ iid: '1',
+ },
+ stubs: {
+ MetricImagesTab,
+ },
+ });
- it('renders the metrics tab', () => {
expect(findMetricsTab().exists()).toBe(true);
});
});
@@ -312,7 +332,9 @@ describe('AlertDetails', () => {
describe('header', () => {
const findHeader = () => wrapper.findByTestId('alert-header');
- const stubs = { TimeAgoTooltip: { template: '<span>now</span>' } };
+ const stubs = {
+ TimeAgoTooltip: { template: '<span>now</span>' },
+ };
describe('individual header fields', () => {
describe.each`
diff --git a/spec/frontend/vue_shared/alert_details/service_spec.js b/spec/frontend/vue_shared/alert_details/service_spec.js
new file mode 100644
index 00000000000..790854d0ca7
--- /dev/null
+++ b/spec/frontend/vue_shared/alert_details/service_spec.js
@@ -0,0 +1,44 @@
+import { fileList, fileListRaw } from 'jest/vue_shared/components/metric_images/mock_data';
+import {
+ getMetricImages,
+ uploadMetricImage,
+ updateMetricImage,
+ deleteMetricImage,
+} from '~/vue_shared/alert_details/service';
+import * as alertManagementAlertsApi from '~/api/alert_management_alerts_api';
+
+jest.mock('~/api/alert_management_alerts_api');
+
+describe('Alert details service', () => {
+ it('fetches metric images', async () => {
+ alertManagementAlertsApi.fetchAlertMetricImages.mockResolvedValue({ data: fileListRaw });
+ const result = await getMetricImages();
+
+ expect(alertManagementAlertsApi.fetchAlertMetricImages).toHaveBeenCalled();
+ expect(result).toEqual(fileList);
+ });
+
+ it('uploads a metric image', async () => {
+ alertManagementAlertsApi.uploadAlertMetricImage.mockResolvedValue({ data: fileListRaw[0] });
+ const result = await uploadMetricImage();
+
+ expect(alertManagementAlertsApi.uploadAlertMetricImage).toHaveBeenCalled();
+ expect(result).toEqual(fileList[0]);
+ });
+
+ it('updates a metric image', async () => {
+ alertManagementAlertsApi.updateAlertMetricImage.mockResolvedValue({ data: fileListRaw[0] });
+ const result = await updateMetricImage();
+
+ expect(alertManagementAlertsApi.updateAlertMetricImage).toHaveBeenCalled();
+ expect(result).toEqual(fileList[0]);
+ });
+
+ it('deletes a metric image', async () => {
+ alertManagementAlertsApi.deleteAlertMetricImage.mockResolvedValue({ data: '' });
+ const result = await deleteMetricImage();
+
+ expect(alertManagementAlertsApi.deleteAlertMetricImage).toHaveBeenCalled();
+ expect(result).toEqual({});
+ });
+});
diff --git a/spec/frontend/vue_shared/components/__snapshots__/awards_list_spec.js.snap b/spec/frontend/vue_shared/components/__snapshots__/awards_list_spec.js.snap
index c14cf0db370..bdf5ea23812 100644
--- a/spec/frontend/vue_shared/components/__snapshots__/awards_list_spec.js.snap
+++ b/spec/frontend/vue_shared/components/__snapshots__/awards_list_spec.js.snap
@@ -218,65 +218,88 @@ exports[`vue_shared/components/awards_list default matches snapshot 1`] = `
<div
class="award-menu-holder gl-my-2"
>
- <button
- aria-label="Add reaction"
- class="btn add-reaction-button js-add-award btn-default btn-md gl-button js-test-add-button-class"
+ <div
+ class="emoji-picker"
+ data-testid="emoji-picker"
title="Add reaction"
- type="button"
>
- <!---->
-
- <!---->
-
- <span
- class="gl-button-text"
+ <div
+ boundary="scrollParent"
+ class="dropdown b-dropdown gl-new-dropdown btn-group"
+ id="__BVID__13"
+ lazy=""
+ menu-class="dropdown-extended-height"
+ no-flip=""
>
- <span
- class="reaction-control-icon reaction-control-icon-neutral"
+ <!---->
+ <button
+ aria-expanded="false"
+ aria-haspopup="true"
+ class="btn dropdown-toggle btn-default btn-md add-reaction-button btn-icon gl-relative! gl-button gl-dropdown-toggle btn-default-secondary"
+ id="__BVID__13__BV_toggle_"
+ type="button"
>
- <svg
- aria-hidden="true"
- class="gl-icon s16"
- data-testid="slight-smile-icon"
- role="img"
+ <span
+ class="gl-sr-only"
>
- <use
- href="#slight-smile"
- />
- </svg>
- </span>
-
- <span
- class="reaction-control-icon reaction-control-icon-positive"
- >
- <svg
- aria-hidden="true"
- class="gl-icon s16"
- data-testid="smiley-icon"
- role="img"
+ Add reaction
+ </span>
+
+ <span
+ class="reaction-control-icon reaction-control-icon-neutral"
>
- <use
- href="#smiley"
- />
- </svg>
- </span>
-
- <span
- class="reaction-control-icon reaction-control-icon-super-positive"
- >
- <svg
- aria-hidden="true"
- class="gl-icon s16"
- data-testid="smile-icon"
- role="img"
+ <svg
+ aria-hidden="true"
+ class="gl-icon s16"
+ data-testid="slight-smile-icon"
+ role="img"
+ >
+ <use
+ href="#slight-smile"
+ />
+ </svg>
+ </span>
+
+ <span
+ class="reaction-control-icon reaction-control-icon-positive"
>
- <use
- href="#smile"
- />
- </svg>
- </span>
- </span>
- </button>
+ <svg
+ aria-hidden="true"
+ class="gl-icon s16"
+ data-testid="smiley-icon"
+ role="img"
+ >
+ <use
+ href="#smiley"
+ />
+ </svg>
+ </span>
+
+ <span
+ class="reaction-control-icon reaction-control-icon-super-positive"
+ >
+ <svg
+ aria-hidden="true"
+ class="gl-icon s16"
+ data-testid="smile-icon"
+ role="img"
+ >
+ <use
+ href="#smile"
+ />
+ </svg>
+ </span>
+ </button>
+ <ul
+ aria-labelledby="__BVID__13__BV_toggle_"
+ class="dropdown-menu dropdown-extended-height dropdown-menu-right"
+ role="menu"
+ tabindex="-1"
+ >
+ <!---->
+ </ul>
+ </div>
+ </div>
</div>
</div>
`;
diff --git a/spec/frontend/vue_shared/components/__snapshots__/identicon_spec.js.snap b/spec/frontend/vue_shared/components/__snapshots__/identicon_spec.js.snap
deleted file mode 100644
index 1d8e04b83a3..00000000000
--- a/spec/frontend/vue_shared/components/__snapshots__/identicon_spec.js.snap
+++ /dev/null
@@ -1,21 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`Identicon entity id is a GraphQL id matches snapshot 1`] = `
-<div
- class="avatar identicon s40 bg2"
->
-
- E
-
-</div>
-`;
-
-exports[`Identicon entity id is a number matches snapshot 1`] = `
-<div
- class="avatar identicon s40 bg2"
->
-
- E
-
-</div>
-`;
diff --git a/spec/frontend/vue_shared/components/awards_list_spec.js b/spec/frontend/vue_shared/components/awards_list_spec.js
index 95e9760c181..1c8cf726aca 100644
--- a/spec/frontend/vue_shared/components/awards_list_spec.js
+++ b/spec/frontend/vue_shared/components/awards_list_spec.js
@@ -76,7 +76,7 @@ describe('vue_shared/components/awards_list', () => {
count: Number(x.find('.js-counter').text()),
};
});
- const findAddAwardButton = () => wrapper.find('.js-add-award');
+ const findAddAwardButton = () => wrapper.find('[data-testid="emoji-picker"]');
describe('default', () => {
beforeEach(() => {
@@ -151,7 +151,6 @@ describe('vue_shared/components/awards_list', () => {
const btn = findAddAwardButton();
expect(btn.exists()).toBe(true);
- expect(btn.classes(TEST_ADD_BUTTON_CLASS)).toBe(true);
});
});
diff --git a/spec/frontend/vue_shared/components/blob_viewers/simple_viewer_spec.js b/spec/frontend/vue_shared/components/blob_viewers/simple_viewer_spec.js
index 663ebd3e12f..4b44311b253 100644
--- a/spec/frontend/vue_shared/components/blob_viewers/simple_viewer_spec.js
+++ b/spec/frontend/vue_shared/components/blob_viewers/simple_viewer_spec.js
@@ -2,9 +2,6 @@ import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
import { HIGHLIGHT_CLASS_NAME } from '~/vue_shared/components/blob_viewers/constants';
import SimpleViewer from '~/vue_shared/components/blob_viewers/simple_viewer.vue';
-import LineHighlighter from '~/blob/line_highlighter';
-
-jest.mock('~/blob/line_highlighter');
describe('Blob Simple Viewer component', () => {
let wrapper;
@@ -30,20 +27,6 @@ describe('Blob Simple Viewer component', () => {
wrapper.destroy();
});
- describe('refactorBlobViewer feature flag', () => {
- it('loads the LineHighlighter if refactorBlobViewer is enabled', () => {
- createComponent('', false, { refactorBlobViewer: true });
-
- expect(LineHighlighter).toHaveBeenCalled();
- });
-
- it('does not load the LineHighlighter if refactorBlobViewer is disabled', () => {
- createComponent('', false, { refactorBlobViewer: false });
-
- expect(LineHighlighter).not.toHaveBeenCalled();
- });
- });
-
it('does not fail if content is empty', () => {
const spy = jest.spyOn(window.console, 'error');
createComponent('');
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 575e8a73050..b6a181e6a0b 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
@@ -26,7 +26,6 @@ import {
tokenValueMilestone,
tokenValueMembership,
tokenValueConfidential,
- tokenValueEmpty,
} from './mock_data';
jest.mock('~/vue_shared/components/filtered_search_bar/filtered_search_utils', () => ({
@@ -207,33 +206,14 @@ describe('FilteredSearchBarRoot', () => {
});
});
- describe('watchers', () => {
- describe('filterValue', () => {
- it('emits component event `onFilter` with empty array and false when filter was never selected', async () => {
- wrapper = createComponent({ initialFilterValue: [tokenValueEmpty] });
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({
- initialRender: false,
- filterValue: [tokenValueEmpty],
- });
-
- await nextTick();
- expect(wrapper.emitted('onFilter')[0]).toEqual([[], false]);
- });
+ describe('events', () => {
+ it('emits component event `onFilter` with empty array and true when initially selected filter value was cleared', async () => {
+ wrapper = createComponent({ initialFilterValue: [tokenValueLabel] });
- it('emits component event `onFilter` with empty array and true when initially selected filter value was cleared', async () => {
- wrapper = createComponent({ initialFilterValue: [tokenValueLabel] });
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({
- initialRender: false,
- filterValue: [tokenValueEmpty],
- });
+ wrapper.find(GlFilteredSearch).vm.$emit('clear');
- await nextTick();
- expect(wrapper.emitted('onFilter')[0]).toEqual([[], true]);
- });
+ await nextTick();
+ expect(wrapper.emitted('onFilter')[0]).toEqual([[], true]);
});
});
diff --git a/spec/frontend/vue_shared/components/form/input_copy_toggle_visibility_spec.js b/spec/frontend/vue_shared/components/form/input_copy_toggle_visibility_spec.js
index b67385cc43e..e636f58d868 100644
--- a/spec/frontend/vue_shared/components/form/input_copy_toggle_visibility_spec.js
+++ b/spec/frontend/vue_shared/components/form/input_copy_toggle_visibility_spec.js
@@ -89,8 +89,11 @@ describe('InputCopyToggleVisibility', () => {
});
describe('when clicked', () => {
+ let event;
+
beforeEach(async () => {
- await findRevealButton().trigger('click');
+ event = { stopPropagation: jest.fn() };
+ await findRevealButton().trigger('click', event);
});
it('displays value', () => {
@@ -110,6 +113,11 @@ describe('InputCopyToggleVisibility', () => {
it('emits `visibility-change` event', () => {
expect(wrapper.emitted('visibility-change')[0]).toEqual([true]);
});
+
+ it('stops propagation on click event', () => {
+ // in case the input is located in a dropdown or modal
+ expect(event.stopPropagation).toHaveBeenCalledTimes(1);
+ });
});
});
diff --git a/spec/frontend/vue_shared/components/help_popover_spec.js b/spec/frontend/vue_shared/components/help_popover_spec.js
index 597fb63d95c..64dce194327 100644
--- a/spec/frontend/vue_shared/components/help_popover_spec.js
+++ b/spec/frontend/vue_shared/components/help_popover_spec.js
@@ -34,7 +34,7 @@ describe('HelpPopover', () => {
it('renders a link button with an icon question', () => {
expect(findQuestionButton().props()).toMatchObject({
- icon: 'question',
+ icon: 'question-o',
variant: 'link',
});
});
diff --git a/spec/frontend/vue_shared/components/identicon_spec.js b/spec/frontend/vue_shared/components/identicon_spec.js
deleted file mode 100644
index 24fc3713e2b..00000000000
--- a/spec/frontend/vue_shared/components/identicon_spec.js
+++ /dev/null
@@ -1,50 +0,0 @@
-import { shallowMount } from '@vue/test-utils';
-import IdenticonComponent from '~/vue_shared/components/identicon.vue';
-
-describe('Identicon', () => {
- let wrapper;
-
- const defaultProps = {
- entityId: 1,
- entityName: 'entity-name',
- sizeClass: 's40',
- };
-
- const createComponent = (props = {}) => {
- wrapper = shallowMount(IdenticonComponent, {
- propsData: {
- ...defaultProps,
- ...props,
- },
- });
- };
-
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
- describe('entity id is a number', () => {
- beforeEach(() => createComponent());
-
- it('matches snapshot', () => {
- expect(wrapper.element).toMatchSnapshot();
- });
-
- it('adds a correct class to identicon', () => {
- expect(wrapper.find({ ref: 'identicon' }).classes()).toContain('bg2');
- });
- });
-
- describe('entity id is a GraphQL id', () => {
- beforeEach(() => createComponent({ entityId: 'gid://gitlab/Project/8' }));
-
- it('matches snapshot', () => {
- expect(wrapper.element).toMatchSnapshot();
- });
-
- it('adds a correct class to identicon', () => {
- expect(wrapper.find({ ref: 'identicon' }).classes()).toContain('bg2');
- });
- });
-});
diff --git a/spec/frontend/vue_shared/components/line_numbers_spec.js b/spec/frontend/vue_shared/components/line_numbers_spec.js
deleted file mode 100644
index 38c26226863..00000000000
--- a/spec/frontend/vue_shared/components/line_numbers_spec.js
+++ /dev/null
@@ -1,37 +0,0 @@
-import { shallowMount } from '@vue/test-utils';
-import { GlIcon, GlLink } from '@gitlab/ui';
-import LineNumbers from '~/vue_shared/components/line_numbers.vue';
-
-describe('Line Numbers component', () => {
- let wrapper;
- const lines = 10;
-
- const createComponent = () => {
- wrapper = shallowMount(LineNumbers, { propsData: { lines } });
- };
-
- const findGlIcon = () => wrapper.findComponent(GlIcon);
- const findLineNumbers = () => wrapper.findAllComponents(GlLink);
- const findFirstLineNumber = () => findLineNumbers().at(0);
-
- beforeEach(() => createComponent());
-
- afterEach(() => wrapper.destroy());
-
- describe('rendering', () => {
- it('renders Line Numbers', () => {
- expect(findLineNumbers().length).toBe(lines);
- expect(findFirstLineNumber().attributes()).toMatchObject({
- id: 'L1',
- to: '#LC1',
- });
- });
-
- it('renders a link icon', () => {
- expect(findGlIcon().props()).toMatchObject({
- size: 12,
- name: 'link',
- });
- });
- });
-});
diff --git a/spec/frontend/vue_shared/components/local_storage_sync_spec.js b/spec/frontend/vue_shared/components/local_storage_sync_spec.js
index dac633fe6c8..a80717a1aea 100644
--- a/spec/frontend/vue_shared/components/local_storage_sync_spec.js
+++ b/spec/frontend/vue_shared/components/local_storage_sync_spec.js
@@ -1,31 +1,29 @@
import { shallowMount } from '@vue/test-utils';
-import { nextTick } from 'vue';
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
+const STORAGE_KEY = 'key';
+
describe('Local Storage Sync', () => {
let wrapper;
- const createComponent = ({ props = {}, slots = {} } = {}) => {
+ const createComponent = ({ value, asString = false, slots = {} } = {}) => {
wrapper = shallowMount(LocalStorageSync, {
- propsData: props,
+ propsData: { storageKey: STORAGE_KEY, value, asString },
slots,
});
};
+ const setStorageValue = (value) => localStorage.setItem(STORAGE_KEY, value);
+ const getStorageValue = (value) => localStorage.getItem(STORAGE_KEY, value);
+
afterEach(() => {
- if (wrapper) {
- wrapper.destroy();
- }
- wrapper = null;
+ wrapper.destroy();
localStorage.clear();
});
it('is a renderless component', () => {
const html = '<div class="test-slot"></div>';
createComponent({
- props: {
- storageKey: 'key',
- },
slots: {
default: html,
},
@@ -35,233 +33,136 @@ describe('Local Storage Sync', () => {
});
describe('localStorage empty', () => {
- const storageKey = 'issue_list_order';
-
it('does not emit input event', () => {
- createComponent({
- props: {
- storageKey,
- value: 'ascending',
- },
- });
-
- expect(wrapper.emitted('input')).toBeFalsy();
- });
-
- it.each('foo', 3, true, ['foo', 'bar'], { foo: 'bar' })(
- 'saves updated value to localStorage',
- async (newValue) => {
- createComponent({
- props: {
- storageKey,
- value: 'initial',
- },
- });
-
- wrapper.setProps({ value: newValue });
+ createComponent({ value: 'ascending' });
- await nextTick();
- expect(localStorage.getItem(storageKey)).toBe(String(newValue));
- },
- );
-
- it('does not save default value', () => {
- const value = 'ascending';
+ expect(wrapper.emitted('input')).toBeUndefined();
+ });
- createComponent({
- props: {
- storageKey,
- value,
- },
- });
+ it('does not save initial value if it did not change', () => {
+ createComponent({ value: 'ascending' });
- expect(localStorage.getItem(storageKey)).toBe(null);
+ expect(getStorageValue()).toBeNull();
});
});
describe('localStorage has saved value', () => {
- const storageKey = 'issue_list_order_by';
const savedValue = 'last_updated';
beforeEach(() => {
- localStorage.setItem(storageKey, savedValue);
+ setStorageValue(savedValue);
+ createComponent({ asString: true });
});
it('emits input event with saved value', () => {
- createComponent({
- props: {
- storageKey,
- value: 'ascending',
- },
- });
-
expect(wrapper.emitted('input')[0][0]).toBe(savedValue);
});
- it('does not overwrite localStorage with prop value', () => {
- createComponent({
- props: {
- storageKey,
- value: 'created',
- },
- });
-
- expect(localStorage.getItem(storageKey)).toBe(savedValue);
+ it('does not overwrite localStorage with initial prop value', () => {
+ expect(getStorageValue()).toBe(savedValue);
});
it('updating the value updates localStorage', async () => {
- createComponent({
- props: {
- storageKey,
- value: 'created',
- },
- });
-
const newValue = 'last_updated';
- wrapper.setProps({
- value: newValue,
- });
+ await wrapper.setProps({ value: newValue });
- await nextTick();
- expect(localStorage.getItem(storageKey)).toBe(newValue);
+ expect(getStorageValue()).toBe(newValue);
});
+ });
+ describe('persist prop', () => {
it('persists the value by default', async () => {
const persistedValue = 'persisted';
+ createComponent({ asString: true });
+ // Sanity check to make sure we start with nothing saved.
+ expect(getStorageValue()).toBeNull();
- createComponent({
- props: {
- storageKey,
- },
- });
+ await wrapper.setProps({ value: persistedValue });
- wrapper.setProps({ value: persistedValue });
- await nextTick();
- expect(localStorage.getItem(storageKey)).toBe(persistedValue);
+ expect(getStorageValue()).toBe(persistedValue);
});
it('does not save a value if persist is set to false', async () => {
+ const value = 'saved';
const notPersistedValue = 'notPersisted';
+ createComponent({ asString: true });
+ // Save some value so we can test that it's not overwritten.
+ await wrapper.setProps({ value });
- createComponent({
- props: {
- storageKey,
- },
- });
+ expect(getStorageValue()).toBe(value);
- wrapper.setProps({ persist: false, value: notPersistedValue });
- await nextTick();
- expect(localStorage.getItem(storageKey)).not.toBe(notPersistedValue);
+ await wrapper.setProps({ persist: false, value: notPersistedValue });
+
+ expect(getStorageValue()).toBe(value);
});
});
- describe('with "asJson" prop set to "true"', () => {
- const storageKey = 'testStorageKey';
-
- describe.each`
- value | serializedValue
- ${null} | ${'null'}
- ${''} | ${'""'}
- ${true} | ${'true'}
- ${false} | ${'false'}
- ${42} | ${'42'}
- ${'42'} | ${'"42"'}
- ${'{ foo: '} | ${'"{ foo: "'}
- ${['test']} | ${'["test"]'}
- ${{ foo: 'bar' }} | ${'{"foo":"bar"}'}
- `('given $value', ({ value, serializedValue }) => {
- describe('is a new value', () => {
- beforeEach(async () => {
- createComponent({
- props: {
- storageKey,
- value: 'initial',
- asJson: true,
- },
- });
-
- wrapper.setProps({ value });
-
- await nextTick();
- });
-
- it('serializes the value correctly to localStorage', () => {
- expect(localStorage.getItem(storageKey)).toBe(serializedValue);
- });
- });
-
- describe('is already stored', () => {
- beforeEach(() => {
- localStorage.setItem(storageKey, serializedValue);
-
- createComponent({
- props: {
- storageKey,
- value: 'initial',
- asJson: true,
- },
- });
- });
-
- it('emits an input event with the deserialized value', () => {
- expect(wrapper.emitted('input')).toEqual([[value]]);
- });
- });
+ describe('saving and restoring', () => {
+ it.each`
+ value | asString
+ ${'foo'} | ${true}
+ ${'foo'} | ${false}
+ ${'{ a: 1 }'} | ${true}
+ ${'{ a: 1 }'} | ${false}
+ ${3} | ${false}
+ ${['foo', 'bar']} | ${false}
+ ${{ foo: 'bar' }} | ${false}
+ ${null} | ${false}
+ ${' '} | ${false}
+ ${true} | ${false}
+ ${false} | ${false}
+ ${42} | ${false}
+ ${'42'} | ${false}
+ ${'{ foo: '} | ${false}
+ `('saves and restores the same value', async ({ value, asString }) => {
+ // Create an initial component to save the value.
+ createComponent({ asString });
+ await wrapper.setProps({ value });
+ wrapper.destroy();
+ // Create a second component to restore the value. Restore is only done once, when the
+ // component is first mounted.
+ createComponent({ asString });
+
+ expect(wrapper.emitted('input')[0][0]).toEqual(value);
});
- describe('with bad JSON in storage', () => {
- const badJSON = '{ badJSON';
-
- beforeEach(() => {
- jest.spyOn(console, 'warn').mockImplementation();
- localStorage.setItem(storageKey, badJSON);
-
- createComponent({
- props: {
- storageKey,
- value: 'initial',
- asJson: true,
- },
- });
- });
-
- it('should console warn', () => {
- // eslint-disable-next-line no-console
- expect(console.warn).toHaveBeenCalledWith(
- `[gitlab] Failed to deserialize value from localStorage (key=${storageKey})`,
- badJSON,
- );
- });
-
- it('should not emit an input event', () => {
- expect(wrapper.emitted('input')).toBeUndefined();
- });
+ it('shows a warning when trying to save a non-string value when asString prop is true', async () => {
+ const spy = jest.spyOn(console, 'warn').mockImplementation();
+ createComponent({ asString: true });
+ await wrapper.setProps({ value: [] });
+
+ expect(spy).toHaveBeenCalled();
});
});
- it('clears localStorage when clear property is true', async () => {
- const storageKey = 'key';
- const value = 'initial';
+ describe('with bad JSON in storage', () => {
+ const badJSON = '{ badJSON';
+ let spy;
- createComponent({
- props: {
- storageKey,
- },
+ beforeEach(() => {
+ spy = jest.spyOn(console, 'warn').mockImplementation();
+ setStorageValue(badJSON);
+ createComponent();
});
- wrapper.setProps({
- value,
+
+ it('should console warn', () => {
+ expect(spy).toHaveBeenCalled();
});
- await nextTick();
+ it('should not emit an input event', () => {
+ expect(wrapper.emitted('input')).toBeUndefined();
+ });
+ });
- expect(localStorage.getItem(storageKey)).toBe(value);
+ it('clears localStorage when clear property is true', async () => {
+ const value = 'initial';
+ createComponent({ asString: true });
+ await wrapper.setProps({ value });
- wrapper.setProps({
- clear: true,
- });
+ expect(getStorageValue()).toBe(value);
- await nextTick();
+ await wrapper.setProps({ clear: true });
- expect(localStorage.getItem(storageKey)).toBe(null);
+ expect(getStorageValue()).toBeNull();
});
});
diff --git a/spec/frontend/vue_shared/components/markdown/apply_suggestion_spec.js b/spec/frontend/vue_shared/components/markdown/apply_suggestion_spec.js
index c56628fcbcd..ecb2b37c3a5 100644
--- a/spec/frontend/vue_shared/components/markdown/apply_suggestion_spec.js
+++ b/spec/frontend/vue_shared/components/markdown/apply_suggestion_spec.js
@@ -1,4 +1,4 @@
-import { GlDropdown, GlFormTextarea, GlButton } from '@gitlab/ui';
+import { GlDropdown, GlFormTextarea, GlButton, GlAlert } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import ApplySuggestionComponent from '~/vue_shared/components/markdown/apply_suggestion.vue';
@@ -10,9 +10,10 @@ describe('Apply Suggestion component', () => {
wrapper = shallowMount(ApplySuggestionComponent, { propsData: { ...propsData, ...props } });
};
- const findDropdown = () => wrapper.find(GlDropdown);
- const findTextArea = () => wrapper.find(GlFormTextarea);
- const findApplyButton = () => wrapper.find(GlButton);
+ const findDropdown = () => wrapper.findComponent(GlDropdown);
+ const findTextArea = () => wrapper.findComponent(GlFormTextarea);
+ const findApplyButton = () => wrapper.findComponent(GlButton);
+ const findAlert = () => wrapper.findComponent(GlAlert);
beforeEach(() => createWrapper());
@@ -53,6 +54,20 @@ describe('Apply Suggestion component', () => {
});
});
+ describe('error', () => {
+ it('displays an error message', () => {
+ const errorMessage = 'Error message';
+ createWrapper({ errorMessage });
+
+ const alert = findAlert();
+
+ expect(alert.exists()).toBe(true);
+ expect(alert.props('variant')).toBe('danger');
+ expect(alert.props('dismissible')).toBe(false);
+ expect(alert.text()).toBe(errorMessage);
+ });
+ });
+
describe('apply suggestion', () => {
it('emits an apply event with no message if no message was added', () => {
findTextArea().vm.$emit('input', null);
diff --git a/spec/frontend/vue_shared/components/markdown/field_spec.js b/spec/frontend/vue_shared/components/markdown/field_spec.js
index b5daa389fc6..d1c4d777d44 100644
--- a/spec/frontend/vue_shared/components/markdown/field_spec.js
+++ b/spec/frontend/vue_shared/components/markdown/field_spec.js
@@ -85,7 +85,7 @@ describe('Markdown field component', () => {
describe('mounted', () => {
const previewHTML = `
<p>markdown preview</p>
- <video src="${FIXTURES_PATH}/static/mock-video.mp4" muted="muted"></video>
+ <video src="${FIXTURES_PATH}/static/mock-video.mp4"></video>
`;
let previewLink;
let writeLink;
@@ -101,6 +101,21 @@ describe('Markdown field component', () => {
expect(subject.find('.zen-backdrop textarea').element).not.toBeNull();
});
+ it('renders referenced commands on markdown preview', async () => {
+ axiosMock
+ .onPost(markdownPreviewPath)
+ .reply(200, { references: { users: [], commands: 'test command' } });
+
+ previewLink = getPreviewLink();
+ previewLink.vm.$emit('click', { target: {} });
+
+ await axios.waitFor(markdownPreviewPath);
+ const referencedCommands = subject.find('[data-testid="referenced-commands"]');
+
+ expect(referencedCommands.exists()).toBe(true);
+ expect(referencedCommands.text()).toContain('test command');
+ });
+
describe('markdown preview', () => {
beforeEach(() => {
axiosMock.onPost(markdownPreviewPath).reply(200, { body: previewHTML });
diff --git a/spec/frontend/vue_shared/components/markdown/header_spec.js b/spec/frontend/vue_shared/components/markdown/header_spec.js
index 9ffb9c6a541..fa4ca63f910 100644
--- a/spec/frontend/vue_shared/components/markdown/header_spec.js
+++ b/spec/frontend/vue_shared/components/markdown/header_spec.js
@@ -95,7 +95,7 @@ describe('Markdown field header component', () => {
it('hides toolbar in preview mode', () => {
createWrapper({ previewMarkdown: true });
- expect(findToolbar().classes().includes('gl-display-none')).toBe(true);
+ expect(findToolbar().classes().includes('gl-display-none!')).toBe(true);
});
it('emits toggle markdown event when clicking preview tab', async () => {
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
new file mode 100644
index 00000000000..5dd12d9edf5
--- /dev/null
+++ b/spec/frontend/vue_shared/components/metric_images/__snapshots__/metric_images_table_spec.js.snap
@@ -0,0 +1,73 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Metrics upload item render the metrics image component 1`] = `
+<gl-card-stub
+ bodyclass="gl-border-1,gl-border-t-solid,gl-border-gray-100,[object Object]"
+ class="collapsible-card border gl-p-0 gl-mb-5"
+ footerclass=""
+ headerclass="gl-display-flex gl-align-items-center gl-border-b-0 gl-py-3"
+>
+ <gl-modal-stub
+ actioncancel="[object Object]"
+ actionprimary="[object Object]"
+ body-class="gl-pb-0! gl-min-h-6!"
+ dismisslabel="Close"
+ modalclass=""
+ modalid="delete-metric-modal"
+ size="sm"
+ titletag="h4"
+ >
+
+ <p>
+ Are you sure you wish to delete this image?
+ </p>
+ </gl-modal-stub>
+
+ <gl-modal-stub
+ actioncancel="[object Object]"
+ actionprimary="[object Object]"
+ data-testid="metric-image-edit-modal"
+ dismisslabel="Close"
+ modalclass=""
+ modalid="edit-metric-modal"
+ size="sm"
+ titletag="h4"
+ >
+
+ <gl-form-group-stub
+ label="Text (optional)"
+ label-for="upload-text-input"
+ labeldescription=""
+ optionaltext="(optional)"
+ >
+ <gl-form-input-stub
+ data-testid="metric-image-text-field"
+ id="upload-text-input"
+ />
+ </gl-form-group-stub>
+
+ <gl-form-group-stub
+ description="Must start with http or https"
+ label="Link (optional)"
+ label-for="upload-url-input"
+ labeldescription=""
+ optionaltext="(optional)"
+ >
+ <gl-form-input-stub
+ data-testid="metric-image-url-field"
+ id="upload-url-input"
+ />
+ </gl-form-group-stub>
+ </gl-modal-stub>
+
+ <div
+ class="gl-display-flex gl-flex-direction-column"
+ data-testid="metric-image-body"
+ >
+ <img
+ class="gl-max-w-full gl-align-self-center"
+ src="test_file_path"
+ />
+ </div>
+</gl-card-stub>
+`;
diff --git a/spec/frontend/vue_shared/components/metric_images/metric_images_tab_spec.js b/spec/frontend/vue_shared/components/metric_images/metric_images_tab_spec.js
new file mode 100644
index 00000000000..2cefa77b72d
--- /dev/null
+++ b/spec/frontend/vue_shared/components/metric_images/metric_images_tab_spec.js
@@ -0,0 +1,174 @@
+import { GlFormInput, GlModal } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import Vue from 'vue';
+import merge from 'lodash/merge';
+import Vuex from 'vuex';
+import MetricImagesTable from '~/vue_shared/components/metric_images/metric_images_table.vue';
+import MetricImagesTab from '~/vue_shared/components/metric_images/metric_images_tab.vue';
+import createStore from '~/vue_shared/components/metric_images/store';
+import waitForPromises from 'helpers/wait_for_promises';
+import UploadDropzone from '~/vue_shared/components/upload_dropzone/upload_dropzone.vue';
+import { fileList, initialData } from './mock_data';
+
+const service = {
+ getMetricImages: jest.fn(),
+};
+
+const mockEvent = { preventDefault: jest.fn() };
+
+Vue.use(Vuex);
+
+describe('Metric images tab', () => {
+ let wrapper;
+ let store;
+
+ const mountComponent = (options = {}) => {
+ store = createStore({}, service);
+
+ wrapper = shallowMount(
+ MetricImagesTab,
+ merge(
+ {
+ store,
+ provide: {
+ canUpdate: true,
+ iid: initialData.issueIid,
+ projectId: initialData.projectId,
+ },
+ },
+ options,
+ ),
+ );
+ };
+
+ beforeEach(() => {
+ mountComponent();
+ });
+
+ afterEach(() => {
+ if (wrapper) {
+ wrapper.destroy();
+ wrapper = null;
+ }
+ });
+
+ const findUploadDropzone = () => wrapper.findComponent(UploadDropzone);
+ const findImages = () => wrapper.findAllComponents(MetricImagesTable);
+ const findModal = () => wrapper.findComponent(GlModal);
+ const submitModal = () => findModal().vm.$emit('primary', mockEvent);
+ const cancelModal = () => findModal().vm.$emit('hidden');
+
+ describe('empty state', () => {
+ beforeEach(() => {
+ mountComponent();
+ });
+
+ it('renders the upload component', () => {
+ expect(findUploadDropzone().exists()).toBe(true);
+ });
+ });
+
+ describe('permissions', () => {
+ beforeEach(() => {
+ mountComponent({ provide: { canUpdate: false } });
+ });
+
+ it('hides the upload component when disallowed', () => {
+ expect(findUploadDropzone().exists()).toBe(false);
+ });
+ });
+
+ describe('onLoad action', () => {
+ it('should load images', async () => {
+ service.getMetricImages.mockImplementation(() => Promise.resolve(fileList));
+
+ mountComponent();
+
+ await waitForPromises();
+
+ expect(findImages().length).toBe(1);
+ });
+ });
+
+ describe('add metric dialog', () => {
+ const testUrl = 'test url';
+
+ it('should open the add metric dialog when clicked', async () => {
+ mountComponent();
+
+ findUploadDropzone().vm.$emit('change');
+
+ await waitForPromises();
+
+ expect(findModal().attributes('visible')).toBe('true');
+ });
+
+ it('should close when cancelled', async () => {
+ mountComponent({
+ data() {
+ return { modalVisible: true };
+ },
+ });
+
+ cancelModal();
+
+ await waitForPromises();
+
+ expect(findModal().attributes('visible')).toBeFalsy();
+ });
+
+ it('should add files and url when selected', async () => {
+ mountComponent({
+ data() {
+ return { modalVisible: true, modalUrl: testUrl, currentFiles: fileList };
+ },
+ });
+
+ const dispatchSpy = jest.spyOn(store, 'dispatch');
+
+ submitModal();
+
+ await waitForPromises();
+
+ expect(dispatchSpy).toHaveBeenCalledWith('uploadImage', {
+ files: fileList,
+ url: testUrl,
+ urlText: '',
+ });
+ });
+
+ describe('url field', () => {
+ beforeEach(() => {
+ mountComponent({
+ data() {
+ return { modalVisible: true, modalUrl: testUrl };
+ },
+ });
+ });
+
+ it('should display the url field', () => {
+ expect(wrapper.find('#upload-url-input').attributes('value')).toBe(testUrl);
+ });
+
+ it('should display the url text field', () => {
+ expect(wrapper.find('#upload-text-input').attributes('value')).toBe('');
+ });
+
+ it('should clear url when cancelled', async () => {
+ cancelModal();
+
+ await waitForPromises();
+
+ expect(wrapper.findComponent(GlFormInput).attributes('value')).toBe('');
+ });
+
+ it('should clear url when submitted', async () => {
+ submitModal();
+
+ await waitForPromises();
+
+ expect(wrapper.findComponent(GlFormInput).attributes('value')).toBe('');
+ });
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/metric_images/metric_images_table_spec.js b/spec/frontend/vue_shared/components/metric_images/metric_images_table_spec.js
new file mode 100644
index 00000000000..d792bd46ccd
--- /dev/null
+++ b/spec/frontend/vue_shared/components/metric_images/metric_images_table_spec.js
@@ -0,0 +1,230 @@
+import { GlLink, GlModal } from '@gitlab/ui';
+import { shallowMount, mount } from '@vue/test-utils';
+import Vue from 'vue';
+import merge from 'lodash/merge';
+import Vuex from 'vuex';
+import createStore from '~/vue_shared/components/metric_images/store';
+import MetricsImageTable from '~/vue_shared/components/metric_images/metric_images_table.vue';
+import waitForPromises from 'helpers/wait_for_promises';
+
+const defaultProps = {
+ id: 1,
+ filePath: 'test_file_path',
+ filename: 'test_file_name',
+};
+
+const mockEvent = { preventDefault: jest.fn() };
+
+Vue.use(Vuex);
+
+describe('Metrics upload item', () => {
+ let wrapper;
+ let store;
+
+ const mountComponent = (options = {}, mountMethod = mount) => {
+ store = createStore();
+
+ wrapper = mountMethod(
+ MetricsImageTable,
+ merge(
+ {
+ store,
+ propsData: {
+ ...defaultProps,
+ },
+ provide: { canUpdate: true },
+ },
+ options,
+ ),
+ );
+ };
+
+ afterEach(() => {
+ if (wrapper) {
+ wrapper.destroy();
+ wrapper = null;
+ }
+ });
+
+ const findImageLink = () => wrapper.findComponent(GlLink);
+ const findLabelTextSpan = () => wrapper.find('[data-testid="metric-image-label-span"]');
+ const findCollapseButton = () => wrapper.find('[data-testid="collapse-button"]');
+ const findMetricImageBody = () => wrapper.find('[data-testid="metric-image-body"]');
+ const findModal = () => wrapper.findComponent(GlModal);
+ const findEditModal = () => wrapper.find('[data-testid="metric-image-edit-modal"]');
+ const findDeleteButton = () => wrapper.find('[data-testid="delete-button"]');
+ const findEditButton = () => wrapper.find('[data-testid="edit-button"]');
+ const findImageTextInput = () => wrapper.find('[data-testid="metric-image-text-field"]');
+ const findImageUrlInput = () => wrapper.find('[data-testid="metric-image-url-field"]');
+
+ const closeModal = () => findModal().vm.$emit('hidden');
+ const submitModal = () => findModal().vm.$emit('primary', mockEvent);
+ const deleteImage = () => findDeleteButton().vm.$emit('click');
+ const closeEditModal = () => findEditModal().vm.$emit('hidden');
+ const submitEditModal = () => findEditModal().vm.$emit('primary', mockEvent);
+ const editImage = () => findEditButton().vm.$emit('click');
+
+ it('render the metrics image component', () => {
+ mountComponent({}, shallowMount);
+
+ expect(wrapper.element).toMatchSnapshot();
+ });
+
+ it('shows a link with the correct url', () => {
+ const testUrl = 'test_url';
+ mountComponent({ propsData: { url: testUrl } });
+
+ expect(findImageLink().attributes('href')).toBe(testUrl);
+ expect(findImageLink().text()).toBe(defaultProps.filename);
+ });
+
+ it('shows a link with the url text, if url text is present', () => {
+ const testUrl = 'test_url';
+ const testUrlText = 'test_url_text';
+ mountComponent({ propsData: { url: testUrl, urlText: testUrlText } });
+
+ expect(findImageLink().attributes('href')).toBe(testUrl);
+ expect(findImageLink().text()).toBe(testUrlText);
+ });
+
+ it('shows the url text with no url, if no url is present', () => {
+ const testUrlText = 'test_url_text';
+ mountComponent({ propsData: { urlText: testUrlText } });
+
+ expect(findLabelTextSpan().text()).toBe(testUrlText);
+ });
+
+ describe('expand and collapse', () => {
+ beforeEach(() => {
+ mountComponent();
+ });
+
+ it('the card is expanded by default', () => {
+ expect(findMetricImageBody().isVisible()).toBe(true);
+ });
+
+ it('the card is collapsed when clicked', async () => {
+ findCollapseButton().trigger('click');
+
+ await waitForPromises();
+
+ expect(findMetricImageBody().isVisible()).toBe(false);
+ });
+ });
+
+ describe('delete functionality', () => {
+ it('should open the delete modal when clicked', async () => {
+ mountComponent({ stubs: { GlModal: true } });
+
+ deleteImage();
+
+ await waitForPromises();
+
+ expect(findModal().attributes('visible')).toBe('true');
+ });
+
+ describe('when the modal is open', () => {
+ beforeEach(() => {
+ mountComponent(
+ {
+ data() {
+ return { modalVisible: true };
+ },
+ },
+ shallowMount,
+ );
+ });
+
+ it('should close the modal when cancelled', async () => {
+ closeModal();
+
+ await waitForPromises();
+
+ expect(findModal().attributes('visible')).toBeFalsy();
+ });
+
+ it('should delete the image when selected', async () => {
+ const dispatchSpy = jest.spyOn(store, 'dispatch').mockImplementation(jest.fn());
+
+ submitModal();
+
+ await waitForPromises();
+
+ expect(dispatchSpy).toHaveBeenCalledWith('deleteImage', defaultProps.id);
+ });
+ });
+
+ describe('canUpdate permission', () => {
+ it('delete button is hidden when user lacks update permissions', () => {
+ mountComponent({ provide: { canUpdate: false } });
+
+ expect(findDeleteButton().exists()).toBe(false);
+ });
+ });
+ });
+
+ describe('edit functionality', () => {
+ it('should open the delete modal when clicked', async () => {
+ mountComponent({ stubs: { GlModal: true } });
+
+ editImage();
+
+ await waitForPromises();
+
+ expect(findEditModal().attributes('visible')).toBe('true');
+ });
+
+ describe('when the modal is open', () => {
+ beforeEach(() => {
+ mountComponent({
+ data() {
+ return { editModalVisible: true };
+ },
+ propsData: { urlText: 'test' },
+ stubs: { GlModal: true },
+ });
+ });
+
+ it('should close the modal when cancelled', async () => {
+ closeEditModal();
+
+ await waitForPromises();
+
+ expect(findEditModal().attributes('visible')).toBeFalsy();
+ });
+
+ it('should delete the image when selected', async () => {
+ const dispatchSpy = jest.spyOn(store, 'dispatch').mockImplementation(jest.fn());
+
+ submitEditModal();
+
+ await waitForPromises();
+
+ expect(dispatchSpy).toHaveBeenCalledWith('updateImage', {
+ imageId: defaultProps.id,
+ url: null,
+ urlText: 'test',
+ });
+ });
+
+ it('should clear edits when the modal is closed', async () => {
+ await findImageTextInput().setValue('test value');
+ await findImageUrlInput().setValue('http://www.gitlab.com');
+
+ expect(findImageTextInput().element.value).toBe('test value');
+ expect(findImageUrlInput().element.value).toBe('http://www.gitlab.com');
+
+ closeEditModal();
+
+ await waitForPromises();
+
+ editImage();
+
+ await waitForPromises();
+
+ expect(findImageTextInput().element.value).toBe('test');
+ expect(findImageUrlInput().element.value).toBe('');
+ });
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/metric_images/mock_data.js b/spec/frontend/vue_shared/components/metric_images/mock_data.js
new file mode 100644
index 00000000000..480491077fb
--- /dev/null
+++ b/spec/frontend/vue_shared/components/metric_images/mock_data.js
@@ -0,0 +1,5 @@
+export const fileList = [{ filePath: 'test', filename: 'hello', id: 5, url: null }];
+
+export const fileListRaw = [{ file_path: 'test', filename: 'hello', id: 5, url: null }];
+
+export const initialData = { issueIid: '123', projectId: 456 };
diff --git a/spec/frontend/vue_shared/components/metric_images/store/actions_spec.js b/spec/frontend/vue_shared/components/metric_images/store/actions_spec.js
new file mode 100644
index 00000000000..518cf354675
--- /dev/null
+++ b/spec/frontend/vue_shared/components/metric_images/store/actions_spec.js
@@ -0,0 +1,158 @@
+import Vue from 'vue';
+import Vuex from 'vuex';
+import actionsFactory from '~/vue_shared/components/metric_images/store/actions';
+import * as types from '~/vue_shared/components/metric_images/store/mutation_types';
+import createStore from '~/vue_shared/components/metric_images/store';
+import testAction from 'helpers/vuex_action_helper';
+import createFlash from '~/flash';
+import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+import { fileList, initialData } from '../mock_data';
+
+jest.mock('~/flash');
+const service = {
+ getMetricImages: jest.fn(),
+ uploadMetricImage: jest.fn(),
+ updateMetricImage: jest.fn(),
+ deleteMetricImage: jest.fn(),
+};
+
+const actions = actionsFactory(service);
+
+const defaultState = {
+ issueIid: 1,
+ projectId: '2',
+};
+
+Vue.use(Vuex);
+
+describe('Metrics tab store actions', () => {
+ let store;
+ let state;
+
+ beforeEach(() => {
+ store = createStore(defaultState);
+ state = store.state;
+ });
+
+ afterEach(() => {
+ createFlash.mockClear();
+ });
+
+ describe('fetching metric images', () => {
+ it('should call success action when fetching metric images', () => {
+ service.getMetricImages.mockImplementation(() => Promise.resolve(fileList));
+
+ testAction(actions.fetchImages, null, state, [
+ { type: types.REQUEST_METRIC_IMAGES },
+ {
+ type: types.RECEIVE_METRIC_IMAGES_SUCCESS,
+ payload: convertObjectPropsToCamelCase(fileList, { deep: true }),
+ },
+ ]);
+ });
+
+ it('should call error action when fetching metric images with an error', async () => {
+ service.getMetricImages.mockImplementation(() => Promise.reject());
+
+ await testAction(
+ actions.fetchImages,
+ null,
+ state,
+ [{ type: types.REQUEST_METRIC_IMAGES }, { type: types.RECEIVE_METRIC_IMAGES_ERROR }],
+ [],
+ );
+ expect(createFlash).toHaveBeenCalled();
+ });
+ });
+
+ describe('uploading metric images', () => {
+ const payload = {
+ // mock the FileList api
+ files: {
+ item() {
+ return fileList[0];
+ },
+ },
+ url: 'test_url',
+ };
+
+ it('should call success action when uploading an image', () => {
+ service.uploadMetricImage.mockImplementation(() => Promise.resolve(fileList[0]));
+
+ testAction(actions.uploadImage, payload, state, [
+ { type: types.REQUEST_METRIC_UPLOAD },
+ {
+ type: types.RECEIVE_METRIC_UPLOAD_SUCCESS,
+ payload: fileList[0],
+ },
+ ]);
+ });
+
+ it('should call error action when failing to upload an image', async () => {
+ service.uploadMetricImage.mockImplementation(() => Promise.reject());
+
+ await testAction(
+ actions.uploadImage,
+ payload,
+ state,
+ [{ type: types.REQUEST_METRIC_UPLOAD }, { type: types.RECEIVE_METRIC_UPLOAD_ERROR }],
+ [],
+ );
+ expect(createFlash).toHaveBeenCalled();
+ });
+ });
+
+ describe('updating metric images', () => {
+ const payload = {
+ url: 'test_url',
+ urlText: 'url text',
+ };
+
+ it('should call success action when updating an image', () => {
+ service.updateMetricImage.mockImplementation(() => Promise.resolve());
+
+ testAction(actions.updateImage, payload, state, [
+ { type: types.REQUEST_METRIC_UPLOAD },
+ {
+ type: types.RECEIVE_METRIC_UPDATE_SUCCESS,
+ },
+ ]);
+ });
+
+ it('should call error action when failing to update an image', async () => {
+ service.updateMetricImage.mockImplementation(() => Promise.reject());
+
+ await testAction(
+ actions.updateImage,
+ payload,
+ state,
+ [{ type: types.REQUEST_METRIC_UPLOAD }, { type: types.RECEIVE_METRIC_UPLOAD_ERROR }],
+ [],
+ );
+ expect(createFlash).toHaveBeenCalled();
+ });
+ });
+
+ describe('deleting a metric image', () => {
+ const payload = fileList[0].id;
+
+ it('should call success action when deleting an image', () => {
+ service.deleteMetricImage.mockImplementation(() => Promise.resolve());
+
+ testAction(actions.deleteImage, payload, state, [
+ {
+ type: types.RECEIVE_METRIC_DELETE_SUCCESS,
+ payload,
+ },
+ ]);
+ });
+ });
+
+ describe('initial data', () => {
+ it('should set the initial data correctly', () => {
+ testAction(actions.setInitialData, initialData, state, [
+ { type: types.SET_INITIAL_DATA, payload: initialData },
+ ]);
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/metric_images/store/mutations_spec.js b/spec/frontend/vue_shared/components/metric_images/store/mutations_spec.js
new file mode 100644
index 00000000000..754f729e657
--- /dev/null
+++ b/spec/frontend/vue_shared/components/metric_images/store/mutations_spec.js
@@ -0,0 +1,147 @@
+import { cloneDeep } from 'lodash';
+import * as types from '~/vue_shared/components/metric_images/store/mutation_types';
+import mutations from '~/vue_shared/components/metric_images/store/mutations';
+import { initialData } from '../mock_data';
+
+const defaultState = {
+ metricImages: [],
+ isLoadingMetricImages: false,
+ isUploadingImage: false,
+};
+
+const testImages = [
+ { filename: 'test.filename', id: 5, filePath: 'test/file/path', url: null },
+ { filename: 'second.filename', id: 6, filePath: 'second/file/path', url: 'test/url' },
+ { filename: 'third.filename', id: 7, filePath: 'third/file/path', url: 'test/url' },
+];
+
+describe('Metric images mutations', () => {
+ let state;
+
+ const createState = (customState = {}) => {
+ state = {
+ ...cloneDeep(defaultState),
+ ...customState,
+ };
+ };
+
+ beforeEach(() => {
+ createState();
+ });
+
+ describe('REQUEST_METRIC_IMAGES', () => {
+ beforeEach(() => {
+ mutations[types.REQUEST_METRIC_IMAGES](state);
+ });
+
+ it('should set the loading state', () => {
+ expect(state.isLoadingMetricImages).toBe(true);
+ });
+ });
+
+ describe('RECEIVE_METRIC_IMAGES_SUCCESS', () => {
+ beforeEach(() => {
+ mutations[types.RECEIVE_METRIC_IMAGES_SUCCESS](state, testImages);
+ });
+
+ it('should unset the loading state', () => {
+ expect(state.isLoadingMetricImages).toBe(false);
+ });
+
+ it('should set the metric images', () => {
+ expect(state.metricImages).toEqual(testImages);
+ });
+ });
+
+ describe('RECEIVE_METRIC_IMAGES_ERROR', () => {
+ beforeEach(() => {
+ mutations[types.RECEIVE_METRIC_IMAGES_ERROR](state);
+ });
+
+ it('should unset the loading state', () => {
+ expect(state.isLoadingMetricImages).toBe(false);
+ });
+ });
+
+ describe('REQUEST_METRIC_UPLOAD', () => {
+ beforeEach(() => {
+ mutations[types.REQUEST_METRIC_UPLOAD](state);
+ });
+
+ it('should set the loading state', () => {
+ expect(state.isUploadingImage).toBe(true);
+ });
+ });
+
+ describe('RECEIVE_METRIC_UPLOAD_SUCCESS', () => {
+ const initialImage = testImages[0];
+ const newImage = testImages[1];
+
+ beforeEach(() => {
+ createState({ metricImages: [initialImage] });
+ mutations[types.RECEIVE_METRIC_UPLOAD_SUCCESS](state, newImage);
+ });
+
+ it('should unset the loading state', () => {
+ expect(state.isUploadingImage).toBe(false);
+ });
+
+ it('should add the new metric image after the existing one', () => {
+ expect(state.metricImages).toMatchObject([initialImage, newImage]);
+ });
+ });
+
+ describe('RECEIVE_METRIC_UPLOAD_ERROR', () => {
+ beforeEach(() => {
+ mutations[types.RECEIVE_METRIC_UPLOAD_ERROR](state);
+ });
+
+ it('should unset the loading state', () => {
+ expect(state.isUploadingImage).toBe(false);
+ });
+ });
+
+ describe('RECEIVE_METRIC_UPDATE_SUCCESS', () => {
+ const initialImage = testImages[0];
+ const newImage = testImages[0];
+ newImage.url = 'https://www.gitlab.com';
+
+ beforeEach(() => {
+ createState({ metricImages: [initialImage] });
+ mutations[types.RECEIVE_METRIC_UPDATE_SUCCESS](state, newImage);
+ });
+
+ it('should unset the loading state', () => {
+ expect(state.isUploadingImage).toBe(false);
+ });
+
+ it('should replace the existing image with the new one', () => {
+ expect(state.metricImages).toMatchObject([newImage]);
+ });
+ });
+
+ describe('RECEIVE_METRIC_DELETE_SUCCESS', () => {
+ const deletedImageId = testImages[1].id;
+ const expectedResult = [testImages[0], testImages[2]];
+
+ beforeEach(() => {
+ createState({ metricImages: [...testImages] });
+ mutations[types.RECEIVE_METRIC_DELETE_SUCCESS](state, deletedImageId);
+ });
+
+ it('should remove the correct metric image', () => {
+ expect(state.metricImages).toEqual(expectedResult);
+ });
+ });
+
+ describe('SET_INITIAL_DATA', () => {
+ beforeEach(() => {
+ mutations[types.SET_INITIAL_DATA](state, initialData);
+ });
+
+ it('should unset the loading state', () => {
+ expect(state.modelIid).toBe(initialData.modelIid);
+ expect(state.projectId).toBe(initialData.projectId);
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/notes/placeholder_note_spec.js b/spec/frontend/vue_shared/components/notes/placeholder_note_spec.js
index c8dab0204d3..6881cb79740 100644
--- a/spec/frontend/vue_shared/components/notes/placeholder_note_spec.js
+++ b/spec/frontend/vue_shared/components/notes/placeholder_note_spec.js
@@ -3,7 +3,7 @@ import Vue from 'vue';
import Vuex from 'vuex';
import IssuePlaceholderNote from '~/vue_shared/components/notes/placeholder_note.vue';
import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
-import { userDataMock } from '../../../notes/mock_data';
+import { userDataMock } from 'jest/notes/mock_data';
Vue.use(Vuex);
diff --git a/spec/frontend/vue_shared/components/project_avatar/default_spec.js b/spec/frontend/vue_shared/components/project_avatar/default_spec.js
deleted file mode 100644
index d042db6051c..00000000000
--- a/spec/frontend/vue_shared/components/project_avatar/default_spec.js
+++ /dev/null
@@ -1,50 +0,0 @@
-import Vue, { nextTick } from 'vue';
-import mountComponent from 'helpers/vue_mount_component_helper';
-import { projectData } from 'jest/ide/mock_data';
-import { TEST_HOST } from 'spec/test_constants';
-import { getFirstCharacterCapitalized } from '~/lib/utils/text_utility';
-import ProjectAvatarDefault from '~/vue_shared/components/deprecated_project_avatar/default.vue';
-
-describe('ProjectAvatarDefault component', () => {
- const Component = Vue.extend(ProjectAvatarDefault);
- let vm;
-
- beforeEach(() => {
- vm = mountComponent(Component, {
- project: projectData,
- });
- });
-
- afterEach(() => {
- vm.$destroy();
- });
-
- it('renders identicon if project has no avatar_url', async () => {
- const expectedText = getFirstCharacterCapitalized(projectData.name);
-
- vm.project = {
- ...vm.project,
- avatar_url: null,
- };
-
- await nextTick();
- const identiconEl = vm.$el.querySelector('.identicon');
-
- expect(identiconEl).not.toBe(null);
- expect(identiconEl.textContent.trim()).toEqual(expectedText);
- });
-
- it('renders avatar image if project has avatar_url', async () => {
- const avatarUrl = `${TEST_HOST}/images/home/nasa.svg`;
-
- vm.project = {
- ...vm.project,
- avatar_url: avatarUrl,
- };
-
- await nextTick();
- expect(vm.$el.querySelector('.avatar')).not.toBeNull();
- expect(vm.$el.querySelector('.identicon')).toBeNull();
- expect(vm.$el.querySelector('img')).toHaveAttr('src', avatarUrl);
- });
-});
diff --git a/spec/frontend/vue_shared/components/project_selector/project_list_item_spec.js b/spec/frontend/vue_shared/components/project_selector/project_list_item_spec.js
index 5afa017aa76..397ab2254b9 100644
--- a/spec/frontend/vue_shared/components/project_selector/project_list_item_spec.js
+++ b/spec/frontend/vue_shared/components/project_selector/project_list_item_spec.js
@@ -2,7 +2,7 @@ import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import mockProjects from 'test_fixtures_static/projects.json';
import { trimText } from 'helpers/text_helper';
-import ProjectAvatar from '~/vue_shared/components/deprecated_project_avatar/default.vue';
+import ProjectAvatar from '~/vue_shared/components/project_avatar.vue';
import ProjectListItem from '~/vue_shared/components/project_selector/project_list_item.vue';
describe('ProjectListItem component', () => {
@@ -52,8 +52,13 @@ describe('ProjectListItem component', () => {
it(`renders the project avatar`, () => {
wrapper = shallowMount(Component, options);
+ const avatar = wrapper.findComponent(ProjectAvatar);
- expect(wrapper.findComponent(ProjectAvatar).exists()).toBe(true);
+ expect(avatar.exists()).toBe(true);
+ expect(avatar.props()).toMatchObject({
+ projectAvatarUrl: '',
+ projectName: project.name_with_namespace,
+ });
});
it(`renders a simple namespace name with a trailing slash`, () => {
diff --git a/spec/frontend/vue_shared/components/registry/persisted_dropdown_selection_spec.js b/spec/frontend/vue_shared/components/registry/persisted_dropdown_selection_spec.js
index c65ded000d3..616fefe847e 100644
--- a/spec/frontend/vue_shared/components/registry/persisted_dropdown_selection_spec.js
+++ b/spec/frontend/vue_shared/components/registry/persisted_dropdown_selection_spec.js
@@ -36,10 +36,10 @@ describe('Persisted dropdown selection', () => {
});
describe('local storage sync', () => {
- it('uses the local storage sync component', () => {
+ it('uses the local storage sync component with the correct props', () => {
createComponent();
- expect(findLocalStorageSync().exists()).toBe(true);
+ expect(findLocalStorageSync().props('asString')).toBe(true);
});
it('passes the right props', () => {
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 6954bd5ccff..ac313e556fc 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
@@ -42,7 +42,7 @@ exports[`RunnerAwsDeploymentsModal renders the modal 1`] = `
<gl-accordion-item-stub
class="gl-font-weight-normal"
title="More Details"
- title-visible="Less Details"
+ titlevisible="Less Details"
>
<p
class="gl-pt-2"
@@ -76,7 +76,7 @@ exports[`RunnerAwsDeploymentsModal renders the modal 1`] = `
<gl-accordion-item-stub
class="gl-font-weight-normal"
title="More Details"
- title-visible="Less Details"
+ titlevisible="Less Details"
>
<p
class="gl-pt-2"
@@ -110,7 +110,7 @@ exports[`RunnerAwsDeploymentsModal renders the modal 1`] = `
<gl-accordion-item-stub
class="gl-font-weight-normal"
title="More Details"
- title-visible="Less Details"
+ titlevisible="Less Details"
>
<p
class="gl-pt-2"
@@ -144,7 +144,7 @@ exports[`RunnerAwsDeploymentsModal renders the modal 1`] = `
<gl-accordion-item-stub
class="gl-font-weight-normal"
title="More Details"
- title-visible="Less Details"
+ titlevisible="Less Details"
>
<p
class="gl-pt-2"
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/actions_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/actions_spec.js
index 2e4c056df61..2bc513e87bf 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/actions_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/actions_spec.js
@@ -21,87 +21,81 @@ describe('LabelsSelect Actions', () => {
});
describe('setInitialState', () => {
- it('sets initial store state', (done) => {
- testAction(
+ it('sets initial store state', () => {
+ return testAction(
actions.setInitialState,
mockInitialState,
state,
[{ type: types.SET_INITIAL_STATE, payload: mockInitialState }],
[],
- done,
);
});
});
describe('toggleDropdownButton', () => {
- it('toggles dropdown button', (done) => {
- testAction(
+ it('toggles dropdown button', () => {
+ return testAction(
actions.toggleDropdownButton,
{},
state,
[{ type: types.TOGGLE_DROPDOWN_BUTTON }],
[],
- done,
);
});
});
describe('toggleDropdownContents', () => {
- it('toggles dropdown contents', (done) => {
- testAction(
+ it('toggles dropdown contents', () => {
+ return testAction(
actions.toggleDropdownContents,
{},
state,
[{ type: types.TOGGLE_DROPDOWN_CONTENTS }],
[],
- done,
);
});
});
describe('toggleDropdownContentsCreateView', () => {
- it('toggles dropdown create view', (done) => {
- testAction(
+ it('toggles dropdown create view', () => {
+ return testAction(
actions.toggleDropdownContentsCreateView,
{},
state,
[{ type: types.TOGGLE_DROPDOWN_CONTENTS_CREATE_VIEW }],
[],
- done,
);
});
});
describe('requestLabels', () => {
- it('sets value of `state.labelsFetchInProgress` to `true`', (done) => {
- testAction(actions.requestLabels, {}, state, [{ type: types.REQUEST_LABELS }], [], done);
+ it('sets value of `state.labelsFetchInProgress` to `true`', () => {
+ return testAction(actions.requestLabels, {}, state, [{ type: types.REQUEST_LABELS }], []);
});
});
describe('receiveLabelsSuccess', () => {
- it('sets provided labels to `state.labels`', (done) => {
+ it('sets provided labels to `state.labels`', () => {
const labels = [{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }];
- testAction(
+ return testAction(
actions.receiveLabelsSuccess,
labels,
state,
[{ type: types.RECEIVE_SET_LABELS_SUCCESS, payload: labels }],
[],
- done,
);
});
});
describe('receiveLabelsFailure', () => {
- it('sets value `state.labelsFetchInProgress` to `false`', (done) => {
- testAction(
+ it('sets value `state.labelsFetchInProgress` to `false`', () => {
+ return testAction(
actions.receiveLabelsFailure,
{},
state,
[{ type: types.RECEIVE_SET_LABELS_FAILURE }],
[],
- done,
);
});
@@ -125,72 +119,67 @@ describe('LabelsSelect Actions', () => {
});
describe('on success', () => {
- it('dispatches `requestLabels` & `receiveLabelsSuccess` actions', (done) => {
+ it('dispatches `requestLabels` & `receiveLabelsSuccess` actions', () => {
const labels = [{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }];
mock.onGet(/labels.json/).replyOnce(200, labels);
- testAction(
+ return testAction(
actions.fetchLabels,
{},
state,
[],
[{ type: 'requestLabels' }, { type: 'receiveLabelsSuccess', payload: labels }],
- done,
);
});
});
describe('on failure', () => {
- it('dispatches `requestLabels` & `receiveLabelsFailure` actions', (done) => {
+ it('dispatches `requestLabels` & `receiveLabelsFailure` actions', () => {
mock.onGet(/labels.json/).replyOnce(500, {});
- testAction(
+ return testAction(
actions.fetchLabels,
{},
state,
[],
[{ type: 'requestLabels' }, { type: 'receiveLabelsFailure' }],
- done,
);
});
});
});
describe('requestCreateLabel', () => {
- it('sets value `state.labelCreateInProgress` to `true`', (done) => {
- testAction(
+ it('sets value `state.labelCreateInProgress` to `true`', () => {
+ return testAction(
actions.requestCreateLabel,
{},
state,
[{ type: types.REQUEST_CREATE_LABEL }],
[],
- done,
);
});
});
describe('receiveCreateLabelSuccess', () => {
- it('sets value `state.labelCreateInProgress` to `false`', (done) => {
- testAction(
+ it('sets value `state.labelCreateInProgress` to `false`', () => {
+ return testAction(
actions.receiveCreateLabelSuccess,
{},
state,
[{ type: types.RECEIVE_CREATE_LABEL_SUCCESS }],
[],
- done,
);
});
});
describe('receiveCreateLabelFailure', () => {
- it('sets value `state.labelCreateInProgress` to `false`', (done) => {
- testAction(
+ it('sets value `state.labelCreateInProgress` to `false`', () => {
+ return testAction(
actions.receiveCreateLabelFailure,
{},
state,
[{ type: types.RECEIVE_CREATE_LABEL_FAILURE }],
[],
- done,
);
});
@@ -214,11 +203,11 @@ describe('LabelsSelect Actions', () => {
});
describe('on success', () => {
- it('dispatches `requestCreateLabel`, `fetchLabels` & `receiveCreateLabelSuccess` & `toggleDropdownContentsCreateView` actions', (done) => {
+ it('dispatches `requestCreateLabel`, `fetchLabels` & `receiveCreateLabelSuccess` & `toggleDropdownContentsCreateView` actions', () => {
const label = { id: 1 };
mock.onPost(/labels.json/).replyOnce(200, label);
- testAction(
+ return testAction(
actions.createLabel,
{},
state,
@@ -229,38 +218,35 @@ describe('LabelsSelect Actions', () => {
{ type: 'receiveCreateLabelSuccess' },
{ type: 'toggleDropdownContentsCreateView' },
],
- done,
);
});
});
describe('on failure', () => {
- it('dispatches `requestCreateLabel` & `receiveCreateLabelFailure` actions', (done) => {
+ it('dispatches `requestCreateLabel` & `receiveCreateLabelFailure` actions', () => {
mock.onPost(/labels.json/).replyOnce(500, {});
- testAction(
+ return testAction(
actions.createLabel,
{},
state,
[],
[{ type: 'requestCreateLabel' }, { type: 'receiveCreateLabelFailure' }],
- done,
);
});
});
});
describe('updateSelectedLabels', () => {
- it('updates `state.labels` based on provided `labels` param', (done) => {
+ it('updates `state.labels` based on provided `labels` param', () => {
const labels = [{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }];
- testAction(
+ return testAction(
actions.updateSelectedLabels,
labels,
state,
[{ type: types.UPDATE_SELECTED_LABELS, payload: { labels } }],
[],
- done,
);
});
});
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/labels_select_root_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/labels_select_root_spec.js
index 67e1a3ce932..1b27a294b90 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/labels_select_root_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/labels_select_root_spec.js
@@ -11,9 +11,15 @@ import DropdownValue from '~/vue_shared/components/sidebar/labels_select_widget/
import issueLabelsQuery from '~/vue_shared/components/sidebar/labels_select_widget/graphql/issue_labels.query.graphql';
import updateIssueLabelsMutation from '~/boards/graphql/issue_set_labels.mutation.graphql';
import updateMergeRequestLabelsMutation from '~/sidebar/queries/update_merge_request_labels.mutation.graphql';
+import issuableLabelsSubscription from 'ee_else_ce/sidebar/queries/issuable_labels.subscription.graphql';
import updateEpicLabelsMutation from '~/vue_shared/components/sidebar/labels_select_widget/graphql/epic_update_labels.mutation.graphql';
import LabelsSelectRoot from '~/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue';
-import { mockConfig, issuableLabelsQueryResponse, updateLabelsMutationResponse } from './mock_data';
+import {
+ mockConfig,
+ issuableLabelsQueryResponse,
+ updateLabelsMutationResponse,
+ issuableLabelsSubscriptionResponse,
+} from './mock_data';
jest.mock('~/flash');
@@ -21,6 +27,7 @@ Vue.use(VueApollo);
const successfulQueryHandler = jest.fn().mockResolvedValue(issuableLabelsQueryResponse);
const successfulMutationHandler = jest.fn().mockResolvedValue(updateLabelsMutationResponse);
+const subscriptionHandler = jest.fn().mockResolvedValue(issuableLabelsSubscriptionResponse);
const errorQueryHandler = jest.fn().mockRejectedValue('Houston, we have a problem');
const updateLabelsMutation = {
@@ -42,10 +49,12 @@ describe('LabelsSelectRoot', () => {
issuableType = IssuableType.Issue,
queryHandler = successfulQueryHandler,
mutationHandler = successfulMutationHandler,
+ isRealtimeEnabled = false,
} = {}) => {
const mockApollo = createMockApollo([
[issueLabelsQuery, queryHandler],
[updateLabelsMutation[issuableType], mutationHandler],
+ [issuableLabelsSubscription, subscriptionHandler],
]);
wrapper = shallowMount(LabelsSelectRoot, {
@@ -65,6 +74,9 @@ describe('LabelsSelectRoot', () => {
allowLabelEdit: true,
allowLabelCreate: true,
labelsManagePath: 'test',
+ glFeatures: {
+ realtimeLabels: isRealtimeEnabled,
+ },
},
});
};
@@ -190,5 +202,26 @@ describe('LabelsSelectRoot', () => {
message: 'An error occurred while updating labels.',
});
});
+
+ it('does not emit `updateSelectedLabels` event when the subscription is triggered and FF is disabled', async () => {
+ createComponent();
+ await waitForPromises();
+
+ expect(wrapper.emitted('updateSelectedLabels')).toBeUndefined();
+ });
+
+ it('emits `updateSelectedLabels` event when the subscription is triggered and FF is enabled', async () => {
+ createComponent({ isRealtimeEnabled: true });
+ await waitForPromises();
+
+ expect(wrapper.emitted('updateSelectedLabels')).toEqual([
+ [
+ {
+ id: '1',
+ labels: issuableLabelsSubscriptionResponse.data.issuableLabelsUpdated.labels.nodes,
+ },
+ ],
+ ]);
+ });
});
});
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/mock_data.js b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/mock_data.js
index 49224fb915c..afad9314ace 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/mock_data.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/mock_data.js
@@ -141,6 +141,34 @@ export const issuableLabelsQueryResponse = {
},
};
+export const issuableLabelsSubscriptionResponse = {
+ data: {
+ issuableLabelsUpdated: {
+ id: '1',
+ labels: {
+ nodes: [
+ {
+ __typename: 'Label',
+ color: '#330066',
+ description: null,
+ id: 'gid://gitlab/ProjectLabel/1',
+ title: 'Label1',
+ textColor: '#000000',
+ },
+ {
+ __typename: 'Label',
+ color: '#000000',
+ description: null,
+ id: 'gid://gitlab/ProjectLabel/2',
+ title: 'Label2',
+ textColor: '#ffffff',
+ },
+ ],
+ },
+ },
+ },
+};
+
export const updateLabelsMutationResponse = {
data: {
updateIssuableLabels: {
diff --git a/spec/frontend/vue_shared/components/source_viewer/components/chunk_line_spec.js b/spec/frontend/vue_shared/components/source_viewer/components/chunk_line_spec.js
new file mode 100644
index 00000000000..eb2eec92534
--- /dev/null
+++ b/spec/frontend/vue_shared/components/source_viewer/components/chunk_line_spec.js
@@ -0,0 +1,69 @@
+import { GlLink } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import ChunkLine from '~/vue_shared/components/source_viewer/components/chunk_line.vue';
+import {
+ BIDI_CHARS,
+ BIDI_CHARS_CLASS_LIST,
+ BIDI_CHAR_TOOLTIP,
+} from '~/vue_shared/components/source_viewer/constants';
+
+const DEFAULT_PROPS = {
+ number: 2,
+ content: '// Line content',
+ language: 'javascript',
+};
+
+describe('Chunk Line component', () => {
+ let wrapper;
+
+ const createComponent = (props = {}) => {
+ wrapper = shallowMountExtended(ChunkLine, { propsData: { ...DEFAULT_PROPS, ...props } });
+ };
+
+ const findLink = () => wrapper.findComponent(GlLink);
+ const findContent = () => wrapper.findByTestId('content');
+ const findWrappedBidiChars = () => wrapper.findAllByTestId('bidi-wrapper');
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ afterEach(() => wrapper.destroy());
+
+ describe('rendering', () => {
+ it('wraps BiDi characters', () => {
+ const content = `// some content ${BIDI_CHARS.toString()} with BiDi chars`;
+ createComponent({ content });
+ const wrappedBidiChars = findWrappedBidiChars();
+
+ expect(wrappedBidiChars.length).toBe(BIDI_CHARS.length);
+
+ wrappedBidiChars.wrappers.forEach((_, i) => {
+ expect(wrappedBidiChars.at(i).text()).toBe(BIDI_CHARS[i]);
+ expect(wrappedBidiChars.at(i).attributes()).toMatchObject({
+ class: BIDI_CHARS_CLASS_LIST,
+ title: BIDI_CHAR_TOOLTIP,
+ });
+ });
+ });
+
+ it('renders a line number', () => {
+ expect(findLink().attributes()).toMatchObject({
+ 'data-line-number': `${DEFAULT_PROPS.number}`,
+ to: `#L${DEFAULT_PROPS.number}`,
+ id: `L${DEFAULT_PROPS.number}`,
+ });
+
+ expect(findLink().text()).toBe(DEFAULT_PROPS.number.toString());
+ });
+
+ it('renders content', () => {
+ expect(findContent().attributes()).toMatchObject({
+ id: `LC${DEFAULT_PROPS.number}`,
+ lang: DEFAULT_PROPS.language,
+ });
+
+ expect(findContent().text()).toBe(DEFAULT_PROPS.content);
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/source_viewer/components/chunk_spec.js b/spec/frontend/vue_shared/components/source_viewer/components/chunk_spec.js
new file mode 100644
index 00000000000..42c4f2eacb8
--- /dev/null
+++ b/spec/frontend/vue_shared/components/source_viewer/components/chunk_spec.js
@@ -0,0 +1,82 @@
+import { GlIntersectionObserver } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import Chunk from '~/vue_shared/components/source_viewer/components/chunk.vue';
+import ChunkLine from '~/vue_shared/components/source_viewer/components/chunk_line.vue';
+
+const DEFAULT_PROPS = {
+ chunkIndex: 2,
+ isHighlighted: false,
+ content: '// Line 1 content \n // Line 2 content',
+ startingFrom: 140,
+ totalLines: 50,
+ language: 'javascript',
+};
+
+describe('Chunk component', () => {
+ let wrapper;
+
+ const createComponent = (props = {}) => {
+ wrapper = shallowMountExtended(Chunk, { propsData: { ...DEFAULT_PROPS, ...props } });
+ };
+
+ const findIntersectionObserver = () => wrapper.findComponent(GlIntersectionObserver);
+ const findChunkLines = () => wrapper.findAllComponents(ChunkLine);
+ const findLineNumbers = () => wrapper.findAllByTestId('line-number');
+ const findContent = () => wrapper.findByTestId('content');
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ afterEach(() => wrapper.destroy());
+
+ describe('Intersection observer', () => {
+ it('renders an Intersection observer component', () => {
+ expect(findIntersectionObserver().exists()).toBe(true);
+ });
+
+ it('emits an appear event when intersection-observer appears', () => {
+ findIntersectionObserver().vm.$emit('appear');
+
+ expect(wrapper.emitted('appear')).toEqual([[DEFAULT_PROPS.chunkIndex]]);
+ });
+
+ it('does not emit an appear event is isHighlighted is true', () => {
+ createComponent({ isHighlighted: true });
+ findIntersectionObserver().vm.$emit('appear');
+
+ expect(wrapper.emitted('appear')).toEqual(undefined);
+ });
+ });
+
+ describe('rendering', () => {
+ it('does not render a Chunk Line component if isHighlighted is false', () => {
+ expect(findChunkLines().length).toBe(0);
+ });
+
+ it('renders simplified line numbers and content if isHighlighted is false', () => {
+ expect(findLineNumbers().length).toBe(DEFAULT_PROPS.totalLines);
+
+ expect(findLineNumbers().at(0).attributes()).toMatchObject({
+ 'data-line-number': `${DEFAULT_PROPS.startingFrom + 1}`,
+ href: `#L${DEFAULT_PROPS.startingFrom + 1}`,
+ id: `L${DEFAULT_PROPS.startingFrom + 1}`,
+ });
+
+ expect(findContent().text()).toBe(DEFAULT_PROPS.content);
+ });
+
+ it('renders Chunk Line components if isHighlighted is true', () => {
+ const splitContent = DEFAULT_PROPS.content.split('\n');
+ createComponent({ isHighlighted: true });
+
+ expect(findChunkLines().length).toBe(splitContent.length);
+
+ expect(findChunkLines().at(0).props()).toMatchObject({
+ number: DEFAULT_PROPS.startingFrom + 1,
+ content: splitContent[0],
+ language: DEFAULT_PROPS.language,
+ });
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/source_viewer/source_viewer_spec.js b/spec/frontend/vue_shared/components/source_viewer/source_viewer_spec.js
index ab579945e22..6a9ea75127d 100644
--- a/spec/frontend/vue_shared/components/source_viewer/source_viewer_spec.js
+++ b/spec/frontend/vue_shared/components/source_viewer/source_viewer_spec.js
@@ -1,24 +1,38 @@
import hljs from 'highlight.js/lib/core';
-import { GlLoadingIcon } from '@gitlab/ui';
-import Vue, { nextTick } from 'vue';
+import Vue from 'vue';
import VueRouter from 'vue-router';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import SourceViewer from '~/vue_shared/components/source_viewer/source_viewer.vue';
+import Chunk from '~/vue_shared/components/source_viewer/components/chunk.vue';
import { ROUGE_TO_HLJS_LANGUAGE_MAP } from '~/vue_shared/components/source_viewer/constants';
-import LineNumbers from '~/vue_shared/components/line_numbers.vue';
import waitForPromises from 'helpers/wait_for_promises';
-import * as sourceViewerUtils from '~/vue_shared/components/source_viewer/utils';
+import LineHighlighter from '~/blob/line_highlighter';
+import eventHub from '~/notes/event_hub';
+jest.mock('~/blob/line_highlighter');
jest.mock('highlight.js/lib/core');
Vue.use(VueRouter);
const router = new VueRouter();
+const generateContent = (content, totalLines = 1) => {
+ let generatedContent = '';
+ for (let i = 0; i < totalLines; i += 1) {
+ generatedContent += `Line: ${i + 1} = ${content}\n`;
+ }
+ return generatedContent;
+};
+
+const execImmediately = (callback) => callback();
+
describe('Source Viewer component', () => {
let wrapper;
const language = 'docker';
const mappedLanguage = ROUGE_TO_HLJS_LANGUAGE_MAP[language];
- const content = `// Some source code`;
- const DEFAULT_BLOB_DATA = { language, rawTextBlob: content };
+ const chunk1 = generateContent('// Some source code 1', 70);
+ const chunk2 = generateContent('// Some source code 2', 70);
+ const content = chunk1 + chunk2;
+ const path = 'some/path.js';
+ const DEFAULT_BLOB_DATA = { language, rawTextBlob: content, path };
const highlightedContent = `<span data-testid='test-highlighted' id='LC1'>${content}</span><span id='LC2'></span>`;
const createComponent = async (blob = {}) => {
@@ -29,15 +43,13 @@ describe('Source Viewer component', () => {
await waitForPromises();
};
- const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
- const findLineNumbers = () => wrapper.findComponent(LineNumbers);
- const findHighlightedContent = () => wrapper.findByTestId('test-highlighted');
- const findFirstLine = () => wrapper.find('#LC1');
+ const findChunks = () => wrapper.findAllComponents(Chunk);
beforeEach(() => {
hljs.highlight.mockImplementation(() => ({ value: highlightedContent }));
hljs.highlightAuto.mockImplementation(() => ({ value: highlightedContent }));
- jest.spyOn(sourceViewerUtils, 'wrapLines');
+ jest.spyOn(window, 'requestIdleCallback').mockImplementation(execImmediately);
+ jest.spyOn(eventHub, '$emit');
return createComponent();
});
@@ -45,6 +57,8 @@ describe('Source Viewer component', () => {
afterEach(() => wrapper.destroy());
describe('highlight.js', () => {
+ beforeEach(() => createComponent({ language: mappedLanguage }));
+
it('registers the language definition', async () => {
const languageDefinition = await import(`highlight.js/lib/languages/${mappedLanguage}`);
@@ -54,72 +68,51 @@ describe('Source Viewer component', () => {
);
});
- it('highlights the content', () => {
- expect(hljs.highlight).toHaveBeenCalledWith(content, { language: mappedLanguage });
+ it('highlights the first chunk', () => {
+ expect(hljs.highlight).toHaveBeenCalledWith(chunk1.trim(), { language: mappedLanguage });
});
describe('auto-detects if a language cannot be loaded', () => {
beforeEach(() => createComponent({ language: 'some_unknown_language' }));
it('highlights the content with auto-detection', () => {
- expect(hljs.highlightAuto).toHaveBeenCalledWith(content);
+ expect(hljs.highlightAuto).toHaveBeenCalledWith(chunk1.trim());
});
});
});
describe('rendering', () => {
- it('renders a loading icon if no highlighted content is available yet', async () => {
- hljs.highlight.mockImplementation(() => ({ value: null }));
- await createComponent();
-
- expect(findLoadingIcon().exists()).toBe(true);
- });
+ it('renders the first chunk', async () => {
+ const firstChunk = findChunks().at(0);
- it('calls the wrapLines helper method with highlightedContent and mappedLanguage', () => {
- expect(sourceViewerUtils.wrapLines).toHaveBeenCalledWith(highlightedContent, mappedLanguage);
- });
-
- it('renders Line Numbers', () => {
- expect(findLineNumbers().props('lines')).toBe(1);
- });
+ expect(firstChunk.props('content')).toContain(chunk1);
- it('renders the highlighted content', () => {
- expect(findHighlightedContent().exists()).toBe(true);
+ expect(firstChunk.props()).toMatchObject({
+ totalLines: 70,
+ startingFrom: 0,
+ });
});
- });
- describe('selecting a line', () => {
- let firstLine;
- let firstLineElement;
+ it('renders the second chunk', async () => {
+ const secondChunk = findChunks().at(1);
- beforeEach(() => {
- firstLine = findFirstLine();
- firstLineElement = firstLine.element;
+ expect(secondChunk.props('content')).toContain(chunk2.trim());
- jest.spyOn(firstLineElement, 'scrollIntoView');
- jest.spyOn(firstLineElement.classList, 'add');
- jest.spyOn(firstLineElement.classList, 'remove');
- });
-
- it('adds the highlight (hll) class', async () => {
- wrapper.vm.$router.push('#LC1');
- await nextTick();
-
- expect(firstLineElement.classList.add).toHaveBeenCalledWith('hll');
+ expect(secondChunk.props()).toMatchObject({
+ totalLines: 70,
+ startingFrom: 70,
+ });
});
+ });
- it('removes the highlight (hll) class from a previously highlighted line', async () => {
- wrapper.vm.$router.push('#LC2');
- await nextTick();
-
- expect(firstLineElement.classList.remove).toHaveBeenCalledWith('hll');
- });
+ it('emits showBlobInteractionZones on the eventHub when chunk appears', () => {
+ findChunks().at(0).vm.$emit('appear');
+ expect(eventHub.$emit).toBeCalledWith('showBlobInteractionZones', path);
+ });
- it('scrolls the line into view', () => {
- expect(firstLineElement.scrollIntoView).toHaveBeenCalledWith({
- behavior: 'smooth',
- block: 'center',
- });
+ describe('LineHighlighter', () => {
+ it('instantiates the lineHighlighter class', async () => {
+ expect(LineHighlighter).toHaveBeenCalledWith({ scrollBehavior: 'auto' });
});
});
});
diff --git a/spec/frontend/vue_shared/components/source_viewer/utils_spec.js b/spec/frontend/vue_shared/components/source_viewer/utils_spec.js
deleted file mode 100644
index 0631e7efd54..00000000000
--- a/spec/frontend/vue_shared/components/source_viewer/utils_spec.js
+++ /dev/null
@@ -1,26 +0,0 @@
-import { wrapLines } from '~/vue_shared/components/source_viewer/utils';
-
-describe('Wrap lines', () => {
- it.each`
- content | language | output
- ${'line 1'} | ${'javascript'} | ${'<span id="LC1" lang="javascript" class="line">line 1</span>'}
- ${'line 1\nline 2'} | ${'html'} | ${`<span id="LC1" lang="html" class="line">line 1</span>\n<span id="LC2" lang="html" class="line">line 2</span>`}
- ${'<span class="hljs-code">line 1\nline 2</span>'} | ${'html'} | ${`<span id="LC1" lang="html" class="hljs-code">line 1\n<span id="LC2" lang="html" class="line">line 2</span></span>`}
- ${'<span class="hljs-code">```bash'} | ${'bash'} | ${'<span id="LC1" lang="bash" class="hljs-code">```bash'}
- ${'<span class="hljs-code">```bash'} | ${'valid-language1'} | ${'<span id="LC1" lang="valid-language1" class="hljs-code">```bash'}
- ${'<span class="hljs-code">```bash'} | ${'valid_language2'} | ${'<span id="LC1" lang="valid_language2" class="hljs-code">```bash'}
- `('returns lines wrapped in spans containing line numbers', ({ content, language, output }) => {
- expect(wrapLines(content, language)).toBe(output);
- });
-
- it.each`
- language
- ${'invalidLanguage>'}
- ${'"invalidLanguage"'}
- ${'<invalidLanguage'}
- `('returns lines safely without XSS language is not valid', ({ language }) => {
- expect(wrapLines('<span class="hljs-code">```bash', language)).toBe(
- '<span id="LC1" lang="" class="hljs-code">```bash',
- );
- });
-});
diff --git a/spec/frontend/vue_shared/components/user_avatar/user_avatar_image_new_spec.js b/spec/frontend/vue_shared/components/user_avatar/user_avatar_image_new_spec.js
index f624f84eabd..5e05b54cb8c 100644
--- a/spec/frontend/vue_shared/components/user_avatar/user_avatar_image_new_spec.js
+++ b/spec/frontend/vue_shared/components/user_avatar/user_avatar_image_new_spec.js
@@ -109,19 +109,33 @@ describe('User Avatar Image Component', () => {
default: ['Action!'],
};
- beforeEach(() => {
- wrapper = shallowMount(UserAvatarImage, {
- propsData: PROVIDED_PROPS,
- slots,
+ describe('when `tooltipText` is provided and no default slot', () => {
+ beforeEach(() => {
+ wrapper = shallowMount(UserAvatarImage, {
+ propsData: { ...PROVIDED_PROPS },
+ });
});
- });
- it('renders the tooltip slot', () => {
- expect(wrapper.findComponent(GlTooltip).exists()).toBe(true);
+ it('renders the tooltip with `tooltipText` as content', () => {
+ expect(wrapper.findComponent(GlTooltip).text()).toBe(PROVIDED_PROPS.tooltipText);
+ });
});
- it('renders the tooltip content', () => {
- expect(wrapper.findComponent(GlTooltip).text()).toContain(slots.default[0]);
+ describe('when `tooltipText` and default slot is provided', () => {
+ beforeEach(() => {
+ wrapper = shallowMount(UserAvatarImage, {
+ propsData: { ...PROVIDED_PROPS },
+ slots,
+ });
+ });
+
+ it('does not render `tooltipText` inside the tooltip', () => {
+ expect(wrapper.findComponent(GlTooltip).text()).not.toBe(PROVIDED_PROPS.tooltipText);
+ });
+
+ it('renders the content provided via default slot', () => {
+ expect(wrapper.findComponent(GlTooltip).text()).toContain(slots.default[0]);
+ });
});
});
});
diff --git a/spec/frontend/vue_shared/components/user_avatar/user_avatar_image_old_spec.js b/spec/frontend/vue_shared/components/user_avatar/user_avatar_image_old_spec.js
index 5051b2b9cae..2c1be6ec47e 100644
--- a/spec/frontend/vue_shared/components/user_avatar/user_avatar_image_old_spec.js
+++ b/spec/frontend/vue_shared/components/user_avatar/user_avatar_image_old_spec.js
@@ -90,33 +90,38 @@ describe('User Avatar Image Component', () => {
});
});
- describe('dynamic tooltip content', () => {
- const props = PROVIDED_PROPS;
+ describe('Dynamic tooltip content', () => {
const slots = {
default: ['Action!'],
};
- beforeEach(() => {
- wrapper = shallowMount(UserAvatarImage, {
- propsData: { props },
- slots,
+ describe('when `tooltipText` is provided and no default slot', () => {
+ beforeEach(() => {
+ wrapper = shallowMount(UserAvatarImage, {
+ propsData: { ...PROVIDED_PROPS },
+ });
});
- });
- it('renders the tooltip slot', () => {
- expect(wrapper.findComponent(GlTooltip).exists()).toBe(true);
+ it('renders the tooltip with `tooltipText` as content', () => {
+ expect(wrapper.findComponent(GlTooltip).text()).toBe(PROVIDED_PROPS.tooltipText);
+ });
});
- it('renders the tooltip content', () => {
- expect(wrapper.findComponent(GlTooltip).text()).toContain(slots.default[0]);
- });
+ describe('when `tooltipText` and default slot is provided', () => {
+ beforeEach(() => {
+ wrapper = shallowMount(UserAvatarImage, {
+ propsData: { ...PROVIDED_PROPS },
+ slots,
+ });
+ });
- it('does not render tooltip data attributes on avatar image', () => {
- const avatarImg = wrapper.find('img');
+ it('does not render `tooltipText` inside the tooltip', () => {
+ expect(wrapper.findComponent(GlTooltip).text()).not.toBe(PROVIDED_PROPS.tooltipText);
+ });
- expect(avatarImg.attributes('title')).toBeFalsy();
- expect(avatarImg.attributes('data-placement')).not.toBeDefined();
- expect(avatarImg.attributes('data-container')).not.toBeDefined();
+ it('renders the content provided via default slot', () => {
+ expect(wrapper.findComponent(GlTooltip).text()).toContain(slots.default[0]);
+ });
});
});
});
diff --git a/spec/frontend/vue_shared/components/user_avatar/user_avatar_list_spec.js b/spec/frontend/vue_shared/components/user_avatar/user_avatar_list_spec.js
index 66bb234aef6..20ff0848cff 100644
--- a/spec/frontend/vue_shared/components/user_avatar/user_avatar_list_spec.js
+++ b/spec/frontend/vue_shared/components/user_avatar/user_avatar_list_spec.js
@@ -153,4 +153,29 @@ describe('UserAvatarList', () => {
});
});
});
+
+ describe('additional styling for the image', () => {
+ it('should not add CSS class when feature flag `glAvatarForAllUserAvatars` is disabled', () => {
+ factory({
+ propsData: { items: createList(1) },
+ });
+
+ const link = wrapper.findComponent(UserAvatarLink);
+ expect(link.props('imgCssClasses')).not.toBe('gl-mr-3');
+ });
+
+ it('should add CSS class when feature flag `glAvatarForAllUserAvatars` is enabled', () => {
+ factory({
+ propsData: { items: createList(1) },
+ provide: {
+ glFeatures: {
+ glAvatarForAllUserAvatars: true,
+ },
+ },
+ });
+
+ const link = wrapper.findComponent(UserAvatarLink);
+ expect(link.props('imgCssClasses')).toBe('gl-mr-3');
+ });
+ });
});
diff --git a/spec/frontend/vue_shared/components/user_select_spec.js b/spec/frontend/vue_shared/components/user_select_spec.js
index cb476910944..ec9128d5e38 100644
--- a/spec/frontend/vue_shared/components/user_select_spec.js
+++ b/spec/frontend/vue_shared/components/user_select_spec.js
@@ -16,7 +16,7 @@ import {
searchResponseOnMR,
projectMembersResponse,
participantsQueryResponse,
-} from '../../sidebar/mock_data';
+} from 'jest/sidebar/mock_data';
const assignee = {
id: 'gid://gitlab/User/4',
diff --git a/spec/frontend/vue_shared/components/web_ide_link_spec.js b/spec/frontend/vue_shared/components/web_ide_link_spec.js
index e79935f8fa6..040461f6be4 100644
--- a/spec/frontend/vue_shared/components/web_ide_link_spec.js
+++ b/spec/frontend/vue_shared/components/web_ide_link_spec.js
@@ -261,7 +261,10 @@ describe('Web IDE link component', () => {
});
it('should update local storage when selection changes', async () => {
- expect(findLocalStorageSync().props('value')).toBe(ACTION_WEB_IDE.key);
+ expect(findLocalStorageSync().props()).toMatchObject({
+ asString: true,
+ value: ACTION_WEB_IDE.key,
+ });
findActionsButton().vm.$emit('select', ACTION_GITPOD.key);
diff --git a/spec/frontend/vue_shared/issuable/list/components/issuable_list_root_spec.js b/spec/frontend/vue_shared/issuable/list/components/issuable_list_root_spec.js
index 64823cd4c6c..058cb30c1d5 100644
--- a/spec/frontend/vue_shared/issuable/list/components/issuable_list_root_spec.js
+++ b/spec/frontend/vue_shared/issuable/list/components/issuable_list_root_spec.js
@@ -1,4 +1,9 @@
-import { GlAlert, GlKeysetPagination, GlSkeletonLoading, GlPagination } from '@gitlab/ui';
+import {
+ GlAlert,
+ GlKeysetPagination,
+ GlDeprecatedSkeletonLoading as GlSkeletonLoading,
+ GlPagination,
+} from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import VueDraggable from 'vuedraggable';
diff --git a/spec/frontend/vue_shared/security_reports/store/modules/sast/actions_spec.js b/spec/frontend/vue_shared/security_reports/store/modules/sast/actions_spec.js
index 6af07273cf6..46bfd7eceb1 100644
--- a/spec/frontend/vue_shared/security_reports/store/modules/sast/actions_spec.js
+++ b/spec/frontend/vue_shared/security_reports/store/modules/sast/actions_spec.js
@@ -26,8 +26,8 @@ describe('sast report actions', () => {
});
describe('setDiffEndpoint', () => {
- it(`should commit ${types.SET_DIFF_ENDPOINT} with the correct path`, (done) => {
- testAction(
+ it(`should commit ${types.SET_DIFF_ENDPOINT} with the correct path`, () => {
+ return testAction(
actions.setDiffEndpoint,
diffEndpoint,
state,
@@ -38,20 +38,19 @@ describe('sast report actions', () => {
},
],
[],
- done,
);
});
});
describe('requestDiff', () => {
- it(`should commit ${types.REQUEST_DIFF}`, (done) => {
- testAction(actions.requestDiff, {}, state, [{ type: types.REQUEST_DIFF }], [], done);
+ it(`should commit ${types.REQUEST_DIFF}`, () => {
+ return testAction(actions.requestDiff, {}, state, [{ type: types.REQUEST_DIFF }], []);
});
});
describe('receiveDiffSuccess', () => {
- it(`should commit ${types.RECEIVE_DIFF_SUCCESS} with the correct response`, (done) => {
- testAction(
+ it(`should commit ${types.RECEIVE_DIFF_SUCCESS} with the correct response`, () => {
+ return testAction(
actions.receiveDiffSuccess,
reports,
state,
@@ -62,14 +61,13 @@ describe('sast report actions', () => {
},
],
[],
- done,
);
});
});
describe('receiveDiffError', () => {
- it(`should commit ${types.RECEIVE_DIFF_ERROR} with the correct response`, (done) => {
- testAction(
+ it(`should commit ${types.RECEIVE_DIFF_ERROR} with the correct response`, () => {
+ return testAction(
actions.receiveDiffError,
error,
state,
@@ -80,7 +78,6 @@ describe('sast report actions', () => {
},
],
[],
- done,
);
});
});
@@ -107,9 +104,9 @@ describe('sast report actions', () => {
.replyOnce(200, reports.enrichData);
});
- it('should dispatch the `receiveDiffSuccess` action', (done) => {
+ it('should dispatch the `receiveDiffSuccess` action', () => {
const { diff, enrichData } = reports;
- testAction(
+ return testAction(
actions.fetchDiff,
{},
{ ...rootState, ...state },
@@ -124,7 +121,6 @@ describe('sast report actions', () => {
},
},
],
- done,
);
});
});
@@ -135,10 +131,10 @@ describe('sast report actions', () => {
mock.onGet(diffEndpoint).replyOnce(200, reports.diff);
});
- it('should dispatch the `receiveDiffSuccess` action with empty enrich data', (done) => {
+ it('should dispatch the `receiveDiffSuccess` action with empty enrich data', () => {
const { diff } = reports;
const enrichData = [];
- testAction(
+ return testAction(
actions.fetchDiff,
{},
{ ...rootState, ...state },
@@ -153,7 +149,6 @@ describe('sast report actions', () => {
},
},
],
- done,
);
});
});
@@ -167,14 +162,13 @@ describe('sast report actions', () => {
.replyOnce(404);
});
- it('should dispatch the `receiveError` action', (done) => {
- testAction(
+ it('should dispatch the `receiveError` action', () => {
+ return testAction(
actions.fetchDiff,
{},
{ ...rootState, ...state },
[],
[{ type: 'requestDiff' }, { type: 'receiveDiffError' }],
- done,
);
});
});
@@ -188,14 +182,13 @@ describe('sast report actions', () => {
.replyOnce(200, reports.enrichData);
});
- it('should dispatch the `receiveDiffError` action', (done) => {
- testAction(
+ it('should dispatch the `receiveDiffError` action', () => {
+ return testAction(
actions.fetchDiff,
{},
{ ...rootState, ...state },
[],
[{ type: 'requestDiff' }, { type: 'receiveDiffError' }],
- done,
);
});
});
diff --git a/spec/frontend/vue_shared/security_reports/store/modules/secret_detection/actions_spec.js b/spec/frontend/vue_shared/security_reports/store/modules/secret_detection/actions_spec.js
index d22fee864e7..4f4f653bb72 100644
--- a/spec/frontend/vue_shared/security_reports/store/modules/secret_detection/actions_spec.js
+++ b/spec/frontend/vue_shared/security_reports/store/modules/secret_detection/actions_spec.js
@@ -26,8 +26,8 @@ describe('secret detection report actions', () => {
});
describe('setDiffEndpoint', () => {
- it(`should commit ${types.SET_DIFF_ENDPOINT} with the correct path`, (done) => {
- testAction(
+ it(`should commit ${types.SET_DIFF_ENDPOINT} with the correct path`, () => {
+ return testAction(
actions.setDiffEndpoint,
diffEndpoint,
state,
@@ -38,20 +38,19 @@ describe('secret detection report actions', () => {
},
],
[],
- done,
);
});
});
describe('requestDiff', () => {
- it(`should commit ${types.REQUEST_DIFF}`, (done) => {
- testAction(actions.requestDiff, {}, state, [{ type: types.REQUEST_DIFF }], [], done);
+ it(`should commit ${types.REQUEST_DIFF}`, () => {
+ return testAction(actions.requestDiff, {}, state, [{ type: types.REQUEST_DIFF }], []);
});
});
describe('receiveDiffSuccess', () => {
- it(`should commit ${types.RECEIVE_DIFF_SUCCESS} with the correct response`, (done) => {
- testAction(
+ it(`should commit ${types.RECEIVE_DIFF_SUCCESS} with the correct response`, () => {
+ return testAction(
actions.receiveDiffSuccess,
reports,
state,
@@ -62,14 +61,13 @@ describe('secret detection report actions', () => {
},
],
[],
- done,
);
});
});
describe('receiveDiffError', () => {
- it(`should commit ${types.RECEIVE_DIFF_ERROR} with the correct response`, (done) => {
- testAction(
+ it(`should commit ${types.RECEIVE_DIFF_ERROR} with the correct response`, () => {
+ return testAction(
actions.receiveDiffError,
error,
state,
@@ -80,7 +78,6 @@ describe('secret detection report actions', () => {
},
],
[],
- done,
);
});
});
@@ -107,9 +104,10 @@ describe('secret detection report actions', () => {
.replyOnce(200, reports.enrichData);
});
- it('should dispatch the `receiveDiffSuccess` action', (done) => {
+ it('should dispatch the `receiveDiffSuccess` action', () => {
const { diff, enrichData } = reports;
- testAction(
+
+ return testAction(
actions.fetchDiff,
{},
{ ...rootState, ...state },
@@ -124,7 +122,6 @@ describe('secret detection report actions', () => {
},
},
],
- done,
);
});
});
@@ -135,10 +132,10 @@ describe('secret detection report actions', () => {
mock.onGet(diffEndpoint).replyOnce(200, reports.diff);
});
- it('should dispatch the `receiveDiffSuccess` action with empty enrich data', (done) => {
+ it('should dispatch the `receiveDiffSuccess` action with empty enrich data', () => {
const { diff } = reports;
const enrichData = [];
- testAction(
+ return testAction(
actions.fetchDiff,
{},
{ ...rootState, ...state },
@@ -153,7 +150,6 @@ describe('secret detection report actions', () => {
},
},
],
- done,
);
});
});
@@ -167,14 +163,13 @@ describe('secret detection report actions', () => {
.replyOnce(404);
});
- it('should dispatch the `receiveDiffError` action', (done) => {
- testAction(
+ it('should dispatch the `receiveDiffError` action', () => {
+ return testAction(
actions.fetchDiff,
{},
{ ...rootState, ...state },
[],
[{ type: 'requestDiff' }, { type: 'receiveDiffError' }],
- done,
);
});
});
@@ -188,14 +183,13 @@ describe('secret detection report actions', () => {
.replyOnce(200, reports.enrichData);
});
- it('should dispatch the `receiveDiffError` action', (done) => {
- testAction(
+ it('should dispatch the `receiveDiffError` action', () => {
+ return testAction(
actions.fetchDiff,
{},
{ ...rootState, ...state },
[],
[{ type: 'requestDiff' }, { type: 'receiveDiffError' }],
- done,
);
});
});
diff --git a/spec/frontend/vuex_shared/modules/modal/actions_spec.js b/spec/frontend/vuex_shared/modules/modal/actions_spec.js
index c151049df2d..928ed7d0d5f 100644
--- a/spec/frontend/vuex_shared/modules/modal/actions_spec.js
+++ b/spec/frontend/vuex_shared/modules/modal/actions_spec.js
@@ -4,28 +4,28 @@ import * as types from '~/vuex_shared/modules/modal/mutation_types';
describe('Vuex ModalModule actions', () => {
describe('open', () => {
- it('works', (done) => {
+ it('works', () => {
const data = { id: 7 };
- testAction(actions.open, data, {}, [{ type: types.OPEN, payload: data }], [], done);
+ return testAction(actions.open, data, {}, [{ type: types.OPEN, payload: data }], []);
});
});
describe('close', () => {
- it('works', (done) => {
- testAction(actions.close, null, {}, [{ type: types.CLOSE }], [], done);
+ it('works', () => {
+ return testAction(actions.close, null, {}, [{ type: types.CLOSE }], []);
});
});
describe('show', () => {
- it('works', (done) => {
- testAction(actions.show, null, {}, [{ type: types.SHOW }], [], done);
+ it('works', () => {
+ return testAction(actions.show, null, {}, [{ type: types.SHOW }], []);
});
});
describe('hide', () => {
- it('works', (done) => {
- testAction(actions.hide, null, {}, [{ type: types.HIDE }], [], done);
+ it('works', () => {
+ return testAction(actions.hide, null, {}, [{ type: types.HIDE }], []);
});
});
});
diff --git a/spec/frontend/work_items/components/item_title_spec.js b/spec/frontend/work_items/components/item_title_spec.js
index 0f6e7091c59..0d85df25b4f 100644
--- a/spec/frontend/work_items/components/item_title_spec.js
+++ b/spec/frontend/work_items/components/item_title_spec.js
@@ -4,10 +4,10 @@ import ItemTitle from '~/work_items/components/item_title.vue';
jest.mock('lodash/escape', () => jest.fn((fn) => fn));
-const createComponent = ({ initialTitle = 'Sample title', disabled = false } = {}) =>
+const createComponent = ({ title = 'Sample title', disabled = false } = {}) =>
shallowMount(ItemTitle, {
propsData: {
- initialTitle,
+ title,
disabled,
},
});
diff --git a/spec/frontend/work_items/components/work_item_actions_spec.js b/spec/frontend/work_items/components/work_item_actions_spec.js
new file mode 100644
index 00000000000..d0e9cfee353
--- /dev/null
+++ b/spec/frontend/work_items/components/work_item_actions_spec.js
@@ -0,0 +1,103 @@
+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),
+ } = {}) => {
+ glModalDirective = jest.fn();
+ wrapper = shallowMount(WorkItemActions, {
+ apolloProvider: createMockApollo([[deleteWorkItem, deleteWorkItemHandler]]),
+ propsData: { workItemId: '123', canUpdate },
+ directives: {
+ glModal: {
+ bind(_, { value }) {
+ glModalDirective(value);
+ },
+ },
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('renders modal', () => {
+ createComponent();
+
+ expect(findModal().exists()).toBe(true);
+ expect(findModal().props('visible')).toBe(false);
+ });
+
+ it('shows confirm modal when clicking Delete work item', () => {
+ createComponent();
+
+ findDeleteButton().vm.$emit('click');
+
+ 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 () => {
+ 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();
+ });
+
+ it('does not render when canUpdate is false', () => {
+ createComponent({
+ canUpdate: 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
new file mode 100644
index 00000000000..9f35ccb853b
--- /dev/null
+++ b/spec/frontend/work_items/components/work_item_detail_modal_spec.js
@@ -0,0 +1,58 @@
+import { GlModal } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+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';
+
+describe('WorkItemDetailModal component', () => {
+ let wrapper;
+
+ Vue.use(VueApollo);
+
+ const findModal = () => wrapper.findComponent(GlModal);
+ const findWorkItemActions = () => wrapper.findComponent(WorkItemActions);
+ const findWorkItemDetail = () => wrapper.findComponent(WorkItemDetail);
+
+ const createComponent = ({ visible = true, workItemId = '1', canUpdate = false } = {}) => {
+ wrapper = shallowMount(WorkItemDetailModal, {
+ propsData: { visible, workItemId, canUpdate },
+ stubs: {
+ GlModal,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe.each([true, false])('when visible=%s', (visible) => {
+ it(`${visible ? 'renders' : 'does not render'} modal`, () => {
+ createComponent({ visible });
+
+ expect(findModal().props('visible')).toBe(visible);
+ });
+ });
+
+ it('renders heading', () => {
+ createComponent();
+
+ expect(wrapper.find('h2').text()).toBe('Work Item');
+ });
+
+ it('renders WorkItemDetail', () => {
+ createComponent();
+
+ expect(findWorkItemDetail().props()).toEqual({ workItemId: '1' });
+ });
+
+ it('shows work item actions', () => {
+ createComponent({
+ canUpdate: true,
+ });
+
+ expect(findWorkItemActions().exists()).toBe(true);
+ });
+});
diff --git a/spec/frontend/work_items/components/work_item_detail_spec.js b/spec/frontend/work_items/components/work_item_detail_spec.js
deleted file mode 100644
index 305f43ad8ba..00000000000
--- a/spec/frontend/work_items/components/work_item_detail_spec.js
+++ /dev/null
@@ -1,40 +0,0 @@
-import { GlModal } 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 WorkItemTitle from '~/work_items/components/item_title.vue';
-import WorkItemDetailModal from '~/work_items/components/work_item_detail_modal.vue';
-import { resolvers } from '~/work_items/graphql/resolvers';
-
-describe('WorkItemDetailModal component', () => {
- let wrapper;
-
- Vue.use(VueApollo);
-
- const findModal = () => wrapper.findComponent(GlModal);
- const findWorkItemTitle = () => wrapper.findComponent(WorkItemTitle);
-
- const createComponent = () => {
- wrapper = shallowMount(WorkItemDetailModal, {
- apolloProvider: createMockApollo([], resolvers),
- propsData: { visible: true },
- });
- };
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- it('renders modal', () => {
- createComponent();
-
- expect(findModal().props()).toMatchObject({ visible: true });
- });
-
- it('renders work item title', () => {
- createComponent();
-
- expect(findWorkItemTitle().exists()).toBe(true);
- });
-});
diff --git a/spec/frontend/work_items/components/work_item_title_spec.js b/spec/frontend/work_items/components/work_item_title_spec.js
new file mode 100644
index 00000000000..9b1ef2d14e4
--- /dev/null
+++ b/spec/frontend/work_items/components/work_item_title_spec.js
@@ -0,0 +1,117 @@
+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 { mockTracking } from 'helpers/tracking_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import ItemTitle from '~/work_items/components/item_title.vue';
+import WorkItemTitle from '~/work_items/components/work_item_title.vue';
+import { i18n } from '~/work_items/constants';
+import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
+import { updateWorkItemMutationResponse, workItemQueryResponse } from '../mock_data';
+
+describe('WorkItemTitle component', () => {
+ let wrapper;
+
+ Vue.use(VueApollo);
+
+ const mutationSuccessHandler = jest.fn().mockResolvedValue(updateWorkItemMutationResponse);
+
+ const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
+ const findItemTitle = () => wrapper.findComponent(ItemTitle);
+
+ const createComponent = ({ loading = false, 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,
+ },
+ });
+ };
+
+ afterEach(() => {
+ 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', () => {
+ expect(findItemTitle().props('title')).toBe(workItemQueryResponse.data.workItem.title);
+ });
+ });
+
+ describe('when updating the title', () => {
+ it('calls a mutation', () => {
+ const title = 'new title!';
+
+ createComponent();
+
+ findItemTitle().vm.$emit('title-changed', title);
+
+ expect(mutationSuccessHandler).toHaveBeenCalledWith({
+ input: {
+ id: workItemQueryResponse.data.workItem.id,
+ title,
+ },
+ });
+ });
+
+ it('does not call a mutation when the title has not changed', () => {
+ createComponent();
+
+ findItemTitle().vm.$emit('title-changed', workItemQueryResponse.data.workItem.title);
+
+ expect(mutationSuccessHandler).not.toHaveBeenCalled();
+ });
+
+ it('emits an error message when the mutation was unsuccessful', async () => {
+ createComponent({ mutationHandler: jest.fn().mockRejectedValue('Error!') });
+
+ findItemTitle().vm.$emit('title-changed', 'new title');
+ await waitForPromises();
+
+ expect(wrapper.emitted('error')).toEqual([[i18n.updateError]]);
+ });
+
+ it('tracks editing the title', async () => {
+ const trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
+
+ createComponent();
+
+ findItemTitle().vm.$emit('title-changed', 'new title');
+ await waitForPromises();
+
+ expect(trackingSpy).toHaveBeenCalledWith('workItems:show', 'updated_title', {
+ category: 'workItems:show',
+ label: 'item_title',
+ property: 'type_Task',
+ });
+ });
+ });
+});
diff --git a/spec/frontend/work_items/mock_data.js b/spec/frontend/work_items/mock_data.js
index 832795fc4ac..722e1708c15 100644
--- a/spec/frontend/work_items/mock_data.js
+++ b/spec/frontend/work_items/mock_data.js
@@ -1,21 +1,14 @@
export const workItemQueryResponse = {
- workItem: {
- __typename: 'WorkItem',
- id: '1',
- title: 'Test',
- workItemType: {
- __typename: 'WorkItemType',
- id: 'work-item-type-1',
- },
- widgets: {
- __typename: 'LocalWorkItemWidgetConnection',
- nodes: [
- {
- __typename: 'LocalTitleWidget',
- type: 'TITLE',
- contentText: 'Test',
- },
- ],
+ data: {
+ workItem: {
+ __typename: 'WorkItem',
+ id: 'gid://gitlab/WorkItem/1',
+ title: 'Test',
+ workItemType: {
+ __typename: 'WorkItemType',
+ id: 'gid://gitlab/WorkItems::Type/5',
+ name: 'Task',
+ },
},
},
};
@@ -23,25 +16,15 @@ export const workItemQueryResponse = {
export const updateWorkItemMutationResponse = {
data: {
workItemUpdate: {
- __typename: 'LocalUpdateWorkItemPayload',
+ __typename: 'WorkItemUpdatePayload',
workItem: {
- __typename: 'LocalWorkItem',
- id: '1',
+ __typename: 'WorkItem',
+ id: 'gid://gitlab/WorkItem/1',
title: 'Updated title',
workItemType: {
__typename: 'WorkItemType',
- id: 'work-item-type-1',
- },
- widgets: {
- __typename: 'LocalWorkItemWidgetConnection',
- nodes: [
- {
- __typename: 'LocalTitleWidget',
- type: 'TITLE',
- enabled: true,
- contentText: 'Updated title',
- },
- ],
+ id: 'gid://gitlab/WorkItems::Type/5',
+ name: 'Task',
},
},
},
@@ -51,11 +34,11 @@ export const updateWorkItemMutationResponse = {
export const projectWorkItemTypesQueryResponse = {
data: {
workspace: {
- id: '1',
+ id: 'gid://gitlab/WorkItem/1',
workItemTypes: {
nodes: [
- { id: 'work-item-1', name: 'Issue' },
- { id: 'work-item-2', name: 'Incident' },
+ { id: 'gid://gitlab/WorkItems::Type/1', name: 'Issue' },
+ { id: 'gid://gitlab/WorkItems::Type/2', name: 'Incident' },
],
},
},
@@ -68,13 +51,53 @@ export const createWorkItemMutationResponse = {
__typename: 'WorkItemCreatePayload',
workItem: {
__typename: 'WorkItem',
- id: '1',
+ id: 'gid://gitlab/WorkItem/1',
title: 'Updated title',
workItemType: {
__typename: 'WorkItemType',
- id: 'work-item-type-1',
+ id: 'gid://gitlab/WorkItems::Type/5',
+ name: 'Task',
},
},
},
},
};
+
+export const createWorkItemFromTaskMutationResponse = {
+ data: {
+ workItemCreateFromTask: {
+ __typename: 'WorkItemCreateFromTaskPayload',
+ errors: [],
+ workItem: {
+ descriptionHtml: '<p>New description</p>',
+ id: 'gid://gitlab/WorkItem/13',
+ __typename: 'WorkItem',
+ },
+ },
+ },
+};
+
+export const deleteWorkItemResponse = {
+ data: { workItemDelete: { errors: [], __typename: 'WorkItemDeletePayload' } },
+};
+
+export const deleteWorkItemFailureResponse = {
+ data: { workItemDelete: null },
+ errors: [
+ {
+ message:
+ "The resource that you are attempting to access does not exist or you don't have permission to perform this action",
+ locations: [{ line: 2, column: 3 }],
+ path: ['workItemDelete'],
+ },
+ ],
+};
+
+export const workItemTitleSubscriptionResponse = {
+ data: {
+ issuableTitleUpdated: {
+ id: 'gid://gitlab/WorkItem/1',
+ title: 'new title',
+ },
+ },
+};
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 185b05c5191..fb1f1d56356 100644
--- a/spec/frontend/work_items/pages/create_work_item_spec.js
+++ b/spec/frontend/work_items/pages/create_work_item_spec.js
@@ -1,15 +1,19 @@
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
-import { GlAlert, GlDropdown, GlDropdownItem } from '@gitlab/ui';
+import { GlAlert, GlFormSelect } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import CreateWorkItem from '~/work_items/pages/create_work_item.vue';
import ItemTitle from '~/work_items/components/item_title.vue';
-import { resolvers } from '~/work_items/graphql/resolvers';
import projectWorkItemTypesQuery from '~/work_items/graphql/project_work_item_types.query.graphql';
import createWorkItemMutation from '~/work_items/graphql/create_work_item.mutation.graphql';
-import { projectWorkItemTypesQueryResponse, createWorkItemMutationResponse } from '../mock_data';
+import createWorkItemFromTaskMutation from '~/work_items/graphql/create_work_item_from_task.mutation.graphql';
+import {
+ projectWorkItemTypesQueryResponse,
+ createWorkItemMutationResponse,
+ createWorkItemFromTaskMutationResponse,
+} from '../mock_data';
jest.mock('~/lib/utils/uuids', () => ({ uuids: () => ['testuuid'] }));
@@ -20,12 +24,15 @@ describe('Create work item component', () => {
let fakeApollo;
const querySuccessHandler = jest.fn().mockResolvedValue(projectWorkItemTypesQueryResponse);
- const mutationSuccessHandler = jest.fn().mockResolvedValue(createWorkItemMutationResponse);
+ const createWorkItemSuccessHandler = jest.fn().mockResolvedValue(createWorkItemMutationResponse);
+ const createWorkItemFromTaskSuccessHandler = jest
+ .fn()
+ .mockResolvedValue(createWorkItemFromTaskMutationResponse);
+ const errorHandler = jest.fn().mockRejectedValue('Houston, we have a problem');
const findAlert = () => wrapper.findComponent(GlAlert);
const findTitleInput = () => wrapper.findComponent(ItemTitle);
- const findDropdown = () => wrapper.findComponent(GlDropdown);
- const findDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
+ const findSelect = () => wrapper.findComponent(GlFormSelect);
const findCreateButton = () => wrapper.find('[data-testid="create-button"]');
const findCancelButton = () => wrapper.find('[data-testid="cancel-button"]');
@@ -36,15 +43,13 @@ describe('Create work item component', () => {
data = {},
props = {},
queryHandler = querySuccessHandler,
- mutationHandler = mutationSuccessHandler,
+ mutationHandler = createWorkItemSuccessHandler,
} = {}) => {
- fakeApollo = createMockApollo(
- [
- [projectWorkItemTypesQuery, queryHandler],
- [createWorkItemMutation, mutationHandler],
- ],
- resolvers,
- );
+ fakeApollo = createMockApollo([
+ [projectWorkItemTypesQuery, queryHandler],
+ [createWorkItemMutation, mutationHandler],
+ [createWorkItemFromTaskMutation, mutationHandler],
+ ]);
wrapper = shallowMount(CreateWorkItem, {
apolloProvider: fakeApollo,
data() {
@@ -123,6 +128,7 @@ describe('Create work item component', () => {
props: {
isModal: true,
},
+ mutationHandler: createWorkItemFromTaskSuccessHandler,
});
});
@@ -133,14 +139,12 @@ describe('Create work item component', () => {
});
it('emits `onCreate` on successful mutation', async () => {
- const mockTitle = 'Test title';
findTitleInput().vm.$emit('title-input', 'Test title');
wrapper.find('form').trigger('submit');
await waitForPromises();
- const expected = { id: '1', title: mockTitle };
- expect(wrapper.emitted('onCreate')).toEqual([[expected]]);
+ expect(wrapper.emitted('onCreate')).toEqual([['<p>New description</p>']]);
});
it('does not right margin for create button', () => {
@@ -177,16 +181,14 @@ describe('Create work item component', () => {
});
it('displays a list of work item types', () => {
- expect(findDropdownItems()).toHaveLength(2);
- expect(findDropdownItems().at(0).text()).toContain('Issue');
+ expect(findSelect().attributes('options').split(',')).toHaveLength(3);
});
it('selects a work item type on click', async () => {
- expect(findDropdown().props('text')).toBe('Type');
- findDropdownItems().at(0).vm.$emit('click');
+ const mockId = 'work-item-1';
+ findSelect().vm.$emit('input', mockId);
await nextTick();
-
- expect(findDropdown().props('text')).toBe('Issue');
+ expect(findSelect().attributes('value')).toBe(mockId);
});
});
@@ -206,21 +208,36 @@ describe('Create work item component', () => {
createComponent({
props: { initialTitle },
});
- expect(findTitleInput().props('initialTitle')).toBe(initialTitle);
+ expect(findTitleInput().props('title')).toBe(initialTitle);
});
describe('when title input field has a text', () => {
- beforeEach(() => {
+ beforeEach(async () => {
const mockTitle = 'Test title';
createComponent();
+ await waitForPromises();
findTitleInput().vm.$emit('title-input', mockTitle);
});
- it('renders a non-disabled Create button', () => {
+ it('renders a disabled Create button', () => {
+ expect(findCreateButton().props('disabled')).toBe(true);
+ });
+
+ it('renders a non-disabled Create button when work item type is selected', async () => {
+ findSelect().vm.$emit('input', 'work-item-1');
+ await nextTick();
expect(findCreateButton().props('disabled')).toBe(false);
});
+ });
+
+ it('shows an alert on mutation error', async () => {
+ createComponent({ mutationHandler: errorHandler });
+ await waitForPromises();
+ findTitleInput().vm.$emit('title-input', 'some title');
+ findSelect().vm.$emit('input', 'work-item-1');
+ wrapper.find('form').trigger('submit');
+ await waitForPromises();
- // TODO: write a proper test here when we have a backend implementation
- it.todo('shows an alert on mutation error');
+ expect(findAlert().text()).toBe(CreateWorkItem.createErrorText);
});
});
diff --git a/spec/frontend/work_items/pages/work_item_detail_spec.js b/spec/frontend/work_items/pages/work_item_detail_spec.js
new file mode 100644
index 00000000000..1eb6c0145e7
--- /dev/null
+++ b/spec/frontend/work_items/pages/work_item_detail_spec.js
@@ -0,0 +1,99 @@
+import { GlAlert } 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 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';
+import workItemTitleSubscription from '~/work_items/graphql/work_item_title.subscription.graphql';
+import { workItemTitleSubscriptionResponse, workItemQueryResponse } from '../mock_data';
+
+describe('WorkItemDetail component', () => {
+ let wrapper;
+
+ Vue.use(VueApollo);
+
+ const successHandler = jest.fn().mockResolvedValue(workItemQueryResponse);
+ const initialSubscriptionHandler = jest.fn().mockResolvedValue(workItemTitleSubscriptionResponse);
+
+ const findAlert = () => wrapper.findComponent(GlAlert);
+ const findWorkItemTitle = () => wrapper.findComponent(WorkItemTitle);
+
+ const createComponent = ({
+ workItemId = workItemQueryResponse.data.workItem.id,
+ handler = successHandler,
+ subscriptionHandler = initialSubscriptionHandler,
+ } = {}) => {
+ wrapper = shallowMount(WorkItemDetail, {
+ apolloProvider: createMockApollo([
+ [workItemQuery, handler],
+ [workItemTitleSubscription, subscriptionHandler],
+ ]),
+ propsData: { workItemId },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('when there is no `workItemId` prop', () => {
+ beforeEach(() => {
+ createComponent({ workItemId: null });
+ });
+
+ it('skips the work item query', () => {
+ expect(successHandler).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('when loading', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders WorkItemTitle in loading state', () => {
+ expect(findWorkItemTitle().props('loading')).toBe(true);
+ });
+ });
+
+ describe('when loaded', () => {
+ beforeEach(() => {
+ createComponent();
+ return waitForPromises();
+ });
+
+ it('does not render WorkItemTitle in loading state', () => {
+ expect(findWorkItemTitle().props('loading')).toBe(false);
+ });
+ });
+
+ it('shows an error message when the work item query was unsuccessful', async () => {
+ const errorHandler = jest.fn().mockRejectedValue('Oops');
+ createComponent({ handler: errorHandler });
+ await waitForPromises();
+
+ expect(errorHandler).toHaveBeenCalled();
+ expect(findAlert().text()).toBe(i18n.fetchError);
+ });
+
+ it('shows an error message when WorkItemTitle emits an `error` event', async () => {
+ createComponent();
+
+ findWorkItemTitle().vm.$emit('error', i18n.updateError);
+ await waitForPromises();
+
+ expect(findAlert().text()).toBe(i18n.updateError);
+ });
+
+ it('calls the subscription', () => {
+ createComponent();
+
+ expect(initialSubscriptionHandler).toHaveBeenCalledWith({
+ issuableId: workItemQueryResponse.data.workItem.id,
+ });
+ });
+});
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 728495e0e23..2803724b9af 100644
--- a/spec/frontend/work_items/pages/work_item_root_spec.js
+++ b/spec/frontend/work_items/pages/work_item_root_spec.js
@@ -1,108 +1,31 @@
-import Vue from 'vue';
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 { mockTracking, unmockTracking } from 'helpers/tracking_helper';
-import workItemQuery from '~/work_items/graphql/work_item.query.graphql';
-import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
+import WorkItemDetail from '~/work_items/components/work_item_detail.vue';
import WorkItemsRoot from '~/work_items/pages/work_item_root.vue';
-import ItemTitle from '~/work_items/components/item_title.vue';
-import { resolvers } from '~/work_items/graphql/resolvers';
-import { workItemQueryResponse, updateWorkItemMutationResponse } from '../mock_data';
Vue.use(VueApollo);
-const WORK_ITEM_ID = '1';
-const WORK_ITEM_GID = `gid://gitlab/WorkItem/${WORK_ITEM_ID}`;
-
describe('Work items root component', () => {
- const mockUpdatedTitle = 'Updated title';
let wrapper;
- let fakeApollo;
-
- const findTitle = () => wrapper.findComponent(ItemTitle);
- const createComponent = ({ queryResponse = workItemQueryResponse } = {}) => {
- fakeApollo = createMockApollo(
- [[updateWorkItemMutation, jest.fn().mockResolvedValue(updateWorkItemMutationResponse)]],
- resolvers,
- {
- possibleTypes: {
- LocalWorkItemWidget: ['LocalTitleWidget'],
- },
- },
- );
- fakeApollo.clients.defaultClient.cache.writeQuery({
- query: workItemQuery,
- variables: {
- id: WORK_ITEM_GID,
- },
- data: queryResponse,
- });
+ const findWorkItemDetail = () => wrapper.findComponent(WorkItemDetail);
+ const createComponent = () => {
wrapper = shallowMount(WorkItemsRoot, {
propsData: {
- id: WORK_ITEM_ID,
+ id: '1',
},
- apolloProvider: fakeApollo,
});
};
afterEach(() => {
wrapper.destroy();
- fakeApollo = null;
});
- it('renders the title', () => {
+ it('renders WorkItemDetail', () => {
createComponent();
- expect(findTitle().exists()).toBe(true);
- expect(findTitle().props('initialTitle')).toBe('Test');
- });
-
- it('updates the title when it is edited', async () => {
- createComponent();
- jest.spyOn(wrapper.vm.$apollo, 'mutate');
-
- await findTitle().vm.$emit('title-changed', mockUpdatedTitle);
-
- expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({
- mutation: updateWorkItemMutation,
- variables: {
- input: {
- id: WORK_ITEM_GID,
- title: mockUpdatedTitle,
- },
- },
- });
- });
-
- describe('tracking', () => {
- let trackingSpy;
-
- beforeEach(() => {
- trackingSpy = mockTracking('_category_', undefined, jest.spyOn);
-
- createComponent();
- });
-
- afterEach(() => {
- unmockTracking();
- });
-
- it('tracks item title updates', async () => {
- await findTitle().vm.$emit('title-changed', mockUpdatedTitle);
-
- await waitForPromises();
-
- expect(trackingSpy).toHaveBeenCalledTimes(1);
- expect(trackingSpy).toHaveBeenCalledWith('workItems:show', undefined, {
- action: 'updated_title',
- category: 'workItems:show',
- label: 'item_title',
- property: '[type_work_item]',
- });
- });
+ expect(findWorkItemDetail().props()).toEqual({ workItemId: 'gid://gitlab/WorkItem/1' });
});
});
diff --git a/spec/frontend/work_items/router_spec.js b/spec/frontend/work_items/router_spec.js
index 8c9054920a8..7e68c5e4f0e 100644
--- a/spec/frontend/work_items/router_spec.js
+++ b/spec/frontend/work_items/router_spec.js
@@ -37,7 +37,7 @@ describe('Work items router', () => {
it('renders work item on `/1` route', async () => {
await createComponent('/1');
- expect(wrapper.find(WorkItemsRoot).exists()).toBe(true);
+ expect(wrapper.findComponent(WorkItemsRoot).exists()).toBe(true);
});
it('renders create work item page on `/new` route', async () => {