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-10-20 12:40:42 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2022-10-20 12:40:42 +0300
commitee664acb356f8123f4f6b00b73c1e1cf0866c7fb (patch)
treef8479f94a28f66654c6a4f6fb99bad6b4e86a40e /spec/frontend
parent62f7d5c5b69180e82ae8196b7b429eeffc8e7b4f (diff)
Add latest changes from gitlab-org/gitlab@15-5-stable-eev15.5.0-rc42
Diffstat (limited to 'spec/frontend')
-rw-r--r--spec/frontend/__helpers__/class_spec_helper.js10
-rw-r--r--spec/frontend/__helpers__/class_spec_helper_spec.js26
-rw-r--r--spec/frontend/__helpers__/dom_shims/index.js1
-rw-r--r--spec/frontend/__helpers__/dom_shims/text_encoder.js4
-rw-r--r--spec/frontend/__helpers__/graphql_transformer.js8
-rw-r--r--spec/frontend/__helpers__/shared_test_setup.js15
-rw-r--r--spec/frontend/__helpers__/stub_component.js2
-rw-r--r--spec/frontend/__helpers__/vue_mount_component_helper.js34
-rw-r--r--spec/frontend/__helpers__/vue_test_utils_helper_spec.js3
-rw-r--r--spec/frontend/__mocks__/monaco-editor/index.js2
-rw-r--r--spec/frontend/__mocks__/monaco-yaml/index.js4
-rw-r--r--spec/frontend/access_tokens/components/access_token_table_app_spec.js147
-rw-r--r--spec/frontend/access_tokens/components/new_access_token_app_spec.js19
-rw-r--r--spec/frontend/access_tokens/index_spec.js73
-rw-r--r--spec/frontend/admin/broadcast_messages/components/base_spec.js112
-rw-r--r--spec/frontend/admin/broadcast_messages/components/messages_table_spec.js51
-rw-r--r--spec/frontend/admin/broadcast_messages/mock_data.js17
-rw-r--r--spec/frontend/admin/deploy_keys/components/table_spec.js4
-rw-r--r--spec/frontend/admin/users/components/users_table_spec.js4
-rw-r--r--spec/frontend/alert_management/components/alert_management_table_spec.js13
-rw-r--r--spec/frontend/alerts_settings/components/alerts_settings_form_spec.js59
-rw-r--r--spec/frontend/alerts_settings/components/alerts_settings_wrapper_spec.js26
-rw-r--r--spec/frontend/api/projects_api_spec.js23
-rw-r--r--spec/frontend/awards_handler_spec.js4
-rw-r--r--spec/frontend/badges/components/badge_form_spec.js194
-rw-r--r--spec/frontend/badges/components/badge_list_row_spec.js137
-rw-r--r--spec/frontend/badges/components/badge_list_spec.js125
-rw-r--r--spec/frontend/badges/components/badge_spec.js150
-rw-r--r--spec/frontend/batch_comments/components/drafts_count_spec.js30
-rw-r--r--spec/frontend/batch_comments/components/preview_item_spec.js36
-rw-r--r--spec/frontend/batch_comments/components/publish_button_spec.js30
-rw-r--r--spec/frontend/behaviors/bind_in_out_spec.js9
-rw-r--r--spec/frontend/blame/blame_redirect_spec.js70
-rw-r--r--spec/frontend/blob/3d_viewer/mesh_object_spec.js2
-rw-r--r--spec/frontend/blob/blob_blame_link_spec.js6
-rw-r--r--spec/frontend/blob/components/blob_content_spec.js4
-rw-r--r--spec/frontend/blob/components/table_contents_spec.js5
-rw-r--r--spec/frontend/boards/board_card_inner_spec.js13
-rw-r--r--spec/frontend/boards/components/board_filtered_search_spec.js8
-rw-r--r--spec/frontend/boards/stores/actions_spec.js254
-rw-r--r--spec/frontend/captcha/init_recaptcha_script_spec.js3
-rw-r--r--spec/frontend/ci_variable_list/components/ci_admin_variables_spec.js16
-rw-r--r--spec/frontend/ci_variable_list/components/ci_group_variables_spec.js16
-rw-r--r--spec/frontend/ci_variable_list/components/ci_project_variables_spec.js20
-rw-r--r--spec/frontend/ci_variable_list/components/legacy_ci_variable_settings_spec.js14
-rw-r--r--spec/frontend/ci_variable_list/mocks.js6
-rw-r--r--spec/frontend/ci_variable_list/store/actions_spec.js12
-rw-r--r--spec/frontend/clusters_list/store/actions_spec.js4
-rw-r--r--spec/frontend/code_navigation/utils/index_spec.js8
-rw-r--r--spec/frontend/commit/commit_box_pipeline_mini_graph_spec.js6
-rw-r--r--spec/frontend/commit/commit_pipeline_status_component_spec.js4
-rw-r--r--spec/frontend/commit/components/commit_box_pipeline_status_spec.js8
-rw-r--r--spec/frontend/content_editor/components/content_editor_spec.js31
-rw-r--r--spec/frontend/content_editor/components/editor_state_observer_spec.js11
-rw-r--r--spec/frontend/content_editor/components/suggestions_dropdown_spec.js286
-rw-r--r--spec/frontend/content_editor/components/wrappers/label_spec.js36
-rw-r--r--spec/frontend/content_editor/extensions/heading_spec.js54
-rw-r--r--spec/frontend/content_editor/markdown_processing_spec_helper.js12
-rw-r--r--spec/frontend/content_editor/services/markdown_serializer_spec.js18
-rw-r--r--spec/frontend/content_editor/services/track_input_rules_and_shortcuts_spec.js2
-rw-r--r--spec/frontend/contributors/component/contributors_spec.js5
-rw-r--r--spec/frontend/contributors/store/actions_spec.js4
-rw-r--r--spec/frontend/crm/contacts_root_spec.js14
-rw-r--r--spec/frontend/crm/organizations_root_spec.js2
-rw-r--r--spec/frontend/cycle_analytics/value_stream_metrics_spec.js4
-rw-r--r--spec/frontend/deploy_freeze/components/deploy_freeze_modal_spec.js11
-rw-r--r--spec/frontend/deploy_freeze/components/deploy_freeze_settings_spec.js2
-rw-r--r--spec/frontend/deploy_freeze/components/deploy_freeze_table_spec.js3
-rw-r--r--spec/frontend/deploy_freeze/helpers.js9
-rw-r--r--spec/frontend/deploy_freeze/store/actions_spec.js3
-rw-r--r--spec/frontend/deploy_freeze/store/mutations_spec.js13
-rw-r--r--spec/frontend/deploy_tokens/components/new_deploy_token_spec.js103
-rw-r--r--spec/frontend/design_management/components/delete_button_spec.js6
-rw-r--r--spec/frontend/design_management/components/design_notes/design_reply_form_spec.js69
-rw-r--r--spec/frontend/design_management/components/design_overlay_spec.js8
-rw-r--r--spec/frontend/design_management/pages/design/index_spec.js10
-rw-r--r--spec/frontend/design_management/pages/index_spec.js4
-rw-r--r--spec/frontend/design_management/utils/cache_update_spec.js8
-rw-r--r--spec/frontend/diffs/components/app_spec.js24
-rw-r--r--spec/frontend/diffs/components/commit_item_spec.js2
-rw-r--r--spec/frontend/diffs/components/diff_content_spec.js2
-rw-r--r--spec/frontend/diffs/components/diff_row_spec.js2
-rw-r--r--spec/frontend/diffs/components/diff_row_utils_spec.js6
-rw-r--r--spec/frontend/diffs/components/diff_view_spec.js16
-rw-r--r--spec/frontend/diffs/components/file_row_stats_spec.js20
-rw-r--r--spec/frontend/diffs/mock_data/diff_code_quality.js6
-rw-r--r--spec/frontend/diffs/store/actions_spec.js37
-rw-r--r--spec/frontend/editor/schema/ci/ci_schema_spec.js47
-rw-r--r--spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/include/empty.yml3
-rw-r--r--spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/include/invalid_variable.yml3
-rw-r--r--spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/include/leading_slash.yml3
-rw-r--r--spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/include/no_slash.yml3
-rw-r--r--spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/include/tailing_slash.yml3
-rw-r--r--spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/include/empty.yml5
-rw-r--r--spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/include/invalid_variable.yml5
-rw-r--r--spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/include/leading_slash.yml5
-rw-r--r--spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/include/no_slash.yml5
-rw-r--r--spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/include/tailing_slash.yml5
-rw-r--r--spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/minimal/empty.yml2
-rw-r--r--spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/minimal/invalid_variable.yml2
-rw-r--r--spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/minimal/leading_slash.yml2
-rw-r--r--spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/minimal/no_slash.yml2
-rw-r--r--spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/minimal/tailing_slash.yml2
-rw-r--r--spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/project/empty.yml3
-rw-r--r--spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/project/invalid_variable.yml3
-rw-r--r--spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/project/leading_slash.yml3
-rw-r--r--spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/project/no_slash.yml3
-rw-r--r--spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/project/tailing_slash.yml3
-rw-r--r--spec/frontend/editor/schema/ci/yaml_tests/negative_tests/variables.yml5
-rw-r--r--spec/frontend/editor/schema/ci/yaml_tests/positive_tests/project_path.yml101
-rw-r--r--spec/frontend/editor/schema/ci/yaml_tests/positive_tests/rules.yml2
-rw-r--r--spec/frontend/editor/schema/ci/yaml_tests/positive_tests/variables.yml8
-rw-r--r--spec/frontend/editor/source_editor_ci_schema_ext_spec.js12
-rw-r--r--spec/frontend/editor/source_editor_instance_spec.js2
-rw-r--r--spec/frontend/editor/source_editor_markdown_livepreview_ext_spec.js4
-rw-r--r--spec/frontend/environments/delete_environment_modal_spec.js6
-rw-r--r--spec/frontend/environments/edit_environment_spec.js4
-rw-r--r--spec/frontend/environments/empty_state_spec.js52
-rw-r--r--spec/frontend/environments/enable_review_app_modal_spec.js23
-rw-r--r--spec/frontend/environments/environment_external_url_spec.js31
-rw-r--r--spec/frontend/environments/environment_folder_spec.js22
-rw-r--r--spec/frontend/environments/environments_app_spec.js53
-rw-r--r--spec/frontend/environments/environments_detail_header_spec.js21
-rw-r--r--spec/frontend/environments/graphql/resolvers_spec.js26
-rw-r--r--spec/frontend/environments/new_environment_spec.js4
-rw-r--r--spec/frontend/error_tracking/components/error_details_spec.js8
-rw-r--r--spec/frontend/error_tracking/store/actions_spec.js6
-rw-r--r--spec/frontend/error_tracking/store/details/actions_spec.js6
-rw-r--r--spec/frontend/error_tracking/store/list/actions_spec.js4
-rw-r--r--spec/frontend/feature_highlight/feature_highlight_helper_spec.js4
-rw-r--r--spec/frontend/fixtures/namespaces.rb47
-rw-r--r--spec/frontend/fixtures/pipeline_schedules.rb72
-rw-r--r--spec/frontend/flash_spec.js7
-rw-r--r--spec/frontend/grafana_integration/components/grafana_integration_spec.js6
-rw-r--r--spec/frontend/groups/components/app_spec.js88
-rw-r--r--spec/frontend/groups/components/group_item_spec.js11
-rw-r--r--spec/frontend/groups/components/groups_spec.js9
-rw-r--r--spec/frontend/groups/components/new_top_level_group_alert_spec.js75
-rw-r--r--spec/frontend/groups/components/overview_tabs_spec.js162
-rw-r--r--spec/frontend/groups/components/transfer_group_form_spec.js2
-rw-r--r--spec/frontend/groups/store/groups_store_spec.js10
-rw-r--r--spec/frontend/header_search/components/app_spec.js26
-rw-r--r--spec/frontend/ide/components/commit_sidebar/actions_spec.js65
-rw-r--r--spec/frontend/ide/components/commit_sidebar/list_item_spec.js125
-rw-r--r--spec/frontend/ide/components/commit_sidebar/message_field_spec.js128
-rw-r--r--spec/frontend/ide/components/commit_sidebar/radio_group_spec.js135
-rw-r--r--spec/frontend/ide/components/file_row_extra_spec.js140
-rw-r--r--spec/frontend/ide/components/file_templates/bar_spec.js71
-rw-r--r--spec/frontend/ide/components/jobs/__snapshots__/stage_spec.js.snap60
-rw-r--r--spec/frontend/ide/components/jobs/detail/description_spec.js35
-rw-r--r--spec/frontend/ide/components/jobs/detail_spec.js162
-rw-r--r--spec/frontend/ide/components/jobs/item_spec.js30
-rw-r--r--spec/frontend/ide/components/jobs/stage_spec.js9
-rw-r--r--spec/frontend/ide/components/new_dropdown/button_spec.js65
-rw-r--r--spec/frontend/ide/components/new_dropdown/modal_spec.js10
-rw-r--r--spec/frontend/ide/components/new_dropdown/upload_spec.js71
-rw-r--r--spec/frontend/ide/components/shared/tokened_input_spec.js135
-rw-r--r--spec/frontend/ide/components/terminal/terminal_spec.js2
-rw-r--r--spec/frontend/ide/init_gitlab_web_ide_spec.js6
-rw-r--r--spec/frontend/ide/stores/actions/merge_request_spec.js8
-rw-r--r--spec/frontend/ide/stores/actions/project_spec.js6
-rw-r--r--spec/frontend/ide/stores/actions_spec.js16
-rw-r--r--spec/frontend/ide/stores/modules/commit/mutations_spec.js2
-rw-r--r--spec/frontend/ide/stores/modules/terminal/actions/session_controls_spec.js6
-rw-r--r--spec/frontend/ide/stores/modules/terminal/actions/session_status_spec.js4
-rw-r--r--spec/frontend/ide/stores/mutations/tree_spec.js4
-rw-r--r--spec/frontend/ide/stores/mutations_spec.js8
-rw-r--r--spec/frontend/ide/utils_spec.js4
-rw-r--r--spec/frontend/import_entities/import_groups/components/import_table_spec.js16
-rw-r--r--spec/frontend/import_entities/import_groups/services/status_poller_spec.js4
-rw-r--r--spec/frontend/import_entities/import_projects/components/advanced_settings_spec.js60
-rw-r--r--spec/frontend/import_entities/import_projects/components/import_projects_table_spec.js22
-rw-r--r--spec/frontend/import_entities/import_projects/components/provider_repo_table_row_spec.js22
-rw-r--r--spec/frontend/import_entities/import_projects/store/actions_spec.js30
-rw-r--r--spec/frontend/integrations/edit/components/integration_form_spec.js45
-rw-r--r--spec/frontend/issuable/bulk_update_sidebar/components/status_dropdown_spec.js (renamed from spec/frontend/issuable/bulk_update_sidebar/components/status_select_spec.js)14
-rw-r--r--spec/frontend/issuable/bulk_update_sidebar/components/subscriptions_dropdown_spec.js76
-rw-r--r--spec/frontend/issuable/related_issues/components/related_issues_root_spec.js8
-rw-r--r--spec/frontend/issues/show/components/edited_spec.js16
-rw-r--r--spec/frontend/issues/show/components/fields/description_spec.js59
-rw-r--r--spec/frontend/issues/show/components/form_spec.js76
-rw-r--r--spec/frontend/issues/show/components/incidents/incident_tabs_spec.js19
-rw-r--r--spec/frontend/issues/show/components/incidents/timeline_events_form_spec.js24
-rw-r--r--spec/frontend/jira_connect/branches/components/new_branch_form_spec.js2
-rw-r--r--spec/frontend/jira_connect/subscriptions/pkce_spec.js4
-rw-r--r--spec/frontend/jobs/components/table/job_table_app_spec.js4
-rw-r--r--spec/frontend/labels/components/promote_label_modal_spec.js116
-rw-r--r--spec/frontend/lib/dompurify_spec.js4
-rw-r--r--spec/frontend/lib/utils/autosave_spec.js57
-rw-r--r--spec/frontend/lib/utils/datetime/date_format_utility_spec.js19
-rw-r--r--spec/frontend/lib/utils/text_markdown_spec.js51
-rw-r--r--spec/frontend/lib/utils/text_utility_spec.js12
-rw-r--r--spec/frontend/listbox/index_spec.js30
-rw-r--r--spec/frontend/members/components/filter_sort/sort_dropdown_spec.js2
-rw-r--r--spec/frontend/members/components/table/member_action_buttons_spec.js2
-rw-r--r--spec/frontend/members/components/table/member_avatar_spec.js2
-rw-r--r--spec/frontend/members/components/table/members_table_cell_spec.js2
-rw-r--r--spec/frontend/members/utils_spec.js12
-rw-r--r--spec/frontend/merge_conflicts/store/actions_spec.js4
-rw-r--r--spec/frontend/merge_request_spec.js10
-rw-r--r--spec/frontend/milestones/components/promote_milestone_modal_spec.js4
-rw-r--r--spec/frontend/monitoring/components/dashboard_spec.js8
-rw-r--r--spec/frontend/monitoring/components/dashboard_url_time_spec.js4
-rw-r--r--spec/frontend/monitoring/requests/index_spec.js2
-rw-r--r--spec/frontend/monitoring/store/actions_spec.js20
-rw-r--r--spec/frontend/monitoring/utils_spec.js14
-rw-r--r--spec/frontend/nav/components/top_nav_app_spec.js28
-rw-r--r--spec/frontend/notebook/cells/output/index_spec.js1
-rw-r--r--spec/frontend/notes/components/__snapshots__/notes_app_spec.js.snap8
-rw-r--r--spec/frontend/notes/components/comment_form_spec.js29
-rw-r--r--spec/frontend/notes/components/diff_discussion_header_spec.js2
-rw-r--r--spec/frontend/notes/components/discussion_actions_spec.js2
-rw-r--r--spec/frontend/notes/components/note_header_spec.js99
-rw-r--r--spec/frontend/notes/components/noteable_note_spec.js2
-rw-r--r--spec/frontend/notes/components/notes_activity_header_spec.js67
-rw-r--r--spec/frontend/notes/components/notes_app_spec.js31
-rw-r--r--spec/frontend/notes/mixins/discussion_navigation_spec.js141
-rw-r--r--spec/frontend/notes/mock_data.js17
-rw-r--r--spec/frontend/notes/utils/get_notes_filter_data_spec.js44
-rw-r--r--spec/frontend/operation_settings/components/metrics_settings_spec.js6
-rw-r--r--spec/frontend/packages_and_registries/harbor_registry/components/tags/tags_list_row_spec.js6
-rw-r--r--spec/frontend/packages_and_registries/harbor_registry/components/tags/tags_list_spec.js4
-rw-r--r--spec/frontend/packages_and_registries/harbor_registry/pages/tags_spec.js4
-rw-r--r--spec/frontend/packages_and_registries/infrastructure_registry/components/details/store/actions_spec.js18
-rw-r--r--spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/infrastructure_title_spec.js2
-rw-r--r--spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/packages_list_app_spec.js8
-rw-r--r--spec/frontend/packages_and_registries/infrastructure_registry/components/list/stores/actions_spec.js10
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/package_title_spec.js.snap24
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/package_title_spec.js22
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/functional/delete_package_spec.js18
-rw-r--r--spec/frontend/packages_and_registries/package_registry/mock_data.js1
-rw-r--r--spec/frontend/packages_and_registries/package_registry/pages/details_spec.js42
-rw-r--r--spec/frontend/packages_and_registries/settings/project/settings/components/cleanup_image_tags_spec.js32
-rw-r--r--spec/frontend/packages_and_registries/settings/project/settings/components/container_expiration_policy_spec.js37
-rw-r--r--spec/frontend/packages_and_registries/settings/project/settings/mock_data.js9
-rw-r--r--spec/frontend/pages/admin/jobs/index/components/stop_jobs_modal_spec.js38
-rw-r--r--spec/frontend/pages/import/fogbugz/new_user_map/components/user_select_spec.js81
-rw-r--r--spec/frontend/pages/projects/forks/new/components/fork_form_spec.js4
-rw-r--r--spec/frontend/pages/projects/forks/new/components/project_namespace_spec.js4
-rw-r--r--spec/frontend/pages/projects/pipeline_schedules/shared/components/timezone_dropdown_spec.js3
-rw-r--r--spec/frontend/pages/shared/wikis/components/wiki_form_spec.js291
-rw-r--r--spec/frontend/pdf/page_spec.js27
-rw-r--r--spec/frontend/performance_bar/components/request_warning_spec.js29
-rw-r--r--spec/frontend/persistent_user_callout_spec.js6
-rw-r--r--spec/frontend/pipeline_editor/components/file-nav/branch_switcher_spec.js2
-rw-r--r--spec/frontend/pipeline_editor/components/ui/pipeline_editor_empty_state_spec.js30
-rw-r--r--spec/frontend/pipeline_editor/pipeline_editor_app_spec.js21
-rw-r--r--spec/frontend/pipeline_editor/pipeline_editor_home_spec.js10
-rw-r--r--spec/frontend/pipeline_new/components/pipeline_new_form_spec.js242
-rw-r--r--spec/frontend/pipeline_new/mock_data.js59
-rw-r--r--spec/frontend/pipeline_schedules/components/pipeline_schedules_form_spec.js25
-rw-r--r--spec/frontend/pipeline_schedules/components/pipeline_schedules_spec.js161
-rw-r--r--spec/frontend/pipeline_schedules/components/table/cells/pipeline_schedule_actions_spec.js49
-rw-r--r--spec/frontend/pipeline_schedules/components/table/cells/pipeline_schedule_last_pipeline_spec.js42
-rw-r--r--spec/frontend/pipeline_schedules/components/table/cells/pipeline_schedule_next_run_spec.js43
-rw-r--r--spec/frontend/pipeline_schedules/components/table/cells/pipeline_schedule_owner_spec.js40
-rw-r--r--spec/frontend/pipeline_schedules/components/table/cells/pipeline_schedule_target_spec.js41
-rw-r--r--spec/frontend/pipeline_schedules/components/table/pipeline_schedules_table_spec.js39
-rw-r--r--spec/frontend/pipeline_schedules/mock_data.js35
-rw-r--r--spec/frontend/pipeline_wizard/components/commit_spec.js4
-rw-r--r--spec/frontend/pipeline_wizard/components/editor_spec.js20
-rw-r--r--spec/frontend/pipeline_wizard/components/widgets/list_spec.js21
-rw-r--r--spec/frontend/pipeline_wizard/components/wrapper_spec.js29
-rw-r--r--spec/frontend/pipeline_wizard/mock/yaml.js3
-rw-r--r--spec/frontend/pipelines/components/jobs/failed_jobs_app_spec.js6
-rw-r--r--spec/frontend/pipelines/components/jobs/failed_jobs_table_spec.js4
-rw-r--r--spec/frontend/pipelines/components/jobs/jobs_app_spec.js6
-rw-r--r--spec/frontend/pipelines/pipeline_multi_actions_spec.js19
-rw-r--r--spec/frontend/pipelines/pipelines_actions_spec.js4
-rw-r--r--spec/frontend/pipelines/pipelines_spec.js16
-rw-r--r--spec/frontend/pipelines/test_reports/stores/actions_spec.js4
-rw-r--r--spec/frontend/pipelines/test_reports/stores/mutations_spec.js4
-rw-r--r--spec/frontend/profile/account/components/update_username_spec.js6
-rw-r--r--spec/frontend/profile/preferences/components/profile_preferences_spec.js16
-rw-r--r--spec/frontend/projects/commit/store/actions_spec.js4
-rw-r--r--spec/frontend/projects/commits/store/actions_spec.js6
-rw-r--r--spec/frontend/projects/compare/components/app_spec.js34
-rw-r--r--spec/frontend/projects/compare/components/mock_data.js1
-rw-r--r--spec/frontend/projects/compare/components/revision_dropdown_legacy_spec.js4
-rw-r--r--spec/frontend/projects/compare/components/revision_dropdown_spec.js6
-rw-r--r--spec/frontend/projects/settings/branch_rules/components/edit/branch_dropdown_spec.js (renamed from spec/frontend/projects/settings/branch_rules/branch_dropdown_spec.js)2
-rw-r--r--spec/frontend/projects/settings/branch_rules/components/edit/index_spec.js (renamed from spec/frontend/projects/settings/branch_rules/rule_edit_spec.js)6
-rw-r--r--spec/frontend/projects/settings/branch_rules/components/edit/protections/index_spec.js (renamed from spec/frontend/projects/settings/branch_rules/components/protections/index_spec.js)8
-rw-r--r--spec/frontend/projects/settings/branch_rules/components/edit/protections/merge_protections_spec.js (renamed from spec/frontend/projects/settings/branch_rules/components/protections/merge_protections_spec.js)4
-rw-r--r--spec/frontend/projects/settings/branch_rules/components/edit/protections/push_protections_spec.js (renamed from spec/frontend/projects/settings/branch_rules/components/protections/push_protections_spec.js)4
-rw-r--r--spec/frontend/projects/settings/branch_rules/components/view/index_spec.js113
-rw-r--r--spec/frontend/projects/settings/branch_rules/components/view/mock_data.js141
-rw-r--r--spec/frontend/projects/settings/branch_rules/components/view/protection_row_spec.js71
-rw-r--r--spec/frontend/projects/settings/branch_rules/components/view/protection_spec.js68
-rw-r--r--spec/frontend/projects/settings/components/default_branch_selector_spec.js46
-rw-r--r--spec/frontend/projects/settings/components/transfer_project_form_spec.js273
-rw-r--r--spec/frontend/projects/settings/repository/branch_rules/app_spec.js18
-rw-r--r--spec/frontend/projects/settings/repository/branch_rules/components/branch_rule_spec.js30
-rw-r--r--spec/frontend/projects/settings/repository/branch_rules/mock_data.js13
-rw-r--r--spec/frontend/protected_branches/protected_branch_edit_spec.js6
-rw-r--r--spec/frontend/ref/components/ref_selector_spec.js28
-rw-r--r--spec/frontend/releases/__snapshots__/util_spec.js.snap12
-rw-r--r--spec/frontend/releases/components/app_index_spec.js6
-rw-r--r--spec/frontend/releases/components/app_show_spec.js6
-rw-r--r--spec/frontend/releases/components/evidence_block_spec.js6
-rw-r--r--spec/frontend/releases/components/tag_field_new_spec.js46
-rw-r--r--spec/frontend/releases/stores/modules/detail/actions_spec.js43
-rw-r--r--spec/frontend/releases/stores/modules/detail/getters_spec.js4
-rw-r--r--spec/frontend/releases/stores/modules/detail/mutations_spec.js11
-rw-r--r--spec/frontend/releases/util_spec.js16
-rw-r--r--spec/frontend/reports/accessibility_report/components/accessibility_issue_body_spec.js112
-rw-r--r--spec/frontend/reports/accessibility_report/grouped_accessibility_reports_app_spec.js125
-rw-r--r--spec/frontend/reports/accessibility_report/mock_data.js53
-rw-r--r--spec/frontend/reports/accessibility_report/store/actions_spec.js115
-rw-r--r--spec/frontend/reports/accessibility_report/store/getters_spec.js149
-rw-r--r--spec/frontend/reports/accessibility_report/store/mutations_spec.js64
-rw-r--r--spec/frontend/reports/components/report_section_spec.js22
-rw-r--r--spec/frontend/repository/commits_service_spec.js6
-rw-r--r--spec/frontend/repository/components/__snapshots__/last_commit_spec.js.snap2
-rw-r--r--spec/frontend/repository/components/blob_controls_spec.js12
-rw-r--r--spec/frontend/repository/components/last_commit_spec.js2
-rw-r--r--spec/frontend/repository/components/new_directory_modal_spec.js4
-rw-r--r--spec/frontend/repository/components/table/__snapshots__/row_spec.js.snap18
-rw-r--r--spec/frontend/repository/components/table/index_spec.js2
-rw-r--r--spec/frontend/repository/components/table/parent_row_spec.js4
-rw-r--r--spec/frontend/repository/components/table/row_spec.js16
-rw-r--r--spec/frontend/repository/components/tree_content_spec.js4
-rw-r--r--spec/frontend/repository/components/upload_blob_modal_spec.js16
-rw-r--r--spec/frontend/repository/pages/blob_spec.js2
-rw-r--r--spec/frontend/repository/pages/index_spec.js2
-rw-r--r--spec/frontend/runner/admin_runner_show/admin_runner_show_app_spec.js4
-rw-r--r--spec/frontend/runner/admin_runners/admin_runners_app_spec.js75
-rw-r--r--spec/frontend/runner/components/cells/link_cell_spec.js2
-rw-r--r--spec/frontend/runner/components/cells/runner_actions_cell_spec.js4
-rw-r--r--spec/frontend/runner/components/cells/runner_owner_cell_spec.js111
-rw-r--r--spec/frontend/runner/components/cells/runner_stacked_summary_cell_spec.js6
-rw-r--r--spec/frontend/runner/components/runner_bulk_delete_checkbox_spec.js99
-rw-r--r--spec/frontend/runner/components/runner_delete_button_spec.js31
-rw-r--r--spec/frontend/runner/components/runner_details_spec.js28
-rw-r--r--spec/frontend/runner/components/runner_filtered_search_bar_spec.js33
-rw-r--r--spec/frontend/runner/components/runner_list_empty_state_spec.js57
-rw-r--r--spec/frontend/runner/components/runner_list_spec.js56
-rw-r--r--spec/frontend/runner/components/runner_membership_toggle_spec.js57
-rw-r--r--spec/frontend/runner/components/runner_stacked_layout_banner_spec.js2
-rw-r--r--spec/frontend/runner/components/runner_type_tabs_spec.js20
-rw-r--r--spec/frontend/runner/components/runner_update_form_spec.js2
-rw-r--r--spec/frontend/runner/components/search_tokens/tag_token_spec.js43
-rw-r--r--spec/frontend/runner/graphql/local_state_spec.js51
-rw-r--r--spec/frontend/runner/group_runner_show/group_runner_show_app_spec.js5
-rw-r--r--spec/frontend/runner/group_runners/group_runners_app_spec.js69
-rw-r--r--spec/frontend/runner/mock_data.js96
-rw-r--r--spec/frontend/search/sidebar/components/app_spec.js41
-rw-r--r--spec/frontend/search/sidebar/components/confidentiality_filter_spec.js2
-rw-r--r--spec/frontend/search/sidebar/components/radio_filter_spec.js2
-rw-r--r--spec/frontend/search/sidebar/components/status_filter_spec.js2
-rw-r--r--spec/frontend/search/sort/components/app_spec.js6
-rw-r--r--spec/frontend/search/store/actions_spec.js6
-rw-r--r--spec/frontend/search/topbar/components/app_spec.js6
-rw-r--r--spec/frontend/search/topbar/components/group_filter_spec.js2
-rw-r--r--spec/frontend/search/topbar/components/project_filter_spec.js2
-rw-r--r--spec/frontend/search_settings/components/search_settings_spec.js46
-rw-r--r--spec/frontend/security_configuration/components/app_spec.js2
-rw-r--r--spec/frontend/security_configuration/components/training_provider_list_spec.js4
-rw-r--r--spec/frontend/security_configuration/components/upgrade_banner_spec.js2
-rw-r--r--spec/frontend/self_monitor/components/self_monitor_form_spec.js4
-rw-r--r--spec/frontend/set_status_modal/set_status_form_spec.js2
-rw-r--r--spec/frontend/set_status_modal/set_status_modal_wrapper_spec.js8
-rw-r--r--spec/frontend/sidebar/assignee_title_spec.js4
-rw-r--r--spec/frontend/sidebar/assignees_spec.js2
-rw-r--r--spec/frontend/sidebar/components/assignees/assignee_avatar_link_spec.js2
-rw-r--r--spec/frontend/sidebar/components/assignees/collapsed_assignee_list_spec.js2
-rw-r--r--spec/frontend/sidebar/components/assignees/collapsed_assignee_spec.js2
-rw-r--r--spec/frontend/sidebar/components/assignees/sidebar_assignees_widget_spec.js6
-rw-r--r--spec/frontend/sidebar/components/assignees/sidebar_editable_item_spec.js36
-rw-r--r--spec/frontend/sidebar/components/assignees/uncollapsed_assignee_list_spec.js2
-rw-r--r--spec/frontend/sidebar/components/confidential/sidebar_confidentiality_form_spec.js6
-rw-r--r--spec/frontend/sidebar/components/confidential/sidebar_confidentiality_widget_spec.js4
-rw-r--r--spec/frontend/sidebar/components/copy_email_to_clipboard_spec.js2
-rw-r--r--spec/frontend/sidebar/components/crm_contacts_spec.js4
-rw-r--r--spec/frontend/sidebar/components/date/sidebar_date_widget_spec.js10
-rw-r--r--spec/frontend/sidebar/components/date/sidebar_formatted_date_spec.js2
-rw-r--r--spec/frontend/sidebar/components/severity/severity_spec.js2
-rw-r--r--spec/frontend/sidebar/components/severity/sidebar_severity_spec.js6
-rw-r--r--spec/frontend/sidebar/components/sidebar_dropdown_widget_spec.js14
-rw-r--r--spec/frontend/sidebar/components/subscriptions/sidebar_subscriptions_widget_spec.js4
-rw-r--r--spec/frontend/sidebar/components/time_tracking/report_spec.js10
-rw-r--r--spec/frontend/sidebar/components/todo_toggle/sidebar_todo_widget_spec.js16
-rw-r--r--spec/frontend/sidebar/issuable_assignees_spec.js2
-rw-r--r--spec/frontend/sidebar/lock/edit_form_buttons_spec.js6
-rw-r--r--spec/frontend/sidebar/lock/issuable_lock_form_spec.js2
-rw-r--r--spec/frontend/sidebar/mock_data.js1
-rw-r--r--spec/frontend/sidebar/participants_spec.js6
-rw-r--r--spec/frontend/sidebar/reviewer_title_spec.js4
-rw-r--r--spec/frontend/sidebar/reviewers_spec.js2
-rw-r--r--spec/frontend/sidebar/sidebar_assignees_spec.js8
-rw-r--r--spec/frontend/sidebar/sidebar_mediator_spec.js18
-rw-r--r--spec/frontend/sidebar/sidebar_move_issue_spec.js4
-rw-r--r--spec/frontend/sidebar/subscriptions_spec.js2
-rw-r--r--spec/frontend/sidebar/todo_spec.js10
-rw-r--r--spec/frontend/smart_interval_spec.js16
-rw-r--r--spec/frontend/snippet/collapsible_input_spec.js4
-rw-r--r--spec/frontend/snippets/components/edit_spec.js14
-rw-r--r--spec/frontend/snippets/components/embed_dropdown_spec.js2
-rw-r--r--spec/frontend/snippets/components/snippet_blob_edit_spec.js10
-rw-r--r--spec/frontend/snippets/components/snippet_blob_view_spec.js89
-rw-r--r--spec/frontend/snippets/components/snippet_header_spec.js14
-rw-r--r--spec/frontend/snippets/components/snippet_title_spec.js8
-rw-r--r--spec/frontend/snippets/components/snippet_visibility_edit_spec.js8
-rw-r--r--spec/frontend/terms/components/app_spec.js2
-rw-r--r--spec/frontend/terraform/components/states_table_spec.js6
-rw-r--r--spec/frontend/terraform/components/terraform_list_spec.js14
-rw-r--r--spec/frontend/toggles/index_spec.js4
-rw-r--r--spec/frontend/token_access/token_access_spec.js16
-rw-r--r--spec/frontend/tooltips/components/tooltips_spec.js18
-rw-r--r--spec/frontend/user_lists/components/edit_user_list_spec.js8
-rw-r--r--spec/frontend/user_lists/components/new_user_list_spec.js2
-rw-r--r--spec/frontend/user_lists/components/user_list_spec.js6
-rw-r--r--spec/frontend/user_lists/components/user_lists_table_spec.js4
-rw-r--r--spec/frontend/user_popovers_spec.js4
-rw-r--r--spec/frontend/vue_merge_request_widget/components/approvals/approvals_spec.js14
-rw-r--r--spec/frontend/vue_merge_request_widget/components/approvals/approvals_summary_optional_spec.js2
-rw-r--r--spec/frontend/vue_merge_request_widget/components/approvals/approvals_summary_spec.js4
-rw-r--r--spec/frontend/vue_merge_request_widget/components/artifacts_list_app_spec.js2
-rw-r--r--spec/frontend/vue_merge_request_widget/components/artifacts_list_spec.js4
-rw-r--r--spec/frontend/vue_merge_request_widget/components/mr_collapsible_extension_spec.js4
-rw-r--r--spec/frontend/vue_merge_request_widget/components/mr_widget_author_time_spec.js4
-rw-r--r--spec/frontend/vue_merge_request_widget/components/mr_widget_expandable_section_spec.js6
-rw-r--r--spec/frontend/vue_merge_request_widget/components/mr_widget_icon_spec.js2
-rw-r--r--spec/frontend/vue_merge_request_widget/components/mr_widget_pipeline_container_spec.js12
-rw-r--r--spec/frontend/vue_merge_request_widget/components/mr_widget_suggest_pipeline_spec.js4
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/__snapshots__/mr_widget_auto_merge_enabled_spec.js.snap10
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/merge_checks_failed_spec.js6
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed_spec.js4
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/mr_widget_merged_spec.js222
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/mr_widget_pipeline_failed_spec.js21
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/mr_widget_ready_to_merge_spec.js77
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/mr_widget_squash_before_merge_spec.js2
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/mr_widget_wip_spec.js2
-rw-r--r--spec/frontend/vue_merge_request_widget/components/terraform/mr_widget_terraform_container_spec.js175
-rw-r--r--spec/frontend/vue_merge_request_widget/components/terraform/terraform_plan_spec.js93
-rw-r--r--spec/frontend/vue_merge_request_widget/components/widget/__snapshots__/dynamic_content_spec.js.snap35
-rw-r--r--spec/frontend/vue_merge_request_widget/components/widget/dynamic_content_spec.js52
-rw-r--r--spec/frontend/vue_merge_request_widget/components/widget/widget_content_row_spec.js65
-rw-r--r--spec/frontend/vue_merge_request_widget/components/widget/widget_content_section_spec.js39
-rw-r--r--spec/frontend/vue_merge_request_widget/components/widget/widget_spec.js7
-rw-r--r--spec/frontend/vue_merge_request_widget/deployment/deployment_action_button_spec.js20
-rw-r--r--spec/frontend/vue_merge_request_widget/deployment/deployment_actions_spec.js20
-rw-r--r--spec/frontend/vue_merge_request_widget/deployment/deployment_spec.js16
-rw-r--r--spec/frontend/vue_merge_request_widget/deployment/deployment_view_button_spec.js94
-rw-r--r--spec/frontend/vue_merge_request_widget/extensions/test_report/index_spec.js2
-rw-r--r--spec/frontend/vue_merge_request_widget/mr_widget_how_to_merge_modal_spec.js2
-rw-r--r--spec/frontend/vue_merge_request_widget/mr_widget_options_spec.js96
-rw-r--r--spec/frontend/vue_merge_request_widget/stores/get_state_key_spec.js17
-rw-r--r--spec/frontend/vue_shared/components/ci_badge_link_spec.js12
-rw-r--r--spec/frontend/vue_shared/components/color_select_dropdown/color_select_root_spec.js6
-rw-r--r--spec/frontend/vue_shared/components/date_time_picker/date_time_picker_lib_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/diff_stats_dropdown_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/diff_viewer/diff_viewer_spec.js51
-rw-r--r--spec/frontend/vue_shared/components/file_finder/item_spec.js118
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/store/modules/filters/actions_spec.js16
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/author_token_spec.js6
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/branch_token_spec.js6
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/crm_contact_token_spec.js14
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/crm_organization_token_spec.js14
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/emoji_token_spec.js6
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/label_token_spec.js6
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/milestone_token_spec.js6
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/release_token_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/gitlab_version_check_spec.js33
-rw-r--r--spec/frontend/vue_shared/components/gl_countdown_spec.js36
-rw-r--r--spec/frontend/vue_shared/components/group_select/utils_spec.js24
-rw-r--r--spec/frontend/vue_shared/components/markdown/header_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/markdown/markdown_editor_spec.js289
-rw-r--r--spec/frontend/vue_shared/components/metric_images/metric_images_table_spec.js6
-rw-r--r--spec/frontend/vue_shared/components/metric_images/store/actions_spec.js10
-rw-r--r--spec/frontend/vue_shared/components/modal_copy_button_spec.js7
-rw-r--r--spec/frontend/vue_shared/components/namespace_select/namespace_select_deprecated_spec.js (renamed from spec/frontend/vue_shared/components/namespace_select/namespace_select_spec.js)18
-rw-r--r--spec/frontend/vue_shared/components/notes/__snapshots__/placeholder_note_spec.js.snap23
-rw-r--r--spec/frontend/vue_shared/components/notes/placeholder_note_spec.js14
-rw-r--r--spec/frontend/vue_shared/components/pagination_bar/pagination_bar_spec.js26
-rw-r--r--spec/frontend/vue_shared/components/panel_resizer_spec.js81
-rw-r--r--spec/frontend/vue_shared/components/registry/__snapshots__/history_item_spec.js.snap8
-rw-r--r--spec/frontend/vue_shared/components/security_reports/artifact_downloads/merge_request_artifact_download_spec.js6
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_vue/labels_select_root_spec.js11
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/actions_spec.js6
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/mutations_spec.js12
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view_spec.js10
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view_spec.js6
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_widget/labels_select_root_spec.js6
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/components/chunk_line_spec.js22
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/highlight_util_spec.js44
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/plugins/index_spec.js14
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/plugins/utils/dependency_linker_util_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/plugins/wrap_bidi_chars_spec.js17
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/plugins/wrap_child_nodes_spec.js22
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/plugins/wrap_comments_spec.js29
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/source_viewer_spec.js21
-rw-r--r--spec/frontend/vue_shared/components/stacked_progress_bar_spec.js124
-rw-r--r--spec/frontend/vue_shared/components/timezone_dropdown/helpers.js6
-rw-r--r--spec/frontend/vue_shared/components/timezone_dropdown/timezone_dropdown_spec.js (renamed from spec/frontend/deploy_freeze/components/timezone_dropdown_spec.js)47
-rw-r--r--spec/frontend/vue_shared/components/url_sync_spec.js62
-rw-r--r--spec/frontend/vue_shared/components/user_avatar/user_avatar_image_new_spec.js134
-rw-r--r--spec/frontend/vue_shared/components/user_avatar/user_avatar_image_old_spec.js127
-rw-r--r--spec/frontend/vue_shared/components/user_avatar/user_avatar_image_spec.js143
-rw-r--r--spec/frontend/vue_shared/components/user_avatar/user_avatar_link_new_spec.js103
-rw-r--r--spec/frontend/vue_shared/components/user_avatar/user_avatar_link_old_spec.js103
-rw-r--r--spec/frontend/vue_shared/components/user_avatar/user_avatar_link_spec.js119
-rw-r--r--spec/frontend/vue_shared/components/user_avatar/user_avatar_list_spec.js25
-rw-r--r--spec/frontend/vue_shared/components/user_popover/user_popover_spec.js70
-rw-r--r--spec/frontend/vue_shared/directives/safe_html_spec.js116
-rw-r--r--spec/frontend/vue_shared/issuable/__snapshots__/issuable_blocked_icon_spec.js.snap (renamed from spec/frontend/boards/components/__snapshots__/board_blocked_icon_spec.js.snap)12
-rw-r--r--spec/frontend/vue_shared/issuable/issuable_blocked_icon_spec.js (renamed from spec/frontend/boards/components/board_blocked_icon_spec.js)13
-rw-r--r--spec/frontend/vue_shared/issuable/show/components/issuable_body_spec.js8
-rw-r--r--spec/frontend/vue_shared/security_reports/security_reports_app_spec.js6
-rw-r--r--spec/frontend/webhooks/components/form_url_app_spec.js142
-rw-r--r--spec/frontend/webhooks/components/form_url_mask_item_spec.js100
-rw-r--r--spec/frontend/whats_new/components/app_spec.js6
-rw-r--r--spec/frontend/work_items/components/work_item_assignees_spec.js8
-rw-r--r--spec/frontend/work_items/components/work_item_description_spec.js31
-rw-r--r--spec/frontend/work_items/components/work_item_detail_spec.js100
-rw-r--r--spec/frontend/work_items/components/work_item_due_date_spec.js4
-rw-r--r--spec/frontend/work_items/components/work_item_labels_spec.js98
-rw-r--r--spec/frontend/work_items/components/work_item_links/work_item_links_form_spec.js2
-rw-r--r--spec/frontend/work_items/components/work_item_links/work_item_links_menu_spec.js4
-rw-r--r--spec/frontend/work_items/components/work_item_links/work_item_links_spec.js31
-rw-r--r--spec/frontend/work_items/components/work_item_milestone_spec.js247
-rw-r--r--spec/frontend/work_items/components/work_item_type_icon_spec.js2
-rw-r--r--spec/frontend/work_items/mock_data.js273
-rw-r--r--spec/frontend/work_items/router_spec.js10
-rw-r--r--spec/frontend/work_items_hierarchy/components/app_spec.js4
-rw-r--r--spec/frontend/work_items_hierarchy/components/hierarchy_spec.js2
526 files changed, 10013 insertions, 5794 deletions
diff --git a/spec/frontend/__helpers__/class_spec_helper.js b/spec/frontend/__helpers__/class_spec_helper.js
deleted file mode 100644
index b26f087f0c5..00000000000
--- a/spec/frontend/__helpers__/class_spec_helper.js
+++ /dev/null
@@ -1,10 +0,0 @@
-// eslint-disable-next-line jest/no-export
-export default class ClassSpecHelper {
- static itShouldBeAStaticMethod(base, method) {
- return it('should be a static method', () => {
- expect(Object.prototype.hasOwnProperty.call(base, method)).toBeTruthy();
- });
- }
-}
-
-window.ClassSpecHelper = ClassSpecHelper;
diff --git a/spec/frontend/__helpers__/class_spec_helper_spec.js b/spec/frontend/__helpers__/class_spec_helper_spec.js
deleted file mode 100644
index 533d5687bde..00000000000
--- a/spec/frontend/__helpers__/class_spec_helper_spec.js
+++ /dev/null
@@ -1,26 +0,0 @@
-/* global ClassSpecHelper */
-
-import './class_spec_helper';
-
-describe('ClassSpecHelper', () => {
- let testContext;
-
- beforeEach(() => {
- testContext = {};
- });
-
- describe('itShouldBeAStaticMethod', () => {
- beforeEach(() => {
- class TestClass {
- instanceMethod() {
- this.prop = 'val';
- }
- static staticMethod() {}
- }
-
- testContext.TestClass = TestClass;
- });
-
- ClassSpecHelper.itShouldBeAStaticMethod(ClassSpecHelper, 'itShouldBeAStaticMethod');
- });
-});
diff --git a/spec/frontend/__helpers__/dom_shims/index.js b/spec/frontend/__helpers__/dom_shims/index.js
index 742d55196b4..3b41e2ca2a7 100644
--- a/spec/frontend/__helpers__/dom_shims/index.js
+++ b/spec/frontend/__helpers__/dom_shims/index.js
@@ -11,3 +11,4 @@ import './window_scroll_to';
import './scroll_by';
import './size_properties';
import './image_element_properties';
+import './text_encoder';
diff --git a/spec/frontend/__helpers__/dom_shims/text_encoder.js b/spec/frontend/__helpers__/dom_shims/text_encoder.js
new file mode 100644
index 00000000000..d3d5221a003
--- /dev/null
+++ b/spec/frontend/__helpers__/dom_shims/text_encoder.js
@@ -0,0 +1,4 @@
+import { TextEncoder, TextDecoder } from 'util';
+
+global.TextEncoder = TextEncoder;
+global.TextDecoder = TextDecoder;
diff --git a/spec/frontend/__helpers__/graphql_transformer.js b/spec/frontend/__helpers__/graphql_transformer.js
new file mode 100644
index 00000000000..e776e2ea6ac
--- /dev/null
+++ b/spec/frontend/__helpers__/graphql_transformer.js
@@ -0,0 +1,8 @@
+/* eslint-disable import/no-commonjs */
+const loader = require('graphql-tag/loader');
+
+module.exports = {
+ process(src) {
+ return loader.call({ cacheable() {} }, src);
+ },
+};
diff --git a/spec/frontend/__helpers__/shared_test_setup.js b/spec/frontend/__helpers__/shared_test_setup.js
index 45a7b8e0352..2fe9fe89a90 100644
--- a/spec/frontend/__helpers__/shared_test_setup.js
+++ b/spec/frontend/__helpers__/shared_test_setup.js
@@ -1,7 +1,7 @@
/* Common setup for both unit and integration test environments */
-import { config as testUtilsConfig } from '@vue/test-utils';
import * as jqueryMatchers from 'custom-jquery-matchers';
import Vue from 'vue';
+import { enableAutoDestroy } from '@vue/test-utils';
import 'jquery';
import Translate from '~/vue_shared/translate';
import setWindowLocation from './set_window_location_helper';
@@ -13,6 +13,8 @@ import './dom_shims';
import './jquery';
import '~/commons/bootstrap';
+enableAutoDestroy(afterEach);
+
// This module has some fairly decent visual test coverage in it's own repository.
jest.mock('@gitlab/favicon-overlay');
jest.mock('~/lib/utils/axios_utils', () => jest.requireActual('helpers/mocks/axios_utils'));
@@ -44,16 +46,6 @@ Object.entries(jqueryMatchers).forEach(([matcherName, matcherFactory]) => {
expect.extend(customMatchers);
-testUtilsConfig.deprecationWarningHandler = (method, message) => {
- const ALLOWED_DEPRECATED_METHODS = [
- // https://gitlab.com/gitlab-org/gitlab/-/issues/295679
- 'finding components with `find` or `get`',
- ];
- if (!ALLOWED_DEPRECATED_METHODS.includes(method)) {
- global.console.error(message);
- }
-};
-
Object.assign(global, {
requestIdleCallback(cb) {
const start = Date.now();
@@ -72,6 +64,7 @@ Object.assign(global, {
beforeEach(() => {
// make sure that each test actually tests something
// see https://jestjs.io/docs/en/expect#expecthasassertions
+ // eslint-disable-next-line jest/no-standalone-expect
expect.hasAssertions();
// Reset the mocked window.location. This ensures tests don't interfere with
diff --git a/spec/frontend/__helpers__/stub_component.js b/spec/frontend/__helpers__/stub_component.js
index 4f9d1ee6f5d..3e9af994ee3 100644
--- a/spec/frontend/__helpers__/stub_component.js
+++ b/spec/frontend/__helpers__/stub_component.js
@@ -38,7 +38,7 @@ export function stubComponent(Component, options = {}) {
// Do not render any slots/scoped slots except default
// This differs from VTU behavior which renders all slots
template: '<div><slot></slot></div>',
- // allows wrapper.find(Component) to work for stub
+ // allows wrapper.findComponent(Component) to work for stub
$_vueTestUtils_original: Component,
...options,
};
diff --git a/spec/frontend/__helpers__/vue_mount_component_helper.js b/spec/frontend/__helpers__/vue_mount_component_helper.js
deleted file mode 100644
index ed43355ea5b..00000000000
--- a/spec/frontend/__helpers__/vue_mount_component_helper.js
+++ /dev/null
@@ -1,34 +0,0 @@
-/**
- * Deprecated. Please do not use.
- * Please see https://gitlab.com/groups/gitlab-org/-/epics/2445
- */
-const mountComponent = (Component, props = {}, el = null) =>
- new Component({
- propsData: props,
- }).$mount(el);
-
-/**
- * Deprecated. Please do not use.
- * Please see https://gitlab.com/groups/gitlab-org/-/epics/2445
- */
-export const createComponentWithStore = (Component, store, propsData = {}) =>
- new Component({
- store,
- propsData,
- });
-
-/**
- * Deprecated. Please do not use.
- * Please see https://gitlab.com/groups/gitlab-org/-/epics/2445
- */
-export const mountComponentWithStore = (Component, { el, props, store }) =>
- new Component({
- store,
- propsData: props || {},
- }).$mount(el);
-
-/**
- * Deprecated. Please do not use.
- * Please see https://gitlab.com/groups/gitlab-org/-/epics/2445
- */
-export default mountComponent;
diff --git a/spec/frontend/__helpers__/vue_test_utils_helper_spec.js b/spec/frontend/__helpers__/vue_test_utils_helper_spec.js
index ae180c3b49d..466333f8a89 100644
--- a/spec/frontend/__helpers__/vue_test_utils_helper_spec.js
+++ b/spec/frontend/__helpers__/vue_test_utils_helper_spec.js
@@ -140,11 +140,12 @@ describe('Vue test utils helpers', () => {
const text = 'foo bar';
const options = { selector: 'div' };
const mockDiv = document.createElement('div');
- const mockVm = new Vue({ render: (h) => h('div') }).$mount();
+ let mockVm;
let wrapper;
beforeEach(() => {
jest.spyOn(vtu, 'createWrapper');
+ mockVm = new Vue({ render: (h) => h('div') }).$mount();
wrapper = extendedWrapper(
shallowMount({
diff --git a/spec/frontend/__mocks__/monaco-editor/index.js b/spec/frontend/__mocks__/monaco-editor/index.js
index 384f9993150..d09672a4ecf 100644
--- a/spec/frontend/__mocks__/monaco-editor/index.js
+++ b/spec/frontend/__mocks__/monaco-editor/index.js
@@ -8,10 +8,8 @@ import 'monaco-editor/esm/vs/language/css/monaco.contribution';
import 'monaco-editor/esm/vs/language/json/monaco.contribution';
import 'monaco-editor/esm/vs/language/html/monaco.contribution';
import 'monaco-editor/esm/vs/basic-languages/monaco.contribution';
-import 'monaco-yaml/lib/esm/monaco.contribution';
// This language starts trying to spin up web workers which obviously breaks in Jest environment
jest.mock('monaco-editor/esm/vs/language/typescript/tsMode');
-jest.mock('monaco-yaml/lib/esm/yamlMode');
export * from 'monaco-editor/esm/vs/editor/editor.api';
diff --git a/spec/frontend/__mocks__/monaco-yaml/index.js b/spec/frontend/__mocks__/monaco-yaml/index.js
new file mode 100644
index 00000000000..36681854d0b
--- /dev/null
+++ b/spec/frontend/__mocks__/monaco-yaml/index.js
@@ -0,0 +1,4 @@
+const setDiagnosticsOptions = jest.fn();
+const yamlDefaults = {};
+
+export { setDiagnosticsOptions, yamlDefaults };
diff --git a/spec/frontend/access_tokens/components/access_token_table_app_spec.js b/spec/frontend/access_tokens/components/access_token_table_app_spec.js
index aed3db4aa4c..2fa14810578 100644
--- a/spec/frontend/access_tokens/components/access_token_table_app_spec.js
+++ b/spec/frontend/access_tokens/components/access_token_table_app_spec.js
@@ -1,6 +1,6 @@
import { GlButton, GlPagination, GlTable } from '@gitlab/ui';
-import { mount } from '@vue/test-utils';
import { nextTick } from 'vue';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
import AccessTokenTableApp from '~/access_tokens/components/access_token_table_app.vue';
import { EVENT_SUCCESS, PAGE_SIZE } from '~/access_tokens/components/constants';
import { __, s__, sprintf } from '~/locale';
@@ -11,7 +11,7 @@ describe('~/access_tokens/components/access_token_table_app', () => {
const accessTokenType = 'personal access token';
const accessTokenTypePlural = 'personal access tokens';
- const initialActiveAccessTokens = [];
+ const information = undefined;
const noActiveTokensMessage = 'This user has no active personal access tokens.';
const showRole = false;
@@ -43,11 +43,12 @@ describe('~/access_tokens/components/access_token_table_app', () => {
];
const createComponent = (props = {}) => {
- wrapper = mount(AccessTokenTableApp, {
+ wrapper = mountExtended(AccessTokenTableApp, {
provide: {
accessTokenType,
accessTokenTypePlural,
- initialActiveAccessTokens,
+ information,
+ initialActiveAccessTokens: defaultActiveAccessTokens,
noActiveTokensMessage,
showRole,
...props,
@@ -71,8 +72,8 @@ describe('~/access_tokens/components/access_token_table_app', () => {
wrapper?.destroy();
});
- it('should render the `GlTable` with default empty message', () => {
- createComponent();
+ it('should render an empty table with a default message', () => {
+ createComponent({ initialActiveAccessTokens: [] });
const cells = findCells();
expect(cells).toHaveLength(1);
@@ -81,58 +82,61 @@ describe('~/access_tokens/components/access_token_table_app', () => {
);
});
- it('should render the `GlTable` with custom empty message', () => {
+ it('should render an empty table with a custom message', () => {
const noTokensMessage = 'This group has no active access tokens.';
- createComponent({ noActiveTokensMessage: noTokensMessage });
+ createComponent({ initialActiveAccessTokens: [], noActiveTokensMessage: noTokensMessage });
const cells = findCells();
expect(cells).toHaveLength(1);
expect(cells.at(0).text()).toBe(noTokensMessage);
});
- it('should render an h5 element', () => {
+ it('should show a title indicating the amount of tokens', () => {
createComponent();
expect(wrapper.find('h5').text()).toBe(
sprintf(__('Active %{accessTokenTypePlural} (%{totalAccessTokens})'), {
accessTokenTypePlural,
- totalAccessTokens: initialActiveAccessTokens.length,
+ totalAccessTokens: defaultActiveAccessTokens.length,
}),
);
});
- it('should render the `GlTable` component with default 6 column headers', () => {
- createComponent();
+ it('should render information section', () => {
+ const info = 'This is my information';
+ createComponent({ information: info });
- const headers = findHeaders();
- expect(headers).toHaveLength(6);
- [
- __('Token name'),
- __('Scopes'),
- s__('AccessTokens|Created'),
- __('Last Used'),
- __('Expires'),
- __('Action'),
- ].forEach((text, index) => {
- expect(headers.at(index).text()).toBe(text);
- });
+ expect(wrapper.findByTestId('information-section').text()).toBe(info);
});
- it('should render the `GlTable` component with 7 headers', () => {
- createComponent({ showRole: true });
+ describe('table headers', () => {
+ it('should include `Action` column', () => {
+ createComponent();
+
+ const headers = findHeaders();
+ expect(headers.wrappers.map((header) => header.text())).toStrictEqual([
+ __('Token name'),
+ __('Scopes'),
+ s__('AccessTokens|Created'),
+ __('Last Used'),
+ __('Expires'),
+ __('Action'),
+ ]);
+ });
- const headers = findHeaders();
- expect(headers).toHaveLength(7);
- [
- __('Token name'),
- __('Scopes'),
- s__('AccessTokens|Created'),
- __('Last Used'),
- __('Expires'),
- __('Role'),
- __('Action'),
- ].forEach((text, index) => {
- expect(headers.at(index).text()).toBe(text);
+ it('should include `Role` column', () => {
+ createComponent({ showRole: true });
+
+ const headers = findHeaders();
+ expect(headers.wrappers.map((header) => header.text())).toStrictEqual([
+ __('Token name'),
+ __('Scopes'),
+ s__('AccessTokens|Created'),
+ __('Last Used'),
+ __('Expires'),
+ __('Role'),
+ __('Action'),
+ ]);
});
});
@@ -150,8 +154,8 @@ describe('~/access_tokens/components/access_token_table_app', () => {
expect(assistiveElement.text()).toBe(s__('AccessTokens|The last time a token was used'));
});
- it('updates the table after a success AJAX event', async () => {
- createComponent({ showRole: true });
+ it('updates the table after new tokens are created', async () => {
+ createComponent({ initialActiveAccessTokens: [], showRole: true });
await triggerSuccess();
const cells = findCells();
@@ -190,16 +194,43 @@ describe('~/access_tokens/components/access_token_table_app', () => {
expect(button.props('category')).toBe('tertiary');
});
- describe('revoke path', () => {
- beforeEach(() => {
- createComponent({ showRole: true });
+ describe('when revoke_path is', () => {
+ describe('absent in all tokens', () => {
+ it('should not include `Action` column', () => {
+ createComponent({
+ initialActiveAccessTokens: defaultActiveAccessTokens.map(
+ ({ revoke_path, ...rest }) => rest,
+ ),
+ showRole: true,
+ });
+
+ const headers = findHeaders();
+ expect(headers).toHaveLength(6);
+ [
+ __('Token name'),
+ __('Scopes'),
+ s__('AccessTokens|Created'),
+ __('Last Used'),
+ __('Expires'),
+ __('Role'),
+ ].forEach((text, index) => {
+ expect(headers.at(index).text()).toBe(text);
+ });
+ });
});
it.each([{ revoke_path: null }, { revoke_path: undefined }])(
- 'with %p, does not show revoke button',
- async (input) => {
- await triggerSuccess(defaultActiveAccessTokens.map((data) => ({ ...data, ...input })));
-
+ '%p in some tokens, does not show revoke button',
+ (input) => {
+ createComponent({
+ initialActiveAccessTokens: [
+ defaultActiveAccessTokens.map((data) => ({ ...data, ...input }))[0],
+ defaultActiveAccessTokens[1],
+ ],
+ showRole: true,
+ });
+
+ expect(findHeaders().at(6).text()).toBe(__('Action'));
expect(findCells().at(6).findComponent(GlButton).exists()).toBe(false);
},
);
@@ -207,7 +238,6 @@ describe('~/access_tokens/components/access_token_table_app', () => {
it('sorts rows alphabetically', async () => {
createComponent({ showRole: true });
- await triggerSuccess();
const cells = findCells();
@@ -226,7 +256,6 @@ describe('~/access_tokens/components/access_token_table_app', () => {
it('sorts rows by date', async () => {
createComponent({ showRole: true });
- await triggerSuccess();
const cells = findCells();
@@ -242,14 +271,20 @@ describe('~/access_tokens/components/access_token_table_app', () => {
expect(cells.at(10).text()).toBe('Never');
});
- it('should show the pagination component when needed', async () => {
- createComponent();
- expect(findPagination().exists()).toBe(false);
+ describe('pagination', () => {
+ it('does not show pagination component', () => {
+ createComponent({
+ initialActiveAccessTokens: Array(PAGE_SIZE).fill(defaultActiveAccessTokens[0]),
+ });
- await triggerSuccess(Array(PAGE_SIZE).fill(defaultActiveAccessTokens[0]));
- expect(findPagination().exists()).toBe(false);
+ expect(findPagination().exists()).toBe(false);
+ });
- await triggerSuccess(Array(PAGE_SIZE + 1).fill(defaultActiveAccessTokens[0]));
- expect(findPagination().exists()).toBe(true);
+ it('shows the pagination component', () => {
+ createComponent({
+ initialActiveAccessTokens: Array(PAGE_SIZE + 1).fill(defaultActiveAccessTokens[0]),
+ });
+ expect(findPagination().exists()).toBe(true);
+ });
});
});
diff --git a/spec/frontend/access_tokens/components/new_access_token_app_spec.js b/spec/frontend/access_tokens/components/new_access_token_app_spec.js
index d12d200d214..b4af11169ad 100644
--- a/spec/frontend/access_tokens/components/new_access_token_app_spec.js
+++ b/spec/frontend/access_tokens/components/new_access_token_app_spec.js
@@ -22,6 +22,8 @@ describe('~/access_tokens/components/new_access_token_app', () => {
});
};
+ const findButtonEl = () => document.querySelector('[type=submit]');
+
const triggerSuccess = async (newToken = 'new token') => {
wrapper
.findComponent(DomElementListener)
@@ -41,7 +43,7 @@ describe('~/access_tokens/components/new_access_token_app', () => {
<input type="text" id="expires_at" value="2022-01-01"/>
<input type="text" value='1'/>
<input type="checkbox" checked/>
- <input type="submit" value="Create"/>
+ <button type="submit" value="Create" class="disabled" disabled="disabled"/>
</form>`,
);
@@ -120,10 +122,10 @@ describe('~/access_tokens/components/new_access_token_app', () => {
});
it('should not reset the submit button value', async () => {
- expect(document.querySelector('input[type=submit]').value).toBe('Create');
+ expect(findButtonEl().value).toBe('Create');
await triggerSuccess();
- expect(document.querySelector('input[type=submit]').value).toBe('Create');
+ expect(findButtonEl().value).toBe('Create');
});
});
});
@@ -162,6 +164,17 @@ describe('~/access_tokens/components/new_access_token_app', () => {
expect(wrapper.findComponent(GlAlert).exists()).toBe(false);
});
+
+ it('should enable the submit button', async () => {
+ const button = findButtonEl();
+ expect(button).toBeDisabled();
+ expect(button.className).toBe('disabled');
+
+ await triggerError();
+
+ expect(button).not.toBeDisabled();
+ expect(button.className).toBe('');
+ });
});
describe('before error or success', () => {
diff --git a/spec/frontend/access_tokens/index_spec.js b/spec/frontend/access_tokens/index_spec.js
index 55575ab25fc..1157e44f41a 100644
--- a/spec/frontend/access_tokens/index_spec.js
+++ b/spec/frontend/access_tokens/index_spec.js
@@ -1,7 +1,4 @@
-/* eslint-disable vue/require-prop-types */
-/* eslint-disable vue/one-component-per-file */
import { createWrapper } from '@vue/test-utils';
-import Vue from 'vue';
import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import {
@@ -10,10 +7,11 @@ import {
initNewAccessTokenApp,
initTokensApp,
} from '~/access_tokens';
-import * as AccessTokenTableApp from '~/access_tokens/components/access_token_table_app.vue';
+import AccessTokenTableApp from '~/access_tokens/components/access_token_table_app.vue';
import ExpiresAtField from '~/access_tokens/components/expires_at_field.vue';
-import * as NewAccessTokenApp from '~/access_tokens/components/new_access_token_app.vue';
-import * as TokensApp from '~/access_tokens/components/tokens_app.vue';
+import NewAccessTokenApp from '~/access_tokens/components/new_access_token_app.vue';
+import TokensApp from '~/access_tokens/components/tokens_app.vue';
+import { FORM_SELECTOR } from '~/access_tokens/components/constants';
import { FEED_TOKEN, INCOMING_EMAIL_TOKEN, STATIC_OBJECT_TOKEN } from '~/access_tokens/constants';
import { __, sprintf } from '~/locale';
@@ -28,26 +26,7 @@ describe('access tokens', () => {
describe('initAccessTokenTableApp', () => {
const accessTokenType = 'personal access token';
const accessTokenTypePlural = 'personal access tokens';
- const initialActiveAccessTokens = [{ id: '1' }];
-
- const FakeAccessTokenTableApp = Vue.component('FakeComponent', {
- inject: [
- 'accessTokenType',
- 'accessTokenTypePlural',
- 'initialActiveAccessTokens',
- 'noActiveTokensMessage',
- 'showRole',
- ],
- props: [
- 'accessTokenType',
- 'accessTokenTypePlural',
- 'initialActiveAccessTokens',
- 'noActiveTokensMessage',
- 'showRole',
- ],
- render: () => null,
- });
- AccessTokenTableApp.default = FakeAccessTokenTableApp;
+ const initialActiveAccessTokens = [{ revoked_path: '1' }];
it('mounts the component and provides required values', () => {
setHTMLFixture(
@@ -60,19 +39,18 @@ describe('access tokens', () => {
);
const vueInstance = initAccessTokenTableApp();
-
wrapper = createWrapper(vueInstance);
- const component = wrapper.findComponent(FakeAccessTokenTableApp);
+ const component = wrapper.findComponent({ name: 'AccessTokenTableRoot' });
expect(component.exists()).toBe(true);
-
- expect(component.props()).toMatchObject({
+ expect(wrapper.findComponent(AccessTokenTableApp).vm).toMatchObject({
// Required value
accessTokenType,
accessTokenTypePlural,
initialActiveAccessTokens,
// Default values
+ information: undefined,
noActiveTokensMessage: sprintf(__('This user has no active %{accessTokenTypePlural}.'), {
accessTokenTypePlural,
}),
@@ -81,12 +59,14 @@ describe('access tokens', () => {
});
it('mounts the component and provides all values', () => {
+ const information = 'Additional information';
const noActiveTokensMessage = 'This group has no active access tokens.';
setHTMLFixture(
`<div id="js-access-token-table-app"
data-access-token-type="${accessTokenType}"
data-access-token-type-plural="${accessTokenTypePlural}"
data-initial-active-access-tokens=${JSON.stringify(initialActiveAccessTokens)}
+ data-information="${information}"
data-no-active-tokens-message="${noActiveTokensMessage}"
data-show-role
>
@@ -94,15 +74,15 @@ describe('access tokens', () => {
);
const vueInstance = initAccessTokenTableApp();
-
wrapper = createWrapper(vueInstance);
- const component = wrapper.findComponent(FakeAccessTokenTableApp);
+ const component = wrapper.findComponent({ name: 'AccessTokenTableRoot' });
expect(component.exists()).toBe(true);
- expect(component.props()).toMatchObject({
+ expect(component.findComponent(AccessTokenTableApp).vm).toMatchObject({
accessTokenType,
accessTokenTypePlural,
initialActiveAccessTokens,
+ information,
noActiveTokensMessage,
showRole: true,
});
@@ -157,23 +137,16 @@ describe('access tokens', () => {
it('mounts the component and sets `accessTokenType` prop', () => {
const accessTokenType = 'personal access token';
setHTMLFixture(
- `<div id="js-new-access-token-app" data-access-token-type="${accessTokenType}"></div>`,
+ `<div id="js-new-access-token-app" data-access-token-type="${accessTokenType}"></div>
+ <form id="${FORM_SELECTOR.slice(1)}"></form>`,
);
- const FakeNewAccessTokenApp = Vue.component('FakeComponent', {
- inject: ['accessTokenType'],
- props: ['accessTokenType'],
- render: () => null,
- });
- NewAccessTokenApp.default = FakeNewAccessTokenApp;
-
const vueInstance = initNewAccessTokenApp();
-
wrapper = createWrapper(vueInstance);
- const component = wrapper.findComponent(FakeNewAccessTokenApp);
+ const component = wrapper.findComponent({ name: 'NewAccessTokenRoot' });
expect(component.exists()).toBe(true);
- expect(component.props('accessTokenType')).toEqual(accessTokenType);
+ expect(component.findComponent(NewAccessTokenApp).vm).toMatchObject({ accessTokenType });
});
it('returns `null`', () => {
@@ -192,20 +165,12 @@ describe('access tokens', () => {
`<div id="js-tokens-app" data-tokens-data=${JSON.stringify(tokensData)}></div>`,
);
- const FakeTokensApp = Vue.component('FakeComponent', {
- inject: ['tokenTypes'],
- props: ['tokenTypes'],
- render: () => null,
- });
- TokensApp.default = FakeTokensApp;
-
const vueInstance = initTokensApp();
-
wrapper = createWrapper(vueInstance);
- const component = wrapper.findComponent(FakeTokensApp);
+ const component = wrapper.findComponent(TokensApp);
expect(component.exists()).toBe(true);
- expect(component.props('tokenTypes')).toEqual(tokensData);
+ expect(component.vm).toMatchObject({ tokenTypes: tokensData });
});
it('returns `null`', () => {
diff --git a/spec/frontend/admin/broadcast_messages/components/base_spec.js b/spec/frontend/admin/broadcast_messages/components/base_spec.js
new file mode 100644
index 00000000000..020e1c1d7c1
--- /dev/null
+++ b/spec/frontend/admin/broadcast_messages/components/base_spec.js
@@ -0,0 +1,112 @@
+import { shallowMount } from '@vue/test-utils';
+import { GlPagination } from '@gitlab/ui';
+import AxiosMockAdapter from 'axios-mock-adapter';
+import { TEST_HOST } from 'helpers/test_constants';
+import waitForPromises from 'helpers/wait_for_promises';
+import { useMockLocationHelper } from 'helpers/mock_window_location_helper';
+import { createAlert } from '~/flash';
+import axios from '~/lib/utils/axios_utils';
+import { redirectTo } from '~/lib/utils/url_utility';
+import BroadcastMessagesBase from '~/admin/broadcast_messages/components/base.vue';
+import MessagesTable from '~/admin/broadcast_messages/components/messages_table.vue';
+import { generateMockMessages, MOCK_MESSAGES } from '../mock_data';
+
+jest.mock('~/flash');
+jest.mock('~/lib/utils/url_utility');
+
+describe('BroadcastMessagesBase', () => {
+ let wrapper;
+ let axiosMock;
+
+ useMockLocationHelper();
+
+ const findTable = () => wrapper.findComponent(MessagesTable);
+ const findPagination = () => wrapper.findComponent(GlPagination);
+
+ function createComponent(props = {}) {
+ wrapper = shallowMount(BroadcastMessagesBase, {
+ propsData: {
+ page: 1,
+ messagesCount: MOCK_MESSAGES.length,
+ messages: MOCK_MESSAGES,
+ ...props,
+ },
+ });
+ }
+
+ beforeEach(() => {
+ axiosMock = new AxiosMockAdapter(axios);
+ });
+
+ afterEach(() => {
+ axiosMock.restore();
+ wrapper.destroy();
+ });
+
+ it('renders the table and pagination when there are existing messages', () => {
+ createComponent();
+
+ expect(findTable().exists()).toBe(true);
+ expect(findPagination().exists()).toBe(true);
+ });
+
+ it('does not render the table when there are no visible messages', () => {
+ createComponent({ messages: [] });
+
+ expect(findTable().exists()).toBe(false);
+ expect(findPagination().exists()).toBe(true);
+ });
+
+ it('does not remove a deleted message if it was not in visibleMessages', async () => {
+ createComponent();
+
+ findTable().vm.$emit('delete-message', -1);
+ await waitForPromises();
+
+ expect(axiosMock.history.delete).toHaveLength(0);
+ expect(wrapper.vm.visibleMessages.length).toBe(MOCK_MESSAGES.length);
+ });
+
+ it('does not remove a deleted message if the request fails', async () => {
+ createComponent();
+ const { id, delete_path } = MOCK_MESSAGES[0];
+ axiosMock.onDelete(delete_path).replyOnce(500);
+
+ findTable().vm.$emit('delete-message', id);
+ await waitForPromises();
+
+ expect(wrapper.vm.visibleMessages.find((m) => m.id === id)).not.toBeUndefined();
+ expect(createAlert).toHaveBeenCalledWith(
+ expect.objectContaining({
+ message: BroadcastMessagesBase.i18n.deleteError,
+ }),
+ );
+ });
+
+ it('removes a deleted message from visibleMessages on success', async () => {
+ createComponent();
+ const { id, delete_path } = MOCK_MESSAGES[0];
+ axiosMock.onDelete(delete_path).replyOnce(200);
+
+ findTable().vm.$emit('delete-message', id);
+ await waitForPromises();
+
+ expect(wrapper.vm.visibleMessages.find((m) => m.id === id)).toBeUndefined();
+ expect(wrapper.vm.totalMessages).toBe(MOCK_MESSAGES.length - 1);
+ });
+
+ it('redirects to the first page when totalMessages changes from 21 to 20', async () => {
+ window.location.pathname = `${TEST_HOST}/admin/broadcast_messages`;
+
+ const messages = generateMockMessages(21);
+ const { id, delete_path } = messages[0];
+ createComponent({ messages, messagesCount: messages.length });
+
+ axiosMock.onDelete(delete_path).replyOnce(200);
+
+ findTable().vm.$emit('delete-message', id);
+ await waitForPromises();
+
+ expect(redirectTo).toHaveBeenCalledWith(`${TEST_HOST}/admin/broadcast_messages?page=1`);
+ });
+});
diff --git a/spec/frontend/admin/broadcast_messages/components/messages_table_spec.js b/spec/frontend/admin/broadcast_messages/components/messages_table_spec.js
new file mode 100644
index 00000000000..349fab03853
--- /dev/null
+++ b/spec/frontend/admin/broadcast_messages/components/messages_table_spec.js
@@ -0,0 +1,51 @@
+import { mount } from '@vue/test-utils';
+import MessagesTable from '~/admin/broadcast_messages/components/messages_table.vue';
+import { MOCK_MESSAGES } from '../mock_data';
+
+describe('MessagesTable', () => {
+ let wrapper;
+
+ const findRows = () => wrapper.findAll('[data-testid="message-row"]');
+ const findTargetRoles = () => wrapper.find('[data-testid="target-roles-th"]');
+ const findDeleteButton = (id) => wrapper.find(`[data-testid="delete-message-${id}"]`);
+
+ function createComponent(props = {}, glFeatures = {}) {
+ wrapper = mount(MessagesTable, {
+ provide: {
+ glFeatures,
+ },
+ propsData: {
+ messages: MOCK_MESSAGES,
+ ...props,
+ },
+ });
+ }
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('renders a table row for each message', () => {
+ createComponent();
+
+ expect(findRows()).toHaveLength(MOCK_MESSAGES.length);
+ });
+
+ it('renders the "Target Roles" column when roleTargetedBroadcastMessages is enabled', () => {
+ createComponent({}, { roleTargetedBroadcastMessages: true });
+ expect(findTargetRoles().exists()).toBe(true);
+ });
+
+ it('does not render the "Target Roles" column when roleTargetedBroadcastMessages is disabled', () => {
+ createComponent();
+ expect(findTargetRoles().exists()).toBe(false);
+ });
+
+ it('emits a delete-message event when a delete button is clicked', () => {
+ const { id } = MOCK_MESSAGES[0];
+ createComponent();
+ findDeleteButton(id).element.click();
+ expect(wrapper.emitted('delete-message')).toHaveLength(1);
+ expect(wrapper.emitted('delete-message')[0]).toEqual([id]);
+ });
+});
diff --git a/spec/frontend/admin/broadcast_messages/mock_data.js b/spec/frontend/admin/broadcast_messages/mock_data.js
new file mode 100644
index 00000000000..8dd98c2319d
--- /dev/null
+++ b/spec/frontend/admin/broadcast_messages/mock_data.js
@@ -0,0 +1,17 @@
+const generateMockMessage = (id) => ({
+ id,
+ delete_path: `/admin/broadcast_messages/${id}.js`,
+ edit_path: `/admin/broadcast_messages/${id}/edit`,
+ starts_at: new Date().toISOString(),
+ ends_at: new Date().toISOString(),
+ preview: '<div>YEET</div>',
+ status: 'Expired',
+ target_path: '*/welcome',
+ target_roles: 'Maintainer, Owner',
+ type: 'Banner',
+});
+
+export const generateMockMessages = (n) =>
+ [...Array(n).keys()].map((id) => generateMockMessage(id + 1));
+
+export const MOCK_MESSAGES = generateMockMessages(5).map((id) => generateMockMessage(id));
diff --git a/spec/frontend/admin/deploy_keys/components/table_spec.js b/spec/frontend/admin/deploy_keys/components/table_spec.js
index a18506c0916..4d4a2caedde 100644
--- a/spec/frontend/admin/deploy_keys/components/table_spec.js
+++ b/spec/frontend/admin/deploy_keys/components/table_spec.js
@@ -9,7 +9,7 @@ import { stubComponent } from 'helpers/stub_component';
import DeployKeysTable from '~/admin/deploy_keys/components/table.vue';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import Api, { DEFAULT_PER_PAGE } from '~/api';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
jest.mock('~/api');
jest.mock('~/flash');
@@ -243,7 +243,7 @@ describe('DeployKeysTable', () => {
itRendersTheEmptyState();
it('displays flash', () => {
- expect(createFlash).toHaveBeenCalledWith({
+ expect(createAlert).toHaveBeenCalledWith({
message: DeployKeysTable.i18n.apiErrorMessage,
captureError: true,
error,
diff --git a/spec/frontend/admin/users/components/users_table_spec.js b/spec/frontend/admin/users/components/users_table_spec.js
index fe07f0fce00..a0aec347b6b 100644
--- a/spec/frontend/admin/users/components/users_table_spec.js
+++ b/spec/frontend/admin/users/components/users_table_spec.js
@@ -10,7 +10,7 @@ import AdminUserActions from '~/admin/users/components/user_actions.vue';
import AdminUserAvatar from '~/admin/users/components/user_avatar.vue';
import AdminUsersTable from '~/admin/users/components/users_table.vue';
import getUsersGroupCountsQuery from '~/admin/users/graphql/queries/get_users_group_counts.query.graphql';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import AdminUserDate from '~/vue_shared/components/user_date.vue';
import { users, paths, createGroupCountResponse } from '../mock_data';
@@ -135,7 +135,7 @@ describe('AdminUsersTable component', () => {
});
it('creates a flash message and captures the error', () => {
- expect(createFlash).toHaveBeenCalledWith({
+ expect(createAlert).toHaveBeenCalledWith({
message: 'Could not load user group counts. Please refresh the page to try again.',
captureError: true,
error: expect.any(Error),
diff --git a/spec/frontend/alert_management/components/alert_management_table_spec.js b/spec/frontend/alert_management/components/alert_management_table_spec.js
index 3e1438c37d6..7fb4f2d2463 100644
--- a/spec/frontend/alert_management/components/alert_management_table_spec.js
+++ b/spec/frontend/alert_management/components/alert_management_table_spec.js
@@ -1,4 +1,4 @@
-import { GlTable, GlAlert, GlLoadingIcon, GlDropdown, GlIcon, GlAvatar } from '@gitlab/ui';
+import { GlTable, GlAlert, GlLoadingIcon, GlDropdown, GlIcon, GlAvatar, GlLink } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
@@ -31,6 +31,7 @@ describe('AlertManagementTable', () => {
const findSearch = () => wrapper.findComponent(FilteredSearchBar);
const findSeverityColumnHeader = () => wrapper.findByTestId('alert-management-severity-sort');
const findFirstIDField = () => wrapper.findAllByTestId('idField').at(0);
+ const findFirstIDLink = () => wrapper.findAllByTestId('idField').at(0).findComponent(GlLink);
const findAssignees = () => wrapper.findAllByTestId('assigneesField');
const findSeverityFields = () => wrapper.findAllByTestId('severityField');
const findIssueFields = () => wrapper.findAllByTestId('issueField');
@@ -135,10 +136,11 @@ describe('AlertManagementTable', () => {
expect(findLoader().exists()).toBe(false);
expect(findAlertsTable().exists()).toBe(true);
expect(findAlerts()).toHaveLength(mockAlerts.length);
- expect(findAlerts().at(0).classes()).toContain('gl-hover-bg-blue-50');
+ expect(findAlerts().at(0).classes()).toContain('gl-hover-bg-gray-50');
+ expect(findAlerts().at(0).classes()).not.toContain('gl-hover-border-blue-200');
});
- it('displays the alert ID and title formatted correctly', () => {
+ it('displays the alert ID and title as a link', () => {
mountComponent({
data: { alerts: { list: mockAlerts }, alertsCount, errored: false },
loading: false,
@@ -146,6 +148,8 @@ describe('AlertManagementTable', () => {
expect(findFirstIDField().exists()).toBe(true);
expect(findFirstIDField().text()).toBe(`#${mockAlerts[0].iid} ${mockAlerts[0].title}`);
+ expect(findFirstIDLink().text()).toBe(`#${mockAlerts[0].iid} ${mockAlerts[0].title}`);
+ expect(findFirstIDLink().attributes('href')).toBe('/1527542/details');
});
it('displays status dropdown', () => {
@@ -266,7 +270,8 @@ describe('AlertManagementTable', () => {
alerts: {
list: [
{
- iid: 1,
+ iid: '1',
+ title: 'SyntaxError: Invalid or unexpected token',
status: 'acknowledged',
startedAt: '2020-03-17T23:18:14.996Z',
severity: 'high',
diff --git a/spec/frontend/alerts_settings/components/alerts_settings_form_spec.js b/spec/frontend/alerts_settings/components/alerts_settings_form_spec.js
index fb9e97e7505..e0075aa71d9 100644
--- a/spec/frontend/alerts_settings/components/alerts_settings_form_spec.js
+++ b/spec/frontend/alerts_settings/components/alerts_settings_form_spec.js
@@ -304,12 +304,12 @@ describe('AlertsSettingsForm', () => {
});
describe.each`
- payload | resetPayloadAndMappingConfirmed | disabled
- ${validSamplePayload} | ${true} | ${undefined}
- ${emptySamplePayload} | ${true} | ${undefined}
- ${validSamplePayload} | ${false} | ${'disabled'}
- ${emptySamplePayload} | ${false} | ${undefined}
- `('', ({ payload, resetPayloadAndMappingConfirmed, disabled }) => {
+ context | payload | resetPayloadAndMappingConfirmed | disabled
+ ${'valid payload, confirmed and enabled'} | ${validSamplePayload} | ${true} | ${undefined}
+ ${'empty payload, confirmed and enabled'} | ${emptySamplePayload} | ${true} | ${undefined}
+ ${'valid payload, unconfirmed and disabled'} | ${validSamplePayload} | ${false} | ${'disabled'}
+ ${'empty payload, unconfirmed and enabled'} | ${emptySamplePayload} | ${false} | ${undefined}
+ `('given $context', ({ payload, resetPayloadAndMappingConfirmed, disabled }) => {
const payloadResetMsg = resetPayloadAndMappingConfirmed
? 'was confirmed'
: 'was not confirmed';
@@ -333,12 +333,12 @@ describe('AlertsSettingsForm', () => {
describe('action buttons for sample payload', () => {
describe.each`
- resetPayloadAndMappingConfirmed | payloadExample | caption
- ${false} | ${validSamplePayload} | ${'Edit payload'}
- ${true} | ${emptySamplePayload} | ${'Parse payload fields'}
- ${true} | ${validSamplePayload} | ${'Parse payload fields'}
- ${false} | ${emptySamplePayload} | ${'Parse payload fields'}
- `('', ({ resetPayloadAndMappingConfirmed, payloadExample, caption }) => {
+ context | resetPayloadAndMappingConfirmed | payloadExample | caption
+ ${'valid payload, unconfirmed'} | ${false} | ${validSamplePayload} | ${'Edit payload'}
+ ${'empty payload, confirmed'} | ${true} | ${emptySamplePayload} | ${'Parse payload fields'}
+ ${'valid payload, confirmed'} | ${true} | ${validSamplePayload} | ${'Parse payload fields'}
+ ${'empty payload, unconfirmed'} | ${false} | ${emptySamplePayload} | ${'Parse payload fields'}
+ `('given $context', ({ resetPayloadAndMappingConfirmed, payloadExample, caption }) => {
const samplePayloadMsg = payloadExample ? 'was provided' : 'was not provided';
const payloadResetMsg = resetPayloadAndMappingConfirmed
? 'was confirmed'
@@ -402,24 +402,27 @@ describe('AlertsSettingsForm', () => {
${true} | ${true} | ${2} | ${false}
${true} | ${false} | ${1} | ${false}
${false} | ${true} | ${1} | ${false}
- `('', ({ alertFieldsProvided, multiIntegrations, integrationOption, visible }) => {
- const visibleMsg = visible ? 'rendered' : 'not rendered';
- const alertFieldsMsg = alertFieldsProvided ? 'provided' : 'not provided';
- const integrationType = integrationOption === 1 ? typeSet.http : typeSet.prometheus;
- const multiIntegrationsEnabled = multiIntegrations ? 'enabled' : 'not enabled';
+ `(
+ 'given alertFieldsProvided: $alertFieldsProvided, multiIntegrations: $multiIntegrations, integrationOption: $integrationOption, visible: $visible',
+ ({ alertFieldsProvided, multiIntegrations, integrationOption, visible }) => {
+ const visibleMsg = visible ? 'rendered' : 'not rendered';
+ const alertFieldsMsg = alertFieldsProvided ? 'provided' : 'not provided';
+ const integrationType = integrationOption === 1 ? typeSet.http : typeSet.prometheus;
+ const multiIntegrationsEnabled = multiIntegrations ? 'enabled' : 'not enabled';
+
+ it(`is ${visibleMsg} when multiIntegrations are ${multiIntegrationsEnabled}, integration type is ${integrationType} and alert fields are ${alertFieldsMsg}`, async () => {
+ createComponent({
+ multiIntegrations,
+ props: {
+ alertFields: alertFieldsProvided ? alertFields : [],
+ },
+ });
+ await selectOptionAtIndex(integrationOption);
- it(`is ${visibleMsg} when multiIntegrations are ${multiIntegrationsEnabled}, integration type is ${integrationType} and alert fields are ${alertFieldsMsg}`, async () => {
- createComponent({
- multiIntegrations,
- props: {
- alertFields: alertFieldsProvided ? alertFields : [],
- },
+ expect(findMappingBuilder().exists()).toBe(visible);
});
- await selectOptionAtIndex(integrationOption);
-
- expect(findMappingBuilder().exists()).toBe(visible);
- });
- });
+ },
+ );
});
describe('Form validation', () => {
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 0266adeb6c7..fcefcb7cf66 100644
--- a/spec/frontend/alerts_settings/components/alerts_settings_wrapper_spec.js
+++ b/spec/frontend/alerts_settings/components/alerts_settings_wrapper_spec.js
@@ -30,7 +30,7 @@ import {
INTEGRATION_INACTIVE_PAYLOAD_TEST_ERROR,
DELETE_INTEGRATION_ERROR,
} from '~/alerts_settings/utils/error_messages';
-import createFlash, { FLASH_TYPES } from '~/flash';
+import { createAlert, VARIANT_SUCCESS } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import httpStatusCodes from '~/lib/utils/http_status';
import {
@@ -327,7 +327,7 @@ describe('AlertsSettingsWrapper', () => {
await waitForPromises();
- expect(createFlash).toHaveBeenCalledWith({ message: ADD_INTEGRATION_ERROR });
+ expect(createAlert).toHaveBeenCalledWith({ message: ADD_INTEGRATION_ERROR });
});
it('shows an error alert when integration token reset fails', async () => {
@@ -336,7 +336,7 @@ describe('AlertsSettingsWrapper', () => {
findAlertsSettingsForm().vm.$emit('reset-token', {});
await waitForPromises();
- expect(createFlash).toHaveBeenCalledWith({ message: RESET_INTEGRATION_TOKEN_ERROR });
+ expect(createAlert).toHaveBeenCalledWith({ message: RESET_INTEGRATION_TOKEN_ERROR });
});
it('shows an error alert when integration update fails', async () => {
@@ -345,7 +345,7 @@ describe('AlertsSettingsWrapper', () => {
findAlertsSettingsForm().vm.$emit('update-integration', {});
await waitForPromises();
- expect(createFlash).toHaveBeenCalledWith({ message: UPDATE_INTEGRATION_ERROR });
+ expect(createAlert).toHaveBeenCalledWith({ message: UPDATE_INTEGRATION_ERROR });
});
describe('Test alert failure', () => {
@@ -360,17 +360,17 @@ describe('AlertsSettingsWrapper', () => {
it('shows an error alert when integration test payload is invalid', async () => {
mock.onPost(/(.*)/).replyOnce(httpStatusCodes.UNPROCESSABLE_ENTITY);
await wrapper.vm.testAlertPayload({ endpoint: '', data: '', token: '' });
- expect(createFlash).toHaveBeenCalledWith({ message: INTEGRATION_PAYLOAD_TEST_ERROR });
- expect(createFlash).toHaveBeenCalledTimes(1);
+ expect(createAlert).toHaveBeenCalledWith({ message: INTEGRATION_PAYLOAD_TEST_ERROR });
+ expect(createAlert).toHaveBeenCalledTimes(1);
});
it('shows an error alert when integration is not activated', async () => {
mock.onPost(/(.*)/).replyOnce(httpStatusCodes.FORBIDDEN);
await wrapper.vm.testAlertPayload({ endpoint: '', data: '', token: '' });
- expect(createFlash).toHaveBeenCalledWith({
+ expect(createAlert).toHaveBeenCalledWith({
message: INTEGRATION_INACTIVE_PAYLOAD_TEST_ERROR,
});
- expect(createFlash).toHaveBeenCalledTimes(1);
+ expect(createAlert).toHaveBeenCalledTimes(1);
});
});
@@ -444,9 +444,9 @@ describe('AlertsSettingsWrapper', () => {
jest.spyOn(alertsUpdateService, 'updateTestAlert').mockResolvedValueOnce({});
findAlertsSettingsForm().vm.$emit('test-alert-payload', '');
await waitForPromises();
- expect(createFlash).toHaveBeenCalledWith({
+ expect(createAlert).toHaveBeenCalledWith({
message: i18n.alertSent,
- type: FLASH_TYPES.SUCCESS,
+ variant: VARIANT_SUCCESS,
});
});
@@ -454,7 +454,7 @@ describe('AlertsSettingsWrapper', () => {
jest.spyOn(alertsUpdateService, 'updateTestAlert').mockRejectedValueOnce({});
findAlertsSettingsForm().vm.$emit('test-alert-payload', '');
await waitForPromises();
- expect(createFlash).toHaveBeenCalledWith({
+ expect(createAlert).toHaveBeenCalledWith({
message: INTEGRATION_PAYLOAD_TEST_ERROR,
});
});
@@ -486,7 +486,7 @@ describe('AlertsSettingsWrapper', () => {
await destroyHttpIntegration(wrapper);
await waitForPromises();
- expect(createFlash).toHaveBeenCalledWith({ message: 'Houston, we have a problem' });
+ expect(createAlert).toHaveBeenCalledWith({ message: 'Houston, we have a problem' });
});
it('displays flash if mutation had a non-recoverable error', async () => {
@@ -497,7 +497,7 @@ describe('AlertsSettingsWrapper', () => {
await destroyHttpIntegration(wrapper);
await waitForPromises();
- expect(createFlash).toHaveBeenCalledWith({
+ expect(createAlert).toHaveBeenCalledWith({
message: DELETE_INTEGRATION_ERROR,
});
});
diff --git a/spec/frontend/api/projects_api_spec.js b/spec/frontend/api/projects_api_spec.js
index 8f40b557e1f..8459021421f 100644
--- a/spec/frontend/api/projects_api_spec.js
+++ b/spec/frontend/api/projects_api_spec.js
@@ -1,5 +1,7 @@
import MockAdapter from 'axios-mock-adapter';
+import getTransferLocationsResponse from 'test_fixtures/api/projects/transfer_locations_page_1.json';
import * as projectsApi from '~/api/projects_api';
+import { DEFAULT_PER_PAGE } from '~/api';
import axios from '~/lib/utils/axios_utils';
describe('~/api/projects_api.js', () => {
@@ -59,4 +61,25 @@ describe('~/api/projects_api.js', () => {
});
});
});
+
+ describe('getTransferLocations', () => {
+ beforeEach(() => {
+ jest.spyOn(axios, 'get');
+ });
+
+ it('retrieves transfer locations from the correct URL and returns them in the response data', async () => {
+ const params = { page: 1 };
+ const expectedUrl = '/api/v7/projects/1/transfer_locations';
+
+ mock.onGet(expectedUrl).replyOnce(200, { data: getTransferLocationsResponse });
+
+ await expect(projectsApi.getTransferLocations(projectId, params)).resolves.toMatchObject({
+ data: { data: getTransferLocationsResponse },
+ });
+
+ expect(axios.get).toHaveBeenCalledWith(expectedUrl, {
+ params: { ...params, per_page: DEFAULT_PER_PAGE },
+ });
+ });
+ });
});
diff --git a/spec/frontend/awards_handler_spec.js b/spec/frontend/awards_handler_spec.js
index b14bc5122b9..1a54b9909ba 100644
--- a/spec/frontend/awards_handler_spec.js
+++ b/spec/frontend/awards_handler_spec.js
@@ -185,7 +185,9 @@ describe('AwardsHandler', () => {
describe('::getAwardUrl', () => {
it('returns the url for request', () => {
- expect(awardsHandler.getAwardUrl()).toBe('http://test.host/-/snippets/1/toggle_award_emoji');
+ expect(awardsHandler.getAwardUrl()).toBe(
+ document.querySelector('.js-awards-block').dataset.awardUrl,
+ );
});
});
diff --git a/spec/frontend/badges/components/badge_form_spec.js b/spec/frontend/badges/components/badge_form_spec.js
index 6d8a00eb50b..0a736df7075 100644
--- a/spec/frontend/badges/components/badge_form_spec.js
+++ b/spec/frontend/badges/components/badge_form_spec.js
@@ -1,195 +1,183 @@
import MockAdapter from 'axios-mock-adapter';
-import Vue, { nextTick } from 'vue';
-import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
+import Vue from 'vue';
+import Vuex from 'vuex';
+import { mount } from '@vue/test-utils';
import { DUMMY_IMAGE_URL, TEST_HOST } from 'helpers/test_constants';
-import { mountComponentWithStore } from 'helpers/vue_mount_component_helper';
import BadgeForm from '~/badges/components/badge_form.vue';
import createEmptyBadge from '~/badges/empty_badge';
-import store from '~/badges/store';
+
+import createState from '~/badges/store/state';
+import mutations from '~/badges/store/mutations';
+import actions from '~/badges/store/actions';
+
import axios from '~/lib/utils/axios_utils';
-// avoid preview background process
-BadgeForm.methods.debouncedPreview = () => {};
+Vue.use(Vuex);
describe('BadgeForm component', () => {
- const Component = Vue.extend(BadgeForm);
let axiosMock;
- let vm;
+ let mockedActions;
+ let wrapper;
+
+ const createComponent = (propsData, customState = {}) => {
+ mockedActions = Object.fromEntries(Object.keys(actions).map((name) => [name, jest.fn()]));
+
+ const store = new Vuex.Store({
+ state: {
+ ...createState(),
+ ...customState,
+ },
+ mutations,
+ actions: mockedActions,
+ });
- beforeEach(() => {
- setHTMLFixture(`
- <div id="dummy-element"></div>
- `);
+ wrapper = mount(BadgeForm, {
+ store,
+ propsData,
+ attachTo: document.body,
+ });
+ };
+ beforeEach(() => {
axiosMock = new MockAdapter(axios);
});
afterEach(() => {
- vm.$destroy();
+ wrapper.destroy();
axiosMock.restore();
- resetHTMLFixture();
});
- describe('methods', () => {
- beforeEach(() => {
- vm = mountComponentWithStore(Component, {
- el: '#dummy-element',
- store,
- props: {
- isEditing: false,
- },
- });
- });
+ it('stops editing when cancel button is clicked', async () => {
+ createComponent({ isEditing: true });
- describe('onCancel', () => {
- it('calls stopEditing', () => {
- jest.spyOn(vm, 'stopEditing').mockImplementation(() => {});
+ const cancelButton = wrapper.find('.row-content-block button');
- vm.onCancel();
+ await cancelButton.trigger('click');
- expect(vm.stopEditing).toHaveBeenCalled();
- });
- });
+ expect(mockedActions.stopEditing).toHaveBeenCalled();
});
- const sharedSubmitTests = (submitAction) => {
+ const sharedSubmitTests = (submitAction, props) => {
const nameSelector = '#badge-name';
const imageUrlSelector = '#badge-image-url';
- const findImageUrlElement = () => vm.$el.querySelector(imageUrlSelector);
+ const findImageUrl = () => wrapper.find(imageUrlSelector);
const linkUrlSelector = '#badge-link-url';
- const findLinkUrlElement = () => vm.$el.querySelector(linkUrlSelector);
+ const findLinkUrl = () => wrapper.find(linkUrlSelector);
const setValue = (inputElementSelector, value) => {
- const inputElement = vm.$el.querySelector(inputElementSelector);
- inputElement.value = value;
- inputElement.dispatchEvent(new Event('input'));
+ const input = wrapper.find(inputElementSelector);
+ return input.setValue(value);
};
const submitForm = () => {
- const submitButton = vm.$el.querySelector('button[type="submit"]');
- submitButton.click();
+ const submitButton = wrapper.find('button[type="submit"]');
+ return submitButton.trigger('click');
};
const expectInvalidInput = (inputElementSelector) => {
- const inputElement = vm.$el.querySelector(inputElementSelector);
+ const input = wrapper.find(inputElementSelector);
- expect(inputElement.checkValidity()).toBe(false);
- const feedbackElement = vm.$el.querySelector(`${inputElementSelector} + .invalid-feedback`);
+ expect(input.element.checkValidity()).toBe(false);
+ const feedbackElement = wrapper.find(`${inputElementSelector} + .invalid-feedback`);
- expect(feedbackElement).toBeVisible();
+ expect(feedbackElement.isVisible()).toBe(true);
};
- beforeEach(async () => {
- jest.spyOn(vm, submitAction).mockReturnValue(Promise.resolve());
- store.replaceState({
- ...store.state,
+ beforeEach(() => {
+ createComponent(props, {
badgeInAddForm: createEmptyBadge(),
badgeInEditForm: createEmptyBadge(),
isSaving: false,
});
- await nextTick();
setValue(nameSelector, 'TestBadge');
setValue(linkUrlSelector, `${TEST_HOST}/link/url`);
setValue(imageUrlSelector, `${window.location.origin}${DUMMY_IMAGE_URL}`);
});
- it('returns immediately if imageUrl is empty', () => {
- setValue(imageUrlSelector, '');
+ it('returns immediately if imageUrl is empty', async () => {
+ await setValue(imageUrlSelector, '');
- submitForm();
+ await submitForm();
expectInvalidInput(imageUrlSelector);
- expect(vm[submitAction]).not.toHaveBeenCalled();
+ expect(mockedActions[submitAction]).not.toHaveBeenCalled();
});
- it('returns immediately if imageUrl is malformed', () => {
- setValue(imageUrlSelector, 'not-a-url');
+ it('returns immediately if imageUrl is malformed', async () => {
+ await setValue(imageUrlSelector, 'not-a-url');
- submitForm();
+ await submitForm();
expectInvalidInput(imageUrlSelector);
- expect(vm[submitAction]).not.toHaveBeenCalled();
+ expect(mockedActions[submitAction]).not.toHaveBeenCalled();
});
- it('returns immediately if linkUrl is empty', () => {
- setValue(linkUrlSelector, '');
+ it('returns immediately if linkUrl is empty', async () => {
+ await setValue(linkUrlSelector, '');
- submitForm();
+ await submitForm();
expectInvalidInput(linkUrlSelector);
- expect(vm[submitAction]).not.toHaveBeenCalled();
+ expect(mockedActions[submitAction]).not.toHaveBeenCalled();
});
- it('returns immediately if linkUrl is malformed', () => {
- setValue(linkUrlSelector, 'not-a-url');
+ it('returns immediately if linkUrl is malformed', async () => {
+ await setValue(linkUrlSelector, 'not-a-url');
- submitForm();
+ await submitForm();
expectInvalidInput(linkUrlSelector);
- expect(vm[submitAction]).not.toHaveBeenCalled();
+ expect(mockedActions[submitAction]).not.toHaveBeenCalled();
});
- it(`calls ${submitAction}`, () => {
- submitForm();
+ it(`calls ${submitAction}`, async () => {
+ await submitForm();
- expect(findImageUrlElement().checkValidity()).toBe(true);
- expect(findLinkUrlElement().checkValidity()).toBe(true);
- expect(vm[submitAction]).toHaveBeenCalled();
+ expect(findImageUrl().element.checkValidity()).toBe(true);
+ expect(findLinkUrl().element.checkValidity()).toBe(true);
+ expect(mockedActions[submitAction]).toHaveBeenCalled();
});
};
describe('if isEditing is false', () => {
- beforeEach(() => {
- vm = mountComponentWithStore(Component, {
- el: '#dummy-element',
- store,
- props: {
- isEditing: false,
- },
- });
- });
+ const props = { isEditing: false };
it('renders one button', () => {
- expect(vm.$el.querySelector('.row-content-block')).toBeNull();
- const buttons = vm.$el.querySelectorAll('.form-group:last-of-type button');
+ createComponent(props);
+
+ expect(wrapper.find('.row-content-block').exists()).toBe(false);
+ const buttons = wrapper.findAll('.form-group:last-of-type button');
- expect(buttons.length).toBe(1);
- const buttonAddElement = buttons[0];
+ expect(buttons).toHaveLength(1);
+ const buttonAddWrapper = buttons.at(0);
- expect(buttonAddElement).toBeVisible();
- expect(buttonAddElement).toHaveText('Add badge');
+ expect(buttonAddWrapper.isVisible()).toBe(true);
+ expect(buttonAddWrapper.text()).toBe('Add badge');
});
- sharedSubmitTests('addBadge');
+ sharedSubmitTests('addBadge', props);
});
describe('if isEditing is true', () => {
- beforeEach(() => {
- vm = mountComponentWithStore(Component, {
- el: '#dummy-element',
- store,
- props: {
- isEditing: true,
- },
- });
- });
+ const props = { isEditing: true };
it('renders two buttons', () => {
- const buttons = vm.$el.querySelectorAll('.row-content-block button');
+ createComponent(props);
+ const buttons = wrapper.findAll('.row-content-block button');
- expect(buttons.length).toBe(2);
- const buttonSaveElement = buttons[1];
+ expect(buttons).toHaveLength(2);
- expect(buttonSaveElement).toBeVisible();
- expect(buttonSaveElement).toHaveText('Save changes');
- const buttonCancelElement = buttons[0];
+ const saveButton = buttons.at(1);
+ expect(saveButton.isVisible()).toBe(true);
+ expect(saveButton.text()).toBe('Save changes');
- expect(buttonCancelElement).toBeVisible();
- expect(buttonCancelElement).toHaveText('Cancel');
+ const cancelButton = buttons.at(0);
+ expect(cancelButton.isVisible()).toBe(true);
+ expect(cancelButton.text()).toBe('Cancel');
});
- sharedSubmitTests('saveBadge');
+ sharedSubmitTests('saveBadge', props);
});
});
diff --git a/spec/frontend/badges/components/badge_list_row_spec.js b/spec/frontend/badges/components/badge_list_row_spec.js
index ad8426f3168..ee7ccac974a 100644
--- a/spec/frontend/badges/components/badge_list_row_spec.js
+++ b/spec/frontend/badges/components/badge_list_row_spec.js
@@ -1,103 +1,118 @@
-import Vue, { nextTick } from 'vue';
+import Vue from 'vue';
+import Vuex from 'vuex';
+import { mount } from '@vue/test-utils';
+
import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
-import { mountComponentWithStore } from 'helpers/vue_mount_component_helper';
import BadgeListRow from '~/badges/components/badge_list_row.vue';
import { GROUP_BADGE, PROJECT_BADGE } from '~/badges/constants';
-import store from '~/badges/store';
+
+import createState from '~/badges/store/state';
+import mutations from '~/badges/store/mutations';
+import actions from '~/badges/store/actions';
+
import { createDummyBadge } from '../dummy_badge';
+Vue.use(Vuex);
+
describe('BadgeListRow component', () => {
- const Component = Vue.extend(BadgeListRow);
let badge;
- let vm;
-
- beforeEach(() => {
- setHTMLFixture(`
- <div id="delete-badge-modal" class="modal"></div>
- <div id="dummy-element"></div>
- `);
- store.replaceState({
- ...store.state,
- kind: PROJECT_BADGE,
+ let wrapper;
+ let mockedActions;
+
+ const createComponent = (kind) => {
+ setHTMLFixture(`<div id="delete-badge-modal" class="modal"></div>`);
+
+ mockedActions = Object.fromEntries(Object.keys(actions).map((name) => [name, jest.fn()]));
+
+ const store = new Vuex.Store({
+ state: {
+ ...createState(),
+ kind: PROJECT_BADGE,
+ },
+ mutations,
+ actions: mockedActions,
});
+
badge = createDummyBadge();
- vm = mountComponentWithStore(Component, {
- el: '#dummy-element',
+ badge.kind = kind;
+ wrapper = mount(BadgeListRow, {
+ attachTo: document.body,
store,
- props: { badge },
+ propsData: { badge },
});
- });
+ };
afterEach(() => {
- vm.$destroy();
+ wrapper.destroy();
resetHTMLFixture();
});
- it('renders the badge', () => {
- const badgeElement = vm.$el.querySelector('.project-badge');
+ describe('for a project badge', () => {
+ beforeEach(() => {
+ createComponent(PROJECT_BADGE);
+ });
- expect(badgeElement).not.toBeNull();
- expect(badgeElement.getAttribute('src')).toBe(badge.renderedImageUrl);
- });
+ it('renders the badge', () => {
+ const badgeImage = wrapper.find('.project-badge');
- it('renders the badge name', () => {
- expect(vm.$el.innerText).toMatch(badge.name);
- });
+ expect(badgeImage.exists()).toBe(true);
+ expect(badgeImage.attributes('src')).toBe(badge.renderedImageUrl);
+ });
- it('renders the badge link', () => {
- expect(vm.$el.innerText).toMatch(badge.linkUrl);
- });
+ it('renders the badge name', () => {
+ expect(wrapper.text()).toMatch(badge.name);
+ });
- it('renders the badge kind', () => {
- expect(vm.$el.innerText).toMatch('Project Badge');
- });
+ it('renders the badge link', () => {
+ expect(wrapper.text()).toMatch(badge.linkUrl);
+ });
- it('shows edit and delete buttons', () => {
- const buttons = vm.$el.querySelectorAll('.table-button-footer button');
+ it('renders the badge kind', () => {
+ expect(wrapper.text()).toMatch('Project Badge');
+ });
- expect(buttons).toHaveLength(2);
- const buttonEditElement = buttons[0];
+ it('shows edit and delete buttons', () => {
+ const buttons = wrapper.findAll('.table-button-footer button');
- expect(buttonEditElement).toBeVisible();
- expect(buttonEditElement).toHaveSpriteIcon('pencil');
- const buttonDeleteElement = buttons[1];
+ expect(buttons).toHaveLength(2);
+ const editButton = buttons.at(0);
- expect(buttonDeleteElement).toBeVisible();
- expect(buttonDeleteElement).toHaveSpriteIcon('remove');
- });
+ expect(editButton.isVisible()).toBe(true);
+ expect(editButton.element).toHaveSpriteIcon('pencil');
- it('calls editBadge when clicking then edit button', () => {
- jest.spyOn(vm, 'editBadge').mockImplementation(() => {});
+ const deleteButton = buttons.at(1);
+ expect(deleteButton.isVisible()).toBe(true);
+ expect(deleteButton.element).toHaveSpriteIcon('remove');
+ });
- const editButton = vm.$el.querySelector('.table-button-footer button:first-of-type');
- editButton.click();
+ it('calls editBadge when clicking then edit button', async () => {
+ const editButton = wrapper.find('.table-button-footer button:first-of-type');
- expect(vm.editBadge).toHaveBeenCalled();
- });
+ await editButton.trigger('click');
+
+ expect(mockedActions.editBadge).toHaveBeenCalled();
+ });
- it('calls updateBadgeInModal and shows modal when clicking then delete button', async () => {
- jest.spyOn(vm, 'updateBadgeInModal').mockImplementation(() => {});
+ it('calls updateBadgeInModal and shows modal when clicking then delete button', async () => {
+ const deleteButton = wrapper.find('.table-button-footer button:last-of-type');
- const deleteButton = vm.$el.querySelector('.table-button-footer button:last-of-type');
- deleteButton.click();
+ await deleteButton.trigger('click');
- await nextTick();
- expect(vm.updateBadgeInModal).toHaveBeenCalled();
+ expect(mockedActions.updateBadgeInModal).toHaveBeenCalled();
+ });
});
describe('for a group badge', () => {
- beforeEach(async () => {
- badge.kind = GROUP_BADGE;
-
- await nextTick();
+ beforeEach(() => {
+ createComponent(GROUP_BADGE);
});
it('renders the badge kind', () => {
- expect(vm.$el.innerText).toMatch('Group Badge');
+ expect(wrapper.text()).toMatch('Group Badge');
});
it('hides edit and delete buttons', () => {
- const buttons = vm.$el.querySelectorAll('.table-button-footer button');
+ const buttons = wrapper.findAll('.table-button-footer button');
expect(buttons).toHaveLength(0);
});
diff --git a/spec/frontend/badges/components/badge_list_spec.js b/spec/frontend/badges/components/badge_list_spec.js
index 32cd9483ef8..606b1bc9cce 100644
--- a/spec/frontend/badges/components/badge_list_spec.js
+++ b/spec/frontend/badges/components/badge_list_spec.js
@@ -1,83 +1,96 @@
-import Vue, { nextTick } from 'vue';
-import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
-import { mountComponentWithStore } from 'helpers/vue_mount_component_helper';
+import Vue from 'vue';
+import Vuex from 'vuex';
+import { mount } from '@vue/test-utils';
+
import BadgeList from '~/badges/components/badge_list.vue';
import { GROUP_BADGE, PROJECT_BADGE } from '~/badges/constants';
-import store from '~/badges/store';
+
+import createState from '~/badges/store/state';
+import mutations from '~/badges/store/mutations';
+import actions from '~/badges/store/actions';
+
import { createDummyBadge } from '../dummy_badge';
-describe('BadgeList component', () => {
- const Component = Vue.extend(BadgeList);
- const numberOfDummyBadges = 3;
- let vm;
-
- beforeEach(() => {
- setHTMLFixture('<div id="dummy-element"></div>');
- const badges = [];
- for (let id = 0; id < numberOfDummyBadges; id += 1) {
- badges.push({ id, ...createDummyBadge() });
- }
- store.replaceState({
- ...store.state,
- badges,
- kind: PROJECT_BADGE,
- isLoading: false,
- });
+Vue.use(Vuex);
- // Can be removed once GlLoadingIcon no longer throws a warning
- jest.spyOn(global.console, 'warn').mockImplementation(() => jest.fn());
+const numberOfDummyBadges = 3;
+const badges = Array.from({ length: numberOfDummyBadges }).map((_, idx) => ({
+ ...createDummyBadge(),
+ id: idx,
+}));
- vm = mountComponentWithStore(Component, {
- el: '#dummy-element',
- store,
+describe('BadgeList component', () => {
+ let wrapper;
+
+ const createComponent = (customState) => {
+ const mockedActions = Object.fromEntries(Object.keys(actions).map((name) => [name, jest.fn()]));
+
+ const store = new Vuex.Store({
+ state: {
+ ...createState(),
+ isLoading: false,
+ ...customState,
+ },
+ mutations,
+ actions: mockedActions,
});
- });
+
+ wrapper = mount(BadgeList, { store });
+ };
afterEach(() => {
- vm.$destroy();
- resetHTMLFixture();
+ wrapper.destroy();
});
- it('renders a header with the badge count', () => {
- const header = vm.$el.querySelector('.card-header');
+ describe('for project badges', () => {
+ it('renders a header with the badge count', () => {
+ createComponent({
+ kind: PROJECT_BADGE,
+ badges,
+ });
- expect(header).toHaveText(new RegExp(`Your badges\\s+${numberOfDummyBadges}`));
- });
+ const header = wrapper.find('.card-header');
- it('renders a row for each badge', () => {
- const rows = vm.$el.querySelectorAll('.gl-responsive-table-row');
+ expect(header.text()).toMatchInterpolatedText('Your badges 3');
+ });
- expect(rows).toHaveLength(numberOfDummyBadges);
- });
+ it('renders a row for each badge', () => {
+ createComponent({
+ kind: PROJECT_BADGE,
+ badges,
+ });
- it('renders a message if no badges exist', async () => {
- store.state.badges = [];
+ const rows = wrapper.findAll('.gl-responsive-table-row');
- await nextTick();
- expect(vm.$el.innerText).toMatch('This project has no badges');
- });
+ expect(rows).toHaveLength(numberOfDummyBadges);
+ });
- it('shows a loading icon when loading', async () => {
- store.state.isLoading = true;
+ it('renders a message if no badges exist', () => {
+ createComponent({
+ kind: PROJECT_BADGE,
+ badges: [],
+ });
- await nextTick();
- const loadingIcon = vm.$el.querySelector('.gl-spinner');
+ expect(wrapper.text()).toMatch('This project has no badges');
+ });
- expect(loadingIcon).toBeVisible();
- });
+ it('shows a loading icon when loading', () => {
+ createComponent({ isLoading: true });
- describe('for group badges', () => {
- beforeEach(async () => {
- store.state.kind = GROUP_BADGE;
+ const loadingIcon = wrapper.find('.gl-spinner');
- await nextTick();
+ expect(loadingIcon.isVisible()).toBe(true);
});
+ });
- it('renders a message if no badges exist', async () => {
- store.state.badges = [];
+ describe('for group badges', () => {
+ it('renders a message if no badges exist', () => {
+ createComponent({
+ kind: GROUP_BADGE,
+ badges: [],
+ });
- await nextTick();
- expect(vm.$el.innerText).toMatch('This group has no badges');
+ expect(wrapper.text()).toMatch('This group has no badges');
});
});
});
diff --git a/spec/frontend/badges/components/badge_spec.js b/spec/frontend/badges/components/badge_spec.js
index 19b3a9f23a6..b468e38f19e 100644
--- a/spec/frontend/badges/components/badge_spec.js
+++ b/spec/frontend/badges/components/badge_spec.js
@@ -1,138 +1,78 @@
-import Vue, { nextTick } from 'vue';
-import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
-import mountComponent from 'helpers/vue_mount_component_helper';
+import { nextTick } from 'vue';
+import { mount } from '@vue/test-utils';
+
import { DUMMY_IMAGE_URL, TEST_HOST } from 'spec/test_constants';
import Badge from '~/badges/components/badge.vue';
describe('Badge component', () => {
- const Component = Vue.extend(Badge);
const dummyProps = {
imageUrl: DUMMY_IMAGE_URL,
linkUrl: `${TEST_HOST}/badge/link/url`,
};
- let vm;
+ let wrapper;
const findElements = () => {
- const buttons = vm.$el.querySelectorAll('button');
+ const buttons = wrapper.findAll('button');
return {
- badgeImage: vm.$el.querySelector('img.project-badge'),
- loadingIcon: vm.$el.querySelector('.gl-spinner'),
- reloadButton: buttons[buttons.length - 1],
+ badgeImage: wrapper.find('img.project-badge'),
+ loadingIcon: wrapper.find('.gl-spinner'),
+ reloadButton: buttons.at(buttons.length - 1),
};
};
- const createComponent = (props, el = null) => {
- vm = mountComponent(Component, props, el);
- const { badgeImage } = findElements();
- return new Promise((resolve) => {
- badgeImage.addEventListener('load', resolve);
- // Manually dispatch load event as it is not triggered
- badgeImage.dispatchEvent(new Event('load'));
- }).then(() => nextTick());
+ const createComponent = (propsData) => {
+ wrapper = mount(Badge, { propsData });
};
afterEach(() => {
- vm.$destroy();
+ wrapper.destroy();
});
- describe('watchers', () => {
- describe('imageUrl', () => {
- it('sets isLoading and resets numRetries and hasError', async () => {
- const props = { ...dummyProps };
- await createComponent(props);
- expect(vm.isLoading).toBe(false);
- vm.hasError = true;
- vm.numRetries = 42;
-
- vm.imageUrl = `${props.imageUrl}#something/else`;
- await nextTick();
- expect(vm.isLoading).toBe(true);
- expect(vm.numRetries).toBe(0);
- expect(vm.hasError).toBe(false);
- });
- });
+ beforeEach(() => {
+ return createComponent({ ...dummyProps }, '#dummy-element');
});
- describe('methods', () => {
- beforeEach(async () => {
- await createComponent({ ...dummyProps });
- });
+ it('shows a badge image after loading', async () => {
+ const { badgeImage, loadingIcon, reloadButton } = findElements();
+ badgeImage.element.dispatchEvent(new Event('load'));
- it('onError resets isLoading and sets hasError', () => {
- vm.hasError = false;
- vm.isLoading = true;
+ await nextTick();
- vm.onError();
+ expect(badgeImage.isVisible()).toBe(true);
+ expect(loadingIcon.isVisible()).toBe(false);
+ expect(reloadButton.isVisible()).toBe(false);
+ expect(wrapper.find('.btn-group').isVisible()).toBe(false);
+ });
- expect(vm.hasError).toBe(true);
- expect(vm.isLoading).toBe(false);
- });
+ it('shows a loading icon when loading', () => {
+ const { badgeImage, loadingIcon, reloadButton } = findElements();
- it('onLoad sets isLoading', () => {
- vm.isLoading = true;
+ expect(badgeImage.isVisible()).toBe(false);
+ expect(loadingIcon.isVisible()).toBe(true);
+ expect(reloadButton.isVisible()).toBe(false);
+ expect(wrapper.find('.btn-group').isVisible()).toBe(false);
+ });
- vm.onLoad();
+ it('shows an error and reload button if loading failed', async () => {
+ const { badgeImage, loadingIcon, reloadButton } = findElements();
+ badgeImage.element.dispatchEvent(new Event('error'));
- expect(vm.isLoading).toBe(false);
- });
+ await nextTick();
- it('reloadImage resets isLoading and hasError and increases numRetries', () => {
- vm.hasError = true;
- vm.isLoading = false;
- vm.numRetries = 0;
+ expect(badgeImage.isVisible()).toBe(false);
+ expect(loadingIcon.isVisible()).toBe(false);
+ expect(reloadButton.isVisible()).toBe(true);
+ expect(reloadButton.element).toHaveSpriteIcon('retry');
+ expect(wrapper.text()).toBe('No badge image');
+ });
- vm.reloadImage();
+ it('retries an image when loading failed and reload button is clicked', async () => {
+ const { badgeImage, reloadButton } = findElements();
+ badgeImage.element.dispatchEvent(new Event('error'));
+ await nextTick();
- expect(vm.hasError).toBe(false);
- expect(vm.isLoading).toBe(true);
- expect(vm.numRetries).toBe(1);
- });
- });
+ await reloadButton.trigger('click');
- describe('behavior', () => {
- beforeEach(() => {
- setHTMLFixture('<div id="dummy-element"></div>');
- return createComponent({ ...dummyProps }, '#dummy-element');
- });
-
- afterEach(() => {
- resetHTMLFixture();
- });
-
- it('shows a badge image after loading', () => {
- expect(vm.isLoading).toBe(false);
- expect(vm.hasError).toBe(false);
- const { badgeImage, loadingIcon, reloadButton } = findElements();
-
- expect(badgeImage).toBeVisible();
- expect(loadingIcon).toBeHidden();
- expect(reloadButton).toBeHidden();
- expect(vm.$el.querySelector('.btn-group')).toBeHidden();
- });
-
- it('shows a loading icon when loading', async () => {
- vm.isLoading = true;
-
- await nextTick();
- const { badgeImage, loadingIcon, reloadButton } = findElements();
-
- expect(badgeImage).toBeHidden();
- expect(loadingIcon).toBeVisible();
- expect(reloadButton).toBeHidden();
- expect(vm.$el.querySelector('.btn-group')).toBeHidden();
- });
-
- it('shows an error and reload button if loading failed', async () => {
- vm.hasError = true;
-
- await nextTick();
- const { badgeImage, loadingIcon, reloadButton } = findElements();
-
- expect(badgeImage).toBeHidden();
- expect(loadingIcon).toBeHidden();
- expect(reloadButton).toBeVisible();
- expect(reloadButton).toHaveSpriteIcon('retry');
- expect(vm.$el.innerText.trim()).toBe('No badge image');
- });
+ expect(badgeImage.attributes('src')).toBe(`${dummyProps.imageUrl}#retries=1`);
});
});
diff --git a/spec/frontend/batch_comments/components/drafts_count_spec.js b/spec/frontend/batch_comments/components/drafts_count_spec.js
index 390ef21929c..c3a7946c85c 100644
--- a/spec/frontend/batch_comments/components/drafts_count_spec.js
+++ b/spec/frontend/batch_comments/components/drafts_count_spec.js
@@ -1,40 +1,36 @@
-import Vue, { nextTick } from 'vue';
-import { mountComponentWithStore } from 'helpers/vue_mount_component_helper';
+import { nextTick } from 'vue';
+import { mount } from '@vue/test-utils';
import DraftsCount from '~/batch_comments/components/drafts_count.vue';
import { createStore } from '~/batch_comments/stores';
describe('Batch comments drafts count component', () => {
- let vm;
- let Component;
-
- beforeAll(() => {
- Component = Vue.extend(DraftsCount);
- });
+ let store;
+ let wrapper;
beforeEach(() => {
- const store = createStore();
+ store = createStore();
store.state.batchComments.drafts.push('comment');
- vm = mountComponentWithStore(Component, { store });
+ wrapper = mount(DraftsCount, { store });
});
afterEach(() => {
- vm.$destroy();
+ wrapper.destroy();
});
it('renders count', () => {
- expect(vm.$el.textContent).toContain('1');
+ expect(wrapper.text()).toContain('1');
});
it('renders screen reader text', async () => {
- const el = vm.$el.querySelector('.sr-only');
+ const el = wrapper.find('.sr-only');
- expect(el.textContent).toContain('draft');
-
- vm.$store.state.batchComments.drafts.push('comment 2');
+ expect(el.text()).toContain('draft');
+ store.state.batchComments.drafts.push('comment 2');
await nextTick();
- expect(el.textContent).toContain('drafts');
+
+ expect(el.text()).toContain('drafts');
});
});
diff --git a/spec/frontend/batch_comments/components/preview_item_spec.js b/spec/frontend/batch_comments/components/preview_item_spec.js
index 91e6b84a216..6a104f0c787 100644
--- a/spec/frontend/batch_comments/components/preview_item_spec.js
+++ b/spec/frontend/batch_comments/components/preview_item_spec.js
@@ -1,5 +1,4 @@
-import Vue from 'vue';
-import { mountComponentWithStore } from 'helpers/vue_mount_component_helper';
+import { mount } from '@vue/test-utils';
import PreviewItem from '~/batch_comments/components/preview_item.vue';
import { createStore } from '~/batch_comments/stores';
import diffsModule from '~/diffs/store/modules';
@@ -8,8 +7,7 @@ import '~/behaviors/markdown/render_gfm';
import { createDraft } from '../mock_data';
describe('Batch comments draft preview item component', () => {
- let vm;
- let Component;
+ let wrapper;
let draft;
function createComponent(isLast = false, extra = {}, extendStore = () => {}) {
@@ -24,21 +22,17 @@ describe('Batch comments draft preview item component', () => {
...extra,
};
- vm = mountComponentWithStore(Component, { store, props: { draft, isLast } });
+ wrapper = mount(PreviewItem, { store, propsData: { draft, isLast } });
}
- beforeAll(() => {
- Component = Vue.extend(PreviewItem);
- });
-
afterEach(() => {
- vm.$destroy();
+ wrapper.destroy();
});
it('renders text content', () => {
createComponent(false, { note_html: '<img src="" /><p>Hello world</p>' });
- expect(vm.$el.querySelector('.review-preview-item-content').innerHTML).toEqual(
+ expect(wrapper.find('.review-preview-item-content').element.innerHTML).toBe(
'<p>Hello world</p>',
);
});
@@ -47,9 +41,7 @@ describe('Batch comments draft preview item component', () => {
it('renders file path', () => {
createComponent(false, { file_path: 'index.js', file_hash: 'abc', position: {} });
- expect(vm.$el.querySelector('.review-preview-item-header-text').textContent).toContain(
- 'index.js',
- );
+ expect(wrapper.find('.review-preview-item-header-text').text()).toContain('index.js');
});
it('renders new line position', () => {
@@ -66,7 +58,7 @@ describe('Batch comments draft preview item component', () => {
},
});
- expect(vm.$el.querySelector('.bold').textContent).toContain(':+1');
+ expect(wrapper.find('.bold').text()).toContain(':+1');
});
it('renders old line position', () => {
@@ -82,7 +74,7 @@ describe('Batch comments draft preview item component', () => {
},
});
- expect(vm.$el.querySelector('.bold').textContent).toContain(':2');
+ expect(wrapper.find('.bold').text()).toContain(':2');
});
it('renders image position', () => {
@@ -92,7 +84,7 @@ describe('Batch comments draft preview item component', () => {
position: { position_type: 'image', x: 10, y: 20 },
});
- expect(vm.$el.querySelector('.bold').textContent).toContain('10x 20y');
+ expect(wrapper.find('.bold').text()).toContain('10x 20y');
});
});
@@ -113,15 +105,13 @@ describe('Batch comments draft preview item component', () => {
});
it('renders title', () => {
- expect(vm.$el.querySelector('.review-preview-item-header-text').textContent).toContain(
+ expect(wrapper.find('.review-preview-item-header-text').text()).toContain(
"Author 'Nick' Name's thread",
);
});
it('renders thread resolved text', () => {
- expect(vm.$el.querySelector('.draft-note-resolution').textContent).toContain(
- 'Thread will be resolved',
- );
+ expect(wrapper.find('.draft-note-resolution').text()).toContain('Thread will be resolved');
});
});
@@ -131,9 +121,7 @@ describe('Batch comments draft preview item component', () => {
store.state.notes.discussions.push({});
});
- expect(vm.$el.querySelector('.review-preview-item-header-text').textContent).toContain(
- 'Your new comment',
- );
+ expect(wrapper.find('.review-preview-item-header-text').text()).toContain('Your new comment');
});
});
});
diff --git a/spec/frontend/batch_comments/components/publish_button_spec.js b/spec/frontend/batch_comments/components/publish_button_spec.js
index 9a782ec09b6..5e3fa3e9446 100644
--- a/spec/frontend/batch_comments/components/publish_button_spec.js
+++ b/spec/frontend/batch_comments/components/publish_button_spec.js
@@ -1,38 +1,34 @@
-import Vue, { nextTick } from 'vue';
-import { mountComponentWithStore } from 'helpers/vue_mount_component_helper';
+import { nextTick } from 'vue';
+import { mount } from '@vue/test-utils';
import PublishButton from '~/batch_comments/components/publish_button.vue';
import { createStore } from '~/batch_comments/stores';
describe('Batch comments publish button component', () => {
- let vm;
- let Component;
-
- beforeAll(() => {
- Component = Vue.extend(PublishButton);
- });
+ let wrapper;
+ let store;
beforeEach(() => {
- const store = createStore();
+ store = createStore();
- vm = mountComponentWithStore(Component, { store, props: { shouldPublish: true } });
+ wrapper = mount(PublishButton, { store, propsData: { shouldPublish: true } });
- jest.spyOn(vm.$store, 'dispatch').mockImplementation();
+ jest.spyOn(store, 'dispatch').mockImplementation();
});
afterEach(() => {
- vm.$destroy();
+ wrapper.destroy();
});
- it('dispatches publishReview on click', () => {
- vm.$el.click();
+ it('dispatches publishReview on click', async () => {
+ await wrapper.trigger('click');
- expect(vm.$store.dispatch).toHaveBeenCalledWith('batchComments/publishReview', undefined);
+ expect(store.dispatch).toHaveBeenCalledWith('batchComments/publishReview', undefined);
});
it('sets loading when isPublishing is true', async () => {
- vm.$store.state.batchComments.isPublishing = true;
+ store.state.batchComments.isPublishing = true;
await nextTick();
- expect(vm.$el.getAttribute('disabled')).toBe('disabled');
+ expect(wrapper.attributes('disabled')).toBe('disabled');
});
});
diff --git a/spec/frontend/behaviors/bind_in_out_spec.js b/spec/frontend/behaviors/bind_in_out_spec.js
index 4d958e30b4d..7b40b1d3cd7 100644
--- a/spec/frontend/behaviors/bind_in_out_spec.js
+++ b/spec/frontend/behaviors/bind_in_out_spec.js
@@ -1,4 +1,3 @@
-import ClassSpecHelper from 'helpers/class_spec_helper';
import BindInOut from '~/behaviors/bind_in_out';
describe('BindInOut', () => {
@@ -142,7 +141,9 @@ describe('BindInOut', () => {
testContext.initAll = BindInOut.initAll();
});
- ClassSpecHelper.itShouldBeAStaticMethod(BindInOut, 'initAll');
+ it('should be a static method', () => {
+ expect(BindInOut.initAll).toEqual(expect.any(Function));
+ });
it('should call .querySelectorAll', () => {
expect(document.querySelectorAll).toHaveBeenCalledWith('*[data-bind-in]');
@@ -169,7 +170,9 @@ describe('BindInOut', () => {
testContext.init = BindInOut.init({}, {});
});
- ClassSpecHelper.itShouldBeAStaticMethod(BindInOut, 'init');
+ it('should be a static method', () => {
+ expect(BindInOut.init).toEqual(expect.any(Function));
+ });
it('should call .addEvents', () => {
expect(BindInOut.prototype.addEvents).toHaveBeenCalled();
diff --git a/spec/frontend/blame/blame_redirect_spec.js b/spec/frontend/blame/blame_redirect_spec.js
new file mode 100644
index 00000000000..beb10139b3a
--- /dev/null
+++ b/spec/frontend/blame/blame_redirect_spec.js
@@ -0,0 +1,70 @@
+import redirectToCorrectPage from '~/blame/blame_redirect';
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
+import { createAlert } from '~/flash';
+
+jest.mock('~/flash');
+
+describe('Blame page redirect', () => {
+ beforeEach(() => {
+ global.window = Object.create(window);
+ const url = 'https://gitlab.com/flightjs/Flight/-/blame/master/file.json';
+ Object.defineProperty(window, 'location', {
+ writable: true,
+ value: {
+ href: url,
+ hash: '',
+ search: '',
+ },
+ });
+
+ setHTMLFixture(`<div class="js-per-page" data-per-page="1000"></div>`);
+ });
+
+ afterEach(() => {
+ createAlert.mockClear();
+ resetHTMLFixture();
+ });
+
+ it('performs redirect to further pages when needed', () => {
+ window.location.hash = '#L1001';
+ redirectToCorrectPage();
+ expect(window.location.href).toMatch('?page=2');
+ });
+
+ it('performs redirect back to first page when needed', () => {
+ window.location.href = 'https://gitlab.com/flightjs/Flight/-/blame/master/file.json';
+ window.location.search = '?page=200';
+ window.location.hash = '#L999';
+ redirectToCorrectPage();
+ expect(window.location.href).toMatch('?page=1');
+ });
+
+ it('doesn`t perform redirect when the line is still on page 1', () => {
+ window.location.hash = '#L1000';
+ redirectToCorrectPage();
+ expect(window.location.href).not.toMatch('?page');
+ });
+
+ it('doesn`t perform redirect when "no_pagination" param is present', () => {
+ window.location.href = 'https://gitlab.com/flightjs/Flight/-/blame/master/file.json';
+ window.location.search = '?no_pagination=true';
+ window.location.hash = '#L1001';
+ redirectToCorrectPage();
+ expect(window.location.href).not.toMatch('?page');
+ });
+
+ it('doesn`t perform redirect when perPage is not present', () => {
+ setHTMLFixture(`<div class="js-per-page"></div>`);
+ window.location.hash = '#L1001';
+ redirectToCorrectPage();
+ expect(window.location.href).not.toMatch('?page');
+ });
+
+ it('shows alert with a message', () => {
+ window.location.hash = '#L1001';
+ redirectToCorrectPage();
+ expect(createAlert).toHaveBeenCalledWith({
+ message: 'Please wait a few moments while we load the file history for this line.',
+ });
+ });
+});
diff --git a/spec/frontend/blob/3d_viewer/mesh_object_spec.js b/spec/frontend/blob/3d_viewer/mesh_object_spec.js
index 3014af073f5..1b0fd362778 100644
--- a/spec/frontend/blob/3d_viewer/mesh_object_spec.js
+++ b/spec/frontend/blob/3d_viewer/mesh_object_spec.js
@@ -1,4 +1,4 @@
-import { BoxGeometry } from 'three/build/three.module';
+import { BoxGeometry } from 'three';
import MeshObject from '~/blob/3d_viewer/mesh_object';
describe('Mesh object', () => {
diff --git a/spec/frontend/blob/blob_blame_link_spec.js b/spec/frontend/blob/blob_blame_link_spec.js
index 0d19177a11f..060e8803520 100644
--- a/spec/frontend/blob/blob_blame_link_spec.js
+++ b/spec/frontend/blob/blob_blame_link_spec.js
@@ -29,19 +29,19 @@ describe('Blob links', () => {
it('adds wrapper elements with correct classes', () => {
const wrapper = document.querySelector('.line-links');
- expect(wrapper).toBeTruthy();
+ expect(wrapper).not.toBeNull();
expect(wrapper.classList).toContain('diff-line-num');
});
it('adds blame link with correct classes and path', () => {
const blameLink = document.querySelector('.file-line-blame');
- expect(blameLink).toBeTruthy();
+ expect(blameLink).not.toBeNull();
expect(blameLink.getAttribute('href')).toBe('/blamePath#L5');
});
it('adds line link within wraper with correct classes and path', () => {
const lineLink = document.querySelector('.file-line-num');
- expect(lineLink).toBeTruthy();
+ expect(lineLink).not.toBeNull();
expect(lineLink.getAttribute('href')).toBe('#L5');
});
});
diff --git a/spec/frontend/blob/components/blob_content_spec.js b/spec/frontend/blob/components/blob_content_spec.js
index 788ee0a86ab..f7b819b6e94 100644
--- a/spec/frontend/blob/components/blob_content_spec.js
+++ b/spec/frontend/blob/components/blob_content_spec.js
@@ -91,13 +91,13 @@ describe('Blob Content component', () => {
it(`properly proxies ${BLOB_RENDER_EVENT_LOAD} event`, () => {
expect(wrapper.emitted(BLOB_RENDER_EVENT_LOAD)).toBeUndefined();
findErrorEl().vm.$emit(BLOB_RENDER_EVENT_LOAD);
- expect(wrapper.emitted(BLOB_RENDER_EVENT_LOAD)).toBeTruthy();
+ expect(wrapper.emitted(BLOB_RENDER_EVENT_LOAD)).toHaveLength(1);
});
it(`properly proxies ${BLOB_RENDER_EVENT_SHOW_SOURCE} event`, () => {
expect(wrapper.emitted(BLOB_RENDER_EVENT_SHOW_SOURCE)).toBeUndefined();
findErrorEl().vm.$emit(BLOB_RENDER_EVENT_SHOW_SOURCE);
- expect(wrapper.emitted(BLOB_RENDER_EVENT_SHOW_SOURCE)).toBeTruthy();
+ expect(wrapper.emitted(BLOB_RENDER_EVENT_SHOW_SOURCE)).toHaveLength(1);
});
});
});
diff --git a/spec/frontend/blob/components/table_contents_spec.js b/spec/frontend/blob/components/table_contents_spec.js
index 2cbac809a0d..5fe328b65ff 100644
--- a/spec/frontend/blob/components/table_contents_spec.js
+++ b/spec/frontend/blob/components/table_contents_spec.js
@@ -71,6 +71,11 @@ describe('Markdown table of contents component', () => {
expect(dropdownItems.exists()).toBe(true);
expect(dropdownItems.length).toBe(4);
+
+ // make sure that this only happens once
+ await setLoaded(true);
+
+ expect(wrapper.findAllComponents(GlDropdownItem).length).toBe(4);
});
it('sets padding for dropdown items', async () => {
diff --git a/spec/frontend/boards/board_card_inner_spec.js b/spec/frontend/boards/board_card_inner_spec.js
index 2c3ec69f9ae..3ebc51c4bcb 100644
--- a/spec/frontend/boards/board_card_inner_spec.js
+++ b/spec/frontend/boards/board_card_inner_spec.js
@@ -5,7 +5,7 @@ import { nextTick } from 'vue';
import setWindowLocation from 'helpers/set_window_location_helper';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import { mountExtended } from 'helpers/vue_test_utils_helper';
-import BoardBlockedIcon from '~/boards/components/board_blocked_icon.vue';
+import IssuableBlockedIcon from '~/vue_shared/components/issuable_blocked_icon/issuable_blocked_icon.vue';
import BoardCardInner from '~/boards/components/board_card_inner.vue';
import BoardCardMoveToPosition from '~/boards/components/board_card_move_to_position.vue';
import WorkItemTypeIcon from '~/work_items/components/work_item_type_icon.vue';
@@ -39,7 +39,7 @@ describe('Board card component', () => {
let list;
let store;
- const findBoardBlockedIcon = () => wrapper.findComponent(BoardBlockedIcon);
+ const findIssuableBlockedIcon = () => wrapper.findComponent(IssuableBlockedIcon);
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findEpicCountablesTotalTooltip = () => wrapper.findComponent(GlTooltip);
const findEpicCountables = () => wrapper.findByTestId('epic-countables');
@@ -189,7 +189,7 @@ describe('Board card component', () => {
},
});
- expect(findBoardBlockedIcon().exists()).toBe(true);
+ expect(findIssuableBlockedIcon().exists()).toBe(true);
});
it('does not show blocked icon if issue is not blocked', () => {
@@ -200,7 +200,7 @@ describe('Board card component', () => {
},
});
- expect(findBoardBlockedIcon().exists()).toBe(false);
+ expect(findIssuableBlockedIcon().exists()).toBe(false);
});
});
@@ -595,5 +595,10 @@ describe('Board card component', () => {
expect(findEpicCountablesTotalWeight().text()).toBe('15');
expect(findEpicProgressTooltip().text()).toBe('10 of 15 weight completed');
});
+
+ it('does not render the move to position icon', () => {
+ createWrapper();
+ expect(findMoveToPositionComponent().exists()).toBe(false);
+ });
});
});
diff --git a/spec/frontend/boards/components/board_filtered_search_spec.js b/spec/frontend/boards/components/board_filtered_search_spec.js
index 731578e15a3..1a07b9f0b78 100644
--- a/spec/frontend/boards/components/board_filtered_search_spec.js
+++ b/spec/frontend/boards/components/board_filtered_search_spec.js
@@ -126,6 +126,7 @@ describe('BoardFilteredSearch', () => {
{ type: 'weight', value: { data: '2', operator: '=' } },
{ type: 'iteration', value: { data: 'Any&3', operator: '=' } },
{ type: 'release', value: { data: 'v1.0.0', operator: '=' } },
+ { type: 'health_status', value: { data: 'onTrack', operator: '=' } },
];
jest.spyOn(urlUtility, 'updateHistory');
findFilteredSearch().vm.$emit('onFilter', mockFilters);
@@ -134,7 +135,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=Any&iteration_cadence_id=3&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&health_status=onTrack',
});
});
@@ -160,7 +161,9 @@ describe('BoardFilteredSearch', () => {
describe('when url params are already set', () => {
beforeEach(() => {
- createComponent({ initialFilterParams: { authorUsername: 'root', labelName: ['label'] } });
+ createComponent({
+ initialFilterParams: { authorUsername: 'root', labelName: ['label'], healthStatus: 'Any' },
+ });
jest.spyOn(store, 'dispatch');
});
@@ -169,6 +172,7 @@ describe('BoardFilteredSearch', () => {
expect(findFilteredSearch().props('initialFilterValue')).toEqual([
{ type: 'author', value: { data: 'root', operator: '=' } },
{ type: 'label', value: { data: 'label', operator: '=' } },
+ { type: 'health_status', value: { data: 'Any', operator: '=' } },
]);
});
});
diff --git a/spec/frontend/boards/stores/actions_spec.js b/spec/frontend/boards/stores/actions_spec.js
index e919300228a..78859525a63 100644
--- a/spec/frontend/boards/stores/actions_spec.js
+++ b/spec/frontend/boards/stores/actions_spec.js
@@ -1047,60 +1047,58 @@ describe('moveIssueCard and undoMoveIssueCard', () => {
let undoMutations;
describe('when re-ordering card', () => {
- beforeEach(
- ({
- itemId = 123,
- fromListId = 'gid://gitlab/List/1',
- toListId = 'gid://gitlab/List/1',
- originalIssue = { foo: 'bar' },
- originalIndex = 0,
- moveBeforeId = undefined,
- moveAfterId = undefined,
- allItemsLoadedInList = true,
- listPosition = undefined,
- } = {}) => {
- state = {
- boardLists: {
- [toListId]: { listType: ListType.backlog },
- [fromListId]: { listType: ListType.backlog },
- },
- boardItems: { [itemId]: originalIssue },
- boardItemsByListId: { [fromListId]: [123] },
- };
- params = {
- itemId,
- fromListId,
- toListId,
- moveBeforeId,
- moveAfterId,
- listPosition,
- allItemsLoadedInList,
- };
- moveMutations = [
- { type: types.REMOVE_BOARD_ITEM_FROM_LIST, payload: { itemId, listId: fromListId } },
- {
- type: types.ADD_BOARD_ITEM_TO_LIST,
- payload: {
- itemId,
- listId: toListId,
- moveBeforeId,
- moveAfterId,
- listPosition,
- allItemsLoadedInList,
- atIndex: originalIndex,
- },
- },
- ];
- undoMutations = [
- { type: types.UPDATE_BOARD_ITEM, payload: originalIssue },
- { type: types.REMOVE_BOARD_ITEM_FROM_LIST, payload: { itemId, listId: fromListId } },
- {
- type: types.ADD_BOARD_ITEM_TO_LIST,
- payload: { itemId, listId: fromListId, atIndex: originalIndex },
+ beforeEach(() => {
+ const itemId = 123;
+ const fromListId = 'gid://gitlab/List/1';
+ const toListId = 'gid://gitlab/List/1';
+ const originalIssue = { foo: 'bar' };
+ const originalIndex = 0;
+ const moveBeforeId = undefined;
+ const moveAfterId = undefined;
+ const allItemsLoadedInList = true;
+ const listPosition = undefined;
+
+ state = {
+ boardLists: {
+ [toListId]: { listType: ListType.backlog },
+ [fromListId]: { listType: ListType.backlog },
+ },
+ boardItems: { [itemId]: originalIssue },
+ boardItemsByListId: { [fromListId]: [123] },
+ };
+ params = {
+ itemId,
+ fromListId,
+ toListId,
+ moveBeforeId,
+ moveAfterId,
+ listPosition,
+ allItemsLoadedInList,
+ };
+ moveMutations = [
+ { type: types.REMOVE_BOARD_ITEM_FROM_LIST, payload: { itemId, listId: fromListId } },
+ {
+ type: types.ADD_BOARD_ITEM_TO_LIST,
+ payload: {
+ itemId,
+ listId: toListId,
+ moveBeforeId,
+ moveAfterId,
+ listPosition,
+ allItemsLoadedInList,
+ atIndex: originalIndex,
},
- ];
- },
- );
+ },
+ ];
+ undoMutations = [
+ { type: types.UPDATE_BOARD_ITEM, payload: originalIssue },
+ { type: types.REMOVE_BOARD_ITEM_FROM_LIST, payload: { itemId, listId: fromListId } },
+ {
+ type: types.ADD_BOARD_ITEM_TO_LIST,
+ payload: { itemId, listId: fromListId, atIndex: originalIndex },
+ },
+ ];
+ });
it('moveIssueCard commits a correct set of actions', () => {
testAction({
@@ -1144,42 +1142,40 @@ describe('moveIssueCard and undoMoveIssueCard', () => {
},
],
])('when %s', (_, { toListType, fromListType }) => {
- beforeEach(
- ({
- itemId = 123,
- fromListId = 'gid://gitlab/List/1',
- toListId = 'gid://gitlab/List/2',
- originalIssue = { foo: 'bar' },
- originalIndex = 0,
- moveBeforeId = undefined,
- moveAfterId = undefined,
- } = {}) => {
- state = {
- boardLists: {
- [fromListId]: { listType: fromListType },
- [toListId]: { listType: toListType },
- },
- boardItems: { [itemId]: originalIssue },
- boardItemsByListId: { [fromListId]: [123], [toListId]: [] },
- };
- params = { itemId, fromListId, toListId, moveBeforeId, moveAfterId };
- moveMutations = [
- { type: types.REMOVE_BOARD_ITEM_FROM_LIST, payload: { itemId, listId: fromListId } },
- {
- type: types.ADD_BOARD_ITEM_TO_LIST,
- payload: { itemId, listId: toListId, moveBeforeId, moveAfterId },
- },
- ];
- undoMutations = [
- { type: types.UPDATE_BOARD_ITEM, payload: originalIssue },
- { type: types.REMOVE_BOARD_ITEM_FROM_LIST, payload: { itemId, listId: toListId } },
- {
- type: types.ADD_BOARD_ITEM_TO_LIST,
- payload: { itemId, listId: fromListId, atIndex: originalIndex },
- },
- ];
- },
- );
+ beforeEach(() => {
+ const itemId = 123;
+ const fromListId = 'gid://gitlab/List/1';
+ const toListId = 'gid://gitlab/List/2';
+ const originalIssue = { foo: 'bar' };
+ const originalIndex = 0;
+ const moveBeforeId = undefined;
+ const moveAfterId = undefined;
+
+ state = {
+ boardLists: {
+ [fromListId]: { listType: fromListType },
+ [toListId]: { listType: toListType },
+ },
+ boardItems: { [itemId]: originalIssue },
+ boardItemsByListId: { [fromListId]: [123], [toListId]: [] },
+ };
+ params = { itemId, fromListId, toListId, moveBeforeId, moveAfterId };
+ moveMutations = [
+ { type: types.REMOVE_BOARD_ITEM_FROM_LIST, payload: { itemId, listId: fromListId } },
+ {
+ type: types.ADD_BOARD_ITEM_TO_LIST,
+ payload: { itemId, listId: toListId, moveBeforeId, moveAfterId },
+ },
+ ];
+ undoMutations = [
+ { type: types.UPDATE_BOARD_ITEM, payload: originalIssue },
+ { type: types.REMOVE_BOARD_ITEM_FROM_LIST, payload: { itemId, listId: toListId } },
+ {
+ type: types.ADD_BOARD_ITEM_TO_LIST,
+ payload: { itemId, listId: fromListId, atIndex: originalIndex },
+ },
+ ];
+ });
it('moveIssueCard commits a correct set of actions', () => {
testAction({
@@ -1216,47 +1212,45 @@ describe('moveIssueCard and undoMoveIssueCard', () => {
},
],
])('when %s', (_, { toListType, fromListType }) => {
- beforeEach(
- ({
- itemId = 123,
- fromListId = 'gid://gitlab/List/1',
- toListId = 'gid://gitlab/List/2',
- originalIssue = { foo: 'bar' },
- originalIndex = 0,
- moveBeforeId = undefined,
- moveAfterId = undefined,
- } = {}) => {
- state = {
- boardLists: {
- [fromListId]: { listType: fromListType },
- [toListId]: { listType: toListType },
- },
- boardItems: { [itemId]: originalIssue },
- boardItemsByListId: { [fromListId]: [123], [toListId]: [] },
- };
- params = { itemId, fromListId, toListId, moveBeforeId, moveAfterId };
- moveMutations = [
- { type: types.REMOVE_BOARD_ITEM_FROM_LIST, payload: { itemId, listId: fromListId } },
- {
- type: types.ADD_BOARD_ITEM_TO_LIST,
- payload: { itemId, listId: toListId, moveBeforeId, moveAfterId },
- },
- {
- type: types.ADD_BOARD_ITEM_TO_LIST,
- payload: { itemId, listId: fromListId, atIndex: originalIndex },
- },
- ];
- undoMutations = [
- { type: types.UPDATE_BOARD_ITEM, payload: originalIssue },
- { type: types.REMOVE_BOARD_ITEM_FROM_LIST, payload: { itemId, listId: fromListId } },
- { type: types.REMOVE_BOARD_ITEM_FROM_LIST, payload: { itemId, listId: toListId } },
- {
- type: types.ADD_BOARD_ITEM_TO_LIST,
- payload: { itemId, listId: fromListId, atIndex: originalIndex },
- },
- ];
- },
- );
+ beforeEach(() => {
+ const itemId = 123;
+ const fromListId = 'gid://gitlab/List/1';
+ const toListId = 'gid://gitlab/List/2';
+ const originalIssue = { foo: 'bar' };
+ const originalIndex = 0;
+ const moveBeforeId = undefined;
+ const moveAfterId = undefined;
+
+ state = {
+ boardLists: {
+ [fromListId]: { listType: fromListType },
+ [toListId]: { listType: toListType },
+ },
+ boardItems: { [itemId]: originalIssue },
+ boardItemsByListId: { [fromListId]: [123], [toListId]: [] },
+ };
+ params = { itemId, fromListId, toListId, moveBeforeId, moveAfterId };
+ moveMutations = [
+ { type: types.REMOVE_BOARD_ITEM_FROM_LIST, payload: { itemId, listId: fromListId } },
+ {
+ type: types.ADD_BOARD_ITEM_TO_LIST,
+ payload: { itemId, listId: toListId, moveBeforeId, moveAfterId },
+ },
+ {
+ type: types.ADD_BOARD_ITEM_TO_LIST,
+ payload: { itemId, listId: fromListId, atIndex: originalIndex },
+ },
+ ];
+ undoMutations = [
+ { type: types.UPDATE_BOARD_ITEM, payload: originalIssue },
+ { type: types.REMOVE_BOARD_ITEM_FROM_LIST, payload: { itemId, listId: fromListId } },
+ { type: types.REMOVE_BOARD_ITEM_FROM_LIST, payload: { itemId, listId: toListId } },
+ {
+ type: types.ADD_BOARD_ITEM_TO_LIST,
+ payload: { itemId, listId: fromListId, atIndex: originalIndex },
+ },
+ ];
+ });
it('moveIssueCard commits a correct set of actions', () => {
testAction({
diff --git a/spec/frontend/captcha/init_recaptcha_script_spec.js b/spec/frontend/captcha/init_recaptcha_script_spec.js
index af07c9e474e..78480821d95 100644
--- a/spec/frontend/captcha/init_recaptcha_script_spec.js
+++ b/spec/frontend/captcha/init_recaptcha_script_spec.js
@@ -1,5 +1,4 @@
import {
- RECAPTCHA_API_URL_PREFIX,
RECAPTCHA_ONLOAD_CALLBACK_NAME,
clearMemoizeCache,
initRecaptchaScript,
@@ -26,7 +25,7 @@ describe('initRecaptchaScript', () => {
<head>
<script
class="js-recaptcha-script"
- src="${RECAPTCHA_API_URL_PREFIX}?onload=${RECAPTCHA_ONLOAD_CALLBACK_NAME}&render=explicit"
+ src="undefined?onload=recaptchaOnloadCallback&render=explicit"
/>
</head>
`);
diff --git a/spec/frontend/ci_variable_list/components/ci_admin_variables_spec.js b/spec/frontend/ci_variable_list/components/ci_admin_variables_spec.js
index 920ceaefb70..864041141b8 100644
--- a/spec/frontend/ci_variable_list/components/ci_admin_variables_spec.js
+++ b/spec/frontend/ci_variable_list/components/ci_admin_variables_spec.js
@@ -4,8 +4,8 @@ import { GlLoadingIcon, GlTable } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import createFlash from '~/flash';
-import { resolvers } from '~/ci_variable_list/graphql/resolvers';
+import { createAlert } from '~/flash';
+import { resolvers } from '~/ci_variable_list/graphql/settings';
import ciAdminVariables from '~/ci_variable_list/components/ci_admin_variables.vue';
import ciVariableSettings from '~/ci_variable_list/components/ci_variable_settings.vue';
@@ -92,8 +92,8 @@ describe('Ci Admin Variable list', () => {
);
});
- it('createFlash was not called', () => {
- expect(createFlash).not.toHaveBeenCalled();
+ it('createAlert was not called', () => {
+ expect(createAlert).not.toHaveBeenCalled();
});
});
@@ -104,8 +104,8 @@ describe('Ci Admin Variable list', () => {
await createComponentWithApollo();
});
- it('calls createFlash with the expected error message', () => {
- expect(createFlash).toHaveBeenCalledWith({ message: variableFetchErrorText });
+ it('calls createAlert with the expected error message', () => {
+ expect(createAlert).toHaveBeenCalledWith({ message: variableFetchErrorText });
});
});
});
@@ -153,7 +153,7 @@ describe('Ci Admin Variable list', () => {
await nextTick();
expect(wrapper.vm.$apollo.mutate).toHaveBeenCalled();
- expect(createFlash).toHaveBeenCalledWith({ message: graphQLErrorMessage });
+ expect(createAlert).toHaveBeenCalledWith({ message: graphQLErrorMessage });
},
);
@@ -171,7 +171,7 @@ describe('Ci Admin Variable list', () => {
await findCiSettings().vm.$emit(event, newVariable);
expect(wrapper.vm.$apollo.mutate).toHaveBeenCalled();
- expect(createFlash).toHaveBeenCalledWith({ message: genericMutationErrorText });
+ expect(createAlert).toHaveBeenCalledWith({ message: genericMutationErrorText });
},
);
});
diff --git a/spec/frontend/ci_variable_list/components/ci_group_variables_spec.js b/spec/frontend/ci_variable_list/components/ci_group_variables_spec.js
index e45656acfd8..8a48e73eb9f 100644
--- a/spec/frontend/ci_variable_list/components/ci_group_variables_spec.js
+++ b/spec/frontend/ci_variable_list/components/ci_group_variables_spec.js
@@ -4,8 +4,8 @@ import { GlLoadingIcon, GlTable } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import createFlash from '~/flash';
-import { resolvers } from '~/ci_variable_list/graphql/resolvers';
+import { createAlert } from '~/flash';
+import { resolvers } from '~/ci_variable_list/graphql/settings';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import ciGroupVariables from '~/ci_variable_list/components/ci_group_variables.vue';
@@ -95,8 +95,8 @@ describe('Ci Group Variable list', () => {
);
});
- it('createFlash was not called', () => {
- expect(createFlash).not.toHaveBeenCalled();
+ it('createAlert was not called', () => {
+ expect(createAlert).not.toHaveBeenCalled();
});
});
@@ -107,8 +107,8 @@ describe('Ci Group Variable list', () => {
await createComponentWithApollo();
});
- it('calls createFlash with the expected error message', () => {
- expect(createFlash).toHaveBeenCalledWith({ message: variableFetchErrorText });
+ it('calls createAlert with the expected error message', () => {
+ expect(createAlert).toHaveBeenCalledWith({ message: variableFetchErrorText });
});
});
});
@@ -158,7 +158,7 @@ describe('Ci Group Variable list', () => {
await nextTick();
expect(wrapper.vm.$apollo.mutate).toHaveBeenCalled();
- expect(createFlash).toHaveBeenCalledWith({ message: graphQLErrorMessage });
+ expect(createAlert).toHaveBeenCalledWith({ message: graphQLErrorMessage });
},
);
@@ -176,7 +176,7 @@ describe('Ci Group Variable list', () => {
await findCiSettings().vm.$emit(event, newVariable);
expect(wrapper.vm.$apollo.mutate).toHaveBeenCalled();
- expect(createFlash).toHaveBeenCalledWith({ message: genericMutationErrorText });
+ expect(createAlert).toHaveBeenCalledWith({ message: genericMutationErrorText });
},
);
});
diff --git a/spec/frontend/ci_variable_list/components/ci_project_variables_spec.js b/spec/frontend/ci_variable_list/components/ci_project_variables_spec.js
index 867f8e0cf8f..c630278fbde 100644
--- a/spec/frontend/ci_variable_list/components/ci_project_variables_spec.js
+++ b/spec/frontend/ci_variable_list/components/ci_project_variables_spec.js
@@ -4,8 +4,8 @@ import { GlLoadingIcon, GlTable } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import createFlash from '~/flash';
-import { resolvers } from '~/ci_variable_list/graphql/resolvers';
+import { createAlert } from '~/flash';
+import { resolvers } from '~/ci_variable_list/graphql/settings';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import ciProjectVariables from '~/ci_variable_list/components/ci_project_variables.vue';
@@ -112,8 +112,8 @@ describe('Ci Project Variable list', () => {
);
});
- it('createFlash was not called', () => {
- expect(createFlash).not.toHaveBeenCalled();
+ it('createAlert was not called', () => {
+ expect(createAlert).not.toHaveBeenCalled();
});
});
@@ -125,8 +125,8 @@ describe('Ci Project Variable list', () => {
await createComponentWithApollo();
});
- it('calls createFlash with the expected error message', () => {
- expect(createFlash).toHaveBeenCalledWith({ message: variableFetchErrorText });
+ it('calls createAlert with the expected error message', () => {
+ expect(createAlert).toHaveBeenCalledWith({ message: variableFetchErrorText });
});
});
@@ -138,8 +138,8 @@ describe('Ci Project Variable list', () => {
await createComponentWithApollo();
});
- it('calls createFlash with the expected error message', () => {
- expect(createFlash).toHaveBeenCalledWith({ message: environmentFetchErrorText });
+ it('calls createAlert with the expected error message', () => {
+ expect(createAlert).toHaveBeenCalledWith({ message: environmentFetchErrorText });
});
});
});
@@ -190,7 +190,7 @@ describe('Ci Project Variable list', () => {
await nextTick();
expect(wrapper.vm.$apollo.mutate).toHaveBeenCalled();
- expect(createFlash).toHaveBeenCalledWith({ message: graphQLErrorMessage });
+ expect(createAlert).toHaveBeenCalledWith({ message: graphQLErrorMessage });
},
);
@@ -208,7 +208,7 @@ describe('Ci Project Variable list', () => {
await findCiSettings().vm.$emit(event, newVariable);
expect(wrapper.vm.$apollo.mutate).toHaveBeenCalled();
- expect(createFlash).toHaveBeenCalledWith({ message: genericMutationErrorText });
+ expect(createAlert).toHaveBeenCalledWith({ message: genericMutationErrorText });
},
);
});
diff --git a/spec/frontend/ci_variable_list/components/legacy_ci_variable_settings_spec.js b/spec/frontend/ci_variable_list/components/legacy_ci_variable_settings_spec.js
index 9c941f99982..7def4dd4f29 100644
--- a/spec/frontend/ci_variable_list/components/legacy_ci_variable_settings_spec.js
+++ b/spec/frontend/ci_variable_list/components/legacy_ci_variable_settings_spec.js
@@ -9,11 +9,11 @@ Vue.use(Vuex);
describe('Ci variable table', () => {
let wrapper;
let store;
- let isGroup;
+ let isProject;
- const createComponent = (groupState) => {
+ const createComponent = (projectState) => {
store = createStore();
- store.state.isGroup = groupState;
+ store.state.isProject = projectState;
jest.spyOn(store, 'dispatch').mockImplementation();
wrapper = shallowMount(LegacyCiVariableSettings, {
store,
@@ -25,14 +25,14 @@ describe('Ci variable table', () => {
});
it('dispatches fetchEnvironments when mounted', () => {
- isGroup = false;
- createComponent(isGroup);
+ isProject = true;
+ createComponent(isProject);
expect(store.dispatch).toHaveBeenCalledWith('fetchEnvironments');
});
it('does not dispatch fetchenvironments when in group context', () => {
- isGroup = true;
- createComponent(isGroup);
+ isProject = false;
+ createComponent(isProject);
expect(store.dispatch).not.toHaveBeenCalled();
});
});
diff --git a/spec/frontend/ci_variable_list/mocks.js b/spec/frontend/ci_variable_list/mocks.js
index 6d633c8b740..6f3e73f8b83 100644
--- a/spec/frontend/ci_variable_list/mocks.js
+++ b/spec/frontend/ci_variable_list/mocks.js
@@ -45,6 +45,12 @@ const createDefaultVars = ({ withScope = true, kind } = {}) => {
return {
__typename: `Ci${kind}VariableConnection`,
+ pageInfo: {
+ startCursor: 'adsjsd12kldpsa',
+ endCursor: 'adsjsd12kldpsa',
+ hasPreviousPage: false,
+ hasNextPage: true,
+ },
nodes: base,
};
};
diff --git a/spec/frontend/ci_variable_list/store/actions_spec.js b/spec/frontend/ci_variable_list/store/actions_spec.js
index eb31fcd3ef4..e8c81a53a55 100644
--- a/spec/frontend/ci_variable_list/store/actions_spec.js
+++ b/spec/frontend/ci_variable_list/store/actions_spec.js
@@ -5,7 +5,7 @@ import * as actions from '~/ci_variable_list/store/actions';
import * as types from '~/ci_variable_list/store/mutation_types';
import getInitialState from '~/ci_variable_list/store/state';
import { prepareDataForDisplay, prepareEnvironments } from '~/ci_variable_list/store/utils';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import mockData from '../services/mock_data';
@@ -118,7 +118,7 @@ describe('CI variable list store actions', () => {
},
],
);
- expect(createFlash).toHaveBeenCalled();
+ expect(createAlert).toHaveBeenCalled();
});
});
@@ -155,7 +155,7 @@ describe('CI variable list store actions', () => {
},
],
);
- expect(createFlash).toHaveBeenCalled();
+ expect(createAlert).toHaveBeenCalled();
});
});
@@ -192,7 +192,7 @@ describe('CI variable list store actions', () => {
},
],
);
- expect(createFlash).toHaveBeenCalled();
+ expect(createAlert).toHaveBeenCalled();
});
});
@@ -219,7 +219,7 @@ describe('CI variable list store actions', () => {
mock.onGet(state.endpoint).reply(500);
await testAction(actions.fetchVariables, {}, state, [], [{ type: 'requestVariables' }]);
- expect(createFlash).toHaveBeenCalledWith({
+ expect(createAlert).toHaveBeenCalledWith({
message: 'There was an error fetching the variables.',
});
});
@@ -249,7 +249,7 @@ describe('CI variable list store actions', () => {
await testAction(actions.fetchEnvironments, {}, state, [], [{ type: 'requestEnvironments' }]);
- expect(createFlash).toHaveBeenCalledWith({
+ expect(createAlert).toHaveBeenCalledWith({
message: 'There was an error fetching the environments information.',
});
});
diff --git a/spec/frontend/clusters_list/store/actions_spec.js b/spec/frontend/clusters_list/store/actions_spec.js
index 7663f329b3f..09b1f80ff9b 100644
--- a/spec/frontend/clusters_list/store/actions_spec.js
+++ b/spec/frontend/clusters_list/store/actions_spec.js
@@ -5,7 +5,7 @@ import waitForPromises from 'helpers/wait_for_promises';
import { MAX_REQUESTS } from '~/clusters_list/constants';
import * as actions from '~/clusters_list/store/actions';
import * as types from '~/clusters_list/store/mutation_types';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import Poll from '~/lib/utils/poll';
import { apiData } from '../mock_data';
@@ -98,7 +98,7 @@ describe('Clusters store actions', () => {
},
],
);
- expect(createFlash).toHaveBeenCalledWith({
+ expect(createAlert).toHaveBeenCalledWith({
message: expect.stringMatching('error'),
});
});
diff --git a/spec/frontend/code_navigation/utils/index_spec.js b/spec/frontend/code_navigation/utils/index_spec.js
index 700c912029c..6f0d93c466c 100644
--- a/spec/frontend/code_navigation/utils/index_spec.js
+++ b/spec/frontend/code_navigation/utils/index_spec.js
@@ -87,5 +87,13 @@ describe('addInteractionClass', () => {
expect(spans[1].textContent).toBe('Text');
expect(spans[2].textContent).toBe(' ');
});
+
+ it('adds the correct class names to wrapped nodes', () => {
+ setHTMLFixture(
+ '<div data-path="index.js"><div class="blob-content"><div id="LC1" class="line"><span class="test"> Text </span></div></div></div>',
+ );
+ addInteractionClass({ ...params, wrapTextNodes: true });
+ expect(findAllSpans()[1].classList.contains('test')).toBe(true);
+ });
});
});
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 fddc767953a..16737003fa0 100644
--- a/spec/frontend/commit/commit_box_pipeline_mini_graph_spec.js
+++ b/spec/frontend/commit/commit_box_pipeline_mini_graph_spec.js
@@ -5,7 +5,7 @@ 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 { createAlert } from '~/flash';
import CommitBoxPipelineMiniGraph from '~/projects/commit_box/info/components/commit_box_pipeline_mini_graph.vue';
import PipelineMiniGraph from '~/pipelines/components/pipeline_mini_graph/pipeline_mini_graph.vue';
import { COMMIT_BOX_POLL_INTERVAL } from '~/projects/commit_box/info/constants';
@@ -178,12 +178,12 @@ describe('Commit box pipeline mini graph', () => {
});
describe('error state', () => {
- it('createFlash should show if there is an error fetching the data', async () => {
+ it('createAlert should show if there is an error fetching the data', async () => {
createComponent({ handler: failedHandler });
await waitForPromises();
- expect(createFlash).toHaveBeenCalledWith({
+ expect(createAlert).toHaveBeenCalledWith({
message: 'There was a problem fetching linked pipelines.',
});
});
diff --git a/spec/frontend/commit/commit_pipeline_status_component_spec.js b/spec/frontend/commit/commit_pipeline_status_component_spec.js
index 73720c1cc88..e75fb697a7b 100644
--- a/spec/frontend/commit/commit_pipeline_status_component_spec.js
+++ b/spec/frontend/commit/commit_pipeline_status_component_spec.js
@@ -3,7 +3,7 @@ import { shallowMount } from '@vue/test-utils';
import Visibility from 'visibilityjs';
import { nextTick } from 'vue';
import fixture from 'test_fixtures/pipelines/pipelines.json';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import Poll from '~/lib/utils/poll';
import CommitPipelineStatus from '~/projects/tree/components/commit_pipeline_status_component.vue';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
@@ -170,7 +170,7 @@ describe('Commit pipeline status component', () => {
});
it('displays flash error message', () => {
- expect(createFlash).toHaveBeenCalled();
+ expect(createAlert).toHaveBeenCalled();
});
});
});
diff --git a/spec/frontend/commit/components/commit_box_pipeline_status_spec.js b/spec/frontend/commit/components/commit_box_pipeline_status_spec.js
index db7b7b45397..8d455f8a3d7 100644
--- a/spec/frontend/commit/components/commit_box_pipeline_status_spec.js
+++ b/spec/frontend/commit/components/commit_box_pipeline_status_spec.js
@@ -4,7 +4,7 @@ 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 { createAlert } 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 {
@@ -78,7 +78,7 @@ describe('Commit box pipeline status', () => {
expect(findStatusIcon().exists()).toBe(true);
expect(findLoadingIcon().exists()).toBe(false);
- expect(createFlash).toHaveBeenCalledTimes(0);
+ expect(createAlert).toHaveBeenCalledTimes(0);
});
it('should link to the latest pipeline', () => {
@@ -97,12 +97,12 @@ describe('Commit box pipeline status', () => {
});
describe('error state', () => {
- it('createFlash should show if there is an error fetching the pipeline status', async () => {
+ it('createAlert should show if there is an error fetching the pipeline status', async () => {
createComponent(failedHandler);
await waitForPromises();
- expect(createFlash).toHaveBeenCalledWith({
+ expect(createAlert).toHaveBeenCalledWith({
message: PIPELINE_STATUS_FETCH_ERROR,
});
});
diff --git a/spec/frontend/content_editor/components/content_editor_spec.js b/spec/frontend/content_editor/components/content_editor_spec.js
index ae52cb05eaf..c1c2a125515 100644
--- a/spec/frontend/content_editor/components/content_editor_spec.js
+++ b/spec/frontend/content_editor/components/content_editor_spec.js
@@ -13,6 +13,7 @@ import MediaBubbleMenu from '~/content_editor/components/bubble_menus/media_bubb
import TopToolbar from '~/content_editor/components/top_toolbar.vue';
import LoadingIndicator from '~/content_editor/components/loading_indicator.vue';
import waitForPromises from 'helpers/wait_for_promises';
+import { KEYDOWN_EVENT } from '~/content_editor/constants';
jest.mock('~/emoji');
@@ -26,12 +27,13 @@ describe('ContentEditor', () => {
const findEditorStateObserver = () => wrapper.findComponent(EditorStateObserver);
const findLoadingIndicator = () => wrapper.findComponent(LoadingIndicator);
const findContentEditorAlert = () => wrapper.findComponent(ContentEditorAlert);
- const createWrapper = ({ markdown } = {}) => {
+ const createWrapper = ({ markdown, autofocus } = {}) => {
wrapper = shallowMountExtended(ContentEditor, {
propsData: {
renderMarkdown,
uploadsPath,
markdown,
+ autofocus,
},
stubs: {
EditorStateObserver,
@@ -70,14 +72,22 @@ describe('ContentEditor', () => {
expect(editorContent.classes()).toContain('md');
});
- it('renders ContentEditorProvider component', async () => {
- await createWrapper();
+ it('allows setting the tiptap editor to autofocus', async () => {
+ createWrapper({ autofocus: 'start' });
+
+ await nextTick();
+
+ expect(findEditorContent().props().editor.options.autofocus).toBe('start');
+ });
+
+ it('renders ContentEditorProvider component', () => {
+ createWrapper();
expect(wrapper.findComponent(ContentEditorProvider).exists()).toBe(true);
});
- it('renders top toolbar component', async () => {
- await createWrapper();
+ it('renders top toolbar component', () => {
+ createWrapper();
expect(wrapper.findComponent(TopToolbar).exists()).toBe(true);
});
@@ -213,6 +223,17 @@ describe('ContentEditor', () => {
});
});
+ describe('when editorStateObserver emits keydown event', () => {
+ it('bubbles up event', () => {
+ const event = new Event('keydown');
+
+ createWrapper();
+
+ findEditorStateObserver().vm.$emit(KEYDOWN_EVENT, event);
+ expect(wrapper.emitted(KEYDOWN_EVENT)).toEqual([[event]]);
+ });
+ });
+
it.each`
name | component
${'formatting'} | ${FormattingBubbleMenu}
diff --git a/spec/frontend/content_editor/components/editor_state_observer_spec.js b/spec/frontend/content_editor/components/editor_state_observer_spec.js
index e8c2d8c8793..9b42f61c98c 100644
--- a/spec/frontend/content_editor/components/editor_state_observer_spec.js
+++ b/spec/frontend/content_editor/components/editor_state_observer_spec.js
@@ -4,7 +4,7 @@ import EditorStateObserver, {
tiptapToComponentMap,
} from '~/content_editor/components/editor_state_observer.vue';
import eventHubFactory from '~/helpers/event_hub_factory';
-import { ALERT_EVENT } from '~/content_editor/constants';
+import { ALERT_EVENT, KEYDOWN_EVENT } from '~/content_editor/constants';
import { createTestEditor } from '../test_utils';
describe('content_editor/components/editor_state_observer', () => {
@@ -14,6 +14,7 @@ describe('content_editor/components/editor_state_observer', () => {
let onSelectionUpdateListener;
let onTransactionListener;
let onAlertListener;
+ let onKeydownListener;
let eventHub;
const buildEditor = () => {
@@ -30,6 +31,7 @@ describe('content_editor/components/editor_state_observer', () => {
selectionUpdate: onSelectionUpdateListener,
transaction: onTransactionListener,
[ALERT_EVENT]: onAlertListener,
+ [KEYDOWN_EVENT]: onKeydownListener,
},
});
};
@@ -39,6 +41,7 @@ describe('content_editor/components/editor_state_observer', () => {
onSelectionUpdateListener = jest.fn();
onTransactionListener = jest.fn();
onAlertListener = jest.fn();
+ onKeydownListener = jest.fn();
buildEditor();
});
@@ -67,8 +70,9 @@ describe('content_editor/components/editor_state_observer', () => {
});
it.each`
- event | listener
- ${ALERT_EVENT} | ${() => onAlertListener}
+ event | listener
+ ${ALERT_EVENT} | ${() => onAlertListener}
+ ${KEYDOWN_EVENT} | ${() => onKeydownListener}
`('listens to $event event in the eventBus object', ({ event, listener }) => {
const args = {};
@@ -97,6 +101,7 @@ describe('content_editor/components/editor_state_observer', () => {
it.each`
event
${ALERT_EVENT}
+ ${KEYDOWN_EVENT}
`('removes $event event hook from eventHub', ({ event }) => {
jest.spyOn(eventHub, '$off');
jest.spyOn(eventHub, '$on');
diff --git a/spec/frontend/content_editor/components/suggestions_dropdown_spec.js b/spec/frontend/content_editor/components/suggestions_dropdown_spec.js
new file mode 100644
index 00000000000..e72eb892e74
--- /dev/null
+++ b/spec/frontend/content_editor/components/suggestions_dropdown_spec.js
@@ -0,0 +1,286 @@
+import { GlAvatarLabeled, GlDropdownItem } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import SuggestionsDropdown from '~/content_editor/components/suggestions_dropdown.vue';
+
+describe('~/content_editor/components/suggestions_dropdown', () => {
+ let wrapper;
+
+ const buildWrapper = ({ propsData } = {}) => {
+ wrapper = extendedWrapper(
+ shallowMount(SuggestionsDropdown, {
+ propsData: {
+ nodeType: 'reference',
+ command: jest.fn(),
+ ...propsData,
+ },
+ }),
+ );
+ };
+
+ const exampleUser = { username: 'root', avatar_url: 'root_avatar.png', type: 'User' };
+ const exampleIssue = { iid: 123, title: 'Test Issue' };
+ const exampleMergeRequest = { iid: 224, title: 'Test MR' };
+ const exampleMilestone1 = { iid: 21, title: '13' };
+ const exampleMilestone2 = { iid: 24, title: 'Milestone with spaces' };
+
+ const exampleCommand = {
+ name: 'due',
+ description: 'Set due date',
+ params: ['<in 2 days | this Friday | December 31st>'],
+ };
+ const exampleEpic = {
+ iid: 8884,
+ title: '❓ Remote Development | Solution validation',
+ reference: 'gitlab-org&8884',
+ };
+ const exampleLabel1 = {
+ title: 'Create',
+ color: '#E44D2A',
+ type: 'GroupLabel',
+ textColor: '#FFFFFF',
+ };
+ const exampleLabel2 = {
+ title: 'Weekly Team Announcement',
+ color: '#E44D2A',
+ type: 'GroupLabel',
+ textColor: '#FFFFFF',
+ };
+ const exampleLabel3 = {
+ title: 'devops::create',
+ color: '#E44D2A',
+ type: 'GroupLabel',
+ textColor: '#FFFFFF',
+ };
+ const exampleVulnerability = {
+ id: 60850147,
+ title: 'System procs network activity',
+ };
+ const exampleSnippet = {
+ id: 2420859,
+ title: 'Project creation QueryRecorder logs',
+ };
+ const exampleEmoji = {
+ c: 'people',
+ e: '😃',
+ d: 'smiling face with open mouth',
+ u: '6.0',
+ name: 'smiley',
+ };
+
+ const insertedEmojiProps = {
+ name: 'smiley',
+ title: 'smiling face with open mouth',
+ moji: '😃',
+ unicodeVersion: '6.0',
+ };
+
+ describe('on item select', () => {
+ it.each`
+ nodeType | referenceType | char | reference | insertedText | insertedProps
+ ${'reference'} | ${'user'} | ${'@'} | ${exampleUser} | ${`@root`} | ${{}}
+ ${'reference'} | ${'issue'} | ${'#'} | ${exampleIssue} | ${`#123`} | ${{}}
+ ${'reference'} | ${'merge_request'} | ${'!'} | ${exampleMergeRequest} | ${`!224`} | ${{}}
+ ${'reference'} | ${'milestone'} | ${'%'} | ${exampleMilestone1} | ${`%13`} | ${{}}
+ ${'reference'} | ${'milestone'} | ${'%'} | ${exampleMilestone2} | ${`%Milestone with spaces`} | ${{ originalText: '%"Milestone with spaces"' }}
+ ${'reference'} | ${'command'} | ${'/'} | ${exampleCommand} | ${'/due'} | ${{}}
+ ${'reference'} | ${'epic'} | ${'&'} | ${exampleEpic} | ${`gitlab-org&8884`} | ${{}}
+ ${'reference'} | ${'label'} | ${'~'} | ${exampleLabel1} | ${`Create`} | ${{}}
+ ${'reference'} | ${'label'} | ${'~'} | ${exampleLabel2} | ${`Weekly Team Announcement`} | ${{ originalText: '~"Weekly Team Announcement"' }}
+ ${'reference'} | ${'label'} | ${'~'} | ${exampleLabel3} | ${`devops::create`} | ${{ originalText: '~"devops::create"', text: 'devops::create' }}
+ ${'reference'} | ${'vulnerability'} | ${'[vulnerability:'} | ${exampleVulnerability} | ${`[vulnerability:60850147]`} | ${{}}
+ ${'reference'} | ${'snippet'} | ${'$'} | ${exampleSnippet} | ${`$2420859`} | ${{}}
+ ${'emoji'} | ${'emoji'} | ${':'} | ${exampleEmoji} | ${`😃`} | ${insertedEmojiProps}
+ `(
+ 'runs a command to insert the selected $referenceType',
+ ({ char, nodeType, referenceType, reference, insertedText, insertedProps }) => {
+ const commandSpy = jest.fn();
+
+ buildWrapper({
+ propsData: {
+ char,
+ command: commandSpy,
+ nodeType,
+ nodeProps: {
+ referenceType,
+ test: 'prop',
+ },
+ items: [reference],
+ },
+ });
+
+ wrapper.findComponent(GlDropdownItem).vm.$emit('click');
+
+ expect(commandSpy).toHaveBeenCalledWith(
+ expect.objectContaining({
+ text: insertedText,
+ test: 'prop',
+ ...insertedProps,
+ }),
+ );
+ },
+ );
+ });
+
+ describe('rendering user references', () => {
+ it('displays avatar labeled component', () => {
+ buildWrapper({
+ propsData: {
+ char: '@',
+ nodeProps: {
+ referenceType: 'user',
+ },
+ items: [exampleUser],
+ },
+ });
+
+ expect(wrapper.findComponent(GlAvatarLabeled).attributes()).toEqual(
+ expect.objectContaining({
+ label: exampleUser.username,
+ shape: 'circle',
+ src: exampleUser.avatar_url,
+ }),
+ );
+ });
+
+ describe.each`
+ referenceType | char | reference | displaysID
+ ${'issue'} | ${'#'} | ${exampleIssue} | ${true}
+ ${'merge_request'} | ${'!'} | ${exampleMergeRequest} | ${true}
+ ${'milestone'} | ${'%'} | ${exampleMilestone1} | ${false}
+ `('rendering $referenceType references', ({ referenceType, char, reference, displaysID }) => {
+ it(`displays ${referenceType} ID and title`, () => {
+ buildWrapper({
+ propsData: {
+ char,
+ nodeType: 'reference',
+ nodeProps: {
+ referenceType,
+ },
+ items: [reference],
+ },
+ });
+
+ if (displaysID) expect(wrapper.text()).toContain(`${reference.iid}`);
+ else expect(wrapper.text()).not.toContain(`${reference.iid}`);
+ expect(wrapper.text()).toContain(`${reference.title}`);
+ });
+ });
+
+ describe.each`
+ referenceType | char | reference
+ ${'snippet'} | ${'$'} | ${exampleSnippet}
+ ${'vulnerability'} | ${'[vulnerability:'} | ${exampleVulnerability}
+ `('rendering $referenceType references', ({ referenceType, char, reference }) => {
+ it(`displays ${referenceType} ID and title`, () => {
+ buildWrapper({
+ propsData: {
+ char,
+ nodeProps: {
+ referenceType,
+ },
+ items: [reference],
+ },
+ });
+
+ expect(wrapper.text()).toContain(`${reference.id}`);
+ expect(wrapper.text()).toContain(`${reference.title}`);
+ });
+ });
+
+ describe('rendering label references', () => {
+ it.each`
+ label | displayedTitle | displayedColor
+ ${exampleLabel1} | ${'Create'} | ${'rgb(228, 77, 42)' /* #E44D2A */}
+ ${exampleLabel2} | ${'Weekly Team Announcement'} | ${'rgb(228, 77, 42)' /* #E44D2A */}
+ ${exampleLabel3} | ${'devops::create'} | ${'rgb(228, 77, 42)' /* #E44D2A */}
+ `('displays label title and color', ({ label, displayedTitle, displayedColor }) => {
+ buildWrapper({
+ propsData: {
+ char: '~',
+ nodeProps: {
+ referenceType: 'label',
+ },
+ items: [label],
+ },
+ });
+
+ expect(wrapper.text()).toContain(displayedTitle);
+ expect(wrapper.text()).not.toContain('"'); // no quotes in the dropdown list
+ expect(wrapper.findByTestId('label-color-box').attributes().style).toEqual(
+ `background-color: ${displayedColor};`,
+ );
+ });
+ });
+
+ describe('rendering epic references', () => {
+ it('displays epic title and reference', () => {
+ buildWrapper({
+ propsData: {
+ char: '&',
+ nodeProps: {
+ referenceType: 'epic',
+ },
+ items: [exampleEpic],
+ },
+ });
+
+ expect(wrapper.text()).toContain(`${exampleEpic.reference}`);
+ expect(wrapper.text()).toContain(`${exampleEpic.title}`);
+ });
+ });
+
+ describe('rendering a command (quick action)', () => {
+ it('displays command name with a slash', () => {
+ buildWrapper({
+ propsData: {
+ char: '/',
+ nodeProps: {
+ referenceType: 'command',
+ },
+ items: [exampleCommand],
+ },
+ });
+
+ expect(wrapper.text()).toContain(`${exampleCommand.name} `);
+ });
+ });
+
+ describe('rendering emoji references', () => {
+ it('displays emoji', () => {
+ const testEmojis = [
+ {
+ c: 'people',
+ e: '😄',
+ d: 'smiling face with open mouth and smiling eyes',
+ u: '6.0',
+ name: 'smile',
+ },
+ {
+ c: 'people',
+ e: '😸',
+ d: 'grinning cat face with smiling eyes',
+ u: '6.0',
+ name: 'smile_cat',
+ },
+ { c: 'people', e: '😃', d: 'smiling face with open mouth', u: '6.0', name: 'smiley' },
+ ];
+
+ buildWrapper({
+ propsData: {
+ char: ':',
+ nodeType: 'emoji',
+ nodeProps: {},
+ items: testEmojis,
+ },
+ });
+
+ testEmojis.forEach((testEmoji) => {
+ expect(wrapper.text()).toContain(testEmoji.e);
+ expect(wrapper.text()).toContain(testEmoji.d);
+ expect(wrapper.text()).toContain(testEmoji.name);
+ });
+ });
+ });
+ });
+});
diff --git a/spec/frontend/content_editor/components/wrappers/label_spec.js b/spec/frontend/content_editor/components/wrappers/label_spec.js
new file mode 100644
index 00000000000..9e58669b0ea
--- /dev/null
+++ b/spec/frontend/content_editor/components/wrappers/label_spec.js
@@ -0,0 +1,36 @@
+import { GlLabel } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import LabelWrapper from '~/content_editor/components/wrappers/label.vue';
+
+describe('content/components/wrappers/label', () => {
+ let wrapper;
+
+ const createWrapper = async (node = {}) => {
+ wrapper = shallowMountExtended(LabelWrapper, {
+ propsData: { node },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it("renders a GlLabel with the node's text and color", () => {
+ createWrapper({ attrs: { color: '#ff0000', text: 'foo bar', originalText: '~"foo bar"' } });
+
+ const glLabel = wrapper.findComponent(GlLabel);
+
+ expect(glLabel.props()).toMatchObject(
+ expect.objectContaining({
+ title: 'foo bar',
+ backgroundColor: '#ff0000',
+ }),
+ );
+ });
+
+ it('renders a scoped label if there is a "::" in the label', () => {
+ createWrapper({ attrs: { color: '#ff0000', text: 'foo::bar', originalText: '~"foo::bar"' } });
+
+ expect(wrapper.findComponent(GlLabel).props().scoped).toBe(true);
+ });
+});
diff --git a/spec/frontend/content_editor/extensions/heading_spec.js b/spec/frontend/content_editor/extensions/heading_spec.js
new file mode 100644
index 00000000000..2fa25e03cdc
--- /dev/null
+++ b/spec/frontend/content_editor/extensions/heading_spec.js
@@ -0,0 +1,54 @@
+import Heading from '~/content_editor/extensions/heading';
+import { createTestEditor, createDocBuilder, triggerNodeInputRule } from '../test_utils';
+
+describe('content_editor/extensions/heading', () => {
+ let tiptapEditor;
+ let doc;
+ let p;
+ let heading;
+
+ beforeEach(() => {
+ tiptapEditor = createTestEditor({ extensions: [Heading] });
+ ({
+ builders: { doc, p, heading },
+ } = createDocBuilder({
+ tiptapEditor,
+ names: {
+ heading: { nodeType: Heading.name },
+ },
+ }));
+ });
+
+ describe('when typing a valid heading input rule', () => {
+ it.each`
+ level | inputRuleText
+ ${1} | ${'# '}
+ ${2} | ${'## '}
+ ${3} | ${'### '}
+ ${4} | ${'#### '}
+ ${5} | ${'##### '}
+ ${6} | ${'###### '}
+ `('inserts a heading node for $inputRuleText', ({ level, inputRuleText }) => {
+ const expectedDoc = doc(heading({ level }));
+
+ triggerNodeInputRule({ tiptapEditor, inputRuleText });
+
+ expect(tiptapEditor.getJSON()).toEqual(expectedDoc.toJSON());
+ });
+ });
+
+ describe('when typing a invalid heading input rule', () => {
+ it.each`
+ inputRuleText
+ ${'#hi'}
+ ${'#\n'}
+ `('does not insert a heading node for $inputRuleText', ({ inputRuleText }) => {
+ const expectedDoc = doc(p());
+
+ triggerNodeInputRule({ tiptapEditor, inputRuleText });
+
+ // no change to the document
+ expect(tiptapEditor.getJSON()).toEqual(expectedDoc.toJSON());
+ });
+ });
+});
diff --git a/spec/frontend/content_editor/markdown_processing_spec_helper.js b/spec/frontend/content_editor/markdown_processing_spec_helper.js
index 228d009e42c..6f10f294fb0 100644
--- a/spec/frontend/content_editor/markdown_processing_spec_helper.js
+++ b/spec/frontend/content_editor/markdown_processing_spec_helper.js
@@ -1,7 +1,10 @@
import fs from 'fs';
import jsYaml from 'js-yaml';
import { memoize } from 'lodash';
+import MockAdapter from 'axios-mock-adapter';
+import axios from 'axios';
import { createContentEditor } from '~/content_editor';
+import httpStatus from '~/lib/utils/http_status';
const getFocusedMarkdownExamples = memoize(
() => process.env.FOCUSED_MARKDOWN_EXAMPLES?.split(',') || [],
@@ -42,6 +45,11 @@ const loadMarkdownApiExamples = (markdownYamlPath) => {
};
const testSerializesHtmlToMarkdownForElement = async ({ markdown, html }) => {
+ const mock = new MockAdapter(axios);
+
+ // Ignore any API requests from the suggestions plugin
+ mock.onGet().reply(httpStatus.OK, []);
+
const contentEditor = createContentEditor({
// Overwrite renderMarkdown to always return this specific html
renderMarkdown: () => html,
@@ -55,6 +63,8 @@ const testSerializesHtmlToMarkdownForElement = async ({ markdown, html }) => {
// Assert that the markdown we ended up with after sending it through all the ContentEditor
// plumbing matches the original markdown from the YAML.
expect(serializedContent.trim()).toBe(markdown.trim());
+
+ mock.restore();
};
// describeMarkdownProcesssing
@@ -74,7 +84,7 @@ export const describeMarkdownProcessing = (description, markdownYamlPath) => {
return;
}
- it(exampleName, async () => {
+ it(`${exampleName}`, async () => {
await testSerializesHtmlToMarkdownForElement(example);
});
});
diff --git a/spec/frontend/content_editor/services/markdown_serializer_spec.js b/spec/frontend/content_editor/services/markdown_serializer_spec.js
index 56394c85e8b..32193d97fd8 100644
--- a/spec/frontend/content_editor/services/markdown_serializer_spec.js
+++ b/spec/frontend/content_editor/services/markdown_serializer_spec.js
@@ -1204,6 +1204,24 @@ Oranges are orange [^1]
);
});
+ it('correctly adds a space between a preceding block element and a markdown table', () => {
+ expect(
+ serialize(
+ bulletList(listItem(paragraph('List item 1')), listItem(paragraph('List item 2'))),
+ table(tableRow(tableHeader(paragraph('header'))), tableRow(tableCell(paragraph('cell')))),
+ ).trim(),
+ ).toBe(
+ `
+* List item 1
+* List item 2
+
+| header |
+|--------|
+| cell |
+ `.trim(),
+ );
+ });
+
it('correctly serializes reference definition', () => {
expect(
serialize(
diff --git a/spec/frontend/content_editor/services/track_input_rules_and_shortcuts_spec.js b/spec/frontend/content_editor/services/track_input_rules_and_shortcuts_spec.js
index 459780cc7cf..8c1a3831a74 100644
--- a/spec/frontend/content_editor/services/track_input_rules_and_shortcuts_spec.js
+++ b/spec/frontend/content_editor/services/track_input_rules_and_shortcuts_spec.js
@@ -44,7 +44,7 @@ describe('content_editor/services/track_input_rules_and_shortcuts', () => {
describe('when creating a heading using an keyboard shortcut', () => {
it('sends a tracking event indicating that a heading was created using an input rule', async () => {
- const shortcuts = Heading.config.addKeyboardShortcuts.call(Heading);
+ const shortcuts = Heading.parent.config.addKeyboardShortcuts.call(Heading);
const [firstShortcut] = Object.keys(shortcuts);
const nodeName = Heading.name;
diff --git a/spec/frontend/contributors/component/contributors_spec.js b/spec/frontend/contributors/component/contributors_spec.js
index bdf3b3636ed..2f0b5719326 100644
--- a/spec/frontend/contributors/component/contributors_spec.js
+++ b/spec/frontend/contributors/component/contributors_spec.js
@@ -1,4 +1,5 @@
import { mount } from '@vue/test-utils';
+import { GlLoadingIcon } from '@gitlab/ui';
import MockAdapter from 'axios-mock-adapter';
import Vue, { nextTick } from 'vue';
import ContributorsCharts from '~/contributors/components/contributors.vue';
@@ -52,14 +53,14 @@ describe('Contributors charts', () => {
it('should display loader whiled loading data', async () => {
wrapper.vm.$store.state.loading = true;
await nextTick();
- expect(wrapper.find('.contributors-loader').exists()).toBe(true);
+ expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
});
it('should render charts when loading completed and there is chart data', async () => {
wrapper.vm.$store.state.loading = false;
wrapper.vm.$store.state.chartData = chartData;
await nextTick();
- expect(wrapper.find('.contributors-loader').exists()).toBe(false);
+ expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(false);
expect(wrapper.find('.contributors-charts').exists()).toBe(true);
expect(wrapper.element).toMatchSnapshot();
});
diff --git a/spec/frontend/contributors/store/actions_spec.js b/spec/frontend/contributors/store/actions_spec.js
index ef0ff8ca208..865f683a91a 100644
--- a/spec/frontend/contributors/store/actions_spec.js
+++ b/spec/frontend/contributors/store/actions_spec.js
@@ -2,7 +2,7 @@ import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
import * as actions from '~/contributors/stores/actions';
import * as types from '~/contributors/stores/mutation_types';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
jest.mock('~/flash.js');
@@ -47,7 +47,7 @@ describe('Contributors store actions', () => {
[{ type: types.SET_LOADING_STATE, payload: true }],
[],
);
- expect(createFlash).toHaveBeenCalledWith({
+ expect(createAlert).toHaveBeenCalledWith({
message: expect.stringMatching('error'),
});
});
diff --git a/spec/frontend/crm/contacts_root_spec.js b/spec/frontend/crm/contacts_root_spec.js
index 7aaaf480c44..ec7172434bf 100644
--- a/spec/frontend/crm/contacts_root_spec.js
+++ b/spec/frontend/crm/contacts_root_spec.js
@@ -87,7 +87,7 @@ describe('Customer relations contacts root app', () => {
editButtonLabel: 'Edit',
title: 'Customer relations contacts',
newContact: 'New contact',
- errorText: 'Something went wrong. Please try again.',
+ errorMsg: 'Something went wrong. Please try again.',
},
serverErrorMessage: '',
filterSearchKey: 'contacts',
@@ -117,6 +117,18 @@ describe('Customer relations contacts root app', () => {
expect(wrapper.text()).toContain('Something went wrong. Please try again.');
});
+
+ it('should be removed on error-alert-dismissed event', async () => {
+ mountComponent({ queryHandler: jest.fn().mockRejectedValue('ERROR') });
+ await waitForPromises();
+
+ expect(wrapper.text()).toContain('Something went wrong. Please try again.');
+
+ findTable().vm.$emit('error-alert-dismissed');
+ await waitForPromises();
+
+ expect(wrapper.text()).not.toContain('Something went wrong. Please try again.');
+ });
});
describe('on successful load', () => {
diff --git a/spec/frontend/crm/organizations_root_spec.js b/spec/frontend/crm/organizations_root_spec.js
index a0b56596177..1fcf6aa8f50 100644
--- a/spec/frontend/crm/organizations_root_spec.js
+++ b/spec/frontend/crm/organizations_root_spec.js
@@ -91,7 +91,7 @@ describe('Customer relations organizations root app', () => {
editButtonLabel: 'Edit',
title: 'Customer relations organizations',
newOrganization: 'New organization',
- errorText: 'Something went wrong. Please try again.',
+ errorMsg: 'Something went wrong. Please try again.',
},
serverErrorMessage: '',
filterSearchKey: 'organizations',
diff --git a/spec/frontend/cycle_analytics/value_stream_metrics_spec.js b/spec/frontend/cycle_analytics/value_stream_metrics_spec.js
index 9c8cd6a3dbc..948dc5c9be2 100644
--- a/spec/frontend/cycle_analytics/value_stream_metrics_spec.js
+++ b/spec/frontend/cycle_analytics/value_stream_metrics_spec.js
@@ -8,7 +8,7 @@ import { METRIC_TYPE_SUMMARY } from '~/api/analytics_api';
import { VSA_METRICS_GROUPS, METRICS_POPOVER_CONTENT } from '~/analytics/shared/constants';
import { prepareTimeMetricsData } from '~/analytics/shared/utils';
import MetricTile from '~/analytics/shared/components/metric_tile.vue';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { group } from './mock_data';
jest.mock('~/flash');
@@ -177,7 +177,7 @@ describe('ValueStreamMetrics', () => {
});
it('should render an error message', () => {
- expect(createFlash).toHaveBeenCalledWith({
+ expect(createAlert).toHaveBeenCalledWith({
message: `There was an error while fetching value stream analytics ${fakeReqName} data.`,
});
});
diff --git a/spec/frontend/deploy_freeze/components/deploy_freeze_modal_spec.js b/spec/frontend/deploy_freeze/components/deploy_freeze_modal_spec.js
index bbafdc000db..113e0d8f60d 100644
--- a/spec/frontend/deploy_freeze/components/deploy_freeze_modal_spec.js
+++ b/spec/frontend/deploy_freeze/components/deploy_freeze_modal_spec.js
@@ -5,8 +5,9 @@ import Vuex from 'vuex';
import Api from '~/api';
import DeployFreezeModal from '~/deploy_freeze/components/deploy_freeze_modal.vue';
import createStore from '~/deploy_freeze/store';
-import TimezoneDropdown from '~/vue_shared/components/timezone_dropdown.vue';
-import { freezePeriodsFixture, timezoneDataFixture } from '../helpers';
+import TimezoneDropdown from '~/vue_shared/components/timezone_dropdown/timezone_dropdown.vue';
+import { freezePeriodsFixture } from '../helpers';
+import { timezoneDataFixture } from '../../vue_shared/components/timezone_dropdown/helpers';
jest.mock('~/api');
@@ -52,7 +53,7 @@ describe('Deploy freeze modal', () => {
describe('Basic interactions', () => {
it('button is disabled when freeze period is invalid', () => {
- expect(submitDeployFreezeButton().attributes('disabled')).toBeTruthy();
+ expect(submitDeployFreezeButton().attributes('disabled')).toBe('true');
});
});
@@ -92,7 +93,7 @@ describe('Deploy freeze modal', () => {
});
it('disables the add deploy freeze button', () => {
- expect(submitDeployFreezeButton().attributes('disabled')).toBeTruthy();
+ expect(submitDeployFreezeButton().attributes('disabled')).toBe('true');
});
});
@@ -103,7 +104,7 @@ describe('Deploy freeze modal', () => {
});
it('does not disable the submit button', () => {
- expect(submitDeployFreezeButton().attributes('disabled')).toBeFalsy();
+ expect(submitDeployFreezeButton().attributes('disabled')).toBeUndefined();
});
});
});
diff --git a/spec/frontend/deploy_freeze/components/deploy_freeze_settings_spec.js b/spec/frontend/deploy_freeze/components/deploy_freeze_settings_spec.js
index 637efe30022..27d8fea9d5e 100644
--- a/spec/frontend/deploy_freeze/components/deploy_freeze_settings_spec.js
+++ b/spec/frontend/deploy_freeze/components/deploy_freeze_settings_spec.js
@@ -5,7 +5,7 @@ import DeployFreezeModal from '~/deploy_freeze/components/deploy_freeze_modal.vu
import DeployFreezeSettings from '~/deploy_freeze/components/deploy_freeze_settings.vue';
import DeployFreezeTable from '~/deploy_freeze/components/deploy_freeze_table.vue';
import createStore from '~/deploy_freeze/store';
-import { timezoneDataFixture } from '../helpers';
+import { timezoneDataFixture } from '../../vue_shared/components/timezone_dropdown/helpers';
Vue.use(Vuex);
diff --git a/spec/frontend/deploy_freeze/components/deploy_freeze_table_spec.js b/spec/frontend/deploy_freeze/components/deploy_freeze_table_spec.js
index 137776edfab..c2d6eb399bc 100644
--- a/spec/frontend/deploy_freeze/components/deploy_freeze_table_spec.js
+++ b/spec/frontend/deploy_freeze/components/deploy_freeze_table_spec.js
@@ -5,7 +5,8 @@ import Vuex from 'vuex';
import DeployFreezeTable from '~/deploy_freeze/components/deploy_freeze_table.vue';
import createStore from '~/deploy_freeze/store';
import { RECEIVE_FREEZE_PERIODS_SUCCESS } from '~/deploy_freeze/store/mutation_types';
-import { freezePeriodsFixture, timezoneDataFixture } from '../helpers';
+import { freezePeriodsFixture } from '../helpers';
+import { timezoneDataFixture } from '../../vue_shared/components/timezone_dropdown/helpers';
Vue.use(Vuex);
diff --git a/spec/frontend/deploy_freeze/helpers.js b/spec/frontend/deploy_freeze/helpers.js
index 43e66183ab5..920901c97a8 100644
--- a/spec/frontend/deploy_freeze/helpers.js
+++ b/spec/frontend/deploy_freeze/helpers.js
@@ -1,10 +1,3 @@
import freezePeriodsFixture from 'test_fixtures/api/freeze-periods/freeze_periods.json';
-import timezoneDataFixture from 'test_fixtures/timezones/short.json';
-import { secondsToHours } from '~/lib/utils/datetime_utility';
-export { freezePeriodsFixture, timezoneDataFixture };
-
-export const findTzByName = (identifier = '') =>
- timezoneDataFixture.find(({ name }) => name.toLowerCase() === identifier.toLowerCase());
-
-export const formatTz = ({ offset, name }) => `[UTC ${secondsToHours(offset)}] ${name}`;
+export { freezePeriodsFixture };
diff --git a/spec/frontend/deploy_freeze/store/actions_spec.js b/spec/frontend/deploy_freeze/store/actions_spec.js
index ad67afdce75..ce0c924bed2 100644
--- a/spec/frontend/deploy_freeze/store/actions_spec.js
+++ b/spec/frontend/deploy_freeze/store/actions_spec.js
@@ -7,7 +7,8 @@ import getInitialState from '~/deploy_freeze/store/state';
import createFlash from '~/flash';
import * as logger from '~/lib/logger';
import axios from '~/lib/utils/axios_utils';
-import { freezePeriodsFixture, timezoneDataFixture } from '../helpers';
+import { freezePeriodsFixture } from '../helpers';
+import { timezoneDataFixture } from '../../vue_shared/components/timezone_dropdown/helpers';
jest.mock('~/api.js');
jest.mock('~/flash.js');
diff --git a/spec/frontend/deploy_freeze/store/mutations_spec.js b/spec/frontend/deploy_freeze/store/mutations_spec.js
index 878a755088c..984105d6655 100644
--- a/spec/frontend/deploy_freeze/store/mutations_spec.js
+++ b/spec/frontend/deploy_freeze/store/mutations_spec.js
@@ -2,7 +2,12 @@ import * as types from '~/deploy_freeze/store/mutation_types';
import mutations from '~/deploy_freeze/store/mutations';
import state from '~/deploy_freeze/store/state';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
-import { findTzByName, formatTz, freezePeriodsFixture, timezoneDataFixture } from '../helpers';
+import { formatTimezone } from '~/lib/utils/datetime_utility';
+import { freezePeriodsFixture } from '../helpers';
+import {
+ timezoneDataFixture,
+ findTzByName,
+} from '../../vue_shared/components/timezone_dropdown/helpers';
describe('Deploy freeze mutations', () => {
let stateCopy;
@@ -28,9 +33,9 @@ describe('Deploy freeze mutations', () => {
describe('RECEIVE_FREEZE_PERIODS_SUCCESS', () => {
it('should set freeze periods and format timezones from identifiers to names', () => {
const timezoneNames = {
- 'Europe/Berlin': '[UTC 2] Berlin',
+ 'Europe/Berlin': '[UTC + 2] Berlin',
'Etc/UTC': '[UTC 0] UTC',
- 'America/New_York': '[UTC -4] Eastern Time (US & Canada)',
+ 'America/New_York': '[UTC - 4] Eastern Time (US & Canada)',
};
mutations[types.RECEIVE_FREEZE_PERIODS_SUCCESS](stateCopy, freezePeriodsFixture);
@@ -51,7 +56,7 @@ describe('Deploy freeze mutations', () => {
it('should set the cron timezone', () => {
const selectedTz = findTzByName('Pacific Time (US & Canada)');
const timezone = {
- formattedTimezone: formatTz(selectedTz),
+ formattedTimezone: formatTimezone(selectedTz),
identifier: selectedTz.identifier,
};
mutations[types.SET_SELECTED_TIMEZONE](stateCopy, timezone);
diff --git a/spec/frontend/deploy_tokens/components/new_deploy_token_spec.js b/spec/frontend/deploy_tokens/components/new_deploy_token_spec.js
new file mode 100644
index 00000000000..19e9ba8b268
--- /dev/null
+++ b/spec/frontend/deploy_tokens/components/new_deploy_token_spec.js
@@ -0,0 +1,103 @@
+import { shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
+import { GlButton, GlFormCheckbox, GlFormInput, GlFormInputGroup, GlDatepicker } from '@gitlab/ui';
+import MockAdapter from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
+import { TEST_HOST } from 'helpers/test_constants';
+import NewDeployToken from '~/deploy_tokens/components/new_deploy_token.vue';
+import waitForPromises from 'helpers/wait_for_promises';
+
+const createNewTokenPath = `${TEST_HOST}/create`;
+const deployTokensHelpUrl = `${TEST_HOST}/help`;
+describe('New Deploy Token', () => {
+ let wrapper;
+
+ const factory = (options = {}) => {
+ const defaults = {
+ containerRegistryEnabled: true,
+ packagesRegistryEnabled: true,
+ tokenType: 'project',
+ };
+ const { containerRegistryEnabled, packagesRegistryEnabled, tokenType } = {
+ ...defaults,
+ ...options,
+ };
+ return shallowMount(NewDeployToken, {
+ propsData: {
+ deployTokensHelpUrl,
+ containerRegistryEnabled,
+ packagesRegistryEnabled,
+ createNewTokenPath,
+ tokenType,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('without a container registry', () => {
+ beforeEach(() => {
+ wrapper = factory({ containerRegistryEnabled: false });
+ });
+
+ it('should not show the read registry scope', () => {
+ wrapper
+ .findAllComponents(GlFormCheckbox)
+ .wrappers.forEach((checkbox) => expect(checkbox.text()).not.toBe('read_registry'));
+ });
+ });
+
+ describe('with a container registry', () => {
+ beforeEach(() => {
+ wrapper = factory();
+ });
+
+ it('should show the read registry scope', () => {
+ const checkbox = wrapper.findAllComponents(GlFormCheckbox).at(1);
+ expect(checkbox.text()).toBe('read_registry');
+ });
+
+ it('should make a request to create a token on submit', () => {
+ const mockAxios = new MockAdapter(axios);
+
+ const date = new Date();
+ const formInputs = wrapper.findAllComponents(GlFormInput);
+ const name = formInputs.at(0);
+ const username = formInputs.at(2);
+ name.vm.$emit('input', 'test name');
+ username.vm.$emit('input', 'test username');
+
+ const datepicker = wrapper.findAllComponents(GlDatepicker).at(0);
+ datepicker.vm.$emit('input', date);
+
+ const [readRepo, readRegistry] = wrapper.findAllComponents(GlFormCheckbox).wrappers;
+ readRepo.vm.$emit('input', true);
+ readRegistry.vm.$emit('input', true);
+
+ mockAxios
+ .onPost(createNewTokenPath, {
+ deploy_token: {
+ name: 'test name',
+ expires_at: date.toISOString(),
+ username: 'test username',
+ read_repository: true,
+ read_registry: true,
+ },
+ })
+ .replyOnce(200, { username: 'test token username', token: 'test token' });
+
+ wrapper.findAllComponents(GlButton).at(0).vm.$emit('click');
+
+ return waitForPromises()
+ .then(() => nextTick())
+ .then(() => {
+ const [tokenUsername, tokenValue] = wrapper.findAllComponents(GlFormInputGroup).wrappers;
+
+ expect(tokenUsername.props('value')).toBe('test token username');
+ expect(tokenValue.props('value')).toBe('test token');
+ });
+ });
+ });
+});
diff --git a/spec/frontend/design_management/components/delete_button_spec.js b/spec/frontend/design_management/components/delete_button_spec.js
index cee1eec792d..426a61f5a47 100644
--- a/spec/frontend/design_management/components/delete_button_spec.js
+++ b/spec/frontend/design_management/components/delete_button_spec.js
@@ -29,12 +29,12 @@ describe('Batch delete button component', () => {
createComponent();
expect(findButton().exists()).toBe(true);
- expect(findButton().attributes('disabled')).toBeFalsy();
+ expect(findButton().attributes('disabled')).toBeUndefined();
});
it('renders disabled button when design is deleting', () => {
createComponent({ isDeleting: true });
- expect(findButton().attributes('disabled')).toBeTruthy();
+ expect(findButton().attributes('disabled')).toBe('true');
});
it('emits `delete-selected-designs` event on modal ok click', async () => {
@@ -45,7 +45,7 @@ describe('Batch delete button component', () => {
findModal().vm.$emit('ok');
await nextTick();
- expect(wrapper.emitted('delete-selected-designs')).toBeTruthy();
+ expect(wrapper.emitted('delete-selected-designs')).toHaveLength(1);
});
it('renders slot content', () => {
diff --git a/spec/frontend/design_management/components/design_notes/design_reply_form_spec.js b/spec/frontend/design_management/components/design_notes/design_reply_form_spec.js
index e36f5c79e3e..5fd61b25edc 100644
--- a/spec/frontend/design_management/components/design_notes/design_reply_form_spec.js
+++ b/spec/frontend/design_management/components/design_notes/design_reply_form_spec.js
@@ -1,16 +1,10 @@
import { mount } from '@vue/test-utils';
import { nextTick } from 'vue';
import Autosave from '~/autosave';
+import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
import DesignReplyForm from '~/design_management/components/design_notes/design_reply_form.vue';
-const showModal = jest.fn();
-
-const GlModal = {
- template: '<div><slot name="modal-title"></slot><slot></slot><slot name="modal-ok"></slot></div>',
- methods: {
- show: showModal,
- },
-};
+jest.mock('~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal');
describe('Design reply form component', () => {
let wrapper;
@@ -19,7 +13,6 @@ describe('Design reply form component', () => {
const findTextarea = () => wrapper.find('textarea');
const findSubmitButton = () => wrapper.findComponent({ ref: 'submitButton' });
const findCancelButton = () => wrapper.findComponent({ ref: 'cancelButton' });
- const findModal = () => wrapper.findComponent({ ref: 'cancelCommentModal' });
function createComponent(props = {}, mountOptions = {}) {
wrapper = mount(DesignReplyForm, {
@@ -29,7 +22,6 @@ describe('Design reply form component', () => {
noteableId: 'gid://gitlab/DesignManagement::Design/6',
...props,
},
- stubs: { GlModal },
...mountOptions,
});
}
@@ -42,6 +34,7 @@ describe('Design reply form component', () => {
afterEach(() => {
wrapper.destroy();
window.gon = originalGon;
+ confirmAction.mockReset();
});
it('textarea has focus after component mount', () => {
@@ -102,7 +95,7 @@ describe('Design reply form component', () => {
});
it('submit button is disabled', () => {
- expect(findSubmitButton().attributes().disabled).toBeTruthy();
+ expect(findSubmitButton().attributes().disabled).toBe('disabled');
});
it('does not emit submitForm event on textarea ctrl+enter keydown', async () => {
@@ -111,7 +104,7 @@ describe('Design reply form component', () => {
});
await nextTick();
- expect(wrapper.emitted('submit-form')).toBeFalsy();
+ expect(wrapper.emitted('submit-form')).toBeUndefined();
});
it('does not emit submitForm event on textarea meta+enter keydown', async () => {
@@ -120,13 +113,13 @@ describe('Design reply form component', () => {
});
await nextTick();
- expect(wrapper.emitted('submit-form')).toBeFalsy();
+ expect(wrapper.emitted('submit-form')).toBeUndefined();
});
it('emits cancelForm event on pressing escape button on textarea', () => {
findTextarea().trigger('keyup.esc');
- expect(wrapper.emitted('cancel-form')).toBeTruthy();
+ expect(wrapper.emitted('cancel-form')).toHaveLength(1);
});
it('emits cancelForm event on clicking Cancel button', () => {
@@ -144,7 +137,7 @@ describe('Design reply form component', () => {
});
it('submit button is enabled', () => {
- expect(findSubmitButton().attributes().disabled).toBeFalsy();
+ expect(findSubmitButton().attributes().disabled).toBeUndefined();
});
it('emits submitForm event on Comment button click', async () => {
@@ -153,7 +146,7 @@ describe('Design reply form component', () => {
findSubmitButton().vm.$emit('click');
await nextTick();
- expect(wrapper.emitted('submit-form')).toBeTruthy();
+ expect(wrapper.emitted('submit-form')).toHaveLength(1);
expect(autosaveResetSpy).toHaveBeenCalled();
});
@@ -165,7 +158,7 @@ describe('Design reply form component', () => {
});
await nextTick();
- expect(wrapper.emitted('submit-form')).toBeTruthy();
+ expect(wrapper.emitted('submit-form')).toHaveLength(1);
expect(autosaveResetSpy).toHaveBeenCalled();
});
@@ -177,7 +170,7 @@ describe('Design reply form component', () => {
});
await nextTick();
- expect(wrapper.emitted('submit-form')).toBeTruthy();
+ expect(wrapper.emitted('submit-form')).toHaveLength(1);
expect(autosaveResetSpy).toHaveBeenCalled();
});
@@ -185,13 +178,13 @@ describe('Design reply form component', () => {
findTextarea().setValue('test2');
await nextTick();
- expect(wrapper.emitted('input')).toBeTruthy();
+ expect(wrapper.emitted('input')).toEqual([['test'], ['test2']]);
});
it('emits cancelForm event on Escape key if text was not changed', () => {
findTextarea().trigger('keyup.esc');
- expect(wrapper.emitted('cancel-form')).toBeTruthy();
+ expect(wrapper.emitted('cancel-form')).toHaveLength(1);
});
it('opens confirmation modal on Escape key when text has changed', async () => {
@@ -199,13 +192,13 @@ describe('Design reply form component', () => {
await nextTick();
findTextarea().trigger('keyup.esc');
- expect(showModal).toHaveBeenCalled();
+ expect(confirmAction).toHaveBeenCalled();
});
it('emits cancelForm event on Cancel button click if text was not changed', () => {
findCancelButton().trigger('click');
- expect(wrapper.emitted('cancel-form')).toBeTruthy();
+ expect(wrapper.emitted('cancel-form')).toHaveLength(1);
});
it('opens confirmation modal on Cancel button click when text has changed', async () => {
@@ -213,17 +206,41 @@ describe('Design reply form component', () => {
await nextTick();
findCancelButton().trigger('click');
- expect(showModal).toHaveBeenCalled();
+ expect(confirmAction).toHaveBeenCalled();
});
- it('emits cancelForm event on modal Ok button click', () => {
+ it('emits cancelForm event when confirmed', async () => {
+ confirmAction.mockResolvedValueOnce(true);
const autosaveResetSpy = jest.spyOn(wrapper.vm.autosaveDiscussion, 'reset');
+ wrapper.setProps({ value: 'test3' });
+ await nextTick();
+
findTextarea().trigger('keyup.esc');
- findModal().vm.$emit('ok');
+ await nextTick();
+
+ expect(confirmAction).toHaveBeenCalled();
+ await nextTick();
- expect(wrapper.emitted('cancel-form')).toBeTruthy();
+ expect(wrapper.emitted('cancel-form')).toHaveLength(1);
expect(autosaveResetSpy).toHaveBeenCalled();
});
+
+ it("doesn't emit cancelForm event when not confirmed", async () => {
+ confirmAction.mockResolvedValueOnce(false);
+ const autosaveResetSpy = jest.spyOn(wrapper.vm.autosaveDiscussion, 'reset');
+
+ wrapper.setProps({ value: 'test3' });
+ await nextTick();
+
+ findTextarea().trigger('keyup.esc');
+ await nextTick();
+
+ expect(confirmAction).toHaveBeenCalled();
+ await nextTick();
+
+ expect(wrapper.emitted('cancel-form')).toBeUndefined();
+ expect(autosaveResetSpy).not.toHaveBeenCalled();
+ });
});
});
diff --git a/spec/frontend/design_management/components/design_overlay_spec.js b/spec/frontend/design_management/components/design_overlay_spec.js
index 056959425a6..169f2dbdccb 100644
--- a/spec/frontend/design_management/components/design_overlay_spec.js
+++ b/spec/frontend/design_management/components/design_overlay_spec.js
@@ -170,6 +170,14 @@ describe('Design overlay component', () => {
});
it('should call an update active discussion mutation when clicking a note without moving it', async () => {
+ createComponent({
+ notes,
+ dimensions: {
+ width: 400,
+ height: 400,
+ },
+ });
+
const note = notes[0];
const { position } = note;
const mutationVariables = {
diff --git a/spec/frontend/design_management/pages/design/index_spec.js b/spec/frontend/design_management/pages/design/index_spec.js
index 774e37a8b21..a11463ab663 100644
--- a/spec/frontend/design_management/pages/design/index_spec.js
+++ b/spec/frontend/design_management/pages/design/index_spec.js
@@ -23,7 +23,7 @@ import {
DESIGN_SNOWPLOW_EVENT_TYPES,
DESIGN_SERVICE_PING_EVENT_TYPES,
} from '~/design_management/utils/tracking';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import mockAllVersions from '../../mock_data/all_versions';
import design from '../../mock_data/design';
import mockResponseWithDesigns from '../../mock_data/designs';
@@ -301,8 +301,8 @@ describe('Design management design index page', () => {
wrapper.vm.onDesignQueryResult({ data: mockResponseNoDesigns, loading: false });
await nextTick();
- expect(createFlash).toHaveBeenCalledTimes(1);
- expect(createFlash).toHaveBeenCalledWith({ message: DESIGN_NOT_FOUND_ERROR });
+ expect(createAlert).toHaveBeenCalledTimes(1);
+ expect(createAlert).toHaveBeenCalledWith({ message: DESIGN_NOT_FOUND_ERROR });
expect(router.push).toHaveBeenCalledTimes(1);
expect(router.push).toHaveBeenCalledWith({ name: DESIGNS_ROUTE_NAME });
});
@@ -323,8 +323,8 @@ describe('Design management design index page', () => {
wrapper.vm.onDesignQueryResult({ data: mockResponseWithDesigns, loading: false });
await nextTick();
- expect(createFlash).toHaveBeenCalledTimes(1);
- expect(createFlash).toHaveBeenCalledWith({ message: DESIGN_VERSION_NOT_EXIST_ERROR });
+ expect(createAlert).toHaveBeenCalledTimes(1);
+ expect(createAlert).toHaveBeenCalledWith({ message: DESIGN_VERSION_NOT_EXIST_ERROR });
expect(router.push).toHaveBeenCalledTimes(1);
expect(router.push).toHaveBeenCalledWith({ name: DESIGNS_ROUTE_NAME });
});
diff --git a/spec/frontend/design_management/pages/index_spec.js b/spec/frontend/design_management/pages/index_spec.js
index 1033b509419..76ece922ded 100644
--- a/spec/frontend/design_management/pages/index_spec.js
+++ b/spec/frontend/design_management/pages/index_spec.js
@@ -29,7 +29,7 @@ import {
DESIGN_TRACKING_PAGE_NAME,
DESIGN_SNOWPLOW_EVENT_TYPES,
} from '~/design_management/utils/tracking';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import DesignDropzone from '~/vue_shared/components/upload_dropzone/upload_dropzone.vue';
import {
designListQueryResponse,
@@ -808,7 +808,7 @@ describe('Design management index page', () => {
await moveDesigns(wrapper);
await waitForPromises();
- expect(createFlash).toHaveBeenCalledWith({ message: 'Houston, we have a problem' });
+ expect(createAlert).toHaveBeenCalledWith({ message: 'Houston, we have a problem' });
});
it('displays alert if mutation had a non-recoverable error', async () => {
diff --git a/spec/frontend/design_management/utils/cache_update_spec.js b/spec/frontend/design_management/utils/cache_update_spec.js
index 5e2c37e24a1..42777adfd58 100644
--- a/spec/frontend/design_management/utils/cache_update_spec.js
+++ b/spec/frontend/design_management/utils/cache_update_spec.js
@@ -10,7 +10,7 @@ import {
ADD_IMAGE_DIFF_NOTE_ERROR,
UPDATE_IMAGE_DIFF_NOTE_ERROR,
} from '~/design_management/utils/error_messages';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import design from '../mock_data/design';
jest.mock('~/flash.js');
@@ -32,10 +32,10 @@ describe('Design Management cache update', () => {
${'updateStoreAfterUploadDesign'} | ${updateStoreAfterUploadDesign} | ${mockErrors[0]} | ${[]}
${'updateStoreAfterUpdateImageDiffNote'} | ${updateStoreAfterRepositionImageDiffNote} | ${UPDATE_IMAGE_DIFF_NOTE_ERROR} | ${[]}
`('$fnName handles errors in response', ({ subject, extraArgs, errorMessage }) => {
- expect(createFlash).not.toHaveBeenCalled();
+ expect(createAlert).not.toHaveBeenCalled();
expect(() => subject(mockStore, { errors: mockErrors }, {}, ...extraArgs)).toThrow();
- expect(createFlash).toHaveBeenCalledTimes(1);
- expect(createFlash).toHaveBeenCalledWith({ message: errorMessage });
+ expect(createAlert).toHaveBeenCalledTimes(1);
+ expect(createAlert).toHaveBeenCalledWith({ message: errorMessage });
});
});
});
diff --git a/spec/frontend/diffs/components/app_spec.js b/spec/frontend/diffs/components/app_spec.js
index b88206c3b9a..936f4744e94 100644
--- a/spec/frontend/diffs/components/app_spec.js
+++ b/spec/frontend/diffs/components/app_spec.js
@@ -152,6 +152,30 @@ describe('diffs/components/app', () => {
});
});
+ describe('fetch diff with no changes', () => {
+ beforeEach(() => {
+ const fetchResolver = () => {
+ store.state.diffs.retrievingBatches = false;
+ return Promise.resolve({ real_size: null });
+ };
+
+ createComponent();
+ jest.spyOn(wrapper.vm, 'fetchDiffFilesMeta').mockImplementation(fetchResolver);
+
+ return nextTick();
+ });
+
+ it('diff counter to be 0 after fetch', async () => {
+ expect(wrapper.vm.diffFilesLength).toEqual(0);
+ wrapper.vm.fetchData(false);
+
+ await nextTick();
+
+ expect(wrapper.vm.fetchDiffFilesMeta).toHaveBeenCalled();
+ expect(wrapper.vm.diffFilesLength).toEqual(0);
+ });
+ });
+
describe('codequality diff', () => {
it('does not fetch code quality data on FOSS', async () => {
createComponent();
diff --git a/spec/frontend/diffs/components/commit_item_spec.js b/spec/frontend/diffs/components/commit_item_spec.js
index 440f169be86..75d55376d09 100644
--- a/spec/frontend/diffs/components/commit_item_spec.js
+++ b/spec/frontend/diffs/components/commit_item_spec.js
@@ -82,7 +82,7 @@ describe('diffs/components/commit_item', () => {
const imgElement = avatarElement.find('img');
expect(avatarElement.attributes('href')).toBe(commit.author.web_url);
- expect(imgElement.classes()).toContain('s32');
+ expect(imgElement.classes()).toContain('gl-avatar-s32');
expect(imgElement.attributes('alt')).toBe(commit.author.name);
expect(imgElement.attributes('src')).toBe(commit.author.avatar_url);
});
diff --git a/spec/frontend/diffs/components/diff_content_spec.js b/spec/frontend/diffs/components/diff_content_spec.js
index 9f593ee0d49..0bce6451ce4 100644
--- a/spec/frontend/diffs/components/diff_content_spec.js
+++ b/spec/frontend/diffs/components/diff_content_spec.js
@@ -53,7 +53,7 @@ describe('DiffContent', () => {
namespaced: true,
getters: {
draftsForFile: () => () => true,
- draftForLine: () => () => true,
+ draftsForLine: () => () => true,
shouldRenderDraftRow: () => () => true,
hasParallelDraftLeft: () => () => true,
hasParallelDraftRight: () => () => true,
diff --git a/spec/frontend/diffs/components/diff_row_spec.js b/spec/frontend/diffs/components/diff_row_spec.js
index a74013dc2d4..a7a95ed2f35 100644
--- a/spec/frontend/diffs/components/diff_row_spec.js
+++ b/spec/frontend/diffs/components/diff_row_spec.js
@@ -219,7 +219,7 @@ describe('DiffRow', () => {
shouldRenderDraftRow: jest.fn(),
hasParallelDraftLeft: jest.fn(),
hasParallelDraftRight: jest.fn(),
- draftForLine: jest.fn(),
+ draftsForLine: jest.fn().mockReturnValue([]),
};
const applyMap = mapParallel(mockDiffContent);
diff --git a/spec/frontend/diffs/components/diff_row_utils_spec.js b/spec/frontend/diffs/components/diff_row_utils_spec.js
index 930b8bcdb08..8b25691ce34 100644
--- a/spec/frontend/diffs/components/diff_row_utils_spec.js
+++ b/spec/frontend/diffs/components/diff_row_utils_spec.js
@@ -216,7 +216,7 @@ describe('mapParallel', () => {
diffFile: {},
hasParallelDraftLeft: () => false,
hasParallelDraftRight: () => false,
- draftForLine: () => ({}),
+ draftsForLine: () => [],
};
const line = { left: side, right: side };
const expectation = {
@@ -234,13 +234,13 @@ describe('mapParallel', () => {
const leftExpectation = {
renderDiscussion: true,
hasDraft: false,
- lineDraft: {},
+ lineDrafts: [],
hasCommentForm: true,
};
const rightExpectation = {
renderDiscussion: false,
hasDraft: false,
- lineDraft: {},
+ lineDrafts: [],
hasCommentForm: false,
};
const mapped = utils.mapParallel(content)(line);
diff --git a/spec/frontend/diffs/components/diff_view_spec.js b/spec/frontend/diffs/components/diff_view_spec.js
index 1dd4a2f6c23..9bff6bd14f1 100644
--- a/spec/frontend/diffs/components/diff_view_spec.js
+++ b/spec/frontend/diffs/components/diff_view_spec.js
@@ -21,7 +21,7 @@ describe('DiffView', () => {
getters: {
shouldRenderDraftRow: () => false,
shouldRenderParallelDraftRow: () => () => true,
- draftForLine: () => false,
+ draftsForLine: () => false,
draftsForFile: () => false,
hasParallelDraftLeft: () => false,
hasParallelDraftRight: () => false,
@@ -75,12 +75,12 @@ describe('DiffView', () => {
});
it.each`
- type | side | container | sides | total
- ${'parallel'} | ${'left'} | ${'.old'} | ${{ left: { lineDraft: {}, renderDiscussion: true }, right: { lineDraft: {}, renderDiscussion: true } }} | ${2}
- ${'parallel'} | ${'right'} | ${'.new'} | ${{ left: { lineDraft: {}, renderDiscussion: true }, right: { lineDraft: {}, renderDiscussion: true } }} | ${2}
- ${'inline'} | ${'left'} | ${'.old'} | ${{ left: { lineDraft: {}, renderDiscussion: true } }} | ${1}
- ${'inline'} | ${'left'} | ${'.old'} | ${{ left: { lineDraft: {}, renderDiscussion: true } }} | ${1}
- ${'inline'} | ${'left'} | ${'.old'} | ${{ left: { lineDraft: {}, renderDiscussion: true } }} | ${1}
+ type | side | container | sides | total
+ ${'parallel'} | ${'left'} | ${'.old'} | ${{ left: { lineDrafts: [], renderDiscussion: true }, right: { lineDrafts: [], renderDiscussion: true } }} | ${2}
+ ${'parallel'} | ${'right'} | ${'.new'} | ${{ left: { lineDrafts: [], renderDiscussion: true }, right: { lineDrafts: [], renderDiscussion: true } }} | ${2}
+ ${'inline'} | ${'left'} | ${'.old'} | ${{ left: { lineDrafts: [], renderDiscussion: true } }} | ${1}
+ ${'inline'} | ${'left'} | ${'.old'} | ${{ left: { lineDrafts: [], renderDiscussion: true } }} | ${1}
+ ${'inline'} | ${'left'} | ${'.old'} | ${{ left: { lineDrafts: [], renderDiscussion: true } }} | ${1}
`(
'renders a $type comment row with comment cell on $side',
({ type, container, sides, total }) => {
@@ -95,7 +95,7 @@ describe('DiffView', () => {
it('renders a draft row', () => {
const wrapper = createWrapper({
- diffLines: [{ renderCommentRow: true, left: { lineDraft: { isDraft: true } } }],
+ diffLines: [{ renderCommentRow: true, left: { lineDrafts: [{ isDraft: true }] } }],
});
expect(wrapper.findComponent(DraftNote).exists()).toBe(true);
});
diff --git a/spec/frontend/diffs/components/file_row_stats_spec.js b/spec/frontend/diffs/components/file_row_stats_spec.js
index 3f5a63c19e5..7d3b60d2ba4 100644
--- a/spec/frontend/diffs/components/file_row_stats_spec.js
+++ b/spec/frontend/diffs/components/file_row_stats_spec.js
@@ -2,13 +2,21 @@ import { mount } from '@vue/test-utils';
import FileRowStats from '~/diffs/components/file_row_stats.vue';
describe('Diff file row stats', () => {
- const wrapper = mount(FileRowStats, {
- propsData: {
- file: {
- addedLines: 20,
- removedLines: 10,
+ let wrapper;
+
+ const createComponent = () => {
+ wrapper = mount(FileRowStats, {
+ propsData: {
+ file: {
+ addedLines: 20,
+ removedLines: 10,
+ },
},
- },
+ });
+ };
+
+ beforeEach(() => {
+ createComponent();
});
it('renders added lines count', () => {
diff --git a/spec/frontend/diffs/mock_data/diff_code_quality.js b/spec/frontend/diffs/mock_data/diff_code_quality.js
index 2ca421a20b4..befab3b676b 100644
--- a/spec/frontend/diffs/mock_data/diff_code_quality.js
+++ b/spec/frontend/diffs/mock_data/diff_code_quality.js
@@ -36,7 +36,7 @@ export const diffCodeQuality = {
old_line: 1,
new_line: null,
codequality: [],
- lineDraft: {},
+ lineDrafts: [],
},
},
{
@@ -45,7 +45,7 @@ export const diffCodeQuality = {
old_line: 2,
new_line: 1,
codequality: [],
- lineDraft: {},
+ lineDrafts: [],
},
},
{
@@ -55,7 +55,7 @@ export const diffCodeQuality = {
new_line: 2,
codequality: [multipleFindingsArr[0]],
- lineDraft: {},
+ lineDrafts: [],
},
},
],
diff --git a/spec/frontend/diffs/store/actions_spec.js b/spec/frontend/diffs/store/actions_spec.js
index 346e43e5a72..bf75f956d7f 100644
--- a/spec/frontend/diffs/store/actions_spec.js
+++ b/spec/frontend/diffs/store/actions_spec.js
@@ -13,7 +13,7 @@ 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';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import * as commonUtils from '~/lib/utils/common_utils';
import { mergeUrlParams } from '~/lib/utils/url_utility';
@@ -54,7 +54,7 @@ describe('DiffsStoreActions', () => {
['requestAnimationFrame', 'requestIdleCallback'].forEach((method) => {
global[method] = originalMethods[method];
});
- createFlash.mockClear();
+ createAlert.mockClear();
mock.restore();
});
@@ -175,35 +175,10 @@ describe('DiffsStoreActions', () => {
[{ type: 'startRenderDiffsQueue' }, { type: 'startRenderDiffsQueue' }],
);
});
-
- it.each`
- viewStyle | otherView
- ${'inline'} | ${'parallel'}
- ${'parallel'} | ${'inline'}
- `(
- 'should make a request with the view parameter "$viewStyle" when the batchEndpoint already contains "$otherView"',
- ({ viewStyle, otherView }) => {
- const endpointBatch = '/fetch/diffs_batch';
-
- 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}`);
- })
- .catch(() => {});
- },
- );
});
describe('fetchDiffFilesMeta', () => {
- const endpointMetadata = '/fetch/diffs_metadata.json?view=inline';
+ const endpointMetadata = '/fetch/diffs_metadata.json?view=inline&w=0';
const noFilesData = { ...diffMetadata };
beforeEach(() => {
@@ -216,7 +191,7 @@ describe('DiffsStoreActions', () => {
return testAction(
diffActions.fetchDiffFilesMeta,
{},
- { endpointMetadata, diffViewType: 'inline' },
+ { endpointMetadata, diffViewType: 'inline', showWhitespace: true },
[
{ type: types.SET_LOADING, payload: true },
{ type: types.SET_LOADING, payload: false },
@@ -254,8 +229,8 @@ describe('DiffsStoreActions', () => {
mock.onGet(endpointCoverage).reply(400);
await testAction(diffActions.fetchCoverageFiles, {}, { endpointCoverage }, [], []);
- expect(createFlash).toHaveBeenCalledTimes(1);
- expect(createFlash).toHaveBeenCalledWith({
+ expect(createAlert).toHaveBeenCalledTimes(1);
+ expect(createAlert).toHaveBeenCalledWith({
message: expect.stringMatching('Something went wrong'),
});
});
diff --git a/spec/frontend/editor/schema/ci/ci_schema_spec.js b/spec/frontend/editor/schema/ci/ci_schema_spec.js
index c9010fbec0c..fc86907c144 100644
--- a/spec/frontend/editor/schema/ci/ci_schema_spec.js
+++ b/spec/frontend/editor/schema/ci/ci_schema_spec.js
@@ -29,12 +29,36 @@ 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';
+import ProjectPathYaml from './yaml_tests/positive_tests/project_path.yml';
+import VariablesYaml from './yaml_tests/positive_tests/variables.yml';
// YAML NEGATIVE TEST
import ArtifactsNegativeYaml from './yaml_tests/negative_tests/artifacts.yml';
import CacheNegativeYaml from './yaml_tests/negative_tests/cache.yml';
import IncludeNegativeYaml from './yaml_tests/negative_tests/include.yml';
import RulesNegativeYaml from './yaml_tests/negative_tests/rules.yml';
+import VariablesNegativeYaml from './yaml_tests/negative_tests/variables.yml';
+
+import ProjectPathIncludeEmptyYaml from './yaml_tests/negative_tests/project_path/include/empty.yml';
+import ProjectPathIncludeInvalidVariableYaml from './yaml_tests/negative_tests/project_path/include/invalid_variable.yml';
+import ProjectPathIncludeLeadSlashYaml from './yaml_tests/negative_tests/project_path/include/leading_slash.yml';
+import ProjectPathIncludeNoSlashYaml from './yaml_tests/negative_tests/project_path/include/no_slash.yml';
+import ProjectPathIncludeTailSlashYaml from './yaml_tests/negative_tests/project_path/include/tailing_slash.yml';
+import ProjectPathTriggerIncludeEmptyYaml from './yaml_tests/negative_tests/project_path/trigger/include/empty.yml';
+import ProjectPathTriggerIncludeInvalidVariableYaml from './yaml_tests/negative_tests/project_path/trigger/include/invalid_variable.yml';
+import ProjectPathTriggerIncludeLeadSlashYaml from './yaml_tests/negative_tests/project_path/trigger/include/leading_slash.yml';
+import ProjectPathTriggerIncludeNoSlashYaml from './yaml_tests/negative_tests/project_path/trigger/include/no_slash.yml';
+import ProjectPathTriggerIncludeTailSlashYaml from './yaml_tests/negative_tests/project_path/trigger/include/tailing_slash.yml';
+import ProjectPathTriggerMinimalEmptyYaml from './yaml_tests/negative_tests/project_path/trigger/minimal/empty.yml';
+import ProjectPathTriggerMinimalInvalidVariableYaml from './yaml_tests/negative_tests/project_path/trigger/minimal/invalid_variable.yml';
+import ProjectPathTriggerMinimalLeadSlashYaml from './yaml_tests/negative_tests/project_path/trigger/minimal/leading_slash.yml';
+import ProjectPathTriggerMinimalNoSlashYaml from './yaml_tests/negative_tests/project_path/trigger/minimal/no_slash.yml';
+import ProjectPathTriggerMinimalTailSlashYaml from './yaml_tests/negative_tests/project_path/trigger/minimal/tailing_slash.yml';
+import ProjectPathTriggerProjectEmptyYaml from './yaml_tests/negative_tests/project_path/trigger/project/empty.yml';
+import ProjectPathTriggerProjectInvalidVariableYaml from './yaml_tests/negative_tests/project_path/trigger/project/invalid_variable.yml';
+import ProjectPathTriggerProjectLeadSlashYaml from './yaml_tests/negative_tests/project_path/trigger/project/leading_slash.yml';
+import ProjectPathTriggerProjectNoSlashYaml from './yaml_tests/negative_tests/project_path/trigger/project/no_slash.yml';
+import ProjectPathTriggerProjectTailSlashYaml from './yaml_tests/negative_tests/project_path/trigger/project/tailing_slash.yml';
const ajv = new Ajv({
strictTypes: false,
@@ -67,6 +91,8 @@ describe('positive tests', () => {
FilterYaml,
IncludeYaml,
RulesYaml,
+ VariablesYaml,
+ ProjectPathYaml,
}),
)('schema validates %s', (_, input) => {
expect(input).toValidateJsonSchema(schema);
@@ -90,6 +116,27 @@ describe('negative tests', () => {
CacheNegativeYaml,
IncludeNegativeYaml,
RulesNegativeYaml,
+ VariablesNegativeYaml,
+ ProjectPathIncludeEmptyYaml,
+ ProjectPathIncludeInvalidVariableYaml,
+ ProjectPathIncludeLeadSlashYaml,
+ ProjectPathIncludeNoSlashYaml,
+ ProjectPathIncludeTailSlashYaml,
+ ProjectPathTriggerIncludeEmptyYaml,
+ ProjectPathTriggerIncludeInvalidVariableYaml,
+ ProjectPathTriggerIncludeLeadSlashYaml,
+ ProjectPathTriggerIncludeNoSlashYaml,
+ ProjectPathTriggerIncludeTailSlashYaml,
+ ProjectPathTriggerMinimalEmptyYaml,
+ ProjectPathTriggerMinimalInvalidVariableYaml,
+ ProjectPathTriggerMinimalLeadSlashYaml,
+ ProjectPathTriggerMinimalNoSlashYaml,
+ ProjectPathTriggerMinimalTailSlashYaml,
+ ProjectPathTriggerProjectEmptyYaml,
+ ProjectPathTriggerProjectInvalidVariableYaml,
+ ProjectPathTriggerProjectLeadSlashYaml,
+ ProjectPathTriggerProjectNoSlashYaml,
+ ProjectPathTriggerProjectTailSlashYaml,
}),
)('schema validates %s', (_, input) => {
expect(input).not.toValidateJsonSchema(schema);
diff --git a/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/include/empty.yml b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/include/empty.yml
new file mode 100644
index 00000000000..d9838fbb6fd
--- /dev/null
+++ b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/include/empty.yml
@@ -0,0 +1,3 @@
+include:
+ - project: ''
+ file: '/templates/.gitlab-ci-template.yml'
diff --git a/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/include/invalid_variable.yml b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/include/invalid_variable.yml
new file mode 100644
index 00000000000..32933f856c7
--- /dev/null
+++ b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/include/invalid_variable.yml
@@ -0,0 +1,3 @@
+include:
+ - project: 'slug#'
+ file: '/templates/.gitlab-ci-template.yml'
diff --git a/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/include/leading_slash.yml b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/include/leading_slash.yml
new file mode 100644
index 00000000000..c463318be31
--- /dev/null
+++ b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/include/leading_slash.yml
@@ -0,0 +1,3 @@
+include:
+ - project: '/slug'
+ file: '/templates/.gitlab-ci-template.yml'
diff --git a/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/include/no_slash.yml b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/include/no_slash.yml
new file mode 100644
index 00000000000..51194a1d40c
--- /dev/null
+++ b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/include/no_slash.yml
@@ -0,0 +1,3 @@
+include:
+ - project: 'slug'
+ file: '/templates/.gitlab-ci-template.yml'
diff --git a/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/include/tailing_slash.yml b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/include/tailing_slash.yml
new file mode 100644
index 00000000000..91f258888d8
--- /dev/null
+++ b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/include/tailing_slash.yml
@@ -0,0 +1,3 @@
+include:
+ - project: 'slug/'
+ file: '/templates/.gitlab-ci-template.yml'
diff --git a/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/include/empty.yml b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/include/empty.yml
new file mode 100644
index 00000000000..ee2bb3e8ace
--- /dev/null
+++ b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/include/empty.yml
@@ -0,0 +1,5 @@
+trigger-include:
+ trigger:
+ include:
+ - file: '/path/to/child-pipeline.yml'
+ project: ''
diff --git a/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/include/invalid_variable.yml b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/include/invalid_variable.yml
new file mode 100644
index 00000000000..770305be0dc
--- /dev/null
+++ b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/include/invalid_variable.yml
@@ -0,0 +1,5 @@
+trigger-include:
+ trigger:
+ include:
+ - file: '/path/to/child-pipeline.yml'
+ project: 'slug#'
diff --git a/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/include/leading_slash.yml b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/include/leading_slash.yml
new file mode 100644
index 00000000000..82fd77cf0d3
--- /dev/null
+++ b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/include/leading_slash.yml
@@ -0,0 +1,5 @@
+trigger-include:
+ trigger:
+ include:
+ - file: '/path/to/child-pipeline.yml'
+ project: '/slug'
diff --git a/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/include/no_slash.yml b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/include/no_slash.yml
new file mode 100644
index 00000000000..f4ea59c7945
--- /dev/null
+++ b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/include/no_slash.yml
@@ -0,0 +1,5 @@
+trigger-include:
+ trigger:
+ include:
+ - file: '/path/to/child-pipeline.yml'
+ project: 'slug'
diff --git a/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/include/tailing_slash.yml b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/include/tailing_slash.yml
new file mode 100644
index 00000000000..a0195c03352
--- /dev/null
+++ b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/include/tailing_slash.yml
@@ -0,0 +1,5 @@
+trigger-include:
+ trigger:
+ include:
+ - file: '/path/to/child-pipeline.yml'
+ project: 'slug/'
diff --git a/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/minimal/empty.yml b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/minimal/empty.yml
new file mode 100644
index 00000000000..cad8dbbf430
--- /dev/null
+++ b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/minimal/empty.yml
@@ -0,0 +1,2 @@
+trigger-minimal:
+ trigger: ''
diff --git a/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/minimal/invalid_variable.yml b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/minimal/invalid_variable.yml
new file mode 100644
index 00000000000..6ca37666d09
--- /dev/null
+++ b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/minimal/invalid_variable.yml
@@ -0,0 +1,2 @@
+trigger-minimal:
+ trigger: 'slug#'
diff --git a/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/minimal/leading_slash.yml b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/minimal/leading_slash.yml
new file mode 100644
index 00000000000..9d7c6b44125
--- /dev/null
+++ b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/minimal/leading_slash.yml
@@ -0,0 +1,2 @@
+trigger-minimal:
+ trigger: '/slug'
diff --git a/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/minimal/no_slash.yml b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/minimal/no_slash.yml
new file mode 100644
index 00000000000..acd047477c8
--- /dev/null
+++ b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/minimal/no_slash.yml
@@ -0,0 +1,2 @@
+trigger-minimal:
+ trigger: 'slug'
diff --git a/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/minimal/tailing_slash.yml b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/minimal/tailing_slash.yml
new file mode 100644
index 00000000000..0fdd00da3de
--- /dev/null
+++ b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/minimal/tailing_slash.yml
@@ -0,0 +1,2 @@
+trigger-minimal:
+ trigger: 'slug/'
diff --git a/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/project/empty.yml b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/project/empty.yml
new file mode 100644
index 00000000000..0aa2330cecb
--- /dev/null
+++ b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/project/empty.yml
@@ -0,0 +1,3 @@
+trigger-project:
+ trigger:
+ project: ''
diff --git a/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/project/invalid_variable.yml b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/project/invalid_variable.yml
new file mode 100644
index 00000000000..3c17ec62039
--- /dev/null
+++ b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/project/invalid_variable.yml
@@ -0,0 +1,3 @@
+trigger-project:
+ trigger:
+ project: 'slug#'
diff --git a/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/project/leading_slash.yml b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/project/leading_slash.yml
new file mode 100644
index 00000000000..f9884603171
--- /dev/null
+++ b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/project/leading_slash.yml
@@ -0,0 +1,3 @@
+trigger-project:
+ trigger:
+ project: '/slug'
diff --git a/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/project/no_slash.yml b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/project/no_slash.yml
new file mode 100644
index 00000000000..d89e09756eb
--- /dev/null
+++ b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/project/no_slash.yml
@@ -0,0 +1,3 @@
+trigger-project:
+ trigger:
+ project: 'slug'
diff --git a/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/project/tailing_slash.yml b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/project/tailing_slash.yml
new file mode 100644
index 00000000000..3c39d6be4cb
--- /dev/null
+++ b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/project/tailing_slash.yml
@@ -0,0 +1,3 @@
+trigger-project:
+ trigger:
+ project: 'slug/'
diff --git a/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/variables.yml b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/variables.yml
new file mode 100644
index 00000000000..a7f23cf0d73
--- /dev/null
+++ b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/variables.yml
@@ -0,0 +1,5 @@
+# invalid variable (unknown keyword is used)
+variables:
+ FOO:
+ value: BAR
+ desc: A single value variable
diff --git a/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/project_path.yml b/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/project_path.yml
new file mode 100644
index 00000000000..8a12cdf4f15
--- /dev/null
+++ b/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/project_path.yml
@@ -0,0 +1,101 @@
+# Covers https://gitlab.com/gitlab-org/gitlab/-/merge_requests/95469
+# Test cases:
+# - include file from project
+# - trigger pipeline from project, 3 forms (see schema at ci.json)
+#
+# Sub-cases - forms of project path:
+# - common case: group/project
+# - sub-group: group/sub-group/project
+# - variable: $FOO
+# - variable in string: group/$VAR/project
+# - invalid variable: $.
+# (testing regex, that does not validate variable names)
+
+# BEGIN CASE: include yml from project
+include:
+ - project: 'group/project'
+ file: '/templates/.gitlab-ci-template.yml'
+
+ - project: 'group/sub-group/project'
+ file: '/templates/.gitlab-ci-template.yml'
+
+ - project: '$FOO'
+ file: '/templates/.gitlab-ci-template.yml'
+
+ - project: 'group/$VAR/project'
+ file: '/templates/.gitlab-ci-template.yml'
+
+ - project: '$.'
+ file: '/templates/.gitlab-ci-template.yml'
+# END CASE
+
+# BEGIN CASE: trigger minimal
+trigger-minimal:
+ trigger: 'group/project'
+
+trigger-minimal-sub-group:
+ trigger: 'group/sub-group/project'
+
+trigger-minimal-variable:
+ trigger: '$FOO'
+
+trigger-minimal-variable-in-string:
+ trigger: 'group/$VAR/project'
+
+trigger-minimal-invalid-variable:
+ trigger: '$.'
+# END CASE
+
+# BEGIN CASE: trigger project
+trigger-project:
+ trigger:
+ project: 'group/project'
+
+trigger-project-sub-group:
+ trigger:
+ project: 'group/sub-group/project'
+
+trigger-project-variable:
+ trigger:
+ project: '$FOO'
+
+trigger-project-variable-in-string:
+ trigger:
+ project: 'group/$VAR/project'
+
+trigger-project-invalid-variable:
+ trigger:
+ project: '$.'
+# END CASE
+
+# BEGIN CASE: trigger file
+trigger-include:
+ trigger:
+ include:
+ - project: 'group/project'
+ file: '/path/to/child-pipeline.yml'
+
+trigger-include-sub-group:
+ trigger:
+ include:
+ - project: 'group/sub-group/project'
+ file: '/path/to/child-pipeline.yml'
+
+trigger-include-variable:
+ trigger:
+ include:
+ - project: '$FOO'
+ file: '/path/to/child-pipeline.yml'
+
+trigger-include-variable-in-string:
+ trigger:
+ include:
+ - project: 'group/$VAR/project'
+ file: '/path/to/child-pipeline.yml'
+
+trigger-include-invalid-variable:
+ trigger:
+ include:
+ - project: '$.'
+ file: '/path/to/child-pipeline.yml'
+# END CASE
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
index 37cae6b4264..ef604f707b5 100644
--- a/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/rules.yml
+++ b/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/rules.yml
@@ -15,7 +15,9 @@ rules:changes as array of strings:
# valid workflow:rules:exists
# valid rules:changes:path
+# valid workflow:name
workflow:
+ name: 'Pipeline name'
rules:
- changes:
paths:
diff --git a/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/variables.yml b/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/variables.yml
new file mode 100644
index 00000000000..ee71087a72e
--- /dev/null
+++ b/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/variables.yml
@@ -0,0 +1,8 @@
+variables:
+ TEST_VAR: "hello world!"
+ 123456: "123456"
+ FOO:
+ value: "BAR"
+ description: "A single value variable"
+ DEPLOY_ENVIRONMENT:
+ description: "A multi-value variable"
diff --git a/spec/frontend/editor/source_editor_ci_schema_ext_spec.js b/spec/frontend/editor/source_editor_ci_schema_ext_spec.js
index 9a14e1a55eb..21f8979f1a9 100644
--- a/spec/frontend/editor/source_editor_ci_schema_ext_spec.js
+++ b/spec/frontend/editor/source_editor_ci_schema_ext_spec.js
@@ -1,4 +1,4 @@
-import { languages } from 'monaco-editor';
+import { setDiagnosticsOptions } from 'monaco-yaml';
import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import { TEST_HOST } from 'helpers/test_constants';
import { CiSchemaExtension } from '~/editor/extensions/source_editor_ci_schema_ext';
@@ -52,16 +52,12 @@ describe('~/editor/editor_ci_config_ext', () => {
});
describe('registerCiSchema', () => {
- beforeEach(() => {
- jest.spyOn(languages.yaml.yamlDefaults, 'setDiagnosticsOptions');
- });
-
describe('register validations options with monaco for yaml language', () => {
const mockProjectNamespace = 'namespace1';
const mockProjectPath = 'project1';
const getConfiguredYmlSchema = () => {
- return languages.yaml.yamlDefaults.setDiagnosticsOptions.mock.calls[0][0].schemas[0];
+ return setDiagnosticsOptions.mock.calls[0][0].schemas[0];
};
it('with expected basic validation configuration', () => {
@@ -77,8 +73,8 @@ describe('~/editor/editor_ci_config_ext', () => {
completion: true,
};
- expect(languages.yaml.yamlDefaults.setDiagnosticsOptions).toHaveBeenCalledTimes(1);
- expect(languages.yaml.yamlDefaults.setDiagnosticsOptions).toHaveBeenCalledWith(
+ expect(setDiagnosticsOptions).toHaveBeenCalledTimes(1);
+ expect(setDiagnosticsOptions).toHaveBeenCalledWith(
expect.objectContaining(expectedOptions),
);
});
diff --git a/spec/frontend/editor/source_editor_instance_spec.js b/spec/frontend/editor/source_editor_instance_spec.js
index 20ba23d56ff..89b5ad27690 100644
--- a/spec/frontend/editor/source_editor_instance_spec.js
+++ b/spec/frontend/editor/source_editor_instance_spec.js
@@ -160,7 +160,7 @@ describe('Source Editor Instance', () => {
});
describe('public API', () => {
- it.each(['use', 'unuse'], 'provides "%s" as public method by default', (method) => {
+ it.each(['use', 'unuse'])('provides "%s" as public method by default', (method) => {
seInstance = new SourceEditorInstance();
expect(seInstance[method]).toBeDefined();
});
diff --git a/spec/frontend/editor/source_editor_markdown_livepreview_ext_spec.js b/spec/frontend/editor/source_editor_markdown_livepreview_ext_spec.js
index fe20c23e4d7..1ff351b6554 100644
--- a/spec/frontend/editor/source_editor_markdown_livepreview_ext_spec.js
+++ b/spec/frontend/editor/source_editor_markdown_livepreview_ext_spec.js
@@ -12,7 +12,7 @@ import {
} from '~/editor/constants';
import { EditorMarkdownPreviewExtension } from '~/editor/extensions/source_editor_markdown_livepreview_ext';
import SourceEditor from '~/editor/source_editor';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import syntaxHighlight from '~/syntax_highlight';
import { spyOnApi } from './helpers';
@@ -279,7 +279,7 @@ describe('Markdown Live Preview Extension for Source Editor', () => {
mockAxios.onPost().reply(500);
await fetchPreview();
- expect(createFlash).toHaveBeenCalled();
+ expect(createAlert).toHaveBeenCalled();
});
});
diff --git a/spec/frontend/environments/delete_environment_modal_spec.js b/spec/frontend/environments/delete_environment_modal_spec.js
index 48e4f661c1d..cc18bf754eb 100644
--- a/spec/frontend/environments/delete_environment_modal_spec.js
+++ b/spec/frontend/environments/delete_environment_modal_spec.js
@@ -6,7 +6,7 @@ import { s__, sprintf } from '~/locale';
import DeleteEnvironmentModal from '~/environments/components/delete_environment_modal.vue';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { resolvedEnvironment } from './graphql/mock_data';
jest.mock('~/flash');
@@ -57,7 +57,7 @@ describe('~/environments/components/delete_environment_modal.vue', () => {
await nextTick();
- expect(createFlash).not.toHaveBeenCalled();
+ expect(createAlert).not.toHaveBeenCalled();
expect(deleteResolver).toHaveBeenCalledWith(
expect.anything(),
@@ -76,7 +76,7 @@ describe('~/environments/components/delete_environment_modal.vue', () => {
await waitForPromises();
- expect(createFlash).toHaveBeenCalledWith(
+ expect(createAlert).toHaveBeenCalledWith(
expect.objectContaining({
message: s__(
'Environments|An error occurred while deleting the environment. Check if the environment stopped; if not, stop it and try again.',
diff --git a/spec/frontend/environments/edit_environment_spec.js b/spec/frontend/environments/edit_environment_spec.js
index 0f2d6e95bf0..5ea23af4c16 100644
--- a/spec/frontend/environments/edit_environment_spec.js
+++ b/spec/frontend/environments/edit_environment_spec.js
@@ -3,7 +3,7 @@ import MockAdapter from 'axios-mock-adapter';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import EditEnvironment from '~/environments/components/edit_environment.vue';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { visitUrl } from '~/lib/utils/url_utility';
@@ -85,7 +85,7 @@ describe('~/environments/components/edit.vue', () => {
await submitForm(expected, [400, { message: ['uh oh!'] }]);
- expect(createFlash).toHaveBeenCalledWith({ message: 'uh oh!' });
+ expect(createAlert).toHaveBeenCalledWith({ message: 'uh oh!' });
expect(showsLoading()).toBe(false);
});
diff --git a/spec/frontend/environments/empty_state_spec.js b/spec/frontend/environments/empty_state_spec.js
index 974afc6d032..02cf2dc3c68 100644
--- a/spec/frontend/environments/empty_state_spec.js
+++ b/spec/frontend/environments/empty_state_spec.js
@@ -4,10 +4,21 @@ import EmptyState from '~/environments/components/empty_state.vue';
import { ENVIRONMENTS_SCOPE } from '~/environments/constants';
const HELP_PATH = '/help';
+const NEW_PATH = '/new';
describe('~/environments/components/empty_state.vue', () => {
let wrapper;
+ const findNewEnvironmentLink = () =>
+ wrapper.findByRole('link', {
+ name: s__('Environments|New environment'),
+ });
+
+ const findDocsLink = () =>
+ wrapper.findByRole('link', {
+ name: s__('Environments|How do I create an environment?'),
+ });
+
const createWrapper = ({ propsData = {} } = {}) =>
mountExtended(EmptyState, {
propsData: {
@@ -15,6 +26,7 @@ describe('~/environments/components/empty_state.vue', () => {
helpPath: HELP_PATH,
...propsData,
},
+ provide: { newEnvironmentPath: NEW_PATH },
});
afterEach(() => {
@@ -44,10 +56,44 @@ describe('~/environments/components/empty_state.vue', () => {
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?'),
- });
+ const link = findDocsLink();
expect(link.attributes('href')).toBe(HELP_PATH);
});
+
+ it('hides a link to creating a new environment', () => {
+ const link = findNewEnvironmentLink();
+
+ expect(link.exists()).toBe(false);
+ });
+
+ describe('with search term', () => {
+ beforeEach(() => {
+ wrapper = createWrapper({ propsData: { hasTerm: true } });
+ });
+
+ it('should show text about searching', () => {
+ const header = wrapper.findByRole('heading', {
+ name: s__('Environments|No results found'),
+ });
+
+ expect(header.exists()).toBe(true);
+
+ const text = wrapper.findByText(s__('Environments|Edit your search and try again'));
+
+ expect(text.exists()).toBe(true);
+ });
+
+ it('hides the documentation link', () => {
+ const link = findDocsLink();
+
+ expect(link.exists()).toBe(false);
+ });
+
+ it('shows a link to create a new environment', () => {
+ const link = findNewEnvironmentLink();
+
+ expect(link.attributes('href')).toBe(NEW_PATH);
+ });
+ });
});
diff --git a/spec/frontend/environments/enable_review_app_modal_spec.js b/spec/frontend/environments/enable_review_app_modal_spec.js
index b6dac811ea6..7939bd600dc 100644
--- a/spec/frontend/environments/enable_review_app_modal_spec.js
+++ b/spec/frontend/environments/enable_review_app_modal_spec.js
@@ -1,7 +1,8 @@
import { shallowMount } from '@vue/test-utils';
import { GlModal } from '@gitlab/ui';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
-import EnableReviewAppButton from '~/environments/components/enable_review_app_modal.vue';
+import EnableReviewAppModal from '~/environments/components/enable_review_app_modal.vue';
+import { REVIEW_APP_MODAL_I18N as i18n } from '~/environments/constants';
import ModalCopyButton from '~/vue_shared/components/modal_copy_button.vue';
// hardcode uniqueId for determinism
@@ -9,10 +10,12 @@ jest.mock('lodash/uniqueId', () => (x) => `${x}77`);
const EXPECTED_COPY_PRE_ID = 'enable-review-app-copy-string-77';
-describe('Enable Review App Button', () => {
+describe('Enable Review App Modal', () => {
let wrapper;
let modal;
+ const findInstructions = () => wrapper.findAll('ol li');
+ const findInstructionAt = (i) => wrapper.findAll('ol li').at(i);
const findCopyString = () => wrapper.find(`#${EXPECTED_COPY_PRE_ID}`);
afterEach(() => {
@@ -22,29 +25,31 @@ describe('Enable Review App Button', () => {
describe('renders the modal', () => {
beforeEach(() => {
wrapper = extendedWrapper(
- shallowMount(EnableReviewAppButton, {
+ shallowMount(EnableReviewAppModal, {
propsData: {
modalId: 'fake-id',
visible: true,
},
- provide: {
- defaultBranchName: 'main',
- },
}),
);
modal = wrapper.findComponent(GlModal);
});
- it('renders the defaultBranchName copy', () => {
- expect(findCopyString().text()).toContain('- main');
+ it('displays instructions', () => {
+ expect(findInstructions().length).toBe(7);
+ expect(findInstructionAt(0).text()).toContain(i18n.instructions.step1);
+ });
+
+ it('renders the snippet to copy', () => {
+ expect(findCopyString().text()).toBe(wrapper.vm.modalInfoCopyStr);
});
it('renders the copyToClipboard button', () => {
expect(wrapper.findComponent(ModalCopyButton).props()).toMatchObject({
modalId: 'fake-id',
target: `#${EXPECTED_COPY_PRE_ID}`,
- title: 'Copy snippet text',
+ title: i18n.copyToClipboardText,
});
});
diff --git a/spec/frontend/environments/environment_external_url_spec.js b/spec/frontend/environments/environment_external_url_spec.js
index 4c133665979..5966993166b 100644
--- a/spec/frontend/environments/environment_external_url_spec.js
+++ b/spec/frontend/environments/environment_external_url_spec.js
@@ -1,16 +1,35 @@
import { mount } from '@vue/test-utils';
+import { s__, __ } from '~/locale';
import ExternalUrlComp from '~/environments/components/environment_external_url.vue';
+import ModalCopyButton from '~/vue_shared/components/modal_copy_button.vue';
describe('External URL Component', () => {
let wrapper;
- const externalUrl = 'https://gitlab.com';
+ let externalUrl;
- beforeEach(() => {
- wrapper = mount(ExternalUrlComp, { propsData: { externalUrl } });
+ describe('with safe link', () => {
+ beforeEach(() => {
+ externalUrl = 'https://gitlab.com';
+ wrapper = mount(ExternalUrlComp, { propsData: { externalUrl } });
+ });
+
+ it('should link to the provided externalUrl prop', () => {
+ expect(wrapper.attributes('href')).toBe(externalUrl);
+ expect(wrapper.find('a').exists()).toBe(true);
+ });
});
- it('should link to the provided externalUrl prop', () => {
- expect(wrapper.attributes('href')).toEqual(externalUrl);
- expect(wrapper.find('a').exists()).toBe(true);
+ describe('with unsafe link', () => {
+ beforeEach(() => {
+ externalUrl = 'postgres://gitlab';
+ wrapper = mount(ExternalUrlComp, { propsData: { externalUrl } });
+ });
+
+ it('should show a copy button instead', () => {
+ const button = wrapper.findComponent(ModalCopyButton);
+ expect(button.props('text')).toBe(externalUrl);
+ expect(button.text()).toBe(__('Copy URL'));
+ expect(button.props('title')).toBe(s__('Environments|Copy live environment URL'));
+ });
});
});
diff --git a/spec/frontend/environments/environment_folder_spec.js b/spec/frontend/environments/environment_folder_spec.js
index 48624f2324b..a37515bc3f7 100644
--- a/spec/frontend/environments/environment_folder_spec.js
+++ b/spec/frontend/environments/environment_folder_spec.js
@@ -31,6 +31,7 @@ describe('~/environments/components/environments_folder.vue', () => {
apolloProvider,
propsData: {
scope: 'available',
+ search: '',
...propsData,
},
stubs: { transition: stubTransition() },
@@ -137,13 +138,26 @@ describe('~/environments/components/environments_folder.vue', () => {
expect(environmentFolderMock).toHaveBeenCalledTimes(1);
expect(environmentFolderMock).toHaveBeenCalledWith(
{},
- {
- environment: nestedEnvironment.latest,
- scope,
- },
+ expect.objectContaining({ scope }),
expect.anything(),
expect.anything(),
);
},
);
+
+ it('should query for the entered parameter', async () => {
+ const search = 'hello';
+
+ wrapper = createWrapper({ nestedEnvironment, search }, createApolloProvider());
+
+ await nextTick();
+ await waitForPromises();
+
+ expect(environmentFolderMock).toHaveBeenCalledWith(
+ expect.anything(),
+ expect.objectContaining({ search }),
+ expect.anything(),
+ expect.anything(),
+ );
+ });
});
diff --git a/spec/frontend/environments/environments_app_spec.js b/spec/frontend/environments/environments_app_spec.js
index aff54107d6b..65a9f2907d2 100644
--- a/spec/frontend/environments/environments_app_spec.js
+++ b/spec/frontend/environments/environments_app_spec.js
@@ -71,7 +71,7 @@ describe('~/environments/components/environments_app.vue', () => {
previousPage: 1,
__typename: 'LocalPageInfo',
},
- location = '?scope=available&page=2',
+ location = '?scope=available&page=2&search=prod',
}) => {
setWindowLocation(location);
environmentAppMock.mockReturnValue(environmentsApp);
@@ -104,7 +104,7 @@ describe('~/environments/components/environments_app.vue', () => {
await createWrapperWithMocked({
environmentsApp: resolvedEnvironmentsApp,
folder: resolvedFolder,
- location: '?scope=bad&page=2',
+ location: '?scope=bad&page=2&search=prod',
});
expect(environmentAppMock).toHaveBeenCalledWith(
@@ -350,7 +350,54 @@ describe('~/environments/components/environments_app.vue', () => {
next.trigger('click');
await nextTick();
- expect(window.location.search).toBe('?scope=available&page=3');
+ expect(window.location.search).toBe('?scope=available&page=3&search=prod');
+ });
+ });
+
+ describe('search', () => {
+ let searchBox;
+
+ const waitForDebounce = async () => {
+ await nextTick();
+ jest.runOnlyPendingTimers();
+ };
+
+ beforeEach(async () => {
+ await createWrapperWithMocked({
+ environmentsApp: resolvedEnvironmentsApp,
+ folder: resolvedFolder,
+ });
+ searchBox = wrapper.findByRole('searchbox', {
+ name: s__('Environments|Search by environment name'),
+ });
+ });
+
+ it('should sync the query params to the new search', async () => {
+ searchBox.setValue('hello');
+
+ await waitForDebounce();
+
+ expect(window.location.search).toBe('?scope=available&page=1&search=hello');
+ });
+
+ it('should query for the entered parameter', async () => {
+ const search = 'hello';
+
+ searchBox.setValue(search);
+
+ await waitForDebounce();
+ await waitForPromises();
+
+ expect(environmentAppMock).toHaveBeenCalledWith(
+ expect.anything(),
+ expect.objectContaining({ search }),
+ expect.anything(),
+ expect.anything(),
+ );
+ });
+
+ it('should sync search term from query params on load', async () => {
+ expect(searchBox.element.value).toBe('prod');
});
});
});
diff --git a/spec/frontend/environments/environments_detail_header_spec.js b/spec/frontend/environments/environments_detail_header_spec.js
index 4687119127d..1f233c05fbf 100644
--- a/spec/frontend/environments/environments_detail_header_spec.js
+++ b/spec/frontend/environments/environments_detail_header_spec.js
@@ -1,10 +1,12 @@
import { GlSprintf } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
+import { __, s__ } from '~/locale';
import DeleteEnvironmentModal from '~/environments/components/delete_environment_modal.vue';
import EnvironmentsDetailHeader from '~/environments/components/environments_detail_header.vue';
import StopEnvironmentModal from '~/environments/components/stop_environment_modal.vue';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
+import ModalCopyButton from '~/vue_shared/components/modal_copy_button.vue';
import { createEnvironment } from './mock_data';
describe('Environments detail header component', () => {
@@ -243,4 +245,23 @@ describe('Environments detail header component', () => {
expect(findDeleteEnvironmentModal().exists()).toBe(true);
});
});
+
+ describe('when the environment has an unsafe external url', () => {
+ const externalUrl = 'postgres://staging';
+
+ beforeEach(() => {
+ createWrapper({
+ props: {
+ environment: createEnvironment({ externalUrl }),
+ },
+ });
+ });
+
+ it('should show a copy button instead', () => {
+ const button = wrapper.findComponent(ModalCopyButton);
+ expect(button.props('title')).toBe(s__('Environments|Copy live environment URL'));
+ expect(button.props('text')).toBe(externalUrl);
+ expect(button.text()).toBe(__('Copy URL'));
+ });
+ });
});
diff --git a/spec/frontend/environments/graphql/resolvers_spec.js b/spec/frontend/environments/graphql/resolvers_spec.js
index 26f0659204a..7684cca2303 100644
--- a/spec/frontend/environments/graphql/resolvers_spec.js
+++ b/spec/frontend/environments/graphql/resolvers_spec.js
@@ -41,11 +41,16 @@ describe('~/frontend/environments/graphql/resolvers', () => {
it('should fetch environments and map them to frontend data', async () => {
const cache = { writeQuery: jest.fn() };
const scope = 'available';
+ const search = '';
mock
- .onGet(ENDPOINT, { params: { nested: true, scope, page: 1 } })
+ .onGet(ENDPOINT, { params: { nested: true, scope, page: 1, search } })
.reply(200, environmentsApp, {});
- const app = await mockResolvers.Query.environmentApp(null, { scope, page: 1 }, { cache });
+ const app = await mockResolvers.Query.environmentApp(
+ null,
+ { scope, page: 1, search },
+ { cache },
+ );
expect(app).toEqual(resolvedEnvironmentsApp);
expect(cache.writeQuery).toHaveBeenCalledWith({
query: pollIntervalQuery,
@@ -57,12 +62,12 @@ describe('~/frontend/environments/graphql/resolvers', () => {
const scope = 'stopped';
const interval = 3000;
mock
- .onGet(ENDPOINT, { params: { nested: true, scope, page: 1 } })
+ .onGet(ENDPOINT, { params: { nested: true, scope, page: 1, search: '' } })
.reply(200, environmentsApp, {
'poll-interval': interval,
});
- await mockResolvers.Query.environmentApp(null, { scope, page: 1 }, { cache });
+ await mockResolvers.Query.environmentApp(null, { scope, page: 1, search: '' }, { cache });
expect(cache.writeQuery).toHaveBeenCalledWith({
query: pollIntervalQuery,
data: { interval },
@@ -72,7 +77,7 @@ describe('~/frontend/environments/graphql/resolvers', () => {
const cache = { writeQuery: jest.fn() };
const scope = 'stopped';
mock
- .onGet(ENDPOINT, { params: { nested: true, scope, page: 1 } })
+ .onGet(ENDPOINT, { params: { nested: true, scope, page: 1, search: '' } })
.reply(200, environmentsApp, {
'x-next-page': '2',
'x-page': '1',
@@ -82,7 +87,7 @@ describe('~/frontend/environments/graphql/resolvers', () => {
'X-Total-Pages': '5',
});
- await mockResolvers.Query.environmentApp(null, { scope, page: 1 }, { cache });
+ await mockResolvers.Query.environmentApp(null, { scope, page: 1, search: '' }, { cache });
expect(cache.writeQuery).toHaveBeenCalledWith({
query: pageInfoQuery,
data: {
@@ -102,10 +107,10 @@ describe('~/frontend/environments/graphql/resolvers', () => {
const cache = { writeQuery: jest.fn() };
const scope = 'stopped';
mock
- .onGet(ENDPOINT, { params: { nested: true, scope, page: 1 } })
+ .onGet(ENDPOINT, { params: { nested: true, scope, page: 1, search: '' } })
.reply(200, environmentsApp, {});
- await mockResolvers.Query.environmentApp(null, { scope, page: 1 }, { cache });
+ await mockResolvers.Query.environmentApp(null, { scope, page: 1, search: '' }, { cache });
expect(cache.writeQuery).toHaveBeenCalledWith({
query: pageInfoQuery,
data: {
@@ -124,11 +129,14 @@ describe('~/frontend/environments/graphql/resolvers', () => {
});
describe('folder', () => {
it('should fetch the folder url passed to it', async () => {
- mock.onGet(ENDPOINT, { params: { per_page: 3, scope: 'available' } }).reply(200, folder);
+ mock
+ .onGet(ENDPOINT, { params: { per_page: 3, scope: 'available', search: '' } })
+ .reply(200, folder);
const environmentFolder = await mockResolvers.Query.folder(null, {
environment: { folderPath: ENDPOINT },
scope: 'available',
+ search: '',
});
expect(environmentFolder).toEqual(resolvedFolder);
diff --git a/spec/frontend/environments/new_environment_spec.js b/spec/frontend/environments/new_environment_spec.js
index 2405cb82eac..6dd4eea7437 100644
--- a/spec/frontend/environments/new_environment_spec.js
+++ b/spec/frontend/environments/new_environment_spec.js
@@ -3,7 +3,7 @@ import MockAdapter from 'axios-mock-adapter';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import NewEnvironment from '~/environments/components/new_environment.vue';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { visitUrl } from '~/lib/utils/url_utility';
@@ -94,7 +94,7 @@ describe('~/environments/components/new.vue', () => {
await submitForm(expected, [400, { message: ['name taken'] }]);
- expect(createFlash).toHaveBeenCalledWith({ message: 'name taken' });
+ expect(createAlert).toHaveBeenCalledWith({ message: 'name taken' });
expect(showsLoading()).toBe(false);
});
});
diff --git a/spec/frontend/error_tracking/components/error_details_spec.js b/spec/frontend/error_tracking/components/error_details_spec.js
index 732eff65495..9d6e46be8c4 100644
--- a/spec/frontend/error_tracking/components/error_details_spec.js
+++ b/spec/frontend/error_tracking/components/error_details_spec.js
@@ -18,7 +18,7 @@ import {
trackErrorDetailsViewsOptions,
trackErrorStatusUpdateOptions,
} from '~/error_tracking/utils';
-import createFlash from '~/flash';
+import { createAlert, VARIANT_WARNING } from '~/flash';
import { __ } from '~/locale';
import Tracking from '~/tracking';
@@ -144,7 +144,7 @@ describe('ErrorDetails', () => {
await nextTick();
expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
- expect(createFlash).not.toHaveBeenCalled();
+ expect(createAlert).not.toHaveBeenCalled();
expect(mocks.$apollo.queries.error.stopPolling).not.toHaveBeenCalled();
});
@@ -156,9 +156,9 @@ describe('ErrorDetails', () => {
await nextTick();
expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(false);
expect(wrapper.findComponent(GlLink).exists()).toBe(false);
- expect(createFlash).toHaveBeenCalledWith({
+ expect(createAlert).toHaveBeenCalledWith({
message: 'Could not connect to Sentry. Refresh the page to try again.',
- type: 'warning',
+ variant: VARIANT_WARNING,
});
expect(mocks.$apollo.queries.error.stopPolling).toHaveBeenCalled();
});
diff --git a/spec/frontend/error_tracking/store/actions_spec.js b/spec/frontend/error_tracking/store/actions_spec.js
index 6bac21341a7..8f085282f80 100644
--- a/spec/frontend/error_tracking/store/actions_spec.js
+++ b/spec/frontend/error_tracking/store/actions_spec.js
@@ -2,7 +2,7 @@ import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
import * as actions from '~/error_tracking/store/actions';
import * as types from '~/error_tracking/store/mutation_types';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { visitUrl } from '~/lib/utils/url_utility';
@@ -20,7 +20,7 @@ describe('Sentry common store actions', () => {
afterEach(() => {
mock.restore();
- createFlash.mockClear();
+ createAlert.mockClear();
});
const endpoint = '123/stacktrace';
const redirectUrl = '/list';
@@ -49,7 +49,7 @@ describe('Sentry common store actions', () => {
mock.onPut().reply(400, {});
await testAction(actions.updateStatus, params, {}, [], []);
expect(visitUrl).not.toHaveBeenCalled();
- expect(createFlash).toHaveBeenCalledTimes(1);
+ expect(createAlert).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 a3a6f7cc309..1893d226270 100644
--- a/spec/frontend/error_tracking/store/details/actions_spec.js
+++ b/spec/frontend/error_tracking/store/details/actions_spec.js
@@ -2,7 +2,7 @@ import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
import * as actions from '~/error_tracking/store/details/actions';
import * as types from '~/error_tracking/store/details/mutation_types';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import Poll from '~/lib/utils/poll';
@@ -19,7 +19,7 @@ describe('Sentry error details store actions', () => {
afterEach(() => {
mockedAdapter.restore();
- createFlash.mockClear();
+ createAlert.mockClear();
if (mockedRestart) {
mockedRestart.mockRestore();
mockedRestart = null;
@@ -53,7 +53,7 @@ describe('Sentry error details store actions', () => {
[{ type: types.SET_LOADING_STACKTRACE, payload: false }],
[],
);
- expect(createFlash).toHaveBeenCalledTimes(1);
+ expect(createAlert).toHaveBeenCalledTimes(1);
});
it('should not restart polling when receiving an empty 204 response', async () => {
diff --git a/spec/frontend/error_tracking/store/list/actions_spec.js b/spec/frontend/error_tracking/store/list/actions_spec.js
index 7173f68bb96..2809bbe834e 100644
--- a/spec/frontend/error_tracking/store/list/actions_spec.js
+++ b/spec/frontend/error_tracking/store/list/actions_spec.js
@@ -2,7 +2,7 @@ import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
import * as actions from '~/error_tracking/store/list/actions';
import * as types from '~/error_tracking/store/list/mutation_types';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import httpStatusCodes from '~/lib/utils/http_status';
@@ -51,7 +51,7 @@ describe('error tracking actions', () => {
],
[],
);
- expect(createFlash).toHaveBeenCalledTimes(1);
+ expect(createAlert).toHaveBeenCalledTimes(1);
});
});
diff --git a/spec/frontend/feature_highlight/feature_highlight_helper_spec.js b/spec/frontend/feature_highlight/feature_highlight_helper_spec.js
index b87571830ca..22bac3fca15 100644
--- a/spec/frontend/feature_highlight/feature_highlight_helper_spec.js
+++ b/spec/frontend/feature_highlight/feature_highlight_helper_spec.js
@@ -1,6 +1,6 @@
import MockAdapter from 'axios-mock-adapter';
import { dismiss } from '~/feature_highlight/feature_highlight_helper';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import httpStatusCodes from '~/lib/utils/http_status';
@@ -32,7 +32,7 @@ describe('feature highlight helper', () => {
await dismiss(endpoint, highlightId);
- expect(createFlash).toHaveBeenCalledWith({
+ expect(createAlert).toHaveBeenCalledWith({
message:
'An error occurred while dismissing the feature highlight. Refresh the page and try dismissing again.',
});
diff --git a/spec/frontend/fixtures/namespaces.rb b/spec/frontend/fixtures/namespaces.rb
index b11f661fe09..a3f295f4e66 100644
--- a/spec/frontend/fixtures/namespaces.rb
+++ b/spec/frontend/fixtures/namespaces.rb
@@ -7,38 +7,43 @@ RSpec.describe 'Jobs (JavaScript fixtures)' do
include JavaScriptFixturesHelpers
include GraphqlHelpers
- describe GraphQL::Query, type: :request do
+ describe API::Projects, type: :request do
let_it_be(:user) { create(:user) }
- let_it_be(:groups) { create_list(:group, 4) }
- before_all do
- groups.each { |group| group.add_owner(user) }
- end
+ describe 'transfer_locations' do
+ let_it_be(:groups) { create_list(:group, 4) }
+ let_it_be(:project) { create(:project, namespace: user.namespace) }
- query_name = 'search_namespaces_where_user_can_transfer_projects'
- query_extension = '.query.graphql'
+ before_all do
+ groups.each { |group| group.add_owner(user) }
+ end
- full_input_path = "projects/settings/graphql/queries/#{query_name}#{query_extension}"
- base_output_path = "graphql/projects/settings/#{query_name}"
+ it 'api/projects/transfer_locations_page_1.json' do
+ get api("/projects/#{project.id}/transfer_locations?per_page=2", user)
- it "#{base_output_path}_page_1#{query_extension}.json" do
- query = get_graphql_query_as_string(full_input_path)
+ expect(response).to be_successful
+ end
- post_graphql(query, current_user: user, variables: { first: 2 })
+ it 'api/projects/transfer_locations_page_2.json' do
+ get api("/projects/#{project.id}/transfer_locations?per_page=2&page=2", user)
- expect_graphql_errors_to_be_empty
+ expect(response).to be_successful
+ end
end
+ end
+
+ describe GraphQL::Query, type: :request do
+ let_it_be(:user) { create(:user) }
+
+ query_name = 'current_user_namespace.query.graphql'
- it "#{base_output_path}_page_2#{query_extension}.json" do
- query = get_graphql_query_as_string(full_input_path)
+ input_path = "projects/settings/graphql/queries/#{query_name}"
+ output_path = "graphql/projects/settings/#{query_name}.json"
- post_graphql(query, current_user: user, variables: { first: 2 })
+ it output_path do
+ query = get_graphql_query_as_string(input_path)
- post_graphql(
- query,
- current_user: user,
- variables: { first: 2, after: graphql_data_at('currentUser', 'groups', 'pageInfo', 'endCursor') }
- )
+ post_graphql(query, current_user: user)
expect_graphql_errors_to_be_empty
end
diff --git a/spec/frontend/fixtures/pipeline_schedules.rb b/spec/frontend/fixtures/pipeline_schedules.rb
index 5b7a445557e..4de0bd762f8 100644
--- a/spec/frontend/fixtures/pipeline_schedules.rb
+++ b/spec/frontend/fixtures/pipeline_schedules.rb
@@ -2,40 +2,74 @@
require 'spec_helper'
-RSpec.describe Projects::PipelineSchedulesController, '(JavaScript fixtures)', type: :controller do
+RSpec.describe 'Pipeline schedules (JavaScript fixtures)' do
+ include ApiHelpers
include JavaScriptFixturesHelpers
+ include GraphqlHelpers
let(:namespace) { create(:namespace, name: 'frontend-fixtures' ) }
let(:project) { create(:project, :public, :repository) }
let(:user) { project.first_owner }
let!(:pipeline_schedule) { create(:ci_pipeline_schedule, project: project, owner: user) }
+ let!(:pipeline_schedule_inactive) { create(:ci_pipeline_schedule, :inactive, project: project, owner: user) }
let!(:pipeline_schedule_populated) { create(:ci_pipeline_schedule, project: project, owner: user) }
let!(:pipeline_schedule_variable1) { create(:ci_pipeline_schedule_variable, key: 'foo', value: 'foovalue', pipeline_schedule: pipeline_schedule_populated) }
let!(:pipeline_schedule_variable2) { create(:ci_pipeline_schedule_variable, key: 'bar', value: 'barvalue', pipeline_schedule: pipeline_schedule_populated) }
- render_views
+ describe Projects::PipelineSchedulesController, type: :controller do
+ render_views
- before do
- sign_in(user)
- end
+ before do
+ sign_in(user)
+ stub_feature_flags(pipeline_schedules_vue: false)
+ end
+
+ it 'pipeline_schedules/edit.html' do
+ get :edit, params: {
+ namespace_id: project.namespace.to_param,
+ project_id: project,
+ id: pipeline_schedule.id
+ }
+
+ expect(response).to be_successful
+ end
- it 'pipeline_schedules/edit.html' do
- get :edit, params: {
- namespace_id: project.namespace.to_param,
- project_id: project,
- id: pipeline_schedule.id
- }
+ it 'pipeline_schedules/edit_with_variables.html' do
+ get :edit, params: {
+ namespace_id: project.namespace.to_param,
+ project_id: project,
+ id: pipeline_schedule_populated.id
+ }
- expect(response).to be_successful
+ expect(response).to be_successful
+ end
end
- it 'pipeline_schedules/edit_with_variables.html' do
- get :edit, params: {
- namespace_id: project.namespace.to_param,
- project_id: project,
- id: pipeline_schedule_populated.id
- }
+ describe GraphQL::Query, type: :request do
+ before do
+ pipeline_schedule.pipelines << build(:ci_pipeline, project: project)
+ end
+
+ fixtures_path = 'graphql/pipeline_schedules/'
+ get_pipeline_schedules_query = 'get_pipeline_schedules.query.graphql'
+
+ let_it_be(:query) do
+ get_graphql_query_as_string("pipeline_schedules/graphql/queries/#{get_pipeline_schedules_query}")
+ end
+
+ it "#{fixtures_path}#{get_pipeline_schedules_query}.json" do
+ post_graphql(query, current_user: user, variables: { projectPath: project.full_path })
+
+ expect_graphql_errors_to_be_empty
+ end
+
+ it "#{fixtures_path}#{get_pipeline_schedules_query}.as_guest.json" do
+ guest = create(:user)
+ project.add_guest(user)
+
+ post_graphql(query, current_user: guest, variables: { projectPath: project.full_path })
- expect(response).to be_successful
+ expect_graphql_errors_to_be_empty
+ end
end
end
diff --git a/spec/frontend/flash_spec.js b/spec/frontend/flash_spec.js
index e26c52f0bf7..a809bf248bf 100644
--- a/spec/frontend/flash_spec.js
+++ b/spec/frontend/flash_spec.js
@@ -285,6 +285,13 @@ describe('Flash', () => {
expect(document.querySelector('.gl-alert')).toBeNull();
});
+ it('does not crash if calling .dismiss() twice', () => {
+ alert = createAlert({ message: mockMessage });
+
+ alert.dismiss();
+ expect(() => alert.dismiss()).not.toThrow();
+ });
+
it('calls onDismiss when dismissed', () => {
const dismissHandler = jest.fn();
diff --git a/spec/frontend/grafana_integration/components/grafana_integration_spec.js b/spec/frontend/grafana_integration/components/grafana_integration_spec.js
index d2111194097..021a3aa41ed 100644
--- a/spec/frontend/grafana_integration/components/grafana_integration_spec.js
+++ b/spec/frontend/grafana_integration/components/grafana_integration_spec.js
@@ -3,7 +3,7 @@ import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
import { TEST_HOST } from 'helpers/test_constants';
import { mountExtended } from 'helpers/vue_test_utils_helper';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import GrafanaIntegration from '~/grafana_integration/components/grafana_integration.vue';
import { createStore } from '~/grafana_integration/store';
import axios from '~/lib/utils/axios_utils';
@@ -30,7 +30,7 @@ describe('grafana integration component', () => {
afterEach(() => {
if (wrapper.destroy) {
wrapper.destroy();
- createFlash.mockReset();
+ createAlert.mockReset();
refreshCurrentPage.mockReset();
}
});
@@ -113,7 +113,7 @@ describe('grafana integration component', () => {
await nextTick();
await jest.runAllTicks();
- expect(createFlash).toHaveBeenCalledWith({
+ expect(createAlert).toHaveBeenCalledWith({
message: `There was an error saving your changes. ${message}`,
});
});
diff --git a/spec/frontend/groups/components/app_spec.js b/spec/frontend/groups/components/app_spec.js
index a4a7530184d..091ec17d58e 100644
--- a/spec/frontend/groups/components/app_spec.js
+++ b/spec/frontend/groups/components/app_spec.js
@@ -3,7 +3,7 @@ import { shallowMount } from '@vue/test-utils';
import AxiosMockAdapter from 'axios-mock-adapter';
import Vue, { nextTick } from 'vue';
import waitForPromises from 'helpers/wait_for_promises';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import appComponent from '~/groups/components/app.vue';
import groupFolderComponent from '~/groups/components/group_folder.vue';
import groupItemComponent from '~/groups/components/group_item.vue';
@@ -11,6 +11,7 @@ import eventHub from '~/groups/event_hub';
import GroupsService from '~/groups/service/groups_service';
import GroupsStore from '~/groups/store/groups_store';
import EmptyState from '~/groups/components/empty_state.vue';
+import GroupsComponent from '~/groups/components/groups.vue';
import axios from '~/lib/utils/axios_utils';
import * as urlUtilities from '~/lib/utils/url_utility';
import setWindowLocation from 'helpers/set_window_location_helper';
@@ -115,7 +116,7 @@ describe('AppComponent', () => {
return vm.fetchGroups({}).then(() => {
expect(vm.isLoading).toBe(false);
expect(window.scrollTo).toHaveBeenCalledWith({ behavior: 'smooth', top: 0 });
- expect(createFlash).toHaveBeenCalledWith({
+ expect(createAlert).toHaveBeenCalledWith({
message: 'An error occurred. Please try again.',
});
});
@@ -326,7 +327,7 @@ describe('AppComponent', () => {
expect(vm.service.leaveGroup).toHaveBeenCalledWith(childGroupItem.leavePath);
return waitForPromises().then(() => {
expect(vm.store.removeGroup).not.toHaveBeenCalled();
- expect(createFlash).toHaveBeenCalledWith({ message });
+ expect(createAlert).toHaveBeenCalledWith({ message });
expect(vm.targetGroup.isBeingRemoved).toBe(false);
});
});
@@ -341,7 +342,7 @@ describe('AppComponent', () => {
expect(vm.service.leaveGroup).toHaveBeenCalledWith(childGroupItem.leavePath);
return waitForPromises().then(() => {
expect(vm.store.removeGroup).not.toHaveBeenCalled();
- expect(createFlash).toHaveBeenCalledWith({ message });
+ expect(createAlert).toHaveBeenCalledWith({ message });
expect(vm.targetGroup.isBeingRemoved).toBe(false);
});
});
@@ -388,24 +389,27 @@ describe('AppComponent', () => {
});
describe.each`
- action | groups | fromSearch | renderEmptyState | expected
- ${'subgroups_and_projects'} | ${[]} | ${false} | ${true} | ${true}
- ${''} | ${[]} | ${false} | ${true} | ${false}
- ${'subgroups_and_projects'} | ${mockGroups} | ${false} | ${true} | ${false}
- ${'subgroups_and_projects'} | ${[]} | ${true} | ${true} | ${false}
+ action | groups | fromSearch | shouldRenderEmptyState | searchEmpty
+ ${'subgroups_and_projects'} | ${[]} | ${false} | ${true} | ${false}
+ ${''} | ${[]} | ${false} | ${false} | ${false}
+ ${'subgroups_and_projects'} | ${mockGroups} | ${false} | ${false} | ${false}
+ ${'subgroups_and_projects'} | ${[]} | ${true} | ${false} | ${true}
`(
- 'when `action` is $action, `groups` is $groups, `fromSearch` is $fromSearch, and `renderEmptyState` is $renderEmptyState',
- ({ action, groups, fromSearch, renderEmptyState, expected }) => {
- it(expected ? 'renders empty state' : 'does not render empty state', async () => {
+ 'when `action` is $action, `groups` is $groups, and `fromSearch` is $fromSearch',
+ ({ action, groups, fromSearch, shouldRenderEmptyState, searchEmpty }) => {
+ it(`${shouldRenderEmptyState ? 'renders' : 'does not render'} empty state`, async () => {
createShallowComponent({
- propsData: { action, renderEmptyState },
+ propsData: { action, renderEmptyState: true },
});
+ await waitForPromises();
+
vm.updateGroups(groups, fromSearch);
await nextTick();
- expect(wrapper.findComponent(EmptyState).exists()).toBe(expected);
+ expect(wrapper.findComponent(EmptyState).exists()).toBe(shouldRenderEmptyState);
+ expect(wrapper.findComponent(GroupsComponent).props('searchEmpty')).toBe(searchEmpty);
});
},
);
@@ -440,18 +444,10 @@ describe('AppComponent', () => {
expect(eventHub.$on).toHaveBeenCalledWith('showLeaveGroupModal', expect.any(Function));
expect(eventHub.$on).toHaveBeenCalledWith('updatePagination', expect.any(Function));
expect(eventHub.$on).toHaveBeenCalledWith('updateGroups', expect.any(Function));
- });
-
- it('should initialize `searchEmptyMessage` prop with correct string when `hideProjects` is `false`', async () => {
- createShallowComponent();
- await nextTick();
- expect(vm.searchEmptyMessage).toBe('No groups or projects matched your search');
- });
-
- it('should initialize `searchEmptyMessage` prop with correct string when `hideProjects` is `true`', async () => {
- createShallowComponent({ propsData: { hideProjects: true } });
- await nextTick();
- expect(vm.searchEmptyMessage).toBe('No groups matched your search');
+ expect(eventHub.$on).toHaveBeenCalledWith(
+ 'fetchFilteredAndSortedGroups',
+ expect.any(Function),
+ );
});
});
@@ -468,6 +464,46 @@ describe('AppComponent', () => {
expect(eventHub.$off).toHaveBeenCalledWith('showLeaveGroupModal', expect.any(Function));
expect(eventHub.$off).toHaveBeenCalledWith('updatePagination', expect.any(Function));
expect(eventHub.$off).toHaveBeenCalledWith('updateGroups', expect.any(Function));
+ expect(eventHub.$off).toHaveBeenCalledWith(
+ 'fetchFilteredAndSortedGroups',
+ expect.any(Function),
+ );
+ });
+ });
+
+ describe('when `fetchFilteredAndSortedGroups` event is emitted', () => {
+ const search = 'Foo bar';
+ const sort = 'created_asc';
+ const emitFetchFilteredAndSortedGroups = () => {
+ eventHub.$emit('fetchFilteredAndSortedGroups', {
+ filterGroupsBy: search,
+ sortBy: sort,
+ });
+ };
+ let setPaginationInfoSpy;
+
+ beforeEach(() => {
+ setPaginationInfoSpy = jest.spyOn(GroupsStore.prototype, 'setPaginationInfo');
+ createShallowComponent();
+ });
+
+ it('renders loading icon', async () => {
+ emitFetchFilteredAndSortedGroups();
+ await nextTick();
+
+ expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
+ });
+
+ it('calls API with expected params', () => {
+ emitFetchFilteredAndSortedGroups();
+
+ expect(getGroupsSpy).toHaveBeenCalledWith(undefined, undefined, search, sort, undefined);
+ });
+
+ it('updates pagination', () => {
+ emitFetchFilteredAndSortedGroups();
+
+ expect(setPaginationInfoSpy).toHaveBeenCalled();
});
});
diff --git a/spec/frontend/groups/components/group_item_spec.js b/spec/frontend/groups/components/group_item_spec.js
index 3aa66644c19..4570aa33a6c 100644
--- a/spec/frontend/groups/components/group_item_spec.js
+++ b/spec/frontend/groups/components/group_item_spec.js
@@ -245,19 +245,14 @@ describe('GroupItemComponent', () => {
expect(vm.$el.querySelector('.group-list-tree')).toBeDefined();
});
});
+
describe('schema.org props', () => {
describe('when showSchemaMarkup is disabled on the group', () => {
- it.each(['itemprop', 'itemtype', 'itemscope'], 'it does not set %s', (attr) => {
+ it.each(['itemprop', 'itemtype', 'itemscope'])('does not set %s', (attr) => {
expect(wrapper.attributes(attr)).toBeUndefined();
});
- it.each(
- ['.js-group-avatar', '.js-group-name', '.js-group-description'],
- 'it does not set `itemprop` on sub-nodes',
- (selector) => {
- expect(wrapper.find(selector).attributes('itemprop')).toBeUndefined();
- },
- );
});
+
describe('when group has microdata', () => {
beforeEach(() => {
const group = withMicrodata({
diff --git a/spec/frontend/groups/components/groups_spec.js b/spec/frontend/groups/components/groups_spec.js
index 866868eff36..0cbb6cc8309 100644
--- a/spec/frontend/groups/components/groups_spec.js
+++ b/spec/frontend/groups/components/groups_spec.js
@@ -1,4 +1,5 @@
import Vue from 'vue';
+import { GlEmptyState } from '@gitlab/ui';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import GroupFolderComponent from '~/groups/components/group_folder.vue';
@@ -15,7 +16,6 @@ describe('GroupsComponent', () => {
const defaultPropsData = {
groups: mockGroups,
pageInfo: mockPageInfo,
- searchEmptyMessage: 'No matching results',
searchEmpty: false,
};
@@ -67,13 +67,16 @@ describe('GroupsComponent', () => {
expect(wrapper.findComponent(GroupFolderComponent).exists()).toBe(true);
expect(findPaginationLinks().exists()).toBe(true);
- expect(wrapper.findByText(defaultPropsData.searchEmptyMessage).exists()).toBe(false);
+ expect(wrapper.findComponent(GlEmptyState).exists()).toBe(false);
});
it('should render empty search message when `searchEmpty` is `true`', () => {
createComponent({ propsData: { searchEmpty: true } });
- expect(wrapper.findByText(defaultPropsData.searchEmptyMessage).exists()).toBe(true);
+ expect(wrapper.findComponent(GlEmptyState).props()).toMatchObject({
+ title: GroupsComponent.i18n.emptyStateTitle,
+ description: GroupsComponent.i18n.emptyStateDescription,
+ });
});
});
});
diff --git a/spec/frontend/groups/components/new_top_level_group_alert_spec.js b/spec/frontend/groups/components/new_top_level_group_alert_spec.js
new file mode 100644
index 00000000000..db9a5c7b16b
--- /dev/null
+++ b/spec/frontend/groups/components/new_top_level_group_alert_spec.js
@@ -0,0 +1,75 @@
+import { shallowMount } from '@vue/test-utils';
+import NewTopLevelGroupAlert from '~/groups/components/new_top_level_group_alert.vue';
+import { makeMockUserCalloutDismisser } from 'helpers/mock_user_callout_dismisser';
+import { helpPagePath } from '~/helpers/help_page_helper';
+
+describe('NewTopLevelGroupAlert', () => {
+ let wrapper;
+ let userCalloutDismissSpy;
+
+ const findAlert = () => wrapper.findComponent({ ref: 'newTopLevelAlert' });
+ const createSubGroupPath = '/groups/new?parent_id=1#create-group-pane';
+
+ const createComponent = ({ shouldShowCallout = true } = {}) => {
+ userCalloutDismissSpy = jest.fn();
+
+ wrapper = shallowMount(NewTopLevelGroupAlert, {
+ provide: {
+ createSubGroupPath,
+ },
+ stubs: {
+ UserCalloutDismisser: makeMockUserCalloutDismisser({
+ dismiss: userCalloutDismissSpy,
+ shouldShowCallout,
+ }),
+ },
+ });
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('when the component is created', () => {
+ beforeEach(() => {
+ createComponent({
+ shouldShowCallout: true,
+ });
+ });
+
+ it('renders a button with a link to create a new sub-group', () => {
+ expect(findAlert().props('primaryButtonText')).toBe(
+ NewTopLevelGroupAlert.i18n.primaryBtnText,
+ );
+ expect(findAlert().props('primaryButtonLink')).toBe(
+ helpPagePath('user/group/subgroups/index'),
+ );
+ });
+ });
+
+ describe('dismissing the alert', () => {
+ beforeEach(() => {
+ findAlert().vm.$emit('dismiss');
+ });
+
+ it('calls the dismiss callback', () => {
+ expect(userCalloutDismissSpy).toHaveBeenCalled();
+ });
+ });
+
+ describe('when the alert has been dismissed', () => {
+ beforeEach(() => {
+ createComponent({
+ shouldShowCallout: false,
+ });
+ });
+
+ it('does not show the alert', () => {
+ expect(findAlert().exists()).toBe(false);
+ });
+ });
+});
diff --git a/spec/frontend/groups/components/overview_tabs_spec.js b/spec/frontend/groups/components/overview_tabs_spec.js
index 352bf25b84f..93e087e10f2 100644
--- a/spec/frontend/groups/components/overview_tabs_spec.js
+++ b/spec/frontend/groups/components/overview_tabs_spec.js
@@ -1,28 +1,46 @@
-import { GlTab } from '@gitlab/ui';
+import { GlSorting, GlSortingItem, GlTab } from '@gitlab/ui';
import { nextTick } from 'vue';
+import { createLocalVue } from '@vue/test-utils';
import AxiosMockAdapter from 'axios-mock-adapter';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import OverviewTabs from '~/groups/components/overview_tabs.vue';
import GroupsApp from '~/groups/components/app.vue';
+import GroupFolderComponent from '~/groups/components/group_folder.vue';
import GroupsStore from '~/groups/store/groups_store';
import GroupsService from '~/groups/service/groups_service';
import { createRouter } from '~/groups/init_overview_tabs';
+import eventHub from '~/groups/event_hub';
import {
ACTIVE_TAB_SUBGROUPS_AND_PROJECTS,
ACTIVE_TAB_SHARED,
ACTIVE_TAB_ARCHIVED,
+ OVERVIEW_TABS_SORTING_ITEMS,
} from '~/groups/constants';
import axios from '~/lib/utils/axios_utils';
+const localVue = createLocalVue();
+localVue.component('GroupFolder', GroupFolderComponent);
const router = createRouter();
+const [SORTING_ITEM_NAME, , SORTING_ITEM_UPDATED] = OVERVIEW_TABS_SORTING_ITEMS;
describe('OverviewTabs', () => {
let wrapper;
+ let axiosMock;
- const endpoints = {
- subgroups_and_projects: '/groups/foobar/-/children.json',
- shared: '/groups/foobar/-/shared_projects.json',
- archived: '/groups/foobar/-/children.json?archived=only',
+ const defaultProvide = {
+ endpoints: {
+ subgroups_and_projects: '/groups/foobar/-/children.json',
+ shared: '/groups/foobar/-/shared_projects.json',
+ archived: '/groups/foobar/-/children.json?archived=only',
+ },
+ newSubgroupPath: '/groups/new',
+ newProjectPath: 'projects/new',
+ newSubgroupIllustration: '',
+ newProjectIllustration: '',
+ emptySubgroupIllustration: '',
+ canCreateSubgroups: false,
+ canCreateProjects: false,
+ initialSort: 'name_asc',
};
const routerMock = {
@@ -31,12 +49,15 @@ describe('OverviewTabs', () => {
const createComponent = async ({
route = { name: ACTIVE_TAB_SUBGROUPS_AND_PROJECTS, params: { group: 'foo/bar/baz' } },
+ provide = {},
} = {}) => {
wrapper = mountExtended(OverviewTabs, {
router,
provide: {
- endpoints,
+ ...defaultProvide,
+ ...provide,
},
+ localVue,
mocks: { $route: route, $router: routerMock },
});
@@ -47,13 +68,13 @@ describe('OverviewTabs', () => {
const findTab = (name) => wrapper.findByRole('tab', { name });
const findSelectedTab = () => wrapper.findByRole('tab', { selected: true });
- afterEach(() => {
- wrapper.destroy();
+ beforeEach(() => {
+ axiosMock = new AxiosMockAdapter(axios);
});
- beforeEach(async () => {
- // eslint-disable-next-line no-new
- new AxiosMockAdapter(axios);
+ afterEach(() => {
+ wrapper.destroy();
+ axiosMock.restore();
});
it('renders `Subgroups and projects` tab with `GroupsApp` component', async () => {
@@ -68,7 +89,7 @@ describe('OverviewTabs', () => {
expect(tabPanel.findComponent(GroupsApp).props()).toMatchObject({
action: ACTIVE_TAB_SUBGROUPS_AND_PROJECTS,
store: new GroupsStore({ showSchemaMarkup: true }),
- service: new GroupsService(endpoints[ACTIVE_TAB_SUBGROUPS_AND_PROJECTS]),
+ service: new GroupsService(defaultProvide.endpoints[ACTIVE_TAB_SUBGROUPS_AND_PROJECTS]),
hideProjects: false,
renderEmptyState: true,
});
@@ -89,7 +110,7 @@ describe('OverviewTabs', () => {
expect(tabPanel.findComponent(GroupsApp).props()).toMatchObject({
action: ACTIVE_TAB_SHARED,
store: new GroupsStore(),
- service: new GroupsService(endpoints[ACTIVE_TAB_SHARED]),
+ service: new GroupsService(defaultProvide.endpoints[ACTIVE_TAB_SHARED]),
hideProjects: false,
renderEmptyState: false,
});
@@ -112,7 +133,7 @@ describe('OverviewTabs', () => {
expect(tabPanel.findComponent(GroupsApp).props()).toMatchObject({
action: ACTIVE_TAB_ARCHIVED,
store: new GroupsStore(),
- service: new GroupsService(endpoints[ACTIVE_TAB_ARCHIVED]),
+ service: new GroupsService(defaultProvide.endpoints[ACTIVE_TAB_ARCHIVED]),
hideProjects: false,
renderEmptyState: false,
});
@@ -120,6 +141,14 @@ describe('OverviewTabs', () => {
expect(tabPanel.vm.$attrs.lazy).toBe(false);
});
+ it('sets `lazy` prop to `false` for initially active tab and `true` for all other tabs', async () => {
+ await createComponent({ route: { name: ACTIVE_TAB_SHARED, params: { group: 'foo/bar' } } });
+
+ expect(findTabPanels().at(0).vm.$attrs.lazy).toBe(true);
+ expect(findTabPanels().at(1).vm.$attrs.lazy).toBe(false);
+ expect(findTabPanels().at(2).vm.$attrs.lazy).toBe(true);
+ });
+
describe.each([
[
{ name: ACTIVE_TAB_SUBGROUPS_AND_PROJECTS, params: { group: 'foo/bar/baz' } },
@@ -184,4 +213,109 @@ describe('OverviewTabs', () => {
expect(routerMock.push).toHaveBeenCalledWith(expectedRoute);
});
});
+
+ describe('searching and sorting', () => {
+ const setup = async () => {
+ jest.spyOn(eventHub, '$emit');
+ await createComponent();
+
+ // Click through tabs so they are all loaded
+ await findTab(OverviewTabs.i18n[ACTIVE_TAB_SHARED]).trigger('click');
+ await findTab(OverviewTabs.i18n[ACTIVE_TAB_ARCHIVED]).trigger('click');
+ await findTab(OverviewTabs.i18n[ACTIVE_TAB_SUBGROUPS_AND_PROJECTS]).trigger('click');
+ };
+
+ const sharedAssertions = ({ search, sort }) => {
+ it('sets `lazy` prop to `true` for all of the non-active tabs so they are reloaded after sort or search is applied', () => {
+ expect(findTabPanels().at(0).vm.$attrs.lazy).toBe(false);
+ expect(findTabPanels().at(1).vm.$attrs.lazy).toBe(true);
+ expect(findTabPanels().at(2).vm.$attrs.lazy).toBe(true);
+ });
+
+ it('emits `fetchFilteredAndSortedGroups` event from `eventHub`', () => {
+ expect(eventHub.$emit).toHaveBeenCalledWith(
+ `${ACTIVE_TAB_SUBGROUPS_AND_PROJECTS}fetchFilteredAndSortedGroups`,
+ {
+ filterGroupsBy: search,
+ sortBy: sort,
+ },
+ );
+ });
+ };
+
+ describe('when search is typed in', () => {
+ const search = 'Foo bar';
+
+ beforeEach(async () => {
+ await setup();
+ await wrapper.findByPlaceholderText(OverviewTabs.i18n.searchPlaceholder).setValue(search);
+ });
+
+ it('updates query string with `filter` key', () => {
+ expect(routerMock.push).toHaveBeenCalledWith({ query: { filter: search } });
+ });
+
+ sharedAssertions({ search, sort: defaultProvide.initialSort });
+ });
+
+ describe('when sort is changed', () => {
+ beforeEach(async () => {
+ await setup();
+ wrapper.findAllComponents(GlSortingItem).at(2).vm.$emit('click');
+ await nextTick();
+ });
+
+ it('updates query string with `sort` key', () => {
+ expect(routerMock.push).toHaveBeenCalledWith({
+ query: { sort: SORTING_ITEM_UPDATED.asc },
+ });
+ });
+
+ sharedAssertions({ search: '', sort: SORTING_ITEM_UPDATED.asc });
+ });
+
+ describe('when sort direction is changed', () => {
+ beforeEach(async () => {
+ await setup();
+ await wrapper
+ .findByRole('button', { name: 'Sorting Direction: Ascending' })
+ .trigger('click');
+ });
+
+ it('updates query string with `sort` key', () => {
+ expect(routerMock.push).toHaveBeenCalledWith({
+ query: { sort: SORTING_ITEM_NAME.desc },
+ });
+ });
+
+ sharedAssertions({ search: '', sort: SORTING_ITEM_NAME.desc });
+ });
+
+ describe('when `filter` and `sort` query strings are set', () => {
+ beforeEach(async () => {
+ await createComponent({
+ route: {
+ name: ACTIVE_TAB_SUBGROUPS_AND_PROJECTS,
+ params: { group: 'foo/bar/baz' },
+ query: { filter: 'Foo bar', sort: SORTING_ITEM_UPDATED.desc },
+ },
+ });
+ });
+
+ it('sets value of search input', () => {
+ expect(
+ wrapper.findByPlaceholderText(OverviewTabs.i18n.searchPlaceholder).element.value,
+ ).toBe('Foo bar');
+ });
+
+ it('sets sort dropdown', () => {
+ expect(wrapper.findComponent(GlSorting).props()).toMatchObject({
+ text: SORTING_ITEM_UPDATED.label,
+ isAscending: false,
+ });
+
+ expect(wrapper.findAllComponents(GlSortingItem).at(2).vm.$attrs.active).toBe(true);
+ });
+ });
+ });
});
diff --git a/spec/frontend/groups/components/transfer_group_form_spec.js b/spec/frontend/groups/components/transfer_group_form_spec.js
index 8cfe8ce8e18..7cbe6e5bbab 100644
--- a/spec/frontend/groups/components/transfer_group_form_spec.js
+++ b/spec/frontend/groups/components/transfer_group_form_spec.js
@@ -2,7 +2,7 @@ import { GlAlert, GlSprintf } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import Component from '~/groups/components/transfer_group_form.vue';
import ConfirmDanger from '~/vue_shared/components/confirm_danger/confirm_danger.vue';
-import NamespaceSelect from '~/vue_shared/components/namespace_select/namespace_select.vue';
+import NamespaceSelect from '~/vue_shared/components/namespace_select/namespace_select_deprecated.vue';
describe('Transfer group form', () => {
let wrapper;
diff --git a/spec/frontend/groups/store/groups_store_spec.js b/spec/frontend/groups/store/groups_store_spec.js
index 8ac5d7099f1..ce1791d0062 100644
--- a/spec/frontend/groups/store/groups_store_spec.js
+++ b/spec/frontend/groups/store/groups_store_spec.js
@@ -16,13 +16,13 @@ describe('ProjectsStore', () => {
store = new GroupsStore();
expect(Object.keys(store.state).length).toBe(2);
- expect(Array.isArray(store.state.groups)).toBeTruthy();
+ expect(Array.isArray(store.state.groups)).toBe(true);
expect(Object.keys(store.state.pageInfo).length).toBe(0);
- expect(store.hideProjects).toBeFalsy();
+ expect(store.hideProjects).toBe(false);
store = new GroupsStore({ hideProjects: true });
- expect(store.hideProjects).toBeTruthy();
+ expect(store.hideProjects).toBe(true);
});
});
@@ -65,8 +65,8 @@ describe('ProjectsStore', () => {
expect(store.formatGroupItem).toHaveBeenCalledWith(expect.any(Object));
expect(mockParentGroupItem.children.length).toBe(1);
expect(Object.keys(mockParentGroupItem.children[0]).indexOf('fullName')).toBeGreaterThan(-1);
- expect(mockParentGroupItem.isOpen).toBeTruthy();
- expect(mockParentGroupItem.isChildrenLoading).toBeFalsy();
+ expect(mockParentGroupItem.isOpen).toBe(true);
+ expect(mockParentGroupItem.isChildrenLoading).toBe(false);
});
});
diff --git a/spec/frontend/header_search/components/app_spec.js b/spec/frontend/header_search/components/app_spec.js
index 6a138f9a247..b0bfe2b45f0 100644
--- a/spec/frontend/header_search/components/app_spec.js
+++ b/spec/frontend/header_search/components/app_spec.js
@@ -2,6 +2,7 @@ import { GlSearchBoxByType, GlToken, GlIcon } from '@gitlab/ui';
import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { mockTracking } from 'helpers/tracking_helper';
import { s__, sprintf } from '~/locale';
import HeaderSearchApp from '~/header_search/components/app.vue';
import HeaderSearchAutocompleteItems from '~/header_search/components/header_search_autocomplete_items.vue';
@@ -360,22 +361,43 @@ describe('HeaderSearchApp', () => {
describe('Header Search Input', () => {
describe('when dropdown is closed', () => {
- it('onFocus opens dropdown', async () => {
+ let trackingSpy;
+
+ beforeEach(() => {
+ trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
+ });
+
+ it('onFocus opens dropdown and triggers snowplow event', async () => {
expect(findHeaderSearchDropdown().exists()).toBe(false);
findHeaderSearchInput().vm.$emit('focus');
await nextTick();
expect(findHeaderSearchDropdown().exists()).toBe(true);
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, 'focus_input', {
+ label: 'global_search',
+ property: 'top_navigation',
+ });
});
- it('onClick opens dropdown', async () => {
+ it('onClick opens dropdown and triggers snowplow event', async () => {
expect(findHeaderSearchDropdown().exists()).toBe(false);
findHeaderSearchInput().vm.$emit('click');
await nextTick();
expect(findHeaderSearchDropdown().exists()).toBe(true);
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, 'focus_input', {
+ label: 'global_search',
+ property: 'top_navigation',
+ });
+ });
+
+ it('onClick followed by onFocus only triggers a single snowplow event', async () => {
+ findHeaderSearchInput().vm.$emit('click');
+ findHeaderSearchInput().vm.$emit('focus');
+
+ expect(trackingSpy).toHaveBeenCalledTimes(1);
});
});
diff --git a/spec/frontend/ide/components/commit_sidebar/actions_spec.js b/spec/frontend/ide/components/commit_sidebar/actions_spec.js
index c9425f6c9cd..dc103fec5d0 100644
--- a/spec/frontend/ide/components/commit_sidebar/actions_spec.js
+++ b/spec/frontend/ide/components/commit_sidebar/actions_spec.js
@@ -1,7 +1,7 @@
import Vue, { nextTick } from 'vue';
-import { createComponentWithStore } from 'helpers/vue_mount_component_helper';
+import { mount } from '@vue/test-utils';
import { projectData, branches } from 'jest/ide/mock_data';
-import commitActions from '~/ide/components/commit_sidebar/actions.vue';
+import CommitActions from '~/ide/components/commit_sidebar/actions.vue';
import { createStore } from '~/ide/stores';
import {
COMMIT_TO_NEW_BRANCH,
@@ -18,32 +18,27 @@ const BRANCH_REGULAR_NO_ACCESS = 'regular/no-access';
describe('IDE commit sidebar actions', () => {
let store;
- let vm;
+ let wrapper;
const createComponent = ({ hasMR = false, currentBranchId = 'main', emptyRepo = false } = {}) => {
- const Component = Vue.extend(commitActions);
-
- vm = createComponentWithStore(Component, store);
-
- vm.$store.state.currentBranchId = currentBranchId;
- vm.$store.state.currentProjectId = 'abcproject';
+ store.state.currentBranchId = currentBranchId;
+ store.state.currentProjectId = 'abcproject';
const proj = { ...projectData };
proj.branches[currentBranchId] = branches.find((branch) => branch.name === currentBranchId);
proj.empty_repo = emptyRepo;
- Vue.set(vm.$store.state.projects, 'abcproject', proj);
+ Vue.set(store.state.projects, 'abcproject', proj);
if (hasMR) {
- vm.$store.state.currentMergeRequestId = '1';
- vm.$store.state.projects[store.state.currentProjectId].mergeRequests[
+ store.state.currentMergeRequestId = '1';
+ store.state.projects[store.state.currentProjectId].mergeRequests[
store.state.currentMergeRequestId
] = { foo: 'bar' };
}
- vm.$mount();
-
- return vm;
+ wrapper = mount(CommitActions, { store });
+ return wrapper;
};
beforeEach(() => {
@@ -52,17 +47,16 @@ describe('IDE commit sidebar actions', () => {
});
afterEach(() => {
- vm.$destroy();
- vm = null;
+ wrapper.destroy();
});
- const findText = () => vm.$el.textContent;
- const findRadios = () => Array.from(vm.$el.querySelectorAll('input[type="radio"]'));
+ const findText = () => wrapper.text();
+ const findRadios = () => wrapper.findAll('input[type="radio"]');
it('renders 2 groups', () => {
createComponent();
- expect(findRadios().length).toBe(2);
+ expect(findRadios()).toHaveLength(2);
});
it('renders current branch text', () => {
@@ -79,41 +73,38 @@ describe('IDE commit sidebar actions', () => {
expect(findText()).not.toContain('Create a new branch and merge request');
});
- describe('currentBranchText', () => {
- it('escapes current branch', () => {
- const injectedSrc = '<img src="x" />';
- createComponent({ currentBranchId: injectedSrc });
+ it('escapes current branch name', () => {
+ const injectedSrc = '<img src="x" />';
+ const escapedSrc = '&lt;img src=&quot;x&quot; /&gt';
+ createComponent({ currentBranchId: injectedSrc });
- expect(vm.currentBranchText).not.toContain(injectedSrc);
- });
+ expect(wrapper.text()).not.toContain(injectedSrc);
+ expect(wrapper.text).not.toContain(escapedSrc);
});
describe('updateSelectedCommitAction', () => {
it('does not return anything if currentBranch does not exist', () => {
createComponent({ currentBranchId: null });
- expect(vm.$store.dispatch).not.toHaveBeenCalled();
+ expect(store.dispatch).not.toHaveBeenCalled();
});
it('is not called on mount if there is already a selected commitAction', () => {
store.state.commitAction = '1';
createComponent({ currentBranchId: null });
- expect(vm.$store.dispatch).not.toHaveBeenCalled();
+ expect(store.dispatch).not.toHaveBeenCalled();
});
it('calls again after staged changes', async () => {
createComponent({ currentBranchId: null });
- vm.$store.state.currentBranchId = 'main';
- vm.$store.state.changedFiles.push({});
- vm.$store.state.stagedFiles.push({});
+ store.state.currentBranchId = 'main';
+ store.state.changedFiles.push({});
+ store.state.stagedFiles.push({});
await nextTick();
- expect(vm.$store.dispatch).toHaveBeenCalledWith(
- ACTION_UPDATE_COMMIT_ACTION,
- expect.anything(),
- );
+ expect(store.dispatch).toHaveBeenCalledWith(ACTION_UPDATE_COMMIT_ACTION, expect.anything());
});
it.each`
@@ -133,9 +124,7 @@ describe('IDE commit sidebar actions', () => {
({ input, expectedOption }) => {
createComponent(input);
- expect(vm.$store.dispatch.mock.calls).toEqual([
- [ACTION_UPDATE_COMMIT_ACTION, expectedOption],
- ]);
+ expect(store.dispatch.mock.calls).toEqual([[ACTION_UPDATE_COMMIT_ACTION, expectedOption]]);
},
);
});
diff --git a/spec/frontend/ide/components/commit_sidebar/list_item_spec.js b/spec/frontend/ide/components/commit_sidebar/list_item_spec.js
index dea920ecb5e..c9571d39acb 100644
--- a/spec/frontend/ide/components/commit_sidebar/list_item_spec.js
+++ b/spec/frontend/ide/components/commit_sidebar/list_item_spec.js
@@ -1,133 +1,136 @@
+import { mount } from '@vue/test-utils';
+import { GlIcon } from '@gitlab/ui';
import Vue, { nextTick } from 'vue';
import { trimText } from 'helpers/text_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import { createComponentWithStore } from 'helpers/vue_mount_component_helper';
-import listItem from '~/ide/components/commit_sidebar/list_item.vue';
+import ListItem from '~/ide/components/commit_sidebar/list_item.vue';
import { createRouter } from '~/ide/ide_router';
import { createStore } from '~/ide/stores';
import { file } from '../../helpers';
describe('Multi-file editor commit sidebar list item', () => {
- let vm;
- let f;
+ let wrapper;
+ let testFile;
let findPathEl;
let store;
let router;
beforeEach(() => {
store = createStore();
- router = createRouter(store);
+ jest.spyOn(store, 'dispatch');
- const Component = Vue.extend(listItem);
+ router = createRouter(store);
- f = file('test-file');
+ testFile = file('test-file');
- store.state.entries[f.path] = f;
+ store.state.entries[testFile.path] = testFile;
- vm = createComponentWithStore(Component, store, {
- file: f,
- activeFileKey: `staged-${f.key}`,
- }).$mount();
+ wrapper = mount(ListItem, {
+ store,
+ propsData: {
+ file: testFile,
+ activeFileKey: `staged-${testFile.key}`,
+ },
+ });
- findPathEl = vm.$el.querySelector('.multi-file-commit-list-path');
+ findPathEl = wrapper.find('.multi-file-commit-list-path');
});
afterEach(() => {
- vm.$destroy();
+ wrapper.destroy();
});
- const findPathText = () => trimText(findPathEl.textContent);
+ const findPathText = () => trimText(findPathEl.text());
it('renders file path', () => {
- expect(findPathText()).toContain(f.path);
+ expect(findPathText()).toContain(testFile.path);
});
it('correctly renders renamed entries', async () => {
- Vue.set(vm.file, 'prevName', 'Old name');
-
+ Vue.set(testFile, 'prevName', 'Old name');
await nextTick();
- expect(findPathText()).toEqual(`Old name → ${f.name}`);
+
+ expect(findPathText()).toEqual(`Old name → ${testFile.name}`);
});
it('correctly renders entry, the name of which did not change after rename (as within a folder)', async () => {
- Vue.set(vm.file, 'prevName', f.name);
-
+ Vue.set(testFile, 'prevName', testFile.name);
await nextTick();
- expect(findPathText()).toEqual(f.name);
+
+ expect(findPathText()).toEqual(testFile.name);
});
it('opens a closed file in the editor when clicking the file path', async () => {
- jest.spyOn(vm, 'openPendingTab');
jest.spyOn(router, 'push').mockImplementation(() => {});
- findPathEl.click();
-
- await nextTick();
+ await findPathEl.trigger('click');
- expect(vm.openPendingTab).toHaveBeenCalled();
+ expect(store.dispatch).toHaveBeenCalledWith('openPendingTab', expect.anything());
expect(router.push).toHaveBeenCalled();
});
it('calls updateViewer with diff when clicking file', async () => {
- jest.spyOn(vm, 'openFileInEditor');
- jest.spyOn(vm, 'updateViewer');
jest.spyOn(router, 'push').mockImplementation(() => {});
- findPathEl.click();
-
+ await findPathEl.trigger('click');
await waitForPromises();
- expect(vm.updateViewer).toHaveBeenCalledWith('diff');
+ expect(store.dispatch).toHaveBeenCalledWith('updateViewer', 'diff');
});
- describe('computed', () => {
- describe('iconName', () => {
- it('returns modified when not a tempFile', () => {
- expect(vm.iconName).toBe('file-modified');
- });
+ describe('icon name', () => {
+ const getIconName = () => wrapper.findComponent(GlIcon).props('name');
+
+ it('is modified when not a tempFile', () => {
+ expect(getIconName()).toBe('file-modified');
+ });
- it('returns addition when not a tempFile', () => {
- f.tempFile = true;
+ it('is addition when is a tempFile', async () => {
+ testFile.tempFile = true;
+ await nextTick();
- expect(vm.iconName).toBe('file-addition');
- });
+ expect(getIconName()).toBe('file-addition');
+ });
- it('returns deletion', () => {
- f.deleted = true;
+ it('is deletion when is deleted', async () => {
+ testFile.deleted = true;
+ await nextTick();
- expect(vm.iconName).toBe('file-deletion');
- });
+ expect(getIconName()).toBe('file-deletion');
});
+ });
- describe('iconClass', () => {
- it('returns modified when not a tempFile', () => {
- expect(vm.iconClass).toContain('ide-file-modified');
- });
+ describe('icon class', () => {
+ const getIconClass = () => wrapper.findComponent(GlIcon).classes();
- it('returns addition when not a tempFile', () => {
- f.tempFile = true;
+ it('is modified when not a tempFile', () => {
+ expect(getIconClass()).toContain('ide-file-modified');
+ });
- expect(vm.iconClass).toContain('ide-file-addition');
- });
+ it('is addition when is a tempFile', async () => {
+ testFile.tempFile = true;
+ await nextTick();
- it('returns deletion', () => {
- f.deleted = true;
+ expect(getIconClass()).toContain('ide-file-addition');
+ });
- expect(vm.iconClass).toContain('ide-file-deletion');
- });
+ it('returns deletion when is deleted', async () => {
+ testFile.deleted = true;
+ await nextTick();
+
+ expect(getIconClass()).toContain('ide-file-deletion');
});
});
describe('is active', () => {
it('does not add active class when dont keys match', () => {
- expect(vm.$el.querySelector('.is-active')).toBe(null);
+ expect(wrapper.find('.is-active').exists()).toBe(false);
});
it('adds active class when keys match', async () => {
- vm.keyPrefix = 'staged';
+ await wrapper.setProps({ keyPrefix: 'staged' });
- await nextTick();
- expect(vm.$el.querySelector('.is-active')).not.toBe(null);
+ expect(wrapper.find('.is-active').exists()).toBe(true);
});
});
});
diff --git a/spec/frontend/ide/components/commit_sidebar/message_field_spec.js b/spec/frontend/ide/components/commit_sidebar/message_field_spec.js
index ace266aec5e..c2ef29c1059 100644
--- a/spec/frontend/ide/components/commit_sidebar/message_field_spec.js
+++ b/spec/frontend/ide/components/commit_sidebar/message_field_spec.js
@@ -1,135 +1,121 @@
-import Vue, { nextTick } from 'vue';
-import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
-import createComponent from 'helpers/vue_mount_component_helper';
+import { nextTick } from 'vue';
+import { mount } from '@vue/test-utils';
import CommitMessageField from '~/ide/components/commit_sidebar/message_field.vue';
describe('IDE commit message field', () => {
- const Component = Vue.extend(CommitMessageField);
- let vm;
+ let wrapper;
beforeEach(() => {
- setHTMLFixture('<div id="app"></div>');
-
- vm = createComponent(
- Component,
- {
+ wrapper = mount(CommitMessageField, {
+ propsData: {
text: '',
placeholder: 'testing',
},
- '#app',
- );
+ attachTo: document.body,
+ });
});
afterEach(() => {
- vm.$destroy();
-
- resetHTMLFixture();
+ wrapper.destroy();
});
+ const findMessage = () => wrapper.find('textarea');
+ const findHighlights = () => wrapper.findAll('.highlights span');
+ const findMarks = () => wrapper.findAll('mark');
+
it('adds is-focused class on focus', async () => {
- vm.$el.querySelector('textarea').focus();
+ await findMessage().trigger('focus');
- await nextTick();
- expect(vm.$el.querySelector('.is-focused')).not.toBeNull();
+ expect(wrapper.find('.is-focused').exists()).toBe(true);
});
it('removed is-focused class on blur', async () => {
- vm.$el.querySelector('textarea').focus();
+ await findMessage().trigger('focus');
- await nextTick();
- expect(vm.$el.querySelector('.is-focused')).not.toBeNull();
+ expect(wrapper.find('.is-focused').exists()).toBe(true);
- vm.$el.querySelector('textarea').blur();
+ await findMessage().trigger('blur');
- await nextTick();
- expect(vm.$el.querySelector('.is-focused')).toBeNull();
+ expect(wrapper.find('.is-focused').exists()).toBe(false);
});
- it('emits input event on input', () => {
- jest.spyOn(vm, '$emit').mockImplementation();
-
- const textarea = vm.$el.querySelector('textarea');
- textarea.value = 'testing';
-
- textarea.dispatchEvent(new Event('input'));
+ it('emits input event on input', async () => {
+ await findMessage().setValue('testing');
- expect(vm.$emit).toHaveBeenCalledWith('input', 'testing');
+ expect(wrapper.emitted('input')[0]).toStrictEqual(['testing']);
});
describe('highlights', () => {
describe('subject line', () => {
it('does not highlight less than 50 characters', async () => {
- vm.text = 'text less than 50 chars';
+ await wrapper.setProps({ text: 'text less than 50 chars' });
- await nextTick();
- expect(vm.$el.querySelector('.highlights span').textContent).toContain(
- 'text less than 50 chars',
- );
+ expect(findHighlights()).toHaveLength(1);
+ expect(findHighlights().at(0).text()).toContain('text less than 50 chars');
- expect(vm.$el.querySelector('mark').style.display).toBe('none');
+ expect(findMarks()).toHaveLength(1);
+ expect(findMarks().at(0).isVisible()).toBe(false);
});
it('highlights characters over 50 length', async () => {
- vm.text =
- 'text less than 50 chars that should not highlighted. text more than 50 should be highlighted';
+ await wrapper.setProps({
+ text:
+ 'text less than 50 chars that should not highlighted. text more than 50 should be highlighted',
+ });
- await nextTick();
- expect(vm.$el.querySelector('.highlights span').textContent).toContain(
+ expect(findHighlights()).toHaveLength(1);
+ expect(findHighlights().at(0).text()).toContain(
'text less than 50 chars that should not highlighte',
);
- expect(vm.$el.querySelector('mark').style.display).not.toBe('none');
- expect(vm.$el.querySelector('mark').textContent).toBe(
- 'd. text more than 50 should be highlighted',
- );
+ expect(findMarks()).toHaveLength(1);
+ expect(findMarks().at(0).isVisible()).toBe(true);
+ expect(findMarks().at(0).text()).toBe('d. text more than 50 should be highlighted');
});
});
describe('body text', () => {
it('does not highlight body text less tan 72 characters', async () => {
- vm.text = 'subject line\nbody content';
+ await wrapper.setProps({ text: 'subject line\nbody content' });
- await nextTick();
- expect(vm.$el.querySelectorAll('.highlights span').length).toBe(2);
- expect(vm.$el.querySelectorAll('mark')[1].style.display).toBe('none');
+ expect(findHighlights()).toHaveLength(2);
+ expect(findMarks().at(1).isVisible()).toBe(false);
});
it('highlights body text more than 72 characters', async () => {
- vm.text =
- 'subject line\nbody content that will be highlighted when it is more than 72 characters in length';
-
- await nextTick();
- expect(vm.$el.querySelectorAll('.highlights span').length).toBe(2);
- expect(vm.$el.querySelectorAll('mark')[1].style.display).not.toBe('none');
- expect(vm.$el.querySelectorAll('mark')[1].textContent).toBe(' in length');
+ await wrapper.setProps({
+ text:
+ 'subject line\nbody content that will be highlighted when it is more than 72 characters in length',
+ });
+
+ expect(findHighlights()).toHaveLength(2);
+ expect(findMarks().at(1).isVisible()).toBe(true);
+ expect(findMarks().at(1).text()).toBe('in length');
});
it('highlights body text & subject line', async () => {
- vm.text =
- 'text less than 50 chars that should not highlighted\nbody content that will be highlighted when it is more than 72 characters in length';
+ await wrapper.setProps({
+ text:
+ 'text less than 50 chars that should not highlighted\nbody content that will be highlighted when it is more than 72 characters in length',
+ });
- await nextTick();
- expect(vm.$el.querySelectorAll('.highlights span').length).toBe(2);
- expect(vm.$el.querySelectorAll('mark').length).toBe(2);
+ expect(findHighlights()).toHaveLength(2);
+ expect(findMarks()).toHaveLength(2);
- expect(vm.$el.querySelectorAll('mark')[0].textContent).toContain('d');
- expect(vm.$el.querySelectorAll('mark')[1].textContent).toBe(' in length');
+ expect(findMarks().at(0).text()).toContain('d');
+ expect(findMarks().at(1).text()).toBe('in length');
});
});
});
describe('scrolling textarea', () => {
it('updates transform of highlights', async () => {
- vm.text = 'subject line\n\n\n\n\n\n\n\n\n\n\nbody content';
+ await wrapper.setProps({ text: 'subject line\n\n\n\n\n\n\n\n\n\n\nbody content' });
+ findMessage().element.scrollTo(0, 50);
await nextTick();
- vm.$el.querySelector('textarea').scrollTo(0, 50);
- vm.handleScroll();
-
- await nextTick();
- expect(vm.scrollTop).toBe(50);
- expect(vm.$el.querySelector('.highlights').style.transform).toBe('translate3d(0, -50px, 0)');
+ expect(wrapper.find('.highlights').element.style.transform).toBe('translate3d(0, -50px, 0)');
});
});
});
diff --git a/spec/frontend/ide/components/commit_sidebar/radio_group_spec.js b/spec/frontend/ide/components/commit_sidebar/radio_group_spec.js
index ee6ed694285..a3fa03a4aa5 100644
--- a/spec/frontend/ide/components/commit_sidebar/radio_group_spec.js
+++ b/spec/frontend/ide/components/commit_sidebar/radio_group_spec.js
@@ -1,123 +1,116 @@
-import Vue, { nextTick } from 'vue';
-import { createComponentWithStore } from 'helpers/vue_mount_component_helper';
+import { GlFormRadioGroup } from '@gitlab/ui';
+import { mount } from '@vue/test-utils';
import RadioGroup from '~/ide/components/commit_sidebar/radio_group.vue';
import { createStore } from '~/ide/stores';
+import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
describe('IDE commit sidebar radio group', () => {
- let vm;
+ let wrapper;
let store;
- beforeEach(async () => {
+ const createComponent = (config = {}) => {
store = createStore();
- const Component = Vue.extend(RadioGroup);
-
store.state.commit.commitAction = '2';
+ store.state.commit.newBranchName = 'test-123';
- vm = createComponentWithStore(Component, store, {
- value: '1',
- label: 'test',
- checked: true,
+ wrapper = mount(RadioGroup, {
+ store,
+ propsData: config.props,
+ slots: config.slots,
+ directives: {
+ GlTooltip: createMockDirective(),
+ },
});
-
- vm.$mount();
-
- await nextTick();
- });
+ };
afterEach(() => {
- vm.$destroy();
+ wrapper.destroy();
});
- it('uses label if present', () => {
- expect(vm.$el.textContent).toContain('test');
- });
+ describe('without input', () => {
+ const props = {
+ value: '1',
+ label: 'test',
+ checked: true,
+ };
- it('uses slot if label is not present', async () => {
- vm.$destroy();
+ it('uses label if present', () => {
+ createComponent({ props });
- vm = new Vue({
- components: {
- RadioGroup,
- },
- store,
- render: (createElement) =>
- createElement('radio-group', { props: { value: '1' } }, 'Testing slot'),
+ expect(wrapper.text()).toContain('test');
});
- vm.$mount();
+ it('uses slot if label is not present', () => {
+ createComponent({ props: { value: '1', checked: true }, slots: { default: 'Testing slot' } });
- await nextTick();
- expect(vm.$el.textContent).toContain('Testing slot');
- });
+ expect(wrapper.text()).toContain('Testing slot');
+ });
- it('updates store when changing radio button', async () => {
- vm.$el.querySelector('input').dispatchEvent(new Event('change'));
+ it('updates store when changing radio button', async () => {
+ createComponent({ props });
- await nextTick();
- expect(store.state.commit.commitAction).toBe('1');
+ await wrapper.find('input').trigger('change');
+
+ expect(store.state.commit.commitAction).toBe('1');
+ });
});
describe('with input', () => {
- beforeEach(async () => {
- vm.$destroy();
-
- const Component = Vue.extend(RadioGroup);
-
- store.state.commit.commitAction = '1';
- store.state.commit.newBranchName = 'test-123';
-
- vm = createComponentWithStore(Component, store, {
- value: '1',
- label: 'test',
- checked: true,
- showInput: true,
- });
-
- vm.$mount();
-
- await nextTick();
- });
+ const props = {
+ value: '2',
+ label: 'test',
+ checked: true,
+ showInput: true,
+ };
it('renders input box when commitAction matches value', () => {
- expect(vm.$el.querySelector('.form-control')).not.toBeNull();
+ createComponent({ props: { ...props, value: '2' } });
+
+ expect(wrapper.find('.form-control').exists()).toBe(true);
});
- it('hides input when commitAction doesnt match value', async () => {
- store.state.commit.commitAction = '2';
+ it('hides input when commitAction doesnt match value', () => {
+ createComponent({ props: { ...props, value: '1' } });
- await nextTick();
- expect(vm.$el.querySelector('.form-control')).toBeNull();
+ expect(wrapper.find('.form-control').exists()).toBe(false);
});
it('updates branch name in store on input', async () => {
- const input = vm.$el.querySelector('.form-control');
- input.value = 'testing-123';
- input.dispatchEvent(new Event('input'));
+ createComponent({ props });
+
+ await wrapper.find('.form-control').setValue('testing-123');
- await nextTick();
expect(store.state.commit.newBranchName).toBe('testing-123');
});
it('renders newBranchName if present', () => {
- const input = vm.$el.querySelector('.form-control');
+ createComponent({ props });
- expect(input.value).toBe('test-123');
+ const input = wrapper.find('.form-control');
+
+ expect(input.element.value).toBe('test-123');
});
});
describe('tooltipTitle', () => {
it('returns title when disabled', () => {
- vm.title = 'test title';
- vm.disabled = true;
+ createComponent({
+ props: { value: '1', label: 'test', disabled: true, title: 'test title' },
+ });
- expect(vm.tooltipTitle).toBe('test title');
+ const tooltip = getBinding(wrapper.findComponent(GlFormRadioGroup).element, 'gl-tooltip');
+ expect(tooltip.value).toBe('test title');
});
it('returns blank when not disabled', () => {
- vm.title = 'test title';
+ createComponent({
+ props: { value: '1', label: 'test', title: 'test title' },
+ });
+
+ const tooltip = getBinding(wrapper.findComponent(GlFormRadioGroup).element, 'gl-tooltip');
- expect(vm.tooltipTitle).not.toBe('test title');
+ expect(tooltip.value).toBe('');
});
});
});
diff --git a/spec/frontend/ide/components/file_row_extra_spec.js b/spec/frontend/ide/components/file_row_extra_spec.js
index 5a7a1fe7db0..281c549a1b4 100644
--- a/spec/frontend/ide/components/file_row_extra_spec.js
+++ b/spec/frontend/ide/components/file_row_extra_spec.js
@@ -1,146 +1,146 @@
-import Vue, { nextTick } from 'vue';
-import { createComponentWithStore } from 'helpers/vue_mount_component_helper';
+import Vuex from 'vuex';
+import { mount } from '@vue/test-utils';
import FileRowExtra from '~/ide/components/file_row_extra.vue';
-import { createStore } from '~/ide/stores';
+import { createStoreOptions } from '~/ide/stores';
import { file } from '../helpers';
describe('IDE extra file row component', () => {
- let Component;
- let vm;
+ let wrapper;
+ let store;
let unstagedFilesCount = 0;
let stagedFilesCount = 0;
let changesCount = 0;
- beforeAll(() => {
- Component = Vue.extend(FileRowExtra);
- });
+ const createComponent = (fileProps) => {
+ const storeConfig = createStoreOptions();
- beforeEach(() => {
- vm = createComponentWithStore(Component, createStore(), {
- file: {
- ...file('test'),
+ store = new Vuex.Store({
+ ...storeConfig,
+ getters: {
+ getUnstagedFilesCountForPath: () => () => unstagedFilesCount,
+ getStagedFilesCountForPath: () => () => stagedFilesCount,
+ getChangesInFolder: () => () => changesCount,
},
- dropdownOpen: false,
});
- jest.spyOn(vm, 'getUnstagedFilesCountForPath', 'get').mockReturnValue(() => unstagedFilesCount);
- jest.spyOn(vm, 'getStagedFilesCountForPath', 'get').mockReturnValue(() => stagedFilesCount);
- jest.spyOn(vm, 'getChangesInFolder', 'get').mockReturnValue(() => changesCount);
-
- vm.$mount();
- });
+ wrapper = mount(FileRowExtra, {
+ store,
+ propsData: {
+ file: {
+ ...file('test'),
+ type: 'tree',
+ ...fileProps,
+ },
+ dropdownOpen: false,
+ },
+ });
+ };
afterEach(() => {
- vm.$destroy();
+ wrapper.destroy();
stagedFilesCount = 0;
unstagedFilesCount = 0;
changesCount = 0;
});
- describe('folderChangesTooltip', () => {
- it('returns undefined when changes count is 0', () => {
- changesCount = 0;
-
- expect(vm.folderChangesTooltip).toBe(undefined);
- });
-
+ describe('folder changes tooltip', () => {
[
{ input: 1, output: '1 changed file' },
{ input: 2, output: '2 changed files' },
].forEach(({ input, output }) => {
- it('returns changed files count if changes count is not 0', () => {
+ it('shows changed files count if changes count is not 0', () => {
changesCount = input;
+ createComponent();
- expect(vm.folderChangesTooltip).toBe(output);
+ expect(wrapper.find('.ide-file-modified').attributes('title')).toBe(output);
});
});
});
describe('show tree changes count', () => {
+ const findTreeChangesCount = () => wrapper.find('.ide-tree-changes');
+
it('does not show for blobs', () => {
- vm.file.type = 'blob';
+ createComponent({ type: 'blob' });
- expect(vm.$el.querySelector('.ide-tree-changes')).toBe(null);
+ expect(findTreeChangesCount().exists()).toBe(false);
});
it('does not show when changes count is 0', () => {
- vm.file.type = 'tree';
+ createComponent({ type: 'tree' });
- expect(vm.$el.querySelector('.ide-tree-changes')).toBe(null);
+ expect(findTreeChangesCount().exists()).toBe(false);
});
- it('does not show when tree is open', async () => {
- vm.file.type = 'tree';
- vm.file.opened = true;
+ it('does not show when tree is open', () => {
changesCount = 1;
+ createComponent({ type: 'tree', opened: true });
- await nextTick();
- expect(vm.$el.querySelector('.ide-tree-changes')).toBe(null);
+ expect(findTreeChangesCount().exists()).toBe(false);
});
- it('shows for trees with changes', async () => {
- vm.file.type = 'tree';
- vm.file.opened = false;
+ it('shows for trees with changes', () => {
changesCount = 1;
+ createComponent({ type: 'tree', opened: false });
- await nextTick();
- expect(vm.$el.querySelector('.ide-tree-changes')).not.toBe(null);
+ expect(findTreeChangesCount().exists()).toBe(true);
});
});
describe('changes file icon', () => {
+ const findChangedFileIcon = () => wrapper.find('.file-changed-icon');
+
it('hides when file is not changed', () => {
- expect(vm.$el.querySelector('.file-changed-icon')).toBe(null);
+ createComponent();
+
+ expect(findChangedFileIcon().exists()).toBe(false);
});
- it('shows when file is changed', async () => {
- vm.file.changed = true;
+ it('shows when file is changed', () => {
+ createComponent({ type: 'blob', changed: true });
- await nextTick();
- expect(vm.$el.querySelector('.file-changed-icon')).not.toBe(null);
+ expect(findChangedFileIcon().exists()).toBe(true);
});
- it('shows when file is staged', async () => {
- vm.file.staged = true;
+ it('shows when file is staged', () => {
+ createComponent({ type: 'blob', staged: true });
- await nextTick();
- expect(vm.$el.querySelector('.file-changed-icon')).not.toBe(null);
+ expect(findChangedFileIcon().exists()).toBe(true);
});
- it('shows when file is a tempFile', async () => {
- vm.file.tempFile = true;
+ it('shows when file is a tempFile', () => {
+ createComponent({ type: 'blob', tempFile: true });
- await nextTick();
- expect(vm.$el.querySelector('.file-changed-icon')).not.toBe(null);
+ expect(findChangedFileIcon().exists()).toBe(true);
});
- it('shows when file is renamed', async () => {
- vm.file.prevPath = 'original-file';
+ it('shows when file is renamed', () => {
+ createComponent({ type: 'blob', prevPath: 'original-file' });
- await nextTick();
- expect(vm.$el.querySelector('.file-changed-icon')).not.toBe(null);
+ expect(findChangedFileIcon().exists()).toBe(true);
});
- it('hides when file is renamed', async () => {
- vm.file.prevPath = 'original-file';
- vm.file.type = 'tree';
+ it('hides when tree is renamed', () => {
+ createComponent({ type: 'tree', prevPath: 'original-path' });
- await nextTick();
- expect(vm.$el.querySelector('.file-changed-icon')).toBe(null);
+ expect(findChangedFileIcon().exists()).toBe(false);
});
});
describe('merge request icon', () => {
+ const findMergeRequestIcon = () => wrapper.find('[data-testid="git-merge-icon"]');
+
it('hides when not a merge request change', () => {
- expect(vm.$el.querySelector('[data-testid="git-merge-icon"]')).toBe(null);
+ createComponent();
+
+ expect(findMergeRequestIcon().exists()).toBe(false);
});
- it('shows when a merge request change', async () => {
- vm.file.mrChange = true;
+ it('shows when a merge request change', () => {
+ createComponent({ mrChange: true });
- await nextTick();
- expect(vm.$el.querySelector('[data-testid="git-merge-icon"]')).not.toBe(null);
+ expect(findMergeRequestIcon().exists()).toBe(true);
});
});
});
diff --git a/spec/frontend/ide/components/file_templates/bar_spec.js b/spec/frontend/ide/components/file_templates/bar_spec.js
index aaf9c17ccbf..60f37260393 100644
--- a/spec/frontend/ide/components/file_templates/bar_spec.js
+++ b/spec/frontend/ide/components/file_templates/bar_spec.js
@@ -1,19 +1,16 @@
-import Vue, { nextTick } from 'vue';
-import { mountComponentWithStore } from 'helpers/vue_mount_component_helper';
+import { nextTick } from 'vue';
+import { mount } from '@vue/test-utils';
import Bar from '~/ide/components/file_templates/bar.vue';
import { createStore } from '~/ide/stores';
import { file } from '../../helpers';
describe('IDE file templates bar component', () => {
- let Component;
- let vm;
-
- beforeAll(() => {
- Component = Vue.extend(Bar);
- });
+ let wrapper;
+ let store;
beforeEach(() => {
- const store = createStore();
+ store = createStore();
+ jest.spyOn(store, 'dispatch').mockImplementation();
store.state.openFiles.push({
...file('file'),
@@ -21,24 +18,22 @@ describe('IDE file templates bar component', () => {
active: true,
});
- vm = mountComponentWithStore(Component, { store });
+ wrapper = mount(Bar, { store });
});
afterEach(() => {
- vm.$destroy();
+ wrapper.destroy();
});
describe('template type dropdown', () => {
it('renders dropdown component', () => {
- expect(vm.$el.querySelector('.dropdown').textContent).toContain('Choose a type');
+ expect(wrapper.find('.dropdown').text()).toContain('Choose a type');
});
- it('calls setSelectedTemplateType when clicking item', () => {
- jest.spyOn(vm, 'setSelectedTemplateType').mockImplementation();
-
- vm.$el.querySelector('.dropdown-menu button').click();
+ it('calls setSelectedTemplateType when clicking item', async () => {
+ await wrapper.find('.dropdown-menu button').trigger('click');
- expect(vm.setSelectedTemplateType).toHaveBeenCalledWith({
+ expect(store.dispatch).toHaveBeenCalledWith('fileTemplates/setSelectedTemplateType', {
name: '.gitlab-ci.yml',
key: 'gitlab_ci_ymls',
});
@@ -46,60 +41,52 @@ describe('IDE file templates bar component', () => {
});
describe('template dropdown', () => {
- beforeEach(async () => {
- vm.$store.state.fileTemplates.templates = [
+ beforeEach(() => {
+ store.state.fileTemplates.templates = [
{
name: 'test',
},
];
- vm.$store.state.fileTemplates.selectedTemplateType = {
+ store.state.fileTemplates.selectedTemplateType = {
name: '.gitlab-ci.yml',
key: 'gitlab_ci_ymls',
};
-
- await nextTick();
});
it('renders dropdown component', () => {
- expect(vm.$el.querySelectorAll('.dropdown')[1].textContent).toContain('Choose a template');
+ expect(wrapper.findAll('.dropdown').at(1).text()).toContain('Choose a template');
});
- it('calls fetchTemplate on dropdown open', () => {
- jest.spyOn(vm, 'fetchTemplate').mockImplementation();
-
- vm.$el.querySelectorAll('.dropdown-menu')[1].querySelector('button').click();
+ it('calls fetchTemplate on dropdown open', async () => {
+ await wrapper.findAll('.dropdown-menu').at(1).find('button').trigger('click');
- expect(vm.fetchTemplate).toHaveBeenCalledWith({
+ expect(store.dispatch).toHaveBeenCalledWith('fileTemplates/fetchTemplate', {
name: 'test',
});
});
});
+ const findUndoButton = () => wrapper.find('.btn-default-secondary');
it('shows undo button if updateSuccess is true', async () => {
- vm.$store.state.fileTemplates.updateSuccess = true;
-
+ store.state.fileTemplates.updateSuccess = true;
await nextTick();
- expect(vm.$el.querySelector('.btn-default').style.display).not.toBe('none');
- });
- it('calls undoFileTemplate when clicking undo button', () => {
- jest.spyOn(vm, 'undoFileTemplate').mockImplementation();
+ expect(findUndoButton().isVisible()).toBe(true);
+ });
- vm.$el.querySelector('.btn-default-secondary').click();
+ it('calls undoFileTemplate when clicking undo button', async () => {
+ await findUndoButton().trigger('click');
- expect(vm.undoFileTemplate).toHaveBeenCalled();
+ expect(store.dispatch).toHaveBeenCalledWith('fileTemplates/undoFileTemplate', undefined);
});
it('calls setSelectedTemplateType if activeFile name matches a template', async () => {
const fileName = '.gitlab-ci.yml';
-
- jest.spyOn(vm, 'setSelectedTemplateType').mockImplementation(() => {});
- vm.$store.state.openFiles[0].name = fileName;
-
- vm.setInitialType();
+ store.state.openFiles = [{ ...file(fileName), opened: true, active: true }];
await nextTick();
- expect(vm.setSelectedTemplateType).toHaveBeenCalledWith({
+
+ expect(store.dispatch).toHaveBeenCalledWith('fileTemplates/setSelectedTemplateType', {
name: fileName,
key: 'gitlab_ci_ymls',
});
diff --git a/spec/frontend/ide/components/jobs/__snapshots__/stage_spec.js.snap b/spec/frontend/ide/components/jobs/__snapshots__/stage_spec.js.snap
deleted file mode 100644
index 45444166a50..00000000000
--- a/spec/frontend/ide/components/jobs/__snapshots__/stage_spec.js.snap
+++ /dev/null
@@ -1,60 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`IDE pipeline stage renders stage details & icon 1`] = `
-<div
- class="ide-stage card gl-mt-3"
->
- <div
- class="card-header"
- >
- <ci-icon-stub
- cssclasses=""
- size="24"
- status="[object Object]"
- />
-
- <strong
- class="gl-ml-3 text-truncate"
- data-container="body"
- >
-
- build
-
- </strong>
-
- <div
- class="gl-mr-3 gl-ml-2"
- >
- <gl-badge-stub
- size="md"
- variant="muted"
- >
- 4
- </gl-badge-stub>
- </div>
-
- <gl-icon-stub
- class="ide-stage-collapse-icon"
- name="chevron-lg-down"
- size="16"
- />
- </div>
-
- <div
- class="card-body p-0"
- >
- <item-stub
- job="[object Object]"
- />
- <item-stub
- job="[object Object]"
- />
- <item-stub
- job="[object Object]"
- />
- <item-stub
- job="[object Object]"
- />
- </div>
-</div>
-`;
diff --git a/spec/frontend/ide/components/jobs/detail/description_spec.js b/spec/frontend/ide/components/jobs/detail/description_spec.js
index 128ccff6568..629c4424314 100644
--- a/spec/frontend/ide/components/jobs/detail/description_spec.js
+++ b/spec/frontend/ide/components/jobs/detail/description_spec.js
@@ -1,44 +1,43 @@
-import Vue from 'vue';
-import mountComponent from 'helpers/vue_mount_component_helper';
+import { mount } from '@vue/test-utils';
+import { GlIcon } from '@gitlab/ui';
import Description from '~/ide/components/jobs/detail/description.vue';
import { jobs } from '../../../mock_data';
describe('IDE job description', () => {
- const Component = Vue.extend(Description);
- let vm;
+ let wrapper;
beforeEach(() => {
- vm = mountComponent(Component, {
- job: jobs[0],
+ wrapper = mount(Description, {
+ propsData: {
+ job: jobs[0],
+ },
});
});
afterEach(() => {
- vm.$destroy();
+ wrapper.destroy();
});
it('renders job details', () => {
- expect(vm.$el.textContent).toContain('#1');
- expect(vm.$el.textContent).toContain('test');
+ expect(wrapper.text()).toContain('#1');
+ expect(wrapper.text()).toContain('test');
});
it('renders CI icon', () => {
- expect(
- vm.$el.querySelector('.ci-status-icon [data-testid="status_success_borderless-icon"]'),
- ).not.toBe(null);
+ expect(wrapper.find('.ci-status-icon').findComponent(GlIcon).exists()).toBe(true);
});
it('renders a borderless CI icon', () => {
- expect(
- vm.$el.querySelector('.borderless [data-testid="status_success_borderless-icon"]'),
- ).not.toBe(null);
+ expect(wrapper.find('.borderless').findComponent(GlIcon).exists()).toBe(true);
});
it('renders bridge job details without the job link', () => {
- vm = mountComponent(Component, {
- job: { ...jobs[0], path: undefined },
+ wrapper = mount(Description, {
+ propsData: {
+ job: { ...jobs[0], path: undefined },
+ },
});
- expect(vm.$el.querySelector('[data-testid="description-detail-link"]')).toBe(null);
+ expect(wrapper.find('[data-testid="description-detail-link"]').exists()).toBe(false);
});
});
diff --git a/spec/frontend/ide/components/jobs/detail_spec.js b/spec/frontend/ide/components/jobs/detail_spec.js
index 9122471d421..bf2be3aa595 100644
--- a/spec/frontend/ide/components/jobs/detail_spec.js
+++ b/spec/frontend/ide/components/jobs/detail_spec.js
@@ -1,15 +1,17 @@
-import Vue, { nextTick } from 'vue';
+import { nextTick } from 'vue';
+import { mount } from '@vue/test-utils';
+
import { TEST_HOST } from 'helpers/test_constants';
-import { createComponentWithStore } from 'helpers/vue_mount_component_helper';
import JobDetail from '~/ide/components/jobs/detail.vue';
import { createStore } from '~/ide/stores';
import { jobs } from '../../mock_data';
describe('IDE jobs detail view', () => {
- let vm;
+ let wrapper;
+ let store;
const createComponent = () => {
- const store = createStore();
+ store = createStore();
store.state.pipelines.detailJob = {
...jobs[0],
@@ -18,163 +20,129 @@ describe('IDE jobs detail view', () => {
rawPath: `${TEST_HOST}/raw`,
};
- return createComponentWithStore(Vue.extend(JobDetail), store);
+ jest.spyOn(store, 'dispatch');
+ store.dispatch.mockResolvedValue();
+
+ wrapper = mount(JobDetail, { store });
};
- beforeEach(() => {
- vm = createComponent();
+ const findBuildJobLog = () => wrapper.find('pre');
+ const findScrollToBottomButton = () => wrapper.find('button[aria-label="Scroll to bottom"]');
+ const findScrollToTopButton = () => wrapper.find('button[aria-label="Scroll to top"]');
- jest.spyOn(vm, 'fetchJobLogs').mockResolvedValue();
+ beforeEach(() => {
+ createComponent();
});
afterEach(() => {
- vm.$destroy();
+ wrapper.destroy();
});
describe('mounted', () => {
- beforeEach(() => {
- vm = vm.$mount();
- });
+ const findJobOutput = () => wrapper.find('.bash');
+ const findBuildLoaderAnimation = () => wrapper.find('.build-loader-animation');
it('calls fetchJobLogs', () => {
- expect(vm.fetchJobLogs).toHaveBeenCalled();
+ expect(store.dispatch).toHaveBeenCalledWith('pipelines/fetchJobLogs', undefined);
});
it('scrolls to bottom', () => {
- expect(vm.$refs.buildJobLog.scrollTo).toHaveBeenCalled();
+ expect(findBuildJobLog().element.scrollTo).toHaveBeenCalled();
});
it('renders job output', () => {
- expect(vm.$el.querySelector('.bash').textContent).toContain('testing');
+ expect(findJobOutput().text()).toContain('testing');
});
it('renders empty message output', async () => {
- vm.$store.state.pipelines.detailJob.output = '';
-
+ store.state.pipelines.detailJob.output = '';
await nextTick();
- expect(vm.$el.querySelector('.bash').textContent).toContain('No messages were logged');
+
+ expect(findJobOutput().text()).toContain('No messages were logged');
});
it('renders loading icon', () => {
- expect(vm.$el.querySelector('.build-loader-animation')).not.toBe(null);
- expect(vm.$el.querySelector('.build-loader-animation').style.display).toBe('');
+ expect(findBuildLoaderAnimation().exists()).toBe(true);
+ expect(findBuildLoaderAnimation().isVisible()).toBe(true);
});
it('hides output when loading', () => {
- expect(vm.$el.querySelector('.bash')).not.toBe(null);
- expect(vm.$el.querySelector('.bash').style.display).toBe('none');
+ expect(findJobOutput().exists()).toBe(true);
+ expect(findJobOutput().isVisible()).toBe(false);
});
it('hide loading icon when isLoading is false', async () => {
- vm.$store.state.pipelines.detailJob.isLoading = false;
-
+ store.state.pipelines.detailJob.isLoading = false;
await nextTick();
- expect(vm.$el.querySelector('.build-loader-animation').style.display).toBe('none');
- });
- it('resets detailJob when clicking header button', () => {
- jest.spyOn(vm, 'setDetailJob').mockImplementation();
+ expect(findBuildLoaderAnimation().isVisible()).toBe(false);
+ });
- vm.$el.querySelector('.btn').click();
+ it('resets detailJob when clicking header button', async () => {
+ await wrapper.find('.btn').trigger('click');
- expect(vm.setDetailJob).toHaveBeenCalledWith(null);
+ expect(store.dispatch).toHaveBeenCalledWith('pipelines/setDetailJob', null);
});
it('renders raw path link', () => {
- expect(vm.$el.querySelector('.controllers-buttons').getAttribute('href')).toBe(
- `${TEST_HOST}/raw`,
- );
+ expect(wrapper.find('.controllers-buttons').attributes('href')).toBe(`${TEST_HOST}/raw`);
});
});
describe('scroll buttons', () => {
beforeEach(() => {
- vm = createComponent();
- jest.spyOn(vm, 'fetchJobLogs').mockResolvedValue();
- });
-
- afterEach(() => {
- vm.$destroy();
+ createComponent();
});
it.each`
- fnName | btnName | scrollPos
- ${'scrollDown'} | ${'down'} | ${0}
- ${'scrollUp'} | ${'up'} | ${1}
- `('triggers $fnName when clicking $btnName button', async ({ fnName, scrollPos }) => {
- jest.spyOn(vm, fnName).mockImplementation();
-
- vm = vm.$mount();
+ fnName | btnName | scrollPos | targetScrollPos
+ ${'scroll down'} | ${'down'} | ${0} | ${200}
+ ${'scroll up'} | ${'up'} | ${200} | ${0}
+ `('triggers $fnName when clicking $btnName button', async ({ scrollPos, targetScrollPos }) => {
+ jest.spyOn(findBuildJobLog().element, 'offsetHeight', 'get').mockReturnValue(0);
+ jest.spyOn(findBuildJobLog().element, 'scrollHeight', 'get').mockReturnValue(200);
+ jest.spyOn(findBuildJobLog().element, 'scrollTop', 'get').mockReturnValue(scrollPos);
+ findBuildJobLog().element.scrollTo.mockReset();
- vm.scrollPos = scrollPos;
-
- await nextTick();
- vm.$el.querySelector('.btn-scroll:not([disabled])').click();
- expect(vm[fnName]).toHaveBeenCalled();
- });
- });
-
- describe('scrollDown', () => {
- beforeEach(() => {
- vm = vm.$mount();
-
- jest.spyOn(vm.$refs.buildJobLog, 'scrollTo').mockImplementation();
- });
-
- it('scrolls build trace to bottom', () => {
- jest.spyOn(vm.$refs.buildJobLog, 'scrollHeight', 'get').mockReturnValue(1000);
-
- vm.scrollDown();
-
- expect(vm.$refs.buildJobLog.scrollTo).toHaveBeenCalledWith(0, 1000);
- });
- });
-
- describe('scrollUp', () => {
- beforeEach(() => {
- vm = vm.$mount();
-
- jest.spyOn(vm.$refs.buildJobLog, 'scrollTo').mockImplementation();
- });
+ await findBuildJobLog().trigger('scroll'); // trigger button updates
- it('scrolls build trace to top', () => {
- vm.scrollUp();
+ await wrapper.find('.controllers button:not(:disabled)').trigger('click');
- expect(vm.$refs.buildJobLog.scrollTo).toHaveBeenCalledWith(0, 0);
+ expect(findBuildJobLog().element.scrollTo).toHaveBeenCalledWith(0, targetScrollPos);
});
});
- describe('scrollBuildLog', () => {
+ describe('scrolling build log', () => {
beforeEach(() => {
- vm = vm.$mount();
- jest.spyOn(vm.$refs.buildJobLog, 'scrollTo').mockImplementation();
- jest.spyOn(vm.$refs.buildJobLog, 'offsetHeight', 'get').mockReturnValue(100);
- jest.spyOn(vm.$refs.buildJobLog, 'scrollHeight', 'get').mockReturnValue(200);
+ jest.spyOn(findBuildJobLog().element, 'offsetHeight', 'get').mockReturnValue(100);
+ jest.spyOn(findBuildJobLog().element, 'scrollHeight', 'get').mockReturnValue(200);
});
- it('sets scrollPos to bottom when at the bottom', () => {
- jest.spyOn(vm.$refs.buildJobLog, 'scrollTop', 'get').mockReturnValue(100);
+ it('keeps scroll at bottom when already at the bottom', async () => {
+ jest.spyOn(findBuildJobLog().element, 'scrollTop', 'get').mockReturnValue(100);
- vm.scrollBuildLog();
+ await findBuildJobLog().trigger('scroll');
- expect(vm.scrollPos).toBe(1);
+ expect(findScrollToBottomButton().attributes('disabled')).toBe('disabled');
+ expect(findScrollToTopButton().attributes('disabled')).not.toBe('disabled');
});
- it('sets scrollPos to top when at the top', () => {
- jest.spyOn(vm.$refs.buildJobLog, 'scrollTop', 'get').mockReturnValue(0);
- vm.scrollPos = 1;
+ it('keeps scroll at top when already at top', async () => {
+ jest.spyOn(findBuildJobLog().element, 'scrollTop', 'get').mockReturnValue(0);
- vm.scrollBuildLog();
+ await findBuildJobLog().trigger('scroll');
- expect(vm.scrollPos).toBe(0);
+ expect(findScrollToBottomButton().attributes('disabled')).not.toBe('disabled');
+ expect(findScrollToTopButton().attributes('disabled')).toBe('disabled');
});
- it('resets scrollPos when not at top or bottom', () => {
- jest.spyOn(vm.$refs.buildJobLog, 'scrollTop', 'get').mockReturnValue(10);
+ it('resets scroll when not at top or bottom', async () => {
+ jest.spyOn(findBuildJobLog().element, 'scrollTop', 'get').mockReturnValue(10);
- vm.scrollBuildLog();
+ await findBuildJobLog().trigger('scroll');
- expect(vm.scrollPos).toBe('');
+ expect(findScrollToBottomButton().attributes('disabled')).not.toBe('disabled');
+ expect(findScrollToTopButton().attributes('disabled')).not.toBe('disabled');
});
});
});
diff --git a/spec/frontend/ide/components/jobs/item_spec.js b/spec/frontend/ide/components/jobs/item_spec.js
index c76760a5522..32e27333e42 100644
--- a/spec/frontend/ide/components/jobs/item_spec.js
+++ b/spec/frontend/ide/components/jobs/item_spec.js
@@ -1,36 +1,38 @@
-import Vue, { nextTick } from 'vue';
-import mountComponent from 'helpers/vue_mount_component_helper';
+import { mount } from '@vue/test-utils';
+import { GlButton } from '@gitlab/ui';
+
import JobItem from '~/ide/components/jobs/item.vue';
import { jobs } from '../../mock_data';
describe('IDE jobs item', () => {
- const Component = Vue.extend(JobItem);
const job = jobs[0];
- let vm;
+ let wrapper;
beforeEach(() => {
- vm = mountComponent(Component, {
- job,
- });
+ wrapper = mount(JobItem, { propsData: { job } });
});
afterEach(() => {
- vm.$destroy();
+ wrapper.destroy();
});
it('renders job details', () => {
- expect(vm.$el.textContent).toContain(job.name);
- expect(vm.$el.textContent).toContain(`#${job.id}`);
+ expect(wrapper.text()).toContain(job.name);
+ expect(wrapper.text()).toContain(`#${job.id}`);
});
it('renders CI icon', () => {
- expect(vm.$el.querySelector('[data-testid="status_success_borderless-icon"]')).not.toBe(null);
+ expect(wrapper.find('[data-testid="status_success_borderless-icon"]').exists()).toBe(true);
});
it('does not render view logs button if not started', async () => {
- vm.job.started = false;
+ await wrapper.setProps({
+ job: {
+ ...jobs[0],
+ started: false,
+ },
+ });
- await nextTick();
- expect(vm.$el.querySelector('.btn')).toBe(null);
+ expect(wrapper.findComponent(GlButton).exists()).toBe(false);
});
});
diff --git a/spec/frontend/ide/components/jobs/stage_spec.js b/spec/frontend/ide/components/jobs/stage_spec.js
index 1d5e5743a4d..52fbff2f497 100644
--- a/spec/frontend/ide/components/jobs/stage_spec.js
+++ b/spec/frontend/ide/components/jobs/stage_spec.js
@@ -18,8 +18,9 @@ describe('IDE pipeline stage', () => {
},
};
- const findHeader = () => wrapper.findComponent({ ref: 'cardHeader' });
- const findJobList = () => wrapper.findComponent({ ref: 'jobList' });
+ const findHeader = () => wrapper.find('[data-testid="card-header"]');
+ const findJobList = () => wrapper.find('[data-testid="job-list"]');
+ const findStageTitle = () => wrapper.find('[data-testid="stage-title"]');
const createComponent = (props) => {
wrapper = shallowMount(Stage, {
@@ -65,9 +66,9 @@ describe('IDE pipeline stage', () => {
expect(wrapper.emitted().clickViewLog[0][0]).toBe(job);
});
- it('renders stage details & icon', () => {
+ it('renders stage title', () => {
createComponent();
- expect(wrapper.element).toMatchSnapshot();
+ expect(findStageTitle().isVisible()).toBe(true);
});
describe('when collapsed', () => {
diff --git a/spec/frontend/ide/components/new_dropdown/button_spec.js b/spec/frontend/ide/components/new_dropdown/button_spec.js
index 298d7b810e1..a9cfdfd20c1 100644
--- a/spec/frontend/ide/components/new_dropdown/button_spec.js
+++ b/spec/frontend/ide/components/new_dropdown/button_spec.js
@@ -1,59 +1,60 @@
-import Vue, { nextTick } from 'vue';
-import mountComponent from 'helpers/vue_mount_component_helper';
+import { mount } from '@vue/test-utils';
import Button from '~/ide/components/new_dropdown/button.vue';
describe('IDE new entry dropdown button component', () => {
- let Component;
- let vm;
-
- beforeAll(() => {
- Component = Vue.extend(Button);
- });
-
- beforeEach(() => {
- vm = mountComponent(Component, {
- label: 'Testing',
- icon: 'doc-new',
+ let wrapper;
+
+ const createComponent = (props = {}) => {
+ wrapper = mount(Button, {
+ propsData: {
+ label: 'Testing',
+ icon: 'doc-new',
+ ...props,
+ },
});
-
- jest.spyOn(vm, '$emit').mockImplementation(() => {});
- });
+ };
afterEach(() => {
- vm.$destroy();
+ wrapper.destroy();
});
it('renders button with label', () => {
- expect(vm.$el.textContent).toContain('Testing');
+ createComponent();
+
+ expect(wrapper.text()).toContain('Testing');
});
it('renders icon', () => {
- expect(vm.$el.querySelector('[data-testid="doc-new-icon"]')).not.toBe(null);
+ createComponent();
+
+ expect(wrapper.find('[data-testid="doc-new-icon"]').exists()).toBe(true);
});
- it('emits click event', () => {
- vm.$el.click();
+ it('emits click event', async () => {
+ createComponent();
- expect(vm.$emit).toHaveBeenCalledWith('click');
+ await wrapper.trigger('click');
+
+ expect(wrapper.emitted('click')).toHaveLength(1);
});
- it('hides label if showLabel is false', async () => {
- vm.showLabel = false;
+ it('hides label if showLabel is false', () => {
+ createComponent({ showLabel: false });
- await nextTick();
- expect(vm.$el.textContent).not.toContain('Testing');
+ expect(wrapper.text()).not.toContain('Testing');
});
- describe('tooltipTitle', () => {
+ describe('tooltip title', () => {
it('returns empty string when showLabel is true', () => {
- expect(vm.tooltipTitle).toBe('');
+ createComponent({ showLabel: true });
+
+ expect(wrapper.attributes('title')).toBe('');
});
- it('returns label', async () => {
- vm.showLabel = false;
+ it('returns label', () => {
+ createComponent({ showLabel: false });
- await nextTick();
- expect(vm.tooltipTitle).toBe('Testing');
+ expect(wrapper.attributes('title')).toBe('Testing');
});
});
});
diff --git a/spec/frontend/ide/components/new_dropdown/modal_spec.js b/spec/frontend/ide/components/new_dropdown/modal_spec.js
index 68cc08d2ebc..c6f9fd0c4ea 100644
--- a/spec/frontend/ide/components/new_dropdown/modal_spec.js
+++ b/spec/frontend/ide/components/new_dropdown/modal_spec.js
@@ -1,6 +1,6 @@
import { GlButton, GlModal } from '@gitlab/ui';
import { nextTick } from 'vue';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import Modal from '~/ide/components/new_dropdown/modal.vue';
import { createStore } from '~/ide/stores';
import { stubComponent } from 'helpers/stub_component';
@@ -341,7 +341,7 @@ describe('new file modal component', () => {
});
it('does not trigger flash', () => {
- expect(createFlash).not.toHaveBeenCalled();
+ expect(createAlert).not.toHaveBeenCalled();
});
});
@@ -360,7 +360,7 @@ describe('new file modal component', () => {
});
it('does not trigger flash', () => {
- expect(createFlash).not.toHaveBeenCalled();
+ expect(createAlert).not.toHaveBeenCalled();
});
});
});
@@ -380,7 +380,7 @@ describe('new file modal component', () => {
});
it('creates flash', () => {
- expect(createFlash).toHaveBeenCalledWith({
+ expect(createAlert).toHaveBeenCalledWith({
message: 'The name "src" is already taken in this directory.',
fadeTransition: false,
addBodyClass: true,
@@ -405,7 +405,7 @@ describe('new file modal component', () => {
});
it('does not create flash', () => {
- expect(createFlash).not.toHaveBeenCalled();
+ expect(createAlert).not.toHaveBeenCalled();
});
it('dispatches event', () => {
diff --git a/spec/frontend/ide/components/new_dropdown/upload_spec.js b/spec/frontend/ide/components/new_dropdown/upload_spec.js
index 3eafe9e7ccb..fc643589d51 100644
--- a/spec/frontend/ide/components/new_dropdown/upload_spec.js
+++ b/spec/frontend/ide/components/new_dropdown/upload_spec.js
@@ -1,39 +1,34 @@
-import Vue from 'vue';
-import createComponent from 'helpers/vue_mount_component_helper';
-import upload from '~/ide/components/new_dropdown/upload.vue';
+import { mount } from '@vue/test-utils';
+import Upload from '~/ide/components/new_dropdown/upload.vue';
describe('new dropdown upload', () => {
- let vm;
+ let wrapper;
beforeEach(() => {
- const Component = Vue.extend(upload);
-
- vm = createComponent(Component, {
- path: '',
+ wrapper = mount(Upload, {
+ propsData: {
+ path: '',
+ },
});
-
- vm.entryName = 'testing';
-
- jest.spyOn(vm, '$emit');
});
afterEach(() => {
- vm.$destroy();
+ wrapper.destroy();
});
describe('openFile', () => {
it('calls for each file', () => {
const files = ['test', 'test2', 'test3'];
- jest.spyOn(vm, 'readFile').mockImplementation(() => {});
- jest.spyOn(vm.$refs.fileUpload, 'files', 'get').mockReturnValue(files);
+ jest.spyOn(wrapper.vm, 'readFile').mockImplementation(() => {});
+ jest.spyOn(wrapper.vm.$refs.fileUpload, 'files', 'get').mockReturnValue(files);
- vm.openFile();
+ wrapper.vm.openFile();
- expect(vm.readFile.mock.calls.length).toBe(3);
+ expect(wrapper.vm.readFile.mock.calls.length).toBe(3);
files.forEach((file, i) => {
- expect(vm.readFile.mock.calls[i]).toEqual([file]);
+ expect(wrapper.vm.readFile.mock.calls[i]).toEqual([file]);
});
});
});
@@ -48,7 +43,7 @@ describe('new dropdown upload', () => {
type: 'images/png',
};
- vm.readFile(file);
+ wrapper.vm.readFile(file);
expect(FileReader.prototype.readAsDataURL).toHaveBeenCalledWith(file);
});
@@ -71,35 +66,39 @@ describe('new dropdown upload', () => {
it('calls readAsText and creates file in plain text (without encoding) if the file content is plain text', async () => {
const waitForCreate = new Promise((resolve) => {
- vm.$on('create', resolve);
+ wrapper.vm.$on('create', resolve);
});
- vm.createFile(textTarget, textFile);
+ wrapper.vm.createFile(textTarget, textFile);
expect(FileReader.prototype.readAsText).toHaveBeenCalledWith(textFile);
await waitForCreate;
- expect(vm.$emit).toHaveBeenCalledWith('create', {
- name: textFile.name,
- type: 'blob',
- content: 'plain text',
- rawPath: '',
- mimeType: 'test/mime-text',
- });
+ expect(wrapper.emitted('create')[0]).toStrictEqual([
+ {
+ name: textFile.name,
+ type: 'blob',
+ content: 'plain text',
+ rawPath: '',
+ mimeType: 'test/mime-text',
+ },
+ ]);
});
it('creates a blob URL for the content if binary', () => {
- vm.createFile(binaryTarget, binaryFile);
+ wrapper.vm.createFile(binaryTarget, binaryFile);
expect(FileReader.prototype.readAsText).not.toHaveBeenCalled();
- expect(vm.$emit).toHaveBeenCalledWith('create', {
- name: binaryFile.name,
- type: 'blob',
- content: 'ðððð',
- rawPath: 'blob:https://gitlab.com/048c7ac1-98de-4a37-ab1b-0206d0ea7e1b',
- mimeType: 'test/mime-binary',
- });
+ expect(wrapper.emitted('create')[0]).toStrictEqual([
+ {
+ name: binaryFile.name,
+ type: 'blob',
+ content: 'ðððð',
+ rawPath: 'blob:https://gitlab.com/048c7ac1-98de-4a37-ab1b-0206d0ea7e1b',
+ mimeType: 'test/mime-binary',
+ },
+ ]);
});
});
});
diff --git a/spec/frontend/ide/components/shared/tokened_input_spec.js b/spec/frontend/ide/components/shared/tokened_input_spec.js
index 2efef9918b1..b70c9659e46 100644
--- a/spec/frontend/ide/components/shared/tokened_input_spec.js
+++ b/spec/frontend/ide/components/shared/tokened_input_spec.js
@@ -1,5 +1,4 @@
-import Vue, { nextTick } from 'vue';
-import mountComponent from 'helpers/vue_mount_component_helper';
+import { mount } from '@vue/test-utils';
import TokenedInput from '~/ide/components/shared/tokened_input.vue';
const TEST_PLACEHOLDER = 'Searching in test';
@@ -10,120 +9,106 @@ const TEST_TOKENS = [
];
const TEST_VALUE = 'lorem';
-function getTokenElements(vm) {
- return Array.from(vm.$el.querySelectorAll('.filtered-search-token button'));
-}
-
-function createBackspaceEvent() {
- const e = new Event('keyup');
- e.keyCode = 8;
- e.which = e.keyCode;
- e.altKey = false;
- e.ctrlKey = true;
- e.shiftKey = false;
- e.metaKey = false;
- return e;
+function getTokenElements(wrapper) {
+ return wrapper.findAll('.filtered-search-token button');
}
describe('IDE shared/TokenedInput', () => {
- const Component = Vue.extend(TokenedInput);
- let vm;
-
- beforeEach(() => {
- vm = mountComponent(Component, {
- tokens: TEST_TOKENS,
- placeholder: TEST_PLACEHOLDER,
- value: TEST_VALUE,
+ let wrapper;
+
+ const createComponent = (props = {}) => {
+ wrapper = mount(TokenedInput, {
+ propsData: {
+ tokens: TEST_TOKENS,
+ placeholder: TEST_PLACEHOLDER,
+ value: TEST_VALUE,
+ ...props,
+ },
+ attachTo: document.body,
});
-
- jest.spyOn(vm, '$emit').mockImplementation(() => {});
- });
+ };
afterEach(() => {
- vm.$destroy();
+ wrapper.destroy();
});
it('renders tokens', () => {
- const renderedTokens = getTokenElements(vm).map((x) => x.textContent.trim());
+ createComponent();
+ const renderedTokens = getTokenElements(wrapper).wrappers.map((w) => w.text());
expect(renderedTokens).toEqual(TEST_TOKENS.map((x) => x.label));
});
it('renders input', () => {
- expect(vm.$refs.input).toBeInstanceOf(HTMLInputElement);
- expect(vm.$refs.input).toHaveValue(TEST_VALUE);
- });
-
- it('renders placeholder, when tokens are empty', async () => {
- vm.tokens = [];
+ createComponent();
- await nextTick();
- expect(vm.$refs.input).toHaveAttr('placeholder', TEST_PLACEHOLDER);
+ expect(wrapper.find('input').element).toBeInstanceOf(HTMLInputElement);
+ expect(wrapper.find('input').element).toHaveValue(TEST_VALUE);
});
- it('triggers "removeToken" on token click', () => {
- getTokenElements(vm)[0].click();
+ it('renders placeholder, when tokens are empty', () => {
+ createComponent({ tokens: [] });
- expect(vm.$emit).toHaveBeenCalledWith('removeToken', TEST_TOKENS[0]);
+ expect(wrapper.find('input').attributes('placeholder')).toBe(TEST_PLACEHOLDER);
});
- it('when input triggers backspace event, it calls "onBackspace"', () => {
- jest.spyOn(vm, 'onBackspace').mockImplementation(() => {});
+ it('triggers "removeToken" on token click', async () => {
+ createComponent();
+ await getTokenElements(wrapper).at(0).trigger('click');
- vm.$refs.input.dispatchEvent(createBackspaceEvent());
- vm.$refs.input.dispatchEvent(createBackspaceEvent());
-
- expect(vm.onBackspace).toHaveBeenCalledTimes(2);
+ expect(wrapper.emitted('removeToken')[0]).toStrictEqual([TEST_TOKENS[0]]);
});
- it('triggers "removeToken" on backspaces when value is empty', () => {
- vm.value = '';
-
- vm.onBackspace();
+ it('removes token on backspace when value is empty', async () => {
+ createComponent({ value: '' });
- expect(vm.$emit).not.toHaveBeenCalled();
- expect(vm.backspaceCount).toEqual(1);
+ expect(wrapper.emitted('removeToken')).toBeUndefined();
- vm.onBackspace();
+ await wrapper.find('input').trigger('keyup.delete');
+ await wrapper.find('input').trigger('keyup.delete');
- expect(vm.$emit).toHaveBeenCalledWith('removeToken', TEST_TOKENS[TEST_TOKENS.length - 1]);
- expect(vm.backspaceCount).toEqual(0);
+ expect(wrapper.emitted('removeToken')[0]).toStrictEqual([TEST_TOKENS[TEST_TOKENS.length - 1]]);
});
- it('does not trigger "removeToken" on backspaces when value is not empty', () => {
- vm.onBackspace();
- vm.onBackspace();
+ it('does not trigger "removeToken" on backspaces when value is not empty', async () => {
+ createComponent({ value: 'SOMETHING' });
+
+ await wrapper.find('input').trigger('keyup.delete');
+ await wrapper.find('input').trigger('keyup.delete');
- expect(vm.backspaceCount).toEqual(0);
- expect(vm.$emit).not.toHaveBeenCalled();
+ expect(wrapper.emitted('removeToken')).toBeUndefined();
});
- it('does not trigger "removeToken" on backspaces when tokens are empty', () => {
- vm.tokens = [];
+ it('does not trigger "removeToken" on backspaces when tokens are empty', async () => {
+ createComponent({ value: '', tokens: [] });
- vm.onBackspace();
- vm.onBackspace();
+ await wrapper.find('input').trigger('keyup.delete');
+ await wrapper.find('input').trigger('keyup.delete');
- expect(vm.backspaceCount).toEqual(0);
- expect(vm.$emit).not.toHaveBeenCalled();
+ expect(wrapper.emitted('removeToken')).toBeUndefined();
});
- it('triggers "focus" on input focus', () => {
- vm.$refs.input.dispatchEvent(new Event('focus'));
+ it('triggers "focus" on input focus', async () => {
+ createComponent();
- expect(vm.$emit).toHaveBeenCalledWith('focus');
+ await wrapper.find('input').trigger('focus');
+
+ expect(wrapper.emitted('focus')).toHaveLength(1);
});
- it('triggers "blur" on input blur', () => {
- vm.$refs.input.dispatchEvent(new Event('blur'));
+ it('triggers "blur" on input blur', async () => {
+ createComponent();
+
+ await wrapper.find('input').trigger('blur');
- expect(vm.$emit).toHaveBeenCalledWith('blur');
+ expect(wrapper.emitted('blur')).toHaveLength(1);
});
- it('triggers "input" with value on input change', () => {
- vm.$refs.input.value = 'something-else';
- vm.$refs.input.dispatchEvent(new Event('input'));
+ it('triggers "input" with value on input change', async () => {
+ createComponent();
+
+ await wrapper.find('input').setValue('something-else');
- expect(vm.$emit).toHaveBeenCalledWith('input', 'something-else');
+ expect(wrapper.emitted('input')[0]).toStrictEqual(['something-else']);
});
});
diff --git a/spec/frontend/ide/components/terminal/terminal_spec.js b/spec/frontend/ide/components/terminal/terminal_spec.js
index 4da3e1910e9..0d22f7f73fe 100644
--- a/spec/frontend/ide/components/terminal/terminal_spec.js
+++ b/spec/frontend/ide/components/terminal/terminal_spec.js
@@ -171,7 +171,7 @@ describe('IDE Terminal', () => {
it('creates the terminal', () => {
expect(GLTerminal).toHaveBeenCalledWith(wrapper.vm.$refs.terminal);
- expect(wrapper.vm.glterminal).toBeTruthy();
+ expect(wrapper.vm.glterminal).toBeInstanceOf(GLTerminal);
});
describe('scroll listener', () => {
diff --git a/spec/frontend/ide/init_gitlab_web_ide_spec.js b/spec/frontend/ide/init_gitlab_web_ide_spec.js
index ec8559f1b56..067da25cb52 100644
--- a/spec/frontend/ide/init_gitlab_web_ide_spec.js
+++ b/spec/frontend/ide/init_gitlab_web_ide_spec.js
@@ -6,7 +6,7 @@ jest.mock('@gitlab/web-ide');
const ROOT_ELEMENT_ID = 'ide';
const TEST_NONCE = 'test123nonce';
-const TEST_PROJECT = { path_with_namespace: 'group1/project1' };
+const TEST_PROJECT_PATH = 'group1/project1';
const TEST_BRANCH_NAME = '12345-foo-patch';
const TEST_GITLAB_URL = 'https://test-gitlab/';
const TEST_GITLAB_WEB_IDE_PUBLIC_PATH = 'test/webpack/assets/gitlab-web-ide/public/path';
@@ -18,7 +18,7 @@ describe('ide/init_gitlab_web_ide', () => {
el.id = ROOT_ELEMENT_ID;
// why: We'll test that this class is removed later
el.classList.add('ide-loading');
- el.dataset.project = JSON.stringify(TEST_PROJECT);
+ el.dataset.projectPath = TEST_PROJECT_PATH;
el.dataset.cspNonce = TEST_NONCE;
el.dataset.branchName = TEST_BRANCH_NAME;
@@ -43,7 +43,7 @@ describe('ide/init_gitlab_web_ide', () => {
it('calls start with element', () => {
expect(start).toHaveBeenCalledWith(findRootElement(), {
baseUrl: `${TEST_HOST}/${TEST_GITLAB_WEB_IDE_PUBLIC_PATH}`,
- projectPath: TEST_PROJECT.path_with_namespace,
+ projectPath: TEST_PROJECT_PATH,
ref: TEST_BRANCH_NAME,
gitlabUrl: TEST_GITLAB_URL,
nonce: TEST_NONCE,
diff --git a/spec/frontend/ide/stores/actions/merge_request_spec.js b/spec/frontend/ide/stores/actions/merge_request_spec.js
index abc3ba5b0a2..f1b2a7b881a 100644
--- a/spec/frontend/ide/stores/actions/merge_request_spec.js
+++ b/spec/frontend/ide/stores/actions/merge_request_spec.js
@@ -3,7 +3,7 @@ import { range } from 'lodash';
import { stubPerformanceWebAPI } from 'helpers/performance';
import { TEST_HOST } from 'helpers/test_constants';
import testAction from 'helpers/vuex_action_helper';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { leftSidebarViews, PERMISSION_READ_MR, MAX_MR_FILES_AUTO_OPEN } from '~/ide/constants';
import service from '~/ide/services';
import { createStore } from '~/ide/stores';
@@ -139,8 +139,8 @@ describe('IDE store merge request actions', () => {
branchId: 'bar',
})
.catch(() => {
- expect(createFlash).toHaveBeenCalled();
- expect(createFlash.mock.calls[0][0].message).toBe(
+ expect(createAlert).toHaveBeenCalled();
+ expect(createAlert.mock.calls[0][0].message).toBe(
'Error fetching merge requests for bar',
);
});
@@ -520,7 +520,7 @@ describe('IDE store merge request actions', () => {
store.dispatch.mockRejectedValue();
return openMergeRequest(store, mr).catch(() => {
- expect(createFlash).toHaveBeenCalledWith({
+ expect(createAlert).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 cc7d39b4d43..5a5ead4c544 100644
--- a/spec/frontend/ide/stores/actions/project_spec.js
+++ b/spec/frontend/ide/stores/actions/project_spec.js
@@ -2,7 +2,7 @@ import MockAdapter from 'axios-mock-adapter';
import { useMockLocationHelper } from 'helpers/mock_window_location_helper';
import testAction from 'helpers/vuex_action_helper';
import api from '~/api';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import service from '~/ide/services';
import { createStore } from '~/ide/stores';
import {
@@ -97,7 +97,7 @@ describe('IDE store project actions', () => {
});
afterEach(() => {
- createFlash.mockRestore();
+ createAlert.mockRestore();
});
it.each`
@@ -122,7 +122,7 @@ describe('IDE store project actions', () => {
if (!responseSuccess) {
expect(logError).toHaveBeenCalled();
- expect(createFlash).toHaveBeenCalled();
+ expect(createAlert).toHaveBeenCalled();
}
});
});
diff --git a/spec/frontend/ide/stores/actions_spec.js b/spec/frontend/ide/stores/actions_spec.js
index f6d54491d77..fd2c3d18813 100644
--- a/spec/frontend/ide/stores/actions_spec.js
+++ b/spec/frontend/ide/stores/actions_spec.js
@@ -4,6 +4,7 @@ import testAction from 'helpers/vuex_action_helper';
import eventHub from '~/ide/eventhub';
import { createRouter } from '~/ide/ide_router';
import { createStore } from '~/ide/stores';
+import { createAlert } from '~/flash';
import {
init,
stageAllChanges,
@@ -29,6 +30,7 @@ jest.mock('~/lib/utils/url_utility', () => ({
visitUrl: jest.fn(),
joinPaths: jest.requireActual('~/lib/utils/url_utility').joinPaths,
}));
+jest.mock('~/flash');
describe('Multi-file store actions', () => {
let store;
@@ -138,7 +140,7 @@ describe('Multi-file store actions', () => {
name: 'testing/test',
type: 'tree',
});
- expect(tree.tree[0].tempFile).toBeTruthy();
+ expect(tree.tree[0].tempFile).toBe(true);
expect(tree.tree[0].name).toBe('test');
expect(tree.tree[0].type).toBe('tree');
});
@@ -158,7 +160,7 @@ describe('Multi-file store actions', () => {
type: 'tree',
});
expect(store.state.entries[tree.path].tempFile).toEqual(false);
- expect(document.querySelector('.flash-alert')).not.toBeNull();
+ expect(createAlert).toHaveBeenCalled();
});
});
@@ -173,7 +175,7 @@ describe('Multi-file store actions', () => {
});
const f = store.state.entries[name];
- expect(f.tempFile).toBeTruthy();
+ expect(f.tempFile).toBe(true);
expect(f.mimeType).toBe('test/mime');
expect(store.state.trees['abcproject/mybranch'].tree.length).toBe(1);
});
@@ -216,8 +218,10 @@ describe('Multi-file store actions', () => {
name: 'test',
type: 'blob',
});
- expect(document.querySelector('.flash-alert')?.textContent.trim()).toEqual(
- `The name "${f.name}" is already taken in this directory.`,
+ expect(createAlert).toHaveBeenCalledWith(
+ expect.objectContaining({
+ message: `The name "${f.name}" is already taken in this directory.`,
+ }),
);
});
});
@@ -930,7 +934,7 @@ describe('Multi-file store actions', () => {
);
expect(dispatch.mock.calls).toHaveLength(0);
- expect(document.querySelector('.flash-alert')).not.toBeNull();
+ expect(createAlert).toHaveBeenCalled();
});
});
});
diff --git a/spec/frontend/ide/stores/modules/commit/mutations_spec.js b/spec/frontend/ide/stores/modules/commit/mutations_spec.js
index 50342832d75..d277157e737 100644
--- a/spec/frontend/ide/stores/modules/commit/mutations_spec.js
+++ b/spec/frontend/ide/stores/modules/commit/mutations_spec.js
@@ -37,7 +37,7 @@ describe('IDE commit module mutations', () => {
it('updates submitCommitLoading', () => {
mutations.UPDATE_LOADING(state, true);
- expect(state.submitCommitLoading).toBeTruthy();
+ expect(state.submitCommitLoading).toBe(true);
});
});
diff --git a/spec/frontend/ide/stores/modules/terminal/actions/session_controls_spec.js b/spec/frontend/ide/stores/modules/terminal/actions/session_controls_spec.js
index ecda7f304ba..f48797415df 100644
--- a/spec/frontend/ide/stores/modules/terminal/actions/session_controls_spec.js
+++ b/spec/frontend/ide/stores/modules/terminal/actions/session_controls_spec.js
@@ -1,6 +1,6 @@
import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import * as actions from '~/ide/stores/modules/terminal/actions/session_controls';
import { STARTING, PENDING, STOPPING, STOPPED } from '~/ide/stores/modules/terminal/constants';
import * as messages from '~/ide/stores/modules/terminal/messages';
@@ -89,7 +89,7 @@ describe('IDE store terminal session controls actions', () => {
it('flashes message', () => {
actions.receiveStartSessionError({ dispatch });
- expect(createFlash).toHaveBeenCalledWith({
+ expect(createAlert).toHaveBeenCalledWith({
message: messages.UNEXPECTED_ERROR_STARTING,
});
});
@@ -163,7 +163,7 @@ describe('IDE store terminal session controls actions', () => {
it('flashes message', () => {
actions.receiveStopSessionError({ dispatch });
- expect(createFlash).toHaveBeenCalledWith({
+ expect(createAlert).toHaveBeenCalledWith({
message: messages.UNEXPECTED_ERROR_STOPPING,
});
});
diff --git a/spec/frontend/ide/stores/modules/terminal/actions/session_status_spec.js b/spec/frontend/ide/stores/modules/terminal/actions/session_status_spec.js
index eabc69b23aa..fe2328f25c2 100644
--- a/spec/frontend/ide/stores/modules/terminal/actions/session_status_spec.js
+++ b/spec/frontend/ide/stores/modules/terminal/actions/session_status_spec.js
@@ -1,6 +1,6 @@
import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import * as actions from '~/ide/stores/modules/terminal/actions/session_status';
import { PENDING, RUNNING, STOPPING, STOPPED } from '~/ide/stores/modules/terminal/constants';
import * as messages from '~/ide/stores/modules/terminal/messages';
@@ -115,7 +115,7 @@ describe('IDE store terminal session controls actions', () => {
it('flashes message', () => {
actions.receiveSessionStatusError({ dispatch });
- expect(createFlash).toHaveBeenCalledWith({
+ expect(createAlert).toHaveBeenCalledWith({
message: messages.UNEXPECTED_ERROR_STATUS,
});
});
diff --git a/spec/frontend/ide/stores/mutations/tree_spec.js b/spec/frontend/ide/stores/mutations/tree_spec.js
index 6935e57578f..a8c0d7ba2c8 100644
--- a/spec/frontend/ide/stores/mutations/tree_spec.js
+++ b/spec/frontend/ide/stores/mutations/tree_spec.js
@@ -17,11 +17,11 @@ describe('Multi-file store tree mutations', () => {
it('toggles tree open', () => {
mutations.TOGGLE_TREE_OPEN(localState, localTree.path);
- expect(localTree.opened).toBeTruthy();
+ expect(localTree.opened).toBe(true);
mutations.TOGGLE_TREE_OPEN(localState, localTree.path);
- expect(localTree.opened).toBeFalsy();
+ expect(localTree.opened).toBe(false);
});
});
diff --git a/spec/frontend/ide/stores/mutations_spec.js b/spec/frontend/ide/stores/mutations_spec.js
index 4602a0837e0..4117f2648bd 100644
--- a/spec/frontend/ide/stores/mutations_spec.js
+++ b/spec/frontend/ide/stores/mutations_spec.js
@@ -30,13 +30,13 @@ describe('Multi-file store mutations', () => {
entry,
});
- expect(entry.loading).toBeTruthy();
+ expect(entry.loading).toBe(true);
mutations.TOGGLE_LOADING(localState, {
entry,
});
- expect(entry.loading).toBeFalsy();
+ expect(entry.loading).toBe(false);
});
it('toggles loading of entry and sets specific value', () => {
@@ -44,14 +44,14 @@ describe('Multi-file store mutations', () => {
entry,
});
- expect(entry.loading).toBeTruthy();
+ expect(entry.loading).toBe(true);
mutations.TOGGLE_LOADING(localState, {
entry,
forceValue: true,
});
- expect(entry.loading).toBeTruthy();
+ expect(entry.loading).toBe(true);
});
});
diff --git a/spec/frontend/ide/utils_spec.js b/spec/frontend/ide/utils_spec.js
index fd9d481251d..4efc0ac6028 100644
--- a/spec/frontend/ide/utils_spec.js
+++ b/spec/frontend/ide/utils_spec.js
@@ -1,4 +1,5 @@
import { languages } from 'monaco-editor';
+import { setDiagnosticsOptions as yamlDiagnosticsOptions } from 'monaco-yaml';
import {
isTextFile,
registerLanguages,
@@ -203,7 +204,6 @@ describe('WebIDE utils', () => {
};
jest.spyOn(languages.json.jsonDefaults, 'setDiagnosticsOptions');
- jest.spyOn(languages.yaml.yamlDefaults, 'setDiagnosticsOptions');
});
it('registers the given schemas with monaco for both json and yaml languages', () => {
@@ -212,7 +212,7 @@ describe('WebIDE utils', () => {
expect(languages.json.jsonDefaults.setDiagnosticsOptions).toHaveBeenCalledWith(
expect.objectContaining({ schemas: [schema] }),
);
- expect(languages.yaml.yamlDefaults.setDiagnosticsOptions).toHaveBeenCalledWith(
+ expect(yamlDiagnosticsOptions).toHaveBeenCalledWith(
expect.objectContaining({ schemas: [schema] }),
);
});
diff --git a/spec/frontend/import_entities/import_groups/components/import_table_spec.js b/spec/frontend/import_entities/import_groups/components/import_table_spec.js
index f97ea046cbe..a0115cb9349 100644
--- a/spec/frontend/import_entities/import_groups/components/import_table_spec.js
+++ b/spec/frontend/import_entities/import_groups/components/import_table_spec.js
@@ -5,13 +5,14 @@ import VueApollo from 'vue-apollo';
import MockAdapter from 'axios-mock-adapter';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import httpStatus from '~/lib/utils/http_status';
import axios from '~/lib/utils/axios_utils';
import { STATUSES } from '~/import_entities/constants';
import { i18n, ROOT_NAMESPACE } from '~/import_entities/import_groups/constants';
import ImportTable from '~/import_entities/import_groups/components/import_table.vue';
import importGroupsMutation from '~/import_entities/import_groups/graphql/mutations/import_groups.mutation.graphql';
+import PaginationBar from '~/vue_shared/components/pagination_bar/pagination_bar.vue';
import PaginationLinks from '~/vue_shared/components/pagination_links.vue';
import { availableNamespacesFixture, generateFakeEntry } from '../graphql/fixtures';
@@ -246,7 +247,7 @@ describe('import table', () => {
await findImportButtons()[0].trigger('click');
await waitForPromises();
- expect(createFlash).toHaveBeenCalledWith(
+ expect(createAlert).toHaveBeenCalledWith(
expect.objectContaining({
message: i18n.ERROR_IMPORT,
}),
@@ -528,6 +529,17 @@ describe('import table', () => {
});
});
+ it('renders pagination bar with storage key', async () => {
+ createComponent({
+ bulkImportSourceGroups: () => new Promise(() => {}),
+ });
+ await waitForPromises();
+
+ expect(wrapper.getComponent(PaginationBar).props('storageKey')).toBe(
+ ImportTable.LOCAL_STORAGE_KEY,
+ );
+ });
+
describe('unavailable features warning', () => {
it('renders alert when there are unavailable features', async () => {
createComponent({
diff --git a/spec/frontend/import_entities/import_groups/services/status_poller_spec.js b/spec/frontend/import_entities/import_groups/services/status_poller_spec.js
index 01f976562c6..13d2a95ca14 100644
--- a/spec/frontend/import_entities/import_groups/services/status_poller_spec.js
+++ b/spec/frontend/import_entities/import_groups/services/status_poller_spec.js
@@ -1,6 +1,6 @@
import MockAdapter from 'axios-mock-adapter';
import Visibility from 'visibilityjs';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { STATUSES } from '~/import_entities/constants';
import { StatusPoller } from '~/import_entities/import_groups/services/status_poller';
import axios from '~/lib/utils/axios_utils';
@@ -83,7 +83,7 @@ describe('Bulk import status poller', () => {
it('when error occurs shows flash with error', () => {
const [[pollConfig]] = Poll.mock.calls;
pollConfig.errorCallback();
- expect(createFlash).toHaveBeenCalled();
+ expect(createAlert).toHaveBeenCalled();
});
it('when success response arrives updates relevant group status', () => {
diff --git a/spec/frontend/import_entities/import_projects/components/advanced_settings_spec.js b/spec/frontend/import_entities/import_projects/components/advanced_settings_spec.js
new file mode 100644
index 00000000000..68716600592
--- /dev/null
+++ b/spec/frontend/import_entities/import_projects/components/advanced_settings_spec.js
@@ -0,0 +1,60 @@
+import { mount } from '@vue/test-utils';
+import { GlFormCheckbox } from '@gitlab/ui';
+import AdvancedSettingsPanel from '~/import_entities/import_projects/components/advanced_settings.vue';
+
+describe('Import Advanced Settings', () => {
+ let wrapper;
+ const OPTIONAL_STAGES = [
+ { name: 'stage1', label: 'Stage 1' },
+ { name: 'stage2', label: 'Stage 2', details: 'Extra details' },
+ ];
+
+ const createComponent = () => {
+ wrapper = mount(AdvancedSettingsPanel, {
+ propsData: {
+ stages: OPTIONAL_STAGES,
+ value: {
+ stage1: false,
+ stage2: false,
+ },
+ },
+ });
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('renders GLFormCheckbox for each optional stage', () => {
+ expect(wrapper.findAllComponents(GlFormCheckbox)).toHaveLength(OPTIONAL_STAGES.length);
+ });
+
+ it('renders label for each optional stage', () => {
+ wrapper.findAllComponents(GlFormCheckbox).wrappers.forEach((w, idx) => {
+ expect(w.text()).toContain(OPTIONAL_STAGES[idx].label);
+ });
+ });
+
+ it('renders details for stage with details', () => {
+ expect(wrapper.findAllComponents(GlFormCheckbox).at(1).text()).toContain(
+ OPTIONAL_STAGES[1].details,
+ );
+ });
+
+ it('emits new stages selection state when checkbox is changed', () => {
+ const firstCheckbox = wrapper.findComponent(GlFormCheckbox);
+
+ firstCheckbox.vm.$emit('change', true);
+
+ expect(wrapper.emitted('input')[0]).toStrictEqual([
+ {
+ stage1: true,
+ stage2: false,
+ },
+ ]);
+ });
+});
diff --git a/spec/frontend/import_entities/import_projects/components/import_projects_table_spec.js b/spec/frontend/import_entities/import_projects/components/import_projects_table_spec.js
index c0ae4294e3d..53807167fe8 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
@@ -5,6 +5,7 @@ import Vuex from 'vuex';
import { STATUSES } from '~/import_entities/constants';
import ImportProjectsTable from '~/import_entities/import_projects/components/import_projects_table.vue';
import ProviderRepoTableRow from '~/import_entities/import_projects/components/provider_repo_table_row.vue';
+import AdvancedSettingsPanel from '~/import_entities/import_projects/components/advanced_settings.vue';
import * as getters from '~/import_entities/import_projects/store/getters';
import state from '~/import_entities/import_projects/store/state';
@@ -45,6 +46,7 @@ describe('ImportProjectsTable', () => {
slots,
filterable,
paginatable,
+ optionalStages,
} = {}) {
Vue.use(Vuex);
@@ -71,6 +73,7 @@ describe('ImportProjectsTable', () => {
providerTitle,
filterable,
paginatable,
+ optionalStages,
},
slots,
stubs: {
@@ -271,4 +274,23 @@ describe('ImportProjectsTable', () => {
expect(wrapper.text().includes(INCOMPATIBLE_TEXT)).toBe(shouldRenderSlot);
},
);
+
+ it('should not render advanced settings panel when no optional steps are passed', () => {
+ createComponent({ state: { providerRepos: [providerRepo] } });
+
+ expect(wrapper.findComponent(AdvancedSettingsPanel).exists()).toBe(false);
+ });
+
+ it('should render advanced settings panel when no optional steps are passed', () => {
+ const OPTIONAL_STAGES = [{ name: 'step1', label: 'Step 1' }];
+ createComponent({ state: { providerRepos: [providerRepo] }, optionalStages: OPTIONAL_STAGES });
+
+ expect(wrapper.findComponent(AdvancedSettingsPanel).exists()).toBe(true);
+ expect(wrapper.findComponent(AdvancedSettingsPanel).props('stages')).toStrictEqual(
+ OPTIONAL_STAGES,
+ );
+ expect(wrapper.findComponent(AdvancedSettingsPanel).props('value')).toStrictEqual({
+ step1: false,
+ });
+ });
});
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 17a07b1e9f9..40934e90b78 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
@@ -44,7 +44,7 @@ describe('ProviderRepoTableRow', () => {
wrapper = shallowMount(ProviderRepoTableRow, {
store,
- propsData: { availableNamespaces, userNamespace, ...props },
+ propsData: { availableNamespaces, userNamespace, optionalStages: {}, ...props },
});
}
@@ -92,10 +92,24 @@ describe('ProviderRepoTableRow', () => {
await nextTick();
- const { calls } = fetchImport.mock;
+ expect(fetchImport).toHaveBeenCalledWith(expect.anything(), {
+ repoId: repo.importSource.id,
+ optionalStages: {},
+ });
+ });
+
+ it('includes optionalStages to import', async () => {
+ const OPTIONAL_STAGES = { stage1: true, stage2: false };
+ await wrapper.setProps({ optionalStages: OPTIONAL_STAGES });
+
+ findImportButton().vm.$emit('click');
+
+ await nextTick();
- expect(calls).toHaveLength(1);
- expect(calls[0][1]).toBe(repo.importSource.id);
+ expect(fetchImport).toHaveBeenCalledWith(expect.anything(), {
+ repoId: repo.importSource.id,
+ optionalStages: OPTIONAL_STAGES,
+ });
});
});
diff --git a/spec/frontend/import_entities/import_projects/store/actions_spec.js b/spec/frontend/import_entities/import_projects/store/actions_spec.js
index 0ebe8525b5a..e154863f339 100644
--- a/spec/frontend/import_entities/import_projects/store/actions_spec.js
+++ b/spec/frontend/import_entities/import_projects/store/actions_spec.js
@@ -1,7 +1,7 @@
import MockAdapter from 'axios-mock-adapter';
import { TEST_HOST } from 'helpers/test_constants';
import testAction from 'helpers/vuex_action_helper';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { STATUSES } from '~/import_entities/constants';
import actionsFactory from '~/import_entities/import_projects/store/actions';
import { getImportTarget } from '~/import_entities/import_projects/store/getters';
@@ -155,7 +155,7 @@ describe('import_projects store actions', () => {
[],
);
- expect(createFlash).toHaveBeenCalledWith({
+ expect(createAlert).toHaveBeenCalledWith({
message: 'Provider rate limit exceeded. Try again later',
});
});
@@ -198,7 +198,7 @@ describe('import_projects store actions', () => {
return testAction(
fetchImport,
- importRepoId,
+ { repoId: importRepoId, optionalStages: {} },
localState,
[
{
@@ -222,7 +222,7 @@ describe('import_projects store actions', () => {
await testAction(
fetchImport,
- importRepoId,
+ { repoId: importRepoId, optionalStages: {} },
localState,
[
{
@@ -234,7 +234,7 @@ describe('import_projects store actions', () => {
[],
);
- expect(createFlash).toHaveBeenCalledWith({
+ expect(createAlert).toHaveBeenCalledWith({
message: 'Importing the project failed',
});
});
@@ -245,7 +245,7 @@ describe('import_projects store actions', () => {
await testAction(
fetchImport,
- importRepoId,
+ { repoId: importRepoId, optionalStages: {} },
localState,
[
{
@@ -257,7 +257,7 @@ describe('import_projects store actions', () => {
[],
);
- expect(createFlash).toHaveBeenCalledWith({
+ expect(createAlert).toHaveBeenCalledWith({
message: `Importing the project failed: ${ERROR_MESSAGE}`,
});
});
@@ -358,7 +358,7 @@ describe('import_projects store actions', () => {
[],
);
- expect(createFlash).toHaveBeenCalledWith({
+ expect(createAlert).toHaveBeenCalledWith({
message: 'Requesting namespaces failed',
});
});
@@ -366,14 +366,22 @@ describe('import_projects store actions', () => {
describe('importAll', () => {
it('dispatches multiple fetchImport actions', async () => {
+ const OPTIONAL_STAGES = { stage1: true, stage2: false };
+
await testAction(
importAll,
- null,
+ { optionalStages: OPTIONAL_STAGES },
localState,
[],
[
- { type: 'fetchImport', payload: importRepoId },
- { type: 'fetchImport', payload: otherImportRepoId },
+ {
+ type: 'fetchImport',
+ payload: { repoId: importRepoId, optionalStages: OPTIONAL_STAGES },
+ },
+ {
+ type: 'fetchImport',
+ payload: { repoId: otherImportRepoId, optionalStages: OPTIONAL_STAGES },
+ },
],
);
});
diff --git a/spec/frontend/integrations/edit/components/integration_form_spec.js b/spec/frontend/integrations/edit/components/integration_form_spec.js
index 21e57a2e33c..0a3beee0507 100644
--- a/spec/frontend/integrations/edit/components/integration_form_spec.js
+++ b/spec/frontend/integrations/edit/components/integration_form_spec.js
@@ -18,6 +18,7 @@ import {
integrationLevels,
I18N_SUCCESSFUL_CONNECTION_MESSAGE,
I18N_DEFAULT_ERROR_MESSAGE,
+ INTEGRATION_FORM_TYPE_SLACK,
billingPlans,
billingPlanNames,
} from '~/integrations/constants';
@@ -88,6 +89,7 @@ describe('IntegrationForm', () => {
const findConnectionSection = () => findAllSections().at(0);
const findConnectionSectionComponent = () =>
findConnectionSection().findComponent(IntegrationSectionConnection);
+ const findHelpHtml = () => wrapper.findByTestId('help-html');
beforeEach(() => {
mockAxios = new MockAdapter(axios);
@@ -712,5 +714,48 @@ describe('IntegrationForm', () => {
expect(refreshCurrentPage).toHaveBeenCalledTimes(1);
});
});
+
+ describe('Help and sections rendering', () => {
+ const dummyHelp = 'Foo Help';
+
+ it.each`
+ integration | flagIsOn | helpHtml | sections | shouldShowSections | shouldShowHelp
+ ${INTEGRATION_FORM_TYPE_SLACK} | ${false} | ${''} | ${[]} | ${false} | ${false}
+ ${INTEGRATION_FORM_TYPE_SLACK} | ${false} | ${dummyHelp} | ${[]} | ${false} | ${true}
+ ${INTEGRATION_FORM_TYPE_SLACK} | ${false} | ${undefined} | ${[mockSectionConnection]} | ${false} | ${false}
+ ${INTEGRATION_FORM_TYPE_SLACK} | ${false} | ${dummyHelp} | ${[mockSectionConnection]} | ${false} | ${true}
+ ${INTEGRATION_FORM_TYPE_SLACK} | ${true} | ${''} | ${[]} | ${false} | ${false}
+ ${INTEGRATION_FORM_TYPE_SLACK} | ${true} | ${dummyHelp} | ${[]} | ${false} | ${true}
+ ${INTEGRATION_FORM_TYPE_SLACK} | ${true} | ${undefined} | ${[mockSectionConnection]} | ${true} | ${false}
+ ${INTEGRATION_FORM_TYPE_SLACK} | ${true} | ${dummyHelp} | ${[mockSectionConnection]} | ${true} | ${true}
+ ${'foo'} | ${false} | ${''} | ${[]} | ${false} | ${false}
+ ${'foo'} | ${false} | ${dummyHelp} | ${[]} | ${false} | ${true}
+ ${'foo'} | ${false} | ${undefined} | ${[mockSectionConnection]} | ${true} | ${false}
+ ${'foo'} | ${false} | ${dummyHelp} | ${[mockSectionConnection]} | ${true} | ${false}
+ ${'foo'} | ${true} | ${''} | ${[]} | ${false} | ${false}
+ ${'foo'} | ${true} | ${dummyHelp} | ${[]} | ${false} | ${true}
+ ${'foo'} | ${true} | ${undefined} | ${[mockSectionConnection]} | ${true} | ${false}
+ ${'foo'} | ${true} | ${dummyHelp} | ${[mockSectionConnection]} | ${true} | ${false}
+ `(
+ '$sections sections, and "$helpHtml" helpHtml when the FF is "$flagIsOn" for "$integration" integration',
+ ({ integration, flagIsOn, helpHtml, sections, shouldShowSections, shouldShowHelp }) => {
+ createComponent({
+ provide: {
+ helpHtml,
+ glFeatures: { integrationSlackAppNotifications: flagIsOn },
+ },
+ customStateProps: {
+ sections,
+ type: integration,
+ },
+ });
+ expect(findAllSections().length > 0).toEqual(shouldShowSections);
+ expect(findHelpHtml().exists()).toBe(shouldShowHelp);
+ if (shouldShowHelp) {
+ expect(findHelpHtml().html()).toContain(helpHtml);
+ }
+ },
+ );
+ });
});
});
diff --git a/spec/frontend/issuable/bulk_update_sidebar/components/status_select_spec.js b/spec/frontend/issuable/bulk_update_sidebar/components/status_dropdown_spec.js
index 8ecbf41ce56..2f281cb88f9 100644
--- a/spec/frontend/issuable/bulk_update_sidebar/components/status_select_spec.js
+++ b/spec/frontend/issuable/bulk_update_sidebar/components/status_dropdown_spec.js
@@ -1,9 +1,9 @@
import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import StatusSelect from '~/issuable/bulk_update_sidebar/components/status_select.vue';
-import { ISSUE_STATUS_SELECT_OPTIONS } from '~/issuable/bulk_update_sidebar/constants';
+import StatusDropdown from '~/issuable/bulk_update_sidebar/components/status_dropdown.vue';
+import { statusDropdownOptions } from '~/issuable/bulk_update_sidebar/constants';
-describe('StatusSelect', () => {
+describe('SubscriptionsDropdown component', () => {
let wrapper;
const findDropdown = () => wrapper.findComponent(GlDropdown);
@@ -11,7 +11,7 @@ describe('StatusSelect', () => {
const findHiddenInput = () => wrapper.find('input');
function createComponent() {
- wrapper = shallowMount(StatusSelect);
+ wrapper = shallowMount(StatusDropdown);
}
afterEach(() => {
@@ -45,14 +45,12 @@ describe('StatusSelect', () => {
it('updates value of the hidden input', () => {
expect(findHiddenInput().attributes('value')).toBe(
- ISSUE_STATUS_SELECT_OPTIONS[selectItemAtIndex].value,
+ statusDropdownOptions[selectItemAtIndex].value,
);
});
it('updates the dropdown text prop', () => {
- expect(findDropdown().props('text')).toBe(
- ISSUE_STATUS_SELECT_OPTIONS[selectItemAtIndex].text,
- );
+ expect(findDropdown().props('text')).toBe(statusDropdownOptions[selectItemAtIndex].text);
});
it('sets dropdown item `is-checked` prop to `true`', () => {
diff --git a/spec/frontend/issuable/bulk_update_sidebar/components/subscriptions_dropdown_spec.js b/spec/frontend/issuable/bulk_update_sidebar/components/subscriptions_dropdown_spec.js
new file mode 100644
index 00000000000..56ef7a1ed39
--- /dev/null
+++ b/spec/frontend/issuable/bulk_update_sidebar/components/subscriptions_dropdown_spec.js
@@ -0,0 +1,76 @@
+import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
+import SubscriptionsDropdown from '~/issuable/bulk_update_sidebar/components/subscriptions_dropdown.vue';
+import { subscriptionsDropdownOptions } from '~/issuable/bulk_update_sidebar/constants';
+
+describe('SubscriptionsDropdown component', () => {
+ let wrapper;
+
+ const findDropdown = () => wrapper.findComponent(GlDropdown);
+ const findAllDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
+ const findHiddenInput = () => wrapper.find('input');
+
+ function createComponent() {
+ wrapper = shallowMount(SubscriptionsDropdown);
+ }
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('with no value selected', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('hidden input value is undefined', () => {
+ expect(findHiddenInput().attributes('value')).toBeUndefined();
+ });
+
+ it('renders default text', () => {
+ expect(findDropdown().props('text')).toBe(SubscriptionsDropdown.i18n.defaultDropdownText);
+ });
+
+ it('renders dropdown items with `is-checked` prop set to `false`', () => {
+ const dropdownItems = findAllDropdownItems();
+
+ expect(dropdownItems.at(0).props('isChecked')).toBe(false);
+ expect(dropdownItems.at(1).props('isChecked')).toBe(false);
+ });
+ });
+
+ describe('when selecting a value', () => {
+ beforeEach(() => {
+ createComponent();
+ findAllDropdownItems().at(0).vm.$emit('click');
+ });
+
+ it('updates value of the hidden input', () => {
+ expect(findHiddenInput().attributes('value')).toBe(subscriptionsDropdownOptions[0].value);
+ });
+
+ it('updates the dropdown text prop', () => {
+ expect(findDropdown().props('text')).toBe(subscriptionsDropdownOptions[0].text);
+ });
+
+ it('sets dropdown item `is-checked` prop to `true`', () => {
+ const dropdownItems = findAllDropdownItems();
+
+ expect(dropdownItems.at(0).props('isChecked')).toBe(true);
+ expect(dropdownItems.at(1).props('isChecked')).toBe(false);
+ });
+
+ describe('when selecting the value that is already selected', () => {
+ it('clears dropdown selection', async () => {
+ findAllDropdownItems().at(0).vm.$emit('click');
+ await nextTick();
+ const dropdownItems = findAllDropdownItems();
+
+ expect(dropdownItems.at(0).props('isChecked')).toBe(false);
+ expect(dropdownItems.at(1).props('isChecked')).toBe(false);
+ expect(findDropdown().props('text')).toBe(SubscriptionsDropdown.i18n.defaultDropdownText);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/issuable/related_issues/components/related_issues_root_spec.js b/spec/frontend/issuable/related_issues/components/related_issues_root_spec.js
index b518d2fbdec..680dbd68493 100644
--- a/spec/frontend/issuable/related_issues/components/related_issues_root_spec.js
+++ b/spec/frontend/issuable/related_issues/components/related_issues_root_spec.js
@@ -7,7 +7,7 @@ import {
issuable1,
issuable2,
} from 'jest/issuable/components/related_issuable_mock_data';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { linkedIssueTypesMap } from '~/related_issues/constants';
import RelatedIssuesBlock from '~/related_issues/components/related_issues_block.vue';
@@ -136,7 +136,7 @@ describe('RelatedIssuesRoot', () => {
await createComponent();
jest.spyOn(wrapper.vm, 'processAllReferences');
jest.spyOn(wrapper.vm.service, 'addRelatedIssues');
- createFlash.mockClear();
+ createAlert.mockClear();
});
it('processes references before submitting', () => {
@@ -207,12 +207,12 @@ describe('RelatedIssuesRoot', () => {
mock.onPost(defaultProps.endpoint).reply(409, { message });
wrapper.vm.store.setPendingReferences([issuable1.reference, issuable2.reference]);
- expect(createFlash).not.toHaveBeenCalled();
+ expect(createAlert).not.toHaveBeenCalled();
findRelatedIssuesBlock().vm.$emit('addIssuableFormSubmit', input);
await waitForPromises();
- expect(createFlash).toHaveBeenCalledWith({ message });
+ expect(createAlert).toHaveBeenCalledWith({ message });
});
});
diff --git a/spec/frontend/issues/show/components/edited_spec.js b/spec/frontend/issues/show/components/edited_spec.js
index 8a240c38b5f..aa6e0a9dceb 100644
--- a/spec/frontend/issues/show/components/edited_spec.js
+++ b/spec/frontend/issues/show/components/edited_spec.js
@@ -1,7 +1,10 @@
-import { shallowMount } from '@vue/test-utils';
+import { mount } from '@vue/test-utils';
+import { getTimeago } from '~/lib/utils/datetime_utility';
import Edited from '~/issues/show/components/edited.vue';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
+const timeago = getTimeago();
+
describe('Edited component', () => {
let wrapper;
@@ -9,7 +12,8 @@ describe('Edited component', () => {
const findTimeAgoTooltip = () => wrapper.findComponent(TimeAgoTooltip);
const formatText = (text) => text.trim().replace(/\s\s+/g, ' ');
- const mountComponent = (propsData) => shallowMount(Edited, { propsData });
+ const mountComponent = (propsData) => mount(Edited, { propsData });
+ const updatedAt = '2017-05-15T12:31:04.428Z';
afterEach(() => {
wrapper.destroy();
@@ -17,12 +21,12 @@ describe('Edited component', () => {
it('renders an edited at+by string', () => {
wrapper = mountComponent({
- updatedAt: '2017-05-15T12:31:04.428Z',
+ updatedAt,
updatedByName: 'Some User',
updatedByPath: '/some_user',
});
- expect(formatText(wrapper.text())).toBe('Edited by Some User');
+ expect(formatText(wrapper.text())).toBe(`Edited ${timeago.format(updatedAt)} by Some User`);
expect(findAuthorLink().attributes('href')).toBe('/some_user');
expect(findTimeAgoTooltip().exists()).toBe(true);
});
@@ -40,10 +44,10 @@ describe('Edited component', () => {
it('if no updatedByName and updatedByPath is provided, no user element will be rendered', () => {
wrapper = mountComponent({
- updatedAt: '2017-05-15T12:31:04.428Z',
+ updatedAt,
});
- expect(formatText(wrapper.text())).toBe('Edited');
+ expect(formatText(wrapper.text())).toBe(`Edited ${timeago.format(updatedAt)}`);
expect(findAuthorLink().exists()).toBe(false);
expect(findTimeAgoTooltip().exists()).toBe(true);
});
diff --git a/spec/frontend/issues/show/components/fields/description_spec.js b/spec/frontend/issues/show/components/fields/description_spec.js
index 61433607a2b..cd4d422583b 100644
--- a/spec/frontend/issues/show/components/fields/description_spec.js
+++ b/spec/frontend/issues/show/components/fields/description_spec.js
@@ -2,13 +2,15 @@ import { shallowMount } from '@vue/test-utils';
import DescriptionField from '~/issues/show/components/fields/description.vue';
import eventHub from '~/issues/show/event_hub';
import MarkdownField from '~/vue_shared/components/markdown/field.vue';
+import MarkdownEditor from '~/vue_shared/components/markdown/markdown_editor.vue';
describe('Description field component', () => {
let wrapper;
const findTextarea = () => wrapper.findComponent({ ref: 'textarea' });
+ const findMarkdownEditor = () => wrapper.findComponent(MarkdownEditor);
- const mountComponent = (description = 'test') =>
+ const mountComponent = ({ description = 'test', contentEditorOnIssues = false } = {}) =>
shallowMount(DescriptionField, {
attachTo: document.body,
propsData: {
@@ -17,6 +19,11 @@ describe('Description field component', () => {
quickActionsDocsPath: '/',
value: description,
},
+ provide: {
+ glFeatures: {
+ contentEditorOnIssues,
+ },
+ },
stubs: {
MarkdownField,
},
@@ -40,7 +47,7 @@ describe('Description field component', () => {
it('renders markdown field with a markdown description', () => {
const markdown = '**test**';
- wrapper = mountComponent(markdown);
+ wrapper = mountComponent({ description: markdown });
expect(findTextarea().element.value).toBe(markdown);
});
@@ -66,4 +73,52 @@ describe('Description field component', () => {
expect(eventHub.$emit).toHaveBeenCalledWith('update.issuable');
});
+
+ describe('when contentEditorOnIssues feature flag is on', () => {
+ beforeEach(() => {
+ wrapper = mountComponent({ contentEditorOnIssues: true });
+ });
+
+ it('uses the MarkdownEditor component to edit markdown', () => {
+ expect(findMarkdownEditor().props()).toEqual(
+ expect.objectContaining({
+ value: 'test',
+ renderMarkdownPath: '/',
+ markdownDocsPath: '/',
+ quickActionsDocsPath: expect.any(String),
+ initOnAutofocus: true,
+ supportsQuickActions: true,
+ enableAutocomplete: true,
+ }),
+ );
+ });
+
+ it('triggers update with meta+enter', () => {
+ findMarkdownEditor().vm.$emit('keydown', {
+ type: 'keydown',
+ keyCode: 13,
+ metaKey: true,
+ });
+
+ expect(eventHub.$emit).toHaveBeenCalledWith('update.issuable');
+ });
+
+ it('triggers update with ctrl+enter', () => {
+ findMarkdownEditor().vm.$emit('keydown', {
+ type: 'keydown',
+ keyCode: 13,
+ ctrlKey: true,
+ });
+
+ expect(eventHub.$emit).toHaveBeenCalledWith('update.issuable');
+ });
+
+ it('emits input event when MarkdownEditor emits input event', () => {
+ const markdown = 'markdown';
+
+ findMarkdownEditor().vm.$emit('input', markdown);
+
+ expect(wrapper.emitted('input')).toEqual([[markdown]]);
+ });
+ });
});
diff --git a/spec/frontend/issues/show/components/form_spec.js b/spec/frontend/issues/show/components/form_spec.js
index 5c0fe991b22..aedb974cbd0 100644
--- a/spec/frontend/issues/show/components/form_spec.js
+++ b/spec/frontend/issues/show/components/form_spec.js
@@ -1,14 +1,16 @@
import { GlAlert } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
-import Autosave from '~/autosave';
+import { getDraft, updateDraft, clearDraft, getLockVersion } from '~/lib/utils/autosave';
import DescriptionTemplate from '~/issues/show/components/fields/description_template.vue';
+import IssuableTitleField from '~/issues/show/components/fields/title.vue';
+import DescriptionField from '~/issues/show/components/fields/description.vue';
import IssueTypeField from '~/issues/show/components/fields/type.vue';
import formComponent from '~/issues/show/components/form.vue';
import LockedWarning from '~/issues/show/components/locked_warning.vue';
import eventHub from '~/issues/show/event_hub';
-jest.mock('~/autosave');
+jest.mock('~/lib/utils/autosave');
describe('Inline edit form component', () => {
let wrapper;
@@ -38,9 +40,14 @@ describe('Inline edit form component', () => {
...defaultProps,
...props,
},
+ stubs: {
+ DescriptionField,
+ },
});
};
+ const findTitleField = () => wrapper.findComponent(IssuableTitleField);
+ const findDescriptionField = () => wrapper.findComponent(DescriptionField);
const findDescriptionTemplate = () => wrapper.findComponent(DescriptionTemplate);
const findIssuableTypeField = () => wrapper.findComponent(IssueTypeField);
const findLockedWarning = () => wrapper.findComponent(LockedWarning);
@@ -108,16 +115,34 @@ describe('Inline edit form component', () => {
});
describe('autosave', () => {
- let spy;
-
beforeEach(() => {
- spy = jest.spyOn(Autosave.prototype, 'reset');
+ getDraft.mockImplementation((autosaveKey) => {
+ return autosaveKey[autosaveKey.length - 1];
+ });
});
- it('initialized Autosave on mount', () => {
+ it('initializes title and description fields with saved drafts', () => {
createComponent();
- expect(Autosave).toHaveBeenCalledTimes(2);
+ expect(findTitleField().props().value).toBe('title');
+ expect(findDescriptionField().props().value).toBe('description');
+ });
+
+ it('updates local storage drafts when title and description change', () => {
+ const updatedTitle = 'updated title';
+ const updatedDescription = 'updated description';
+
+ createComponent();
+
+ findTitleField().vm.$emit('input', updatedTitle);
+ findDescriptionField().vm.$emit('input', updatedDescription);
+
+ expect(updateDraft).toHaveBeenCalledWith(expect.any(Array), updatedTitle);
+ expect(updateDraft).toHaveBeenCalledWith(
+ expect.any(Array),
+ updatedDescription,
+ defaultProps.formState.lock_version,
+ );
});
it('calls reset on autosave when eventHub emits appropriate events', () => {
@@ -125,33 +150,60 @@ describe('Inline edit form component', () => {
eventHub.$emit('close.form');
- expect(spy).toHaveBeenCalledTimes(2);
+ expect(clearDraft).toHaveBeenCalledTimes(2);
eventHub.$emit('delete.issuable');
- expect(spy).toHaveBeenCalledTimes(4);
+ expect(clearDraft).toHaveBeenCalledTimes(4);
eventHub.$emit('update.issuable');
- expect(spy).toHaveBeenCalledTimes(6);
+ expect(clearDraft).toHaveBeenCalledTimes(6);
});
describe('outdated description', () => {
+ const clientSideMockVersion = 'lock version from local storage';
+ const serverSideMockVersion = 'lock version from server';
+
+ const mockGetLockVersion = () => getLockVersion.mockResolvedValue(clientSideMockVersion);
+
it('does not show warning if lock version from server is the same as the local lock version', () => {
createComponent();
expect(findAlert().exists()).toBe(false);
});
it('shows warning if lock version from server differs than the local lock version', async () => {
- Autosave.prototype.getSavedLockVersion.mockResolvedValue('lock version from local storage');
+ mockGetLockVersion();
createComponent({
- formState: { ...defaultProps.formState, lock_version: 'lock version from server' },
+ formState: { ...defaultProps.formState, lock_version: serverSideMockVersion },
});
await nextTick();
expect(findAlert().exists()).toBe(true);
});
+
+ describe('when saved draft is discarded', () => {
+ beforeEach(async () => {
+ mockGetLockVersion();
+
+ createComponent({
+ formState: { ...defaultProps.formState, lock_version: serverSideMockVersion },
+ });
+
+ await nextTick();
+
+ findAlert().vm.$emit('secondaryAction');
+ });
+
+ it('hides the warning alert', () => {
+ expect(findAlert().exists()).toBe(false);
+ });
+
+ it('clears the description draft', () => {
+ expect(clearDraft).toHaveBeenCalledWith(expect.any(Array));
+ });
+ });
});
});
});
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 d92aeabba0f..458c1c3f858 100644
--- a/spec/frontend/issues/show/components/incidents/incident_tabs_spec.js
+++ b/spec/frontend/issues/show/components/incidents/incident_tabs_spec.js
@@ -5,7 +5,6 @@ import { trackIncidentDetailsViewsOptions } from '~/incidents/constants';
import DescriptionComponent from '~/issues/show/components/description.vue';
import HighlightBar from '~/issues/show/components/incidents/highlight_bar.vue';
import IncidentTabs from '~/issues/show/components/incidents/incident_tabs.vue';
-import TimelineTab from '~/issues/show/components/incidents/timeline_events_tab.vue';
import INVALID_URL from '~/lib/utils/invalid_url';
import Tracking from '~/tracking';
import AlertDetailsTable from '~/vue_shared/components/alert_details_table.vue';
@@ -38,7 +37,6 @@ describe('Incident Tabs component', () => {
projectId: '',
issuableId: '',
uploadMetricsFeatureAvailable: true,
- glFeatures: { incidentTimeline: true },
},
data() {
return { alert: mockAlert, ...data };
@@ -67,7 +65,6 @@ describe('Incident Tabs component', () => {
const findAlertDetailsComponent = () => wrapper.findComponent(AlertDetailsTable);
const findDescriptionComponent = () => wrapper.findComponent(DescriptionComponent);
const findHighlightBarComponent = () => wrapper.findComponent(HighlightBar);
- const findTimelineTab = () => wrapper.findComponent(TimelineTab);
describe('empty state', () => {
beforeEach(() => {
@@ -128,20 +125,4 @@ describe('Incident Tabs component', () => {
expect(Tracking.event).toHaveBeenCalledWith(category, action);
});
});
-
- describe('incident timeline tab', () => {
- beforeEach(() => {
- mountComponent();
- });
-
- it('renders the timeline tab when feature flag is enabled', () => {
- expect(findTimelineTab().exists()).toBe(true);
- });
-
- it('does not render timeline tab when feature flag is disabled', () => {
- mountComponent({}, { provide: { glFeatures: { incidentTimeline: false } } });
-
- expect(findTimelineTab().exists()).toBe(false);
- });
- });
});
diff --git a/spec/frontend/issues/show/components/incidents/timeline_events_form_spec.js b/spec/frontend/issues/show/components/incidents/timeline_events_form_spec.js
index 7f086a276f7..2e7449974e5 100644
--- a/spec/frontend/issues/show/components/incidents/timeline_events_form_spec.js
+++ b/spec/frontend/issues/show/components/incidents/timeline_events_form_spec.js
@@ -22,12 +22,15 @@ describe('Timeline events form', () => {
useFakeDate(fakeDate);
let wrapper;
- const mountComponent = ({ mountMethod = shallowMountExtended }) => {
+ const mountComponent = ({ mountMethod = shallowMountExtended } = {}) => {
wrapper = mountMethod(TimelineEventsForm, {
propsData: {
showSaveAndAdd: true,
isEventProcessed: false,
},
+ stubs: {
+ GlButton: true,
+ },
});
};
@@ -48,17 +51,18 @@ describe('Timeline events form', () => {
findHourInput().setValue(5);
findMinuteInput().setValue(45);
};
+ const findTextarea = () => wrapper.findByTestId('input-note');
const submitForm = async () => {
- findSubmitButton().trigger('click');
+ findSubmitButton().vm.$emit('click');
await waitForPromises();
};
const submitFormAndAddAnother = async () => {
- findSubmitAndAddButton().trigger('click');
+ findSubmitAndAddButton().vm.$emit('click');
await waitForPromises();
};
const cancelForm = async () => {
- findCancelButton().trigger('click');
+ findCancelButton().vm.$emit('click');
await waitForPromises();
};
@@ -118,5 +122,17 @@ describe('Timeline events form', () => {
expect(findHourInput().element.value).toBe('0');
expect(findMinuteInput().element.value).toBe('0');
});
+
+ it('should disable the save buttons when event content does not exist', async () => {
+ expect(findSubmitButton().props('disabled')).toBe(true);
+ expect(findSubmitAndAddButton().props('disabled')).toBe(true);
+ });
+
+ it('should enable the save buttons when event content exists', async () => {
+ await findTextarea().setValue('hello');
+
+ expect(findSubmitButton().props('disabled')).toBe(false);
+ expect(findSubmitAndAddButton().props('disabled')).toBe(false);
+ });
});
});
diff --git a/spec/frontend/jira_connect/branches/components/new_branch_form_spec.js b/spec/frontend/jira_connect/branches/components/new_branch_form_spec.js
index cc8346253ee..d41031f9eaa 100644
--- a/spec/frontend/jira_connect/branches/components/new_branch_form_spec.js
+++ b/spec/frontend/jira_connect/branches/components/new_branch_form_spec.js
@@ -238,7 +238,7 @@ describe('NewBranchForm', () => {
scenario | mutation | alertTitle | alertText
${'with errors-as-data'} | ${mockCreateBranchMutationWithErrors} | ${CREATE_BRANCH_ERROR_WITH_CONTEXT} | ${mockCreateBranchMutationResponseWithErrors.data.createBranch.errors[0]}
${'top-level error'} | ${mockCreateBranchMutationFailed} | ${''} | ${CREATE_BRANCH_ERROR_GENERIC}
- `('', ({ mutation, alertTitle, alertText }) => {
+ `('given $scenario', ({ mutation, alertTitle, alertText }) => {
beforeEach(async () => {
createComponent({
mockApollo: createMockApolloProvider({
diff --git a/spec/frontend/jira_connect/subscriptions/pkce_spec.js b/spec/frontend/jira_connect/subscriptions/pkce_spec.js
index 4ee88059b7a..671922c36d8 100644
--- a/spec/frontend/jira_connect/subscriptions/pkce_spec.js
+++ b/spec/frontend/jira_connect/subscriptions/pkce_spec.js
@@ -1,11 +1,7 @@
import crypto from 'crypto';
-import { TextEncoder, TextDecoder } from 'util';
import { createCodeVerifier, createCodeChallenge } from '~/jira_connect/subscriptions/pkce';
-global.TextEncoder = TextEncoder;
-global.TextDecoder = TextDecoder;
-
describe('pkce', () => {
beforeAll(() => {
Object.defineProperty(global.self, 'crypto', {
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 8c724a8030b..109cef6f817 100644
--- a/spec/frontend/jobs/components/table/job_table_app_spec.js
+++ b/spec/frontend/jobs/components/table/job_table_app_spec.js
@@ -12,7 +12,7 @@ import { s__ } from '~/locale';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { TEST_HOST } from 'spec/test_constants';
-import createFlash from '~/flash';
+import { createAlert } 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';
@@ -229,7 +229,7 @@ describe('Job table app', () => {
await findFilteredSearch().vm.$emit('filterJobsBySearch', ['raw text']);
- expect(createFlash).toHaveBeenCalledWith(expectedWarning);
+ expect(createAlert).toHaveBeenCalledWith(expectedWarning);
expect(wrapper.vm.$apollo.queries.jobs.refetch).toHaveBeenCalledTimes(0);
});
diff --git a/spec/frontend/labels/components/promote_label_modal_spec.js b/spec/frontend/labels/components/promote_label_modal_spec.js
index 8cfaba6f98a..8953e3cbcd8 100644
--- a/spec/frontend/labels/components/promote_label_modal_spec.js
+++ b/spec/frontend/labels/components/promote_label_modal_spec.js
@@ -1,98 +1,100 @@
-import Vue from 'vue';
+import { shallowMount } from '@vue/test-utils';
+import { GlModal, GlSprintf } from '@gitlab/ui';
+import AxiosMockAdapter from 'axios-mock-adapter';
+
import { TEST_HOST } from 'helpers/test_constants';
-import mountComponent from 'helpers/vue_mount_component_helper';
+import { stubComponent } from 'helpers/stub_component';
+
import axios from '~/lib/utils/axios_utils';
-import promoteLabelModal from '~/labels/components/promote_label_modal.vue';
+import PromoteLabelModal from '~/labels/components/promote_label_modal.vue';
import eventHub from '~/labels/event_hub';
describe('Promote label modal', () => {
- let vm;
- const Component = Vue.extend(promoteLabelModal);
+ let wrapper;
+ let axiosMock;
+
const labelMockData = {
labelTitle: 'Documentation',
- labelColor: '#5cb85c',
- labelTextColor: '#ffffff',
+ labelColor: 'rgb(92, 184, 92)',
+ labelTextColor: 'rgb(255, 255, 255)',
url: `${TEST_HOST}/dummy/promote/labels`,
groupName: 'group',
};
- describe('Modal title and description', () => {
- beforeEach(() => {
- vm = mountComponent(Component, labelMockData);
+ const createComponent = () => {
+ wrapper = shallowMount(PromoteLabelModal, {
+ propsData: labelMockData,
+ stubs: {
+ GlSprintf,
+ GlModal: stubComponent(GlModal, {
+ template: `<div><slot name="modal-title"></slot><slot></slot></div>`,
+ }),
+ },
});
+ };
- afterEach(() => {
- vm.$destroy();
- });
+ beforeEach(() => {
+ axiosMock = new AxiosMockAdapter(axios);
+ createComponent();
+ });
+ afterEach(() => {
+ axiosMock.reset();
+ wrapper.destroy();
+ });
+
+ describe('Modal title and description', () => {
it('contains the proper description', () => {
- expect(vm.text).toContain(
+ expect(wrapper.text()).toContain(
`Promoting ${labelMockData.labelTitle} will make it available for all projects inside ${labelMockData.groupName}`,
);
});
it('contains a label span with the color', () => {
- expect(vm.labelColor).not.toBe(null);
- expect(vm.labelColor).toBe(labelMockData.labelColor);
- expect(vm.labelTitle).toBe(labelMockData.labelTitle);
+ const label = wrapper.find('.modal-title-with-label .label');
+
+ expect(label.element.style.backgroundColor).toBe(labelMockData.labelColor);
+ expect(label.element.style.color).toBe(labelMockData.labelTextColor);
+ expect(label.text()).toBe(labelMockData.labelTitle);
});
});
describe('When requesting a label promotion', () => {
beforeEach(() => {
- vm = mountComponent(Component, {
- ...labelMockData,
- });
jest.spyOn(eventHub, '$emit').mockImplementation(() => {});
});
- afterEach(() => {
- vm.$destroy();
- });
-
- it('redirects when a label is promoted', () => {
+ it('redirects when a label is promoted', async () => {
const responseURL = `${TEST_HOST}/dummy/endpoint`;
- jest.spyOn(axios, 'post').mockImplementation((url) => {
- expect(url).toBe(labelMockData.url);
- expect(eventHub.$emit).toHaveBeenCalledWith(
- 'promoteLabelModal.requestStarted',
- labelMockData.url,
- );
- return Promise.resolve({
- request: {
- responseURL,
- },
- });
- });
+ axiosMock.onPost(labelMockData.url).reply(200, { url: responseURL });
- return vm.onSubmit().then(() => {
- expect(eventHub.$emit).toHaveBeenCalledWith('promoteLabelModal.requestFinished', {
- labelUrl: labelMockData.url,
- successful: true,
- });
+ wrapper.findComponent(GlModal).vm.$emit('primary');
+
+ expect(eventHub.$emit).toHaveBeenCalledWith(
+ 'promoteLabelModal.requestStarted',
+ labelMockData.url,
+ );
+
+ await axios.waitForAll();
+
+ expect(eventHub.$emit).toHaveBeenCalledWith('promoteLabelModal.requestFinished', {
+ labelUrl: labelMockData.url,
+ successful: true,
});
});
- it('displays an error if promoting a label failed', () => {
+ it('displays an error if promoting a label failed', async () => {
const dummyError = new Error('promoting label failed');
dummyError.response = { status: 500 };
+ axiosMock.onPost(labelMockData.url).reply(500, { error: dummyError });
- jest.spyOn(axios, 'post').mockImplementation((url) => {
- expect(url).toBe(labelMockData.url);
- expect(eventHub.$emit).toHaveBeenCalledWith(
- 'promoteLabelModal.requestStarted',
- labelMockData.url,
- );
+ wrapper.findComponent(GlModal).vm.$emit('primary');
- return Promise.reject(dummyError);
- });
+ await axios.waitForAll();
- return vm.onSubmit().catch((error) => {
- expect(error).toBe(dummyError);
- expect(eventHub.$emit).toHaveBeenCalledWith('promoteLabelModal.requestFinished', {
- labelUrl: labelMockData.url,
- successful: false,
- });
+ expect(eventHub.$emit).toHaveBeenCalledWith('promoteLabelModal.requestFinished', {
+ labelUrl: labelMockData.url,
+ successful: false,
});
});
});
diff --git a/spec/frontend/lib/dompurify_spec.js b/spec/frontend/lib/dompurify_spec.js
index 5523cc0606e..412408ce377 100644
--- a/spec/frontend/lib/dompurify_spec.js
+++ b/spec/frontend/lib/dompurify_spec.js
@@ -1,4 +1,4 @@
-import { sanitize } from '~/lib/dompurify';
+import { sanitize, defaultConfig } from '~/lib/dompurify';
// GDK
const rootGon = {
@@ -45,7 +45,7 @@ const invalidProtocolUrls = [
/* eslint-enable no-script-url */
const validProtocolUrls = ['slack://open', 'x-devonthink-item://90909', 'x-devonthink-item:90909'];
-const forbiddenDataAttrs = ['data-remote', 'data-url', 'data-type', 'data-method'];
+const forbiddenDataAttrs = defaultConfig.FORBID_ATTR;
const acceptedDataAttrs = ['data-random', 'data-custom'];
describe('~/lib/dompurify', () => {
diff --git a/spec/frontend/lib/utils/autosave_spec.js b/spec/frontend/lib/utils/autosave_spec.js
index 12e97f6cdec..afb49dd6db4 100644
--- a/spec/frontend/lib/utils/autosave_spec.js
+++ b/spec/frontend/lib/utils/autosave_spec.js
@@ -1,32 +1,42 @@
-import { clearDraft, getDraft, updateDraft } from '~/lib/utils/autosave';
+import { clearDraft, getDraft, updateDraft, getLockVersion } from '~/lib/utils/autosave';
describe('autosave utils', () => {
const autosaveKey = 'dummy-autosave-key';
const text = 'some dummy text';
+ const lockVersion = '2';
+ const normalizedAutosaveKey = `autosave/${autosaveKey}`;
+ const lockVersionKey = `autosave/${autosaveKey}/lockVersion`;
describe('clearDraft', () => {
beforeEach(() => {
- localStorage.setItem(`autosave/${autosaveKey}`, text);
+ localStorage.setItem(normalizedAutosaveKey, text);
+ localStorage.setItem(lockVersionKey, lockVersion);
});
afterEach(() => {
- localStorage.removeItem(`autosave/${autosaveKey}`);
+ localStorage.removeItem(normalizedAutosaveKey);
});
it('removes the draft from localStorage', () => {
clearDraft(autosaveKey);
- expect(localStorage.getItem(`autosave/${autosaveKey}`)).toBe(null);
+ expect(localStorage.getItem(normalizedAutosaveKey)).toBe(null);
+ });
+
+ it('removes the lockVersion from localStorage', () => {
+ clearDraft(autosaveKey);
+
+ expect(localStorage.getItem(lockVersionKey)).toBe(null);
});
});
describe('getDraft', () => {
beforeEach(() => {
- localStorage.setItem(`autosave/${autosaveKey}`, text);
+ localStorage.setItem(normalizedAutosaveKey, text);
});
afterEach(() => {
- localStorage.removeItem(`autosave/${autosaveKey}`);
+ localStorage.removeItem(normalizedAutosaveKey);
});
it('returns the draft from localStorage', () => {
@@ -36,7 +46,7 @@ describe('autosave utils', () => {
});
it('returns null if no entry exists in localStorage', () => {
- localStorage.removeItem(`autosave/${autosaveKey}`);
+ localStorage.removeItem(normalizedAutosaveKey);
const result = getDraft(autosaveKey);
@@ -46,19 +56,44 @@ describe('autosave utils', () => {
describe('updateDraft', () => {
beforeEach(() => {
- localStorage.setItem(`autosave/${autosaveKey}`, text);
+ localStorage.setItem(normalizedAutosaveKey, text);
});
afterEach(() => {
- localStorage.removeItem(`autosave/${autosaveKey}`);
+ localStorage.removeItem(normalizedAutosaveKey);
});
- it('removes the draft from localStorage', () => {
+ it('updates the stored draft', () => {
const newText = 'new text';
updateDraft(autosaveKey, newText);
- expect(localStorage.getItem(`autosave/${autosaveKey}`)).toBe(newText);
+ expect(localStorage.getItem(normalizedAutosaveKey)).toBe(newText);
+ });
+
+ describe('when lockVersion is provided', () => {
+ it('updates the stored lockVersion', () => {
+ const newText = 'new text';
+ const newLockVersion = '2';
+
+ updateDraft(autosaveKey, newText, lockVersion);
+
+ expect(localStorage.getItem(lockVersionKey)).toBe(newLockVersion);
+ });
+ });
+ });
+
+ describe('getLockVersion', () => {
+ beforeEach(() => {
+ localStorage.setItem(lockVersionKey, lockVersion);
+ });
+
+ afterEach(() => {
+ localStorage.removeItem(lockVersionKey);
+ });
+
+ it('returns the lockVersion from localStorage', () => {
+ expect(getLockVersion(autosaveKey)).toBe(lockVersion);
});
});
});
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 018ae12c908..2e0bb6a8dcd 100644
--- a/spec/frontend/lib/utils/datetime/date_format_utility_spec.js
+++ b/spec/frontend/lib/utils/datetime/date_format_utility_spec.js
@@ -145,3 +145,22 @@ describe('durationTimeFormatted', () => {
expect(utils.durationTimeFormatted(duration)).toBe(expectedOutput);
});
});
+
+describe('formatUtcOffset', () => {
+ it.each`
+ offset | expected
+ ${-32400} | ${'- 9'}
+ ${'-12600'} | ${'- 3.5'}
+ ${0} | ${'0'}
+ ${'10800'} | ${'+ 3'}
+ ${19800} | ${'+ 5.5'}
+ ${0} | ${'0'}
+ ${[]} | ${'0'}
+ ${{}} | ${'0'}
+ ${true} | ${'0'}
+ ${null} | ${'0'}
+ ${undefined} | ${'0'}
+ `('returns $expected given $offset', ({ offset, expected }) => {
+ expect(utils.formatUtcOffset(offset)).toEqual(expected);
+ });
+});
diff --git a/spec/frontend/lib/utils/text_markdown_spec.js b/spec/frontend/lib/utils/text_markdown_spec.js
index 8d179baa505..9fbb3d0a660 100644
--- a/spec/frontend/lib/utils/text_markdown_spec.js
+++ b/spec/frontend/lib/utils/text_markdown_spec.js
@@ -4,15 +4,30 @@ import {
keypressNoteText,
compositionStartNoteText,
compositionEndNoteText,
+ updateTextForToolbarBtn,
} from '~/lib/utils/text_markdown';
import '~/lib/utils/jquery_at_who';
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
describe('init markdown', () => {
+ let mdArea;
let textArea;
+ let indentButton;
+ let outdentButton;
beforeAll(() => {
- textArea = document.createElement('textarea');
- document.querySelector('body').appendChild(textArea);
+ setHTMLFixture(
+ `<div class='md-area'>
+ <textarea></textarea>
+ <button data-md-command="indentLines" id="indentButton"></button>
+ <button data-md-command="outdentLines" id="outdentButton"></button>
+ </div>`,
+ );
+ mdArea = document.querySelector('.md-area');
+ textArea = mdArea.querySelector('textarea');
+ indentButton = mdArea.querySelector('#indentButton');
+ outdentButton = mdArea.querySelector('#outdentButton');
+
textArea.focus();
// needed for the underlying insertText to work
@@ -20,7 +35,7 @@ describe('init markdown', () => {
});
afterAll(() => {
- textArea.parentNode.removeChild(textArea);
+ resetHTMLFixture();
});
describe('insertMarkdownText', () => {
@@ -183,6 +198,7 @@ describe('init markdown', () => {
textArea.addEventListener('keydown', keypressNoteText);
textArea.addEventListener('compositionstart', compositionStartNoteText);
textArea.addEventListener('compositionend', compositionEndNoteText);
+ gon.markdown_automatic_lists = true;
});
it.each`
@@ -302,19 +318,22 @@ describe('init markdown', () => {
expect(textArea.value).toEqual(expected);
expect(textArea.selectionStart).toBe(expected.length);
});
- });
- });
- describe('shifting selected lines left or right', () => {
- const indentEvent = new KeyboardEvent('keydown', { key: ']', metaKey: true });
- const outdentEvent = new KeyboardEvent('keydown', { key: '[', metaKey: true });
+ it('does nothing if user preference disabled', () => {
+ const text = '- test';
- beforeEach(() => {
- textArea.addEventListener('keydown', keypressNoteText);
- textArea.addEventListener('compositionstart', compositionStartNoteText);
- textArea.addEventListener('compositionend', compositionEndNoteText);
+ gon.markdown_automatic_lists = false;
+
+ textArea.value = text;
+ textArea.setSelectionRange(text.length, text.length);
+ textArea.dispatchEvent(enterEvent);
+
+ expect(textArea.value).toEqual(text);
+ });
});
+ });
+ describe('shifting selected lines left or right', () => {
it.each`
selectionStart | selectionEnd | expected | expectedSelectionStart | expectedSelectionEnd
${0} | ${0} | ${' 012\n456\n89'} | ${2} | ${2}
@@ -338,7 +357,7 @@ describe('init markdown', () => {
textArea.value = text;
textArea.setSelectionRange(selectionStart, selectionEnd);
- textArea.dispatchEvent(indentEvent);
+ updateTextForToolbarBtn($(indentButton));
expect(textArea.value).toEqual(expected);
expect(textArea.selectionStart).toEqual(expectedSelectionStart);
@@ -350,7 +369,7 @@ describe('init markdown', () => {
textArea.value = '012\n\n89';
textArea.setSelectionRange(4, 4);
- textArea.dispatchEvent(indentEvent);
+ updateTextForToolbarBtn($(indentButton));
expect(textArea.value).toEqual('012\n \n89');
expect(textArea.selectionStart).toEqual(6);
@@ -381,7 +400,7 @@ describe('init markdown', () => {
textArea.value = text;
textArea.setSelectionRange(selectionStart, selectionEnd);
- textArea.dispatchEvent(outdentEvent);
+ updateTextForToolbarBtn($(outdentButton));
expect(textArea.value).toEqual(expected);
expect(textArea.selectionStart).toEqual(expectedSelectionStart);
@@ -393,7 +412,7 @@ describe('init markdown', () => {
textArea.value = '012\n\n89';
textArea.setSelectionRange(4, 4);
- textArea.dispatchEvent(outdentEvent);
+ updateTextForToolbarBtn($(outdentButton));
expect(textArea.value).toEqual('012\n\n89');
expect(textArea.selectionStart).toEqual(4);
diff --git a/spec/frontend/lib/utils/text_utility_spec.js b/spec/frontend/lib/utils/text_utility_spec.js
index 49a160c9f23..f2572ca0ad2 100644
--- a/spec/frontend/lib/utils/text_utility_spec.js
+++ b/spec/frontend/lib/utils/text_utility_spec.js
@@ -386,4 +386,16 @@ describe('text_utility', () => {
expect(textUtils.limitedCounterWithDelimiter(120)).toBe(120);
});
});
+
+ describe('base64EncodeUnicode', () => {
+ it('encodes unicode characters', () => {
+ expect(textUtils.base64EncodeUnicode('😀')).toBe('8J+YgA==');
+ });
+ });
+
+ describe('base64DecodeUnicode', () => {
+ it('decodes unicode characters', () => {
+ expect(textUtils.base64DecodeUnicode('8J+YgA==')).toBe('😀');
+ });
+ });
});
diff --git a/spec/frontend/listbox/index_spec.js b/spec/frontend/listbox/index_spec.js
index 07c6cca535a..fd41531796b 100644
--- a/spec/frontend/listbox/index_spec.js
+++ b/spec/frontend/listbox/index_spec.js
@@ -1,6 +1,6 @@
import { nextTick } from 'vue';
-import { getAllByRole, getByRole } from '@testing-library/dom';
-import { GlDropdown } from '@gitlab/ui';
+import { getAllByRole, getByTestId } from '@testing-library/dom';
+import { GlListbox } from '@gitlab/ui';
import { createWrapper } from '@vue/test-utils';
import { initListbox, parseAttributes } from '~/listbox';
import { getFixture, setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
@@ -28,20 +28,6 @@ describe('initListbox', () => {
instance = initListbox(...args);
};
- // TODO: Rewrite these finders to use better semantics once the
- // implementation is switched to GlListbox
- // https://gitlab.com/gitlab-org/gitlab/-/issues/348738
- const findToggleButton = () => document.body.querySelector('.gl-dropdown-toggle');
- const findItem = (text) => getByRole(document.body, 'menuitem', { name: text });
- const findItems = () => getAllByRole(document.body, 'menuitem');
- const findSelectedItems = () =>
- findItems().filter(
- (menuitem) =>
- !menuitem
- .querySelector('.gl-new-dropdown-item-check-icon')
- .classList.contains('gl-visibility-hidden'),
- );
-
it('returns null given no element', () => {
setup();
@@ -55,6 +41,10 @@ describe('initListbox', () => {
describe('given a valid element', () => {
let onChangeSpy;
+ const listbox = () => createWrapper(instance).findComponent(GlListbox);
+ const findToggleButton = () => getByTestId(document.body, 'base-dropdown-toggle');
+ const findSelectedItems = () => getAllByRole(document.body, 'option', { selected: true });
+
beforeEach(async () => {
setHTMLFixture(fixture);
onChangeSpy = jest.fn();
@@ -85,10 +75,9 @@ describe('initListbox', () => {
expect(instance.$el.classList).toContain('test-class-1', 'test-class-2');
});
- describe.each(parsedAttributes.items)('clicking on an item', (item) => {
+ describe.each(parsedAttributes.items)('selecting an item', (item) => {
beforeEach(async () => {
- findItem(item.text).click();
-
+ listbox().vm.$emit('select', item.value);
await nextTick();
});
@@ -108,8 +97,7 @@ describe('initListbox', () => {
});
it('passes the "right" prop through to the underlying component', () => {
- const wrapper = createWrapper(instance).findComponent(GlDropdown);
- expect(wrapper.props('right')).toBe(parsedAttributes.right);
+ expect(listbox().props('right')).toBe(parsedAttributes.right);
});
});
});
diff --git a/spec/frontend/members/components/filter_sort/sort_dropdown_spec.js b/spec/frontend/members/components/filter_sort/sort_dropdown_spec.js
index 5581fd52458..ef3c8bde3cf 100644
--- a/spec/frontend/members/components/filter_sort/sort_dropdown_spec.js
+++ b/spec/frontend/members/components/filter_sort/sort_dropdown_spec.js
@@ -45,7 +45,7 @@ describe('SortDropdown', () => {
const findSortingComponent = () => wrapper.findComponent(GlSorting);
const findSortDirectionToggle = () =>
- findSortingComponent().find('button[title="Sort direction"]');
+ findSortingComponent().find('button[title^="Sort direction"]');
const findDropdownToggle = () => wrapper.find('button[aria-haspopup="true"]');
const findDropdownItemByText = (text) =>
wrapper
diff --git a/spec/frontend/members/components/table/member_action_buttons_spec.js b/spec/frontend/members/components/table/member_action_buttons_spec.js
index f3f50bf620a..03cfc6ca0f6 100644
--- a/spec/frontend/members/components/table/member_action_buttons_spec.js
+++ b/spec/frontend/members/components/table/member_action_buttons_spec.js
@@ -27,7 +27,7 @@ describe('MemberActionButtons', () => {
wrapper.destroy();
});
- test.each`
+ it.each`
memberType | member | expectedComponent | expectedComponentName
${MEMBER_TYPES.user} | ${memberMock} | ${UserActionButtons} | ${'UserActionButtons'}
${MEMBER_TYPES.group} | ${group} | ${GroupActionButtons} | ${'GroupActionButtons'}
diff --git a/spec/frontend/members/components/table/member_avatar_spec.js b/spec/frontend/members/components/table/member_avatar_spec.js
index 35f82c28fc5..dc5c97f41df 100644
--- a/spec/frontend/members/components/table/member_avatar_spec.js
+++ b/spec/frontend/members/components/table/member_avatar_spec.js
@@ -22,7 +22,7 @@ describe('MemberList', () => {
wrapper.destroy();
});
- test.each`
+ it.each`
memberType | member | expectedComponent | expectedComponentName
${MEMBER_TYPES.user} | ${memberMock} | ${UserAvatar} | ${'UserAvatar'}
${MEMBER_TYPES.group} | ${group} | ${GroupAvatar} | ${'GroupAvatar'}
diff --git a/spec/frontend/members/components/table/members_table_cell_spec.js b/spec/frontend/members/components/table/members_table_cell_spec.js
index fd56699602e..0b0140b0cdb 100644
--- a/spec/frontend/members/components/table/members_table_cell_spec.js
+++ b/spec/frontend/members/components/table/members_table_cell_spec.js
@@ -95,7 +95,7 @@ describe('MembersTableCell', () => {
wrapper = null;
});
- test.each`
+ it.each`
member | expectedMemberType
${memberMock} | ${MEMBER_TYPES.user}
${group} | ${MEMBER_TYPES.group}
diff --git a/spec/frontend/members/utils_spec.js b/spec/frontend/members/utils_spec.js
index 0271483801c..8bef2096a2a 100644
--- a/spec/frontend/members/utils_spec.js
+++ b/spec/frontend/members/utils_spec.js
@@ -89,7 +89,7 @@ describe('Members Utils', () => {
});
describe('isGroup', () => {
- test.each`
+ it.each`
member | expected
${group} | ${true}
${memberMock} | ${false}
@@ -99,7 +99,7 @@ describe('Members Utils', () => {
});
describe('isDirectMember', () => {
- test.each`
+ it.each`
member | expected
${directMember} | ${true}
${inheritedMember} | ${false}
@@ -109,7 +109,7 @@ describe('Members Utils', () => {
});
describe('isCurrentUser', () => {
- test.each`
+ it.each`
currentUserId | expected
${IS_CURRENT_USER_ID} | ${true}
${IS_NOT_CURRENT_USER_ID} | ${false}
@@ -119,7 +119,7 @@ describe('Members Utils', () => {
});
describe('canRemove', () => {
- test.each`
+ it.each`
member | expected
${{ ...directMember, canRemove: true }} | ${true}
${{ ...inheritedMember, canRemove: true }} | ${false}
@@ -130,7 +130,7 @@ describe('Members Utils', () => {
});
describe('canResend', () => {
- test.each`
+ it.each`
member | expected
${invite} | ${true}
${{ ...invite, invite: { ...invite.invite, canResend: false } }} | ${false}
@@ -140,7 +140,7 @@ describe('Members Utils', () => {
});
describe('canUpdate', () => {
- test.each`
+ it.each`
member | currentUserId | expected
${{ ...directMember, canUpdate: true }} | ${IS_NOT_CURRENT_USER_ID} | ${true}
${{ ...directMember, canUpdate: true }} | ${IS_CURRENT_USER_ID} | ${false}
diff --git a/spec/frontend/merge_conflicts/store/actions_spec.js b/spec/frontend/merge_conflicts/store/actions_spec.js
index e73769cba51..50eac982e20 100644
--- a/spec/frontend/merge_conflicts/store/actions_spec.js
+++ b/spec/frontend/merge_conflicts/store/actions_spec.js
@@ -3,7 +3,7 @@ import MockAdapter from 'axios-mock-adapter';
import Cookies from '~/lib/utils/cookies';
import { useMockLocationHelper } from 'helpers/mock_window_location_helper';
import testAction from 'helpers/vuex_action_helper';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { INTERACTIVE_RESOLVE_MODE, EDIT_RESOLVE_MODE } from '~/merge_conflicts/constants';
import * as actions from '~/merge_conflicts/store/actions';
import * as types from '~/merge_conflicts/store/mutation_types';
@@ -125,7 +125,7 @@ describe('merge conflicts actions', () => {
],
[],
);
- expect(createFlash).toHaveBeenCalledWith({
+ expect(createAlert).toHaveBeenCalledWith({
message: 'Failed to save merge conflicts resolutions. Please try again!',
});
});
diff --git a/spec/frontend/merge_request_spec.js b/spec/frontend/merge_request_spec.js
index bcf64204c7a..16e3e49a297 100644
--- a/spec/frontend/merge_request_spec.js
+++ b/spec/frontend/merge_request_spec.js
@@ -3,9 +3,12 @@ import $ from 'jquery';
import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import { TEST_HOST } from 'spec/test_constants';
import waitForPromises from 'helpers/wait_for_promises';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import MergeRequest from '~/merge_request';
+jest.mock('~/flash');
+
describe('MergeRequest', () => {
const test = {};
describe('task lists', () => {
@@ -95,8 +98,11 @@ describe('MergeRequest', () => {
await waitForPromises();
- expect(document.querySelector('.flash-container .flash-text').innerText.trim()).toBe(
- 'Someone edited this merge request at the same time you did. Please refresh the page to see changes.',
+ expect(createAlert).toHaveBeenCalledWith(
+ expect.objectContaining({
+ message:
+ 'Someone edited this merge request at the same time you did. Please refresh the page to see changes.',
+ }),
);
});
});
diff --git a/spec/frontend/milestones/components/promote_milestone_modal_spec.js b/spec/frontend/milestones/components/promote_milestone_modal_spec.js
index 11eaa92f2b0..60657fbc9b8 100644
--- a/spec/frontend/milestones/components/promote_milestone_modal_spec.js
+++ b/spec/frontend/milestones/components/promote_milestone_modal_spec.js
@@ -3,7 +3,7 @@ import { shallowMount } from '@vue/test-utils';
import { setHTMLFixture } from 'helpers/fixtures';
import { TEST_HOST } from 'helpers/test_constants';
import waitForPromises from 'helpers/wait_for_promises';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import * as urlUtils from '~/lib/utils/url_utility';
import PromoteMilestoneModal from '~/milestones/components/promote_milestone_modal.vue';
@@ -103,7 +103,7 @@ describe('Promote milestone modal', () => {
wrapper.findComponent(GlModal).vm.$emit('primary');
await waitForPromises();
- expect(createFlash).toHaveBeenCalledWith({ message: dummyError });
+ expect(createAlert).toHaveBeenCalledWith({ message: dummyError });
});
});
});
diff --git a/spec/frontend/monitoring/components/dashboard_spec.js b/spec/frontend/monitoring/components/dashboard_spec.js
index 1de6b6e3e98..1d17a9116df 100644
--- a/spec/frontend/monitoring/components/dashboard_spec.js
+++ b/spec/frontend/monitoring/components/dashboard_spec.js
@@ -4,7 +4,7 @@ import { nextTick } from 'vue';
import setWindowLocation from 'helpers/set_window_location_helper';
import { TEST_HOST } from 'helpers/test_constants';
import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { ESC_KEY } from '~/lib/utils/keys';
import { objectToQuery } from '~/lib/utils/url_utility';
@@ -198,7 +198,7 @@ describe('Dashboard', () => {
);
await nextTick();
- expect(createFlash).toHaveBeenCalled();
+ expect(createAlert).toHaveBeenCalled();
});
it('does not display a warning if there are no validation warnings', async () => {
@@ -210,7 +210,7 @@ describe('Dashboard', () => {
);
await nextTick();
- expect(createFlash).not.toHaveBeenCalled();
+ expect(createAlert).not.toHaveBeenCalled();
});
});
@@ -275,7 +275,7 @@ describe('Dashboard', () => {
setupStoreWithData(store);
await nextTick();
- expect(createFlash).toHaveBeenCalled();
+ expect(createAlert).toHaveBeenCalled();
expect(store.dispatch).not.toHaveBeenCalledWith(
'monitoringDashboard/setExpandedPanel',
expect.anything(),
diff --git a/spec/frontend/monitoring/components/dashboard_url_time_spec.js b/spec/frontend/monitoring/components/dashboard_url_time_spec.js
index a327e234581..9873654bdda 100644
--- a/spec/frontend/monitoring/components/dashboard_url_time_spec.js
+++ b/spec/frontend/monitoring/components/dashboard_url_time_spec.js
@@ -1,7 +1,7 @@
import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import { nextTick } from 'vue';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import {
queryToObject,
@@ -115,7 +115,7 @@ describe('dashboard invalid url parameters', () => {
createMountedWrapper();
await nextTick();
- expect(createFlash).toHaveBeenCalled();
+ expect(createAlert).toHaveBeenCalled();
expect(findDateTimePicker().props('value')).toEqual(defaultTimeRange);
diff --git a/spec/frontend/monitoring/requests/index_spec.js b/spec/frontend/monitoring/requests/index_spec.js
index 03bf5d70153..6f9af911a9f 100644
--- a/spec/frontend/monitoring/requests/index_spec.js
+++ b/spec/frontend/monitoring/requests/index_spec.js
@@ -129,7 +129,7 @@ describe('monitoring metrics_requests', () => {
});
});
- test.each`
+ it.each`
code | reason
${statusCodes.BAD_REQUEST} | ${'Parameters are missing or incorrect'}
${statusCodes.UNPROCESSABLE_ENTITY} | ${"Expression can't be executed"}
diff --git a/spec/frontend/monitoring/store/actions_spec.js b/spec/frontend/monitoring/store/actions_spec.js
index a872a7780eb..ca66768c3cc 100644
--- a/spec/frontend/monitoring/store/actions_spec.js
+++ b/spec/frontend/monitoring/store/actions_spec.js
@@ -1,7 +1,7 @@
import MockAdapter from 'axios-mock-adapter';
import { backoffMockImplementation } from 'helpers/backoff_helper';
import testAction from 'helpers/vuex_action_helper';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import * as commonUtils from '~/lib/utils/common_utils';
import statusCodes from '~/lib/utils/http_status';
@@ -82,7 +82,7 @@ describe('Monitoring store actions', () => {
mock.reset();
commonUtils.backOff.mockReset();
- createFlash.mockReset();
+ createAlert.mockReset();
});
// Setup
@@ -241,7 +241,7 @@ describe('Monitoring store actions', () => {
'receiveMetricsDashboardFailure',
new Error('Request failed with status code 500'),
);
- expect(createFlash).toHaveBeenCalled();
+ expect(createAlert).toHaveBeenCalled();
});
it('dispatches a failure action when a message is returned', async () => {
@@ -250,7 +250,7 @@ describe('Monitoring store actions', () => {
'receiveMetricsDashboardFailure',
new Error('Request failed with status code 500'),
);
- expect(createFlash).toHaveBeenCalledWith({
+ expect(createAlert).toHaveBeenCalledWith({
message: expect.stringContaining(mockDashboardsErrorResponse.message),
});
});
@@ -263,7 +263,7 @@ describe('Monitoring store actions', () => {
'receiveMetricsDashboardFailure',
new Error('Request failed with status code 500'),
);
- expect(createFlash).not.toHaveBeenCalled();
+ expect(createAlert).not.toHaveBeenCalled();
});
});
});
@@ -328,7 +328,7 @@ describe('Monitoring store actions', () => {
},
});
- expect(createFlash).not.toHaveBeenCalled();
+ expect(createAlert).not.toHaveBeenCalled();
});
it('dispatches fetchPrometheusMetric for each panel query', async () => {
@@ -385,7 +385,7 @@ describe('Monitoring store actions', () => {
defaultQueryParams,
});
- expect(createFlash).toHaveBeenCalledTimes(1);
+ expect(createAlert).toHaveBeenCalledTimes(1);
});
});
@@ -570,7 +570,7 @@ describe('Monitoring store actions', () => {
[],
[{ type: 'receiveDeploymentsDataFailure' }],
() => {
- expect(createFlash).toHaveBeenCalled();
+ expect(createAlert).toHaveBeenCalled();
},
);
});
@@ -1084,8 +1084,8 @@ describe('Monitoring store actions', () => {
return testAction(fetchVariableMetricLabelValues, { defaultQueryParams }, state, [], []).then(
() => {
- expect(createFlash).toHaveBeenCalledTimes(1);
- expect(createFlash).toHaveBeenCalledWith({
+ expect(createAlert).toHaveBeenCalledTimes(1);
+ expect(createAlert).toHaveBeenCalledWith({
message: expect.stringContaining('error getting options for variable "label1"'),
});
},
diff --git a/spec/frontend/monitoring/utils_spec.js b/spec/frontend/monitoring/utils_spec.js
index 31975052077..6c6c3d6b90f 100644
--- a/spec/frontend/monitoring/utils_spec.js
+++ b/spec/frontend/monitoring/utils_spec.js
@@ -290,7 +290,7 @@ describe('monitoring/utils', () => {
expect(() => expandedPanelPayloadFromUrl(metricsDashboardViewModel, search)).toThrow();
});
- test.each`
+ it.each`
group | title | yLabel | missingField
${'NOT_A_GROUP'} | ${title} | ${yLabel} | ${'group'}
${group} | ${'NOT_A_TITLE'} | ${yLabel} | ${'title'}
@@ -367,7 +367,7 @@ describe('monitoring/utils', () => {
],
};
- [
+ it.each([
{
input: { metrics: undefined },
output: {},
@@ -393,12 +393,10 @@ describe('monitoring/utils', () => {
output: multipleMetricExpected,
testCase: 'barChartsDataParser returns multiple series object with multiple metrics',
},
- ].forEach(({ input, output, testCase }) => {
- it(testCase, () => {
- expect(monitoringUtils.barChartsDataParser(input.metrics)).toEqual(
- expect.objectContaining(output),
- );
- });
+ ])('$testCase', ({ input, output }) => {
+ expect(monitoringUtils.barChartsDataParser(input.metrics)).toEqual(
+ expect.objectContaining(output),
+ );
});
});
diff --git a/spec/frontend/nav/components/top_nav_app_spec.js b/spec/frontend/nav/components/top_nav_app_spec.js
index 745707c1d28..b32ab5ebe09 100644
--- a/spec/frontend/nav/components/top_nav_app_spec.js
+++ b/spec/frontend/nav/components/top_nav_app_spec.js
@@ -1,5 +1,6 @@
import { GlNavItemDropdown } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
+import { mount, shallowMount } from '@vue/test-utils';
+import { mockTracking } from 'helpers/tracking_helper';
import TopNavApp from '~/nav/components/top_nav_app.vue';
import TopNavDropdownMenu from '~/nav/components/top_nav_dropdown_menu.vue';
import { TEST_NAV_DATA } from '../mock_data';
@@ -8,6 +9,14 @@ describe('~/nav/components/top_nav_app.vue', () => {
let wrapper;
const createComponent = () => {
+ wrapper = mount(TopNavApp, {
+ propsData: {
+ navData: TEST_NAV_DATA,
+ },
+ });
+ };
+
+ const createComponentShallow = () => {
wrapper = shallowMount(TopNavApp, {
propsData: {
navData: TEST_NAV_DATA,
@@ -16,6 +25,7 @@ describe('~/nav/components/top_nav_app.vue', () => {
};
const findNavItemDropdown = () => wrapper.findComponent(GlNavItemDropdown);
+ const findNavItemDropdowToggle = () => findNavItemDropdown().find('.js-top-nav-dropdown-toggle');
const findMenu = () => wrapper.findComponent(TopNavDropdownMenu);
afterEach(() => {
@@ -24,7 +34,7 @@ describe('~/nav/components/top_nav_app.vue', () => {
describe('default', () => {
beforeEach(() => {
- createComponent();
+ createComponentShallow();
});
it('renders nav item dropdown', () => {
@@ -45,4 +55,18 @@ describe('~/nav/components/top_nav_app.vue', () => {
});
});
});
+
+ describe('tracking', () => {
+ it('emits a tracking event when the toggle is clicked', () => {
+ const trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
+ createComponent();
+
+ findNavItemDropdowToggle().trigger('click');
+
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_nav', {
+ label: 'hamburger_menu',
+ property: 'top_navigation',
+ });
+ });
+ });
});
diff --git a/spec/frontend/notebook/cells/output/index_spec.js b/spec/frontend/notebook/cells/output/index_spec.js
index 97a7e22be60..8bf049235a9 100644
--- a/spec/frontend/notebook/cells/output/index_spec.js
+++ b/spec/frontend/notebook/cells/output/index_spec.js
@@ -53,6 +53,7 @@ describe('Output component', () => {
expect(iframe.exists()).toBe(true);
expect(iframe.element.getAttribute('sandbox')).toBe('');
expect(iframe.element.getAttribute('srcdoc')).toBe('<p>test</p>');
+ expect(iframe.element.getAttribute('scrolling')).toBe('auto');
});
it('renders multiple raw HTML outputs', () => {
diff --git a/spec/frontend/notes/components/__snapshots__/notes_app_spec.js.snap b/spec/frontend/notes/components/__snapshots__/notes_app_spec.js.snap
index 5f4b3e04a79..bc29903d4bf 100644
--- a/spec/frontend/notes/components/__snapshots__/notes_app_spec.js.snap
+++ b/spec/frontend/notes/components/__snapshots__/notes_app_spec.js.snap
@@ -3,15 +3,15 @@
exports[`note_app when sort direction is asc shows skeleton notes after the loaded discussions 1`] = `
"<ul id=\\"notes-list\\" class=\\"notes main-notes-list timeline\\">
<noteable-discussion-stub discussion=\\"[object Object]\\" renderdifffile=\\"true\\" helppagepath=\\"\\" isoverviewtab=\\"true\\"></noteable-discussion-stub>
- <skeleton-loading-container-stub></skeleton-loading-container-stub>
- <discussion-filter-note-stub style=\\"display: none;\\"></discussion-filter-note-stub>
+ <skeleton-loading-container-stub class=\\"note-skeleton\\"></skeleton-loading-container-stub>
+ <!---->
</ul>"
`;
exports[`note_app when sort direction is desc shows skeleton notes before the loaded discussions 1`] = `
"<ul id=\\"notes-list\\" class=\\"notes main-notes-list timeline\\">
- <skeleton-loading-container-stub></skeleton-loading-container-stub>
+ <skeleton-loading-container-stub class=\\"note-skeleton\\"></skeleton-loading-container-stub>
<noteable-discussion-stub discussion=\\"[object Object]\\" renderdifffile=\\"true\\" helppagepath=\\"\\" isoverviewtab=\\"true\\"></noteable-discussion-stub>
- <discussion-filter-note-stub style=\\"display: none;\\"></discussion-filter-note-stub>
+ <!---->
</ul>"
`;
diff --git a/spec/frontend/notes/components/comment_form_spec.js b/spec/frontend/notes/components/comment_form_spec.js
index 55e4ef42e37..701ff492702 100644
--- a/spec/frontend/notes/components/comment_form_spec.js
+++ b/spec/frontend/notes/components/comment_form_spec.js
@@ -7,7 +7,7 @@ import Vuex from 'vuex';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import batchComments from '~/batch_comments/stores/modules/batch_comments';
import { refreshUserMergeRequestCounts } from '~/commons/nav/user_merge_requests';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import CommentForm from '~/notes/components/comment_form.vue';
import CommentTypeDropdown from '~/notes/components/comment_type_dropdown.vue';
@@ -71,11 +71,19 @@ describe('issue_comment_form component', () => {
};
const notableDataMockCanUpdateIssuable = createNotableDataMock({
- current_user: { can_update: true, can_create_note: true },
+ current_user: { can_update: true, can_create_note: true, can_create_confidential_note: true },
});
const notableDataMockCannotUpdateIssuable = createNotableDataMock({
- current_user: { can_update: false, can_create_note: true },
+ current_user: {
+ can_update: false,
+ can_create_note: false,
+ can_create_confidential_note: false,
+ },
+ });
+
+ const notableDataMockCannotCreateConfidentialNote = createNotableDataMock({
+ current_user: { can_update: false, can_create_note: true, can_create_confidential_note: false },
});
const mountComponent = ({
@@ -490,7 +498,7 @@ describe('issue_comment_form component', () => {
await nextTick();
await nextTick();
- expect(createFlash).toHaveBeenCalledWith({
+ expect(createAlert).toHaveBeenCalledWith({
message: `Something went wrong while closing the ${type}. Please try again later.`,
});
});
@@ -526,7 +534,7 @@ describe('issue_comment_form component', () => {
await nextTick();
await nextTick();
- expect(createFlash).toHaveBeenCalledWith({
+ expect(createAlert).toHaveBeenCalledWith({
message: `Something went wrong while reopening the ${type}. Please try again later.`,
});
});
@@ -562,6 +570,17 @@ describe('issue_comment_form component', () => {
expect(checkbox.element.checked).toBe(false);
});
+ it('should not render checkbox if user is not at least a reporter', () => {
+ mountComponent({
+ mountFunction: mount,
+ initialData: { note: 'confidential note' },
+ noteableData: { ...notableDataMockCannotCreateConfidentialNote },
+ });
+
+ const checkbox = findConfidentialNoteCheckbox();
+ expect(checkbox.exists()).toBe(false);
+ });
+
it.each`
noteableType | rendered | message
${'Issue'} | ${true} | ${'render'}
diff --git a/spec/frontend/notes/components/diff_discussion_header_spec.js b/spec/frontend/notes/components/diff_discussion_header_spec.js
index 5800f68b114..bb44563b87a 100644
--- a/spec/frontend/notes/components/diff_discussion_header_spec.js
+++ b/spec/frontend/notes/components/diff_discussion_header_spec.js
@@ -42,7 +42,7 @@ describe('diff_discussion_header component', () => {
expect(props).toMatchObject({
src: firstNoteAuthor.avatar_url,
alt: firstNoteAuthor.name,
- size: { default: 24, md: 32 },
+ size: 32,
});
});
});
diff --git a/spec/frontend/notes/components/discussion_actions_spec.js b/spec/frontend/notes/components/discussion_actions_spec.js
index d16c13d6fd3..e414ada1854 100644
--- a/spec/frontend/notes/components/discussion_actions_spec.js
+++ b/spec/frontend/notes/components/discussion_actions_spec.js
@@ -81,7 +81,7 @@ describe('DiscussionActions', () => {
});
});
- it(shouldRender ? 'renders resolve buttons' : 'does not render resolve buttons', () => {
+ it(`${shouldRender ? 'renders' : 'does not render'} resolve buttons`, () => {
expect(wrapper.findComponent(ResolveDiscussionButton).exists()).toBe(shouldRender);
expect(wrapper.findComponent(ResolveWithIssueButton).exists()).toBe(shouldRender);
});
diff --git a/spec/frontend/notes/components/note_header_spec.js b/spec/frontend/notes/components/note_header_spec.js
index 76177229cff..b870cda2a24 100644
--- a/spec/frontend/notes/components/note_header_spec.js
+++ b/spec/frontend/notes/components/note_header_spec.js
@@ -1,10 +1,7 @@
-import { GlSprintf } from '@gitlab/ui';
import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import NoteHeader from '~/notes/components/note_header.vue';
-import { AVAILABILITY_STATUS } from '~/set_status_modal/constants';
-import UserNameWithStatus from '~/sidebar/components/assignees/user_name_with_status.vue';
Vue.use(Vuex);
@@ -23,7 +20,6 @@ describe('NoteHeader component', () => {
const findTimestamp = () => wrapper.findComponent({ ref: 'noteTimestamp' });
const findInternalNoteIndicator = () => wrapper.findByTestId('internalNoteIndicator');
const findSpinner = () => wrapper.findComponent({ ref: 'spinner' });
- const findAuthorStatus = () => wrapper.findComponent({ ref: 'authorStatus' });
const statusHtml =
'"<span class="user-status-emoji has-tooltip" title="foo bar" data-html="true" data-placement="top"><gl-emoji title="basketball and hoop" data-name="basketball" data-unicode-version="6.0">🏀</gl-emoji></span>"';
@@ -37,22 +33,14 @@ describe('NoteHeader component', () => {
username: 'root',
show_status: true,
status_tooltip_html: statusHtml,
- availability: '',
};
- const createComponent = (props, userAttributes = false) => {
+ const createComponent = (props) => {
wrapper = shallowMountExtended(NoteHeader, {
store: new Vuex.Store({
actions,
}),
propsData: { ...props },
- stubs: { GlSprintf, UserNameWithStatus },
- provide: {
- glFeatures: {
- removeUserAttributesProjects: userAttributes,
- removeUserAttributesGroups: userAttributes,
- },
- },
});
};
@@ -61,26 +49,6 @@ describe('NoteHeader component', () => {
wrapper = null;
});
- describe('when removeUserAttributesProjects feature flag is enabled', () => {
- it('does not render busy status', () => {
- createComponent({ author: { ...author, availability: AVAILABILITY_STATUS.BUSY } }, true);
-
- expect(wrapper.find('.note-header-info').text()).not.toContain('(Busy)');
- });
-
- it('does not render author status', () => {
- createComponent({ author }, true);
-
- expect(findAuthorStatus().exists()).toBe(false);
- });
-
- it('does not render username', () => {
- createComponent({ author }, true);
-
- expect(wrapper.find('.note-header-info').text()).not.toContain('@');
- });
- });
-
it('does not render discussion actions when includeToggle is false', () => {
createComponent({
includeToggle: false,
@@ -145,39 +113,6 @@ describe('NoteHeader component', () => {
expect(wrapper.find('.js-user-link').exists()).toBe(true);
});
-
- it('renders busy status if author availability is set', () => {
- createComponent({ author: { ...author, availability: AVAILABILITY_STATUS.BUSY } });
-
- expect(wrapper.find('.js-user-link').text()).toContain('(Busy)');
- });
-
- it('renders author status', () => {
- createComponent({ author });
-
- expect(findAuthorStatus().exists()).toBe(true);
- });
-
- it('does not render author status if show_status=false', () => {
- createComponent({
- author: { ...author, status: { availability: AVAILABILITY_STATUS.BUSY }, show_status: false },
- });
-
- expect(findAuthorStatus().exists()).toBe(false);
- });
-
- it('does not render author status if status_tooltip_html=null', () => {
- createComponent({
- author: {
- ...author,
- status: { availability: AVAILABILITY_STATUS.BUSY },
- status_tooltip_html: null,
- },
- });
-
- expect(findAuthorStatus().exists()).toBe(false);
- });
-
it('renders deleted user text if author is not passed as a prop', () => {
createComponent();
@@ -270,24 +205,6 @@ describe('NoteHeader component', () => {
});
});
- describe('when author status tooltip is opened', () => {
- it('removes `title` attribute from emoji to prevent duplicate tooltips', () => {
- createComponent({
- author: {
- ...author,
- status_tooltip_html: statusHtml,
- },
- });
-
- return nextTick().then(() => {
- const authorStatus = findAuthorStatus();
- authorStatus.trigger('mouseenter');
-
- expect(authorStatus.find('gl-emoji').attributes('title')).toBeUndefined();
- });
- });
- });
-
describe('when author username link is hovered', () => {
it('toggles hover specific CSS classes on author name link', async () => {
createComponent({ author });
@@ -327,4 +244,18 @@ describe('NoteHeader component', () => {
);
});
});
+
+ it('does render username', () => {
+ createComponent({ author }, true);
+
+ expect(wrapper.find('.note-header-info').text()).toContain('@');
+ });
+
+ describe('with system note', () => {
+ it('does not render username', () => {
+ createComponent({ author, isSystemNote: true }, true);
+
+ expect(wrapper.find('.note-header-info').text()).not.toContain('@');
+ });
+ });
});
diff --git a/spec/frontend/notes/components/noteable_note_spec.js b/spec/frontend/notes/components/noteable_note_spec.js
index b044d40cbe4..3d7195752d3 100644
--- a/spec/frontend/notes/components/noteable_note_spec.js
+++ b/spec/frontend/notes/components/noteable_note_spec.js
@@ -214,7 +214,7 @@ describe('issue_note', () => {
expect(avatarProps.src).toBe(author.avatar_url);
expect(avatarProps.entityName).toBe(author.username);
expect(avatarProps.alt).toBe(author.name);
- expect(avatarProps.size).toEqual({ default: 24, md: 32 });
+ expect(avatarProps.size).toEqual(32);
});
it('should render note header content', () => {
diff --git a/spec/frontend/notes/components/notes_activity_header_spec.js b/spec/frontend/notes/components/notes_activity_header_spec.js
new file mode 100644
index 00000000000..5b3165bf401
--- /dev/null
+++ b/spec/frontend/notes/components/notes_activity_header_spec.js
@@ -0,0 +1,67 @@
+import { shallowMount } from '@vue/test-utils';
+import { __ } from '~/locale';
+import NotesActivityHeader from '~/notes/components/notes_activity_header.vue';
+import DiscussionFilter from '~/notes/components/discussion_filter.vue';
+import TimelineToggle from '~/notes/components/timeline_toggle.vue';
+import createStore from '~/notes/stores';
+import waitForPromises from 'helpers/wait_for_promises';
+import { notesFilters } from '../mock_data';
+
+describe('~/notes/components/notes_activity_header.vue', () => {
+ let wrapper;
+
+ const findTitle = () => wrapper.find('h2');
+
+ const createComponent = ({ props = {}, ...options } = {}) => {
+ wrapper = shallowMount(NotesActivityHeader, {
+ propsData: {
+ notesFilters,
+ ...props,
+ },
+ // why: Rendering async timeline toggle requires store
+ store: createStore(),
+ ...options,
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('default', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders title', () => {
+ expect(findTitle().text()).toBe(__('Activity'));
+ });
+
+ it('renders discussion filter', () => {
+ expect(wrapper.findComponent(DiscussionFilter).props()).toEqual({
+ filters: notesFilters,
+ selectedValue: 0,
+ });
+ });
+
+ it('does not render timeline toggle', () => {
+ expect(wrapper.findComponent(TimelineToggle).exists()).toBe(false);
+ });
+ });
+
+ it('with notesFilterValue prop, passes to discussion filter', () => {
+ createComponent({ props: { notesFilterValue: 1 } });
+
+ expect(wrapper.findComponent(DiscussionFilter).props('selectedValue')).toBe(1);
+ });
+
+ it('with showTimelineViewToggle injected, renders timeline toggle asynchronously', async () => {
+ createComponent({ provide: { showTimelineViewToggle: () => true } });
+
+ expect(wrapper.findComponent(TimelineToggle).exists()).toBe(false);
+
+ await waitForPromises();
+
+ expect(wrapper.findComponent(TimelineToggle).exists()).toBe(true);
+ });
+});
diff --git a/spec/frontend/notes/components/notes_app_spec.js b/spec/frontend/notes/components/notes_app_spec.js
index d4cb07d97dc..9051fcab97f 100644
--- a/spec/frontend/notes/components/notes_app_spec.js
+++ b/spec/frontend/notes/components/notes_app_spec.js
@@ -11,6 +11,7 @@ import axios from '~/lib/utils/axios_utils';
import * as urlUtility from '~/lib/utils/url_utility';
import CommentForm from '~/notes/components/comment_form.vue';
import NotesApp from '~/notes/components/notes_app.vue';
+import NotesActivityHeader from '~/notes/components/notes_activity_header.vue';
import * as constants from '~/notes/constants';
import createStore from '~/notes/stores';
import '~/behaviors/markdown/render_gfm';
@@ -20,11 +21,14 @@ import * as mockData from '../mock_data';
const TYPE_COMMENT_FORM = 'comment-form';
const TYPE_NOTES_LIST = 'notes-list';
+const TEST_NOTES_FILTER_VALUE = 1;
const propsData = {
noteableData: mockData.noteableDataMock,
notesData: mockData.notesDataMock,
userData: mockData.userDataMock,
+ notesFilters: mockData.notesFilters,
+ notesFilterValue: TEST_NOTES_FILTER_VALUE,
};
describe('note_app', () => {
@@ -47,7 +51,7 @@ describe('note_app', () => {
axiosMock = new AxiosMockAdapter(axios);
store = createStore();
- mountComponent = () => {
+ mountComponent = ({ props = {} } = {}) => {
return mount(
{
components: {
@@ -58,7 +62,10 @@ describe('note_app', () => {
</div>`,
},
{
- propsData,
+ propsData: {
+ ...propsData,
+ ...props,
+ },
store,
},
);
@@ -144,6 +151,13 @@ describe('note_app', () => {
it('updates discussions badge', () => {
expect(document.querySelector('.js-discussions-count').textContent).toEqual('2');
});
+
+ it('should render notes activity header', () => {
+ expect(wrapper.findComponent(NotesActivityHeader).props()).toEqual({
+ notesFilterValue: TEST_NOTES_FILTER_VALUE,
+ notesFilters: mockData.notesFilters,
+ });
+ });
});
describe('render with comments disabled', () => {
@@ -151,8 +165,15 @@ describe('note_app', () => {
setHTMLFixture('<div class="js-discussions-count"></div>');
axiosMock.onAny().reply(mockData.getIndividualNoteResponse);
- store.state.commentsDisabled = true;
- wrapper = mountComponent();
+ wrapper = mountComponent({
+ // why: In this integration test, previously we manually set store.state.commentsDisabled
+ // This stopped working when we added `<discussion-filter>` into the component tree.
+ // Let's lean into the integration scope and use a prop that "disables comments".
+ props: {
+ notesFilterValue: constants.HISTORY_ONLY_FILTER_VALUE,
+ },
+ });
+
return waitForPromises();
});
@@ -358,7 +379,7 @@ describe('note_app', () => {
it('should listen hashchange event', () => {
const notesApp = wrapper.findComponent(NotesApp);
const hash = 'some dummy hash';
- jest.spyOn(urlUtility, 'getLocationHash').mockReturnValueOnce(hash);
+ jest.spyOn(urlUtility, 'getLocationHash').mockReturnValue(hash);
const setTargetNoteHash = jest.spyOn(notesApp.vm, 'setTargetNoteHash');
window.dispatchEvent(new Event('hashchange'), hash);
diff --git a/spec/frontend/notes/mixins/discussion_navigation_spec.js b/spec/frontend/notes/mixins/discussion_navigation_spec.js
index 1b4e8026d84..45625d0a23f 100644
--- a/spec/frontend/notes/mixins/discussion_navigation_spec.js
+++ b/spec/frontend/notes/mixins/discussion_navigation_spec.js
@@ -4,7 +4,6 @@ import Vuex from 'vuex';
import { setHTMLFixture } from 'helpers/fixtures';
import createEventHub from '~/helpers/event_hub_factory';
import * as utils from '~/lib/utils/common_utils';
-import eventHub from '~/notes/event_hub';
import discussionNavigation from '~/notes/mixins/discussion_navigation';
import notesModule from '~/notes/stores/modules';
@@ -35,13 +34,15 @@ describe('Discussion navigation mixin', () => {
beforeEach(() => {
setHTMLFixture(
- [...'abcde']
+ `<div class="notes">
+ ${[...'abcde']
.map(
(id) =>
`<ul class="notes" data-discussion-id="${id}"></ul>
<div class="discussion" data-discussion-id="${id}"></div>`,
)
- .join(''),
+ .join('')}
+ </div>`,
);
jest.spyOn(utils, 'scrollToElementWithContext');
@@ -58,7 +59,7 @@ describe('Discussion navigation mixin', () => {
},
diffs: {
namespaced: true,
- actions: { scrollToFile },
+ actions: { scrollToFile, disableVirtualScroller: () => {} },
state: { diffFiles: [] },
},
},
@@ -73,9 +74,6 @@ describe('Discussion navigation mixin', () => {
jest.clearAllMocks();
});
- const findDiscussion = (selector, id) =>
- document.querySelector(`${selector}[data-discussion-id="${id}"]`);
-
describe('jumpToFirstUnresolvedDiscussion method', () => {
let vm;
@@ -110,14 +108,14 @@ describe('Discussion navigation mixin', () => {
});
describe.each`
- fn | args | currentId | expected
- ${'jumpToNextDiscussion'} | ${[]} | ${null} | ${'a'}
- ${'jumpToNextDiscussion'} | ${[]} | ${'a'} | ${'c'}
- ${'jumpToNextDiscussion'} | ${[]} | ${'e'} | ${'a'}
- ${'jumpToPreviousDiscussion'} | ${[]} | ${null} | ${'e'}
- ${'jumpToPreviousDiscussion'} | ${[]} | ${'e'} | ${'c'}
- ${'jumpToPreviousDiscussion'} | ${[]} | ${'c'} | ${'a'}
- `('$fn (args = $args, currentId = $currentId)', ({ fn, args, currentId, expected }) => {
+ fn | args | currentId
+ ${'jumpToNextDiscussion'} | ${[]} | ${null}
+ ${'jumpToNextDiscussion'} | ${[]} | ${'a'}
+ ${'jumpToNextDiscussion'} | ${[]} | ${'e'}
+ ${'jumpToPreviousDiscussion'} | ${[]} | ${null}
+ ${'jumpToPreviousDiscussion'} | ${[]} | ${'e'}
+ ${'jumpToPreviousDiscussion'} | ${[]} | ${'c'}
+ `('$fn (args = $args, currentId = $currentId)', ({ fn, args, currentId }) => {
beforeEach(() => {
store.state.notes.currentDiscussionId = currentId;
});
@@ -130,125 +128,18 @@ describe('Discussion navigation mixin', () => {
await nextTick();
});
- it('expands discussion', () => {
- expect(expandDiscussion).toHaveBeenCalled();
- });
-
- it('scrolls to element', () => {
- expect(utils.scrollToElement).toHaveBeenCalled();
- });
- });
-
- describe('on `diffs` active tab', () => {
- beforeEach(async () => {
- window.mrTabs.currentAction = 'diffs';
- wrapper.vm[fn](...args);
-
+ it('expands discussion', async () => {
await nextTick();
- });
- it('sets current discussion', () => {
- expect(store.state.notes.currentDiscussionId).toEqual(expected);
- });
-
- it('expands discussion', () => {
expect(expandDiscussion).toHaveBeenCalled();
});
- it('scrolls when scrollToDiscussion is emitted', () => {
- expect(utils.scrollToElementWithContext).not.toHaveBeenCalled();
-
- eventHub.$emit('scrollToDiscussion');
-
- expect(utils.scrollToElementWithContext).toHaveBeenCalledWith(
- findDiscussion('ul.notes', expected),
- { behavior: 'auto', offset: 0 },
- );
- });
- });
-
- describe('on `other` active tab', () => {
- beforeEach(async () => {
- window.mrTabs.currentAction = 'other';
- wrapper.vm[fn](...args);
-
+ it('scrolls to element', async () => {
await nextTick();
- });
- it('sets current discussion', () => {
- expect(store.state.notes.currentDiscussionId).toEqual(expected);
- });
-
- it('does not expand discussion yet', () => {
- expect(expandDiscussion).not.toHaveBeenCalled();
- });
-
- it('shows mrTabs', () => {
- expect(window.mrTabs.tabShown).toHaveBeenCalledWith('show');
- });
-
- describe('when tab is changed', () => {
- beforeEach(() => {
- window.mrTabs.eventHub.$emit('MergeRequestTabChange');
-
- jest.runAllTimers();
- });
-
- it('expands discussion', () => {
- expect(expandDiscussion).toHaveBeenCalledWith(expect.anything(), {
- discussionId: expected,
- });
- });
-
- it('scrolls to discussion', () => {
- expect(utils.scrollToElement).toHaveBeenCalledWith(
- findDiscussion('div.discussion', expected),
- { behavior: 'auto', offset: 0 },
- );
- });
+ expect(utils.scrollToElement).toHaveBeenCalled();
});
});
});
-
- describe('virtual scrolling feature', () => {
- beforeEach(() => {
- jest.spyOn(store, 'dispatch');
-
- store.state.notes.currentDiscussionId = 'a';
- window.location.hash = 'test';
- });
-
- afterEach(() => {
- window.gon = {};
- window.location.hash = '';
- });
-
- it('resets location hash', async () => {
- wrapper.vm.jumpToNextDiscussion();
-
- await nextTick();
-
- expect(window.location.hash).toBe('');
- });
-
- it.each`
- tabValue
- ${'diffs'}
- ${'other'}
- `(
- 'calls scrollToFile with setHash as $hashValue when the tab is $tabValue',
- async ({ tabValue }) => {
- window.mrTabs.currentAction = tabValue;
-
- wrapper.vm.jumpToNextDiscussion();
-
- await nextTick();
-
- expect(store.dispatch).toHaveBeenCalledWith('diffs/scrollToFile', {
- path: 'test.js',
- });
- },
- );
- });
});
});
diff --git a/spec/frontend/notes/mock_data.js b/spec/frontend/notes/mock_data.js
index 9fa7166474a..286f2adc1d8 100644
--- a/spec/frontend/notes/mock_data.js
+++ b/spec/frontend/notes/mock_data.js
@@ -1,4 +1,5 @@
// Copied to ee/spec/frontend/notes/mock_data.js
+import { __ } from '~/locale';
export const notesDataMock = {
discussionsPath: '/gitlab-org/gitlab-foss/issues/26/discussions.json',
@@ -35,6 +36,7 @@ export const noteableDataMock = {
can_create_note: true,
can_update: true,
can_award_emoji: true,
+ can_create_confidential_note: true,
},
description: '',
due_date: null,
@@ -1292,3 +1294,18 @@ export const draftDiffDiscussion = {
file_path: 'lib/foo.rb',
isDraft: true,
};
+
+export const notesFilters = [
+ {
+ title: __('Show all activity'),
+ value: 0,
+ },
+ {
+ title: __('Show comments only'),
+ value: 1,
+ },
+ {
+ title: __('Show history only'),
+ value: 2,
+ },
+];
diff --git a/spec/frontend/notes/utils/get_notes_filter_data_spec.js b/spec/frontend/notes/utils/get_notes_filter_data_spec.js
new file mode 100644
index 00000000000..c3a8d3bc619
--- /dev/null
+++ b/spec/frontend/notes/utils/get_notes_filter_data_spec.js
@@ -0,0 +1,44 @@
+import { getNotesFilterData } from '~/notes/utils/get_notes_filter_data';
+import { notesFilters } from '../mock_data';
+
+// what: This is the format we expect the element attribute to be in
+// why: For readability, we make this clear by hardcoding the indecise instead of using `reduce`.
+const TEST_NOTES_FILTERS_ATTR = {
+ [notesFilters[0].title]: notesFilters[0].value,
+ [notesFilters[1].title]: notesFilters[1].value,
+ [notesFilters[2].title]: notesFilters[2].value,
+};
+
+describe('~/notes/utils/get_notes_filter_data', () => {
+ it.each([
+ {
+ desc: 'empty',
+ attributes: {},
+ expectation: {
+ notesFilters: [],
+ notesFilterValue: undefined,
+ },
+ },
+ {
+ desc: 'valid attributes',
+ attributes: {
+ 'data-notes-filters': JSON.stringify(TEST_NOTES_FILTERS_ATTR),
+ 'data-notes-filter-value': '1',
+ },
+ expectation: {
+ notesFilters,
+ notesFilterValue: 1,
+ },
+ },
+ ])('with $desc, parses data from element attributes', ({ attributes, expectation }) => {
+ const el = document.createElement('div');
+
+ Object.entries(attributes).forEach(([key, value]) => {
+ el.setAttribute(key, value);
+ });
+
+ const actual = getNotesFilterData(el);
+
+ expect(actual).toStrictEqual(expectation);
+ });
+});
diff --git a/spec/frontend/operation_settings/components/metrics_settings_spec.js b/spec/frontend/operation_settings/components/metrics_settings_spec.js
index 810049220ae..732dfdd42fb 100644
--- a/spec/frontend/operation_settings/components/metrics_settings_spec.js
+++ b/spec/frontend/operation_settings/components/metrics_settings_spec.js
@@ -2,7 +2,7 @@ import { GlButton, GlLink, GlFormGroup, GlFormInput, GlFormSelect } from '@gitla
import { mount, shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
import { TEST_HOST } from 'helpers/test_constants';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { refreshCurrentPage } from '~/lib/utils/url_utility';
import { timezones } from '~/monitoring/format_date';
@@ -52,7 +52,7 @@ describe('operation settings external dashboard component', () => {
}
axios.patch.mockReset();
refreshCurrentPage.mockReset();
- createFlash.mockReset();
+ createAlert.mockReset();
});
it('renders header text', () => {
@@ -208,7 +208,7 @@ describe('operation settings external dashboard component', () => {
await nextTick();
await jest.runAllTicks();
- expect(createFlash).toHaveBeenCalledWith({
+ expect(createAlert).toHaveBeenCalledWith({
message: `There was an error saving your changes. ${message}`,
});
});
diff --git a/spec/frontend/packages_and_registries/harbor_registry/components/tags/tags_list_row_spec.js b/spec/frontend/packages_and_registries/harbor_registry/components/tags/tags_list_row_spec.js
index 6fe3dabc603..849215e286b 100644
--- a/spec/frontend/packages_and_registries/harbor_registry/components/tags/tags_list_row_spec.js
+++ b/spec/frontend/packages_and_registries/harbor_registry/components/tags/tags_list_row_spec.js
@@ -8,8 +8,8 @@ import { defaultConfig, harborTagsList } from '../../mock_data';
describe('Harbor tag list row', () => {
let wrapper;
- const findListItem = () => wrapper.find(ListItem);
- const findClipboardButton = () => wrapper.find(ClipboardButton);
+ const findListItem = () => wrapper.findComponent(ListItem);
+ const findClipboardButton = () => wrapper.findComponent(ClipboardButton);
const findByTestId = (testId) => wrapper.findByTestId(testId);
const $route = {
@@ -58,7 +58,7 @@ describe('Harbor tag list row', () => {
expect(findByTestId('name').text()).toBe(harborTagsList[0].name);
});
- describe(' clipboard button', () => {
+ describe('clipboard button', () => {
it('exists', () => {
expect(findClipboardButton().exists()).toBe(true);
});
diff --git a/spec/frontend/packages_and_registries/harbor_registry/components/tags/tags_list_spec.js b/spec/frontend/packages_and_registries/harbor_registry/components/tags/tags_list_spec.js
index 6bcf6611d07..4c6b2b6daaa 100644
--- a/spec/frontend/packages_and_registries/harbor_registry/components/tags/tags_list_spec.js
+++ b/spec/frontend/packages_and_registries/harbor_registry/components/tags/tags_list_spec.js
@@ -8,9 +8,9 @@ import { defaultConfig, harborTagsResponse } from '../../mock_data';
describe('Harbor Tags List', () => {
let wrapper;
- const findTagsLoader = () => wrapper.find(TagsLoader);
+ const findTagsLoader = () => wrapper.findComponent(TagsLoader);
const findTagsListRows = () => wrapper.findAllComponents(TagsListRow);
- const findRegistryList = () => wrapper.find(RegistryList);
+ const findRegistryList = () => wrapper.findComponent(RegistryList);
const mountComponent = ({ propsData, config = defaultConfig }) => {
wrapper = shallowMount(TagsList, {
diff --git a/spec/frontend/packages_and_registries/harbor_registry/pages/tags_spec.js b/spec/frontend/packages_and_registries/harbor_registry/pages/tags_spec.js
index 7e0f05e736b..10901c6ec1e 100644
--- a/spec/frontend/packages_and_registries/harbor_registry/pages/tags_spec.js
+++ b/spec/frontend/packages_and_registries/harbor_registry/pages/tags_spec.js
@@ -15,8 +15,8 @@ jest.mock('~/rest_api', () => ({
describe('Harbor Tags page', () => {
let wrapper;
- const findTagsHeader = () => wrapper.find(TagsHeader);
- const findTagsList = () => wrapper.find(TagsList);
+ const findTagsHeader = () => wrapper.findComponent(TagsHeader);
+ const findTagsList = () => wrapper.findComponent(TagsList);
const waitForHarborTagsRequest = async () => {
await waitForPromises();
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 31ab108558c..bb970336b94 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
@@ -1,6 +1,6 @@
import testAction from 'helpers/vuex_action_helper';
import Api from '~/api';
-import createFlash from '~/flash';
+import { createAlert, VARIANT_SUCCESS, VARIANT_WARNING } from '~/flash';
import { FETCH_PACKAGE_VERSIONS_ERROR } from '~/packages_and_registries/infrastructure_registry/details/constants';
import {
fetchPackageVersions,
@@ -67,9 +67,9 @@ describe('Actions Package details store', () => {
[],
);
expect(Api.projectPackage).toHaveBeenCalledWith(packageEntity.project_id, packageEntity.id);
- expect(createFlash).toHaveBeenCalledWith({
+ expect(createAlert).toHaveBeenCalledWith({
message: FETCH_PACKAGE_VERSIONS_ERROR,
- type: 'warning',
+ variant: VARIANT_WARNING,
});
});
});
@@ -87,9 +87,9 @@ describe('Actions Package details store', () => {
Api.deleteProjectPackage = jest.fn().mockRejectedValue();
await testAction(deletePackage, undefined, { packageEntity }, [], []);
- expect(createFlash).toHaveBeenCalledWith({
+ expect(createAlert).toHaveBeenCalledWith({
message: DELETE_PACKAGE_ERROR_MESSAGE,
- type: 'warning',
+ variant: VARIANT_WARNING,
});
});
});
@@ -112,18 +112,18 @@ describe('Actions Package details store', () => {
packageEntity.id,
fileId,
);
- expect(createFlash).toHaveBeenCalledWith({
+ expect(createAlert).toHaveBeenCalledWith({
message: DELETE_PACKAGE_FILE_SUCCESS_MESSAGE,
- type: 'success',
+ variant: VARIANT_SUCCESS,
});
});
it('should create flash on API error', async () => {
Api.deleteProjectPackageFile = jest.fn().mockRejectedValue();
await testAction(deletePackageFile, fileId, { packageEntity }, [], []);
- expect(createFlash).toHaveBeenCalledWith({
+ expect(createAlert).toHaveBeenCalledWith({
message: DELETE_PACKAGE_FILE_ERROR_MESSAGE,
- type: 'warning',
+ variant: VARIANT_WARNING,
});
});
});
diff --git a/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/infrastructure_title_spec.js b/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/infrastructure_title_spec.js
index 93d013bb458..aca6b0942cc 100644
--- a/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/infrastructure_title_spec.js
+++ b/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/infrastructure_title_spec.js
@@ -74,7 +74,7 @@ describe('Infrastructure Title', () => {
mountComponent({ ...exampleProps, count });
});
- it(exist ? 'exists' : 'does not exist', () => {
+ it(`${exist ? 'exists' : 'does not exist'}`, () => {
expect(findMetadataItem().exists()).toBe(exist);
});
diff --git a/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/packages_list_app_spec.js b/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/packages_list_app_spec.js
index db1d3f3f633..dff95364d7d 100644
--- a/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/packages_list_app_spec.js
+++ b/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/packages_list_app_spec.js
@@ -3,7 +3,7 @@ import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import Vuex from 'vuex';
import setWindowLocation from 'helpers/set_window_location_helper';
-import createFlash from '~/flash';
+import { createAlert, VARIANT_INFO } from '~/flash';
import * as commonUtils from '~/lib/utils/common_utils';
import PackageListApp from '~/packages_and_registries/infrastructure_registry/list/components/packages_list_app.vue';
import { DELETE_PACKAGE_SUCCESS_MESSAGE } from '~/packages_and_registries/infrastructure_registry/list/constants';
@@ -222,9 +222,9 @@ describe('packages_list_app', () => {
it(`creates a flash if the query string contains ${SHOW_DELETE_SUCCESS_ALERT}`, () => {
mountComponent();
- expect(createFlash).toHaveBeenCalledWith({
+ expect(createAlert).toHaveBeenCalledWith({
message: DELETE_PACKAGE_SUCCESS_MESSAGE,
- type: 'notice',
+ variant: VARIANT_INFO,
});
});
@@ -238,7 +238,7 @@ describe('packages_list_app', () => {
setWindowLocation('?');
mountComponent();
- expect(createFlash).not.toHaveBeenCalled();
+ expect(createAlert).not.toHaveBeenCalled();
expect(commonUtils.historyReplaceState).not.toHaveBeenCalled();
});
});
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 d596f2dae33..36417eaf793 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
@@ -2,7 +2,7 @@ import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
import Api from '~/api';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { MISSING_DELETE_PATH_ERROR } from '~/packages_and_registries/infrastructure_registry/list/constants';
import * as actions from '~/packages_and_registries/infrastructure_registry/list/stores/actions';
import * as types from '~/packages_and_registries/infrastructure_registry/list/stores/mutation_types';
@@ -107,7 +107,7 @@ describe('Actions Package list store', () => {
{ type: 'setLoading', payload: false },
],
);
- expect(createFlash).toHaveBeenCalled();
+ expect(createAlert).toHaveBeenCalled();
});
it('should force the terraform_module type when forceTerraform is true', async () => {
@@ -209,17 +209,17 @@ describe('Actions Package list store', () => {
{ type: 'setLoading', payload: false },
],
);
- expect(createFlash).toHaveBeenCalled();
+ expect(createAlert).toHaveBeenCalled();
});
it.each`
property | actionPayload
${'_links'} | ${{}}
${'delete_api_path'} | ${{ _links: {} }}
- `('should reject and createFlash when $property is missing', ({ actionPayload }) => {
+ `('should reject and createAlert 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({
+ expect(createAlert).toHaveBeenCalledWith({
message: DELETE_PACKAGE_ERROR_MESSAGE,
});
});
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/package_title_spec.js.snap b/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/package_title_spec.js.snap
index 61923233d2e..047fa04947c 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/package_title_spec.js.snap
+++ b/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/package_title_spec.js.snap
@@ -79,6 +79,18 @@ exports[`PackageTitle renders with tags 1`] = `
texttooltip=""
/>
</div>
+ <div
+ class="gl-display-flex gl-align-items-center gl-mr-5"
+ >
+ <metadata-item-stub
+ data-testid="package-last-downloaded-at"
+ icon="download"
+ link=""
+ size="m"
+ text="Last downloaded Aug 17, 2021"
+ texttooltip=""
+ />
+ </div>
</div>
</div>
@@ -164,6 +176,18 @@ exports[`PackageTitle renders without tags 1`] = `
texttooltip=""
/>
</div>
+ <div
+ class="gl-display-flex gl-align-items-center gl-mr-5"
+ >
+ <metadata-item-stub
+ data-testid="package-last-downloaded-at"
+ icon="download"
+ link=""
+ size="m"
+ text="Last downloaded Aug 17, 2021"
+ texttooltip=""
+ />
+ </div>
</div>
</div>
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/package_title_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/package_title_spec.js
index 37416dcd4e7..1fda77f2aaa 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/details/package_title_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/details/package_title_spec.js
@@ -49,6 +49,7 @@ describe('PackageTitle', () => {
const findPackageSize = () => wrapper.findByTestId('package-size');
const findPipelineProject = () => wrapper.findByTestId('pipeline-project');
const findPackageRef = () => wrapper.findByTestId('package-ref');
+ const findPackageLastDownloadedAt = () => wrapper.findByTestId('package-last-downloaded-at');
const findPackageTags = () => wrapper.findComponent(PackageTags);
const findPackageBadges = () => wrapper.findAllByTestId('tag-badge');
const findSubHeaderText = () => wrapper.findByTestId('sub-header');
@@ -227,4 +228,25 @@ describe('PackageTitle', () => {
});
});
});
+
+ describe('package last downloaded at', () => {
+ it('does not display the data if missing', async () => {
+ await createComponent({
+ ...packageData(),
+ lastDownloadedAt: null,
+ });
+
+ expect(findPackageLastDownloadedAt().exists()).toBe(false);
+ });
+
+ it('correctly shows the data if present', async () => {
+ await createComponent();
+
+ expect(findPackageLastDownloadedAt().props()).toMatchObject({
+ text: 'Last downloaded Aug 17, 2021',
+ icon: 'download',
+ size: 'm',
+ });
+ });
+ });
});
diff --git a/spec/frontend/packages_and_registries/package_registry/components/functional/delete_package_spec.js b/spec/frontend/packages_and_registries/package_registry/components/functional/delete_package_spec.js
index 14a70def7d0..93c2196b210 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/functional/delete_package_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/functional/delete_package_spec.js
@@ -3,7 +3,7 @@ import VueApollo from 'vue-apollo';
import waitForPromises from 'helpers/wait_for_promises';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
-import createFlash from '~/flash';
+import { createAlert, VARIANT_SUCCESS, VARIANT_WARNING } from '~/flash';
import DeletePackage from '~/packages_and_registries/package_registry/components/functional/delete_package.vue';
import destroyPackageMutation from '~/packages_and_registries/package_registry/graphql/mutations/destroy_package.mutation.graphql';
@@ -104,22 +104,22 @@ describe('DeletePackage', () => {
expect(wrapper.emitted('end')).toEqual([[]]);
});
- it('does not call createFlash', async () => {
+ it('does not call createAlert', async () => {
createComponent();
await clickOnButtonAndWait(eventPayload);
- expect(createFlash).not.toHaveBeenCalled();
+ expect(createAlert).not.toHaveBeenCalled();
});
- it('calls createFlash with the success message when showSuccessAlert is true', async () => {
+ it('calls createAlert with the success message when showSuccessAlert is true', async () => {
createComponent({ showSuccessAlert: true });
await clickOnButtonAndWait(eventPayload);
- expect(createFlash).toHaveBeenCalledWith({
+ expect(createAlert).toHaveBeenCalledWith({
message: DeletePackage.i18n.successMessage,
- type: 'success',
+ variant: VARIANT_SUCCESS,
});
});
});
@@ -141,14 +141,14 @@ describe('DeletePackage', () => {
expect(wrapper.emitted('end')).toEqual([[]]);
});
- it('calls createFlash with the error message', async () => {
+ it('calls createAlert with the error message', async () => {
createComponent({ showSuccessAlert: true });
await clickOnButtonAndWait(eventPayload);
- expect(createFlash).toHaveBeenCalledWith({
+ expect(createAlert).toHaveBeenCalledWith({
message: DeletePackage.i18n.errorMessage,
- type: 'warning',
+ variant: VARIANT_WARNING,
captureError: true,
error: expect.any(Error),
});
diff --git a/spec/frontend/packages_and_registries/package_registry/mock_data.js b/spec/frontend/packages_and_registries/package_registry/mock_data.js
index 22236424e6a..c2b6fb734d6 100644
--- a/spec/frontend/packages_and_registries/package_registry/mock_data.js
+++ b/spec/frontend/packages_and_registries/package_registry/mock_data.js
@@ -127,6 +127,7 @@ export const packageData = (extend) => ({
version: '1.0.0',
createdAt: '2020-08-17T14:23:32Z',
updatedAt: '2020-08-17T14:23:32Z',
+ lastDownloadedAt: '2021-08-17T14:23:32Z',
status: 'DEFAULT',
mavenUrl: 'http://gdk.test:3000/api/v4/projects/1/packages/maven',
npmUrl: 'http://gdk.test:3000/api/v4/projects/1/packages/npm',
diff --git a/spec/frontend/packages_and_registries/package_registry/pages/details_spec.js b/spec/frontend/packages_and_registries/package_registry/pages/details_spec.js
index 83158d1cc5e..a32e76a132e 100644
--- a/spec/frontend/packages_and_registries/package_registry/pages/details_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/pages/details_spec.js
@@ -1,4 +1,4 @@
-import { GlEmptyState, GlBadge, GlTabs, GlTab } from '@gitlab/ui';
+import { GlEmptyState, GlBadge, GlTabs, GlTab, GlSprintf } from '@gitlab/ui';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
@@ -6,7 +6,7 @@ import createMockApollo from 'helpers/mock_apollo_helper';
import { useMockLocationHelper } from 'helpers/mock_window_location_helper';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import AdditionalMetadata from '~/packages_and_registries/package_registry/components/details/additional_metadata.vue';
import PackagesApp from '~/packages_and_registries/package_registry/pages/details.vue';
@@ -86,11 +86,17 @@ describe('PackagesApp', () => {
PackageTitle,
DeletePackage,
GlModal: {
- template: '<div></div>',
+ template: `
+ <div>
+ <slot name="modal-title"></slot>
+ <p><slot></slot></p>
+ </div>
+ `,
methods: {
show: jest.fn(),
},
},
+ GlSprintf,
GlTabs,
GlTab,
},
@@ -149,7 +155,7 @@ describe('PackagesApp', () => {
await waitForPromises();
- expect(createFlash).toHaveBeenCalledWith(
+ expect(createAlert).toHaveBeenCalledWith(
expect.objectContaining({
message: FETCH_PACKAGE_DETAILS_ERROR_MESSAGE,
}),
@@ -245,7 +251,9 @@ describe('PackagesApp', () => {
await findDeleteButton().trigger('click');
- expect(findDeleteModal().exists()).toBe(true);
+ expect(findDeleteModal().find('p').text()).toBe(
+ 'You are about to delete version 1.0.0 of @gitlab-org/package-15. Are you sure?',
+ );
});
describe('successful request', () => {
@@ -359,6 +367,12 @@ describe('PackagesApp', () => {
expect(showDeletePackageSpy).toHaveBeenCalled();
expect(showDeleteFileSpy).not.toHaveBeenCalled();
+
+ await waitForPromises();
+
+ expect(findDeleteModal().find('p').text()).toBe(
+ 'Deleting the last package asset will remove version 1.0.0 of @gitlab-org/package-15. Are you sure?',
+ );
});
it('confirming on the modal sets the loading state', async () => {
@@ -383,7 +397,7 @@ describe('PackagesApp', () => {
await doDeleteFile();
- expect(createFlash).toHaveBeenCalledWith(
+ expect(createAlert).toHaveBeenCalledWith(
expect.objectContaining({
message: DELETE_PACKAGE_FILE_SUCCESS_MESSAGE,
}),
@@ -399,7 +413,7 @@ describe('PackagesApp', () => {
await doDeleteFile();
- expect(createFlash).toHaveBeenCalledWith(
+ expect(createAlert).toHaveBeenCalledWith(
expect.objectContaining({
message: DELETE_PACKAGE_FILE_ERROR_MESSAGE,
}),
@@ -416,7 +430,7 @@ describe('PackagesApp', () => {
await doDeleteFile();
- expect(createFlash).toHaveBeenCalledWith(
+ expect(createAlert).toHaveBeenCalledWith(
expect.objectContaining({
message: DELETE_PACKAGE_FILE_ERROR_MESSAGE,
}),
@@ -468,7 +482,7 @@ describe('PackagesApp', () => {
await doDeleteFiles();
- expect(createFlash).toHaveBeenCalledWith(
+ expect(createAlert).toHaveBeenCalledWith(
expect.objectContaining({
message: DELETE_PACKAGE_FILES_SUCCESS_MESSAGE,
}),
@@ -484,7 +498,7 @@ describe('PackagesApp', () => {
await doDeleteFiles();
- expect(createFlash).toHaveBeenCalledWith(
+ expect(createAlert).toHaveBeenCalledWith(
expect.objectContaining({
message: DELETE_PACKAGE_FILES_ERROR_MESSAGE,
}),
@@ -501,7 +515,7 @@ describe('PackagesApp', () => {
await doDeleteFiles();
- expect(createFlash).toHaveBeenCalledWith(
+ expect(createAlert).toHaveBeenCalledWith(
expect.objectContaining({
message: DELETE_PACKAGE_FILES_ERROR_MESSAGE,
}),
@@ -533,6 +547,12 @@ describe('PackagesApp', () => {
findPackageFiles().vm.$emit('delete-files', packageFiles());
expect(showDeletePackageSpy).toHaveBeenCalled();
+
+ await waitForPromises();
+
+ expect(findDeleteModal().find('p').text()).toBe(
+ 'Deleting all package assets will remove version 1.0.0 of @gitlab-org/package-15. Are you sure?',
+ );
});
});
});
diff --git a/spec/frontend/packages_and_registries/settings/project/settings/components/cleanup_image_tags_spec.js b/spec/frontend/packages_and_registries/settings/project/settings/components/cleanup_image_tags_spec.js
index 8b60f31512b..2bb99fb8e8f 100644
--- a/spec/frontend/packages_and_registries/settings/project/settings/components/cleanup_image_tags_spec.js
+++ b/spec/frontend/packages_and_registries/settings/project/settings/components/cleanup_image_tags_spec.js
@@ -19,6 +19,7 @@ import {
expirationPolicyPayload,
emptyExpirationPolicyPayload,
containerExpirationPolicyData,
+ nullExpirationPolicyPayload,
} from '../mock_data';
describe('Cleanup image tags project settings', () => {
@@ -98,15 +99,30 @@ describe('Cleanup image tags project settings', () => {
expect(findDescription().text()).toMatchInterpolatedText(CONTAINER_CLEANUP_POLICY_DESCRIPTION);
});
+ it('when loading does not render form or alert components', () => {
+ mountComponentWithApollo({
+ resolver: jest.fn().mockResolvedValue(),
+ });
+
+ expect(findFormComponent().exists()).toBe(false);
+ expect(findAlert().exists()).toBe(false);
+ });
+
describe('the form is disabled', () => {
- it('hides the form', () => {
- mountComponent();
+ it('hides the form', async () => {
+ mountComponentWithApollo({
+ resolver: jest.fn().mockResolvedValue(nullExpirationPolicyPayload()),
+ });
+ await waitForPromises();
expect(findFormComponent().exists()).toBe(false);
});
- it('shows an alert', () => {
- mountComponent();
+ it('shows an alert', async () => {
+ mountComponentWithApollo({
+ resolver: jest.fn().mockResolvedValue(nullExpirationPolicyPayload()),
+ });
+ await waitForPromises();
const text = findAlert().text();
expect(text).toContain(UNAVAILABLE_FEATURE_INTRO_TEXT);
@@ -114,8 +130,12 @@ describe('Cleanup image tags project settings', () => {
});
describe('an admin is visiting the page', () => {
- it('shows the admin part of the alert message', () => {
- mountComponent({ ...defaultProvidedValues, isAdmin: true });
+ it('shows the admin part of the alert message', async () => {
+ mountComponentWithApollo({
+ provide: { ...defaultProvidedValues, isAdmin: true },
+ resolver: jest.fn().mockResolvedValue(nullExpirationPolicyPayload()),
+ });
+ await waitForPromises();
const sprintf = findAlert().findComponent(GlSprintf);
expect(sprintf.text()).toBe('administration settings');
diff --git a/spec/frontend/packages_and_registries/settings/project/settings/components/container_expiration_policy_spec.js b/spec/frontend/packages_and_registries/settings/project/settings/components/container_expiration_policy_spec.js
index 35baeaeac61..43484d26d76 100644
--- a/spec/frontend/packages_and_registries/settings/project/settings/components/container_expiration_policy_spec.js
+++ b/spec/frontend/packages_and_registries/settings/project/settings/components/container_expiration_policy_spec.js
@@ -16,7 +16,11 @@ import {
import expirationPolicyQuery from '~/packages_and_registries/settings/project/graphql/queries/get_expiration_policy.query.graphql';
import SettingsBlock from '~/vue_shared/components/settings/settings_block.vue';
-import { expirationPolicyPayload, emptyExpirationPolicyPayload } from '../mock_data';
+import {
+ expirationPolicyPayload,
+ emptyExpirationPolicyPayload,
+ nullExpirationPolicyPayload,
+} from '../mock_data';
describe('Container expiration policy project settings', () => {
let wrapper;
@@ -78,15 +82,30 @@ describe('Container expiration policy project settings', () => {
expect(findButton().attributes('href')).toBe(defaultProvidedValues.cleanupSettingsPath);
});
+ it('when loading does not render form or alert components', () => {
+ mountComponentWithApollo({
+ resolver: jest.fn().mockResolvedValue(),
+ });
+
+ expect(findFormComponent().exists()).toBe(false);
+ expect(findAlert().exists()).toBe(false);
+ });
+
describe('the form is disabled', () => {
- it('the form is hidden', () => {
- mountComponent();
+ it('hides the form', async () => {
+ mountComponentWithApollo({
+ resolver: jest.fn().mockResolvedValue(nullExpirationPolicyPayload()),
+ });
+ await waitForPromises();
expect(findFormComponent().exists()).toBe(false);
});
- it('shows an alert', () => {
- mountComponent();
+ it('shows an alert', async () => {
+ mountComponentWithApollo({
+ resolver: jest.fn().mockResolvedValue(nullExpirationPolicyPayload()),
+ });
+ await waitForPromises();
const text = findAlert().text();
expect(text).toContain(UNAVAILABLE_FEATURE_INTRO_TEXT);
@@ -94,8 +113,12 @@ describe('Container expiration policy project settings', () => {
});
describe('an admin is visiting the page', () => {
- it('shows the admin part of the alert message', () => {
- mountComponent({ ...defaultProvidedValues, isAdmin: true });
+ it('shows the admin part of the alert message', async () => {
+ mountComponentWithApollo({
+ provide: { ...defaultProvidedValues, isAdmin: true },
+ resolver: jest.fn().mockResolvedValue(nullExpirationPolicyPayload()),
+ });
+ await waitForPromises();
const sprintf = findAlert().findComponent(GlSprintf);
expect(sprintf.text()).toBe('administration settings');
diff --git a/spec/frontend/packages_and_registries/settings/project/settings/mock_data.js b/spec/frontend/packages_and_registries/settings/project/settings/mock_data.js
index 0696144215c..3204ca01f99 100644
--- a/spec/frontend/packages_and_registries/settings/project/settings/mock_data.js
+++ b/spec/frontend/packages_and_registries/settings/project/settings/mock_data.js
@@ -29,6 +29,15 @@ export const emptyExpirationPolicyPayload = () => ({
},
});
+export const nullExpirationPolicyPayload = () => ({
+ data: {
+ project: {
+ id: '1',
+ containerExpirationPolicy: null,
+ },
+ },
+});
+
export const expirationPolicyMutationPayload = ({ override, errors = [] } = {}) => ({
data: {
updateContainerExpirationPolicy: {
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 ebf21c01324..17669331370 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
@@ -1,9 +1,10 @@
-import Vue from 'vue';
+import Vue, { nextTick } from 'vue';
+import { mount } from '@vue/test-utils';
+import { GlModal } from '@gitlab/ui';
import { TEST_HOST } from 'helpers/test_constants';
-import mountComponent from 'helpers/vue_mount_component_helper';
import axios from '~/lib/utils/axios_utils';
import { redirectTo } from '~/lib/utils/url_utility';
-import stopJobsModal from '~/pages/admin/jobs/index/components/stop_jobs_modal.vue';
+import StopJobsModal from '~/pages/admin/jobs/index/components/stop_jobs_modal.vue';
jest.mock('~/lib/utils/url_utility', () => ({
...jest.requireActual('~/lib/utils/url_utility'),
@@ -14,20 +15,23 @@ describe('stop_jobs_modal.vue', () => {
const props = {
url: `${TEST_HOST}/stop_jobs_modal.vue/stopAll`,
};
- let vm;
+ let wrapper;
- afterEach(() => {
- vm.$destroy();
+ beforeEach(() => {
+ wrapper = mount(StopJobsModal, { propsData: props });
});
- beforeEach(() => {
- const Component = Vue.extend(stopJobsModal);
- vm = mountComponent(Component, props);
+ afterEach(() => {
+ wrapper.destroy();
});
- describe('onSubmit', () => {
+ describe('on submit', () => {
it('stops jobs and redirects to overview page', async () => {
const responseURL = `${TEST_HOST}/stop_jobs_modal.vue/jobs`;
+ // TODO: We can't use axios-mock-adapter because our current version
+ // does not support responseURL
+ //
+ // see https://gitlab.com/gitlab-org/gitlab/-/issues/375308 for details
jest.spyOn(axios, 'post').mockImplementation((url) => {
expect(url).toBe(props.url);
return Promise.resolve({
@@ -37,18 +41,28 @@ describe('stop_jobs_modal.vue', () => {
});
});
- await vm.onSubmit();
+ wrapper.findComponent(GlModal).vm.$emit('primary');
+ await nextTick();
+
expect(redirectTo).toHaveBeenCalledWith(responseURL);
});
it('displays error if stopping jobs failed', async () => {
+ Vue.config.errorHandler = () => {}; // silencing thrown error
+
const dummyError = new Error('stopping jobs failed');
+ // TODO: We can't use axios-mock-adapter because our current version
+ // does not support responseURL
+ //
+ // see https://gitlab.com/gitlab-org/gitlab/-/issues/375308 for details
jest.spyOn(axios, 'post').mockImplementation((url) => {
expect(url).toBe(props.url);
return Promise.reject(dummyError);
});
- await expect(vm.onSubmit()).rejects.toEqual(dummyError);
+ wrapper.findComponent(GlModal).vm.$emit('primary');
+ await nextTick();
+
expect(redirectTo).not.toHaveBeenCalled();
});
});
diff --git a/spec/frontend/pages/import/fogbugz/new_user_map/components/user_select_spec.js b/spec/frontend/pages/import/fogbugz/new_user_map/components/user_select_spec.js
new file mode 100644
index 00000000000..c1e1545944b
--- /dev/null
+++ b/spec/frontend/pages/import/fogbugz/new_user_map/components/user_select_spec.js
@@ -0,0 +1,81 @@
+import Vue, { nextTick } from 'vue';
+import VueApollo from 'vue-apollo';
+import { GlListbox } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import searchUsersQuery from '~/graphql_shared/queries/users_search_all.query.graphql';
+
+import createMockApollo from 'helpers/mock_apollo_helper';
+import UserSelect from '~/pages/import/fogbugz/new_user_map/components/user_select.vue';
+
+Vue.use(VueApollo);
+
+const USERS_RESPONSE = {
+ data: {
+ users: {
+ nodes: [
+ {
+ id: 'gid://gitlab/User/44',
+ avatarUrl: '/avatar1',
+ webUrl: '/reported_user_22',
+ name: 'Birgit Steuber',
+ username: 'reported_user_22',
+ __typename: 'UserCore',
+ },
+ {
+ id: 'gid://gitlab/User/43',
+ avatarUrl: '/avatar2',
+ webUrl: '/reported_user_21',
+ name: 'Luke Spinka',
+ username: 'reported_user_21',
+ __typename: 'UserCore',
+ },
+ ],
+ __typename: 'UserCoreConnection',
+ },
+ },
+};
+
+describe('fogbugz user select component', () => {
+ let wrapper;
+ const searchQueryHandlerSuccess = jest.fn().mockResolvedValue(USERS_RESPONSE);
+
+ const createComponent = (propsData = { name: 'demo' }) => {
+ const fakeApollo = createMockApollo([[searchUsersQuery, searchQueryHandlerSuccess]]);
+
+ wrapper = shallowMount(UserSelect, {
+ apolloProvider: fakeApollo,
+ propsData,
+ });
+ };
+
+ it('renders hidden input with name from props', () => {
+ const name = 'test';
+ createComponent({ name });
+ expect(wrapper.find('input').attributes('name')).toBe(name);
+ });
+
+ it('syncs input value with value emitted from listbox', async () => {
+ createComponent();
+
+ const id = 8;
+
+ wrapper.findComponent(GlListbox).vm.$emit('select', `gid://gitlab/User/${id}`);
+ await nextTick();
+
+ expect(wrapper.get('input').attributes('value')).toBe(id.toString());
+ });
+
+ it('filters users when search is performed in listbox', async () => {
+ createComponent();
+ jest.runOnlyPendingTimers();
+
+ wrapper.findComponent(GlListbox).vm.$emit('search', 'test');
+ await nextTick();
+ jest.runOnlyPendingTimers();
+
+ expect(searchQueryHandlerSuccess).toHaveBeenCalledWith({
+ first: expect.anything(),
+ search: 'test',
+ });
+ });
+});
diff --git a/spec/frontend/pages/projects/forks/new/components/fork_form_spec.js b/spec/frontend/pages/projects/forks/new/components/fork_form_spec.js
index f221a90da61..727c5164cdc 100644
--- a/spec/frontend/pages/projects/forks/new/components/fork_form_spec.js
+++ b/spec/frontend/pages/projects/forks/new/components/fork_form_spec.js
@@ -6,7 +6,7 @@ import AxiosMockAdapter from 'axios-mock-adapter';
import { kebabCase } from 'lodash';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import * as urlUtility from '~/lib/utils/url_utility';
import ForkForm from '~/pages/projects/forks/new/components/fork_form.vue';
import createMockApollo from 'helpers/mock_apollo_helper';
@@ -449,7 +449,7 @@ describe('ForkForm component', () => {
await submitForm();
expect(urlUtility.redirectTo).not.toHaveBeenCalled();
- expect(createFlash).toHaveBeenCalledWith({
+ expect(createAlert).toHaveBeenCalledWith({
message: 'An error occurred while forking the project. Please try again.',
});
});
diff --git a/spec/frontend/pages/projects/forks/new/components/project_namespace_spec.js b/spec/frontend/pages/projects/forks/new/components/project_namespace_spec.js
index 1a88aebae32..f6d3957115f 100644
--- a/spec/frontend/pages/projects/forks/new/components/project_namespace_spec.js
+++ b/spec/frontend/pages/projects/forks/new/components/project_namespace_spec.js
@@ -10,7 +10,7 @@ import { mount, shallowMount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import searchQuery from '~/pages/projects/forks/new/queries/search_forkable_namespaces.query.graphql';
import ProjectNamespace from '~/pages/projects/forks/new/components/project_namespace.vue';
@@ -167,7 +167,7 @@ describe('ProjectNamespace component', () => {
});
it('creates a flash message and captures the error', () => {
- expect(createFlash).toHaveBeenCalledWith({
+ expect(createAlert).toHaveBeenCalledWith({
message: 'Something went wrong while loading data. Please refresh the page to try again.',
captureError: true,
error: expect.any(Error),
diff --git a/spec/frontend/pages/projects/pipeline_schedules/shared/components/timezone_dropdown_spec.js b/spec/frontend/pages/projects/pipeline_schedules/shared/components/timezone_dropdown_spec.js
index 5b9c48f0d9b..f54d56c3af4 100644
--- a/spec/frontend/pages/projects/pipeline_schedules/shared/components/timezone_dropdown_spec.js
+++ b/spec/frontend/pages/projects/pipeline_schedules/shared/components/timezone_dropdown_spec.js
@@ -1,8 +1,7 @@
import $ from 'jquery';
import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
+import { formatUtcOffset, formatTimezone } from '~/lib/utils/datetime_utility';
import TimezoneDropdown, {
- formatUtcOffset,
- formatTimezone,
findTimezoneByIdentifier,
} from '~/pages/projects/pipeline_schedules/shared/components/timezone_dropdown';
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 b37d2f06191..0f947e84e0f 100644
--- a/spec/frontend/pages/shared/wikis/components/wiki_form_spec.js
+++ b/spec/frontend/pages/shared/wikis/components/wiki_form_spec.js
@@ -1,15 +1,12 @@
import { nextTick } from 'vue';
-import { GlAlert, GlButton, GlFormInput, GlFormGroup, GlSegmentedControl } from '@gitlab/ui';
+import { GlAlert, GlButton, GlFormInput, GlFormGroup } from '@gitlab/ui';
import { mount, shallowMount } from '@vue/test-utils';
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import { mockTracking } from 'helpers/tracking_helper';
-import { stubComponent } from 'helpers/stub_component';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
-import waitForPromises from 'helpers/wait_for_promises';
-import ContentEditor from '~/content_editor/components/content_editor.vue';
-import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
import WikiForm from '~/pages/shared/wikis/components/wiki_form.vue';
+import MarkdownEditor from '~/vue_shared/components/markdown/markdown_editor.vue';
import {
CONTENT_EDITOR_LOADED_ACTION,
SAVED_USING_CONTENT_EDITOR_ACTION,
@@ -18,8 +15,6 @@ import {
WIKI_FORMAT_UPDATED_ACTION,
} from '~/pages/shared/wikis/constants';
-import MarkdownField from '~/vue_shared/components/markdown/field.vue';
-
jest.mock('~/emoji');
describe('WikiForm', () => {
@@ -30,16 +25,12 @@ describe('WikiForm', () => {
const findForm = () => wrapper.find('form');
const findTitle = () => wrapper.find('#wiki_title');
const findFormat = () => wrapper.find('#wiki_format');
- const findContent = () => wrapper.find('#wiki_content');
const findMessage = () => wrapper.find('#wiki_message');
+ const findMarkdownEditor = () => wrapper.findComponent(MarkdownEditor);
const findSubmitButton = () => wrapper.findByTestId('wiki-submit-button');
const findCancelButton = () => wrapper.findByTestId('wiki-cancel-button');
- const findToggleEditingModeButton = () => wrapper.findByTestId('toggle-editing-mode-button');
const findTitleHelpLink = () => wrapper.findByText('Learn more.');
const findMarkdownHelpLink = () => wrapper.findByTestId('wiki-markdown-help-link');
- const findContentEditor = () => wrapper.findComponent(ContentEditor);
- const findClassicEditor = () => wrapper.findComponent(MarkdownField);
- const findLocalStorageSync = () => wrapper.findComponent(LocalStorageSync);
const setFormat = (value) => {
const format = findFormat();
@@ -53,13 +44,6 @@ describe('WikiForm', () => {
await nextTick();
};
- const dispatchBeforeUnload = () => {
- const e = new Event('beforeunload');
- jest.spyOn(e, 'preventDefault');
- window.dispatchEvent(e);
- return e;
- };
-
const pageInfoNew = {
persisted: false,
uploadsPath: '/project/path/-/wikis/attachments',
@@ -103,11 +87,8 @@ describe('WikiForm', () => {
},
},
stubs: {
- MarkdownField,
GlAlert,
GlButton,
- GlSegmentedControl,
- LocalStorageSync: stubComponent(LocalStorageSync),
GlFormInput,
GlFormGroup,
},
@@ -126,6 +107,22 @@ describe('WikiForm', () => {
wrapper = null;
});
+ it('displays markdown editor', () => {
+ createWrapper({ persisted: true });
+
+ expect(findMarkdownEditor().props()).toEqual(
+ expect.objectContaining({
+ value: pageInfoPersisted.content,
+ renderMarkdownPath: pageInfoPersisted.markdownPreviewPath,
+ markdownDocsPath: pageInfoPersisted.markdownHelpPath,
+ uploadsPath: pageInfoPersisted.uploadsPath,
+ initOnAutofocus: pageInfoPersisted.persisted,
+ formFieldId: 'wiki_content',
+ formFieldName: 'wiki[content]',
+ }),
+ );
+ });
+
it.each`
title | persisted | message
${'my page'} | ${false} | ${'Create my page'}
@@ -154,7 +151,7 @@ describe('WikiForm', () => {
it('does not trim page content by default', () => {
createWrapper({ persisted: true });
- expect(findContent().element.value).toBe(' My page content ');
+ expect(findMarkdownEditor().props().value).toBe(' My page content ');
});
it.each`
@@ -168,7 +165,9 @@ describe('WikiForm', () => {
await setFormat(format);
- expect(findClassicEditor().props('enablePreview')).toBe(enabled);
+ nextTick();
+
+ expect(findMarkdownEditor().props('enablePreview')).toBe(enabled);
});
it.each`
@@ -185,14 +184,6 @@ describe('WikiForm', () => {
expect(wrapper.text()).toContain(text);
});
- it('starts with no unload warning', () => {
- createWrapper();
-
- const e = dispatchBeforeUnload();
- expect(typeof e.returnValue).not.toBe('string');
- expect(e.preventDefault).not.toHaveBeenCalled();
- });
-
it.each`
persisted | titleHelpText | titleHelpLink
${true} | ${'You can move this page by adding the path to the beginning of the title.'} | ${'/help/user/project/wiki/index#move-a-wiki-page'}
@@ -219,15 +210,7 @@ describe('WikiForm', () => {
beforeEach(async () => {
createWrapper({ mountFn: mount, persisted: true });
- const input = findContent();
-
- await input.setValue(' Lorem ipsum dolar sit! ');
- });
-
- it('sets before unload warning', () => {
- const e = dispatchBeforeUnload();
-
- expect(e.preventDefault).toHaveBeenCalledTimes(1);
+ await findMarkdownEditor().vm.$emit('input', ' Lorem ipsum dolar sit! ');
});
describe('form submit', () => {
@@ -235,17 +218,12 @@ describe('WikiForm', () => {
await triggerFormSubmit();
});
- it('when form submitted, unsets before unload warning', () => {
- const e = dispatchBeforeUnload();
- expect(e.preventDefault).not.toHaveBeenCalled();
- });
-
it('triggers wiki format tracking event', () => {
expect(trackingSpy).toHaveBeenCalledTimes(1);
});
it('does not trim page content', () => {
- expect(findContent().element.value).toBe(' Lorem ipsum dolar sit! ');
+ expect(findMarkdownEditor().props().value).toBe(' Lorem ipsum dolar sit! ');
});
});
});
@@ -264,7 +242,7 @@ describe('WikiForm', () => {
createWrapper({ mountFn: mount });
await findTitle().setValue(title);
- await findContent().setValue(content);
+ await findMarkdownEditor().vm.$emit('input', content);
expect(findSubmitButton().props().disabled).toBe(disabledAttr);
},
@@ -296,208 +274,59 @@ describe('WikiForm', () => {
);
});
- describe('toggle editing mode control', () => {
- beforeEach(() => {
- createWrapper({ mountFn: mount });
- });
+ it.each`
+ format | enabled | action
+ ${'markdown'} | ${true} | ${'enables'}
+ ${'rdoc'} | ${false} | ${'disables'}
+ ${'asciidoc'} | ${false} | ${'disables'}
+ ${'org'} | ${false} | ${'disables'}
+ `('$action content editor when format is $format', async ({ format, enabled }) => {
+ createWrapper({ mountFn: mount });
- it.each`
- format | exists | action
- ${'markdown'} | ${true} | ${'displays'}
- ${'rdoc'} | ${false} | ${'hides'}
- ${'asciidoc'} | ${false} | ${'hides'}
- ${'org'} | ${false} | ${'hides'}
- `('$action toggle editing mode button when format is $format', async ({ format, exists }) => {
- await setFormat(format);
-
- expect(findToggleEditingModeButton().exists()).toBe(exists);
- });
+ setFormat(format);
- describe('when content editor is not active', () => {
- it('displays "Source" label in the toggle editing mode button', () => {
- expect(findToggleEditingModeButton().props().checked).toBe('source');
- });
+ await nextTick();
- describe('when clicking the toggle editing mode button', () => {
- beforeEach(async () => {
- await findToggleEditingModeButton().vm.$emit('input', 'richText');
- });
+ expect(findMarkdownEditor().props().enableContentEditor).toBe(enabled);
+ });
- it('hides the classic editor', () => {
- expect(findClassicEditor().exists()).toBe(false);
- });
+ describe('when markdown editor activates the content editor', () => {
+ beforeEach(async () => {
+ createWrapper({ mountFn: mount, persisted: true });
- it('shows the content editor', () => {
- expect(findContentEditor().exists()).toBe(true);
- });
- });
+ await findMarkdownEditor().vm.$emit('contentEditor');
});
- describe('markdown editor type persistance', () => {
- it('loads content editor by default if it is persisted in local storage', async () => {
- expect(findClassicEditor().exists()).toBe(true);
- expect(findContentEditor().exists()).toBe(false);
-
- // enable content editor
- await findLocalStorageSync().vm.$emit('input', 'richText');
-
- expect(findContentEditor().exists()).toBe(true);
- expect(findClassicEditor().exists()).toBe(false);
- });
+ it('disables the format dropdown', () => {
+ expect(findFormat().element.getAttribute('disabled')).toBeDefined();
});
- describe('when content editor is active', () => {
- beforeEach(() => {
- createWrapper();
- findToggleEditingModeButton().vm.$emit('input', 'richText');
- });
-
- it('displays "Edit Rich" label in the toggle editing mode button', () => {
- expect(findToggleEditingModeButton().props().checked).toBe('richText');
- });
-
- describe('when clicking the toggle editing mode button', () => {
- beforeEach(async () => {
- await findToggleEditingModeButton().vm.$emit('input', 'source');
- await nextTick();
- });
-
- it('hides the content editor', () => {
- expect(findContentEditor().exists()).toBe(false);
- });
-
- it('displays the classic editor', () => {
- expect(findClassicEditor().exists()).toBe(true);
- });
- });
-
- describe('when content editor is loading', () => {
- beforeEach(async () => {
- findContentEditor().vm.$emit('loading');
-
- await nextTick();
- });
-
- it('disables toggle editing mode button', () => {
- expect(findToggleEditingModeButton().attributes().disabled).toBe('true');
- });
-
- describe('when content editor loads successfully', () => {
- it('enables toggle editing mode button', async () => {
- findContentEditor().vm.$emit('loadingSuccess');
-
- await nextTick();
-
- expect(findToggleEditingModeButton().attributes().disabled).not.toBeDefined();
- });
- });
-
- describe('when content editor fails to load', () => {
- it('enables toggle editing mode button', async () => {
- findContentEditor().vm.$emit('loadingError');
-
- await nextTick();
-
- expect(findToggleEditingModeButton().attributes().disabled).not.toBeDefined();
- });
- });
+ it('sends tracking event when editor loads', async () => {
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, CONTENT_EDITOR_LOADED_ACTION, {
+ label: WIKI_CONTENT_EDITOR_TRACKING_LABEL,
});
});
- });
-
- describe('wiki content editor', () => {
- describe('clicking "Edit rich text": editor fails to load', () => {
- beforeEach(async () => {
- createWrapper({ mountFn: mount });
- mock.onPost(/preview-markdown/).reply(400);
-
- await findToggleEditingModeButton().vm.$emit('input', 'richText');
-
- // try waiting for content editor to load (but it will never actually load)
- await waitForPromises();
- });
-
- it('disables the submit button', () => {
- expect(findSubmitButton().props('disabled')).toBe(true);
- });
-
- describe('toggling editing modes to the classic editor', () => {
- beforeEach(() => {
- return findToggleEditingModeButton().vm.$emit('input', 'source');
- });
- it('switches to classic editor', () => {
- expect(findContentEditor().exists()).toBe(false);
- expect(findClassicEditor().exists()).toBe(true);
- });
- });
- });
+ describe('when triggering form submit', () => {
+ const updatedMarkdown = 'hello **world**';
- describe('clicking "Edit rich text": editor loads successfully', () => {
beforeEach(async () => {
- createWrapper({ persisted: true, mountFn: mount });
-
- mock.onPost(/preview-markdown/).reply(200, { body: '<p>hello <strong>world</strong></p>' });
-
- await findToggleEditingModeButton().vm.$emit('input', 'richText');
- await waitForPromises();
- });
-
- it('shows the rich text editor when loading finishes', async () => {
- expect(findContentEditor().exists()).toBe(true);
+ findMarkdownEditor().vm.$emit('input', updatedMarkdown);
+ await triggerFormSubmit();
});
- it('sends tracking event when editor loads', async () => {
- expect(trackingSpy).toHaveBeenCalledWith(undefined, CONTENT_EDITOR_LOADED_ACTION, {
+ it('triggers tracking events on form submit', async () => {
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, SAVED_USING_CONTENT_EDITOR_ACTION, {
label: WIKI_CONTENT_EDITOR_TRACKING_LABEL,
});
- });
-
- it('disables the format dropdown', () => {
- expect(findFormat().element.getAttribute('disabled')).toBeDefined();
- });
- describe('when wiki content is updated', () => {
- const updatedMarkdown = 'hello **world**';
-
- beforeEach(() => {
- findContentEditor().vm.$emit('change', {
- empty: false,
- changed: true,
- markdown: updatedMarkdown,
- });
- });
-
- it('sets before unload warning', () => {
- const e = dispatchBeforeUnload();
- expect(e.preventDefault).toHaveBeenCalledTimes(1);
- });
-
- it('unsets before unload warning on form submit', async () => {
- await triggerFormSubmit();
-
- const e = dispatchBeforeUnload();
- expect(e.preventDefault).not.toHaveBeenCalled();
- });
-
- it('triggers tracking events on form submit', async () => {
- await triggerFormSubmit();
- expect(trackingSpy).toHaveBeenCalledWith(undefined, SAVED_USING_CONTENT_EDITOR_ACTION, {
- label: WIKI_CONTENT_EDITOR_TRACKING_LABEL,
- });
-
- expect(trackingSpy).toHaveBeenCalledWith(undefined, WIKI_FORMAT_UPDATED_ACTION, {
- label: WIKI_FORMAT_LABEL,
- extra: {
- value: findFormat().element.value,
- old_format: pageInfoPersisted.format,
- project_path: pageInfoPersisted.path,
- },
- });
- });
-
- it('sets content field to the content editor updated markdown', async () => {
- expect(findContent().element.value).toBe(updatedMarkdown);
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, WIKI_FORMAT_UPDATED_ACTION, {
+ label: WIKI_FORMAT_LABEL,
+ extra: {
+ value: findFormat().element.value,
+ old_format: pageInfoPersisted.format,
+ project_path: pageInfoPersisted.path,
+ },
});
});
});
diff --git a/spec/frontend/pdf/page_spec.js b/spec/frontend/pdf/page_spec.js
index 07a7f1bb2ff..4cf83a3252d 100644
--- a/spec/frontend/pdf/page_spec.js
+++ b/spec/frontend/pdf/page_spec.js
@@ -1,17 +1,16 @@
-import Vue, { nextTick } from 'vue';
-import mountComponent from 'helpers/vue_mount_component_helper';
+import { nextTick } from 'vue';
+import { mount } from '@vue/test-utils';
import PageComponent from '~/pdf/page/index.vue';
jest.mock('pdfjs-dist/webpack', () => {
- return { default: jest.requireActual('pdfjs-dist/build/pdf') };
+ return { default: jest.requireActual('pdfjs-dist/legacy/build/pdf') };
});
describe('Page component', () => {
- const Component = Vue.extend(PageComponent);
- let vm;
+ let wrapper;
afterEach(() => {
- vm.$destroy();
+ wrapper.destroy();
});
it('renders the page when mounting', async () => {
@@ -20,16 +19,18 @@ describe('Page component', () => {
getViewport: jest.fn().mockReturnValue({}),
};
- vm = mountComponent(Component, {
- page: testPage,
- number: 1,
+ wrapper = mount(PageComponent, {
+ propsData: {
+ page: testPage,
+ number: 1,
+ },
});
- expect(vm.rendering).toBe(true);
-
await nextTick();
- expect(testPage.render).toHaveBeenCalledWith(vm.renderContext);
- expect(vm.rendering).toBe(false);
+ expect(testPage.render).toHaveBeenCalledWith({
+ canvasContext: wrapper.find('canvas').element.getContext('2d'),
+ viewport: testPage.getViewport(),
+ });
});
});
diff --git a/spec/frontend/performance_bar/components/request_warning_spec.js b/spec/frontend/performance_bar/components/request_warning_spec.js
index d558c7b018a..9dd8ea9f933 100644
--- a/spec/frontend/performance_bar/components/request_warning_spec.js
+++ b/spec/frontend/performance_bar/components/request_warning_spec.js
@@ -2,14 +2,21 @@ import { shallowMount } from '@vue/test-utils';
import RequestWarning from '~/performance_bar/components/request_warning.vue';
describe('request warning', () => {
+ let wrapper;
const htmlId = 'request-123';
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
describe('when the request has warnings', () => {
- const wrapper = shallowMount(RequestWarning, {
- propsData: {
- htmlId,
- warnings: ['gitaly calls: 30 over 10', 'gitaly duration: 1500 over 1000'],
- },
+ beforeEach(() => {
+ wrapper = shallowMount(RequestWarning, {
+ propsData: {
+ htmlId,
+ warnings: ['gitaly calls: 30 over 10', 'gitaly duration: 1500 over 1000'],
+ },
+ });
});
it('adds a warning emoji with the correct ID', () => {
@@ -19,11 +26,13 @@ describe('request warning', () => {
});
describe('when the request does not have warnings', () => {
- const wrapper = shallowMount(RequestWarning, {
- propsData: {
- htmlId,
- warnings: [],
- },
+ beforeEach(() => {
+ wrapper = shallowMount(RequestWarning, {
+ propsData: {
+ htmlId,
+ warnings: [],
+ },
+ });
});
it('does nothing', () => {
diff --git a/spec/frontend/persistent_user_callout_spec.js b/spec/frontend/persistent_user_callout_spec.js
index 9cd5bb9e9a1..c9574208900 100644
--- a/spec/frontend/persistent_user_callout_spec.js
+++ b/spec/frontend/persistent_user_callout_spec.js
@@ -1,7 +1,7 @@
import MockAdapter from 'axios-mock-adapter';
import { useMockLocationHelper } from 'helpers/mock_window_location_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import PersistentUserCallout from '~/persistent_user_callout';
@@ -108,7 +108,7 @@ describe('PersistentUserCallout', () => {
await waitForPromises();
expect(persistentUserCallout.container.remove).not.toHaveBeenCalled();
- expect(createFlash).toHaveBeenCalledWith({
+ expect(createAlert).toHaveBeenCalledWith({
message: 'An error occurred while dismissing the alert. Refresh the page and try again.',
});
});
@@ -214,7 +214,7 @@ describe('PersistentUserCallout', () => {
await waitForPromises();
expect(window.location.assign).not.toHaveBeenCalled();
- expect(createFlash).toHaveBeenCalledWith({
+ expect(createAlert).toHaveBeenCalledWith({
message:
'An error occurred while acknowledging the notification. Refresh the page and try again.',
});
diff --git a/spec/frontend/pipeline_editor/components/file-nav/branch_switcher_spec.js b/spec/frontend/pipeline_editor/components/file-nav/branch_switcher_spec.js
index 8f6f4d8cff9..f0347ad19ac 100644
--- a/spec/frontend/pipeline_editor/components/file-nav/branch_switcher_spec.js
+++ b/spec/frontend/pipeline_editor/components/file-nav/branch_switcher_spec.js
@@ -360,7 +360,7 @@ describe('Pipeline editor branch switcher', () => {
});
describe('loading icon', () => {
- test.each`
+ it.each`
isQueryLoading | isRendered
${true} | ${true}
${false} | ${false}
diff --git a/spec/frontend/pipeline_editor/components/ui/pipeline_editor_empty_state_spec.js b/spec/frontend/pipeline_editor/components/ui/pipeline_editor_empty_state_spec.js
index 8e0a73b6e7c..c76c3460e99 100644
--- a/spec/frontend/pipeline_editor/components/ui/pipeline_editor_empty_state_spec.js
+++ b/spec/frontend/pipeline_editor/components/ui/pipeline_editor_empty_state_spec.js
@@ -7,6 +7,7 @@ describe('Pipeline editor empty state', () => {
let wrapper;
const defaultProvide = {
emptyStateIllustrationPath: 'my/svg/path',
+ usesExternalConfig: false,
};
const createComponent = ({ provide } = {}) => {
@@ -18,6 +19,7 @@ describe('Pipeline editor empty state', () => {
const findFileNav = () => wrapper.findComponent(PipelineEditorFileNav);
const findSvgImage = () => wrapper.find('img');
const findTitle = () => wrapper.find('h1');
+ const findExternalCiInstructions = () => wrapper.find('p');
const findConfirmButton = () => wrapper.findComponent(GlButton);
const findDescription = () => wrapper.findComponent(GlSprintf);
@@ -25,7 +27,33 @@ describe('Pipeline editor empty state', () => {
wrapper.destroy();
});
- describe('template', () => {
+ describe('when project uses an external CI config', () => {
+ beforeEach(() => {
+ createComponent({
+ provide: { usesExternalConfig: true },
+ });
+ });
+
+ it('renders an svg image', () => {
+ expect(findSvgImage().exists()).toBe(true);
+ });
+
+ it('renders the correct title and instructions', () => {
+ expect(findTitle().exists()).toBe(true);
+ expect(findExternalCiInstructions().exists()).toBe(true);
+
+ expect(findExternalCiInstructions().html()).toContain(
+ wrapper.vm.$options.i18n.externalCiInstructions,
+ );
+ expect(findTitle().text()).toBe(wrapper.vm.$options.i18n.externalCiNote);
+ });
+
+ it('does not render the CTA button', () => {
+ expect(findConfirmButton().exists()).toBe(false);
+ });
+ });
+
+ describe('when project uses an accessible CI config', () => {
beforeEach(() => {
createComponent();
});
diff --git a/spec/frontend/pipeline_editor/pipeline_editor_app_spec.js b/spec/frontend/pipeline_editor/pipeline_editor_app_spec.js
index 1989f23a415..9fe1536d3f5 100644
--- a/spec/frontend/pipeline_editor/pipeline_editor_app_spec.js
+++ b/spec/frontend/pipeline_editor/pipeline_editor_app_spec.js
@@ -55,11 +55,12 @@ jest.mock('~/lib/utils/url_utility', () => ({
const localVue = createLocalVue();
localVue.use(VueApollo);
-const mockProvide = {
+const defaultProvide = {
ciConfigPath: mockCiConfigPath,
defaultBranch: mockDefaultBranch,
newMergeRequestPath: mockNewMergeRequestPath,
projectFullPath: mockProjectFullPath,
+ usesExternalConfig: false,
};
describe('Pipeline editor app component', () => {
@@ -79,7 +80,7 @@ describe('Pipeline editor app component', () => {
stubs = {},
} = {}) => {
wrapper = shallowMount(PipelineEditorApp, {
- provide: { ...mockProvide, ...provide },
+ provide: { ...defaultProvide, ...provide },
stubs,
mocks: {
$apollo: {
@@ -229,6 +230,22 @@ describe('Pipeline editor app component', () => {
mockLatestCommitShaQuery.mockResolvedValue(mockCommitShaResults);
});
+ describe('when project uses an external CI config file', () => {
+ beforeEach(async () => {
+ await createComponentWithApollo({
+ provide: {
+ usesExternalConfig: true,
+ },
+ });
+ });
+
+ it('shows an empty state and does not show editor home component', () => {
+ expect(findEmptyState().exists()).toBe(true);
+ expect(findAlert().exists()).toBe(false);
+ expect(findEditorHome().exists()).toBe(false);
+ });
+ });
+
describe('when file exists', () => {
beforeEach(async () => {
await createComponentWithApollo();
diff --git a/spec/frontend/pipeline_editor/pipeline_editor_home_spec.js b/spec/frontend/pipeline_editor/pipeline_editor_home_spec.js
index e317d1ddcc2..2b06660c4b3 100644
--- a/spec/frontend/pipeline_editor/pipeline_editor_home_spec.js
+++ b/spec/frontend/pipeline_editor/pipeline_editor_home_spec.js
@@ -149,20 +149,20 @@ describe('Pipeline editor home wrapper', () => {
await nextTick();
- expect(findCommitSection().exists()).toBe(shouldShow);
+ expect(findCommitSection().isVisible()).toBe(shouldShow);
},
);
it('shows the commit form again when coming back to the create tab', async () => {
- expect(findCommitSection().exists()).toBe(true);
+ expect(findCommitSection().isVisible()).toBe(true);
findPipelineEditorTabs().vm.$emit('set-current-tab', MERGED_TAB);
await nextTick();
- expect(findCommitSection().exists()).toBe(false);
+ expect(findCommitSection().isVisible()).toBe(false);
findPipelineEditorTabs().vm.$emit('set-current-tab', CREATE_TAB);
await nextTick();
- expect(findCommitSection().exists()).toBe(true);
+ expect(findCommitSection().isVisible()).toBe(true);
});
describe('rendering with tab params', () => {
@@ -178,7 +178,7 @@ describe('Pipeline editor home wrapper', () => {
setWindowLocation(`https://gitlab.test/ci/editor/?tab=${TABS_INDEX[tab]}`);
await createComponent({ stubs: { PipelineEditorTabs } });
- expect(findCommitSection().exists()).toBe(shouldShow);
+ expect(findCommitSection().isVisible()).toBe(shouldShow);
},
);
});
diff --git a/spec/frontend/pipeline_new/components/pipeline_new_form_spec.js b/spec/frontend/pipeline_new/components/pipeline_new_form_spec.js
index 5ce29bd6c5d..3e699b93fd3 100644
--- a/spec/frontend/pipeline_new/components/pipeline_new_form_spec.js
+++ b/spec/frontend/pipeline_new/components/pipeline_new_form_spec.js
@@ -1,72 +1,101 @@
-import { GlForm, GlSprintf, GlLoadingIcon } from '@gitlab/ui';
-import { mount, shallowMount } from '@vue/test-utils';
+import Vue, { nextTick } from 'vue';
+import VueApollo from 'vue-apollo';
+import { GlForm, GlDropdownItem, GlSprintf, GlLoadingIcon } from '@gitlab/ui';
import MockAdapter from 'axios-mock-adapter';
-import { nextTick } from 'vue';
import CreditCardValidationRequiredAlert from 'ee_component/billings/components/cc_validation_required_alert.vue';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import { shallowMountExtended, mountExtended } from 'helpers/vue_test_utils_helper';
import { TEST_HOST } from 'helpers/test_constants';
import waitForPromises from 'helpers/wait_for_promises';
import axios from '~/lib/utils/axios_utils';
import httpStatusCodes from '~/lib/utils/http_status';
import { redirectTo } from '~/lib/utils/url_utility';
import PipelineNewForm from '~/pipeline_new/components/pipeline_new_form.vue';
+import ciConfigVariablesQuery from '~/pipeline_new/graphql/queries/ci_config_variables.graphql';
+import { resolvers } from '~/pipeline_new/graphql/resolvers';
import RefsDropdown from '~/pipeline_new/components/refs_dropdown.vue';
import {
+ mockCreditCardValidationRequiredError,
+ mockCiConfigVariablesResponse,
+ mockCiConfigVariablesResponseWithoutDesc,
+ mockEmptyCiConfigVariablesResponse,
+ mockError,
mockQueryParams,
mockPostParams,
mockProjectId,
- mockError,
mockRefs,
- mockCreditCardValidationRequiredError,
+ mockYamlVariables,
} from '../mock_data';
+Vue.use(VueApollo);
+
jest.mock('~/lib/utils/url_utility', () => ({
redirectTo: jest.fn(),
}));
const projectRefsEndpoint = '/root/project/refs';
const pipelinesPath = '/root/project/-/pipelines';
-const configVariablesPath = '/root/project/-/pipelines/config_variables';
+const projectPath = '/root/project/-/pipelines/config_variables';
const newPipelinePostResponse = { id: 1 };
const defaultBranch = 'main';
describe('Pipeline New Form', () => {
let wrapper;
let mock;
+ let mockApollo;
+ let mockCiConfigVariables;
let dummySubmitEvent;
const findForm = () => wrapper.findComponent(GlForm);
const findRefsDropdown = () => wrapper.findComponent(RefsDropdown);
- const findSubmitButton = () => wrapper.find('[data-testid="run_pipeline_button"]');
- const findVariableRows = () => wrapper.findAll('[data-testid="ci-variable-row"]');
- const findRemoveIcons = () => wrapper.findAll('[data-testid="remove-ci-variable-row"]');
- const findDropdowns = () => wrapper.findAll('[data-testid="pipeline-form-ci-variable-type"]');
- const findKeyInputs = () => wrapper.findAll('[data-testid="pipeline-form-ci-variable-key"]');
- const findValueInputs = () => wrapper.findAll('[data-testid="pipeline-form-ci-variable-value"]');
- const findErrorAlert = () => wrapper.find('[data-testid="run-pipeline-error-alert"]');
- const findWarningAlert = () => wrapper.find('[data-testid="run-pipeline-warning-alert"]');
+ const findSubmitButton = () => wrapper.findByTestId('run_pipeline_button');
+ const findVariableRows = () => wrapper.findAllByTestId('ci-variable-row');
+ const findRemoveIcons = () => wrapper.findAllByTestId('remove-ci-variable-row');
+ const findVariableTypes = () => wrapper.findAllByTestId('pipeline-form-ci-variable-type');
+ const findKeyInputs = () => wrapper.findAllByTestId('pipeline-form-ci-variable-key');
+ const findValueInputs = () => wrapper.findAllByTestId('pipeline-form-ci-variable-value');
+ const findValueDropdowns = () =>
+ wrapper.findAllByTestId('pipeline-form-ci-variable-value-dropdown');
+ const findValueDropdownItems = (dropdown) => dropdown.findAllComponents(GlDropdownItem);
+ const findErrorAlert = () => wrapper.findByTestId('run-pipeline-error-alert');
+ const findWarningAlert = () => wrapper.findByTestId('run-pipeline-warning-alert');
const findWarningAlertSummary = () => findWarningAlert().findComponent(GlSprintf);
- const findWarnings = () => wrapper.findAll('[data-testid="run-pipeline-warning"]');
+ const findWarnings = () => wrapper.findAllByTestId('run-pipeline-warning');
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findCCAlert = () => wrapper.findComponent(CreditCardValidationRequiredAlert);
const getFormPostParams = () => JSON.parse(mock.history.post[0].data);
- const selectBranch = (branch) => {
+ const selectBranch = async (branch) => {
// Select a branch in the dropdown
findRefsDropdown().vm.$emit('input', {
shortName: branch,
fullName: `refs/heads/${branch}`,
});
+
+ await waitForPromises();
+ };
+
+ const changeKeyInputValue = async (keyInputIndex, value) => {
+ const input = findKeyInputs().at(keyInputIndex);
+ input.element.value = value;
+ input.trigger('change');
+
+ await nextTick();
};
- const createComponent = (props = {}, method = shallowMount) => {
+ const createComponentWithApollo = ({ method = shallowMountExtended, props = {} } = {}) => {
+ const handlers = [[ciConfigVariablesQuery, mockCiConfigVariables]];
+ mockApollo = createMockApollo(handlers, resolvers);
+
wrapper = method(PipelineNewForm, {
+ apolloProvider: mockApollo,
provide: {
projectRefsEndpoint,
},
propsData: {
projectId: mockProjectId,
pipelinesPath,
- configVariablesPath,
+ projectPath,
defaultBranch,
refParam: defaultBranch,
settingsLink: '',
@@ -78,7 +107,7 @@ describe('Pipeline New Form', () => {
beforeEach(() => {
mock = new MockAdapter(axios);
- mock.onGet(configVariablesPath).reply(httpStatusCodes.OK, {});
+ mockCiConfigVariables = jest.fn();
mock.onGet(projectRefsEndpoint).reply(httpStatusCodes.OK, mockRefs);
dummySubmitEvent = {
@@ -87,24 +116,20 @@ describe('Pipeline New Form', () => {
});
afterEach(() => {
- wrapper.destroy();
- wrapper = null;
-
mock.restore();
+ wrapper.destroy();
});
describe('Form', () => {
beforeEach(async () => {
- createComponent(mockQueryParams, mount);
-
- mock.onPost(pipelinesPath).reply(httpStatusCodes.OK, newPipelinePostResponse);
-
+ mockCiConfigVariables.mockResolvedValue(mockEmptyCiConfigVariablesResponse);
+ createComponentWithApollo({ props: mockQueryParams, method: mountExtended });
await waitForPromises();
});
it('displays the correct values for the provided query params', async () => {
- expect(findDropdowns().at(0).props('text')).toBe('Variable');
- expect(findDropdowns().at(1).props('text')).toBe('File');
+ expect(findVariableTypes().at(0).props('text')).toBe('Variable');
+ expect(findVariableTypes().at(1).props('text')).toBe('File');
expect(findRefsDropdown().props('value')).toEqual({ shortName: 'tag-1' });
expect(findVariableRows()).toHaveLength(3);
});
@@ -117,7 +142,7 @@ describe('Pipeline New Form', () => {
it('displays an empty variable for the user to fill out', async () => {
expect(findKeyInputs().at(2).element.value).toBe('');
expect(findValueInputs().at(2).element.value).toBe('');
- expect(findDropdowns().at(2).props('text')).toBe('Variable');
+ expect(findVariableTypes().at(2).props('text')).toBe('Variable');
});
it('does not display remove icon for last row', () => {
@@ -147,13 +172,12 @@ describe('Pipeline New Form', () => {
describe('Pipeline creation', () => {
beforeEach(async () => {
+ mockCiConfigVariables.mockResolvedValue(mockEmptyCiConfigVariablesResponse);
mock.onPost(pipelinesPath).reply(httpStatusCodes.OK, newPipelinePostResponse);
-
- await waitForPromises();
});
it('does not submit the native HTML form', async () => {
- createComponent();
+ createComponentWithApollo();
findForm().vm.$emit('submit', dummySubmitEvent);
@@ -161,7 +185,7 @@ describe('Pipeline New Form', () => {
});
it('disables the submit button immediately after submitting', async () => {
- createComponent();
+ createComponentWithApollo();
expect(findSubmitButton().props('disabled')).toBe(false);
@@ -172,7 +196,7 @@ describe('Pipeline New Form', () => {
});
it('creates pipeline with full ref and variables', async () => {
- createComponent();
+ createComponentWithApollo();
findForm().vm.$emit('submit', dummySubmitEvent);
await waitForPromises();
@@ -182,7 +206,7 @@ describe('Pipeline New Form', () => {
});
it('creates a pipeline with short ref and variables from the query params', async () => {
- createComponent(mockQueryParams);
+ createComponentWithApollo({ props: mockQueryParams });
await waitForPromises();
@@ -197,64 +221,51 @@ describe('Pipeline New Form', () => {
describe('When the ref has been changed', () => {
beforeEach(async () => {
- createComponent({}, mount);
+ mockCiConfigVariables.mockResolvedValue(mockEmptyCiConfigVariablesResponse);
+ createComponentWithApollo({ method: mountExtended });
await waitForPromises();
});
- it('variables persist between ref changes', async () => {
- selectBranch('main');
-
- await waitForPromises();
- const mainInput = findKeyInputs().at(0);
- mainInput.element.value = 'build_var';
- mainInput.trigger('change');
+ it('variables persist between ref changes', async () => {
+ await selectBranch('main');
+ await changeKeyInputValue(0, 'build_var');
- await nextTick();
+ await selectBranch('branch-1');
+ await changeKeyInputValue(0, 'deploy_var');
- selectBranch('branch-1');
+ await selectBranch('main');
- await waitForPromises();
+ expect(findKeyInputs().at(0).element.value).toBe('build_var');
+ expect(findVariableRows().length).toBe(2);
- const branchOneInput = findKeyInputs().at(0);
- branchOneInput.element.value = 'deploy_var';
- branchOneInput.trigger('change');
+ await selectBranch('branch-1');
- await nextTick();
+ expect(findKeyInputs().at(0).element.value).toBe('deploy_var');
+ expect(findVariableRows().length).toBe(2);
+ });
- selectBranch('main');
+ it('skips query call when form variables are already cached', async () => {
+ await selectBranch('main');
+ await changeKeyInputValue(0, 'build_var');
- await waitForPromises();
+ expect(mockCiConfigVariables).toHaveBeenCalledTimes(1);
- expect(findKeyInputs().at(0).element.value).toBe('build_var');
- expect(findVariableRows().length).toBe(2);
+ await selectBranch('branch-1');
- selectBranch('branch-1');
+ expect(mockCiConfigVariables).toHaveBeenCalledTimes(2);
- await waitForPromises();
+ // no additional call since `main` form values have been cached
+ await selectBranch('main');
- expect(findKeyInputs().at(0).element.value).toBe('deploy_var');
- expect(findVariableRows().length).toBe(2);
+ expect(mockCiConfigVariables).toHaveBeenCalledTimes(2);
});
});
describe('when yml defines a variable', () => {
- const mockYmlKey = 'yml_var';
- const mockYmlValue = 'yml_var_val';
- const mockYmlMultiLineValue = `A value
- with multiple
- lines`;
- const mockYmlDesc = 'A var from yml.';
-
it('loading icon is shown when content is requested and hidden when received', async () => {
- createComponent(mockQueryParams, mount);
-
- mock.onGet(configVariablesPath).reply(httpStatusCodes.OK, {
- [mockYmlKey]: {
- value: mockYmlValue,
- description: mockYmlDesc,
- },
- });
+ mockCiConfigVariables.mockResolvedValue(mockEmptyCiConfigVariablesResponse);
+ createComponentWithApollo({ props: mockQueryParams, method: mountExtended });
expect(findLoadingIcon().exists()).toBe(true);
@@ -263,51 +274,62 @@ describe('Pipeline New Form', () => {
expect(findLoadingIcon().exists()).toBe(false);
});
- it('multi-line strings are added to the value field without removing line breaks', async () => {
- createComponent(mockQueryParams, mount);
+ describe('with different predefined values', () => {
+ beforeEach(async () => {
+ mockCiConfigVariables.mockResolvedValue(mockCiConfigVariablesResponse);
+ createComponentWithApollo({ method: mountExtended });
+ await waitForPromises();
+ });
+
+ it('multi-line strings are added to the value field without removing line breaks', () => {
+ expect(findValueInputs().at(1).element.value).toBe(mockYamlVariables[1].value);
+ });
- mock.onGet(configVariablesPath).reply(httpStatusCodes.OK, {
- [mockYmlKey]: {
- value: mockYmlMultiLineValue,
- description: mockYmlDesc,
- },
+ it('multiple predefined values are rendered as a dropdown', () => {
+ const dropdown = findValueDropdowns().at(0);
+ const dropdownItems = findValueDropdownItems(dropdown);
+ const { valueOptions } = mockYamlVariables[2];
+
+ expect(dropdownItems.at(0).text()).toBe(valueOptions[0]);
+ expect(dropdownItems.at(1).text()).toBe(valueOptions[1]);
+ expect(dropdownItems.at(2).text()).toBe(valueOptions[2]);
});
- await waitForPromises();
+ it('variables with multiple predefined values sets the first option as the default', () => {
+ const dropdown = findValueDropdowns().at(0);
+ const { valueOptions } = mockYamlVariables[2];
- expect(findValueInputs().at(0).element.value).toBe(mockYmlMultiLineValue);
+ expect(dropdown.props('text')).toBe(valueOptions[0]);
+ });
});
describe('with description', () => {
beforeEach(async () => {
- createComponent(mockQueryParams, mount);
-
- mock.onGet(configVariablesPath).reply(httpStatusCodes.OK, {
- [mockYmlKey]: {
- value: mockYmlValue,
- description: mockYmlDesc,
- },
- });
-
+ mockCiConfigVariables.mockResolvedValue(mockCiConfigVariablesResponse);
+ createComponentWithApollo({ props: mockQueryParams, method: mountExtended });
await waitForPromises();
});
it('displays all the variables', async () => {
- expect(findVariableRows()).toHaveLength(4);
+ expect(findVariableRows()).toHaveLength(6);
});
it('displays a variable from yml', () => {
- expect(findKeyInputs().at(0).element.value).toBe(mockYmlKey);
- expect(findValueInputs().at(0).element.value).toBe(mockYmlValue);
+ expect(findKeyInputs().at(0).element.value).toBe(mockYamlVariables[0].key);
+ expect(findValueInputs().at(0).element.value).toBe(mockYamlVariables[0].value);
});
it('displays a variable from provided query params', () => {
- expect(findKeyInputs().at(1).element.value).toBe('test_var');
- expect(findValueInputs().at(1).element.value).toBe('test_var_val');
+ expect(findKeyInputs().at(3).element.value).toBe(
+ Object.keys(mockQueryParams.variableParams)[0],
+ );
+ expect(findValueInputs().at(3).element.value).toBe(
+ Object.values(mockQueryParams.fileParams)[0],
+ );
});
it('adds a description to the first variable from yml', () => {
- expect(findVariableRows().at(0).text()).toContain(mockYmlDesc);
+ expect(findVariableRows().at(0).text()).toContain(mockYamlVariables[0].description);
});
it('removes the description when a variable key changes', async () => {
@@ -316,39 +338,27 @@ describe('Pipeline New Form', () => {
await nextTick();
- expect(findVariableRows().at(0).text()).not.toContain(mockYmlDesc);
+ expect(findVariableRows().at(0).text()).not.toContain(mockYamlVariables[0].description);
});
});
describe('without description', () => {
beforeEach(async () => {
- createComponent(mockQueryParams, mount);
-
- mock.onGet(configVariablesPath).reply(httpStatusCodes.OK, {
- [mockYmlKey]: {
- value: mockYmlValue,
- description: null,
- },
- yml_var2: {
- value: 'yml_var2_val',
- },
- yml_var3: {
- description: '',
- },
- });
-
+ mockCiConfigVariables.mockResolvedValue(mockCiConfigVariablesResponseWithoutDesc);
+ createComponentWithApollo({ method: mountExtended });
await waitForPromises();
});
- it('displays all the variables', async () => {
- expect(findVariableRows()).toHaveLength(3);
+ it('displays variables with description only', async () => {
+ expect(findVariableRows()).toHaveLength(2); // extra empty variable is added at the end
});
});
});
describe('Form errors and warnings', () => {
beforeEach(() => {
- createComponent();
+ mockCiConfigVariables.mockResolvedValue(mockEmptyCiConfigVariablesResponse);
+ createComponentWithApollo();
});
describe('when the refs cannot be loaded', () => {
diff --git a/spec/frontend/pipeline_new/mock_data.js b/spec/frontend/pipeline_new/mock_data.js
index e99684ff417..e95a65171fc 100644
--- a/spec/frontend/pipeline_new/mock_data.js
+++ b/spec/frontend/pipeline_new/mock_data.js
@@ -65,3 +65,62 @@ export const mockVariables = [
},
{ uniqueId: 'var-refs/heads/main4', variable_type: 'env_var', key: '', value: '' },
];
+
+export const mockYamlVariables = [
+ {
+ description: 'This is a variable with a value.',
+ key: 'VAR_WITH_VALUE',
+ value: 'test_value',
+ valueOptions: null,
+ },
+ {
+ description: 'This is a variable with a multi-line value.',
+ key: 'VAR_WITH_MULTILINE',
+ value: `this is
+ a multiline value`,
+ valueOptions: null,
+ },
+ {
+ description: 'This is a variable with predefined values.',
+ key: 'VAR_WITH_OPTIONS',
+ value: 'development',
+ valueOptions: ['development', 'staging', 'production'],
+ },
+];
+
+export const mockYamlVariablesWithoutDesc = [
+ {
+ description: 'This is a variable with a value.',
+ key: 'VAR_WITH_VALUE',
+ value: 'test_value',
+ valueOptions: null,
+ },
+ {
+ description: null,
+ key: 'VAR_WITH_MULTILINE',
+ value: `this is
+ a multiline value`,
+ valueOptions: null,
+ },
+ {
+ description: null,
+ key: 'VAR_WITH_OPTIONS',
+ value: 'development',
+ valueOptions: ['development', 'staging', 'production'],
+ },
+];
+
+export const mockCiConfigVariablesQueryResponse = (ciConfigVariables) => ({
+ data: {
+ project: {
+ id: 1,
+ ciConfigVariables,
+ },
+ },
+});
+
+export const mockCiConfigVariablesResponse = mockCiConfigVariablesQueryResponse(mockYamlVariables);
+export const mockEmptyCiConfigVariablesResponse = mockCiConfigVariablesQueryResponse([]);
+export const mockCiConfigVariablesResponseWithoutDesc = mockCiConfigVariablesQueryResponse(
+ mockYamlVariablesWithoutDesc,
+);
diff --git a/spec/frontend/pipeline_schedules/components/pipeline_schedules_form_spec.js b/spec/frontend/pipeline_schedules/components/pipeline_schedules_form_spec.js
new file mode 100644
index 00000000000..4b5a9611251
--- /dev/null
+++ b/spec/frontend/pipeline_schedules/components/pipeline_schedules_form_spec.js
@@ -0,0 +1,25 @@
+import { shallowMount } from '@vue/test-utils';
+import { GlForm } from '@gitlab/ui';
+import PipelineSchedulesForm from '~/pipeline_schedules/components/pipeline_schedules_form.vue';
+
+describe('Pipeline schedules form', () => {
+ let wrapper;
+
+ const createComponent = () => {
+ wrapper = shallowMount(PipelineSchedulesForm);
+ };
+
+ const findForm = () => wrapper.findComponent(GlForm);
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('displays form', () => {
+ expect(findForm().exists()).toBe(true);
+ });
+});
diff --git a/spec/frontend/pipeline_schedules/components/pipeline_schedules_spec.js b/spec/frontend/pipeline_schedules/components/pipeline_schedules_spec.js
new file mode 100644
index 00000000000..cce8f480928
--- /dev/null
+++ b/spec/frontend/pipeline_schedules/components/pipeline_schedules_spec.js
@@ -0,0 +1,161 @@
+import { GlAlert, GlLoadingIcon, GlModal } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import Vue, { nextTick } from 'vue';
+import VueApollo from 'vue-apollo';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import PipelineSchedules from '~/pipeline_schedules/components/pipeline_schedules.vue';
+import PipelineSchedulesTable from '~/pipeline_schedules/components/table/pipeline_schedules_table.vue';
+import deletePipelineScheduleMutation from '~/pipeline_schedules/graphql/mutations/delete_pipeline_schedule.mutation.graphql';
+import getPipelineSchedulesQuery from '~/pipeline_schedules/graphql/queries/get_pipeline_schedules.query.graphql';
+import {
+ mockGetPipelineSchedulesGraphQLResponse,
+ mockPipelineScheduleNodes,
+ deleteMutationResponse,
+} from '../mock_data';
+
+Vue.use(VueApollo);
+
+describe('Pipeline schedules app', () => {
+ let wrapper;
+
+ const successHandler = jest.fn().mockResolvedValue(mockGetPipelineSchedulesGraphQLResponse);
+ const failedHandler = jest.fn().mockRejectedValue(new Error('GraphQL error'));
+
+ const deleteMutationHandlerSuccess = jest.fn().mockResolvedValue(deleteMutationResponse);
+ const deleteMutationHandlerFailed = jest.fn().mockRejectedValue(new Error('GraphQL error'));
+
+ const createMockApolloProvider = (
+ requestHandlers = [[getPipelineSchedulesQuery, successHandler]],
+ ) => {
+ return createMockApollo(requestHandlers);
+ };
+
+ const createComponent = (requestHandlers) => {
+ wrapper = shallowMount(PipelineSchedules, {
+ provide: {
+ fullPath: 'gitlab-org/gitlab',
+ },
+ apolloProvider: createMockApolloProvider(requestHandlers),
+ });
+ };
+
+ const findTable = () => wrapper.findComponent(PipelineSchedulesTable);
+ const findAlert = () => wrapper.findComponent(GlAlert);
+ const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
+ const findModal = () => wrapper.findComponent(GlModal);
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('displays table', async () => {
+ createComponent();
+
+ await waitForPromises();
+
+ expect(findTable().exists()).toBe(true);
+ expect(findAlert().exists()).toBe(false);
+ });
+
+ it('fetches query and passes an array of pipeline schedules', async () => {
+ createComponent();
+
+ expect(successHandler).toHaveBeenCalled();
+
+ await waitForPromises();
+
+ expect(findTable().props('schedules')).toEqual(mockPipelineScheduleNodes);
+ });
+
+ it('handles loading state', async () => {
+ createComponent();
+
+ expect(findLoadingIcon().exists()).toBe(true);
+
+ await waitForPromises();
+
+ expect(findLoadingIcon().exists()).toBe(false);
+ });
+
+ it('shows query error alert', async () => {
+ createComponent([[getPipelineSchedulesQuery, failedHandler]]);
+
+ await waitForPromises();
+
+ expect(findAlert().text()).toBe('There was a problem fetching pipeline schedules.');
+ });
+
+ it('shows delete mutation error alert', async () => {
+ createComponent([
+ [getPipelineSchedulesQuery, successHandler],
+ [deletePipelineScheduleMutation, deleteMutationHandlerFailed],
+ ]);
+
+ await waitForPromises();
+
+ findModal().vm.$emit('primary');
+
+ await waitForPromises();
+
+ expect(findAlert().text()).toBe('There was a problem deleting the pipeline schedule.');
+ });
+
+ it('deletes pipeline schedule and refetches query', async () => {
+ createComponent([
+ [getPipelineSchedulesQuery, successHandler],
+ [deletePipelineScheduleMutation, deleteMutationHandlerSuccess],
+ ]);
+
+ jest.spyOn(wrapper.vm.$apollo.queries.schedules, 'refetch');
+
+ await waitForPromises();
+
+ const scheduleId = mockPipelineScheduleNodes[0].id;
+
+ findTable().vm.$emit('showDeleteModal', scheduleId);
+
+ expect(wrapper.vm.$apollo.queries.schedules.refetch).not.toHaveBeenCalled();
+
+ findModal().vm.$emit('primary');
+
+ await waitForPromises();
+
+ expect(deleteMutationHandlerSuccess).toHaveBeenCalledWith({
+ id: scheduleId,
+ });
+ expect(wrapper.vm.$apollo.queries.schedules.refetch).toHaveBeenCalled();
+ });
+
+ it('modal should be visible after event', async () => {
+ createComponent();
+
+ await waitForPromises();
+
+ expect(findModal().props('visible')).toBe(false);
+
+ findTable().vm.$emit('showDeleteModal', mockPipelineScheduleNodes[0].id);
+
+ await nextTick();
+
+ expect(findModal().props('visible')).toBe(true);
+ });
+
+ it('modal should be hidden', async () => {
+ createComponent();
+
+ await waitForPromises();
+
+ findTable().vm.$emit('showDeleteModal', mockPipelineScheduleNodes[0].id);
+
+ await nextTick();
+
+ expect(findModal().props('visible')).toBe(true);
+
+ findModal().vm.$emit('hide');
+
+ await nextTick();
+
+ expect(findModal().props('visible')).toBe(false);
+ });
+});
diff --git a/spec/frontend/pipeline_schedules/components/table/cells/pipeline_schedule_actions_spec.js b/spec/frontend/pipeline_schedules/components/table/cells/pipeline_schedule_actions_spec.js
new file mode 100644
index 00000000000..ecc1bdeb679
--- /dev/null
+++ b/spec/frontend/pipeline_schedules/components/table/cells/pipeline_schedule_actions_spec.js
@@ -0,0 +1,49 @@
+import { GlButton } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import PipelineScheduleActions from '~/pipeline_schedules/components/table/cells/pipeline_schedule_actions.vue';
+import { mockPipelineScheduleNodes, mockPipelineScheduleAsGuestNodes } from '../../../mock_data';
+
+describe('Pipeline schedule actions', () => {
+ let wrapper;
+
+ const defaultProps = {
+ schedule: mockPipelineScheduleNodes[0],
+ };
+
+ const createComponent = (props = defaultProps) => {
+ wrapper = shallowMountExtended(PipelineScheduleActions, {
+ propsData: {
+ ...props,
+ },
+ });
+ };
+
+ const findAllButtons = () => wrapper.findAllComponents(GlButton);
+ const findDeleteBtn = () => wrapper.findByTestId('delete-pipeline-schedule-btn');
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('displays action buttons', () => {
+ createComponent();
+
+ expect(findAllButtons()).toHaveLength(3);
+ });
+
+ it('does not display action buttons', () => {
+ createComponent({ schedule: mockPipelineScheduleAsGuestNodes[0] });
+
+ expect(findAllButtons()).toHaveLength(0);
+ });
+
+ it('delete button emits showDeleteModal event and schedule id', () => {
+ createComponent();
+
+ findDeleteBtn().vm.$emit('click');
+
+ expect(wrapper.emitted()).toEqual({
+ showDeleteModal: [[mockPipelineScheduleNodes[0].id]],
+ });
+ });
+});
diff --git a/spec/frontend/pipeline_schedules/components/table/cells/pipeline_schedule_last_pipeline_spec.js b/spec/frontend/pipeline_schedules/components/table/cells/pipeline_schedule_last_pipeline_spec.js
new file mode 100644
index 00000000000..5a47b24232f
--- /dev/null
+++ b/spec/frontend/pipeline_schedules/components/table/cells/pipeline_schedule_last_pipeline_spec.js
@@ -0,0 +1,42 @@
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import CiBadge from '~/vue_shared/components/ci_badge_link.vue';
+import PipelineScheduleLastPipeline from '~/pipeline_schedules/components/table/cells/pipeline_schedule_last_pipeline.vue';
+import { mockPipelineScheduleNodes } from '../../../mock_data';
+
+describe('Pipeline schedule last pipeline', () => {
+ let wrapper;
+
+ const defaultProps = {
+ schedule: mockPipelineScheduleNodes[2],
+ };
+
+ const createComponent = (props = defaultProps) => {
+ wrapper = shallowMountExtended(PipelineScheduleLastPipeline, {
+ propsData: {
+ ...props,
+ },
+ });
+ };
+
+ const findCIBadge = () => wrapper.findComponent(CiBadge);
+ const findStatusText = () => wrapper.findByTestId('pipeline-schedule-status-text');
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('displays pipeline status', () => {
+ createComponent();
+
+ expect(findCIBadge().exists()).toBe(true);
+ expect(findCIBadge().props('status')).toBe(defaultProps.schedule.lastPipeline.detailedStatus);
+ expect(findStatusText().exists()).toBe(false);
+ });
+
+ it('displays "none" status text', () => {
+ createComponent({ schedule: mockPipelineScheduleNodes[0] });
+
+ expect(findStatusText().text()).toBe('None');
+ expect(findCIBadge().exists()).toBe(false);
+ });
+});
diff --git a/spec/frontend/pipeline_schedules/components/table/cells/pipeline_schedule_next_run_spec.js b/spec/frontend/pipeline_schedules/components/table/cells/pipeline_schedule_next_run_spec.js
new file mode 100644
index 00000000000..b1bdc1e91a0
--- /dev/null
+++ b/spec/frontend/pipeline_schedules/components/table/cells/pipeline_schedule_next_run_spec.js
@@ -0,0 +1,43 @@
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import PipelineScheduleNextRun from '~/pipeline_schedules/components/table/cells/pipeline_schedule_next_run.vue';
+import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
+import { mockPipelineScheduleNodes } from '../../../mock_data';
+
+describe('Pipeline schedule next run', () => {
+ let wrapper;
+
+ const defaultProps = {
+ schedule: mockPipelineScheduleNodes[0],
+ };
+
+ const createComponent = (props = defaultProps) => {
+ wrapper = shallowMountExtended(PipelineScheduleNextRun, {
+ propsData: {
+ ...props,
+ },
+ });
+ };
+
+ const findTimeAgo = () => wrapper.findComponent(TimeAgoTooltip);
+ const findInactive = () => wrapper.findByTestId('pipeline-schedule-inactive');
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('displays time ago', () => {
+ createComponent();
+
+ expect(findTimeAgo().exists()).toBe(true);
+ expect(findInactive().exists()).toBe(false);
+ expect(findTimeAgo().props('time')).toBe(defaultProps.schedule.realNextRun);
+ });
+
+ it('displays inactive state', () => {
+ const inactiveSchedule = mockPipelineScheduleNodes[1];
+ createComponent({ schedule: inactiveSchedule });
+
+ expect(findInactive().text()).toBe('Inactive');
+ expect(findTimeAgo().exists()).toBe(false);
+ });
+});
diff --git a/spec/frontend/pipeline_schedules/components/table/cells/pipeline_schedule_owner_spec.js b/spec/frontend/pipeline_schedules/components/table/cells/pipeline_schedule_owner_spec.js
new file mode 100644
index 00000000000..3ab04958f5e
--- /dev/null
+++ b/spec/frontend/pipeline_schedules/components/table/cells/pipeline_schedule_owner_spec.js
@@ -0,0 +1,40 @@
+import { GlAvatar, GlAvatarLink } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import PipelineScheduleOwner from '~/pipeline_schedules/components/table/cells/pipeline_schedule_owner.vue';
+import { mockPipelineScheduleNodes } from '../../../mock_data';
+
+describe('Pipeline schedule owner', () => {
+ let wrapper;
+
+ const defaultProps = {
+ schedule: mockPipelineScheduleNodes[0],
+ };
+
+ const createComponent = (props = defaultProps) => {
+ wrapper = shallowMount(PipelineScheduleOwner, {
+ propsData: {
+ ...props,
+ },
+ });
+ };
+
+ const findAvatar = () => wrapper.findComponent(GlAvatar);
+ const findAvatarLink = () => wrapper.findComponent(GlAvatarLink);
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('displays avatar', () => {
+ expect(findAvatar().exists()).toBe(true);
+ expect(findAvatar().props('src')).toBe(defaultProps.schedule.owner.avatarUrl);
+ });
+
+ it('avatar links to user', () => {
+ expect(findAvatarLink().attributes('href')).toBe(defaultProps.schedule.owner.webPath);
+ });
+});
diff --git a/spec/frontend/pipeline_schedules/components/table/cells/pipeline_schedule_target_spec.js b/spec/frontend/pipeline_schedules/components/table/cells/pipeline_schedule_target_spec.js
new file mode 100644
index 00000000000..6817e58790b
--- /dev/null
+++ b/spec/frontend/pipeline_schedules/components/table/cells/pipeline_schedule_target_spec.js
@@ -0,0 +1,41 @@
+import { GlIcon, GlLink } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import PipelineScheduleTarget from '~/pipeline_schedules/components/table/cells/pipeline_schedule_target.vue';
+import { mockPipelineScheduleNodes } from '../../../mock_data';
+
+describe('Pipeline schedule target', () => {
+ let wrapper;
+
+ const defaultProps = {
+ schedule: mockPipelineScheduleNodes[0],
+ };
+
+ const createComponent = (props = defaultProps) => {
+ wrapper = shallowMount(PipelineScheduleTarget, {
+ propsData: {
+ ...props,
+ },
+ });
+ };
+
+ const findIcon = () => wrapper.findComponent(GlIcon);
+ const findLink = () => wrapper.findComponent(GlLink);
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('displays icon', () => {
+ expect(findIcon().exists()).toBe(true);
+ expect(findIcon().props('name')).toBe('fork');
+ });
+
+ it('displays ref link', () => {
+ expect(findLink().attributes('href')).toBe(defaultProps.schedule.refPath);
+ expect(findLink().text()).toBe(defaultProps.schedule.refForDisplay);
+ });
+});
diff --git a/spec/frontend/pipeline_schedules/components/table/pipeline_schedules_table_spec.js b/spec/frontend/pipeline_schedules/components/table/pipeline_schedules_table_spec.js
new file mode 100644
index 00000000000..914897946ee
--- /dev/null
+++ b/spec/frontend/pipeline_schedules/components/table/pipeline_schedules_table_spec.js
@@ -0,0 +1,39 @@
+import { GlTableLite } from '@gitlab/ui';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import PipelineSchedulesTable from '~/pipeline_schedules/components/table/pipeline_schedules_table.vue';
+import { mockPipelineScheduleNodes } from '../../mock_data';
+
+describe('Pipeline schedules table', () => {
+ let wrapper;
+
+ const defaultProps = {
+ schedules: mockPipelineScheduleNodes,
+ };
+
+ const createComponent = (props = defaultProps) => {
+ wrapper = mountExtended(PipelineSchedulesTable, {
+ propsData: {
+ ...props,
+ },
+ });
+ };
+
+ const findTable = () => wrapper.findComponent(GlTableLite);
+ const findScheduleDescription = () => wrapper.findByTestId('pipeline-schedule-description');
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('displays table', () => {
+ expect(findTable().exists()).toBe(true);
+ });
+
+ it('displays schedule description', () => {
+ expect(findScheduleDescription().text()).toBe('pipeline schedule');
+ });
+});
diff --git a/spec/frontend/pipeline_schedules/mock_data.js b/spec/frontend/pipeline_schedules/mock_data.js
new file mode 100644
index 00000000000..0a60998d8fb
--- /dev/null
+++ b/spec/frontend/pipeline_schedules/mock_data.js
@@ -0,0 +1,35 @@
+// Fixture located at spec/frontend/fixtures/pipeline_schedules.rb
+import mockGetPipelineSchedulesGraphQLResponse from 'test_fixtures/graphql/pipeline_schedules/get_pipeline_schedules.query.graphql.json';
+import mockGetPipelineSchedulesAsGuestGraphQLResponse from 'test_fixtures/graphql/pipeline_schedules/get_pipeline_schedules.query.graphql.as_guest.json';
+
+const {
+ data: {
+ project: {
+ pipelineSchedules: { nodes },
+ },
+ },
+} = mockGetPipelineSchedulesGraphQLResponse;
+
+const {
+ data: {
+ project: {
+ pipelineSchedules: { nodes: guestNodes },
+ },
+ },
+} = mockGetPipelineSchedulesAsGuestGraphQLResponse;
+
+export const mockPipelineScheduleNodes = nodes;
+
+export const mockPipelineScheduleAsGuestNodes = guestNodes;
+
+export const deleteMutationResponse = {
+ data: {
+ pipelineScheduleDelete: {
+ clientMutationId: null,
+ errors: [],
+ __typename: 'PipelineScheduleDeletePayload',
+ },
+ },
+};
+
+export { mockGetPipelineSchedulesGraphQLResponse };
diff --git a/spec/frontend/pipeline_wizard/components/commit_spec.js b/spec/frontend/pipeline_wizard/components/commit_spec.js
index d7e019c642e..fa30b9c2b97 100644
--- a/spec/frontend/pipeline_wizard/components/commit_spec.js
+++ b/spec/frontend/pipeline_wizard/components/commit_spec.js
@@ -211,7 +211,7 @@ describe('Pipeline Wizard - Commit Page', () => {
}) => {
let consoleSpy;
- beforeAll(async () => {
+ beforeEach(async () => {
createComponent(
{
filename,
@@ -246,7 +246,7 @@ describe('Pipeline Wizard - Commit Page', () => {
await waitForPromises();
});
- afterAll(() => {
+ afterEach(() => {
wrapper.destroy();
});
diff --git a/spec/frontend/pipeline_wizard/components/editor_spec.js b/spec/frontend/pipeline_wizard/components/editor_spec.js
index 26e4b8eb0ea..dd0a609043a 100644
--- a/spec/frontend/pipeline_wizard/components/editor_spec.js
+++ b/spec/frontend/pipeline_wizard/components/editor_spec.js
@@ -3,12 +3,20 @@ import { Document } from 'yaml';
import YamlEditor from '~/pipeline_wizard/components/editor.vue';
describe('Pages Yaml Editor wrapper', () => {
+ let wrapper;
+
const defaultOptions = {
propsData: { doc: new Document({ foo: 'bar' }), filename: 'foo.yml' },
};
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
describe('mount hook', () => {
- const wrapper = mount(YamlEditor, defaultOptions);
+ beforeEach(() => {
+ wrapper = mount(YamlEditor, defaultOptions);
+ });
it('editor is mounted', () => {
expect(wrapper.vm.editor).not.toBeUndefined();
@@ -19,16 +27,11 @@ describe('Pages Yaml Editor wrapper', () => {
describe('watchers', () => {
describe('doc', () => {
const doc = new Document({ baz: ['bar'] });
- let wrapper;
beforeEach(() => {
wrapper = mount(YamlEditor, defaultOptions);
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it("causes the editor's value to be set to the stringified document", async () => {
await wrapper.setProps({ doc });
expect(wrapper.vm.editor.getValue()).toEqual(doc.toString());
@@ -48,7 +51,10 @@ describe('Pages Yaml Editor wrapper', () => {
describe('highlight', () => {
const highlight = 'foo';
- const wrapper = mount(YamlEditor, defaultOptions);
+
+ beforeEach(() => {
+ wrapper = mount(YamlEditor, defaultOptions);
+ });
it('calls editor.highlight(path, keep=true)', async () => {
const highlightSpy = jest.spyOn(wrapper.vm.yamlEditorExtension.obj, 'highlight');
diff --git a/spec/frontend/pipeline_wizard/components/widgets/list_spec.js b/spec/frontend/pipeline_wizard/components/widgets/list_spec.js
index 796356634bc..c9e9f5caebe 100644
--- a/spec/frontend/pipeline_wizard/components/widgets/list_spec.js
+++ b/spec/frontend/pipeline_wizard/components/widgets/list_spec.js
@@ -22,6 +22,9 @@ describe('Pipeline Wizard - List Widget', () => {
const setValueOnInputField = (value, atIndex = 0) => {
return findGlFormInputGroupByIndex(atIndex).vm.$emit('input', value);
};
+ const getValueOfInputField = (atIndex = 0) => {
+ return findGlFormInputGroupByIndex(atIndex).get('input').element.value;
+ };
const findAddStepButton = () => wrapper.findByTestId('add-step-button');
const addStep = () => findAddStepButton().vm.$emit('click');
@@ -103,6 +106,24 @@ describe('Pipeline Wizard - List Widget', () => {
expect(addStepBtn.text()).toBe('add another step');
});
+ it('deletes the correct input item', async () => {
+ createComponent({}, mountExtended);
+
+ await addStep();
+ await addStep();
+ setValueOnInputField('foo', 0);
+ setValueOnInputField('bar', 1);
+ setValueOnInputField('baz', 2);
+
+ const button = findAllGlFormInputGroups().at(1).find('[data-testid="remove-step-button"]');
+
+ button.vm.$emit('click');
+ await nextTick();
+
+ expect(getValueOfInputField(0)).toBe('foo');
+ expect(getValueOfInputField(1)).toBe('baz');
+ });
+
it('the "add step" button increases the number of input fields', async () => {
createComponent();
diff --git a/spec/frontend/pipeline_wizard/components/wrapper_spec.js b/spec/frontend/pipeline_wizard/components/wrapper_spec.js
index f064bf01c86..d5b78cebcb3 100644
--- a/spec/frontend/pipeline_wizard/components/wrapper_spec.js
+++ b/spec/frontend/pipeline_wizard/components/wrapper_spec.js
@@ -132,7 +132,7 @@ describe('Pipeline Wizard - wrapper.vue', () => {
expectStepDef,
expectProgressBarValue,
}) => {
- beforeAll(async () => {
+ beforeEach(async () => {
createComponent();
for (const emittedValue of navigationEventChain) {
@@ -145,7 +145,7 @@ describe('Pipeline Wizard - wrapper.vue', () => {
}
});
- afterAll(() => {
+ afterEach(() => {
wrapper.destroy();
});
@@ -184,11 +184,11 @@ describe('Pipeline Wizard - wrapper.vue', () => {
});
describe('editor overlay', () => {
- beforeAll(() => {
+ beforeEach(() => {
createComponent();
});
- afterAll(() => {
+ afterEach(() => {
wrapper.destroy();
});
@@ -236,11 +236,11 @@ describe('Pipeline Wizard - wrapper.vue', () => {
});
describe('line highlights', () => {
- beforeAll(() => {
+ beforeEach(() => {
createComponent();
});
- afterAll(() => {
+ afterEach(() => {
wrapper.destroy();
});
@@ -266,7 +266,7 @@ describe('Pipeline Wizard - wrapper.vue', () => {
});
describe('integration test', () => {
- beforeAll(async () => {
+ beforeEach(async () => {
createComponent({}, mountExtended);
});
@@ -290,14 +290,25 @@ describe('Pipeline Wizard - wrapper.vue', () => {
describe('navigating back', () => {
let inputField;
- beforeAll(async () => {
+ beforeEach(async () => {
+ createComponent({}, mountExtended);
+
+ findFirstInputFieldForTarget('$FOO').setValue('fooVal');
+ await nextTick();
+
+ findFirstVisibleStep().vm.$emit('next');
+ await nextTick();
+
+ findFirstInputFieldForTarget('$BAR').setValue('barVal');
+ await nextTick();
+
findFirstVisibleStep().vm.$emit('back');
await nextTick();
inputField = findFirstInputFieldForTarget('$FOO');
});
- afterAll(() => {
+ afterEach(() => {
wrapper.destroy();
inputField = undefined;
});
diff --git a/spec/frontend/pipeline_wizard/mock/yaml.js b/spec/frontend/pipeline_wizard/mock/yaml.js
index 12b6f1052b2..014a32c5700 100644
--- a/spec/frontend/pipeline_wizard/mock/yaml.js
+++ b/spec/frontend/pipeline_wizard/mock/yaml.js
@@ -62,8 +62,7 @@ export const steps = `
export const compiledScenario1 = `foo: fooVal
`;
-export const compiledScenario2 = `foo: fooVal
-bar: barVal
+export const compiledScenario2 = `bar: barVal
`;
export const compiledScenario3 = `foo: newFooVal
diff --git a/spec/frontend/pipelines/components/jobs/failed_jobs_app_spec.js b/spec/frontend/pipelines/components/jobs/failed_jobs_app_spec.js
index bfbb5f934b9..d1da7cb3acf 100644
--- a/spec/frontend/pipelines/components/jobs/failed_jobs_app_spec.js
+++ b/spec/frontend/pipelines/components/jobs/failed_jobs_app_spec.js
@@ -4,7 +4,7 @@ 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 { createAlert } from '~/flash';
import FailedJobsApp from '~/pipelines/components/jobs/failed_jobs_app.vue';
import FailedJobsTable from '~/pipelines/components/jobs/failed_jobs_table.vue';
import GetFailedJobsQuery from '~/pipelines/graphql/queries/get_failed_jobs.query.graphql';
@@ -70,7 +70,7 @@ describe('Failed Jobs App', () => {
await waitForPromises();
expect(findJobsTable().exists()).toBe(true);
- expect(createFlash).not.toHaveBeenCalled();
+ expect(createAlert).not.toHaveBeenCalled();
});
it('handles query fetch error correctly', async () => {
@@ -80,7 +80,7 @@ describe('Failed Jobs App', () => {
await waitForPromises();
- expect(createFlash).toHaveBeenCalledWith({
+ expect(createAlert).toHaveBeenCalledWith({
message: 'There was a problem fetching the failed jobs.',
});
});
diff --git a/spec/frontend/pipelines/components/jobs/failed_jobs_table_spec.js b/spec/frontend/pipelines/components/jobs/failed_jobs_table_spec.js
index b597a3bf4b0..0df15afd70d 100644
--- a/spec/frontend/pipelines/components/jobs/failed_jobs_table_spec.js
+++ b/spec/frontend/pipelines/components/jobs/failed_jobs_table_spec.js
@@ -4,7 +4,7 @@ import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { mountExtended } from 'helpers/vue_test_utils_helper';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { redirectTo } from '~/lib/utils/url_utility';
import FailedJobsTable from '~/pipelines/components/jobs/failed_jobs_table.vue';
import RetryFailedJobMutation from '~/pipelines/graphql/mutations/retry_failed_job.mutation.graphql';
@@ -88,7 +88,7 @@ describe('Failed Jobs Table', () => {
await waitForPromises();
- expect(createFlash).toHaveBeenCalledWith({
+ expect(createAlert).toHaveBeenCalledWith({
message: 'There was a problem retrying the failed job.',
});
});
diff --git a/spec/frontend/pipelines/components/jobs/jobs_app_spec.js b/spec/frontend/pipelines/components/jobs/jobs_app_spec.js
index 89b6f764b2f..9bc14266593 100644
--- a/spec/frontend/pipelines/components/jobs/jobs_app_spec.js
+++ b/spec/frontend/pipelines/components/jobs/jobs_app_spec.js
@@ -4,7 +4,7 @@ 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 { createAlert } from '~/flash';
import JobsApp from '~/pipelines/components/jobs/jobs_app.vue';
import JobsTable from '~/jobs/components/table/jobs_table.vue';
import getPipelineJobsQuery from '~/pipelines/graphql/queries/get_pipeline_jobs.query.graphql';
@@ -88,7 +88,7 @@ describe('Jobs app', () => {
expect(findJobsTable().exists()).toBe(true);
expect(findSkeletonLoader().exists()).toBe(false);
- expect(createFlash).not.toHaveBeenCalled();
+ expect(createAlert).not.toHaveBeenCalled();
});
it('handles job fetch error correctly', async () => {
@@ -98,7 +98,7 @@ describe('Jobs app', () => {
await waitForPromises();
- expect(createFlash).toHaveBeenCalledWith({
+ expect(createAlert).toHaveBeenCalledWith({
message: 'An error occurred while fetching the pipelines jobs.',
});
});
diff --git a/spec/frontend/pipelines/pipeline_multi_actions_spec.js b/spec/frontend/pipelines/pipeline_multi_actions_spec.js
index 149b40330e2..f0dae8ebcbe 100644
--- a/spec/frontend/pipelines/pipeline_multi_actions_spec.js
+++ b/spec/frontend/pipelines/pipeline_multi_actions_spec.js
@@ -1,4 +1,4 @@
-import { GlAlert, GlDropdown, GlSprintf, GlLoadingIcon } from '@gitlab/ui';
+import { GlAlert, GlDropdown, GlSprintf, GlLoadingIcon, GlSearchBoxByType } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
@@ -46,6 +46,7 @@ describe('Pipeline Multi Actions Dropdown', () => {
},
stubs: {
GlSprintf,
+ GlDropdown,
},
}),
);
@@ -56,6 +57,7 @@ describe('Pipeline Multi Actions Dropdown', () => {
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findAllArtifactItems = () => wrapper.findAllByTestId(artifactItemTestId);
const findFirstArtifactItem = () => wrapper.findByTestId(artifactItemTestId);
+ const findSearchBox = () => wrapper.findComponent(GlSearchBoxByType);
const findEmptyMessage = () => wrapper.findByTestId('artifacts-empty-message');
beforeEach(() => {
@@ -75,7 +77,7 @@ describe('Pipeline Multi Actions Dropdown', () => {
});
describe('Artifacts', () => {
- it('should fetch artifacts on dropdown click', async () => {
+ it('should fetch artifacts and show search box on dropdown click', async () => {
const endpoint = artifactsEndpoint.replace(artifactsEndpointPlaceholder, pipelineId);
mockAxios.onGet(endpoint).replyOnce(200, { artifacts });
createComponent();
@@ -84,6 +86,16 @@ describe('Pipeline Multi Actions Dropdown', () => {
expect(mockAxios.history.get).toHaveLength(1);
expect(wrapper.vm.artifacts).toEqual(artifacts);
+ expect(findSearchBox().exists()).toBe(true);
+ });
+
+ it('should focus the search box when opened with artifacts', () => {
+ createComponent({ mockData: { artifacts } });
+ wrapper.vm.$refs.searchInput.focusInput = jest.fn();
+
+ findDropdown().vm.$emit('shown');
+
+ expect(wrapper.vm.$refs.searchInput.focusInput).toHaveBeenCalled();
});
it('should render all the provided artifacts when search query is empty', () => {
@@ -109,10 +121,11 @@ describe('Pipeline Multi Actions Dropdown', () => {
expect(findFirstArtifactItem().text()).toBe(artifacts[0].name);
});
- it('should render empty message when no artifacts are found', () => {
+ it('should render empty message and no search box when no artifacts are found', () => {
createComponent({ mockData: { artifacts: [] } });
expect(findEmptyMessage().exists()).toBe(true);
+ expect(findSearchBox().exists()).toBe(false);
});
describe('while loading artifacts', () => {
diff --git a/spec/frontend/pipelines/pipelines_actions_spec.js b/spec/frontend/pipelines/pipelines_actions_spec.js
index fdfced38dca..26e61efc4f6 100644
--- a/spec/frontend/pipelines/pipelines_actions_spec.js
+++ b/spec/frontend/pipelines/pipelines_actions_spec.js
@@ -5,7 +5,7 @@ import { nextTick } from 'vue';
import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { TEST_HOST } from 'spec/test_constants';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
import PipelinesManualActions from '~/pipelines/components/pipelines_list/pipelines_manual_actions.vue';
@@ -95,7 +95,7 @@ describe('Pipelines Actions dropdown', () => {
await waitForPromises();
expect(findDropdown().props('loading')).toBe(false);
- expect(createFlash).toHaveBeenCalledTimes(1);
+ expect(createAlert).toHaveBeenCalledTimes(1);
});
});
diff --git a/spec/frontend/pipelines/pipelines_spec.js b/spec/frontend/pipelines/pipelines_spec.js
index cc2ff90de57..a3f15e25f36 100644
--- a/spec/frontend/pipelines/pipelines_spec.js
+++ b/spec/frontend/pipelines/pipelines_spec.js
@@ -11,7 +11,7 @@ import { mockTracking } from 'helpers/tracking_helper';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import Api from '~/api';
-import createFlash from '~/flash';
+import { createAlert, VARIANT_WARNING } 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';
@@ -261,9 +261,14 @@ describe('Pipelines', () => {
);
});
- it('tracks tab change click', () => {
+ it.each(['all', 'finished', 'branches', 'tags'])('tracks %p tab click', async (scope) => {
+ goToTab(scope);
+
+ await waitForPromises();
+
expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_filter_tabs', {
label: TRACKING_CATEGORIES.tabs,
+ property: scope,
});
});
});
@@ -356,8 +361,11 @@ describe('Pipelines', () => {
});
it('displays a warning message if raw text search is used', () => {
- expect(createFlash).toHaveBeenCalledTimes(1);
- expect(createFlash).toHaveBeenCalledWith({ message: RAW_TEXT_WARNING, type: 'warning' });
+ expect(createAlert).toHaveBeenCalledTimes(1);
+ expect(createAlert).toHaveBeenCalledWith({
+ message: RAW_TEXT_WARNING,
+ variant: VARIANT_WARNING,
+ });
});
it('should update browser bar', () => {
diff --git a/spec/frontend/pipelines/test_reports/stores/actions_spec.js b/spec/frontend/pipelines/test_reports/stores/actions_spec.js
index 74a9d8c354f..6e61ef97257 100644
--- a/spec/frontend/pipelines/test_reports/stores/actions_spec.js
+++ b/spec/frontend/pipelines/test_reports/stores/actions_spec.js
@@ -2,7 +2,7 @@ import MockAdapter from 'axios-mock-adapter';
import testReports from 'test_fixtures/pipelines/test_report.json';
import { TEST_HOST } from 'helpers/test_constants';
import testAction from 'helpers/vuex_action_helper';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import * as actions from '~/pipelines/stores/test_reports/actions';
import * as types from '~/pipelines/stores/test_reports/mutation_types';
@@ -56,7 +56,7 @@ describe('Actions TestReports Store', () => {
[],
[{ type: 'toggleLoading' }, { type: 'toggleLoading' }],
);
- expect(createFlash).toHaveBeenCalled();
+ expect(createAlert).toHaveBeenCalled();
});
});
diff --git a/spec/frontend/pipelines/test_reports/stores/mutations_spec.js b/spec/frontend/pipelines/test_reports/stores/mutations_spec.js
index f9b9da01a2b..ed0cc71eb97 100644
--- a/spec/frontend/pipelines/test_reports/stores/mutations_spec.js
+++ b/spec/frontend/pipelines/test_reports/stores/mutations_spec.js
@@ -1,7 +1,7 @@
import testReports from 'test_fixtures/pipelines/test_report.json';
import * as types from '~/pipelines/stores/test_reports/mutation_types';
import mutations from '~/pipelines/stores/test_reports/mutations';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
jest.mock('~/flash.js');
@@ -61,7 +61,7 @@ describe('Mutations TestReports Store', () => {
it('should show a flash message otherwise', () => {
mutations[types.SET_SUITE_ERROR](mockState, {});
- expect(createFlash).toHaveBeenCalled();
+ expect(createAlert).toHaveBeenCalled();
});
});
diff --git a/spec/frontend/profile/account/components/update_username_spec.js b/spec/frontend/profile/account/components/update_username_spec.js
index e331eed1863..575df9fb3c0 100644
--- a/spec/frontend/profile/account/components/update_username_spec.js
+++ b/spec/frontend/profile/account/components/update_username_spec.js
@@ -3,7 +3,7 @@ import { shallowMount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import { nextTick } from 'vue';
import { TEST_HOST } from 'helpers/test_constants';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import UpdateUsername from '~/profile/account/components/update_username.vue';
@@ -149,7 +149,7 @@ describe('UpdateUsername component', () => {
await expect(wrapper.vm.onConfirm()).rejects.toThrow();
- expect(createFlash).toHaveBeenCalledWith({
+ expect(createAlert).toHaveBeenCalledWith({
message: 'Invalid username',
});
});
@@ -161,7 +161,7 @@ describe('UpdateUsername component', () => {
await expect(wrapper.vm.onConfirm()).rejects.toThrow();
- expect(createFlash).toHaveBeenCalledWith({
+ expect(createAlert).toHaveBeenCalledWith({
message: 'An error occurred while updating your username, please try again.',
});
});
diff --git a/spec/frontend/profile/preferences/components/profile_preferences_spec.js b/spec/frontend/profile/preferences/components/profile_preferences_spec.js
index 89ce838a383..91cd868daac 100644
--- a/spec/frontend/profile/preferences/components/profile_preferences_spec.js
+++ b/spec/frontend/profile/preferences/components/profile_preferences_spec.js
@@ -3,7 +3,7 @@ import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
import { useMockLocationHelper } from 'helpers/mock_window_location_helper';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
-import createFlash from '~/flash';
+import { createAlert, VARIANT_DANGER, VARIANT_INFO } from '~/flash';
import IntegrationView from '~/profile/preferences/components/integration_view.vue';
import ProfilePreferences from '~/profile/preferences/components/profile_preferences.vue';
import { i18n } from '~/profile/preferences/constants';
@@ -149,7 +149,10 @@ describe('ProfilePreferences component', () => {
const successEvent = new CustomEvent('ajax:success');
form.dispatchEvent(successEvent);
- expect(createFlash).toHaveBeenCalledWith({ message: i18n.defaultSuccess, type: 'notice' });
+ expect(createAlert).toHaveBeenCalledWith({
+ message: i18n.defaultSuccess,
+ variant: VARIANT_INFO,
+ });
});
it('displays the custom success message', () => {
@@ -157,14 +160,17 @@ describe('ProfilePreferences component', () => {
const successEvent = new CustomEvent('ajax:success', { detail: [{ message }] });
form.dispatchEvent(successEvent);
- expect(createFlash).toHaveBeenCalledWith({ message, type: 'notice' });
+ expect(createAlert).toHaveBeenCalledWith({ message, variant: VARIANT_INFO });
});
it('displays the default error message', () => {
const errorEvent = new CustomEvent('ajax:error');
form.dispatchEvent(errorEvent);
- expect(createFlash).toHaveBeenCalledWith({ message: i18n.defaultError, type: 'alert' });
+ expect(createAlert).toHaveBeenCalledWith({
+ message: i18n.defaultError,
+ variant: VARIANT_DANGER,
+ });
});
it('displays the custom error message', () => {
@@ -172,7 +178,7 @@ describe('ProfilePreferences component', () => {
const errorEvent = new CustomEvent('ajax:error', { detail: [{ message }] });
form.dispatchEvent(errorEvent);
- expect(createFlash).toHaveBeenCalledWith({ message, type: 'alert' });
+ expect(createAlert).toHaveBeenCalledWith({ message, variant: VARIANT_DANGER });
});
});
diff --git a/spec/frontend/projects/commit/store/actions_spec.js b/spec/frontend/projects/commit/store/actions_spec.js
index 56dffcbd48e..008710984b9 100644
--- a/spec/frontend/projects/commit/store/actions_spec.js
+++ b/spec/frontend/projects/commit/store/actions_spec.js
@@ -1,6 +1,6 @@
import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { PROJECT_BRANCHES_ERROR } from '~/projects/commit/constants';
import * as actions from '~/projects/commit/store/actions';
@@ -68,7 +68,7 @@ describe('Commit form modal store actions', () => {
await testAction(actions.fetchBranches, {}, state, [], [{ type: 'requestBranches' }]);
- expect(createFlash).toHaveBeenCalledWith({ message: PROJECT_BRANCHES_ERROR });
+ expect(createAlert).toHaveBeenCalledWith({ message: PROJECT_BRANCHES_ERROR });
});
});
diff --git a/spec/frontend/projects/commits/store/actions_spec.js b/spec/frontend/projects/commits/store/actions_spec.js
index fdb12640b26..930b801af71 100644
--- a/spec/frontend/projects/commits/store/actions_spec.js
+++ b/spec/frontend/projects/commits/store/actions_spec.js
@@ -1,7 +1,7 @@
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import actions from '~/projects/commits/store/actions';
import * as types from '~/projects/commits/store/mutation_types';
import createState from '~/projects/commits/store/state';
@@ -38,8 +38,8 @@ describe('Project commits actions', () => {
const mockDispatchContext = { dispatch: () => {}, commit: () => {}, state };
actions.receiveAuthorsError(mockDispatchContext);
- expect(createFlash).toHaveBeenCalledTimes(1);
- expect(createFlash).toHaveBeenCalledWith({
+ expect(createAlert).toHaveBeenCalledTimes(1);
+ expect(createAlert).toHaveBeenCalledWith({
message: 'An error occurred fetching the project authors.',
});
});
diff --git a/spec/frontend/projects/compare/components/app_spec.js b/spec/frontend/projects/compare/components/app_spec.js
index 2dbecf7cc61..9b052a17caa 100644
--- a/spec/frontend/projects/compare/components/app_spec.js
+++ b/spec/frontend/projects/compare/components/app_spec.js
@@ -134,6 +134,40 @@ describe('CompareApp component', () => {
});
});
+ describe('mode dropdown', () => {
+ const findModeDropdownButton = () => wrapper.find('[data-testid="modeDropdown"]');
+ const findEnableStraightModeButton = () =>
+ wrapper.find('[data-testid="enableStraightModeButton"]');
+ const findDisableStraightModeButton = () =>
+ wrapper.find('[data-testid="disableStraightModeButton"]');
+
+ it('renders the mode dropdown button', () => {
+ expect(findModeDropdownButton().exists()).toBe(true);
+ });
+
+ it('has the correct text', () => {
+ expect(findEnableStraightModeButton().text()).toBe('...');
+ expect(findDisableStraightModeButton().text()).toBe('..');
+ });
+
+ it('straight mode button when clicked', async () => {
+ expect(wrapper.props('straight')).toBe(false);
+ expect(wrapper.find('input[name="straight"]').attributes('value')).toBe('false');
+
+ findEnableStraightModeButton().vm.$emit('click');
+
+ await nextTick();
+
+ expect(wrapper.find('input[name="straight"]').attributes('value')).toBe('true');
+
+ findDisableStraightModeButton().vm.$emit('click');
+
+ await nextTick();
+
+ expect(wrapper.find('input[name="straight"]').attributes('value')).toBe('false');
+ });
+ });
+
describe('merge request buttons', () => {
const findProjectMrButton = () => wrapper.find('[data-testid="projectMrButton"]');
const findCreateMrButton = () => wrapper.find('[data-testid="createMrButton"]');
diff --git a/spec/frontend/projects/compare/components/mock_data.js b/spec/frontend/projects/compare/components/mock_data.js
index 81d64469a2a..28d9a394038 100644
--- a/spec/frontend/projects/compare/components/mock_data.js
+++ b/spec/frontend/projects/compare/components/mock_data.js
@@ -17,6 +17,7 @@ export const appDefaultProps = {
projects: [sourceProject],
paramsFrom: 'main',
paramsTo: 'target/branch',
+ straight: false,
createMrPath: '',
sourceProjectRefsPath,
targetProjectRefsPath,
diff --git a/spec/frontend/projects/compare/components/revision_dropdown_legacy_spec.js b/spec/frontend/projects/compare/components/revision_dropdown_legacy_spec.js
index f64af1aa994..c21c0f4f9d1 100644
--- a/spec/frontend/projects/compare/components/revision_dropdown_legacy_spec.js
+++ b/spec/frontend/projects/compare/components/revision_dropdown_legacy_spec.js
@@ -2,7 +2,7 @@ import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import AxiosMockAdapter from 'axios-mock-adapter';
import { nextTick } from 'vue';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import RevisionDropdown from '~/projects/compare/components/revision_dropdown_legacy.vue';
@@ -79,7 +79,7 @@ describe('RevisionDropdown component', () => {
axiosMock.onGet('some/invalid/path').replyOnce(404);
await wrapper.vm.fetchBranchesAndTags();
- expect(createFlash).toHaveBeenCalled();
+ expect(createAlert).toHaveBeenCalled();
});
describe('GlDropdown component', () => {
diff --git a/spec/frontend/projects/compare/components/revision_dropdown_spec.js b/spec/frontend/projects/compare/components/revision_dropdown_spec.js
index 35e32fd3da0..d598bafea92 100644
--- a/spec/frontend/projects/compare/components/revision_dropdown_spec.js
+++ b/spec/frontend/projects/compare/components/revision_dropdown_spec.js
@@ -2,7 +2,7 @@ import { GlDropdown, GlDropdownItem, GlSearchBoxByType } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import AxiosMockAdapter from 'axios-mock-adapter';
import { nextTick } from 'vue';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import RevisionDropdown from '~/projects/compare/components/revision_dropdown.vue';
import { revisionDropdownDefaultProps as defaultProps } from './mock_data';
@@ -67,7 +67,7 @@ describe('RevisionDropdown component', () => {
createComponent();
await wrapper.vm.fetchBranchesAndTags();
- expect(createFlash).toHaveBeenCalled();
+ expect(createAlert).toHaveBeenCalled();
});
it('makes a new request when refsProjectPath is changed', async () => {
@@ -93,7 +93,7 @@ describe('RevisionDropdown component', () => {
createComponent();
await wrapper.vm.searchBranchesAndTags();
- expect(createFlash).toHaveBeenCalled();
+ expect(createAlert).toHaveBeenCalled();
});
it('makes request with search param', async () => {
diff --git a/spec/frontend/projects/settings/branch_rules/branch_dropdown_spec.js b/spec/frontend/projects/settings/branch_rules/components/edit/branch_dropdown_spec.js
index 79bce5a4b3f..11f219c1f90 100644
--- a/spec/frontend/projects/settings/branch_rules/branch_dropdown_spec.js
+++ b/spec/frontend/projects/settings/branch_rules/components/edit/branch_dropdown_spec.js
@@ -4,7 +4,7 @@ import { GlDropdown, GlSearchBoxByType, GlDropdownItem, GlSprintf } from '@gitla
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import BranchDropdown, {
i18n,
-} from '~/projects/settings/branch_rules/components/branch_dropdown.vue';
+} from '~/projects/settings/branch_rules/components/edit/branch_dropdown.vue';
import createMockApollo from 'helpers/mock_apollo_helper';
import branchesQuery from '~/projects/settings/branch_rules/queries/branches.query.graphql';
import waitForPromises from 'helpers/wait_for_promises';
diff --git a/spec/frontend/projects/settings/branch_rules/rule_edit_spec.js b/spec/frontend/projects/settings/branch_rules/components/edit/index_spec.js
index b0b2b9191d4..21e63fdb24d 100644
--- a/spec/frontend/projects/settings/branch_rules/rule_edit_spec.js
+++ b/spec/frontend/projects/settings/branch_rules/components/edit/index_spec.js
@@ -1,9 +1,9 @@
import { nextTick } from 'vue';
import { getParameterByName } from '~/lib/utils/url_utility';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import RuleEdit from '~/projects/settings/branch_rules/components/rule_edit.vue';
-import BranchDropdown from '~/projects/settings/branch_rules/components/branch_dropdown.vue';
-import Protections from '~/projects/settings/branch_rules/components/protections/index.vue';
+import RuleEdit from '~/projects/settings/branch_rules/components/edit/index.vue';
+import BranchDropdown from '~/projects/settings/branch_rules/components/edit/branch_dropdown.vue';
+import Protections from '~/projects/settings/branch_rules/components/edit/protections/index.vue';
jest.mock('~/lib/utils/url_utility', () => ({
getParameterByName: jest.fn().mockImplementation(() => 'main'),
diff --git a/spec/frontend/projects/settings/branch_rules/components/protections/index_spec.js b/spec/frontend/projects/settings/branch_rules/components/edit/protections/index_spec.js
index 3592fa50622..ee90ff8318f 100644
--- a/spec/frontend/projects/settings/branch_rules/components/protections/index_spec.js
+++ b/spec/frontend/projects/settings/branch_rules/components/edit/protections/index_spec.js
@@ -3,10 +3,10 @@ import { GlLink } from '@gitlab/ui';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import Protections, {
i18n,
-} from '~/projects/settings/branch_rules/components/protections/index.vue';
-import PushProtections from '~/projects/settings/branch_rules/components/protections/push_protections.vue';
-import MergeProtections from '~/projects/settings/branch_rules/components/protections/merge_protections.vue';
-import { protections } from '../../mock_data';
+} from '~/projects/settings/branch_rules/components/edit/protections/index.vue';
+import PushProtections from '~/projects/settings/branch_rules/components/edit/protections/push_protections.vue';
+import MergeProtections from '~/projects/settings/branch_rules/components/edit/protections/merge_protections.vue';
+import { protections } from '../../../mock_data';
describe('Branch Protections', () => {
let wrapper;
diff --git a/spec/frontend/projects/settings/branch_rules/components/protections/merge_protections_spec.js b/spec/frontend/projects/settings/branch_rules/components/edit/protections/merge_protections_spec.js
index 0e168a2ad78..b5fdc46d600 100644
--- a/spec/frontend/projects/settings/branch_rules/components/protections/merge_protections_spec.js
+++ b/spec/frontend/projects/settings/branch_rules/components/edit/protections/merge_protections_spec.js
@@ -2,8 +2,8 @@ import { GlFormGroup, GlFormCheckbox } from '@gitlab/ui';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import MergeProtections, {
i18n,
-} from '~/projects/settings/branch_rules/components/protections/merge_protections.vue';
-import { membersAllowedToMerge, requireCodeOwnersApproval } from '../../mock_data';
+} from '~/projects/settings/branch_rules/components/edit/protections/merge_protections.vue';
+import { membersAllowedToMerge, requireCodeOwnersApproval } from '../../../mock_data';
describe('Merge Protections', () => {
let wrapper;
diff --git a/spec/frontend/projects/settings/branch_rules/components/protections/push_protections_spec.js b/spec/frontend/projects/settings/branch_rules/components/edit/protections/push_protections_spec.js
index d54dad08338..60bb7a51dcb 100644
--- a/spec/frontend/projects/settings/branch_rules/components/protections/push_protections_spec.js
+++ b/spec/frontend/projects/settings/branch_rules/components/edit/protections/push_protections_spec.js
@@ -2,8 +2,8 @@ import { GlFormGroup, GlSprintf, GlFormCheckbox } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import PushProtections, {
i18n,
-} from '~/projects/settings/branch_rules/components/protections/push_protections.vue';
-import { membersAllowedToPush, allowForcePush } from '../../mock_data';
+} from '~/projects/settings/branch_rules/components/edit/protections/push_protections.vue';
+import { membersAllowedToPush, allowForcePush } from '../../../mock_data';
describe('Push Protections', () => {
let wrapper;
diff --git a/spec/frontend/projects/settings/branch_rules/components/view/index_spec.js b/spec/frontend/projects/settings/branch_rules/components/view/index_spec.js
new file mode 100644
index 00000000000..bf4026b65db
--- /dev/null
+++ b/spec/frontend/projects/settings/branch_rules/components/view/index_spec.js
@@ -0,0 +1,113 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import * as util from '~/lib/utils/url_utility';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import RuleView from '~/projects/settings/branch_rules/components/view/index.vue';
+import {
+ I18N,
+ ALL_BRANCHES_WILDCARD,
+} from '~/projects/settings/branch_rules/components/view/constants';
+import Protection from '~/projects/settings/branch_rules/components/view/protection.vue';
+import branchRulesQuery from '~/projects/settings/branch_rules/queries/branch_rules_details.query.graphql';
+import { sprintf } from '~/locale';
+import { branchProtectionsMockResponse } from './mock_data';
+
+jest.mock('~/lib/utils/url_utility', () => ({
+ getParameterByName: jest.fn().mockReturnValue('main'),
+ joinPaths: jest.fn(),
+}));
+
+Vue.use(VueApollo);
+
+const protectionMockProps = {
+ headerLinkHref: 'protected/branches',
+ headerLinkTitle: 'Manage in Protected Branches',
+ roles: [{ accessLevelDescription: 'Maintainers' }],
+ users: [{ avatarUrl: 'test.com/user.png', name: 'peter', webUrl: 'test.com' }],
+};
+
+describe('View branch rules', () => {
+ let wrapper;
+ let fakeApollo;
+ const projectPath = 'test/testing';
+ const protectedBranchesPath = 'protected/branches';
+ const approvalRulesPath = 'approval/rules';
+ const branchProtectionsMockRequestHandler = jest
+ .fn()
+ .mockResolvedValue(branchProtectionsMockResponse);
+
+ const createComponent = async () => {
+ fakeApollo = createMockApollo([[branchRulesQuery, branchProtectionsMockRequestHandler]]);
+
+ wrapper = shallowMountExtended(RuleView, {
+ apolloProvider: fakeApollo,
+ provide: { projectPath, protectedBranchesPath, approvalRulesPath },
+ });
+
+ await waitForPromises();
+ };
+
+ beforeEach(() => createComponent());
+
+ afterEach(() => wrapper.destroy());
+
+ const findBranchName = () => wrapper.findByTestId('branch');
+ const findBranchTitle = () => wrapper.findByTestId('branch-title');
+ const findBranchProtectionTitle = () => wrapper.findByText(I18N.protectBranchTitle);
+ const findBranchProtections = () => wrapper.findAllComponents(Protection);
+ const findForcePushTitle = () => wrapper.findByText(I18N.allowForcePushDescription);
+ const findApprovalsTitle = () => wrapper.findByText(I18N.approvalsTitle);
+
+ it('gets the branch param from url and renders it in the view', () => {
+ expect(util.getParameterByName).toHaveBeenCalledWith('branch');
+ expect(findBranchName().text()).toBe('main');
+ expect(findBranchTitle().text()).toBe(I18N.branchNameOrPattern);
+ });
+
+ it('renders the correct label if all branches are targeted', async () => {
+ jest.spyOn(util, 'getParameterByName').mockReturnValueOnce(ALL_BRANCHES_WILDCARD);
+ await createComponent();
+
+ expect(findBranchName().text()).toBe(I18N.allBranches);
+ expect(findBranchTitle().text()).toBe(I18N.targetBranch);
+ jest.restoreAllMocks();
+ });
+
+ it('renders the correct branch title', () => {
+ expect(findBranchTitle().exists()).toBe(true);
+ });
+
+ it('renders a branch protection title', () => {
+ expect(findBranchProtectionTitle().exists()).toBe(true);
+ });
+
+ it('renders a branch protection component for push rules', () => {
+ expect(findBranchProtections().at(0).props()).toMatchObject({
+ header: sprintf(I18N.allowedToPushHeader, { total: 2 }),
+ ...protectionMockProps,
+ });
+ });
+
+ it('renders force push protection', () => {
+ expect(findForcePushTitle().exists()).toBe(true);
+ });
+
+ it('renders a branch protection component for merge rules', () => {
+ expect(findBranchProtections().at(1).props()).toMatchObject({
+ header: sprintf(I18N.allowedToMergeHeader, { total: 2 }),
+ ...protectionMockProps,
+ });
+ });
+
+ it('renders a branch protection component for approvals', () => {
+ expect(findApprovalsTitle().exists()).toBe(true);
+
+ expect(findBranchProtections().at(2).props()).toMatchObject({
+ header: sprintf(I18N.approvalsHeader, { total: 0 }),
+ headerLinkHref: approvalRulesPath,
+ headerLinkTitle: I18N.manageApprovalsLinkTitle,
+ });
+ });
+});
diff --git a/spec/frontend/projects/settings/branch_rules/components/view/mock_data.js b/spec/frontend/projects/settings/branch_rules/components/view/mock_data.js
new file mode 100644
index 00000000000..c3f573061da
--- /dev/null
+++ b/spec/frontend/projects/settings/branch_rules/components/view/mock_data.js
@@ -0,0 +1,141 @@
+const usersMock = [
+ {
+ username: 'usr1',
+ webUrl: 'http://test.test/usr1',
+ name: 'User 1',
+ avatarUrl: 'http://test.test/avt1.png',
+ },
+ {
+ username: 'usr2',
+ webUrl: 'http://test.test/usr2',
+ name: 'User 2',
+ avatarUrl: 'http://test.test/avt2.png',
+ },
+ {
+ username: 'usr3',
+ webUrl: 'http://test.test/usr3',
+ name: 'User 3',
+ avatarUrl: 'http://test.test/avt3.png',
+ },
+ {
+ username: 'usr4',
+ webUrl: 'http://test.test/usr4',
+ name: 'User 4',
+ avatarUrl: 'http://test.test/avt4.png',
+ },
+ {
+ username: 'usr5',
+ webUrl: 'http://test.test/usr5',
+ name: 'User 5',
+ avatarUrl: 'http://test.test/avt5.png',
+ },
+];
+
+const accessLevelsMock = [
+ { accessLevelDescription: 'Administrator' },
+ { accessLevelDescription: 'Maintainer' },
+];
+
+const approvalsRequired = 3;
+
+const groupsMock = [{ name: 'test_group_1' }, { name: 'test_group_2' }];
+
+export const protectionPropsMock = {
+ header: 'Test protection',
+ headerLinkTitle: 'Test link title',
+ headerLinkHref: 'Test link href',
+ roles: accessLevelsMock,
+ users: usersMock,
+ groups: groupsMock,
+ approvals: [
+ {
+ name: 'test',
+ eligibleApprovers: { nodes: usersMock },
+ approvalsRequired,
+ },
+ ],
+};
+
+export const protectionRowPropsMock = {
+ title: 'Test title',
+ users: usersMock,
+ accessLevels: accessLevelsMock,
+ approvalsRequired,
+};
+
+export const accessLevelsMockResponse = [
+ {
+ __typename: 'PushAccessLevelEdge',
+ node: {
+ __typename: 'PushAccessLevel',
+ accessLevel: 40,
+ accessLevelDescription: 'Jona Langworth',
+ group: null,
+ user: {
+ __typename: 'UserCore',
+ id: '123',
+ webUrl: 'test.com',
+ name: 'peter',
+ avatarUrl: 'test.com/user.png',
+ },
+ },
+ },
+ {
+ __typename: 'PushAccessLevelEdge',
+ node: {
+ __typename: 'PushAccessLevel',
+ accessLevel: 40,
+ accessLevelDescription: 'Maintainers',
+ group: null,
+ user: null,
+ },
+ },
+];
+
+export const branchProtectionsMockResponse = {
+ data: {
+ project: {
+ id: 'gid://gitlab/Project/6',
+ __typename: 'Project',
+ branchRules: {
+ __typename: 'BranchRuleConnection',
+ nodes: [
+ {
+ __typename: 'BranchRule',
+ name: 'main',
+ branchProtection: {
+ __typename: 'BranchProtection',
+ allowForcePush: true,
+ codeOwnerApprovalRequired: true,
+ mergeAccessLevels: {
+ __typename: 'MergeAccessLevelConnection',
+ edges: accessLevelsMockResponse,
+ },
+ pushAccessLevels: {
+ __typename: 'PushAccessLevelConnection',
+ edges: accessLevelsMockResponse,
+ },
+ },
+ },
+ {
+ __typename: 'BranchRule',
+ name: '*',
+ branchProtection: {
+ __typename: 'BranchProtection',
+ allowForcePush: true,
+ codeOwnerApprovalRequired: true,
+ mergeAccessLevels: {
+ __typename: 'MergeAccessLevelConnection',
+ edges: [],
+ },
+ pushAccessLevels: {
+ __typename: 'PushAccessLevelConnection',
+ edges: [],
+ },
+ },
+ },
+ ],
+ },
+ },
+ },
+};
diff --git a/spec/frontend/projects/settings/branch_rules/components/view/protection_row_spec.js b/spec/frontend/projects/settings/branch_rules/components/view/protection_row_spec.js
new file mode 100644
index 00000000000..b0a69bedd3e
--- /dev/null
+++ b/spec/frontend/projects/settings/branch_rules/components/view/protection_row_spec.js
@@ -0,0 +1,71 @@
+import { GlAvatarsInline, GlAvatar, GlAvatarLink } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import ProtectionRow, {
+ MAX_VISIBLE_AVATARS,
+ AVATAR_SIZE,
+} from '~/projects/settings/branch_rules/components/view/protection_row.vue';
+import { protectionRowPropsMock } from './mock_data';
+
+describe('Branch rule protection row', () => {
+ let wrapper;
+
+ const createComponent = () => {
+ wrapper = shallowMountExtended(ProtectionRow, {
+ propsData: protectionRowPropsMock,
+ stubs: { GlAvatarsInline },
+ });
+ };
+
+ beforeEach(() => createComponent());
+
+ afterEach(() => wrapper.destroy());
+
+ const findTitle = () => wrapper.findByText(protectionRowPropsMock.title);
+ const findAvatarsInline = () => wrapper.findComponent(GlAvatarsInline);
+ const findAvatarLinks = () => wrapper.findAllComponents(GlAvatarLink);
+ const findAvatars = () => wrapper.findAllComponents(GlAvatar);
+ const findAccessLevels = () => wrapper.findAllByTestId('access-level');
+ const findApprovalsRequired = () =>
+ wrapper.findByText(`${protectionRowPropsMock.approvalsRequired} approvals required`);
+
+ it('renders a title', () => {
+ expect(findTitle().exists()).toBe(true);
+ });
+
+ it('renders an avatars-inline component', () => {
+ expect(findAvatarsInline().props('avatars')).toMatchObject(protectionRowPropsMock.users);
+ expect(findAvatarsInline().props('badgeSrOnlyText')).toBe('1 additional user');
+ });
+
+ it('renders avatar-link components', () => {
+ expect(findAvatarLinks().length).toBe(MAX_VISIBLE_AVATARS);
+
+ expect(findAvatarLinks().at(1).attributes('href')).toBe(protectionRowPropsMock.users[1].webUrl);
+ expect(findAvatarLinks().at(1).attributes('title')).toBe(protectionRowPropsMock.users[1].name);
+ });
+
+ it('renders avatar components', () => {
+ expect(findAvatars().length).toBe(MAX_VISIBLE_AVATARS);
+
+ expect(findAvatars().at(1).attributes('src')).toBe(protectionRowPropsMock.users[1].avatarUrl);
+ expect(findAvatars().at(1).attributes('label')).toBe(protectionRowPropsMock.users[1].name);
+ expect(findAvatars().at(1).props('size')).toBe(AVATAR_SIZE);
+ });
+
+ it('renders access level descriptions', () => {
+ expect(findAccessLevels().length).toBe(protectionRowPropsMock.accessLevels.length);
+
+ expect(findAccessLevels().at(0).text()).toBe(
+ protectionRowPropsMock.accessLevels[0].accessLevelDescription,
+ );
+ expect(findAccessLevels().at(1).text()).toContain(',');
+
+ expect(findAccessLevels().at(1).text()).toContain(
+ protectionRowPropsMock.accessLevels[1].accessLevelDescription,
+ );
+ });
+
+ it('renders the number of approvals required', () => {
+ expect(findApprovalsRequired().exists()).toBe(true);
+ });
+});
diff --git a/spec/frontend/projects/settings/branch_rules/components/view/protection_spec.js b/spec/frontend/projects/settings/branch_rules/components/view/protection_spec.js
new file mode 100644
index 00000000000..e2fbb4f5bbb
--- /dev/null
+++ b/spec/frontend/projects/settings/branch_rules/components/view/protection_spec.js
@@ -0,0 +1,68 @@
+import { GlCard, GlLink } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import Protection, { i18n } from '~/projects/settings/branch_rules/components/view/protection.vue';
+import ProtectionRow from '~/projects/settings/branch_rules/components/view/protection_row.vue';
+import { protectionPropsMock } from './mock_data';
+
+describe('Branch rule protection', () => {
+ let wrapper;
+
+ const createComponent = () => {
+ wrapper = shallowMountExtended(Protection, {
+ propsData: protectionPropsMock,
+ stubs: { GlCard },
+ });
+ };
+
+ beforeEach(() => createComponent());
+
+ afterEach(() => wrapper.destroy());
+
+ const findCard = () => wrapper.findComponent(GlCard);
+ const findHeader = () => wrapper.findByText(protectionPropsMock.header);
+ const findLink = () => wrapper.findComponent(GlLink);
+ const findProtectionRows = () => wrapper.findAllComponents(ProtectionRow);
+
+ it('renders a card component', () => {
+ expect(findCard().exists()).toBe(true);
+ });
+
+ it('renders a header with a link', () => {
+ expect(findHeader().exists()).toBe(true);
+ expect(findLink().text()).toBe(protectionPropsMock.headerLinkTitle);
+ expect(findLink().attributes('href')).toBe(protectionPropsMock.headerLinkHref);
+ });
+
+ it('renders a protection row for roles', () => {
+ expect(findProtectionRows().at(0).props()).toMatchObject({
+ accessLevels: protectionPropsMock.roles,
+ showDivider: false,
+ title: i18n.rolesTitle,
+ });
+ });
+
+ it('renders a protection row for users', () => {
+ expect(findProtectionRows().at(1).props()).toMatchObject({
+ users: protectionPropsMock.users,
+ showDivider: true,
+ title: i18n.usersTitle,
+ });
+ });
+
+ it('renders a protection row for groups', () => {
+ expect(findProtectionRows().at(2).props()).toMatchObject({
+ accessLevels: protectionPropsMock.groups,
+ showDivider: true,
+ title: i18n.groupsTitle,
+ });
+ });
+
+ it('renders a protection row for approvals', () => {
+ const approval = protectionPropsMock.approvals[0];
+ expect(findProtectionRows().at(3).props()).toMatchObject({
+ title: approval.name,
+ users: approval.eligibleApprovers.nodes,
+ approvalsRequired: approval.approvalsRequired,
+ });
+ });
+});
diff --git a/spec/frontend/projects/settings/components/default_branch_selector_spec.js b/spec/frontend/projects/settings/components/default_branch_selector_spec.js
new file mode 100644
index 00000000000..94648d87524
--- /dev/null
+++ b/spec/frontend/projects/settings/components/default_branch_selector_spec.js
@@ -0,0 +1,46 @@
+import { shallowMount } from '@vue/test-utils';
+import DefaultBranchSelector from '~/projects/settings/components/default_branch_selector.vue';
+import RefSelector from '~/ref/components/ref_selector.vue';
+import { REF_TYPE_BRANCHES } from '~/ref/constants';
+
+describe('projects/settings/components/default_branch_selector', () => {
+ const persistedDefaultBranch = 'main';
+ const projectId = '123';
+ let wrapper;
+
+ const findRefSelector = () => wrapper.findComponent(RefSelector);
+
+ const buildWrapper = () => {
+ wrapper = shallowMount(DefaultBranchSelector, {
+ propsData: {
+ persistedDefaultBranch,
+ projectId,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ beforeEach(() => {
+ buildWrapper();
+ });
+
+ it('displays a RefSelector component', () => {
+ expect(findRefSelector().props()).toEqual({
+ value: persistedDefaultBranch,
+ enabledRefTypes: [REF_TYPE_BRANCHES],
+ projectId,
+ state: true,
+ translations: {
+ dropdownHeader: expect.any(String),
+ searchPlaceholder: expect.any(String),
+ },
+ useSymbolicRefNames: false,
+ name: 'project[default_branch]',
+ });
+
+ expect(findRefSelector().classes()).toContain('gl-w-full');
+ });
+});
diff --git a/spec/frontend/projects/settings/components/transfer_project_form_spec.js b/spec/frontend/projects/settings/components/transfer_project_form_spec.js
index bde7148078d..6e639f895a8 100644
--- a/spec/frontend/projects/settings/components/transfer_project_form_spec.js
+++ b/spec/frontend/projects/settings/components/transfer_project_form_spec.js
@@ -1,41 +1,65 @@
import Vue, { nextTick } from 'vue';
+import { GlAlert } from '@gitlab/ui';
import VueApollo from 'vue-apollo';
-import searchNamespacesWhereUserCanTransferProjectsQueryResponsePage1 from 'test_fixtures/graphql/projects/settings/search_namespaces_where_user_can_transfer_projects_page_1.query.graphql.json';
-import searchNamespacesWhereUserCanTransferProjectsQueryResponsePage2 from 'test_fixtures/graphql/projects/settings/search_namespaces_where_user_can_transfer_projects_page_2.query.graphql.json';
-import {
- groupNamespaces,
- userNamespaces,
-} from 'jest/vue_shared/components/namespace_select/mock_data';
+import currentUserNamespaceQueryResponse from 'test_fixtures/graphql/projects/settings/current_user_namespace.query.graphql.json';
+import transferLocationsResponsePage1 from 'test_fixtures/api/projects/transfer_locations_page_1.json';
+import transferLocationsResponsePage2 from 'test_fixtures/api/projects/transfer_locations_page_2.json';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import TransferProjectForm from '~/projects/settings/components/transfer_project_form.vue';
-import NamespaceSelect from '~/vue_shared/components/namespace_select/namespace_select.vue';
+import NamespaceSelect from '~/vue_shared/components/namespace_select/namespace_select_deprecated.vue';
import ConfirmDanger from '~/vue_shared/components/confirm_danger/confirm_danger.vue';
-import searchNamespacesWhereUserCanTransferProjectsQuery from '~/projects/settings/graphql/queries/search_namespaces_where_user_can_transfer_projects.query.graphql';
+import currentUserNamespaceQuery from '~/projects/settings/graphql/queries/current_user_namespace.query.graphql';
+import { getTransferLocations } from '~/api/projects_api';
import waitForPromises from 'helpers/wait_for_promises';
+jest.mock('~/api/projects_api', () => ({
+ getTransferLocations: jest.fn(),
+}));
+
describe('Transfer project form', () => {
let wrapper;
+ const projectId = '1';
const confirmButtonText = 'Confirm';
const confirmationPhrase = 'You must construct additional pylons!';
- const runDebounce = () => jest.runAllTimers();
-
Vue.use(VueApollo);
- const defaultQueryHandler = jest
- .fn()
- .mockResolvedValue(searchNamespacesWhereUserCanTransferProjectsQueryResponsePage1);
+ const defaultQueryHandler = jest.fn().mockResolvedValue(currentUserNamespaceQueryResponse);
+ const mockResolvedGetTransferLocations = ({
+ data = transferLocationsResponsePage1,
+ page = '1',
+ nextPage = '2',
+ prevPage = null,
+ } = {}) => {
+ getTransferLocations.mockResolvedValueOnce({
+ data,
+ headers: {
+ 'x-per-page': '2',
+ 'x-page': page,
+ 'x-total': '4',
+ 'x-total-pages': '2',
+ 'x-next-page': nextPage,
+ 'x-prev-page': prevPage,
+ },
+ });
+ };
+ const mockRejectedGetTransferLocations = () => {
+ const error = new Error();
+
+ getTransferLocations.mockRejectedValueOnce(error);
+ };
const createComponent = ({
- requestHandlers = [[searchNamespacesWhereUserCanTransferProjectsQuery, defaultQueryHandler]],
+ requestHandlers = [[currentUserNamespaceQuery, defaultQueryHandler]],
} = {}) => {
wrapper = shallowMountExtended(TransferProjectForm, {
+ provide: {
+ projectId,
+ },
propsData: {
- userNamespaces,
- groupNamespaces,
confirmButtonText,
confirmationPhrase,
},
@@ -44,7 +68,12 @@ describe('Transfer project form', () => {
};
const findNamespaceSelect = () => wrapper.findComponent(NamespaceSelect);
+ const showNamespaceSelect = async () => {
+ findNamespaceSelect().vm.$emit('show');
+ await waitForPromises();
+ };
const findConfirmDanger = () => wrapper.findComponent(ConfirmDanger);
+ const findAlert = () => wrapper.findComponent(GlAlert);
afterEach(() => {
wrapper.destroy();
@@ -69,66 +98,113 @@ describe('Transfer project form', () => {
});
describe('with a selected namespace', () => {
- const [selectedItem] = groupNamespaces;
+ const [selectedItem] = transferLocationsResponsePage1;
- beforeEach(() => {
+ const arrange = async () => {
+ mockResolvedGetTransferLocations();
createComponent();
-
+ await showNamespaceSelect();
findNamespaceSelect().vm.$emit('select', selectedItem);
- });
+ };
+
+ it('emits the `selectNamespace` event when a namespace is selected', async () => {
+ await arrange();
- it('emits the `selectNamespace` event when a namespace is selected', () => {
const args = [selectedItem.id];
expect(wrapper.emitted('selectNamespace')).toEqual([args]);
});
- it('enables the confirm button', () => {
+ it('enables the confirm button', async () => {
+ await arrange();
+
expect(findConfirmDanger().attributes('disabled')).toBeUndefined();
});
- it('clicking the confirm button emits the `confirm` event', () => {
+ it('clicking the confirm button emits the `confirm` event', async () => {
+ await arrange();
+
findConfirmDanger().vm.$emit('confirm');
expect(wrapper.emitted('confirm')).toBeDefined();
});
});
- it('passes correct props to `NamespaceSelect` component', async () => {
- createComponent();
+ describe('when `NamespaceSelect` is opened', () => {
+ it('fetches user and group namespaces and passes correct props to `NamespaceSelect` component', async () => {
+ mockResolvedGetTransferLocations();
+ createComponent();
+ await showNamespaceSelect();
+
+ const { namespace } = currentUserNamespaceQueryResponse.data.currentUser;
+
+ expect(findNamespaceSelect().props()).toMatchObject({
+ userNamespaces: [
+ {
+ id: getIdFromGraphQLId(namespace.id),
+ humanName: namespace.fullName,
+ },
+ ],
+ groupNamespaces: transferLocationsResponsePage1.map(({ id, full_name: humanName }) => ({
+ id,
+ humanName,
+ })),
+ hasNextPageOfGroups: true,
+ isLoading: false,
+ isSearchLoading: false,
+ shouldFilterNamespaces: false,
+ });
+ });
- runDebounce();
- await waitForPromises();
+ describe('when namespaces have already been fetched', () => {
+ beforeEach(async () => {
+ mockResolvedGetTransferLocations();
+ createComponent();
+ await showNamespaceSelect();
+ });
+
+ it('does not fetch namespaces', async () => {
+ getTransferLocations.mockClear();
+ defaultQueryHandler.mockClear();
+
+ await showNamespaceSelect();
- const {
- namespace,
- groups,
- } = searchNamespacesWhereUserCanTransferProjectsQueryResponsePage1.data.currentUser;
-
- expect(findNamespaceSelect().props()).toMatchObject({
- userNamespaces: [
- {
- id: getIdFromGraphQLId(namespace.id),
- humanName: namespace.fullName,
- },
- ],
- groupNamespaces: groups.nodes.map((node) => ({
- id: getIdFromGraphQLId(node.id),
- humanName: node.fullName,
- })),
- hasNextPageOfGroups: true,
- isLoadingMoreGroups: false,
- isSearchLoading: false,
- shouldFilterNamespaces: false,
+ expect(getTransferLocations).not.toHaveBeenCalled();
+ expect(defaultQueryHandler).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('when `getTransferLocations` API call fails', () => {
+ it('displays error alert', async () => {
+ mockRejectedGetTransferLocations();
+ createComponent();
+ await showNamespaceSelect();
+
+ expect(findAlert().exists()).toBe(true);
+ });
+ });
+
+ describe('when `currentUser` GraphQL query fails', () => {
+ it('displays error alert', async () => {
+ mockResolvedGetTransferLocations();
+ const error = new Error();
+ createComponent({
+ requestHandlers: [[currentUserNamespaceQuery, jest.fn().mockRejectedValueOnce(error)]],
+ });
+ await showNamespaceSelect();
+
+ expect(findAlert().exists()).toBe(true);
+ });
});
});
describe('when `search` event is fired', () => {
const arrange = async () => {
+ mockResolvedGetTransferLocations();
createComponent();
-
+ await showNamespaceSelect();
+ mockResolvedGetTransferLocations();
findNamespaceSelect().vm.$emit('search', 'foo');
-
await nextTick();
};
@@ -138,87 +214,106 @@ describe('Transfer project form', () => {
expect(findNamespaceSelect().props('isSearchLoading')).toBe(true);
});
- it('passes `search` variable to query', async () => {
+ it('passes `search` param to API call', async () => {
await arrange();
- runDebounce();
await waitForPromises();
- expect(defaultQueryHandler).toHaveBeenCalledWith(expect.objectContaining({ search: 'foo' }));
+ expect(getTransferLocations).toHaveBeenCalledWith(
+ projectId,
+ expect.objectContaining({ search: 'foo' }),
+ );
+ });
+
+ describe('when `getTransferLocations` API call fails', () => {
+ it('displays dismissible error alert', async () => {
+ mockResolvedGetTransferLocations();
+ createComponent();
+ await showNamespaceSelect();
+ mockRejectedGetTransferLocations();
+ findNamespaceSelect().vm.$emit('search', 'foo');
+ await waitForPromises();
+
+ const alert = findAlert();
+
+ expect(alert.exists()).toBe(true);
+
+ alert.vm.$emit('dismiss');
+ await nextTick();
+
+ expect(alert.exists()).toBe(false);
+ });
});
});
describe('when `load-more-groups` event is fired', () => {
- let queryHandler;
-
const arrange = async () => {
- queryHandler = jest.fn();
- queryHandler.mockResolvedValueOnce(
- searchNamespacesWhereUserCanTransferProjectsQueryResponsePage1,
- );
- queryHandler.mockResolvedValueOnce(
- searchNamespacesWhereUserCanTransferProjectsQueryResponsePage2,
- );
+ mockResolvedGetTransferLocations();
+ createComponent();
+ await showNamespaceSelect();
- createComponent({
- requestHandlers: [[searchNamespacesWhereUserCanTransferProjectsQuery, queryHandler]],
+ mockResolvedGetTransferLocations({
+ data: transferLocationsResponsePage2,
+ page: '2',
+ nextPage: null,
+ prevPage: '1',
});
- runDebounce();
- await waitForPromises();
-
findNamespaceSelect().vm.$emit('load-more-groups');
await nextTick();
};
- it('sets `isLoadingMoreGroups` prop to `true`', async () => {
+ it('sets `isLoading` prop to `true`', async () => {
await arrange();
- expect(findNamespaceSelect().props('isLoadingMoreGroups')).toBe(true);
+ expect(findNamespaceSelect().props('isLoading')).toBe(true);
});
- it('passes `after` and `first` variables to query', async () => {
+ it('passes `page` param to API call', async () => {
await arrange();
- runDebounce();
await waitForPromises();
- expect(queryHandler).toHaveBeenCalledWith(
- expect.objectContaining({
- first: 25,
- after:
- searchNamespacesWhereUserCanTransferProjectsQueryResponsePage1.data.currentUser.groups
- .pageInfo.endCursor,
- }),
+ expect(getTransferLocations).toHaveBeenCalledWith(
+ projectId,
+ expect.objectContaining({ page: 2 }),
);
});
it('updates `groupNamespaces` prop with new groups', async () => {
await arrange();
- runDebounce();
await waitForPromises();
- expect(findNamespaceSelect().props('groupNamespaces')).toEqual(
- [
- ...searchNamespacesWhereUserCanTransferProjectsQueryResponsePage1.data.currentUser.groups
- .nodes,
- ...searchNamespacesWhereUserCanTransferProjectsQueryResponsePage2.data.currentUser.groups
- .nodes,
- ].map((node) => ({
- id: getIdFromGraphQLId(node.id),
- humanName: node.fullName,
- })),
+ expect(findNamespaceSelect().props('groupNamespaces')).toMatchObject(
+ [...transferLocationsResponsePage1, ...transferLocationsResponsePage2].map(
+ ({ id, full_name: humanName }) => ({
+ id,
+ humanName,
+ }),
+ ),
);
});
it('updates `hasNextPageOfGroups` prop', async () => {
await arrange();
- runDebounce();
await waitForPromises();
expect(findNamespaceSelect().props('hasNextPageOfGroups')).toBe(false);
});
+
+ describe('when `getTransferLocations` API call fails', () => {
+ it('displays error alert', async () => {
+ mockResolvedGetTransferLocations();
+ createComponent();
+ await showNamespaceSelect();
+ mockRejectedGetTransferLocations();
+ findNamespaceSelect().vm.$emit('load-more-groups');
+ await waitForPromises();
+
+ expect(findAlert().exists()).toBe(true);
+ });
+ });
});
});
diff --git a/spec/frontend/projects/settings/repository/branch_rules/app_spec.js b/spec/frontend/projects/settings/repository/branch_rules/app_spec.js
index e920cd48163..4603436c40a 100644
--- a/spec/frontend/projects/settings/repository/branch_rules/app_spec.js
+++ b/spec/frontend/projects/settings/repository/branch_rules/app_spec.js
@@ -6,8 +6,8 @@ import { mountExtended } from 'helpers/vue_test_utils_helper';
import BranchRules, { i18n } from '~/projects/settings/repository/branch_rules/app.vue';
import BranchRule from '~/projects/settings/repository/branch_rules/components/branch_rule.vue';
import branchRulesQuery from '~/projects/settings/repository/branch_rules/graphql/queries/branch_rules.query.graphql';
-import createFlash from '~/flash';
-import { branchRulesMockResponse, propsDataMock } from './mock_data';
+import { createAlert } from '~/flash';
+import { branchRulesMockResponse, appProvideMock } from './mock_data';
jest.mock('~/flash');
@@ -24,9 +24,7 @@ describe('Branch rules app', () => {
wrapper = mountExtended(BranchRules, {
apolloProvider: fakeApollo,
- propsData: {
- ...propsDataMock,
- },
+ provide: appProvideMock,
});
await waitForPromises();
@@ -39,7 +37,7 @@ describe('Branch rules app', () => {
it('displays an error if branch rules query fails', async () => {
await createComponent({ queryHandler: jest.fn().mockRejectedValue() });
- expect(createFlash).toHaveBeenCalledWith({ message: i18n.queryError });
+ expect(createAlert).toHaveBeenCalledWith({ message: i18n.queryError });
});
it('displays an empty state if no branch rules are present', async () => {
@@ -49,7 +47,11 @@ describe('Branch rules app', () => {
it('renders branch rules', () => {
const { nodes } = branchRulesMockResponse.data.project.branchRules;
- expect(findAllBranchRules().at(0).text()).toBe(nodes[0].name);
- expect(findAllBranchRules().at(1).text()).toBe(nodes[1].name);
+
+ expect(findAllBranchRules().length).toBe(nodes.length);
+
+ expect(findAllBranchRules().at(0).props('name')).toBe(nodes[0].name);
+
+ expect(findAllBranchRules().at(1).props('name')).toBe(nodes[1].name);
});
});
diff --git a/spec/frontend/projects/settings/repository/branch_rules/components/branch_rule_spec.js b/spec/frontend/projects/settings/repository/branch_rules/components/branch_rule_spec.js
index 924dab60704..2bc705f538b 100644
--- a/spec/frontend/projects/settings/repository/branch_rules/components/branch_rule_spec.js
+++ b/spec/frontend/projects/settings/repository/branch_rules/components/branch_rule_spec.js
@@ -2,26 +2,24 @@ import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import BranchRule, {
i18n,
} from '~/projects/settings/repository/branch_rules/components/branch_rule.vue';
-
-const defaultProps = {
- name: 'main',
- isDefault: true,
- isProtected: true,
- approvalDetails: ['requires approval from TEST', '2 status checks'],
-};
+import { branchRuleProvideMock, branchRulePropsMock } from '../mock_data';
describe('Branch rule', () => {
let wrapper;
const createComponent = (props = {}) => {
- wrapper = shallowMountExtended(BranchRule, { propsData: { ...defaultProps, ...props } });
+ wrapper = shallowMountExtended(BranchRule, {
+ provide: branchRuleProvideMock,
+ propsData: { ...branchRulePropsMock, ...props },
+ });
};
const findDefaultBadge = () => wrapper.findByText(i18n.defaultLabel);
const findProtectedBadge = () => wrapper.findByText(i18n.protectedLabel);
- const findBranchName = () => wrapper.findByText(defaultProps.name);
+ const findBranchName = () => wrapper.findByText(branchRulePropsMock.name);
const findProtectionDetailsList = () => wrapper.findByRole('list');
const findProtectionDetailsListItems = () => wrapper.findAllByRole('listitem');
+ const findDetailsButton = () => wrapper.findByText(i18n.detailsButtonLabel);
beforeEach(() => createComponent());
@@ -52,7 +50,17 @@ describe('Branch rule', () => {
});
it('renders the protection details list items', () => {
- expect(findProtectionDetailsListItems().at(0).text()).toBe(defaultProps.approvalDetails[0]);
- expect(findProtectionDetailsListItems().at(1).text()).toBe(defaultProps.approvalDetails[1]);
+ expect(findProtectionDetailsListItems().at(0).text()).toBe(
+ branchRulePropsMock.approvalDetails[0],
+ );
+ expect(findProtectionDetailsListItems().at(1).text()).toBe(
+ branchRulePropsMock.approvalDetails[1],
+ );
+ });
+
+ it('renders a detail button with the correct href', () => {
+ expect(findDetailsButton().attributes('href')).toBe(
+ `${branchRuleProvideMock.branchRulesPath}?branch=${branchRulePropsMock.name}`,
+ );
});
});
diff --git a/spec/frontend/projects/settings/repository/branch_rules/mock_data.js b/spec/frontend/projects/settings/repository/branch_rules/mock_data.js
index 14ed35f047d..bac82992c4d 100644
--- a/spec/frontend/projects/settings/repository/branch_rules/mock_data.js
+++ b/spec/frontend/projects/settings/repository/branch_rules/mock_data.js
@@ -20,6 +20,17 @@ export const branchRulesMockResponse = {
},
};
-export const propsDataMock = {
+export const appProvideMock = {
projectPath: 'some/project/path',
};
+
+export const branchRuleProvideMock = {
+ branchRulesPath: 'settings/repository/branch_rules',
+};
+
+export const branchRulePropsMock = {
+ name: 'main',
+ isDefault: true,
+ isProtected: true,
+ approvalDetails: ['requires approval from TEST', '2 status checks'],
+};
diff --git a/spec/frontend/protected_branches/protected_branch_edit_spec.js b/spec/frontend/protected_branches/protected_branch_edit_spec.js
index 6ef1b58a956..0aec4fbc037 100644
--- a/spec/frontend/protected_branches/protected_branch_edit_spec.js
+++ b/spec/frontend/protected_branches/protected_branch_edit_spec.js
@@ -2,7 +2,7 @@ import MockAdapter from 'axios-mock-adapter';
import $ from 'jquery';
import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import { TEST_HOST } from 'helpers/test_constants';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import ProtectedBranchEdit from '~/protected_branches/protected_branch_edit';
@@ -136,7 +136,7 @@ describe('ProtectedBranchEdit', () => {
expect(toggle).not.toHaveClass(IS_DISABLED_CLASS);
expect(toggle.querySelector(IS_LOADING_SELECTOR)).toBe(null);
- expect(createFlash).not.toHaveBeenCalled();
+ expect(createAlert).not.toHaveBeenCalled();
});
});
@@ -149,7 +149,7 @@ describe('ProtectedBranchEdit', () => {
it('flashes error', async () => {
await axios.waitForAll();
- expect(createFlash).toHaveBeenCalled();
+ expect(createAlert).toHaveBeenCalled();
});
});
});
diff --git a/spec/frontend/ref/components/ref_selector_spec.js b/spec/frontend/ref/components/ref_selector_spec.js
index 6c5af5a2625..96601a729b2 100644
--- a/spec/frontend/ref/components/ref_selector_spec.js
+++ b/spec/frontend/ref/components/ref_selector_spec.js
@@ -109,6 +109,8 @@ describe('Ref selector component', () => {
const findCommitDropdownItems = () => findCommitsSection().findAllComponents(GlDropdownItem);
const findFirstCommitDropdownItem = () => findCommitDropdownItems().at(0);
+ const findHiddenInputField = () => wrapper.find('[data-testid="selected-ref-form-field"]');
+
//
// Expecters
//
@@ -181,6 +183,24 @@ describe('Ref selector component', () => {
expect(findLoadingIcon().exists()).toBe(false);
});
});
+
+ describe('when name property is provided', () => {
+ it('renders an forrm input hidden field', () => {
+ const name = 'default_tag';
+
+ createComponent({ propsData: { name } });
+
+ expect(findHiddenInputField().attributes().name).toBe(name);
+ });
+ });
+
+ describe('when name property is not provided', () => {
+ it('renders an forrm input hidden field', () => {
+ createComponent();
+
+ expect(findHiddenInputField().exists()).toBe(false);
+ });
+ });
});
describe('post-initialization behavior', () => {
@@ -194,7 +214,7 @@ describe('Ref selector component', () => {
});
it('adds the provided ID to the GlDropdown instance', () => {
- expect(wrapper.attributes().id).toBe(id);
+ expect(wrapper.findComponent(GlDropdown).attributes().id).toBe(id);
});
});
@@ -202,7 +222,7 @@ describe('Ref selector component', () => {
const preselectedRef = fixtures.branches[0].name;
beforeEach(() => {
- createComponent({ propsData: { value: preselectedRef } });
+ createComponent({ propsData: { value: preselectedRef, name: 'selectedRef' } });
return waitForRequests();
});
@@ -210,6 +230,10 @@ describe('Ref selector component', () => {
it('renders the pre-selected ref name', () => {
expect(findButtonContent().text()).toBe(preselectedRef);
});
+
+ it('binds hidden input field to the pre-selected ref', () => {
+ expect(findHiddenInputField().attributes().value).toBe(preselectedRef);
+ });
});
describe('when the selected ref is updated by the parent component', () => {
diff --git a/spec/frontend/releases/__snapshots__/util_spec.js.snap b/spec/frontend/releases/__snapshots__/util_spec.js.snap
index 55e3dda60a0..d88d79d2cde 100644
--- a/spec/frontend/releases/__snapshots__/util_spec.js.snap
+++ b/spec/frontend/releases/__snapshots__/util_spec.js.snap
@@ -155,8 +155,8 @@ Object {
Object {
"__typename": "ReleaseEvidence",
"collectedAt": "2018-12-03T00:00:00Z",
- "filepath": "http://localhost/releases-namespace/releases-project/-/releases/v1.1/evidences/1.json",
- "id": "gid://gitlab/Releases::Evidence/1",
+ "filepath": Any<String>,
+ "id": Any<String>,
"sha": "760d6cdfb0879c3ffedec13af470e0f71cf52c6cde4d",
},
],
@@ -198,10 +198,10 @@ Object {
],
"paginationInfo": Object {
"__typename": "PageInfo",
- "endCursor": "eyJyZWxlYXNlZF9hdCI6IjIwMTgtMTItMTAgMDA6MDA6MDAuMDAwMDAwMDAwICswMDAwIiwiaWQiOiIxIn0",
+ "endCursor": Any<String>,
"hasNextPage": false,
"hasPreviousPage": false,
- "startCursor": "eyJyZWxlYXNlZF9hdCI6IjIwMTktMDEtMTAgMDA6MDA6MDAuMDAwMDAwMDAwICswMDAwIiwiaWQiOiIyIn0",
+ "startCursor": Any<String>,
},
}
`;
@@ -377,8 +377,8 @@ Object {
Object {
"__typename": "ReleaseEvidence",
"collectedAt": "2018-12-03T00:00:00Z",
- "filepath": "http://localhost/releases-namespace/releases-project/-/releases/v1.1/evidences/1.json",
- "id": "gid://gitlab/Releases::Evidence/1",
+ "filepath": Any<String>,
+ "id": Any<String>,
"sha": "760d6cdfb0879c3ffedec13af470e0f71cf52c6cde4d",
},
],
diff --git a/spec/frontend/releases/components/app_index_spec.js b/spec/frontend/releases/components/app_index_spec.js
index f64f07de90e..48589a54ec4 100644
--- a/spec/frontend/releases/components/app_index_spec.js
+++ b/spec/frontend/releases/components/app_index_spec.js
@@ -6,7 +6,7 @@ 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 { createAlert } from '~/flash';
import { historyPushState } from '~/lib/utils/common_utils';
import { sprintf, __ } from '~/locale';
import ReleasesIndexApp from '~/releases/components/app_index.vue';
@@ -161,13 +161,13 @@ describe('app_index.vue', () => {
it(`${toDescription(flashMessage)} show a flash message`, async () => {
await waitForPromises();
if (flashMessage) {
- expect(createFlash).toHaveBeenCalledWith({
+ expect(createAlert).toHaveBeenCalledWith({
message: ReleasesIndexApp.i18n.errorMessage,
captureError: true,
error: expect.any(Error),
});
} else {
- expect(createFlash).not.toHaveBeenCalled();
+ expect(createAlert).not.toHaveBeenCalled();
}
});
diff --git a/spec/frontend/releases/components/app_show_spec.js b/spec/frontend/releases/components/app_show_spec.js
index 9ca25b3b69a..c5cb8589ee8 100644
--- a/spec/frontend/releases/components/app_show_spec.js
+++ b/spec/frontend/releases/components/app_show_spec.js
@@ -4,7 +4,7 @@ import VueApollo from 'vue-apollo';
import oneReleaseQueryResponse from 'test_fixtures/graphql/releases/graphql/queries/one_release.query.graphql.json';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import ReleaseShowApp from '~/releases/components/app_show.vue';
import ReleaseBlock from '~/releases/components/release_block.vue';
import ReleaseSkeletonLoader from '~/releases/components/release_skeleton_loader.vue';
@@ -53,13 +53,13 @@ describe('Release show component', () => {
const expectNoFlash = () => {
it('does not show a flash message', () => {
- expect(createFlash).not.toHaveBeenCalled();
+ expect(createAlert).not.toHaveBeenCalled();
});
};
const expectFlashWithMessage = (message) => {
it(`shows a flash message that reads "${message}"`, () => {
- expect(createFlash).toHaveBeenCalledWith({
+ expect(createAlert).toHaveBeenCalledWith({
message,
captureError: true,
error: expect.any(Error),
diff --git a/spec/frontend/releases/components/evidence_block_spec.js b/spec/frontend/releases/components/evidence_block_spec.js
index 2db1e9e38a2..6f935215dd7 100644
--- a/spec/frontend/releases/components/evidence_block_spec.js
+++ b/spec/frontend/releases/components/evidence_block_spec.js
@@ -36,7 +36,7 @@ describe('Evidence Block', () => {
});
it('renders the title for the dowload link', () => {
- expect(wrapper.findComponent(GlLink).text()).toBe(`v1.1-evidences-1.json`);
+ expect(wrapper.findComponent(GlLink).text()).toMatch(/v1\.1-evidences-[0-9]+\.json/);
});
it('renders the correct hover text for the download', () => {
@@ -44,7 +44,9 @@ describe('Evidence Block', () => {
});
it('renders the correct file link for download', () => {
- expect(wrapper.findComponent(GlLink).attributes().download).toBe(`v1.1-evidences-1.json`);
+ expect(wrapper.findComponent(GlLink).attributes().download).toMatch(
+ /v1\.1-evidences-[0-9]+\.json/,
+ );
});
describe('sha text', () => {
diff --git a/spec/frontend/releases/components/tag_field_new_spec.js b/spec/frontend/releases/components/tag_field_new_spec.js
index b8047cae8c2..fcba0da3462 100644
--- a/spec/frontend/releases/components/tag_field_new_spec.js
+++ b/spec/frontend/releases/components/tag_field_new_spec.js
@@ -1,14 +1,17 @@
-import { GlDropdownItem } from '@gitlab/ui';
+import { GlDropdownItem, GlFormGroup, GlSprintf } from '@gitlab/ui';
import { mount, shallowMount } from '@vue/test-utils';
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import Vue, { nextTick } from 'vue';
+import { trimText } from 'helpers/text_helper';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
import { __ } from '~/locale';
import TagFieldNew from '~/releases/components/tag_field_new.vue';
import createStore from '~/releases/stores';
import createEditNewModule from '~/releases/stores/modules/edit_new';
const TEST_TAG_NAME = 'test-tag-name';
+const TEST_TAG_MESSAGE = 'Test tag message';
const TEST_PROJECT_ID = '1234';
const TEST_CREATE_FROM = 'test-create-from';
const NONEXISTENT_TAG_NAME = 'nonexistent-tag';
@@ -47,6 +50,8 @@ describe('releases/components/tag_field_new', () => {
store,
stubs: {
RefSelector: RefSelectorStub,
+ GlFormGroup,
+ GlSprintf,
},
});
};
@@ -61,9 +66,11 @@ describe('releases/components/tag_field_new', () => {
});
store.state.editNew.createFrom = TEST_CREATE_FROM;
+ store.state.editNew.showCreateFrom = true;
store.state.editNew.release = {
tagName: TEST_TAG_NAME,
+ tagMessage: '',
assets: {
links: [],
},
@@ -86,6 +93,9 @@ describe('releases/components/tag_field_new', () => {
const findCreateNewTagOption = () => wrapper.findComponent(GlDropdownItem);
+ const findAnnotatedTagMessageFormGroup = () =>
+ wrapper.find('[data-testid="annotated-tag-message-field"]');
+
describe('"Tag name" field', () => {
describe('rendering and behavior', () => {
beforeEach(() => createComponent());
@@ -124,6 +134,10 @@ describe('releases/components/tag_field_new', () => {
expect(findCreateFromFormGroup().exists()).toBe(false);
});
+ it('hides the "Tag message" field', () => {
+ expect(findAnnotatedTagMessageFormGroup().exists()).toBe(false);
+ });
+
it('fetches the release notes for the tag', () => {
const expectedUrl = `/api/v4/projects/1234/repository/tags/${updatedTagName}`;
expect(mock.history.get).toContainEqual(expect.objectContaining({ url: expectedUrl }));
@@ -230,4 +244,34 @@ describe('releases/components/tag_field_new', () => {
});
});
});
+
+ describe('"Annotated Tag" field', () => {
+ beforeEach(() => {
+ createComponent(mountExtended);
+ });
+
+ it('renders a label', () => {
+ expect(wrapper.findByRole('textbox', { name: 'Set tag message' }).exists()).toBe(true);
+ });
+
+ it('renders a description', () => {
+ expect(trimText(findAnnotatedTagMessageFormGroup().text())).toContain(
+ 'Add a message to the tag. Leaving this blank creates a lightweight tag.',
+ );
+ });
+
+ it('updates the store', async () => {
+ await findAnnotatedTagMessageFormGroup().find('textarea').setValue(TEST_TAG_MESSAGE);
+
+ expect(store.state.editNew.release.tagMessage).toBe(TEST_TAG_MESSAGE);
+ });
+
+ it('shows a link', () => {
+ const link = wrapper.findByRole('link', {
+ name: 'lightweight tag',
+ });
+
+ expect(link.attributes('href')).toBe('https://git-scm.com/book/en/v2/Git-Basics-Tagging/');
+ });
+ });
});
diff --git a/spec/frontend/releases/stores/modules/detail/actions_spec.js b/spec/frontend/releases/stores/modules/detail/actions_spec.js
index 48fba3adb24..eeee6747349 100644
--- a/spec/frontend/releases/stores/modules/detail/actions_spec.js
+++ b/spec/frontend/releases/stores/modules/detail/actions_spec.js
@@ -2,7 +2,7 @@ import { cloneDeep } from 'lodash';
import originalOneReleaseForEditingQueryResponse from 'test_fixtures/graphql/releases/graphql/queries/one_release_for_editing.query.graphql.json';
import testAction from 'helpers/vuex_action_helper';
import { getTag } from '~/api/tags_api';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { redirectTo } from '~/lib/utils/url_utility';
import { s__ } from '~/locale';
import { ASSET_LINK_TYPE } from '~/releases/constants';
@@ -59,7 +59,7 @@ describe('Release edit/new actions', () => {
releaseResponse = cloneDeep(originalOneReleaseForEditingQueryResponse);
gon.api_version = 'v4';
error = new Error('Yikes!');
- createFlash.mockClear();
+ createAlert.mockClear();
});
describe('when creating a new release', () => {
@@ -151,8 +151,8 @@ describe('Release edit/new actions', () => {
it(`shows a flash message`, () => {
return actions.fetchRelease({ commit: jest.fn(), state, rootState: state }).then(() => {
- expect(createFlash).toHaveBeenCalledTimes(1);
- expect(createFlash).toHaveBeenCalledWith({
+ expect(createAlert).toHaveBeenCalledTimes(1);
+ expect(createAlert).toHaveBeenCalledWith({
message: 'Something went wrong while getting the release details.',
});
});
@@ -169,6 +169,15 @@ describe('Release edit/new actions', () => {
});
});
+ describe('updateReleaseTagMessage', () => {
+ it(`commits ${types.UPDATE_RELEASE_TAG_MESSAGE} with the updated tag name`, () => {
+ const newMessage = 'updated-tag-message';
+ return testAction(actions.updateReleaseTagMessage, newMessage, state, [
+ { type: types.UPDATE_RELEASE_TAG_MESSAGE, payload: newMessage },
+ ]);
+ });
+ });
+
describe('updateReleasedAt', () => {
it(`commits ${types.UPDATE_RELEASED_AT} with the updated date`, () => {
const newDate = new Date();
@@ -370,8 +379,8 @@ describe('Release edit/new actions', () => {
return actions
.createRelease({ commit: jest.fn(), dispatch: jest.fn(), state, getters: {} })
.then(() => {
- expect(createFlash).toHaveBeenCalledTimes(1);
- expect(createFlash).toHaveBeenCalledWith({
+ expect(createAlert).toHaveBeenCalledTimes(1);
+ expect(createAlert).toHaveBeenCalledWith({
message: 'Yikes!',
});
});
@@ -396,8 +405,8 @@ describe('Release edit/new actions', () => {
return actions
.createRelease({ commit: jest.fn(), dispatch: jest.fn(), state, getters: {} })
.then(() => {
- expect(createFlash).toHaveBeenCalledTimes(1);
- expect(createFlash).toHaveBeenCalledWith({
+ expect(createAlert).toHaveBeenCalledTimes(1);
+ expect(createAlert).toHaveBeenCalledWith({
message: 'Something went wrong while creating a new release.',
});
});
@@ -527,8 +536,8 @@ describe('Release edit/new actions', () => {
it('shows a flash message', async () => {
await actions.updateRelease({ commit, dispatch, state, getters });
- expect(createFlash).toHaveBeenCalledTimes(1);
- expect(createFlash).toHaveBeenCalledWith({
+ expect(createAlert).toHaveBeenCalledTimes(1);
+ expect(createAlert).toHaveBeenCalledWith({
message: 'Something went wrong while saving the release details.',
});
});
@@ -547,8 +556,8 @@ describe('Release edit/new actions', () => {
it('shows a flash message', async () => {
await actions.updateRelease({ commit, dispatch, state, getters });
- expect(createFlash).toHaveBeenCalledTimes(1);
- expect(createFlash).toHaveBeenCalledWith({
+ expect(createAlert).toHaveBeenCalledTimes(1);
+ expect(createAlert).toHaveBeenCalledWith({
message: 'Something went wrong while saving the release details.',
});
});
@@ -700,8 +709,8 @@ describe('Release edit/new actions', () => {
it('shows a flash message', async () => {
await actions.deleteRelease({ commit, dispatch, state, getters });
- expect(createFlash).toHaveBeenCalledTimes(1);
- expect(createFlash).toHaveBeenCalledWith({
+ expect(createAlert).toHaveBeenCalledTimes(1);
+ expect(createAlert).toHaveBeenCalledWith({
message: 'Something went wrong while deleting the release.',
});
});
@@ -736,8 +745,8 @@ describe('Release edit/new actions', () => {
it('shows a flash message', async () => {
await actions.deleteRelease({ commit, dispatch, state, getters });
- expect(createFlash).toHaveBeenCalledTimes(1);
- expect(createFlash).toHaveBeenCalledWith({
+ expect(createAlert).toHaveBeenCalledTimes(1);
+ expect(createAlert).toHaveBeenCalledWith({
message: 'Something went wrong while deleting the release.',
});
});
@@ -779,7 +788,7 @@ describe('Release edit/new actions', () => {
[],
);
- expect(createFlash).toHaveBeenCalledWith({
+ expect(createAlert).toHaveBeenCalledWith({
message: s__('Release|Unable to fetch the tag notes.'),
});
expect(getTag).toHaveBeenCalledWith(state.projectId, tagName);
diff --git a/spec/frontend/releases/stores/modules/detail/getters_spec.js b/spec/frontend/releases/stores/modules/detail/getters_spec.js
index 2982dc5c46c..f8b87ec71dc 100644
--- a/spec/frontend/releases/stores/modules/detail/getters_spec.js
+++ b/spec/frontend/releases/stores/modules/detail/getters_spec.js
@@ -317,7 +317,7 @@ describe('Release edit/new getters', () => {
{ milestones: ['release.milestone[0].title'] },
],
])('releaseUpdateMutatationVariables', (description, state, expectedVariables) => {
- it(description, () => {
+ it(`${description}`, () => {
const expectedVariablesObject = { input: expect.objectContaining(expectedVariables) };
const actualVariables = getters.releaseUpdateMutatationVariables(state, {
@@ -332,6 +332,7 @@ describe('Release edit/new getters', () => {
it('returns all the data needed for the releaseCreate GraphQL query', () => {
const state = {
createFrom: 'main',
+ release: { tagMessage: 'hello' },
};
const otherGetters = {
@@ -352,6 +353,7 @@ describe('Release edit/new getters', () => {
const expectedVariables = {
input: {
name: 'release.name',
+ tagMessage: 'hello',
ref: 'main',
assets: {
links: [
diff --git a/spec/frontend/releases/stores/modules/detail/mutations_spec.js b/spec/frontend/releases/stores/modules/detail/mutations_spec.js
index 8bbf550b77d..944769d22cc 100644
--- a/spec/frontend/releases/stores/modules/detail/mutations_spec.js
+++ b/spec/frontend/releases/stores/modules/detail/mutations_spec.js
@@ -26,6 +26,7 @@ describe('Release edit/new mutations', () => {
expect(state.release).toEqual({
tagName: 'v1.3',
+ tagMessage: '',
name: '',
description: '',
milestones: [],
@@ -90,6 +91,16 @@ describe('Release edit/new mutations', () => {
});
});
+ describe(`${types.UPDATE_RELEASE_TAG_MESSAGE}`, () => {
+ it("updates the release's tag message", () => {
+ state.release = release;
+ const newMessage = 'updated-tag-message';
+ mutations[types.UPDATE_RELEASE_TAG_MESSAGE](state, newMessage);
+
+ expect(state.release.tagMessage).toBe(newMessage);
+ });
+ });
+
describe(`${types.UPDATE_RELEASED_AT}`, () => {
it("updates the release's released at date", () => {
state.release = release;
diff --git a/spec/frontend/releases/util_spec.js b/spec/frontend/releases/util_spec.js
index 055c8e8b39f..14cce8320e9 100644
--- a/spec/frontend/releases/util_spec.js
+++ b/spec/frontend/releases/util_spec.js
@@ -115,8 +115,18 @@ describe('releases/util.js', () => {
author: {
id: expect.any(String),
},
+ evidences: [
+ {
+ id: expect.any(String),
+ filepath: expect.any(String),
+ },
+ ],
},
],
+ paginationInfo: {
+ startCursor: expect.any(String),
+ endCursor: expect.any(String),
+ },
});
});
});
@@ -128,6 +138,12 @@ describe('releases/util.js', () => {
author: {
id: expect.any(String),
},
+ evidences: [
+ {
+ id: expect.any(String),
+ filepath: expect.any(String),
+ },
+ ],
},
});
});
diff --git a/spec/frontend/reports/accessibility_report/components/accessibility_issue_body_spec.js b/spec/frontend/reports/accessibility_report/components/accessibility_issue_body_spec.js
deleted file mode 100644
index d835ca4c733..00000000000
--- a/spec/frontend/reports/accessibility_report/components/accessibility_issue_body_spec.js
+++ /dev/null
@@ -1,112 +0,0 @@
-import { GlBadge } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
-import AccessibilityIssueBody from '~/reports/accessibility_report/components/accessibility_issue_body.vue';
-
-const issue = {
- name:
- 'The accessibility scanning found 2 errors of the following type: WCAG2AA.Principle4.Guideline4_1.4_1_2.H91.A.NoContent',
- code: 'WCAG2AA.Principle4.Guideline4_1.4_1_2.H91.A.NoContent',
- message: 'This element has insufficient contrast at this conformance level.',
- status: 'failed',
- className: 'spec.test_spec',
- learnMoreUrl: 'https://www.w3.org/TR/WCAG20-TECHS/H91.html',
-};
-
-describe('CustomMetricsForm', () => {
- let wrapper;
-
- const mountComponent = ({ name, code, message, status, className }, isNew = false) => {
- wrapper = shallowMount(AccessibilityIssueBody, {
- propsData: {
- issue: {
- name,
- code,
- message,
- status,
- className,
- },
- isNew,
- },
- });
- };
-
- const findIsNewBadge = () => wrapper.findComponent(GlBadge);
-
- beforeEach(() => {
- mountComponent(issue);
- });
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- it('Displays the issue message', () => {
- const description = wrapper.findComponent({ ref: 'accessibility-issue-description' }).text();
-
- expect(description).toContain(`Message: ${issue.message}`);
- });
-
- describe('When an issue code is present', () => {
- it('Creates the correct URL for learning more about the issue code', () => {
- const learnMoreUrl = wrapper
- .findComponent({ ref: 'accessibility-issue-learn-more' })
- .attributes('href');
-
- expect(learnMoreUrl).toBe(issue.learnMoreUrl);
- });
- });
-
- describe('When an issue code is not present', () => {
- beforeEach(() => {
- mountComponent({
- ...issue,
- code: undefined,
- });
- });
-
- it('Creates a URL leading to the overview documentation page', () => {
- const learnMoreUrl = wrapper
- .findComponent({ ref: 'accessibility-issue-learn-more' })
- .attributes('href');
-
- expect(learnMoreUrl).toBe('https://www.w3.org/TR/WCAG20-TECHS/Overview.html');
- });
- });
-
- describe('When an issue code does not contain the TECHS code', () => {
- beforeEach(() => {
- mountComponent({
- ...issue,
- code: 'WCAG2AA.Principle4.Guideline4_1.4_1_2',
- });
- });
-
- it('Creates a URL leading to the overview documentation page', () => {
- const learnMoreUrl = wrapper
- .findComponent({ ref: 'accessibility-issue-learn-more' })
- .attributes('href');
-
- expect(learnMoreUrl).toBe('https://www.w3.org/TR/WCAG20-TECHS/Overview.html');
- });
- });
-
- describe('When issue is new', () => {
- beforeEach(() => {
- mountComponent(issue, true);
- });
-
- it('Renders the new badge', () => {
- expect(findIsNewBadge().exists()).toBe(true);
- });
- });
-
- describe('When issue is not new', () => {
- beforeEach(() => {
- mountComponent(issue, false);
- });
-
- it('Does not render the new badge', () => {
- expect(findIsNewBadge().exists()).toBe(false);
- });
- });
-});
diff --git a/spec/frontend/reports/accessibility_report/grouped_accessibility_reports_app_spec.js b/spec/frontend/reports/accessibility_report/grouped_accessibility_reports_app_spec.js
deleted file mode 100644
index 9d3535291eb..00000000000
--- a/spec/frontend/reports/accessibility_report/grouped_accessibility_reports_app_spec.js
+++ /dev/null
@@ -1,125 +0,0 @@
-import { mount } from '@vue/test-utils';
-import Vue from 'vue';
-import Vuex from 'vuex';
-import AccessibilityIssueBody from '~/reports/accessibility_report/components/accessibility_issue_body.vue';
-import GroupedAccessibilityReportsApp from '~/reports/accessibility_report/grouped_accessibility_reports_app.vue';
-import { getStoreConfig } from '~/reports/accessibility_report/store';
-import { mockReport } from './mock_data';
-
-Vue.use(Vuex);
-
-describe('Grouped accessibility reports app', () => {
- let wrapper;
- let mockStore;
-
- const mountComponent = () => {
- wrapper = mount(GroupedAccessibilityReportsApp, {
- store: mockStore,
- propsData: {
- endpoint: 'endpoint.json',
- },
- });
- };
-
- const findHeader = () => wrapper.find('[data-testid="report-section-code-text"]');
-
- beforeEach(() => {
- mockStore = new Vuex.Store({
- ...getStoreConfig(),
- actions: { fetchReport: () => {}, setEndpoint: () => {} },
- });
-
- mountComponent();
- });
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- describe('while loading', () => {
- beforeEach(() => {
- mockStore.state.isLoading = true;
- mountComponent();
- });
-
- it('renders loading state', () => {
- expect(findHeader().text()).toEqual('Accessibility scanning results are being parsed');
- });
- });
-
- describe('with error', () => {
- beforeEach(() => {
- mockStore.state.isLoading = false;
- mockStore.state.hasError = true;
- mountComponent();
- });
-
- it('renders error state', () => {
- expect(findHeader().text()).toEqual('Accessibility scanning failed loading results');
- });
- });
-
- describe('with a report', () => {
- describe('with no issues', () => {
- beforeEach(() => {
- mockStore.state.report = {
- summary: {
- errored: 0,
- },
- };
- });
-
- it('renders no issues header', () => {
- expect(findHeader().text()).toContain(
- 'Accessibility scanning detected no issues for the source branch only',
- );
- });
- });
-
- describe('with one issue', () => {
- beforeEach(() => {
- mockStore.state.report = {
- summary: {
- errored: 1,
- },
- };
- });
-
- it('renders one issue header', () => {
- expect(findHeader().text()).toContain(
- 'Accessibility scanning detected 1 issue for the source branch only',
- );
- });
- });
-
- describe('with multiple issues', () => {
- beforeEach(() => {
- mockStore.state.report = {
- summary: {
- errored: 2,
- },
- };
- });
-
- it('renders multiple issues header', () => {
- expect(findHeader().text()).toContain(
- 'Accessibility scanning detected 2 issues for the source branch only',
- );
- });
- });
-
- describe('with issues to show', () => {
- beforeEach(() => {
- mockStore.state.report = mockReport;
- });
-
- it('renders custom accessibility issue body', () => {
- const issueBody = wrapper.findComponent(AccessibilityIssueBody);
-
- expect(issueBody.props('issue').code).toBe(mockReport.new_errors[0].code);
- expect(issueBody.props('issue').message).toBe(mockReport.new_errors[0].message);
- expect(issueBody.props('isNew')).toBe(true);
- });
- });
- });
-});
diff --git a/spec/frontend/reports/accessibility_report/mock_data.js b/spec/frontend/reports/accessibility_report/mock_data.js
deleted file mode 100644
index 9dace1e7c54..00000000000
--- a/spec/frontend/reports/accessibility_report/mock_data.js
+++ /dev/null
@@ -1,53 +0,0 @@
-export const mockReport = {
- status: 'failed',
- summary: {
- total: 2,
- resolved: 0,
- errored: 2,
- },
- new_errors: [
- {
- code: 'WCAG2AA.Principle1.Guideline1_4.1_4_3.G18.Fail',
- type: 'error',
- typeCode: 1,
- message:
- 'This element has insufficient contrast at this conformance level. Expected a contrast ratio of at least 4.5:1, but text in this element has a contrast ratio of 3.84:1. Recommendation: change text colour to #767676.',
- context: '<a href="/stages-devops-lifecycle/" class="main-nav-link">Product</a>',
- selector: '#main-nav > div:nth-child(2) > ul > li:nth-child(1) > a',
- runner: 'htmlcs',
- runnerExtras: {},
- },
- ],
- new_notes: [],
- new_warnings: [],
- resolved_errors: [
- {
- code: 'WCAG2AA.Principle4.Guideline4_1.4_1_2.H91.A.NoContent',
- type: 'error',
- typeCode: 1,
- message:
- 'Anchor element found with a valid href attribute, but no link content has been supplied.',
- context: '<a href="/" class="navbar-brand animated"><svg height="36" viewBox="0 0 1...</a>',
- selector: '#main-nav > div:nth-child(1) > a',
- runner: 'htmlcs',
- runnerExtras: {},
- },
- ],
- resolved_notes: [],
- resolved_warnings: [],
- existing_errors: [
- {
- code: 'WCAG2AA.Principle4.Guideline4_1.4_1_2.H91.A.NoContent',
- type: 'error',
- typeCode: 1,
- message:
- 'Anchor element found with a valid href attribute, but no link content has been supplied.',
- context: '<a href="/" class="navbar-brand animated"><svg height="36" viewBox="0 0 1...</a>',
- selector: '#main-nav > div:nth-child(1) > a',
- runner: 'htmlcs',
- runnerExtras: {},
- },
- ],
- existing_notes: [],
- existing_warnings: [],
-};
diff --git a/spec/frontend/reports/accessibility_report/store/actions_spec.js b/spec/frontend/reports/accessibility_report/store/actions_spec.js
deleted file mode 100644
index bab6c4905a7..00000000000
--- a/spec/frontend/reports/accessibility_report/store/actions_spec.js
+++ /dev/null
@@ -1,115 +0,0 @@
-import MockAdapter from 'axios-mock-adapter';
-import testAction from 'helpers/vuex_action_helper';
-import { TEST_HOST } from 'spec/test_constants';
-import axios from '~/lib/utils/axios_utils';
-import createStore from '~/reports/accessibility_report/store';
-import * as actions from '~/reports/accessibility_report/store/actions';
-import * as types from '~/reports/accessibility_report/store/mutation_types';
-import { mockReport } from '../mock_data';
-
-describe('Accessibility Reports actions', () => {
- let localState;
- let localStore;
-
- beforeEach(() => {
- localStore = createStore();
- localState = localStore.state;
- });
-
- describe('setEndpoints', () => {
- it('should commit SET_ENDPOINTS mutation', () => {
- const endpoint = 'endpoint.json';
-
- return testAction(
- actions.setEndpoint,
- endpoint,
- localState,
- [{ type: types.SET_ENDPOINT, payload: endpoint }],
- [],
- );
- });
- });
-
- describe('fetchReport', () => {
- let mock;
-
- beforeEach(() => {
- localState.endpoint = `${TEST_HOST}/endpoint.json`;
- mock = new MockAdapter(axios);
- });
-
- afterEach(() => {
- mock.restore();
- actions.stopPolling();
- actions.clearEtagPoll();
- });
-
- describe('success', () => {
- it('should commit REQUEST_REPORT mutation and dispatch receiveReportSuccess', () => {
- const data = { report: { summary: {} } };
- mock.onGet(`${TEST_HOST}/endpoint.json`).reply(200, data);
-
- return testAction(
- actions.fetchReport,
- null,
- localState,
- [{ type: types.REQUEST_REPORT }],
- [
- {
- payload: { status: 200, data },
- type: 'receiveReportSuccess',
- },
- ],
- );
- });
- });
-
- describe('error', () => {
- it('should commit REQUEST_REPORT and RECEIVE_REPORT_ERROR mutations', () => {
- mock.onGet(`${TEST_HOST}/endpoint.json`).reply(500);
-
- return testAction(
- actions.fetchReport,
- null,
- localState,
- [{ type: types.REQUEST_REPORT }],
- [{ type: 'receiveReportError' }],
- );
- });
- });
- });
-
- describe('receiveReportSuccess', () => {
- 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' }],
- );
- });
-
- it('should not commit RECEIVE_REPORTS_SUCCESS mutation with 204', () => {
- return testAction(
- actions.receiveReportSuccess,
- { status: 204, data: mockReport },
- localState,
- [],
- [],
- );
- });
- });
-
- describe('receiveReportError', () => {
- it('should commit RECEIVE_REPORT_ERROR mutation', () => {
- return testAction(
- actions.receiveReportError,
- null,
- localState,
- [{ type: types.RECEIVE_REPORT_ERROR }],
- [{ type: 'stopPolling' }],
- );
- });
- });
-});
diff --git a/spec/frontend/reports/accessibility_report/store/getters_spec.js b/spec/frontend/reports/accessibility_report/store/getters_spec.js
deleted file mode 100644
index 96344596003..00000000000
--- a/spec/frontend/reports/accessibility_report/store/getters_spec.js
+++ /dev/null
@@ -1,149 +0,0 @@
-import createStore from '~/reports/accessibility_report/store';
-import * as getters from '~/reports/accessibility_report/store/getters';
-import { LOADING, ERROR, SUCCESS, STATUS_FAILED } from '~/reports/constants';
-
-describe('Accessibility reports store getters', () => {
- let localState;
- let localStore;
-
- beforeEach(() => {
- localStore = createStore();
- localState = localStore.state;
- });
-
- describe('summaryStatus', () => {
- describe('when summary is loading', () => {
- it('returns loading status', () => {
- localState.isLoading = true;
-
- expect(getters.summaryStatus(localState)).toEqual(LOADING);
- });
- });
-
- describe('when summary has error', () => {
- it('returns error status', () => {
- localState.hasError = true;
-
- expect(getters.summaryStatus(localState)).toEqual(ERROR);
- });
- });
-
- describe('when summary has failed status', () => {
- it('returns loading status', () => {
- localState.status = STATUS_FAILED;
-
- expect(getters.summaryStatus(localState)).toEqual(ERROR);
- });
- });
-
- describe('when summary has successfully loaded', () => {
- it('returns loading status', () => {
- expect(getters.summaryStatus(localState)).toEqual(SUCCESS);
- });
- });
- });
-
- describe('groupedSummaryText', () => {
- describe('when state is loading', () => {
- it('returns the loading summary message', () => {
- localState.isLoading = true;
- const result = 'Accessibility scanning results are being parsed';
-
- expect(getters.groupedSummaryText(localState)).toEqual(result);
- });
- });
-
- describe('when state has error', () => {
- it('returns the error summary message', () => {
- localState.hasError = true;
- const result = 'Accessibility scanning failed loading results';
-
- expect(getters.groupedSummaryText(localState)).toEqual(result);
- });
- });
-
- describe('when state has successfully loaded', () => {
- describe('when report has errors', () => {
- it('returns summary message containing number of errors', () => {
- localState.report = {
- summary: {
- errored: 2,
- },
- };
- const result = 'Accessibility scanning detected 2 issues for the source branch only';
-
- expect(getters.groupedSummaryText(localState)).toEqual(result);
- });
- });
-
- describe('when report has no errors', () => {
- it('returns summary message containing no errors', () => {
- localState.report = {
- summary: {
- errored: 0,
- },
- };
- const result = 'Accessibility scanning detected no issues for the source branch only';
-
- expect(getters.groupedSummaryText(localState)).toEqual(result);
- });
- });
- });
- });
-
- describe('shouldRenderIssuesList', () => {
- describe('when has issues to render', () => {
- it('returns true', () => {
- localState.report = {
- existing_errors: [{ name: 'Issue' }],
- };
-
- expect(getters.shouldRenderIssuesList(localState)).toEqual(true);
- });
- });
-
- describe('when does not have issues to render', () => {
- it('returns false', () => {
- localState.report = {
- status: 'success',
- summary: { errored: 0 },
- };
-
- expect(getters.shouldRenderIssuesList(localState)).toEqual(false);
- });
- });
- });
-
- describe('unresolvedIssues', () => {
- it('returns the array unresolved errors', () => {
- localState.report = {
- existing_errors: [1],
- };
- const result = [1];
-
- expect(getters.unresolvedIssues(localState)).toEqual(result);
- });
- });
-
- describe('resolvedIssues', () => {
- it('returns array of resolved errors', () => {
- localState.report = {
- resolved_errors: [1],
- };
- const result = [1];
-
- expect(getters.resolvedIssues(localState)).toEqual(result);
- });
- });
-
- describe('newIssues', () => {
- it('returns array of new errors', () => {
- localState.report = {
- new_errors: [1],
- };
- const result = [1];
-
- expect(getters.newIssues(localState)).toEqual(result);
- });
- });
-});
diff --git a/spec/frontend/reports/accessibility_report/store/mutations_spec.js b/spec/frontend/reports/accessibility_report/store/mutations_spec.js
deleted file mode 100644
index b336261d804..00000000000
--- a/spec/frontend/reports/accessibility_report/store/mutations_spec.js
+++ /dev/null
@@ -1,64 +0,0 @@
-import createStore from '~/reports/accessibility_report/store';
-import mutations from '~/reports/accessibility_report/store/mutations';
-
-describe('Accessibility Reports mutations', () => {
- let localState;
- let localStore;
-
- beforeEach(() => {
- localStore = createStore();
- localState = localStore.state;
- });
-
- describe('SET_ENDPOINT', () => {
- it('sets endpoint to given value', () => {
- const endpoint = 'endpoint.json';
- mutations.SET_ENDPOINT(localState, endpoint);
-
- expect(localState.endpoint).toEqual(endpoint);
- });
- });
-
- describe('REQUEST_REPORT', () => {
- it('sets isLoading to true', () => {
- mutations.REQUEST_REPORT(localState);
-
- expect(localState.isLoading).toEqual(true);
- });
- });
-
- describe('RECEIVE_REPORT_SUCCESS', () => {
- it('sets isLoading to false', () => {
- mutations.RECEIVE_REPORT_SUCCESS(localState, {});
-
- expect(localState.isLoading).toEqual(false);
- });
-
- it('sets hasError to false', () => {
- mutations.RECEIVE_REPORT_SUCCESS(localState, {});
-
- expect(localState.hasError).toEqual(false);
- });
-
- it('sets report to response report', () => {
- const report = { data: 'testing' };
- mutations.RECEIVE_REPORT_SUCCESS(localState, report);
-
- expect(localState.report).toEqual(report);
- });
- });
-
- describe('RECEIVE_REPORT_ERROR', () => {
- it('sets isLoading to false', () => {
- mutations.RECEIVE_REPORT_ERROR(localState);
-
- expect(localState.isLoading).toEqual(false);
- });
-
- it('sets hasError to true', () => {
- mutations.RECEIVE_REPORT_ERROR(localState);
-
- expect(localState.hasError).toEqual(true);
- });
- });
-});
diff --git a/spec/frontend/reports/components/report_section_spec.js b/spec/frontend/reports/components/report_section_spec.js
index bdfba8d6878..cc35b99a199 100644
--- a/spec/frontend/reports/components/report_section_spec.js
+++ b/spec/frontend/reports/components/report_section_spec.js
@@ -7,9 +7,13 @@ import ReportSection from '~/reports/components/report_section.vue';
describe('ReportSection component', () => {
let wrapper;
- const findButton = () => wrapper.findComponent(GlButton);
+ const findExpandButton = () => wrapper.findComponent(GlButton);
const findPopover = () => wrapper.findComponent(HelpPopover);
const findReportSection = () => wrapper.find('.js-report-section-container');
+ const expectExpandButtonOpen = () =>
+ expect(findExpandButton().props('icon')).toBe('chevron-lg-up');
+ const expectExpandButtonClosed = () =>
+ expect(findExpandButton().props('icon')).toBe('chevron-lg-down');
const resolvedIssues = [
{
@@ -122,22 +126,22 @@ describe('ReportSection component', () => {
it('toggles issues', async () => {
createComponent({ props: { hasIssues: true } });
- await findButton().trigger('click');
+ await findExpandButton().trigger('click');
expect(findReportSection().isVisible()).toBe(true);
- expect(findButton().text()).toBe('Collapse');
+ expectExpandButtonOpen();
- await findButton().trigger('click');
+ await findExpandButton().trigger('click');
expect(findReportSection().isVisible()).toBe(false);
- expect(findButton().text()).toBe('Expand');
+ expectExpandButtonClosed();
});
it('is always expanded, if always-open is set to true', () => {
createComponent({ props: { hasIssues: true, alwaysOpen: true } });
expect(findReportSection().isVisible()).toBe(true);
- expect(findButton().exists()).toBe(false);
+ expect(findExpandButton().exists()).toBe(false);
});
});
});
@@ -148,7 +152,7 @@ describe('ReportSection component', () => {
expect(wrapper.emitted('toggleEvent')).toBeUndefined();
- findButton().trigger('click');
+ findExpandButton().trigger('click');
expect(wrapper.emitted('toggleEvent')).toEqual([[]]);
});
@@ -158,7 +162,7 @@ describe('ReportSection component', () => {
expect(wrapper.emitted('toggleEvent')).toBeUndefined();
- findButton().trigger('click');
+ findExpandButton().trigger('click');
expect(wrapper.emitted('toggleEvent')).toBeUndefined();
});
@@ -208,7 +212,7 @@ describe('ReportSection component', () => {
});
it('should still render the expand/collapse button', () => {
- expect(findButton().text()).toBe('Expand');
+ expectExpandButtonClosed();
});
});
diff --git a/spec/frontend/repository/commits_service_spec.js b/spec/frontend/repository/commits_service_spec.js
index 697fa7c4fd1..de7c56f239a 100644
--- a/spec/frontend/repository/commits_service_spec.js
+++ b/spec/frontend/repository/commits_service_spec.js
@@ -2,7 +2,7 @@ import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import { loadCommits, isRequested, resetRequestedCommits } from '~/repository/commits_service';
import httpStatus from '~/lib/utils/http_status';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { I18N_COMMIT_DATA_FETCH_ERROR } from '~/repository/constants';
jest.mock('~/flash');
@@ -65,13 +65,13 @@ describe('commits service', () => {
expect(isRequested(300)).toBe(false);
});
- it('calls `createFlash` when the request fails', async () => {
+ it('calls `createAlert` when the request fails', async () => {
const invalidPath = '/#@ some/path';
const invalidUrl = `${url}${invalidPath}`;
mock.onGet(invalidUrl).replyOnce(httpStatus.INTERNAL_SERVER_ERROR, [], {});
await requestCommits(1, 'my-project', invalidPath);
- expect(createFlash).toHaveBeenCalledWith({ message: I18N_COMMIT_DATA_FETCH_ERROR });
+ expect(createAlert).toHaveBeenCalledWith({ message: I18N_COMMIT_DATA_FETCH_ERROR });
});
});
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 01494cb6a24..6fe60f3c2e6 100644
--- a/spec/frontend/repository/components/__snapshots__/last_commit_spec.js.snap
+++ b/spec/frontend/repository/components/__snapshots__/last_commit_spec.js.snap
@@ -7,7 +7,7 @@ exports[`Repository last commit component renders commit widget 1`] = `
<user-avatar-link-stub
class="gl-my-2 gl-mr-4"
imgalt=""
- imgcssclasses="gl-mr-0!"
+ imgcssclasses=""
imgsize="32"
imgsrc="https://test.com"
linkhref="/test"
diff --git a/spec/frontend/repository/components/blob_controls_spec.js b/spec/frontend/repository/components/blob_controls_spec.js
index 6da1861ea7c..0d52542397f 100644
--- a/spec/frontend/repository/components/blob_controls_spec.js
+++ b/spec/frontend/repository/components/blob_controls_spec.js
@@ -8,9 +8,13 @@ import blobControlsQuery from '~/repository/queries/blob_controls.query.graphql'
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import createRouter from '~/repository/router';
import { updateElementsVisibility } from '~/repository/utils/dom';
+import ShortcutsBlob from '~/behaviors/shortcuts/shortcuts_blob';
+import BlobLinePermalinkUpdater from '~/blob/blob_line_permalink_updater';
import { blobControlsDataMock, refMock } from '../mock_data';
jest.mock('~/repository/utils/dom');
+jest.mock('~/behaviors/shortcuts/shortcuts_blob');
+jest.mock('~/blob/blob_line_permalink_updater');
let router;
let wrapper;
@@ -82,4 +86,12 @@ describe('Blob controls component', () => {
expect(updateElementsVisibility).toHaveBeenCalledWith('.tree-controls', true);
},
);
+
+ it('loads the ShortcutsBlob', () => {
+ expect(ShortcutsBlob).toHaveBeenCalled();
+ });
+
+ it('loads the BlobLinePermalinkUpdater', () => {
+ expect(BlobLinePermalinkUpdater).toHaveBeenCalled();
+ });
});
diff --git a/spec/frontend/repository/components/last_commit_spec.js b/spec/frontend/repository/components/last_commit_spec.js
index bf9528953b6..964b135bee3 100644
--- a/spec/frontend/repository/components/last_commit_spec.js
+++ b/spec/frontend/repository/components/last_commit_spec.js
@@ -185,7 +185,7 @@ describe('Repository last commit component', () => {
it('strips the first newline of the description', () => {
expect(findCommitRowDescription().html()).toBe(
- '<pre class="commit-row-description gl-mb-3">Update ADOPTERS.md</pre>',
+ '<pre class="commit-row-description gl-mb-3 gl-white-space-pre-line">Update ADOPTERS.md</pre>',
);
});
diff --git a/spec/frontend/repository/components/new_directory_modal_spec.js b/spec/frontend/repository/components/new_directory_modal_spec.js
index aaf751a9a8d..cf0d48280f4 100644
--- a/spec/frontend/repository/components/new_directory_modal_spec.js
+++ b/spec/frontend/repository/components/new_directory_modal_spec.js
@@ -4,7 +4,7 @@ import { nextTick } from 'vue';
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import waitForPromises from 'helpers/wait_for_promises';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import httpStatusCodes from '~/lib/utils/http_status';
import { visitUrl } from '~/lib/utils/url_utility';
import NewDirectoryModal from '~/repository/components/new_directory_modal.vue';
@@ -194,7 +194,7 @@ describe('NewDirectoryModal', () => {
await fillForm({ dirName: 'foo', branchName: 'master', commitMessage: 'foo' });
await submitForm();
- expect(createFlash).toHaveBeenCalledWith({
+ expect(createAlert).toHaveBeenCalledWith({
message: NewDirectoryModal.i18n.ERROR_MESSAGE,
});
});
diff --git a/spec/frontend/repository/components/table/__snapshots__/row_spec.js.snap b/spec/frontend/repository/components/table/__snapshots__/row_spec.js.snap
index 26064e9b248..b99d741e984 100644
--- a/spec/frontend/repository/components/table/__snapshots__/row_spec.js.snap
+++ b/spec/frontend/repository/components/table/__snapshots__/row_spec.js.snap
@@ -40,10 +40,10 @@ exports[`Repository table row component renders a symlink table row 1`] = `
</td>
<td
- class="d-none d-sm-table-cell tree-commit cursor-default"
+ class="d-none d-sm-table-cell tree-commit cursor-default gl-text-secondary"
>
<gl-link-stub
- class="str-truncated-100 tree-commit-link"
+ class="str-truncated-100 tree-commit-link gl-text-secondary"
/>
<gl-intersection-observer-stub>
@@ -52,7 +52,7 @@ exports[`Repository table row component renders a symlink table row 1`] = `
</td>
<td
- class="tree-time-ago text-right cursor-default"
+ class="tree-time-ago text-right cursor-default gl-text-secondary"
>
<timeago-tooltip-stub
cssclass=""
@@ -105,10 +105,10 @@ exports[`Repository table row component renders table row 1`] = `
</td>
<td
- class="d-none d-sm-table-cell tree-commit cursor-default"
+ class="d-none d-sm-table-cell tree-commit cursor-default gl-text-secondary"
>
<gl-link-stub
- class="str-truncated-100 tree-commit-link"
+ class="str-truncated-100 tree-commit-link gl-text-secondary"
/>
<gl-intersection-observer-stub>
@@ -117,7 +117,7 @@ exports[`Repository table row component renders table row 1`] = `
</td>
<td
- class="tree-time-ago text-right cursor-default"
+ class="tree-time-ago text-right cursor-default gl-text-secondary"
>
<timeago-tooltip-stub
cssclass=""
@@ -170,10 +170,10 @@ exports[`Repository table row component renders table row for path with special
</td>
<td
- class="d-none d-sm-table-cell tree-commit cursor-default"
+ class="d-none d-sm-table-cell tree-commit cursor-default gl-text-secondary"
>
<gl-link-stub
- class="str-truncated-100 tree-commit-link"
+ class="str-truncated-100 tree-commit-link gl-text-secondary"
/>
<gl-intersection-observer-stub>
@@ -182,7 +182,7 @@ exports[`Repository table row component renders table row for path with special
</td>
<td
- class="tree-time-ago text-right cursor-default"
+ class="tree-time-ago text-right cursor-default gl-text-secondary"
>
<timeago-tooltip-stub
cssclass=""
diff --git a/spec/frontend/repository/components/table/index_spec.js b/spec/frontend/repository/components/table/index_spec.js
index 697d2dcc7f5..2180f78a8df 100644
--- a/spec/frontend/repository/components/table/index_spec.js
+++ b/spec/frontend/repository/components/table/index_spec.js
@@ -159,7 +159,7 @@ describe('Repository table component', () => {
});
describe('Show more button', () => {
- const showMoreButton = () => vm.find(GlButton);
+ const showMoreButton = () => vm.findComponent(GlButton);
it.each`
hasMore | expectButtonToExist
diff --git a/spec/frontend/repository/components/table/parent_row_spec.js b/spec/frontend/repository/components/table/parent_row_spec.js
index 9daae8c36ef..03fb4242e40 100644
--- a/spec/frontend/repository/components/table/parent_row_spec.js
+++ b/spec/frontend/repository/components/table/parent_row_spec.js
@@ -39,7 +39,7 @@ describe('Repository parent row component', () => {
`('renders link in $path to $to', ({ path, to }) => {
factory(path);
- expect(vm.find(RouterLinkStub).props().to).toEqual({
+ expect(vm.findComponent(RouterLinkStub).props().to).toEqual({
path: to,
});
});
@@ -69,6 +69,6 @@ describe('Repository parent row component', () => {
it('renders loading icon when loading parent', () => {
factory('app/assets', 'app');
- expect(vm.find(GlLoadingIcon).exists()).toBe(true);
+ expect(vm.findComponent(GlLoadingIcon).exists()).toBe(true);
});
});
diff --git a/spec/frontend/repository/components/table/row_spec.js b/spec/frontend/repository/components/table/row_spec.js
index 13b09e57473..64aa6d179a8 100644
--- a/spec/frontend/repository/components/table/row_spec.js
+++ b/spec/frontend/repository/components/table/row_spec.js
@@ -47,7 +47,7 @@ function factory(propsData = {}) {
}
describe('Repository table row component', () => {
- const findRouterLink = () => vm.find(RouterLinkStub);
+ const findRouterLink = () => vm.findComponent(RouterLinkStub);
const findIntersectionObserver = () => vm.findComponent(GlIntersectionObserver);
afterEach(() => {
@@ -124,7 +124,7 @@ describe('Repository table row component', () => {
});
await nextTick();
- expect(vm.find(component).exists()).toBe(true);
+ expect(vm.findComponent(component).exists()).toBe(true);
});
it.each`
@@ -141,7 +141,7 @@ describe('Repository table row component', () => {
});
await nextTick();
- expect(vm.find({ ref: 'link' }).props('to')).toEqual({
+ expect(vm.findComponent({ ref: 'link' }).props('to')).toEqual({
path: `/-/tree/main/${encodeURIComponent(path)}`,
});
});
@@ -197,7 +197,7 @@ describe('Repository table row component', () => {
});
await nextTick();
- expect(vm.find(GlBadge).exists()).toBe(true);
+ expect(vm.findComponent(GlBadge).exists()).toBe(true);
});
it('renders commit and web links with href for submodule', async () => {
@@ -213,7 +213,7 @@ describe('Repository table row component', () => {
await nextTick();
expect(vm.find('a').attributes('href')).toEqual('https://test.com');
- expect(vm.find(GlLink).attributes('href')).toEqual('https://test.com/commit');
+ expect(vm.findComponent(GlLink).attributes('href')).toEqual('https://test.com/commit');
});
it('renders lock icon', async () => {
@@ -226,8 +226,8 @@ describe('Repository table row component', () => {
});
await nextTick();
- expect(vm.find(GlIcon).exists()).toBe(true);
- expect(vm.find(GlIcon).props('name')).toBe('lock');
+ expect(vm.findComponent(GlIcon).exists()).toBe(true);
+ expect(vm.findComponent(GlIcon).props('name')).toBe('lock');
});
it('renders loading icon when path is loading', () => {
@@ -240,7 +240,7 @@ describe('Repository table row component', () => {
loadingPath: 'test',
});
- expect(vm.find(FileIcon).props('loading')).toBe(true);
+ expect(vm.findComponent(FileIcon).props('loading')).toBe(true);
});
describe('row visibility', () => {
diff --git a/spec/frontend/repository/components/tree_content_spec.js b/spec/frontend/repository/components/tree_content_spec.js
index 9d3a5394df8..352f4314232 100644
--- a/spec/frontend/repository/components/tree_content_spec.js
+++ b/spec/frontend/repository/components/tree_content_spec.js
@@ -38,7 +38,7 @@ function factory(path, data = () => ({})) {
}
describe('Repository table component', () => {
- const findFileTable = () => vm.find(FileTable);
+ const findFileTable = () => vm.findComponent(FileTable);
afterEach(() => {
vm.destroy();
@@ -53,7 +53,7 @@ describe('Repository table component', () => {
await nextTick();
- expect(vm.find(FilePreview).exists()).toBe(true);
+ expect(vm.findComponent(FilePreview).exists()).toBe(true);
});
it('trigger fetchFiles and resetRequestedCommits when mounted', async () => {
diff --git a/spec/frontend/repository/components/upload_blob_modal_spec.js b/spec/frontend/repository/components/upload_blob_modal_spec.js
index 505ff7f3dd6..8db169b02b4 100644
--- a/spec/frontend/repository/components/upload_blob_modal_spec.js
+++ b/spec/frontend/repository/components/upload_blob_modal_spec.js
@@ -4,7 +4,7 @@ import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import { nextTick } from 'vue';
import waitForPromises from 'helpers/wait_for_promises';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import httpStatusCodes from '~/lib/utils/http_status';
import { visitUrl } from '~/lib/utils/url_utility';
import UploadBlobModal from '~/repository/components/upload_blob_modal.vue';
@@ -47,12 +47,12 @@ describe('UploadBlobModal', () => {
});
};
- const findModal = () => wrapper.find(GlModal);
- const findAlert = () => wrapper.find(GlAlert);
- const findCommitMessage = () => wrapper.find(GlFormTextarea);
- const findBranchName = () => wrapper.find(GlFormInput);
- const findMrToggle = () => wrapper.find(GlToggle);
- const findUploadDropzone = () => wrapper.find(UploadDropzone);
+ const findModal = () => wrapper.findComponent(GlModal);
+ const findAlert = () => wrapper.findComponent(GlAlert);
+ const findCommitMessage = () => wrapper.findComponent(GlFormTextarea);
+ const findBranchName = () => wrapper.findComponent(GlFormInput);
+ const findMrToggle = () => wrapper.findComponent(GlToggle);
+ const findUploadDropzone = () => wrapper.findComponent(UploadDropzone);
const actionButtonDisabledState = () => findModal().props('actionPrimary').attributes[0].disabled;
const cancelButtonDisabledState = () => findModal().props('actionCancel').attributes[0].disabled;
const actionButtonLoadingState = () => findModal().props('actionPrimary').attributes[0].loading;
@@ -185,7 +185,7 @@ describe('UploadBlobModal', () => {
});
it('creates a flash error', () => {
- expect(createFlash).toHaveBeenCalledWith({
+ expect(createAlert).toHaveBeenCalledWith({
message: 'Error uploading file. Please try again.',
});
});
diff --git a/spec/frontend/repository/pages/blob_spec.js b/spec/frontend/repository/pages/blob_spec.js
index 41ab4d616b8..4fe6188370e 100644
--- a/spec/frontend/repository/pages/blob_spec.js
+++ b/spec/frontend/repository/pages/blob_spec.js
@@ -7,7 +7,7 @@ jest.mock('~/repository/utils/dom');
describe('Repository blob page component', () => {
let wrapper;
- const findBlobContentViewer = () => wrapper.find(BlobContentViewer);
+ const findBlobContentViewer = () => wrapper.findComponent(BlobContentViewer);
const path = 'file.js';
beforeEach(() => {
diff --git a/spec/frontend/repository/pages/index_spec.js b/spec/frontend/repository/pages/index_spec.js
index c0afb7931b1..559257d414c 100644
--- a/spec/frontend/repository/pages/index_spec.js
+++ b/spec/frontend/repository/pages/index_spec.js
@@ -34,7 +34,7 @@ describe('Repository index page component', () => {
it('renders TreePage', () => {
factory();
- const child = wrapper.find(TreePage);
+ const child = wrapper.findComponent(TreePage);
expect(child.exists()).toBe(true);
expect(child.props()).toEqual({ path: '/' });
diff --git a/spec/frontend/runner/admin_runner_show/admin_runner_show_app_spec.js b/spec/frontend/runner/admin_runner_show/admin_runner_show_app_spec.js
index 7ab4aeee9bc..64f66d8f3ba 100644
--- a/spec/frontend/runner/admin_runner_show/admin_runner_show_app_spec.js
+++ b/spec/frontend/runner/admin_runner_show/admin_runner_show_app_spec.js
@@ -104,6 +104,10 @@ describe('AdminRunnerShowApp', () => {
Platform darwin
Configuration Runs untagged jobs
Maximum job timeout None
+ Token expiry
+ Runner authentication token expiration
+ Runner authentication tokens will expire based on a set interval.
+ They will automatically rotate once expired. Learn more Never expires
Tags None`.replace(/\s+/g, ' ');
expect(wrapper.text().replace(/\s+/g, ' ')).toContain(expected);
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 55a298e1695..7afde3bdc96 100644
--- a/spec/frontend/runner/admin_runners/admin_runners_app_spec.js
+++ b/spec/frontend/runner/admin_runners/admin_runners_app_spec.js
@@ -20,8 +20,6 @@ import AdminRunnersApp from '~/runner/admin_runners/admin_runners_app.vue';
import RunnerStackedLayoutBanner from '~/runner/components/runner_stacked_layout_banner.vue';
import RunnerTypeTabs from '~/runner/components/runner_type_tabs.vue';
import RunnerFilteredSearchBar from '~/runner/components/runner_filtered_search_bar.vue';
-import RunnerBulkDelete from '~/runner/components/runner_bulk_delete.vue';
-import RunnerBulkDeleteCheckbox from '~/runner/components/runner_bulk_delete_checkbox.vue';
import RunnerList from '~/runner/components/runner_list.vue';
import RunnerListEmptyState from '~/runner/components/runner_list_empty_state.vue';
import RunnerStats from '~/runner/components/stat/runner_stats.vue';
@@ -45,6 +43,7 @@ import {
PARAM_KEY_STATUS,
PARAM_KEY_TAG,
STATUS_ONLINE,
+ DEFAULT_MEMBERSHIP,
RUNNER_PAGE_SIZE,
} from '~/runner/constants';
import allRunnersQuery from 'ee_else_ce/runner/graphql/list/all_runners.query.graphql';
@@ -83,8 +82,6 @@ const COUNT_QUERIES = 7; // 4 tabs + 3 status queries
describe('AdminRunnersApp', () => {
let wrapper;
- let cacheConfig;
- let localMutations;
let showToast;
const findRunnerStackedLayoutBanner = () => wrapper.findComponent(RunnerStackedLayoutBanner);
@@ -92,8 +89,6 @@ describe('AdminRunnersApp', () => {
const findRunnerActionsCell = () => wrapper.findComponent(RunnerActionsCell);
const findRegistrationDropdown = () => wrapper.findComponent(RegistrationDropdown);
const findRunnerTypeTabs = () => wrapper.findComponent(RunnerTypeTabs);
- const findRunnerBulkDelete = () => wrapper.findComponent(RunnerBulkDelete);
- const findRunnerBulkDeleteCheckbox = () => wrapper.findComponent(RunnerBulkDeleteCheckbox);
const findRunnerList = () => wrapper.findComponent(RunnerList);
const findRunnerListEmptyState = () => wrapper.findComponent(RunnerListEmptyState);
const findRunnerPagination = () => extendedWrapper(wrapper.findComponent(RunnerPagination));
@@ -106,7 +101,7 @@ describe('AdminRunnersApp', () => {
provide,
...options
} = {}) => {
- ({ cacheConfig, localMutations } = createLocalState());
+ const { cacheConfig, localMutations } = createLocalState();
const handlers = [
[allRunnersQuery, mockRunnersHandler],
@@ -195,7 +190,7 @@ describe('AdminRunnersApp', () => {
const { id, shortSha } = mockRunners[0];
const numericId = getIdFromGraphQLId(id);
- const runnerLink = wrapper.find('tr [data-testid="td-summary"]').find(GlLink);
+ const runnerLink = wrapper.find('tr [data-testid="td-summary"]').findComponent(GlLink);
expect(runnerLink.text()).toBe(`#${numericId} (${shortSha})`);
expect(runnerLink.attributes('href')).toBe(`http://localhost/admin/runners/${numericId}`);
@@ -204,7 +199,9 @@ describe('AdminRunnersApp', () => {
it('renders runner actions for each runner', async () => {
await createComponent({ mountFn: mountExtended });
- const runnerActions = wrapper.find('tr [data-testid="td-actions"]').find(RunnerActionsCell);
+ const runnerActions = wrapper
+ .find('tr [data-testid="td-actions"]')
+ .findComponent(RunnerActionsCell);
const runner = mockRunners[0];
expect(runnerActions.props()).toEqual({
@@ -219,6 +216,7 @@ describe('AdminRunnersApp', () => {
expect(mockRunnersHandler).toHaveBeenLastCalledWith({
status: undefined,
type: undefined,
+ membership: DEFAULT_MEMBERSHIP,
sort: DEFAULT_SORT,
first: RUNNER_PAGE_SIZE,
});
@@ -255,7 +253,7 @@ describe('AdminRunnersApp', () => {
});
it('Links to the runner page', async () => {
- const runnerLink = wrapper.find('tr [data-testid="td-summary"]').find(GlLink);
+ const runnerLink = wrapper.find('tr [data-testid="td-summary"]').findComponent(GlLink);
expect(runnerLink.text()).toBe(`#${id} (${shortSha})`);
expect(runnerLink.attributes('href')).toBe(`http://localhost/admin/runners/${id}`);
@@ -288,6 +286,7 @@ describe('AdminRunnersApp', () => {
it('sets the filters in the search bar', () => {
expect(findRunnerFilteredSearchBar().props('value')).toEqual({
runnerType: INSTANCE_TYPE,
+ membership: DEFAULT_MEMBERSHIP,
filters: [
{ type: PARAM_KEY_STATUS, value: { data: STATUS_ONLINE, operator: '=' } },
{ type: PARAM_KEY_PAUSED, value: { data: 'true', operator: '=' } },
@@ -301,6 +300,7 @@ describe('AdminRunnersApp', () => {
expect(mockRunnersHandler).toHaveBeenLastCalledWith({
status: STATUS_ONLINE,
type: INSTANCE_TYPE,
+ membership: DEFAULT_MEMBERSHIP,
paused: true,
sort: DEFAULT_SORT,
first: RUNNER_PAGE_SIZE,
@@ -310,6 +310,7 @@ describe('AdminRunnersApp', () => {
it('fetches count results for requested status', () => {
expect(mockRunnersCountHandler).toHaveBeenCalledWith({
type: INSTANCE_TYPE,
+ membership: DEFAULT_MEMBERSHIP,
status: STATUS_ONLINE,
paused: true,
});
@@ -322,6 +323,7 @@ describe('AdminRunnersApp', () => {
findRunnerFilteredSearchBar().vm.$emit('input', {
runnerType: null,
+ membership: DEFAULT_MEMBERSHIP,
filters: [{ type: PARAM_KEY_STATUS, value: { data: STATUS_ONLINE, operator: '=' } }],
sort: CREATED_ASC,
});
@@ -339,6 +341,7 @@ describe('AdminRunnersApp', () => {
it('requests the runners with filters', () => {
expect(mockRunnersHandler).toHaveBeenLastCalledWith({
status: STATUS_ONLINE,
+ membership: DEFAULT_MEMBERSHIP,
sort: CREATED_ASC,
first: RUNNER_PAGE_SIZE,
});
@@ -347,6 +350,7 @@ describe('AdminRunnersApp', () => {
it('fetches count results for requested status', () => {
expect(mockRunnersCountHandler).toHaveBeenCalledWith({
status: STATUS_ONLINE,
+ membership: DEFAULT_MEMBERSHIP,
});
});
});
@@ -357,65 +361,26 @@ describe('AdminRunnersApp', () => {
expect(findRunnerPagination().attributes('disabled')).toBe('true');
});
- describe('when bulk delete is enabled', () => {
+ describe('Bulk delete', () => {
describe('Before runners are deleted', () => {
beforeEach(async () => {
- await createComponent({
- mountFn: mountExtended,
- provide: {
- glFeatures: { adminRunnersBulkDelete: true },
- },
- });
- });
-
- it('runner bulk delete is available', () => {
- expect(findRunnerBulkDelete().props('runners')).toEqual(mockRunners);
- });
-
- it('runner bulk delete checkbox is available', () => {
- expect(findRunnerBulkDeleteCheckbox().props('runners')).toEqual(mockRunners);
+ await createComponent({ mountFn: mountExtended });
});
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 runners are deleted', () => {
beforeEach(async () => {
- await createComponent({
- mountFn: mountExtended,
- provide: {
- glFeatures: { adminRunnersBulkDelete: true },
- },
- });
+ await createComponent({ mountFn: mountExtended });
});
it('count data is refetched', async () => {
expect(mockRunnersCountHandler).toHaveBeenCalledTimes(COUNT_QUERIES);
- findRunnerBulkDelete().vm.$emit('deleted', { message: 'Runners deleted' });
+ findRunnerList().vm.$emit('deleted', { message: 'Runners deleted' });
expect(mockRunnersCountHandler).toHaveBeenCalledTimes(COUNT_QUERIES * 2);
});
@@ -423,7 +388,7 @@ describe('AdminRunnersApp', () => {
it('toast is shown', async () => {
expect(showToast).toHaveBeenCalledTimes(0);
- findRunnerBulkDelete().vm.$emit('deleted', { message: 'Runners deleted' });
+ findRunnerList().vm.$emit('deleted', { message: 'Runners deleted' });
expect(showToast).toHaveBeenCalledTimes(1);
expect(showToast).toHaveBeenCalledWith('Runners deleted');
@@ -457,6 +422,7 @@ describe('AdminRunnersApp', () => {
beforeEach(async () => {
findRunnerFilteredSearchBar().vm.$emit('input', {
runnerType: null,
+ membership: DEFAULT_MEMBERSHIP,
filters: [{ type: PARAM_KEY_STATUS, value: { data: STATUS_ONLINE, operator: '=' } }],
sort: CREATED_ASC,
});
@@ -504,6 +470,7 @@ describe('AdminRunnersApp', () => {
await findRunnerPaginationNext().trigger('click');
expect(mockRunnersHandler).toHaveBeenLastCalledWith({
+ membership: DEFAULT_MEMBERSHIP,
sort: CREATED_DESC,
first: RUNNER_PAGE_SIZE,
after: pageInfo.endCursor,
diff --git a/spec/frontend/runner/components/cells/link_cell_spec.js b/spec/frontend/runner/components/cells/link_cell_spec.js
index a59a0eaa5d8..46ab1adb6b6 100644
--- a/spec/frontend/runner/components/cells/link_cell_spec.js
+++ b/spec/frontend/runner/components/cells/link_cell_spec.js
@@ -5,7 +5,7 @@ import LinkCell from '~/runner/components/cells/link_cell.vue';
describe('LinkCell', () => {
let wrapper;
- const findGlLink = () => wrapper.find(GlLink);
+ const findGlLink = () => wrapper.findComponent(GlLink);
const findSpan = () => wrapper.find('span');
const createComponent = ({ props = {}, ...options } = {}) => {
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 ffd6f126627..58974d4f85f 100644
--- a/spec/frontend/runner/components/cells/runner_actions_cell_spec.js
+++ b/spec/frontend/runner/components/cells/runner_actions_cell_spec.js
@@ -122,7 +122,7 @@ describe('RunnerActionsCell', () => {
expect(wrapper.emitted('deleted')).toEqual([[value]]);
});
- it('Renders the runner delete disabled button when user cannot delete', () => {
+ it('Does not render the runner delete button when user cannot delete', () => {
createComponent({
runner: {
userPermissions: {
@@ -132,7 +132,7 @@ describe('RunnerActionsCell', () => {
},
});
- expect(findDeleteBtn().props('disabled')).toBe(true);
+ expect(findDeleteBtn().exists()).toBe(false);
});
});
});
diff --git a/spec/frontend/runner/components/cells/runner_owner_cell_spec.js b/spec/frontend/runner/components/cells/runner_owner_cell_spec.js
new file mode 100644
index 00000000000..e9965d8855d
--- /dev/null
+++ b/spec/frontend/runner/components/cells/runner_owner_cell_spec.js
@@ -0,0 +1,111 @@
+import { shallowMount } from '@vue/test-utils';
+import { GlLink } from '@gitlab/ui';
+import { s__ } from '~/locale';
+import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
+
+import RunnerOwnerCell from '~/runner/components/cells/runner_owner_cell.vue';
+
+import { INSTANCE_TYPE, GROUP_TYPE, PROJECT_TYPE } from '~/runner/constants';
+
+describe('RunnerOwnerCell', () => {
+ let wrapper;
+
+ const findLink = () => wrapper.findComponent(GlLink);
+ const getLinkTooltip = () => getBinding(findLink().element, 'gl-tooltip').value;
+
+ const createComponent = ({ runner } = {}) => {
+ wrapper = shallowMount(RunnerOwnerCell, {
+ directives: {
+ GlTooltip: createMockDirective(),
+ },
+ propsData: {
+ runner,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('When its an instance runner', () => {
+ beforeEach(() => {
+ createComponent({
+ runner: {
+ runnerType: INSTANCE_TYPE,
+ },
+ });
+ });
+
+ it('shows an administrator label', () => {
+ expect(findLink().exists()).toBe(false);
+ expect(wrapper.text()).toBe(s__('Runners|Administrator'));
+ });
+ });
+
+ describe('When its a group runner', () => {
+ const mockName = 'Group 2';
+ const mockFullName = 'Group 1 / Group 2';
+ const mockWebUrl = '/group-1/group-2';
+
+ beforeEach(() => {
+ createComponent({
+ runner: {
+ runnerType: GROUP_TYPE,
+ groups: {
+ nodes: [
+ {
+ name: mockName,
+ fullName: mockFullName,
+ webUrl: mockWebUrl,
+ },
+ ],
+ },
+ },
+ });
+ });
+
+ it('Displays a group link', () => {
+ expect(findLink().attributes('href')).toBe(mockWebUrl);
+ expect(wrapper.text()).toBe(mockName);
+ expect(getLinkTooltip()).toBe(mockFullName);
+ });
+ });
+
+ describe('When its a project runner', () => {
+ const mockName = 'Project 1';
+ const mockNameWithNamespace = 'Group 1 / Project 1';
+ const mockWebUrl = '/group-1/project-1';
+
+ beforeEach(() => {
+ createComponent({
+ runner: {
+ runnerType: PROJECT_TYPE,
+ ownerProject: {
+ name: mockName,
+ nameWithNamespace: mockNameWithNamespace,
+ webUrl: mockWebUrl,
+ },
+ },
+ });
+ });
+
+ it('Displays a project link', () => {
+ expect(findLink().attributes('href')).toBe(mockWebUrl);
+ expect(wrapper.text()).toBe(mockName);
+ expect(getLinkTooltip()).toBe(mockNameWithNamespace);
+ });
+ });
+
+ describe('When its an empty runner', () => {
+ beforeEach(() => {
+ createComponent({
+ runner: {},
+ });
+ });
+
+ it('shows no label', () => {
+ expect(wrapper.text()).toBe('');
+ });
+ });
+});
diff --git a/spec/frontend/runner/components/cells/runner_stacked_summary_cell_spec.js b/spec/frontend/runner/components/cells/runner_stacked_summary_cell_spec.js
index 21ec9f61f37..e7cadefc140 100644
--- a/spec/frontend/runner/components/cells/runner_stacked_summary_cell_spec.js
+++ b/spec/frontend/runner/components/cells/runner_stacked_summary_cell_spec.js
@@ -85,7 +85,7 @@ describe('RunnerTypeCell', () => {
contactedAt: '2022-01-02',
});
- expect(findRunnerSummaryField('clock').find(TimeAgo).props('time')).toBe('2022-01-02');
+ expect(findRunnerSummaryField('clock').findComponent(TimeAgo).props('time')).toBe('2022-01-02');
});
it('Displays empty last contact', () => {
@@ -93,7 +93,7 @@ describe('RunnerTypeCell', () => {
contactedAt: null,
});
- expect(findRunnerSummaryField('clock').find(TimeAgo).exists()).toBe(false);
+ expect(findRunnerSummaryField('clock').findComponent(TimeAgo).exists()).toBe(false);
expect(findRunnerSummaryField('clock').text()).toContain(__('Never'));
});
@@ -134,7 +134,7 @@ describe('RunnerTypeCell', () => {
});
it('Displays created at', () => {
- expect(findRunnerSummaryField('calendar').find(TimeAgo).props('time')).toBe(
+ expect(findRunnerSummaryField('calendar').findComponent(TimeAgo).props('time')).toBe(
mockRunner.createdAt,
);
});
diff --git a/spec/frontend/runner/components/runner_bulk_delete_checkbox_spec.js b/spec/frontend/runner/components/runner_bulk_delete_checkbox_spec.js
index 0ac89e82314..424a4e61ccd 100644
--- a/spec/frontend/runner/components/runner_bulk_delete_checkbox_spec.js
+++ b/spec/frontend/runner/components/runner_bulk_delete_checkbox_spec.js
@@ -5,11 +5,21 @@ import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import RunnerBulkDeleteCheckbox from '~/runner/components/runner_bulk_delete_checkbox.vue';
import createMockApollo from 'helpers/mock_apollo_helper';
import { createLocalState } from '~/runner/graphql/list/local_state';
-import { allRunnersData } from '../mock_data';
Vue.use(VueApollo);
-jest.mock('~/flash');
+const makeRunner = (id, deleteRunner = true) => ({
+ id,
+ userPermissions: { deleteRunner },
+});
+
+// Multi-select checkbox possible states:
+const stateToAttrs = {
+ unchecked: { disabled: undefined, checked: undefined, indeterminate: undefined },
+ checked: { disabled: undefined, checked: 'true', indeterminate: undefined },
+ indeterminate: { disabled: undefined, checked: undefined, indeterminate: 'true' },
+ disabled: { disabled: 'true', checked: undefined, indeterminate: undefined },
+};
describe('RunnerBulkDeleteCheckbox', () => {
let wrapper;
@@ -18,12 +28,14 @@ describe('RunnerBulkDeleteCheckbox', () => {
const findCheckbox = () => wrapper.findComponent(GlFormCheckbox);
- const mockRunners = allRunnersData.data.runners.nodes;
- const mockIds = allRunnersData.data.runners.nodes.map(({ id }) => id);
- const mockId = mockIds[0];
- const mockIdAnotherPage = 'RUNNER_IN_ANOTHER_PAGE_ID';
+ const expectCheckboxToBe = (state) => {
+ const expected = stateToAttrs[state];
+ expect(findCheckbox().attributes('disabled')).toBe(expected.disabled);
+ expect(findCheckbox().attributes('checked')).toBe(expected.checked);
+ expect(findCheckbox().attributes('indeterminate')).toBe(expected.indeterminate);
+ };
- const createComponent = ({ props = {} } = {}) => {
+ const createComponent = ({ runners = [] } = {}) => {
const { cacheConfig, localMutations } = mockState;
const apolloProvider = createMockApollo(undefined, undefined, cacheConfig);
@@ -33,8 +45,7 @@ describe('RunnerBulkDeleteCheckbox', () => {
localMutations,
},
propsData: {
- runners: mockRunners,
- ...props,
+ runners,
},
});
};
@@ -49,31 +60,61 @@ describe('RunnerBulkDeleteCheckbox', () => {
jest.spyOn(mockState.localMutations, 'setRunnersChecked');
});
- describe.each`
- case | is | checkedRunnerIds | disabled | checked | indeterminate
- ${'no runners'} | ${'unchecked'} | ${[]} | ${undefined} | ${undefined} | ${undefined}
- ${'no runners in this page'} | ${'unchecked'} | ${[mockIdAnotherPage]} | ${undefined} | ${undefined} | ${undefined}
- ${'all runners'} | ${'checked'} | ${mockIds} | ${undefined} | ${'true'} | ${undefined}
- ${'some runners'} | ${'indeterminate'} | ${[mockId]} | ${undefined} | ${undefined} | ${'true'}
- ${'all plus other runners'} | ${'checked'} | ${[...mockIds, mockIdAnotherPage]} | ${undefined} | ${'true'} | ${undefined}
- `('When $case are checked', ({ is, checkedRunnerIds, disabled, checked, indeterminate }) => {
- beforeEach(async () => {
+ describe('when all runners can be deleted', () => {
+ const mockIds = ['1', '2', '3'];
+ const mockIdAnotherPage = '4';
+ const mockRunners = mockIds.map((id) => makeRunner(id));
+
+ it.each`
+ case | checkedRunnerIds | state
+ ${'no runners'} | ${[]} | ${'unchecked'}
+ ${'no runners in this page'} | ${[mockIdAnotherPage]} | ${'unchecked'}
+ ${'all runners'} | ${mockIds} | ${'checked'}
+ ${'some runners'} | ${[mockIds[0]]} | ${'indeterminate'}
+ ${'all plus other runners'} | ${[...mockIds, mockIdAnotherPage]} | ${'checked'}
+ `('if $case are checked, checkbox is $state', ({ checkedRunnerIds, state }) => {
mockCheckedRunnerIds = checkedRunnerIds;
- createComponent();
+ createComponent({ runners: mockRunners });
+ expectCheckboxToBe(state);
});
+ });
+
+ describe('when some runners cannot be deleted', () => {
+ it('all allowed runners are selected, checkbox is checked', () => {
+ mockCheckedRunnerIds = ['a', 'b', 'c'];
+ createComponent({
+ runners: [makeRunner('a'), makeRunner('b'), makeRunner('c', false)],
+ });
- it(`is ${is}`, () => {
- expect(findCheckbox().attributes('disabled')).toBe(disabled);
- expect(findCheckbox().attributes('checked')).toBe(checked);
- expect(findCheckbox().attributes('indeterminate')).toBe(indeterminate);
+ expectCheckboxToBe('checked');
+ });
+
+ it('some allowed runners are selected, checkbox is indeterminate', () => {
+ mockCheckedRunnerIds = ['a', 'b'];
+ createComponent({
+ runners: [makeRunner('a'), makeRunner('b'), makeRunner('c')],
+ });
+
+ expectCheckboxToBe('indeterminate');
+ });
+
+ it('no allowed runners are selected, checkbox is disabled', () => {
+ mockCheckedRunnerIds = ['a', 'b'];
+ createComponent({
+ runners: [makeRunner('a', false), makeRunner('b', false)],
+ });
+
+ expectCheckboxToBe('disabled');
});
});
describe('When user selects', () => {
+ const mockRunners = [makeRunner('1'), makeRunner('2')];
+
beforeEach(() => {
- mockCheckedRunnerIds = mockIds;
- createComponent();
+ mockCheckedRunnerIds = ['1', '2'];
+ createComponent({ runners: mockRunners });
});
it.each([[true], [false]])('sets checked to %s', (checked) => {
@@ -89,13 +130,11 @@ describe('RunnerBulkDeleteCheckbox', () => {
describe('When runners are loading', () => {
beforeEach(() => {
- createComponent({ props: { runners: [] } });
+ createComponent();
});
- it(`is disabled`, () => {
- expect(findCheckbox().attributes('disabled')).toBe('true');
- expect(findCheckbox().attributes('checked')).toBe(undefined);
- expect(findCheckbox().attributes('indeterminate')).toBe(undefined);
+ it('is disabled', () => {
+ expectCheckboxToBe('disabled');
});
});
});
diff --git a/spec/frontend/runner/components/runner_delete_button_spec.js b/spec/frontend/runner/components/runner_delete_button_spec.js
index 52fe803c536..c8fb7a69379 100644
--- a/spec/frontend/runner/components/runner_delete_button_spec.js
+++ b/spec/frontend/runner/components/runner_delete_button_spec.js
@@ -9,11 +9,7 @@ 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,
- I18N_DELETE_DISABLED_MANY_PROJECTS,
- I18N_DELETE_DISABLED_UNKNOWN_REASON,
-} from '~/runner/constants';
+import { I18N_DELETE_RUNNER } from '~/runner/constants';
import RunnerDeleteButton from '~/runner/components/runner_delete_button.vue';
import RunnerDeleteModal from '~/runner/components/runner_delete_modal.vue';
@@ -267,29 +263,4 @@ 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_details_spec.js b/spec/frontend/runner/components/runner_details_spec.js
index f2281223a25..e6cc936e260 100644
--- a/spec/frontend/runner/components/runner_details_spec.js
+++ b/spec/frontend/runner/components/runner_details_spec.js
@@ -25,12 +25,7 @@ describe('RunnerDetails', () => {
const findDetailGroups = () => wrapper.findComponent(RunnerGroups);
- const createComponent = ({
- props = {},
- stubs,
- mountFn = shallowMountExtended,
- enforceRunnerTokenExpiresAt = false,
- } = {}) => {
+ const createComponent = ({ props = {}, stubs, mountFn = shallowMountExtended } = {}) => {
wrapper = mountFn(RunnerDetails, {
propsData: {
...props,
@@ -39,9 +34,6 @@ describe('RunnerDetails', () => {
RunnerDetail,
...stubs,
},
- provide: {
- glFeatures: { enforceRunnerTokenExpiresAt },
- },
});
};
@@ -82,7 +74,6 @@ describe('RunnerDetails', () => {
...runner,
},
},
- enforceRunnerTokenExpiresAt: true,
stubs: {
GlIntersperse,
GlSprintf,
@@ -135,22 +126,5 @@ describe('RunnerDetails', () => {
expect(findDetailGroups().props('runner')).toEqual(mockGroupRunner);
});
});
-
- describe('Token expiration field', () => {
- it.each`
- case | flag | shown
- ${'is shown when feature flag is enabled'} | ${true} | ${true}
- ${'is not shown when feature flag is disabled'} | ${false} | ${false}
- `('$case', ({ flag, shown }) => {
- createComponent({
- props: {
- runner: mockGroupRunner,
- },
- enforceRunnerTokenExpiresAt: flag,
- });
-
- expect(findDd('Token expiry', wrapper).exists()).toBe(shown);
- });
- });
});
});
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 e35bec3aa38..c92e19f9263 100644
--- a/spec/frontend/runner/components/runner_filtered_search_bar_spec.js
+++ b/spec/frontend/runner/components/runner_filtered_search_bar_spec.js
@@ -4,10 +4,26 @@ 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_ONLINE, INSTANCE_TYPE } from '~/runner/constants';
+import {
+ PARAM_KEY_STATUS,
+ PARAM_KEY_TAG,
+ STATUS_ONLINE,
+ INSTANCE_TYPE,
+ DEFAULT_MEMBERSHIP,
+ DEFAULT_SORT,
+ CONTACTED_DESC,
+} 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';
+const mockSearch = {
+ runnerType: null,
+ membership: DEFAULT_MEMBERSHIP,
+ filters: [],
+ pagination: { page: 1 },
+ sort: DEFAULT_SORT,
+};
+
describe('RunnerList', () => {
let wrapper;
@@ -15,8 +31,7 @@ describe('RunnerList', () => {
const findGlFilteredSearch = () => wrapper.findComponent(GlFilteredSearch);
const findSortOptions = () => wrapper.findAllComponents(GlDropdownItem);
- const mockDefaultSort = 'CREATED_DESC';
- const mockOtherSort = 'CONTACTED_DESC';
+ const mockOtherSort = CONTACTED_DESC;
const mockFilters = [
{ type: PARAM_KEY_STATUS, value: { data: STATUS_ONLINE, operator: '=' } },
{ type: 'filtered-search-term', value: { data: '' } },
@@ -32,11 +47,7 @@ describe('RunnerList', () => {
propsData: {
namespace: 'runners',
tokens: [],
- value: {
- runnerType: null,
- filters: [],
- sort: mockDefaultSort,
- },
+ value: mockSearch,
...props,
},
stubs: {
@@ -115,6 +126,7 @@ describe('RunnerList', () => {
props: {
value: {
runnerType: INSTANCE_TYPE,
+ membership: DEFAULT_MEMBERSHIP,
sort: mockOtherSort,
filters: mockFilters,
},
@@ -141,6 +153,7 @@ describe('RunnerList', () => {
expectToHaveLastEmittedInput({
runnerType: INSTANCE_TYPE,
+ membership: DEFAULT_MEMBERSHIP,
filters: mockFilters,
sort: mockOtherSort,
pagination: {},
@@ -154,8 +167,9 @@ describe('RunnerList', () => {
expectToHaveLastEmittedInput({
runnerType: null,
+ membership: DEFAULT_MEMBERSHIP,
filters: mockFilters,
- sort: mockDefaultSort,
+ sort: DEFAULT_SORT,
pagination: {},
});
});
@@ -165,6 +179,7 @@ describe('RunnerList', () => {
expectToHaveLastEmittedInput({
runnerType: null,
+ membership: DEFAULT_MEMBERSHIP,
filters: [],
sort: mockOtherSort,
pagination: {},
diff --git a/spec/frontend/runner/components/runner_list_empty_state_spec.js b/spec/frontend/runner/components/runner_list_empty_state_spec.js
index 59cff863106..038162b889e 100644
--- a/spec/frontend/runner/components/runner_list_empty_state_spec.js
+++ b/spec/frontend/runner/components/runner_list_empty_state_spec.js
@@ -8,6 +8,7 @@ import RunnerListEmptyState from '~/runner/components/runner_list_empty_state.vu
const mockSvgPath = 'mock-svg-path.svg';
const mockFilteredSvgPath = 'mock-filtered-svg-path.svg';
+const mockRegistrationToken = 'REGISTRATION_TOKEN';
describe('RunnerListEmptyState', () => {
let wrapper;
@@ -21,6 +22,7 @@ describe('RunnerListEmptyState', () => {
propsData: {
svgPath: mockSvgPath,
filteredSvgPath: mockFilteredSvgPath,
+ registrationToken: mockRegistrationToken,
...props,
},
directives: {
@@ -35,27 +37,52 @@ describe('RunnerListEmptyState', () => {
};
describe('when search is not filtered', () => {
- beforeEach(() => {
- createComponent();
- });
+ const title = s__('Runners|Get started with runners');
- it('renders an illustration', () => {
- expect(findEmptyState().props('svgPath')).toBe(mockSvgPath);
- });
+ describe('when there is a registration token', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders an illustration', () => {
+ expect(findEmptyState().props('svgPath')).toBe(mockSvgPath);
+ });
+
+ it('displays "no results" text with instructions', () => {
+ const desc = s__(
+ 'Runners|Runners are the agents that run your CI/CD jobs. Follow the %{linkStart}installation and registration instructions%{linkEnd} to set up a runner.',
+ );
- it('displays "no results" text', () => {
- const title = s__('Runners|Get started with runners');
- const desc = s__(
- 'Runners|Runners are the agents that run your CI/CD jobs. Follow the %{linkStart}installation and registration instructions%{linkEnd} to set up a runner.',
- );
+ expect(findEmptyState().text()).toMatchInterpolatedText(`${title} ${desc}`);
+ });
- expect(findEmptyState().text()).toMatchInterpolatedText(`${title} ${desc}`);
+ it('opens a runner registration instructions modal with a link', () => {
+ const { value } = getBinding(findLink().element, 'gl-modal');
+
+ expect(findRunnerInstructionsModal().props('modalId')).toEqual(value);
+ });
});
- it('opens a runner registration instructions modal with a link', () => {
- const { value } = getBinding(findLink().element, 'gl-modal');
+ describe('when there is no registration token', () => {
+ beforeEach(() => {
+ createComponent({ props: { registrationToken: null } });
+ });
+
+ it('renders an illustration', () => {
+ expect(findEmptyState().props('svgPath')).toBe(mockSvgPath);
+ });
+
+ it('displays "no results" text', () => {
+ const desc = s__(
+ 'Runners|Runners are the agents that run your CI/CD jobs. To register new runners, please contact your administrator.',
+ );
+
+ expect(findEmptyState().text()).toMatchInterpolatedText(`${title} ${desc}`);
+ });
- expect(findRunnerInstructionsModal().props('modalId')).toEqual(value);
+ it('has no registration instructions link', () => {
+ expect(findLink().exists()).toBe(false);
+ });
});
});
diff --git a/spec/frontend/runner/components/runner_list_spec.js b/spec/frontend/runner/components/runner_list_spec.js
index 54a9e713721..a31990f8f7e 100644
--- a/spec/frontend/runner/components/runner_list_spec.js
+++ b/spec/frontend/runner/components/runner_list_spec.js
@@ -1,12 +1,19 @@
import { GlTableLite, GlSkeletonLoader } from '@gitlab/ui';
+import HelpPopover from '~/vue_shared/components/help_popover.vue';
import {
extendedWrapper,
shallowMountExtended,
mountExtended,
} from 'helpers/vue_test_utils_helper';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import { s__ } from '~/locale';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import { createLocalState } from '~/runner/graphql/list/local_state';
+
import RunnerList from '~/runner/components/runner_list.vue';
-import RunnerStatusPopover from '~/runner/components/runner_status_popover.vue';
+import RunnerBulkDelete from '~/runner/components/runner_bulk_delete.vue';
+import RunnerBulkDeleteCheckbox from '~/runner/components/runner_bulk_delete_checkbox.vue';
+
import { I18N_PROJECT_TYPE, I18N_STATUS_NEVER_CONTACTED } from '~/runner/constants';
import { allRunnersData, onlineContactTimeoutSecs, staleTimeoutSecs } from '../mock_data';
@@ -15,6 +22,8 @@ const mockActiveRunnersCount = mockRunners.length;
describe('RunnerList', () => {
let wrapper;
+ let cacheConfig;
+ let localMutations;
const findSkeletonLoader = () => wrapper.findComponent(GlSkeletonLoader);
const findTable = () => wrapper.findComponent(GlTableLite);
@@ -22,18 +31,24 @@ describe('RunnerList', () => {
const findRows = () => wrapper.findAll('[data-testid^="runner-row-"]');
const findCell = ({ row = 0, fieldKey }) =>
extendedWrapper(findRows().at(row).find(`[data-testid="td-${fieldKey}"]`));
+ const findRunnerBulkDelete = () => wrapper.findComponent(RunnerBulkDelete);
+ const findRunnerBulkDeleteCheckbox = () => wrapper.findComponent(RunnerBulkDeleteCheckbox);
const createComponent = (
{ props = {}, provide = {}, ...options } = {},
mountFn = shallowMountExtended,
) => {
+ ({ cacheConfig, localMutations } = createLocalState());
+
wrapper = mountFn(RunnerList, {
+ apolloProvider: createMockApollo([], {}, cacheConfig),
propsData: {
runners: mockRunners,
activeRunnersCount: mockActiveRunnersCount,
...props,
},
provide: {
+ localMutations,
onlineContactTimeoutSecs,
staleTimeoutSecs,
...provide,
@@ -50,7 +65,7 @@ describe('RunnerList', () => {
createComponent(
{
stubs: {
- RunnerStatusPopover: {
+ HelpPopover: {
template: '<div/>',
},
},
@@ -60,11 +75,13 @@ describe('RunnerList', () => {
const headerLabels = findHeaders().wrappers.map((w) => w.text());
- expect(findHeaders().at(0).findComponent(RunnerStatusPopover).exists()).toBe(true);
+ expect(findHeaders().at(0).findComponent(HelpPopover).exists()).toBe(true);
+ expect(findHeaders().at(2).findComponent(HelpPopover).exists()).toBe(true);
expect(headerLabels).toEqual([
- 'Status',
- 'Runner',
+ s__('Runners|Status'),
+ s__('Runners|Runner'),
+ s__('Runners|Owner'),
'', // actions has no label
]);
});
@@ -123,21 +140,40 @@ describe('RunnerList', () => {
);
});
+ it('runner bulk delete is available', () => {
+ expect(findRunnerBulkDelete().props('runners')).toEqual(mockRunners);
+ });
+
+ it('runner bulk delete checkbox is available', () => {
+ expect(findRunnerBulkDeleteCheckbox().props('runners')).toEqual(mockRunners);
+ });
+
it('Displays a checkbox field', () => {
expect(findCell({ fieldKey: 'checkbox' }).find('input').exists()).toBe(true);
});
- it('Emits a checked event', async () => {
- const checkbox = findCell({ fieldKey: 'checkbox' }).find('input');
+ it('Sets a runner as checked', async () => {
+ const runner = mockRunners[0];
+ const setRunnerCheckedMock = jest
+ .spyOn(localMutations, 'setRunnerChecked')
+ .mockImplementation(() => {});
+ const checkbox = findCell({ fieldKey: 'checkbox' }).find('input');
await checkbox.setChecked();
- expect(wrapper.emitted('checked')).toHaveLength(1);
- expect(wrapper.emitted('checked')[0][0]).toEqual({
+ expect(setRunnerCheckedMock).toHaveBeenCalledTimes(1);
+ expect(setRunnerCheckedMock).toHaveBeenCalledWith({
+ runner,
isChecked: true,
- runner: mockRunners[0],
});
});
+
+ it('Emits a deleted event', async () => {
+ const event = { message: 'Deleted!' };
+ findRunnerBulkDelete().vm.$emit('deleted', event);
+
+ expect(wrapper.emitted('deleted')).toEqual([[event]]);
+ });
});
describe('Scoped cell slots', () => {
diff --git a/spec/frontend/runner/components/runner_membership_toggle_spec.js b/spec/frontend/runner/components/runner_membership_toggle_spec.js
new file mode 100644
index 00000000000..1a7ae22618a
--- /dev/null
+++ b/spec/frontend/runner/components/runner_membership_toggle_spec.js
@@ -0,0 +1,57 @@
+import { GlToggle } from '@gitlab/ui';
+import { shallowMount, mount } from '@vue/test-utils';
+import RunnerMembershipToggle from '~/runner/components/runner_membership_toggle.vue';
+import {
+ I18N_SHOW_ONLY_INHERITED,
+ MEMBERSHIP_DESCENDANTS,
+ MEMBERSHIP_ALL_AVAILABLE,
+} from '~/runner/constants';
+
+describe('RunnerMembershipToggle', () => {
+ let wrapper;
+
+ const findToggle = () => wrapper.findComponent(GlToggle);
+
+ const createComponent = ({ props = {}, mountFn = shallowMount } = {}) => {
+ wrapper = mountFn(RunnerMembershipToggle, {
+ propsData: props,
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('Displays text', () => {
+ createComponent({ mountFn: mount });
+
+ expect(wrapper.text()).toBe(I18N_SHOW_ONLY_INHERITED);
+ });
+
+ it.each`
+ membershipValue | toggleValue
+ ${MEMBERSHIP_DESCENDANTS} | ${true}
+ ${MEMBERSHIP_ALL_AVAILABLE} | ${false}
+ `(
+ 'Displays a membership of $membershipValue as enabled=$toggleValue',
+ ({ membershipValue, toggleValue }) => {
+ createComponent({ props: { value: membershipValue } });
+
+ expect(findToggle().props('value')).toBe(toggleValue);
+ },
+ );
+
+ it.each`
+ changeEvt | membershipValue
+ ${true} | ${MEMBERSHIP_DESCENDANTS}
+ ${false} | ${MEMBERSHIP_ALL_AVAILABLE}
+ `(
+ 'Emits $changeEvt when value is changed to $membershipValue',
+ ({ changeEvt, membershipValue }) => {
+ createComponent();
+ findToggle().vm.$emit('change', changeEvt);
+
+ expect(wrapper.emitted('input')).toStrictEqual([[membershipValue]]);
+ },
+ );
+});
diff --git a/spec/frontend/runner/components/runner_stacked_layout_banner_spec.js b/spec/frontend/runner/components/runner_stacked_layout_banner_spec.js
index 1a8aced9292..d1f04f0ee37 100644
--- a/spec/frontend/runner/components/runner_stacked_layout_banner_spec.js
+++ b/spec/frontend/runner/components/runner_stacked_layout_banner_spec.js
@@ -29,6 +29,8 @@ describe('RunnerStackedLayoutBanner', () => {
});
it('Does not display a banner when dismissed', async () => {
+ createComponent();
+
findLocalStorageSync().vm.$emit('input', true);
await nextTick();
diff --git a/spec/frontend/runner/components/runner_type_tabs_spec.js b/spec/frontend/runner/components/runner_type_tabs_spec.js
index 45ab8684332..dde35533bc3 100644
--- a/spec/frontend/runner/components/runner_type_tabs_spec.js
+++ b/spec/frontend/runner/components/runner_type_tabs_spec.js
@@ -2,9 +2,21 @@ import { GlTab } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import RunnerTypeTabs from '~/runner/components/runner_type_tabs.vue';
import RunnerCount from '~/runner/components/stat/runner_count.vue';
-import { INSTANCE_TYPE, GROUP_TYPE, PROJECT_TYPE } from '~/runner/constants';
-
-const mockSearch = { runnerType: null, filters: [], pagination: { page: 1 }, sort: 'CREATED_DESC' };
+import {
+ INSTANCE_TYPE,
+ GROUP_TYPE,
+ PROJECT_TYPE,
+ DEFAULT_MEMBERSHIP,
+ DEFAULT_SORT,
+} from '~/runner/constants';
+
+const mockSearch = {
+ runnerType: null,
+ membership: DEFAULT_MEMBERSHIP,
+ filters: [],
+ pagination: { page: 1 },
+ sort: DEFAULT_SORT,
+};
const mockCount = (type, multiplier = 1) => {
let count;
@@ -113,7 +125,7 @@ describe('RunnerTypeTabs', () => {
});
findTabs().wrappers.forEach((tab) => {
- expect(tab.find(RunnerCount).props()).toEqual({
+ expect(tab.findComponent(RunnerCount).props()).toEqual({
scope: INSTANCE_TYPE,
skip: false,
variables: expect.objectContaining(mockVariables),
diff --git a/spec/frontend/runner/components/runner_update_form_spec.js b/spec/frontend/runner/components/runner_update_form_spec.js
index 7b67a89f989..e12736216a0 100644
--- a/spec/frontend/runner/components/runner_update_form_spec.js
+++ b/spec/frontend/runner/components/runner_update_form_spec.js
@@ -145,7 +145,7 @@ describe('RunnerUpdateForm', () => {
});
it('Form skeleton is shown', () => {
- expect(wrapper.find(GlSkeletonLoader).exists()).toBe(true);
+ expect(wrapper.findComponent(GlSkeletonLoader).exists()).toBe(true);
expect(findFields()).toHaveLength(0);
});
diff --git a/spec/frontend/runner/components/search_tokens/tag_token_spec.js b/spec/frontend/runner/components/search_tokens/tag_token_spec.js
index 22f0561ca5f..a7363eb11cd 100644
--- a/spec/frontend/runner/components/search_tokens/tag_token_spec.js
+++ b/spec/frontend/runner/components/search_tokens/tag_token_spec.js
@@ -77,7 +77,7 @@ describe('TagToken', () => {
const findToken = () => wrapper.findComponent(GlToken);
const findGlLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
- beforeEach(async () => {
+ beforeEach(() => {
mock = new MockAdapter(axios);
mock.onGet(TAG_SUGGESTIONS_PATH, { params: { search: '' } }).reply(200, mockTags);
@@ -86,9 +86,6 @@ describe('TagToken', () => {
.reply(200, mockTagsFiltered);
getRecentlyUsedSuggestions.mockReturnValue([]);
-
- createComponent();
- await waitForPromises();
});
afterEach(() => {
@@ -97,11 +94,17 @@ describe('TagToken', () => {
});
describe('when the tags token is displayed', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
it('requests tags suggestions', () => {
expect(mock.history.get[0].params).toEqual({ search: '' });
});
- it('displays tags suggestions', () => {
+ it('displays tags suggestions', async () => {
+ await waitForPromises();
+
mockTags.forEach(({ name }, i) => {
expect(findGlFilteredSearchSuggestions().at(i).text()).toBe(name);
});
@@ -132,13 +135,13 @@ describe('TagToken', () => {
});
describe('when the users filters suggestions', () => {
- beforeEach(async () => {
+ beforeEach(() => {
+ createComponent();
+
findGlFilteredSearchToken().vm.$emit('input', { data: mockSearchTerm });
});
- it('requests filtered tags suggestions', async () => {
- await waitForPromises();
-
+ it('requests filtered tags suggestions', () => {
expect(mock.history.get[1].params).toEqual({ search: mockSearchTerm });
});
@@ -166,7 +169,7 @@ describe('TagToken', () => {
await waitForPromises();
});
- it('error is shown', async () => {
+ it('error is shown', () => {
expect(createAlert).toHaveBeenCalledTimes(1);
expect(createAlert).toHaveBeenCalledWith({ message: expect.any(String) });
});
@@ -180,8 +183,26 @@ describe('TagToken', () => {
await waitForPromises();
});
- it('selected tag is displayed', async () => {
+ it('selected tag is displayed', () => {
expect(findToken().exists()).toBe(true);
});
});
+
+ describe('when suggestions are disabled', () => {
+ beforeEach(async () => {
+ createComponent({
+ config: {
+ ...mockTagTokenConfig,
+ suggestionsDisabled: true,
+ },
+ });
+
+ await waitForPromises();
+ });
+
+ it('displays no suggestions', () => {
+ expect(findGlFilteredSearchSuggestions()).toHaveLength(0);
+ expect(mock.history.get).toHaveLength(0);
+ });
+ });
});
diff --git a/spec/frontend/runner/graphql/local_state_spec.js b/spec/frontend/runner/graphql/local_state_spec.js
index ae874fef00d..915170b53f9 100644
--- a/spec/frontend/runner/graphql/local_state_spec.js
+++ b/spec/frontend/runner/graphql/local_state_spec.js
@@ -4,6 +4,13 @@ import { createLocalState } from '~/runner/graphql/list/local_state';
import getCheckedRunnerIdsQuery from '~/runner/graphql/list/checked_runner_ids.query.graphql';
import { RUNNER_TYPENAME } from '~/runner/constants';
+const makeRunner = (id, deleteRunner = true) => ({
+ id,
+ userPermissions: {
+ deleteRunner,
+ },
+});
+
describe('~/runner/graphql/list/local_state', () => {
let localState;
let apolloClient;
@@ -57,16 +64,21 @@ describe('~/runner/graphql/list/local_state', () => {
});
it('returns checked runners that have a reference in the cache', () => {
- addMockRunnerToCache('a');
- localState.localMutations.setRunnerChecked({ runner: { id: 'a' }, isChecked: true });
+ const id = 'a';
+
+ addMockRunnerToCache(id);
+ localState.localMutations.setRunnerChecked({
+ runner: makeRunner(id),
+ isChecked: true,
+ });
expect(queryCheckedRunnerIds()).toEqual(['a']);
});
it('return checked runners that are not dangling references', () => {
addMockRunnerToCache('a'); // 'b' is missing from the cache, perhaps because it was deleted
- localState.localMutations.setRunnerChecked({ runner: { id: 'a' }, isChecked: true });
- localState.localMutations.setRunnerChecked({ runner: { id: 'b' }, isChecked: true });
+ localState.localMutations.setRunnerChecked({ runner: makeRunner('a'), isChecked: true });
+ localState.localMutations.setRunnerChecked({ runner: makeRunner('b'), isChecked: true });
expect(queryCheckedRunnerIds()).toEqual(['a']);
});
@@ -81,7 +93,7 @@ describe('~/runner/graphql/list/local_state', () => {
beforeEach(() => {
inputs.forEach(([id, isChecked]) => {
addMockRunnerToCache(id);
- localState.localMutations.setRunnerChecked({ runner: { id }, isChecked });
+ localState.localMutations.setRunnerChecked({ runner: makeRunner(id), isChecked });
});
});
it(`for inputs="${inputs}" has a ids="[${expected}]"`, () => {
@@ -102,7 +114,7 @@ describe('~/runner/graphql/list/local_state', () => {
ids.forEach(addMockRunnerToCache);
localState.localMutations.setRunnersChecked({
- runners: ids.map((id) => ({ id })),
+ runners: ids.map((id) => makeRunner(id)),
isChecked,
});
});
@@ -117,7 +129,7 @@ describe('~/runner/graphql/list/local_state', () => {
it('clears all checked items', () => {
['a', 'b', 'c'].forEach((id) => {
addMockRunnerToCache(id);
- localState.localMutations.setRunnerChecked({ runner: { id }, isChecked: true });
+ localState.localMutations.setRunnerChecked({ runner: makeRunner(id), isChecked: true });
});
expect(queryCheckedRunnerIds()).toEqual(['a', 'b', 'c']);
@@ -127,4 +139,29 @@ describe('~/runner/graphql/list/local_state', () => {
expect(queryCheckedRunnerIds()).toEqual([]);
});
});
+
+ describe('when some runners cannot be deleted', () => {
+ beforeEach(() => {
+ addMockRunnerToCache('a');
+ addMockRunnerToCache('b');
+ });
+
+ it('setRunnerChecked does not check runner that cannot be deleted', () => {
+ localState.localMutations.setRunnerChecked({
+ runner: makeRunner('a', false),
+ isChecked: true,
+ });
+
+ expect(queryCheckedRunnerIds()).toEqual([]);
+ });
+
+ it('setRunnersChecked does not check runner that cannot be deleted', () => {
+ localState.localMutations.setRunnersChecked({
+ runners: [makeRunner('a', false), makeRunner('b', false)],
+ isChecked: true,
+ });
+
+ expect(queryCheckedRunnerIds()).toEqual([]);
+ });
+ });
});
diff --git a/spec/frontend/runner/group_runner_show/group_runner_show_app_spec.js b/spec/frontend/runner/group_runner_show/group_runner_show_app_spec.js
index cee1d436942..a3b67674c94 100644
--- a/spec/frontend/runner/group_runner_show/group_runner_show_app_spec.js
+++ b/spec/frontend/runner/group_runner_show/group_runner_show_app_spec.js
@@ -101,6 +101,11 @@ describe('GroupRunnerShowApp', () => {
Platform darwin
Configuration Runs untagged jobs
Maximum job timeout None
+ Token expiry
+ Runner authentication token expiration
+ Runner authentication tokens will expire based on a set interval.
+ They will automatically rotate once expired. Learn more
+ Never expires
Tags None`.replace(/\s+/g, ' ');
expect(wrapper.text().replace(/\s+/g, ' ')).toContain(expected);
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 a17502c7eec..7482926e151 100644
--- a/spec/frontend/runner/group_runners/group_runners_app_spec.js
+++ b/spec/frontend/runner/group_runners/group_runners_app_spec.js
@@ -14,6 +14,7 @@ import { s__ } from '~/locale';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { updateHistory } from '~/lib/utils/url_utility';
import { upgradeStatusTokenConfig } from 'ee_else_ce/runner/components/search_tokens/upgrade_status_token_config';
+import { createLocalState } from '~/runner/graphql/list/local_state';
import RunnerStackedLayoutBanner from '~/runner/components/runner_stacked_layout_banner.vue';
import RunnerTypeTabs from '~/runner/components/runner_type_tabs.vue';
@@ -24,6 +25,7 @@ import RunnerStats from '~/runner/components/stat/runner_stats.vue';
import RunnerActionsCell from '~/runner/components/cells/runner_actions_cell.vue';
import RegistrationDropdown from '~/runner/components/registration/registration_dropdown.vue';
import RunnerPagination from '~/runner/components/runner_pagination.vue';
+import RunnerMembershipToggle from '~/runner/components/runner_membership_toggle.vue';
import {
CREATED_ASC,
@@ -36,9 +38,12 @@ import {
GROUP_TYPE,
PARAM_KEY_PAUSED,
PARAM_KEY_STATUS,
+ PARAM_KEY_TAG,
STATUS_ONLINE,
STATUS_OFFLINE,
STATUS_STALE,
+ MEMBERSHIP_ALL_AVAILABLE,
+ MEMBERSHIP_DESCENDANTS,
RUNNER_PAGE_SIZE,
I18N_EDIT,
} from '~/runner/constants';
@@ -89,15 +94,23 @@ describe('GroupRunnersApp', () => {
const findRunnerPagination = () => extendedWrapper(wrapper.findComponent(RunnerPagination));
const findRunnerPaginationNext = () => findRunnerPagination().findByText(s__('Pagination|Next'));
const findRunnerFilteredSearchBar = () => wrapper.findComponent(RunnerFilteredSearchBar);
+ const findRunnerMembershipToggle = () => wrapper.findComponent(RunnerMembershipToggle);
+
+ const createComponent = ({
+ props = {},
+ provide = {},
+ mountFn = shallowMountExtended,
+ ...options
+ } = {}) => {
+ const { cacheConfig, localMutations } = createLocalState();
- const createComponent = ({ props = {}, mountFn = shallowMountExtended, ...options } = {}) => {
const handlers = [
[groupRunnersQuery, mockGroupRunnersHandler],
[groupRunnersCountQuery, mockGroupRunnersCountHandler],
];
wrapper = mountFn(GroupRunnersApp, {
- apolloProvider: createMockApollo(handlers),
+ apolloProvider: createMockApollo(handlers, {}, cacheConfig),
propsData: {
registrationToken: mockRegistrationToken,
groupFullPath: mockGroupFullPath,
@@ -105,10 +118,12 @@ describe('GroupRunnersApp', () => {
...props,
},
provide: {
+ localMutations,
onlineContactTimeoutSecs,
staleTimeoutSecs,
emptyStateSvgPath,
emptyStateFilteredSvgPath,
+ ...provide,
},
...options,
});
@@ -147,19 +162,50 @@ describe('GroupRunnersApp', () => {
expect(findRegistrationDropdown().props('type')).toBe(GROUP_TYPE);
});
+ describe('show all available runners toggle', () => {
+ it('shows the membership toggle', () => {
+ createComponent();
+ expect(findRunnerMembershipToggle().exists()).toBe(true);
+ });
+
+ it('sets the membership toggle', () => {
+ setWindowLocation(`?membership[]=${MEMBERSHIP_ALL_AVAILABLE}`);
+
+ createComponent();
+
+ expect(findRunnerMembershipToggle().props('value')).toBe(MEMBERSHIP_ALL_AVAILABLE);
+ });
+
+ it('requests filter', async () => {
+ createComponent();
+ findRunnerMembershipToggle().vm.$emit('input', MEMBERSHIP_ALL_AVAILABLE);
+
+ await waitForPromises();
+
+ expect(mockGroupRunnersHandler).toHaveBeenLastCalledWith(
+ expect.objectContaining({
+ membership: MEMBERSHIP_ALL_AVAILABLE,
+ }),
+ );
+ });
+ });
+
it('shows total runner counts', async () => {
await createComponent({ mountFn: mountExtended });
expect(mockGroupRunnersCountHandler).toHaveBeenCalledWith({
status: STATUS_ONLINE,
+ membership: MEMBERSHIP_DESCENDANTS,
groupFullPath: mockGroupFullPath,
});
expect(mockGroupRunnersCountHandler).toHaveBeenCalledWith({
status: STATUS_OFFLINE,
+ membership: MEMBERSHIP_DESCENDANTS,
groupFullPath: mockGroupFullPath,
});
expect(mockGroupRunnersCountHandler).toHaveBeenCalledWith({
status: STATUS_STALE,
+ membership: MEMBERSHIP_DESCENDANTS,
groupFullPath: mockGroupFullPath,
});
@@ -183,6 +229,7 @@ describe('GroupRunnersApp', () => {
groupFullPath: mockGroupFullPath,
status: undefined,
type: undefined,
+ membership: MEMBERSHIP_DESCENDANTS,
sort: DEFAULT_SORT,
first: RUNNER_PAGE_SIZE,
});
@@ -202,6 +249,10 @@ describe('GroupRunnersApp', () => {
type: PARAM_KEY_STATUS,
options: expect.any(Array),
}),
+ expect.objectContaining({
+ type: PARAM_KEY_TAG,
+ suggestionsDisabled: true,
+ }),
upgradeStatusTokenConfig,
]);
});
@@ -213,7 +264,7 @@ describe('GroupRunnersApp', () => {
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
+ const FILTERED_COUNT_QUERIES = 6; // Smart queries that display a count of runners in tabs and single stats
beforeEach(async () => {
await createComponent({ mountFn: mountExtended });
@@ -266,6 +317,7 @@ describe('GroupRunnersApp', () => {
it('sets the filters in the search bar', () => {
expect(findRunnerFilteredSearchBar().props('value')).toEqual({
runnerType: INSTANCE_TYPE,
+ membership: MEMBERSHIP_DESCENDANTS,
filters: [{ type: 'status', value: { data: STATUS_ONLINE, operator: '=' } }],
sort: 'CREATED_DESC',
pagination: {},
@@ -277,6 +329,7 @@ describe('GroupRunnersApp', () => {
groupFullPath: mockGroupFullPath,
status: STATUS_ONLINE,
type: INSTANCE_TYPE,
+ membership: MEMBERSHIP_DESCENDANTS,
sort: DEFAULT_SORT,
first: RUNNER_PAGE_SIZE,
});
@@ -286,6 +339,7 @@ describe('GroupRunnersApp', () => {
expect(mockGroupRunnersCountHandler).toHaveBeenCalledWith({
groupFullPath: mockGroupFullPath,
type: INSTANCE_TYPE,
+ membership: MEMBERSHIP_DESCENDANTS,
status: STATUS_ONLINE,
});
});
@@ -297,6 +351,7 @@ describe('GroupRunnersApp', () => {
findRunnerFilteredSearchBar().vm.$emit('input', {
runnerType: null,
+ membership: MEMBERSHIP_DESCENDANTS,
filters: [{ type: PARAM_KEY_STATUS, value: { data: STATUS_ONLINE, operator: '=' } }],
sort: CREATED_ASC,
});
@@ -315,6 +370,7 @@ describe('GroupRunnersApp', () => {
expect(mockGroupRunnersHandler).toHaveBeenLastCalledWith({
groupFullPath: mockGroupFullPath,
status: STATUS_ONLINE,
+ membership: MEMBERSHIP_DESCENDANTS,
sort: CREATED_ASC,
first: RUNNER_PAGE_SIZE,
});
@@ -324,6 +380,7 @@ describe('GroupRunnersApp', () => {
expect(mockGroupRunnersCountHandler).toHaveBeenCalledWith({
groupFullPath: mockGroupFullPath,
status: STATUS_ONLINE,
+ membership: MEMBERSHIP_DESCENDANTS,
});
});
});
@@ -334,6 +391,11 @@ describe('GroupRunnersApp', () => {
expect(findRunnerPagination().attributes('disabled')).toBe('true');
});
+ it('runners cannot be deleted in bulk', () => {
+ createComponent();
+ expect(findRunnerList().props('checkable')).toBe(false);
+ });
+
describe('when no runners are found', () => {
beforeEach(async () => {
mockGroupRunnersHandler.mockResolvedValue({
@@ -395,6 +457,7 @@ describe('GroupRunnersApp', () => {
expect(mockGroupRunnersHandler).toHaveBeenLastCalledWith({
groupFullPath: mockGroupFullPath,
+ membership: MEMBERSHIP_DESCENDANTS,
sort: CREATED_DESC,
first: RUNNER_PAGE_SIZE,
after: pageInfo.endCursor,
diff --git a/spec/frontend/runner/mock_data.js b/spec/frontend/runner/mock_data.js
index 555ec40184f..da0c0433b3e 100644
--- a/spec/frontend/runner/mock_data.js
+++ b/spec/frontend/runner/mock_data.js
@@ -17,7 +17,7 @@ import groupRunnersData from 'test_fixtures/graphql/runner/list/group_runners.qu
import groupRunnersDataPaginated from 'test_fixtures/graphql/runner/list/group_runners.query.graphql.paginated.json';
import groupRunnersCountData from 'test_fixtures/graphql/runner/list/group_runners_count.query.graphql.json';
-import { RUNNER_PAGE_SIZE } from '~/runner/constants';
+import { DEFAULT_MEMBERSHIP, RUNNER_PAGE_SIZE } from '~/runner/constants';
const emptyPageInfo = {
__typename: 'PageInfo',
@@ -34,8 +34,18 @@ export const mockSearchExamples = [
{
name: 'a default query',
urlQuery: '',
- search: { runnerType: null, filters: [], pagination: {}, sort: 'CREATED_DESC' },
- graphqlVariables: { sort: 'CREATED_DESC', first: RUNNER_PAGE_SIZE },
+ search: {
+ runnerType: null,
+ membership: DEFAULT_MEMBERSHIP,
+ filters: [],
+ pagination: {},
+ sort: 'CREATED_DESC',
+ },
+ graphqlVariables: {
+ membership: DEFAULT_MEMBERSHIP,
+ sort: 'CREATED_DESC',
+ first: RUNNER_PAGE_SIZE,
+ },
isDefault: true,
},
{
@@ -43,17 +53,24 @@ export const mockSearchExamples = [
urlQuery: '?status[]=ACTIVE',
search: {
runnerType: null,
+ membership: DEFAULT_MEMBERSHIP,
filters: [{ type: 'status', value: { data: 'ACTIVE', operator: '=' } }],
pagination: {},
sort: 'CREATED_DESC',
},
- graphqlVariables: { status: 'ACTIVE', sort: 'CREATED_DESC', first: RUNNER_PAGE_SIZE },
+ graphqlVariables: {
+ membership: DEFAULT_MEMBERSHIP,
+ status: 'ACTIVE',
+ sort: 'CREATED_DESC',
+ first: RUNNER_PAGE_SIZE,
+ },
},
{
name: 'a single term text search',
urlQuery: '?search=something',
search: {
runnerType: null,
+ membership: DEFAULT_MEMBERSHIP,
filters: [
{
type: 'filtered-search-term',
@@ -63,13 +80,19 @@ export const mockSearchExamples = [
pagination: {},
sort: 'CREATED_DESC',
},
- graphqlVariables: { search: 'something', sort: 'CREATED_DESC', first: RUNNER_PAGE_SIZE },
+ graphqlVariables: {
+ membership: DEFAULT_MEMBERSHIP,
+ search: 'something',
+ sort: 'CREATED_DESC',
+ first: RUNNER_PAGE_SIZE,
+ },
},
{
name: 'a two terms text search',
urlQuery: '?search=something+else',
search: {
runnerType: null,
+ membership: DEFAULT_MEMBERSHIP,
filters: [
{
type: 'filtered-search-term',
@@ -83,24 +106,36 @@ export const mockSearchExamples = [
pagination: {},
sort: 'CREATED_DESC',
},
- graphqlVariables: { search: 'something else', sort: 'CREATED_DESC', first: RUNNER_PAGE_SIZE },
+ graphqlVariables: {
+ membership: DEFAULT_MEMBERSHIP,
+ search: 'something else',
+ sort: 'CREATED_DESC',
+ first: RUNNER_PAGE_SIZE,
+ },
},
{
name: 'single instance type',
urlQuery: '?runner_type[]=INSTANCE_TYPE',
search: {
runnerType: 'INSTANCE_TYPE',
+ membership: DEFAULT_MEMBERSHIP,
filters: [],
pagination: {},
sort: 'CREATED_DESC',
},
- graphqlVariables: { type: 'INSTANCE_TYPE', sort: 'CREATED_DESC', first: RUNNER_PAGE_SIZE },
+ graphqlVariables: {
+ type: 'INSTANCE_TYPE',
+ membership: DEFAULT_MEMBERSHIP,
+ sort: 'CREATED_DESC',
+ first: RUNNER_PAGE_SIZE,
+ },
},
{
name: 'multiple runner status',
urlQuery: '?status[]=ACTIVE&status[]=PAUSED',
search: {
runnerType: null,
+ membership: DEFAULT_MEMBERSHIP,
filters: [
{ type: 'status', value: { data: 'ACTIVE', operator: '=' } },
{ type: 'status', value: { data: 'PAUSED', operator: '=' } },
@@ -108,13 +143,19 @@ export const mockSearchExamples = [
pagination: {},
sort: 'CREATED_DESC',
},
- graphqlVariables: { status: 'ACTIVE', sort: 'CREATED_DESC', first: RUNNER_PAGE_SIZE },
+ graphqlVariables: {
+ status: 'ACTIVE',
+ membership: DEFAULT_MEMBERSHIP,
+ sort: 'CREATED_DESC',
+ first: RUNNER_PAGE_SIZE,
+ },
},
{
name: 'multiple status, a single instance type and a non default sort',
urlQuery: '?status[]=ACTIVE&runner_type[]=INSTANCE_TYPE&sort=CREATED_ASC',
search: {
runnerType: 'INSTANCE_TYPE',
+ membership: DEFAULT_MEMBERSHIP,
filters: [{ type: 'status', value: { data: 'ACTIVE', operator: '=' } }],
pagination: {},
sort: 'CREATED_ASC',
@@ -122,6 +163,7 @@ export const mockSearchExamples = [
graphqlVariables: {
status: 'ACTIVE',
type: 'INSTANCE_TYPE',
+ membership: DEFAULT_MEMBERSHIP,
sort: 'CREATED_ASC',
first: RUNNER_PAGE_SIZE,
},
@@ -131,11 +173,13 @@ export const mockSearchExamples = [
urlQuery: '?tag[]=tag-1',
search: {
runnerType: null,
+ membership: DEFAULT_MEMBERSHIP,
filters: [{ type: 'tag', value: { data: 'tag-1', operator: '=' } }],
pagination: {},
sort: 'CREATED_DESC',
},
graphqlVariables: {
+ membership: DEFAULT_MEMBERSHIP,
tagList: ['tag-1'],
first: 20,
sort: 'CREATED_DESC',
@@ -146,6 +190,7 @@ export const mockSearchExamples = [
urlQuery: '?tag[]=tag-1&tag[]=tag-2',
search: {
runnerType: null,
+ membership: DEFAULT_MEMBERSHIP,
filters: [
{ type: 'tag', value: { data: 'tag-1', operator: '=' } },
{ type: 'tag', value: { data: 'tag-2', operator: '=' } },
@@ -154,6 +199,7 @@ export const mockSearchExamples = [
sort: 'CREATED_DESC',
},
graphqlVariables: {
+ membership: DEFAULT_MEMBERSHIP,
tagList: ['tag-1', 'tag-2'],
first: 20,
sort: 'CREATED_DESC',
@@ -164,22 +210,34 @@ export const mockSearchExamples = [
urlQuery: '?after=AFTER_CURSOR',
search: {
runnerType: null,
+ membership: DEFAULT_MEMBERSHIP,
filters: [],
pagination: { after: 'AFTER_CURSOR' },
sort: 'CREATED_DESC',
},
- graphqlVariables: { sort: 'CREATED_DESC', after: 'AFTER_CURSOR', first: RUNNER_PAGE_SIZE },
+ graphqlVariables: {
+ membership: DEFAULT_MEMBERSHIP,
+ sort: 'CREATED_DESC',
+ after: 'AFTER_CURSOR',
+ first: RUNNER_PAGE_SIZE,
+ },
},
{
name: 'the previous page',
urlQuery: '?before=BEFORE_CURSOR',
search: {
runnerType: null,
+ membership: DEFAULT_MEMBERSHIP,
filters: [],
pagination: { before: 'BEFORE_CURSOR' },
sort: 'CREATED_DESC',
},
- graphqlVariables: { sort: 'CREATED_DESC', before: 'BEFORE_CURSOR', last: RUNNER_PAGE_SIZE },
+ graphqlVariables: {
+ membership: DEFAULT_MEMBERSHIP,
+ sort: 'CREATED_DESC',
+ before: 'BEFORE_CURSOR',
+ last: RUNNER_PAGE_SIZE,
+ },
},
{
name: 'the next page filtered by a status, an instance type, tags and a non default sort',
@@ -187,6 +245,7 @@ export const mockSearchExamples = [
'?status[]=ACTIVE&runner_type[]=INSTANCE_TYPE&tag[]=tag-1&tag[]=tag-2&sort=CREATED_ASC&after=AFTER_CURSOR',
search: {
runnerType: 'INSTANCE_TYPE',
+ membership: DEFAULT_MEMBERSHIP,
filters: [
{ type: 'status', value: { data: 'ACTIVE', operator: '=' } },
{ type: 'tag', value: { data: 'tag-1', operator: '=' } },
@@ -198,6 +257,7 @@ export const mockSearchExamples = [
graphqlVariables: {
status: 'ACTIVE',
type: 'INSTANCE_TYPE',
+ membership: DEFAULT_MEMBERSHIP,
tagList: ['tag-1', 'tag-2'],
sort: 'CREATED_ASC',
after: 'AFTER_CURSOR',
@@ -209,22 +269,34 @@ export const mockSearchExamples = [
urlQuery: '?paused[]=true',
search: {
runnerType: null,
+ membership: DEFAULT_MEMBERSHIP,
filters: [{ type: 'paused', value: { data: 'true', operator: '=' } }],
pagination: {},
sort: 'CREATED_DESC',
},
- graphqlVariables: { paused: true, sort: 'CREATED_DESC', first: RUNNER_PAGE_SIZE },
+ graphqlVariables: {
+ paused: true,
+ membership: DEFAULT_MEMBERSHIP,
+ sort: 'CREATED_DESC',
+ first: RUNNER_PAGE_SIZE,
+ },
},
{
name: 'active runners',
urlQuery: '?paused[]=false',
search: {
runnerType: null,
+ membership: DEFAULT_MEMBERSHIP,
filters: [{ type: 'paused', value: { data: 'false', operator: '=' } }],
pagination: {},
sort: 'CREATED_DESC',
},
- graphqlVariables: { paused: false, sort: 'CREATED_DESC', first: RUNNER_PAGE_SIZE },
+ graphqlVariables: {
+ paused: false,
+ membership: DEFAULT_MEMBERSHIP,
+ sort: 'CREATED_DESC',
+ first: RUNNER_PAGE_SIZE,
+ },
},
];
diff --git a/spec/frontend/search/sidebar/components/app_spec.js b/spec/frontend/search/sidebar/components/app_spec.js
index 3bea0748c47..89959feec39 100644
--- a/spec/frontend/search/sidebar/components/app_spec.js
+++ b/spec/frontend/search/sidebar/components/app_spec.js
@@ -42,20 +42,39 @@ describe('GlobalSearchSidebar', () => {
const findResetLinkButton = () => wrapper.findComponent(GlLink);
describe('template', () => {
- beforeEach(() => {
- createComponent();
- });
+ describe('scope=projects', () => {
+ beforeEach(() => {
+ createComponent({ urlQuery: { ...MOCK_QUERY, scope: 'projects' } });
+ });
- it('renders StatusFilter always', () => {
- expect(findStatusFilter().exists()).toBe(true);
- });
+ it("doesn't render StatusFilter", () => {
+ expect(findStatusFilter().exists()).toBe(false);
+ });
+
+ it("doesn't render ConfidentialityFilter", () => {
+ expect(findConfidentialityFilter().exists()).toBe(false);
+ });
- it('renders ConfidentialityFilter always', () => {
- expect(findConfidentialityFilter().exists()).toBe(true);
+ it("doesn't render ApplyButton", () => {
+ expect(findApplyButton().exists()).toBe(false);
+ });
});
- it('renders ApplyButton always', () => {
- expect(findApplyButton().exists()).toBe(true);
+ describe('scope=issues', () => {
+ beforeEach(() => {
+ createComponent({ urlQuery: MOCK_QUERY });
+ });
+ it('renders StatusFilter', () => {
+ expect(findStatusFilter().exists()).toBe(true);
+ });
+
+ it('renders ConfidentialityFilter', () => {
+ expect(findConfidentialityFilter().exists()).toBe(true);
+ });
+
+ it('renders ApplyButton', () => {
+ expect(findApplyButton().exists()).toBe(true);
+ });
});
});
@@ -115,7 +134,7 @@ describe('GlobalSearchSidebar', () => {
describe('actions', () => {
beforeEach(() => {
- createComponent();
+ createComponent({});
});
it('clicking ApplyButton calls applyQuery', () => {
diff --git a/spec/frontend/search/sidebar/components/confidentiality_filter_spec.js b/spec/frontend/search/sidebar/components/confidentiality_filter_spec.js
index a377ddae0eb..c57eabd57b9 100644
--- a/spec/frontend/search/sidebar/components/confidentiality_filter_spec.js
+++ b/spec/frontend/search/sidebar/components/confidentiality_filter_spec.js
@@ -34,7 +34,7 @@ describe('ConfidentialityFilter', () => {
wrapper = null;
});
- const findRadioFilter = () => wrapper.find(RadioFilter);
+ const findRadioFilter = () => wrapper.findComponent(RadioFilter);
describe('template', () => {
beforeEach(() => {
diff --git a/spec/frontend/search/sidebar/components/radio_filter_spec.js b/spec/frontend/search/sidebar/components/radio_filter_spec.js
index c0a8259b4fe..94d529348a9 100644
--- a/spec/frontend/search/sidebar/components/radio_filter_spec.js
+++ b/spec/frontend/search/sidebar/components/radio_filter_spec.js
@@ -43,7 +43,7 @@ describe('RadioFilter', () => {
wrapper = null;
});
- const findGlRadioButtonGroup = () => wrapper.find(GlFormRadioGroup);
+ const findGlRadioButtonGroup = () => wrapper.findComponent(GlFormRadioGroup);
const findGlRadioButtons = () => findGlRadioButtonGroup().findAllComponents(GlFormRadio);
const findGlRadioButtonsText = () => findGlRadioButtons().wrappers.map((w) => w.text());
diff --git a/spec/frontend/search/sidebar/components/status_filter_spec.js b/spec/frontend/search/sidebar/components/status_filter_spec.js
index 5d8ecd8733a..f3152c014b6 100644
--- a/spec/frontend/search/sidebar/components/status_filter_spec.js
+++ b/spec/frontend/search/sidebar/components/status_filter_spec.js
@@ -34,7 +34,7 @@ describe('StatusFilter', () => {
wrapper = null;
});
- const findRadioFilter = () => wrapper.find(RadioFilter);
+ const findRadioFilter = () => wrapper.findComponent(RadioFilter);
describe('template', () => {
beforeEach(() => {
diff --git a/spec/frontend/search/sort/components/app_spec.js b/spec/frontend/search/sort/components/app_spec.js
index 0e8eebba3cb..a566b9b99d3 100644
--- a/spec/frontend/search/sort/components/app_spec.js
+++ b/spec/frontend/search/sort/components/app_spec.js
@@ -43,9 +43,9 @@ describe('GlobalSearchSort', () => {
wrapper = null;
});
- const findSortButtonGroup = () => wrapper.find(GlButtonGroup);
- const findSortDropdown = () => wrapper.find(GlDropdown);
- const findSortDirectionButton = () => wrapper.find(GlButton);
+ const findSortButtonGroup = () => wrapper.findComponent(GlButtonGroup);
+ const findSortDropdown = () => wrapper.findComponent(GlDropdown);
+ const findSortDirectionButton = () => wrapper.findComponent(GlButton);
const findDropdownItems = () => findSortDropdown().findAllComponents(GlDropdownItem);
const findDropdownItemsText = () => findDropdownItems().wrappers.map((w) => w.text());
diff --git a/spec/frontend/search/store/actions_spec.js b/spec/frontend/search/store/actions_spec.js
index 2f93d3f6805..c442ffa521d 100644
--- a/spec/frontend/search/store/actions_spec.js
+++ b/spec/frontend/search/store/actions_spec.js
@@ -1,7 +1,7 @@
import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
import Api from '~/api';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import * as urlUtils from '~/lib/utils/url_utility';
import * as actions from '~/search/store/actions';
@@ -37,8 +37,8 @@ describe('Global Search Store Actions', () => {
let state;
const flashCallback = (callCount) => {
- expect(createFlash).toHaveBeenCalledTimes(callCount);
- createFlash.mockClear();
+ expect(createAlert).toHaveBeenCalledTimes(callCount);
+ createAlert.mockClear();
};
beforeEach(() => {
diff --git a/spec/frontend/search/topbar/components/app_spec.js b/spec/frontend/search/topbar/components/app_spec.js
index 0a44688bfe0..c7fd7084101 100644
--- a/spec/frontend/search/topbar/components/app_spec.js
+++ b/spec/frontend/search/topbar/components/app_spec.js
@@ -36,9 +36,9 @@ describe('GlobalSearchTopbar', () => {
wrapper.destroy();
});
- const findGlSearchBox = () => wrapper.find(GlSearchBoxByClick);
- const findGroupFilter = () => wrapper.find(GroupFilter);
- const findProjectFilter = () => wrapper.find(ProjectFilter);
+ const findGlSearchBox = () => wrapper.findComponent(GlSearchBoxByClick);
+ const findGroupFilter = () => wrapper.findComponent(GroupFilter);
+ const findProjectFilter = () => wrapper.findComponent(ProjectFilter);
describe('template', () => {
beforeEach(() => {
diff --git a/spec/frontend/search/topbar/components/group_filter_spec.js b/spec/frontend/search/topbar/components/group_filter_spec.js
index bd173791fee..b2d0297fdc2 100644
--- a/spec/frontend/search/topbar/components/group_filter_spec.js
+++ b/spec/frontend/search/topbar/components/group_filter_spec.js
@@ -53,7 +53,7 @@ describe('GroupFilter', () => {
wrapper.destroy();
});
- const findSearchableDropdown = () => wrapper.find(SearchableDropdown);
+ const findSearchableDropdown = () => wrapper.findComponent(SearchableDropdown);
describe('template', () => {
beforeEach(() => {
diff --git a/spec/frontend/search/topbar/components/project_filter_spec.js b/spec/frontend/search/topbar/components/project_filter_spec.js
index 5afcd281d0c..297a536e075 100644
--- a/spec/frontend/search/topbar/components/project_filter_spec.js
+++ b/spec/frontend/search/topbar/components/project_filter_spec.js
@@ -53,7 +53,7 @@ describe('ProjectFilter', () => {
wrapper.destroy();
});
- const findSearchableDropdown = () => wrapper.find(SearchableDropdown);
+ const findSearchableDropdown = () => wrapper.findComponent(SearchableDropdown);
describe('template', () => {
beforeEach(() => {
diff --git a/spec/frontend/search_settings/components/search_settings_spec.js b/spec/frontend/search_settings/components/search_settings_spec.js
index d0a2018c7f0..3f856968db6 100644
--- a/spec/frontend/search_settings/components/search_settings_spec.js
+++ b/spec/frontend/search_settings/components/search_settings_spec.js
@@ -1,4 +1,4 @@
-import { GlSearchBoxByType } from '@gitlab/ui';
+import { GlEmptyState, GlSearchBoxByType } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { setHTMLFixture } from 'helpers/fixtures';
import SearchSettings from '~/search_settings/components/search_settings.vue';
@@ -14,7 +14,7 @@ describe('search_settings/components/search_settings.vue', () => {
const EXTRA_SETTINGS_ID = 'js-extra-settings';
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>.`;
-
+ const HIDE_WHEN_EMPTY_CLASS = 'js-hide-when-nothing-matches-search';
let wrapper;
const buildWrapper = () => {
@@ -22,6 +22,7 @@ describe('search_settings/components/search_settings.vue', () => {
propsData: {
searchRoot: document.querySelector(`#${ROOT_ID}`),
sectionSelector: SECTION_SELECTOR,
+ hideWhenEmptySelector: `.${HIDE_WHEN_EMPTY_CLASS}`,
isExpandedFn: isExpanded,
},
// Add real listeners so we can simplify and strengthen some tests.
@@ -45,7 +46,9 @@ describe('search_settings/components/search_settings.vue', () => {
};
const findMatchSiblingElement = () => document.querySelector(`[data-testid="sibling"]`);
- const findSearchBox = () => wrapper.find(GlSearchBoxByType);
+ const findSearchBox = () => wrapper.findComponent(GlSearchBoxByType);
+ const findEmptyState = () => wrapper.findComponent(GlEmptyState);
+ const findHideWhenEmpty = () => document.querySelector(`.${HIDE_WHEN_EMPTY_CLASS}`);
const search = (term) => {
findSearchBox().vm.$emit('input', term);
};
@@ -67,6 +70,9 @@ describe('search_settings/components/search_settings.vue', () => {
<span>${TEXT_CONTAIN_SEARCH_TERM}</span>
<span>${TEXT_WITH_SIBLING_ELEMENTS}</span>
</section>
+ <div class="row ${HIDE_WHEN_EMPTY_CLASS}">
+ <button type="submit">Save</button>
+ </div>
</div>
</div>
`);
@@ -93,13 +99,41 @@ describe('search_settings/components/search_settings.vue', () => {
expect(wrapper.emitted('expand')).toEqual([[section]]);
});
+ describe('when nothing matches the search term', () => {
+ beforeEach(() => {
+ search('xxxxxxxxxxx');
+ });
+
+ it('shows an empty state', () => {
+ expect(findEmptyState().exists()).toBe(true);
+ });
+
+ it('hides the form buttons', () => {
+ expect(findHideWhenEmpty()).toHaveClass(HIDE_CLASS);
+ });
+ });
+
+ describe('when something matches the search term', () => {
+ beforeEach(() => {
+ search(SEARCH_TERM);
+ });
+
+ it('shows no empty state', () => {
+ expect(findEmptyState().exists()).toBe(false);
+ });
+
+ it('shows the form buttons', () => {
+ expect(findHideWhenEmpty()).not.toHaveClass(HIDE_CLASS);
+ });
+ });
+
it('highlight elements that match the search term', () => {
search(SEARCH_TERM);
expect(highlightedElementsCount()).toBe(3);
});
- it('highlight only search term and not the whole line', () => {
+ it('highlights only search term and not the whole line', () => {
search(SEARCH_TERM);
expect(highlightedTextNodes()).toBe(true);
@@ -142,6 +176,10 @@ describe('search_settings/components/search_settings.vue', () => {
expect(visibleSectionsCount()).toBe(sectionsCount());
});
+ it('hides the empty state', () => {
+ expect(findEmptyState().exists()).toBe(false);
+ });
+
it('removes the highlight from all elements', () => {
expect(highlightedElementsCount()).toBe(0);
});
diff --git a/spec/frontend/security_configuration/components/app_spec.js b/spec/frontend/security_configuration/components/app_spec.js
index 222cabc6a63..ddefda2ffc3 100644
--- a/spec/frontend/security_configuration/components/app_spec.js
+++ b/spec/frontend/security_configuration/components/app_spec.js
@@ -281,7 +281,7 @@ describe('App component', () => {
});
});
- it(shouldRender ? 'renders' : 'does not render', () => {
+ it(`${shouldRender ? 'renders' : 'does not render'}`, () => {
expect(findAutoDevopsEnabledAlert().exists()).toBe(shouldRender);
});
});
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 b6451af57d7..8f2b5383191 100644
--- a/spec/frontend/security_configuration/components/training_provider_list_spec.js
+++ b/spec/frontend/security_configuration/components/training_provider_list_spec.js
@@ -193,7 +193,7 @@ describe('TrainingProviderList component', () => {
});
it(`shows the learn more link for enabled card ${index}`, () => {
- const learnMoreLink = findCards().at(index).find(GlLink);
+ const learnMoreLink = findCards().at(index).findComponent(GlLink);
const tempLogo = TEMP_PROVIDER_URLS[name];
if (tempLogo) {
@@ -224,7 +224,7 @@ describe('TrainingProviderList component', () => {
});
it('shows a info-tooltip that describes the purpose of a primary provider', () => {
- const infoIcon = findPrimaryProviderRadios().at(index).find(GlIcon);
+ const infoIcon = findPrimaryProviderRadios().at(index).findComponent(GlIcon);
const tooltip = getBinding(infoIcon.element, 'gl-tooltip');
expect(infoIcon.props()).toMatchObject({
diff --git a/spec/frontend/security_configuration/components/upgrade_banner_spec.js b/spec/frontend/security_configuration/components/upgrade_banner_spec.js
index ff44acfc4f9..c34d8e47a6c 100644
--- a/spec/frontend/security_configuration/components/upgrade_banner_spec.js
+++ b/spec/frontend/security_configuration/components/upgrade_banner_spec.js
@@ -79,7 +79,7 @@ describe('UpgradeBanner component', () => {
expect(wrapperText).toContain('statistics in the merge request');
expect(wrapperText).toContain('statistics across projects');
expect(wrapperText).toContain('Runtime security metrics');
- expect(wrapperText).toContain('More scan types, including Container Scanning,');
+ expect(wrapperText).toContain('More scan types, including DAST,');
});
describe('when user interacts', () => {
diff --git a/spec/frontend/self_monitor/components/self_monitor_form_spec.js b/spec/frontend/self_monitor/components/self_monitor_form_spec.js
index 89ad5a00a14..c690bbf1c57 100644
--- a/spec/frontend/self_monitor/components/self_monitor_form_spec.js
+++ b/spec/frontend/self_monitor/components/self_monitor_form_spec.js
@@ -42,7 +42,7 @@ describe('self monitor component', () => {
it('renders as an expand button by default', () => {
wrapper = shallowMount(SelfMonitor, { store });
- const button = wrapper.find(GlButton);
+ const button = wrapper.findComponent(GlButton);
expect(button.text()).toBe('Expand');
});
@@ -79,7 +79,7 @@ describe('self monitor component', () => {
wrapper = shallowMount(SelfMonitor, { store });
expect(
- wrapper.find({ ref: 'selfMonitoringFormText' }).find('a').attributes('href'),
+ wrapper.findComponent({ ref: 'selfMonitoringFormText' }).find('a').attributes('href'),
).toEqual(`${TEST_HOST}/instance-administrators-random/gitlab-self-monitoring`);
});
diff --git a/spec/frontend/set_status_modal/set_status_form_spec.js b/spec/frontend/set_status_modal/set_status_form_spec.js
index 8e1623eedf5..486e06d2906 100644
--- a/spec/frontend/set_status_modal/set_status_form_spec.js
+++ b/spec/frontend/set_status_modal/set_status_form_spec.js
@@ -127,6 +127,8 @@ describe('SetStatusForm', () => {
describe('when `Clear status after` dropdown is changed', () => {
it('emits `clear-status-after-click`', async () => {
+ await createComponent();
+
await wrapper.findByTestId('thirtyMinutes').trigger('click');
expect(wrapper.emitted('clear-status-after-click')).toEqual([[timeRanges[0]]]);
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 c5fb590646d..53d2a9e0978 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
@@ -4,7 +4,7 @@ import { mountExtended } from 'helpers/vue_test_utils_helper';
import { initEmojiMock, clearEmojiMock } from 'helpers/emoji';
import * as UserApi from '~/api/user_api';
import EmojiPicker from '~/emoji/components/picker.vue';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import stubChildren from 'helpers/stub_children';
import SetStatusModalWrapper from '~/set_status_modal/set_status_modal_wrapper.vue';
import { AVAILABILITY_STATUS } from '~/set_status_modal/constants';
@@ -51,11 +51,11 @@ describe('SetStatusModalWrapper', () => {
});
};
- const findModal = () => wrapper.find(GlModal);
+ const findModal = () => wrapper.findComponent(GlModal);
const findMessageField = () =>
wrapper.findByPlaceholderText(SetStatusForm.i18n.statusMessagePlaceholder);
const findClearStatusButton = () => wrapper.find('.js-clear-user-status-button');
- const findAvailabilityCheckbox = () => wrapper.find(GlFormCheckbox);
+ const findAvailabilityCheckbox = () => wrapper.findComponent(GlFormCheckbox);
const findClearStatusAtMessage = () => wrapper.find('[data-testid="clear-status-at-message"]');
const getEmojiPicker = () => wrapper.findComponent(EmojiPickerStub);
@@ -253,7 +253,7 @@ describe('SetStatusModalWrapper', () => {
findModal().vm.$emit('primary');
await nextTick();
- expect(createFlash).toHaveBeenCalledWith({
+ expect(createAlert).toHaveBeenCalledWith({
message: "Sorry, we weren't able to set your status. Please try again later.",
});
});
diff --git a/spec/frontend/sidebar/assignee_title_spec.js b/spec/frontend/sidebar/assignee_title_spec.js
index e29e3d489a5..14a6bdbf907 100644
--- a/spec/frontend/sidebar/assignee_title_spec.js
+++ b/spec/frontend/sidebar/assignee_title_spec.js
@@ -85,7 +85,7 @@ describe('AssigneeTitle component', () => {
editable: false,
});
- expect(wrapper.find(GlLoadingIcon).exists()).toBe(false);
+ expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(false);
});
it('renders spinner when loading', () => {
@@ -95,7 +95,7 @@ describe('AssigneeTitle component', () => {
editable: false,
});
- expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
+ expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
});
it('does not render edit link when not editable', () => {
diff --git a/spec/frontend/sidebar/assignees_spec.js b/spec/frontend/sidebar/assignees_spec.js
index c2aff456abb..7cf7fd33022 100644
--- a/spec/frontend/sidebar/assignees_spec.js
+++ b/spec/frontend/sidebar/assignees_spec.js
@@ -33,7 +33,7 @@ describe('Assignee component', () => {
it('displays no assignee icon when collapsed', () => {
createWrapper();
const collapsedChildren = findCollapsedChildren();
- const userIcon = collapsedChildren.at(0).find(GlIcon);
+ const userIcon = collapsedChildren.at(0).findComponent(GlIcon);
expect(collapsedChildren.length).toBe(1);
expect(collapsedChildren.at(0).attributes('aria-label')).toBe('None');
diff --git a/spec/frontend/sidebar/components/assignees/assignee_avatar_link_spec.js b/spec/frontend/sidebar/components/assignees/assignee_avatar_link_spec.js
index 8cde70ff8da..4764f3607bc 100644
--- a/spec/frontend/sidebar/components/assignees/assignee_avatar_link_spec.js
+++ b/spec/frontend/sidebar/components/assignees/assignee_avatar_link_spec.js
@@ -46,7 +46,7 @@ describe('AssigneeAvatarLink component', () => {
it('renders assignee avatar', () => {
createComponent();
- expect(wrapper.find(AssigneeAvatar).props()).toEqual(
+ expect(wrapper.findComponent(AssigneeAvatar).props()).toEqual(
expect.objectContaining({
issuableType: TEST_ISSUABLE_TYPE,
user: userDataMock(),
diff --git a/spec/frontend/sidebar/components/assignees/collapsed_assignee_list_spec.js b/spec/frontend/sidebar/components/assignees/collapsed_assignee_list_spec.js
index 81ff51133bf..7e7d4921cfa 100644
--- a/spec/frontend/sidebar/components/assignees/collapsed_assignee_list_spec.js
+++ b/spec/frontend/sidebar/components/assignees/collapsed_assignee_list_spec.js
@@ -21,7 +21,7 @@ describe('CollapsedAssigneeList component', () => {
});
}
- const findNoUsersIcon = () => wrapper.find(GlIcon);
+ const findNoUsersIcon = () => wrapper.findComponent(GlIcon);
const findAvatarCounter = () => wrapper.find('.avatar-counter');
const findAssignees = () => wrapper.findAllComponents(CollapsedAssignee);
const getTooltipTitle = () => wrapper.attributes('title');
diff --git a/spec/frontend/sidebar/components/assignees/collapsed_assignee_spec.js b/spec/frontend/sidebar/components/assignees/collapsed_assignee_spec.js
index 2d5a3653631..4db95114b96 100644
--- a/spec/frontend/sidebar/components/assignees/collapsed_assignee_spec.js
+++ b/spec/frontend/sidebar/components/assignees/collapsed_assignee_spec.js
@@ -34,7 +34,7 @@ describe('CollapsedAssignee assignee component', () => {
it('has assignee avatar', () => {
createComponent();
- expect(wrapper.find(AssigneeAvatar).props()).toEqual({
+ expect(wrapper.findComponent(AssigneeAvatar).props()).toEqual({
imgSize: 24,
user: TEST_USER,
issuableType: TEST_ISSUABLE_TYPE,
diff --git a/spec/frontend/sidebar/components/assignees/sidebar_assignees_widget_spec.js b/spec/frontend/sidebar/components/assignees/sidebar_assignees_widget_spec.js
index 3644a51c7fd..cbb4c41dd14 100644
--- a/spec/frontend/sidebar/components/assignees/sidebar_assignees_widget_spec.js
+++ b/spec/frontend/sidebar/components/assignees/sidebar_assignees_widget_spec.js
@@ -5,7 +5,7 @@ import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { IssuableType } from '~/issues/constants';
import SidebarAssigneesRealtime from '~/sidebar/components/assignees/assignees_realtime.vue';
import IssuableAssignees from '~/sidebar/components/assignees/issuable_assignees.vue';
@@ -167,7 +167,7 @@ describe('Sidebar assignees widget', () => {
});
await waitForPromises();
- expect(createFlash).toHaveBeenCalledWith({
+ expect(createAlert).toHaveBeenCalledWith({
message: 'An error occurred while fetching participants.',
});
});
@@ -333,7 +333,7 @@ describe('Sidebar assignees widget', () => {
await waitForPromises();
- expect(createFlash).toHaveBeenCalledWith({
+ expect(createAlert).toHaveBeenCalledWith({
message: 'An error occurred while updating assignees.',
});
});
diff --git a/spec/frontend/sidebar/components/assignees/sidebar_editable_item_spec.js b/spec/frontend/sidebar/components/assignees/sidebar_editable_item_spec.js
index 724fba62479..6c22d2f687d 100644
--- a/spec/frontend/sidebar/components/assignees/sidebar_editable_item_spec.js
+++ b/spec/frontend/sidebar/components/assignees/sidebar_editable_item_spec.js
@@ -67,15 +67,33 @@ describe('boards sidebar remove issue', () => {
expect(findLoader().exists()).toBe(true);
});
- it('shows expanded content and hides collapsed content when clicking edit button', async () => {
- const slots = { default: '<div>Select item</div>' };
- createComponent({ canUpdate: true, slots });
- findEditButton().vm.$emit('click');
-
- await nextTick();
-
- expect(findCollapsed().isVisible()).toBe(false);
- expect(findExpanded().isVisible()).toBe(true);
+ describe('when clicking edit button', () => {
+ describe('when can edit', () => {
+ it('shows expanded (editable) content', async () => {
+ const slots = { default: '<div>Select item</div>' };
+ createComponent({ canUpdate: true, slots });
+ findEditButton().vm.$emit('click');
+
+ await nextTick();
+
+ expect(findCollapsed().isVisible()).toBe(false);
+ expect(findExpanded().isVisible()).toBe(true);
+ });
+ });
+
+ describe('when cannot edit', () => {
+ it('shows collapsed (non-editable) content', async () => {
+ const slots = { default: '<div>Select item</div>' };
+ createComponent({ canUpdate: false, slots });
+ // Simulate parent component calling `expand` method when user
+ // clicks on collapsed sidebar (e.g. in sidebar_weight_widget.vue)
+ wrapper.vm.expand();
+ await nextTick();
+
+ expect(findCollapsed().isVisible()).toBe(true);
+ expect(findExpanded().isVisible()).toBe(false);
+ });
+ });
});
});
diff --git a/spec/frontend/sidebar/components/assignees/uncollapsed_assignee_list_spec.js b/spec/frontend/sidebar/components/assignees/uncollapsed_assignee_list_spec.js
index b902d7313fd..03c2e1a37a9 100644
--- a/spec/frontend/sidebar/components/assignees/uncollapsed_assignee_list_spec.js
+++ b/spec/frontend/sidebar/components/assignees/uncollapsed_assignee_list_spec.js
@@ -46,7 +46,7 @@ describe('UncollapsedAssigneeList component', () => {
});
it('calls the AssigneeAvatarLink with the proper props', () => {
- expect(wrapper.find(AssigneeAvatarLink).exists()).toBe(true);
+ expect(wrapper.findComponent(AssigneeAvatarLink).exists()).toBe(true);
});
it('Shows one user with avatar, username and author name', () => {
diff --git a/spec/frontend/sidebar/components/confidential/sidebar_confidentiality_form_spec.js b/spec/frontend/sidebar/components/confidential/sidebar_confidentiality_form_spec.js
index 1ea035c7184..b27f7c6b4e1 100644
--- a/spec/frontend/sidebar/components/confidential/sidebar_confidentiality_form_spec.js
+++ b/spec/frontend/sidebar/components/confidential/sidebar_confidentiality_form_spec.js
@@ -2,7 +2,7 @@ import { GlSprintf } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
import waitForPromises from 'helpers/wait_for_promises';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import SidebarConfidentialityForm from '~/sidebar/components/confidential/sidebar_confidentiality_form.vue';
import { confidentialityQueries } from '~/sidebar/constants';
@@ -63,7 +63,7 @@ describe('Sidebar Confidentiality Form', () => {
findConfidentialToggle().vm.$emit('click', new MouseEvent('click'));
await waitForPromises();
- expect(createFlash).toHaveBeenCalledWith({
+ expect(createAlert).toHaveBeenCalledWith({
message: 'Something went wrong while setting issue confidentiality.',
});
});
@@ -77,7 +77,7 @@ describe('Sidebar Confidentiality Form', () => {
findConfidentialToggle().vm.$emit('click', new MouseEvent('click'));
await waitForPromises();
- expect(createFlash).toHaveBeenCalledWith({
+ expect(createAlert).toHaveBeenCalledWith({
message: 'Houston, we have a problem!',
});
});
diff --git a/spec/frontend/sidebar/components/confidential/sidebar_confidentiality_widget_spec.js b/spec/frontend/sidebar/components/confidential/sidebar_confidentiality_widget_spec.js
index 3a3f0b1d9fa..e486a8e9ec7 100644
--- a/spec/frontend/sidebar/components/confidential/sidebar_confidentiality_widget_spec.js
+++ b/spec/frontend/sidebar/components/confidential/sidebar_confidentiality_widget_spec.js
@@ -4,7 +4,7 @@ import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import SidebarConfidentialityContent from '~/sidebar/components/confidential/sidebar_confidentiality_content.vue';
import SidebarConfidentialityForm from '~/sidebar/components/confidential/sidebar_confidentiality_form.vue';
import SidebarConfidentialityWidget, {
@@ -126,7 +126,7 @@ describe('Sidebar Confidentiality Widget', () => {
});
await waitForPromises();
- expect(createFlash).toHaveBeenCalled();
+ expect(createAlert).toHaveBeenCalled();
});
it('closes the form and dispatches an event when `closeForm` is emitted', async () => {
diff --git a/spec/frontend/sidebar/components/copy_email_to_clipboard_spec.js b/spec/frontend/sidebar/components/copy_email_to_clipboard_spec.js
index 699b2bbd0b1..69a8d645973 100644
--- a/spec/frontend/sidebar/components/copy_email_to_clipboard_spec.js
+++ b/spec/frontend/sidebar/components/copy_email_to_clipboard_spec.js
@@ -12,6 +12,6 @@ describe('CopyEmailToClipboard component', () => {
});
it('sets CopyableField `value` prop to issueEmailAddress', () => {
- expect(wrapper.find(CopyableField).props('value')).toBe(mockIssueEmailAddress);
+ expect(wrapper.findComponent(CopyableField).props('value')).toBe(mockIssueEmailAddress);
});
});
diff --git a/spec/frontend/sidebar/components/crm_contacts_spec.js b/spec/frontend/sidebar/components/crm_contacts_spec.js
index 6456829258f..6d76fa1f9df 100644
--- a/spec/frontend/sidebar/components/crm_contacts_spec.js
+++ b/spec/frontend/sidebar/components/crm_contacts_spec.js
@@ -3,7 +3,7 @@ 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 createFlash from '~/flash';
+import { createAlert } from '~/flash';
import CrmContacts from '~/sidebar/components/crm_contacts/crm_contacts.vue';
import getIssueCrmContactsQuery from '~/sidebar/components/crm_contacts/queries/get_issue_crm_contacts.query.graphql';
import issueCrmContactsSubscription from '~/sidebar/components/crm_contacts/queries/issue_crm_contacts.subscription.graphql';
@@ -47,7 +47,7 @@ describe('Issue crm contacts component', () => {
mountComponent({ queryHandler: jest.fn().mockRejectedValue('ERROR') });
await waitForPromises();
- expect(createFlash).toHaveBeenCalled();
+ expect(createAlert).toHaveBeenCalled();
});
it('calls the query with correct variables', () => {
diff --git a/spec/frontend/sidebar/components/date/sidebar_date_widget_spec.js b/spec/frontend/sidebar/components/date/sidebar_date_widget_spec.js
index 1e2173e2988..67413cffdda 100644
--- a/spec/frontend/sidebar/components/date/sidebar_date_widget_spec.js
+++ b/spec/frontend/sidebar/components/date/sidebar_date_widget_spec.js
@@ -4,7 +4,7 @@ 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 { createAlert } from '~/flash';
import SidebarDateWidget from '~/sidebar/components/date/sidebar_date_widget.vue';
import SidebarFormattedDate from '~/sidebar/components/date/sidebar_formatted_date.vue';
import SidebarInheritDate from '~/sidebar/components/date/sidebar_inherit_date.vue';
@@ -28,7 +28,7 @@ describe('Sidebar date Widget', () => {
const findEditableItem = () => wrapper.findComponent(SidebarEditableItem);
const findPopoverIcon = () => wrapper.find('[data-testid="inherit-date-popover"]');
- const findDatePicker = () => wrapper.find(GlDatepicker);
+ const findDatePicker = () => wrapper.findComponent(GlDatepicker);
const createComponent = ({
dueDateQueryHandler = jest.fn().mockResolvedValue(issuableDueDateResponse()),
@@ -149,14 +149,14 @@ describe('Sidebar date Widget', () => {
createComponent({ canInherit });
await waitForPromises();
- expect(wrapper.find(component).exists()).toBe(expected);
+ expect(wrapper.findComponent(component).exists()).toBe(expected);
},
);
it('does not render SidebarInheritDate when canInherit is true and date is loading', async () => {
createComponent({ canInherit: true });
- expect(wrapper.find(SidebarInheritDate).exists()).toBe(false);
+ expect(wrapper.findComponent(SidebarInheritDate).exists()).toBe(false);
});
it('displays a flash message when query is rejected', async () => {
@@ -165,7 +165,7 @@ describe('Sidebar date Widget', () => {
});
await waitForPromises();
- expect(createFlash).toHaveBeenCalled();
+ expect(createAlert).toHaveBeenCalled();
});
it.each`
diff --git a/spec/frontend/sidebar/components/date/sidebar_formatted_date_spec.js b/spec/frontend/sidebar/components/date/sidebar_formatted_date_spec.js
index 1eda4ea977f..cbe01263dcd 100644
--- a/spec/frontend/sidebar/components/date/sidebar_formatted_date_spec.js
+++ b/spec/frontend/sidebar/components/date/sidebar_formatted_date_spec.js
@@ -5,7 +5,7 @@ import SidebarFormattedDate from '~/sidebar/components/date/sidebar_formatted_da
describe('SidebarFormattedDate', () => {
let wrapper;
const findFormattedDate = () => wrapper.find("[data-testid='sidebar-date-value']");
- const findRemoveButton = () => wrapper.find(GlButton);
+ const findRemoveButton = () => wrapper.findComponent(GlButton);
const createComponent = ({ hasDate = true } = {}) => {
wrapper = shallowMount(SidebarFormattedDate, {
diff --git a/spec/frontend/sidebar/components/severity/severity_spec.js b/spec/frontend/sidebar/components/severity/severity_spec.js
index 1e4624e4dcd..2146155791e 100644
--- a/spec/frontend/sidebar/components/severity/severity_spec.js
+++ b/spec/frontend/sidebar/components/severity/severity_spec.js
@@ -21,7 +21,7 @@ describe('SeverityToken', () => {
}
});
- const findIcon = () => wrapper.find(GlIcon);
+ const findIcon = () => wrapper.findComponent(GlIcon);
it('renders severity token for each severity type', () => {
Object.values(INCIDENT_SEVERITY).forEach((severity) => {
diff --git a/spec/frontend/sidebar/components/severity/sidebar_severity_spec.js b/spec/frontend/sidebar/components/severity/sidebar_severity_spec.js
index 83eb9a18597..bdea33371d8 100644
--- a/spec/frontend/sidebar/components/severity/sidebar_severity_spec.js
+++ b/spec/frontend/sidebar/components/severity/sidebar_severity_spec.js
@@ -2,7 +2,7 @@ import { GlDropdown, GlDropdownItem, GlLoadingIcon, GlTooltip, GlSprintf } from
import { nextTick } from 'vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { INCIDENT_SEVERITY, ISSUABLE_TYPES } from '~/sidebar/components/severity/constants';
import updateIssuableSeverity from '~/sidebar/components/severity/graphql/mutations/update_issuable_severity.mutation.graphql';
import SeverityToken from '~/sidebar/components/severity/severity.vue';
@@ -59,7 +59,7 @@ describe('SidebarSeverity', () => {
const findCriticalSeverityDropdownItem = () => wrapper.findComponent(GlDropdownItem);
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findTooltip = () => wrapper.findComponent(GlTooltip);
- const findCollapsedSeverity = () => wrapper.find({ ref: 'severity' });
+ const findCollapsedSeverity = () => wrapper.findComponent({ ref: 'severity' });
describe('Severity widget', () => {
it('renders severity dropdown and token', () => {
@@ -104,7 +104,7 @@ describe('SidebarSeverity', () => {
await waitForPromises();
- expect(createFlash).toHaveBeenCalled();
+ expect(createAlert).toHaveBeenCalled();
});
it('shows loading icon while updating', async () => {
diff --git a/spec/frontend/sidebar/components/sidebar_dropdown_widget_spec.js b/spec/frontend/sidebar/components/sidebar_dropdown_widget_spec.js
index 6761731c093..8ab4d8ea051 100644
--- a/spec/frontend/sidebar/components/sidebar_dropdown_widget_spec.js
+++ b/spec/frontend/sidebar/components/sidebar_dropdown_widget_spec.js
@@ -15,7 +15,7 @@ import createMockApollo from 'helpers/mock_apollo_helper';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { IssuableType } from '~/issues/constants';
import { timeFor } from '~/lib/utils/datetime_utility';
@@ -369,9 +369,9 @@ describe('SidebarDropdownWidget', () => {
findDropdownItemWithText('title').vm.$emit('click');
});
- it(`calls createFlash with "${expectedMsg}"`, async () => {
+ it(`calls createAlert with "${expectedMsg}"`, async () => {
await nextTick();
- expect(createFlash).toHaveBeenCalledWith({
+ expect(createAlert).toHaveBeenCalledWith({
message: expectedMsg,
captureError: true,
error: expectedMsg,
@@ -455,14 +455,14 @@ describe('SidebarDropdownWidget', () => {
describe('milestones', () => {
let projectMilestonesSpy;
- it('should call createFlash if milestones query fails', async () => {
+ it('should call createAlert if milestones query fails', async () => {
await createComponentWithApollo({
projectMilestonesSpy: jest.fn().mockRejectedValue(error),
});
await clickEdit();
- expect(createFlash).toHaveBeenCalledWith({
+ expect(createAlert).toHaveBeenCalledWith({
message: wrapper.vm.i18n.listFetchError,
captureError: true,
error: expect.any(Error),
@@ -514,12 +514,12 @@ describe('SidebarDropdownWidget', () => {
});
describe('currentAttributes', () => {
- it('should call createFlash if currentAttributes query fails', async () => {
+ it('should call createAlert if currentAttributes query fails', async () => {
await createComponentWithApollo({
currentMilestoneSpy: jest.fn().mockRejectedValue(error),
});
- expect(createFlash).toHaveBeenCalledWith({
+ expect(createAlert).toHaveBeenCalledWith({
message: wrapper.vm.i18n.currentFetchError,
captureError: true,
error: expect.any(Error),
diff --git a/spec/frontend/sidebar/components/subscriptions/sidebar_subscriptions_widget_spec.js b/spec/frontend/sidebar/components/subscriptions/sidebar_subscriptions_widget_spec.js
index 430acf9f9e7..c94f9918243 100644
--- a/spec/frontend/sidebar/components/subscriptions/sidebar_subscriptions_widget_spec.js
+++ b/spec/frontend/sidebar/components/subscriptions/sidebar_subscriptions_widget_spec.js
@@ -4,7 +4,7 @@ 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 { createAlert } from '~/flash';
import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue';
import SidebarSubscriptionWidget from '~/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue';
import issueSubscribedQuery from '~/sidebar/queries/issue_subscribed.query.graphql';
@@ -144,7 +144,7 @@ describe('Sidebar Subscriptions Widget', () => {
});
await waitForPromises();
- expect(createFlash).toHaveBeenCalled();
+ expect(createAlert).toHaveBeenCalled();
});
describe('merge request', () => {
diff --git a/spec/frontend/sidebar/components/time_tracking/report_spec.js b/spec/frontend/sidebar/components/time_tracking/report_spec.js
index 4e619a4e609..af72122052f 100644
--- a/spec/frontend/sidebar/components/time_tracking/report_spec.js
+++ b/spec/frontend/sidebar/components/time_tracking/report_spec.js
@@ -6,7 +6,7 @@ import VueApollo from 'vue-apollo';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import Report from '~/sidebar/components/time_tracking/report.vue';
import getIssueTimelogsQuery from '~/vue_shared/components/sidebar/queries/get_issue_timelogs.query.graphql';
import getMrTimelogsQuery from '~/vue_shared/components/sidebar/queries/get_mr_timelogs.query.graphql';
@@ -65,7 +65,7 @@ describe('Issuable Time Tracking Report', () => {
mountComponent({ queryHandler: jest.fn().mockRejectedValue('ERROR') });
await waitForPromises();
- expect(createFlash).toHaveBeenCalled();
+ expect(createAlert).toHaveBeenCalled();
});
describe('for issue', () => {
@@ -153,7 +153,7 @@ describe('Issuable Time Tracking Report', () => {
await findDeleteButton().trigger('click');
await waitForPromises();
- expect(createFlash).not.toHaveBeenCalled();
+ expect(createAlert).not.toHaveBeenCalled();
expect(mutateSpy).toHaveBeenCalledWith({
mutation: deleteTimelogMutation,
variables: {
@@ -164,7 +164,7 @@ describe('Issuable Time Tracking Report', () => {
});
});
- it('calls `createFlash` with errorMessage and does not remove the row on promise reject', async () => {
+ it('calls `createAlert` with errorMessage and does not remove the row on promise reject', async () => {
const mutateSpy = jest.spyOn(wrapper.vm.$apollo, 'mutate').mockRejectedValue({});
await waitForPromises();
@@ -180,7 +180,7 @@ describe('Issuable Time Tracking Report', () => {
},
});
- expect(createFlash).toHaveBeenCalledWith({
+ expect(createAlert).toHaveBeenCalledWith({
message: 'An error occurred while removing the timelog.',
captureError: true,
error: expect.any(Object),
diff --git a/spec/frontend/sidebar/components/todo_toggle/sidebar_todo_widget_spec.js b/spec/frontend/sidebar/components/todo_toggle/sidebar_todo_widget_spec.js
index ea931782d1e..f73491ca95f 100644
--- a/spec/frontend/sidebar/components/todo_toggle/sidebar_todo_widget_spec.js
+++ b/spec/frontend/sidebar/components/todo_toggle/sidebar_todo_widget_spec.js
@@ -4,7 +4,7 @@ import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import SidebarTodoWidget from '~/sidebar/components/todo_toggle/sidebar_todo_widget.vue';
import epicTodoQuery from '~/sidebar/queries/epic_todo.query.graphql';
import TodoButton from '~/vue_shared/components/sidebar/todo_toggle/todo_button.vue';
@@ -83,7 +83,7 @@ describe('Sidebar Todo Widget', () => {
});
await waitForPromises();
- expect(createFlash).toHaveBeenCalled();
+ expect(createAlert).toHaveBeenCalled();
});
describe('collapsed', () => {
@@ -97,13 +97,13 @@ describe('Sidebar Todo Widget', () => {
});
it('shows add todo icon', () => {
- expect(wrapper.find(GlIcon).exists()).toBe(true);
+ expect(wrapper.findComponent(GlIcon).exists()).toBe(true);
- expect(wrapper.find(GlIcon).props('name')).toBe('todo-add');
+ expect(wrapper.findComponent(GlIcon).props('name')).toBe('todo-add');
});
it('sets default tooltip title', () => {
- expect(wrapper.find(GlButton).attributes('title')).toBe('Add a to do');
+ expect(wrapper.findComponent(GlButton).attributes('title')).toBe('Add a to do');
});
it('when user has a to do', async () => {
@@ -112,12 +112,12 @@ describe('Sidebar Todo Widget', () => {
});
await waitForPromises();
- expect(wrapper.find(GlIcon).props('name')).toBe('todo-done');
- expect(wrapper.find(GlButton).attributes('title')).toBe('Mark as done');
+ expect(wrapper.findComponent(GlIcon).props('name')).toBe('todo-done');
+ expect(wrapper.findComponent(GlButton).attributes('title')).toBe('Mark as done');
});
it('emits `todoUpdated` event on click on icon', async () => {
- wrapper.find(GlIcon).vm.$emit('click', event);
+ wrapper.findComponent(GlIcon).vm.$emit('click', event);
await nextTick();
expect(wrapper.emitted('todoUpdated')).toEqual([[false]]);
diff --git a/spec/frontend/sidebar/issuable_assignees_spec.js b/spec/frontend/sidebar/issuable_assignees_spec.js
index dc59b68bbd4..1161fefcc64 100644
--- a/spec/frontend/sidebar/issuable_assignees_spec.js
+++ b/spec/frontend/sidebar/issuable_assignees_spec.js
@@ -17,7 +17,7 @@ describe('IssuableAssignees', () => {
},
});
};
- const findUncollapsedAssigneeList = () => wrapper.find(UncollapsedAssigneeList);
+ const findUncollapsedAssigneeList = () => wrapper.findComponent(UncollapsedAssigneeList);
const findEmptyAssignee = () => wrapper.find('[data-testid="none"]');
afterEach(() => {
diff --git a/spec/frontend/sidebar/lock/edit_form_buttons_spec.js b/spec/frontend/sidebar/lock/edit_form_buttons_spec.js
index 971744edb0f..2abb0c24d7d 100644
--- a/spec/frontend/sidebar/lock/edit_form_buttons_spec.js
+++ b/spec/frontend/sidebar/lock/edit_form_buttons_spec.js
@@ -1,6 +1,6 @@
import { mount } from '@vue/test-utils';
import { nextTick } from 'vue';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { createStore as createMrStore } from '~/mr_notes/stores';
import createStore from '~/notes/stores';
import EditFormButtons from '~/sidebar/components/lock/edit_form_buttons.vue';
@@ -129,7 +129,7 @@ describe('EditFormButtons', () => {
});
it('does not flash an error message', () => {
- expect(createFlash).not.toHaveBeenCalled();
+ expect(createAlert).not.toHaveBeenCalled();
});
});
@@ -162,7 +162,7 @@ describe('EditFormButtons', () => {
});
it('calls flash with the correct message', () => {
- expect(createFlash).toHaveBeenCalledWith({
+ expect(createAlert).toHaveBeenCalledWith({
message: `Something went wrong trying to change the locked state of this ${issuableDisplayName}`,
});
});
diff --git a/spec/frontend/sidebar/lock/issuable_lock_form_spec.js b/spec/frontend/sidebar/lock/issuable_lock_form_spec.js
index 986ccaea4b6..8f825847cfc 100644
--- a/spec/frontend/sidebar/lock/issuable_lock_form_spec.js
+++ b/spec/frontend/sidebar/lock/issuable_lock_form_spec.js
@@ -26,7 +26,7 @@ describe('IssuableLockForm', () => {
const findSidebarCollapseIcon = () => wrapper.find('[data-testid="sidebar-collapse-icon"]');
const findLockStatus = () => wrapper.find('[data-testid="lock-status"]');
const findEditLink = () => wrapper.find('[data-testid="edit-link"]');
- const findEditForm = () => wrapper.find(EditForm);
+ const findEditForm = () => wrapper.findComponent(EditForm);
const findSidebarLockStatusTooltip = () =>
getBinding(findSidebarCollapseIcon().element, 'gl-tooltip');
diff --git a/spec/frontend/sidebar/mock_data.js b/spec/frontend/sidebar/mock_data.js
index 2afe9647cbe..391cbb1e0d5 100644
--- a/spec/frontend/sidebar/mock_data.js
+++ b/spec/frontend/sidebar/mock_data.js
@@ -283,7 +283,6 @@ export const epicParticipantsResponse = () => ({
name: 'Jacki Kub',
username: 'francina.skiles',
webUrl: '/franc',
- status: null,
},
],
},
diff --git a/spec/frontend/sidebar/participants_spec.js b/spec/frontend/sidebar/participants_spec.js
index 2517b625225..f7a626a189c 100644
--- a/spec/frontend/sidebar/participants_spec.js
+++ b/spec/frontend/sidebar/participants_spec.js
@@ -36,7 +36,7 @@ describe('Participants', () => {
loading: true,
});
- expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
+ expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
});
it('does not show loading spinner not loading', () => {
@@ -44,7 +44,7 @@ describe('Participants', () => {
loading: false,
});
- expect(wrapper.find(GlLoadingIcon).exists()).toBe(false);
+ expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(false);
});
it('shows participant count when given', () => {
@@ -73,7 +73,7 @@ describe('Participants', () => {
loading: true,
});
- expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
+ expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
});
it('when only showing visible participants, shows an avatar only for each participant under the limit', async () => {
diff --git a/spec/frontend/sidebar/reviewer_title_spec.js b/spec/frontend/sidebar/reviewer_title_spec.js
index 6b4eed5ad0f..68ecd62e4c6 100644
--- a/spec/frontend/sidebar/reviewer_title_spec.js
+++ b/spec/frontend/sidebar/reviewer_title_spec.js
@@ -47,7 +47,7 @@ describe('ReviewerTitle component', () => {
editable: false,
});
- expect(wrapper.find(GlLoadingIcon).exists()).toBe(false);
+ expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(false);
});
it('renders spinner when loading', () => {
@@ -57,7 +57,7 @@ describe('ReviewerTitle component', () => {
editable: false,
});
- expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
+ expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
});
it('does not render edit link when not editable', () => {
diff --git a/spec/frontend/sidebar/reviewers_spec.js b/spec/frontend/sidebar/reviewers_spec.js
index 88bacc9b7f7..229f7ffbe04 100644
--- a/spec/frontend/sidebar/reviewers_spec.js
+++ b/spec/frontend/sidebar/reviewers_spec.js
@@ -43,7 +43,7 @@ describe('Reviewer component', () => {
it('displays no reviewer icon when collapsed', () => {
createWrapper();
const collapsedChildren = findCollapsedChildren();
- const userIcon = collapsedChildren.at(0).find(GlIcon);
+ const userIcon = collapsedChildren.at(0).findComponent(GlIcon);
expect(collapsedChildren.length).toBe(1);
expect(collapsedChildren.at(0).attributes('aria-label')).toBe('None');
diff --git a/spec/frontend/sidebar/sidebar_assignees_spec.js b/spec/frontend/sidebar/sidebar_assignees_spec.js
index 68d20060c37..2cb2425532b 100644
--- a/spec/frontend/sidebar/sidebar_assignees_spec.js
+++ b/spec/frontend/sidebar/sidebar_assignees_spec.js
@@ -73,19 +73,19 @@ describe('sidebar assignees', () => {
it('hides assignees until fetched', async () => {
createComponent();
- expect(wrapper.find(Assigness).exists()).toBe(false);
+ expect(wrapper.findComponent(Assigness).exists()).toBe(false);
wrapper.vm.store.isFetching.assignees = false;
await nextTick();
- expect(wrapper.find(Assigness).exists()).toBe(true);
+ expect(wrapper.findComponent(Assigness).exists()).toBe(true);
});
describe('when issuableType is issue', () => {
it('finds AssigneesRealtime component', () => {
createComponent();
- expect(wrapper.find(AssigneesRealtime).exists()).toBe(true);
+ expect(wrapper.findComponent(AssigneesRealtime).exists()).toBe(true);
});
});
@@ -93,7 +93,7 @@ describe('sidebar assignees', () => {
it('does not find AssigneesRealtime component', () => {
createComponent({ issuableType: 'MR' });
- expect(wrapper.find(AssigneesRealtime).exists()).toBe(false);
+ expect(wrapper.findComponent(AssigneesRealtime).exists()).toBe(false);
});
});
});
diff --git a/spec/frontend/sidebar/sidebar_mediator_spec.js b/spec/frontend/sidebar/sidebar_mediator_spec.js
index 355f0c45bbe..bb5e7f7ff16 100644
--- a/spec/frontend/sidebar/sidebar_mediator_spec.js
+++ b/spec/frontend/sidebar/sidebar_mediator_spec.js
@@ -1,7 +1,7 @@
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import * as urlUtility from '~/lib/utils/url_utility';
-import SidebarService, { gqClient } from '~/sidebar/services/sidebar_service';
+import SidebarService from '~/sidebar/services/sidebar_service';
import SidebarMediator from '~/sidebar/sidebar_mediator';
import SidebarStore from '~/sidebar/stores/sidebar_store';
import Mock from './mock_data';
@@ -42,22 +42,14 @@ describe('Sidebar mediator', () => {
});
});
- it('fetches the data', () => {
+ it('fetches the data', async () => {
const mockData = Mock.responseMap.GET[mediatorMockData.endpoint];
mock.onGet(mediatorMockData.endpoint).reply(200, mockData);
-
- const mockGraphQlData = Mock.graphQlResponseData;
- const graphQlSpy = jest.spyOn(gqClient, 'query').mockReturnValue({
- data: mockGraphQlData,
- });
const spy = jest.spyOn(mediator, 'processFetchedData').mockReturnValue(Promise.resolve());
+ await mediator.fetch();
- return mediator.fetch().then(() => {
- expect(spy).toHaveBeenCalledWith(mockData, mockGraphQlData);
-
- spy.mockRestore();
- graphQlSpy.mockRestore();
- });
+ expect(spy).toHaveBeenCalledWith(mockData);
+ spy.mockRestore();
});
it('processes fetched data', () => {
diff --git a/spec/frontend/sidebar/sidebar_move_issue_spec.js b/spec/frontend/sidebar/sidebar_move_issue_spec.js
index 2e6807ed9d8..195cc6ddeeb 100644
--- a/spec/frontend/sidebar/sidebar_move_issue_spec.js
+++ b/spec/frontend/sidebar/sidebar_move_issue_spec.js
@@ -1,7 +1,7 @@
import MockAdapter from 'axios-mock-adapter';
import $ from 'jquery';
import waitForPromises from 'helpers/wait_for_promises';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import SidebarMoveIssue from '~/sidebar/lib/sidebar_move_issue';
import SidebarService from '~/sidebar/services/sidebar_service';
@@ -115,7 +115,7 @@ describe('SidebarMoveIssue', () => {
// Wait for the move issue request to fail
await waitForPromises();
- expect(createFlash).toHaveBeenCalled();
+ expect(createAlert).toHaveBeenCalled();
expect(test.$confirmButton.prop('disabled')).toBe(false);
expect(test.$confirmButton.hasClass('is-loading')).toBe(false);
});
diff --git a/spec/frontend/sidebar/subscriptions_spec.js b/spec/frontend/sidebar/subscriptions_spec.js
index 6ab8e1e0ebc..1a1aa370eef 100644
--- a/spec/frontend/sidebar/subscriptions_spec.js
+++ b/spec/frontend/sidebar/subscriptions_spec.js
@@ -108,7 +108,7 @@ describe('Subscriptions', () => {
expect(wrapper.findByTestId('subscription-title').text()).toContain(
subscribeDisabledDescription,
);
- expect(wrapper.find({ ref: 'tooltip' }).attributes('title')).toBe(
+ expect(wrapper.findComponent({ ref: 'tooltip' }).attributes('title')).toBe(
subscribeDisabledDescription,
);
});
diff --git a/spec/frontend/sidebar/todo_spec.js b/spec/frontend/sidebar/todo_spec.js
index 5f696b237e0..8e6597bf80f 100644
--- a/spec/frontend/sidebar/todo_spec.js
+++ b/spec/frontend/sidebar/todo_spec.js
@@ -43,8 +43,8 @@ describe('SidebarTodo', () => {
({ isTodo, iconClass, label, icon }) => {
createComponent({ isTodo });
- expect(wrapper.find(GlIcon).classes().join(' ')).toStrictEqual(iconClass);
- expect(wrapper.find(GlIcon).props('name')).toStrictEqual(icon);
+ expect(wrapper.findComponent(GlIcon).classes().join(' ')).toStrictEqual(iconClass);
+ expect(wrapper.findComponent(GlIcon).props('name')).toStrictEqual(icon);
expect(wrapper.find('button').text()).toBe(label);
},
);
@@ -76,19 +76,19 @@ describe('SidebarTodo', () => {
it('renders button icon when `collapsed` prop is `true`', () => {
createComponent({ collapsed: true });
- expect(wrapper.find(GlIcon).props('name')).toBe('todo-done');
+ expect(wrapper.findComponent(GlIcon).props('name')).toBe('todo-done');
});
it('renders loading icon when `isActionActive` prop is true', () => {
createComponent({ isActionActive: true });
- expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
+ expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
});
it('hides button icon when `isActionActive` prop is true', () => {
createComponent({ collapsed: true, isActionActive: true });
- expect(wrapper.find(GlIcon).isVisible()).toBe(false);
+ expect(wrapper.findComponent(GlIcon).isVisible()).toBe(false);
});
});
});
diff --git a/spec/frontend/smart_interval_spec.js b/spec/frontend/smart_interval_spec.js
index 5dda097ae6a..64928fc4ae9 100644
--- a/spec/frontend/smart_interval_spec.js
+++ b/spec/frontend/smart_interval_spec.js
@@ -109,7 +109,7 @@ describe('SmartInterval', () => {
return waitForPromises().then(() => {
const { intervalId } = interval.state;
- expect(intervalId).toBeTruthy();
+ expect(intervalId).not.toBeUndefined();
});
});
});
@@ -130,7 +130,7 @@ describe('SmartInterval', () => {
jest.runOnlyPendingTimers();
return waitForPromises().then(() => {
- expect(interval.state.intervalId).toBeTruthy();
+ expect(interval.state.intervalId).not.toBeUndefined();
// simulates triggering of visibilitychange event
interval.onVisibilityChange({ target: { visibilityState: 'hidden' } });
@@ -148,16 +148,16 @@ describe('SmartInterval', () => {
jest.runOnlyPendingTimers();
return waitForPromises().then(() => {
- expect(interval.state.intervalId).toBeTruthy();
+ expect(interval.state.intervalId).not.toBeUndefined();
expect(
interval.getCurrentInterval() >= DEFAULT_STARTING_INTERVAL &&
interval.getCurrentInterval() <= DEFAULT_MAX_INTERVAL,
- ).toBeTruthy();
+ ).toBe(true);
// simulates triggering of visibilitychange event
interval.onVisibilityChange({ target: { visibilityState: 'hidden' } });
- expect(interval.state.intervalId).toBeTruthy();
+ expect(interval.state.intervalId).not.toBeUndefined();
expect(interval.getCurrentInterval()).toBe(HIDDEN_INTERVAL);
});
});
@@ -166,7 +166,7 @@ describe('SmartInterval', () => {
jest.runOnlyPendingTimers();
return waitForPromises().then(() => {
- expect(interval.state.intervalId).toBeTruthy();
+ expect(interval.state.intervalId).not.toBeUndefined();
// simulates triggering of visibilitychange event
interval.onVisibilityChange({ target: { visibilityState: 'hidden' } });
@@ -176,7 +176,7 @@ describe('SmartInterval', () => {
// simulates triggering of visibilitychange event
interval.onVisibilityChange({ target: { visibilityState: 'visible' } });
- expect(interval.state.intervalId).toBeTruthy();
+ expect(interval.state.intervalId).not.toBeUndefined();
});
});
@@ -194,7 +194,7 @@ describe('SmartInterval', () => {
it('should execute callback before first interval', () => {
interval = createDefaultSmartInterval({ immediateExecution: true });
- expect(interval.cfg.immediateExecution).toBeFalsy();
+ expect(interval.cfg.immediateExecution).toBe(false);
});
});
});
diff --git a/spec/frontend/snippet/collapsible_input_spec.js b/spec/frontend/snippet/collapsible_input_spec.js
index 56e64d136c2..4a6fd33b9e4 100644
--- a/spec/frontend/snippet/collapsible_input_spec.js
+++ b/spec/frontend/snippet/collapsible_input_spec.js
@@ -9,7 +9,7 @@ describe('~/snippet/collapsible_input', () => {
beforeEach(() => {
setHTMLFixture(`
- <form>
+ <form>
<div class="js-collapsible-input js-title">
<div class="js-collapsed d-none">
<input type="text" />
@@ -72,7 +72,7 @@ describe('~/snippet/collapsible_input', () => {
${'is collapsed'} | ${''} | ${true}
${'stays open if given value'} | ${'Hello world!'} | ${false}
`('when loses focus', ({ desc, value, isCollapsed }) => {
- it(desc, () => {
+ it(`${desc}`, () => {
findExpandedInput(descriptionEl).value = value;
focusIn(fooEl);
diff --git a/spec/frontend/snippets/components/edit_spec.js b/spec/frontend/snippets/components/edit_spec.js
index cf897414ccb..e7dab0ad79d 100644
--- a/spec/frontend/snippets/components/edit_spec.js
+++ b/spec/frontend/snippets/components/edit_spec.js
@@ -9,7 +9,7 @@ import { stubPerformanceWebAPI } from 'helpers/performance';
import waitForPromises from 'helpers/wait_for_promises';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import GetSnippetQuery from 'shared_queries/snippet/snippet.query.graphql';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import * as urlUtils from '~/lib/utils/url_utility';
import SnippetEditApp from '~/snippets/components/edit.vue';
import SnippetBlobActionsEdit from '~/snippets/components/snippet_blob_actions_edit.vue';
@@ -206,7 +206,7 @@ describe('Snippet Edit app', () => {
});
it('should hide loader', () => {
- expect(wrapper.find(GlLoadingIcon).exists()).toBe(false);
+ expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(false);
});
});
@@ -237,7 +237,7 @@ describe('Snippet Edit app', () => {
!titleHasErrors,
);
- expect(wrapper.find(SnippetBlobActionsEdit).props('isValid')).toEqual(
+ expect(wrapper.findComponent(SnippetBlobActionsEdit).props('isValid')).toEqual(
!blobActionsHasErrors,
);
},
@@ -273,7 +273,7 @@ describe('Snippet Edit app', () => {
selectedLevel: visibility,
});
- expect(wrapper.find(SnippetVisibilityEdit).props('value')).toBe(visibility);
+ expect(wrapper.findComponent(SnippetVisibilityEdit).props('value')).toBe(visibility);
});
describe('form submission handling', () => {
@@ -361,7 +361,7 @@ describe('Snippet Edit app', () => {
await waitForPromises();
expect(urlUtils.redirectTo).not.toHaveBeenCalled();
- expect(createFlash).toHaveBeenCalledWith({
+ expect(createAlert).toHaveBeenCalledWith({
message: `Can't create snippet: ${TEST_MUTATION_ERROR}`,
});
});
@@ -385,7 +385,7 @@ describe('Snippet Edit app', () => {
});
expect(urlUtils.redirectTo).not.toHaveBeenCalled();
- expect(createFlash).toHaveBeenCalledWith({
+ expect(createAlert).toHaveBeenCalledWith({
message: `Can't update snippet: ${TEST_MUTATION_ERROR}`,
});
},
@@ -407,7 +407,7 @@ describe('Snippet Edit app', () => {
it('should flash', () => {
// Apollo automatically wraps the resolver's error in a NetworkError
- expect(createFlash).toHaveBeenCalledWith({
+ expect(createAlert).toHaveBeenCalledWith({
message: `Can't update snippet: ${TEST_API_ERROR.message}`,
});
});
diff --git a/spec/frontend/snippets/components/embed_dropdown_spec.js b/spec/frontend/snippets/components/embed_dropdown_spec.js
index 389b1c618a3..ed5ea6cab8a 100644
--- a/spec/frontend/snippets/components/embed_dropdown_spec.js
+++ b/spec/frontend/snippets/components/embed_dropdown_spec.js
@@ -36,7 +36,7 @@ describe('snippets/components/embed_dropdown', () => {
sections.push(current);
} else {
- const value = x.find(GlFormInputGroup).props('value');
+ const value = x.findComponent(GlFormInputGroup).props('value');
const copyValue = x.find('button[title="Copy"]').attributes('data-clipboard-text');
Object.assign(current, {
diff --git a/spec/frontend/snippets/components/snippet_blob_edit_spec.js b/spec/frontend/snippets/components/snippet_blob_edit_spec.js
index 7ea27864519..33b8e2be969 100644
--- a/spec/frontend/snippets/components/snippet_blob_edit_spec.js
+++ b/spec/frontend/snippets/components/snippet_blob_edit_spec.js
@@ -4,7 +4,7 @@ import AxiosMockAdapter from 'axios-mock-adapter';
import { TEST_HOST } from 'helpers/test_constants';
import waitForPromises from 'helpers/wait_for_promises';
import BlobHeaderEdit from '~/blob/components/blob_edit_header.vue';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { joinPaths } from '~/lib/utils/url_utility';
import SnippetBlobEdit from '~/snippets/components/snippet_blob_edit.vue';
@@ -46,9 +46,9 @@ describe('Snippet Blob Edit component', () => {
});
};
- const findLoadingIcon = () => wrapper.find(GlLoadingIcon);
- const findHeader = () => wrapper.find(BlobHeaderEdit);
- const findContent = () => wrapper.find(SourceEditor);
+ const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
+ const findHeader = () => wrapper.findComponent(BlobHeaderEdit);
+ const findContent = () => wrapper.findComponent(SourceEditor);
const getLastUpdatedArgs = () => {
const event = wrapper.emitted()['blob-updated'];
@@ -125,7 +125,7 @@ describe('Snippet Blob Edit component', () => {
it('should call flash', async () => {
await waitForPromises();
- expect(createFlash).toHaveBeenCalledWith({
+ expect(createAlert).toHaveBeenCalledWith({
message: "Can't fetch content for the blob: Error: Request failed with status code 500",
});
});
diff --git a/spec/frontend/snippets/components/snippet_blob_view_spec.js b/spec/frontend/snippets/components/snippet_blob_view_spec.js
index aa31377f390..c7ff8c21d80 100644
--- a/spec/frontend/snippets/components/snippet_blob_view_spec.js
+++ b/spec/frontend/snippets/components/snippet_blob_view_spec.js
@@ -69,13 +69,13 @@ describe('Blob Embeddable', () => {
describe('rendering', () => {
it('renders correct components', () => {
createComponent();
- expect(wrapper.find(BlobHeader).exists()).toBe(true);
- expect(wrapper.find(BlobContent).exists()).toBe(true);
+ expect(wrapper.findComponent(BlobHeader).exists()).toBe(true);
+ expect(wrapper.findComponent(BlobContent).exists()).toBe(true);
});
it('sets simple viewer correctly', () => {
createComponent();
- expect(wrapper.find(SimpleViewer).exists()).toBe(true);
+ expect(wrapper.findComponent(SimpleViewer).exists()).toBe(true);
});
it('sets rich viewer correctly', () => {
@@ -83,20 +83,20 @@ describe('Blob Embeddable', () => {
createComponent({
data,
});
- expect(wrapper.find(RichViewer).exists()).toBe(true);
+ expect(wrapper.findComponent(RichViewer).exists()).toBe(true);
});
it('correctly switches viewer type', async () => {
createComponent();
- expect(wrapper.find(SimpleViewer).exists()).toBe(true);
+ expect(wrapper.findComponent(SimpleViewer).exists()).toBe(true);
wrapper.vm.switchViewer(RichViewerMock.type);
await nextTick();
- expect(wrapper.find(RichViewer).exists()).toBe(true);
+ expect(wrapper.findComponent(RichViewer).exists()).toBe(true);
await wrapper.vm.switchViewer(SimpleViewerMock.type);
- expect(wrapper.find(SimpleViewer).exists()).toBe(true);
+ expect(wrapper.findComponent(SimpleViewer).exists()).toBe(true);
});
it('passes information about render error down to blob header', () => {
@@ -110,7 +110,7 @@ describe('Blob Embeddable', () => {
},
});
- expect(wrapper.find(BlobHeader).props('hasRenderError')).toBe(true);
+ expect(wrapper.findComponent(BlobHeader).props('hasRenderError')).toBe(true);
});
describe('bob content in multi-file scenario', () => {
@@ -161,7 +161,7 @@ describe('Blob Embeddable', () => {
await nextTick();
- const findContent = () => wrapper.find(BlobContent);
+ const findContent = () => wrapper.findComponent(BlobContent);
expect(findContent().props('content')).toBe(expectedContent);
},
@@ -169,36 +169,69 @@ describe('Blob Embeddable', () => {
});
describe('URLS with hash', () => {
- beforeEach(() => {
- window.location.hash = '#LC2';
- });
-
afterEach(() => {
window.location.hash = '';
});
- it('renders simple viewer by default if URL contains hash', () => {
- createComponent({
- data: {},
+ describe('if hash starts with #LC', () => {
+ beforeEach(() => {
+ window.location.hash = '#LC2';
+ });
+
+ it('renders simple viewer by default', () => {
+ createComponent({
+ data: {},
+ });
+
+ expect(wrapper.vm.activeViewerType).toBe(SimpleViewerMock.type);
+ expect(wrapper.findComponent(SimpleViewer).exists()).toBe(true);
});
- expect(wrapper.vm.activeViewerType).toBe(SimpleViewerMock.type);
- expect(wrapper.find(SimpleViewer).exists()).toBe(true);
+ describe('switchViewer()', () => {
+ it('switches to the passed viewer', async () => {
+ createComponent();
+
+ wrapper.vm.switchViewer(RichViewerMock.type);
+
+ await nextTick();
+ expect(wrapper.vm.activeViewerType).toBe(RichViewerMock.type);
+ expect(wrapper.findComponent(RichViewer).exists()).toBe(true);
+
+ await wrapper.vm.switchViewer(SimpleViewerMock.type);
+ expect(wrapper.vm.activeViewerType).toBe(SimpleViewerMock.type);
+ expect(wrapper.findComponent(SimpleViewer).exists()).toBe(true);
+ });
+ });
});
- describe('switchViewer()', () => {
- it('switches to the passed viewer', async () => {
- createComponent();
+ describe('if hash starts with anything else', () => {
+ beforeEach(() => {
+ window.location.hash = '#last-headline';
+ });
- wrapper.vm.switchViewer(RichViewerMock.type);
+ it('renders rich viewer by default', () => {
+ createComponent({
+ data: {},
+ });
- await nextTick();
expect(wrapper.vm.activeViewerType).toBe(RichViewerMock.type);
- expect(wrapper.find(RichViewer).exists()).toBe(true);
+ expect(wrapper.findComponent(RichViewer).exists()).toBe(true);
+ });
- await wrapper.vm.switchViewer(SimpleViewerMock.type);
- expect(wrapper.vm.activeViewerType).toBe(SimpleViewerMock.type);
- expect(wrapper.find(SimpleViewer).exists()).toBe(true);
+ describe('switchViewer()', () => {
+ it('switches to the passed viewer', async () => {
+ createComponent();
+
+ wrapper.vm.switchViewer(SimpleViewerMock.type);
+
+ await nextTick();
+ expect(wrapper.vm.activeViewerType).toBe(SimpleViewerMock.type);
+ expect(wrapper.findComponent(SimpleViewer).exists()).toBe(true);
+
+ await wrapper.vm.switchViewer(RichViewerMock.type);
+ expect(wrapper.vm.activeViewerType).toBe(RichViewerMock.type);
+ expect(wrapper.findComponent(RichViewer).exists()).toBe(true);
+ });
});
});
});
@@ -206,7 +239,7 @@ describe('Blob Embeddable', () => {
describe('functionality', () => {
describe('render error', () => {
- const findContentEl = () => wrapper.find(BlobContent);
+ const findContentEl = () => wrapper.findComponent(BlobContent);
it('correctly sets blob on the blob-content-error component', () => {
createComponent();
diff --git a/spec/frontend/snippets/components/snippet_header_spec.js b/spec/frontend/snippets/components/snippet_header_spec.js
index b750225a383..c930c9f635b 100644
--- a/spec/frontend/snippets/components/snippet_header_spec.js
+++ b/spec/frontend/snippets/components/snippet_header_spec.js
@@ -10,7 +10,7 @@ import { differenceInMilliseconds } from '~/lib/utils/datetime_utility';
import SnippetHeader, { i18n } from '~/snippets/components/snippet_header.vue';
import DeleteSnippetMutation from '~/snippets/mutations/delete_snippet.mutation.graphql';
import axios from '~/lib/utils/axios_utils';
-import createFlash, { FLASH_TYPES } from '~/flash';
+import { createAlert, VARIANT_DANGER, VARIANT_SUCCESS } from '~/flash';
jest.mock('~/flash');
@@ -267,9 +267,9 @@ describe('Snippet header component', () => {
});
it.each`
- request | variant | text
- ${200} | ${'SUCCESS'} | ${i18n.snippetSpamSuccess}
- ${500} | ${'DANGER'} | ${i18n.snippetSpamFailure}
+ request | variant | text
+ ${200} | ${VARIANT_SUCCESS} | ${i18n.snippetSpamSuccess}
+ ${500} | ${VARIANT_DANGER} | ${i18n.snippetSpamFailure}
`(
'renders a "$variant" flash message with "$text" message for a request with a "$request" response',
async ({ request, variant, text }) => {
@@ -278,9 +278,9 @@ describe('Snippet header component', () => {
submitAsSpamBtn.trigger('click');
await waitForPromises();
- expect(createFlash).toHaveBeenLastCalledWith({
+ expect(createAlert).toHaveBeenLastCalledWith({
message: expect.stringContaining(text),
- type: FLASH_TYPES[variant],
+ variant,
});
},
);
@@ -311,7 +311,7 @@ describe('Snippet header component', () => {
it('renders modal for deletion of a snippet', () => {
createComponent();
- expect(wrapper.find(GlModal).exists()).toBe(true);
+ expect(wrapper.findComponent(GlModal).exists()).toBe(true);
});
it.each`
diff --git a/spec/frontend/snippets/components/snippet_title_spec.js b/spec/frontend/snippets/components/snippet_title_spec.js
index 48fb51ce703..7c40735d64e 100644
--- a/spec/frontend/snippets/components/snippet_title_spec.js
+++ b/spec/frontend/snippets/components/snippet_title_spec.js
@@ -39,12 +39,12 @@ describe('Snippet header component', () => {
createComponent();
expect(wrapper.text().trim()).toContain(title);
- expect(wrapper.find(SnippetDescription).props('description')).toBe(descriptionHtml);
+ expect(wrapper.findComponent(SnippetDescription).props('description')).toBe(descriptionHtml);
});
it('does not render recent changes time stamp if there were no updates', () => {
createComponent();
- expect(wrapper.find(GlSprintf).exists()).toBe(false);
+ expect(wrapper.findComponent(GlSprintf).exists()).toBe(false);
});
it('does not render recent changes time stamp if the time for creation and updates match', () => {
@@ -57,7 +57,7 @@ describe('Snippet header component', () => {
});
createComponent({ props });
- expect(wrapper.find(GlSprintf).exists()).toBe(false);
+ expect(wrapper.findComponent(GlSprintf).exists()).toBe(false);
});
it('renders translated string with most recent changes timestamp if changes were made', () => {
@@ -70,6 +70,6 @@ describe('Snippet header component', () => {
});
createComponent({ props });
- expect(wrapper.find(GlSprintf).exists()).toBe(true);
+ expect(wrapper.findComponent(GlSprintf).exists()).toBe(true);
});
});
diff --git a/spec/frontend/snippets/components/snippet_visibility_edit_spec.js b/spec/frontend/snippets/components/snippet_visibility_edit_spec.js
index 2d043a5caba..29eb002ef4a 100644
--- a/spec/frontend/snippets/components/snippet_visibility_edit_spec.js
+++ b/spec/frontend/snippets/components/snippet_visibility_edit_spec.js
@@ -39,13 +39,13 @@ describe('Snippet Visibility Edit component', () => {
});
}
- const findLink = () => wrapper.find('label').find(GlLink);
- const findRadios = () => wrapper.find(GlFormRadioGroup).findAllComponents(GlFormRadio);
+ const findLink = () => wrapper.find('label').findComponent(GlLink);
+ const findRadios = () => wrapper.findComponent(GlFormRadioGroup).findAllComponents(GlFormRadio);
const findRadiosData = () =>
findRadios().wrappers.map((x) => {
return {
value: x.find('input').attributes('value'),
- icon: x.find(GlIcon).props('name'),
+ icon: x.findComponent(GlIcon).props('name'),
description: x.find('.help-text').text(),
text: x.find('.js-visibility-option').text(),
};
@@ -147,7 +147,7 @@ describe('Snippet Visibility Edit component', () => {
createComponent({ propsData: { value } });
- expect(wrapper.find(GlFormRadioGroup).attributes('checked')).toBe(value);
+ expect(wrapper.findComponent(GlFormRadioGroup).attributes('checked')).toBe(value);
});
});
});
diff --git a/spec/frontend/terms/components/app_spec.js b/spec/frontend/terms/components/app_spec.js
index ee78b35843a..f1dbc004da8 100644
--- a/spec/frontend/terms/components/app_spec.js
+++ b/spec/frontend/terms/components/app_spec.js
@@ -74,7 +74,7 @@ describe('TermsApp', () => {
expect(findButton(defaultProvide.paths.accept).attributes('disabled')).toBe('disabled');
- wrapper.find(GlIntersectionObserver).vm.$emit('appear');
+ wrapper.findComponent(GlIntersectionObserver).vm.$emit('appear');
await nextTick();
diff --git a/spec/frontend/terraform/components/states_table_spec.js b/spec/frontend/terraform/components/states_table_spec.js
index 12a44452717..0b3b169891b 100644
--- a/spec/frontend/terraform/components/states_table_spec.js
+++ b/spec/frontend/terraform/components/states_table_spec.js
@@ -160,8 +160,8 @@ describe('StatesTable', () => {
const state = states.at(lineNumber);
expect(state.text()).toContain(name);
- expect(state.find(GlBadge).exists()).toBe(hasBadge);
- expect(state.find(GlLoadingIcon).exists()).toBe(loading);
+ expect(state.findComponent(GlBadge).exists()).toBe(hasBadge);
+ expect(state.findComponent(GlLoadingIcon).exists()).toBe(loading);
if (hasBadge) {
const badge = wrapper.findByTestId(`state-badge-${name}`);
@@ -198,7 +198,7 @@ describe('StatesTable', () => {
const states = wrapper.findAll('[data-testid="terraform-states-table-pipeline"]');
const state = states.at(lineNumber);
- expect(state.find(GlTooltip).exists()).toBe(toolTipAdded);
+ expect(state.findComponent(GlTooltip).exists()).toBe(toolTipAdded);
expect(state.text()).toMatchInterpolatedText(pipelineText);
},
);
diff --git a/spec/frontend/terraform/components/terraform_list_spec.js b/spec/frontend/terraform/components/terraform_list_spec.js
index cfd82768098..580951e799a 100644
--- a/spec/frontend/terraform/components/terraform_list_spec.js
+++ b/spec/frontend/terraform/components/terraform_list_spec.js
@@ -57,11 +57,11 @@ describe('TerraformList', () => {
});
};
- const findBadge = () => wrapper.find(GlBadge);
- const findEmptyState = () => wrapper.find(EmptyState);
- const findPaginationButtons = () => wrapper.find(GlKeysetPagination);
- const findStatesTable = () => wrapper.find(StatesTable);
- const findTab = () => wrapper.find(GlTab);
+ const findBadge = () => wrapper.findComponent(GlBadge);
+ const findEmptyState = () => wrapper.findComponent(EmptyState);
+ const findPaginationButtons = () => wrapper.findComponent(GlKeysetPagination);
+ const findStatesTable = () => wrapper.findComponent(StatesTable);
+ const findTab = () => wrapper.findComponent(GlTab);
afterEach(() => {
wrapper.destroy();
@@ -182,7 +182,7 @@ describe('TerraformList', () => {
});
it('displays an alert message', () => {
- expect(wrapper.find(GlAlert).exists()).toBe(true);
+ expect(wrapper.findComponent(GlAlert).exists()).toBe(true);
});
});
@@ -195,7 +195,7 @@ describe('TerraformList', () => {
});
it('displays a loading icon', () => {
- expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
+ expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
});
});
});
diff --git a/spec/frontend/toggles/index_spec.js b/spec/frontend/toggles/index_spec.js
index 19c4d6f1f1d..f8c43e0ad0c 100644
--- a/spec/frontend/toggles/index_spec.js
+++ b/spec/frontend/toggles/index_spec.js
@@ -83,12 +83,12 @@ describe('toggles/index.js', () => {
expect(listener).toHaveBeenCalledTimes(0);
- wrapper.find(GlToggle).vm.$emit(event, true);
+ wrapper.findComponent(GlToggle).vm.$emit(event, true);
expect(listener).toHaveBeenCalledTimes(1);
expect(listener).toHaveBeenLastCalledWith(true);
- wrapper.find(GlToggle).vm.$emit(event, false);
+ wrapper.findComponent(GlToggle).vm.$emit(event, false);
expect(listener).toHaveBeenCalledTimes(2);
expect(listener).toHaveBeenLastCalledWith(false);
diff --git a/spec/frontend/token_access/token_access_spec.js b/spec/frontend/token_access/token_access_spec.js
index 024e7dfff8c..c55ac32b6a6 100644
--- a/spec/frontend/token_access/token_access_spec.js
+++ b/spec/frontend/token_access/token_access_spec.js
@@ -4,7 +4,7 @@ import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import TokenAccess from '~/token_access/components/token_access.vue';
import addProjectCIJobTokenScopeMutation from '~/token_access/graphql/mutations/add_project_ci_job_token_scope.mutation.graphql';
import removeProjectCIJobTokenScopeMutation from '~/token_access/graphql/mutations/remove_project_ci_job_token_scope.mutation.graphql';
@@ -40,7 +40,7 @@ describe('TokenAccess component', () => {
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findAddProjectBtn = () => wrapper.findByRole('button', { name: 'Add project' });
const findRemoveProjectBtn = () => wrapper.findByRole('button', { name: 'Remove access' });
- const findTokenSection = () => wrapper.find('[data-testid="token-section"]');
+ const findTokenDisabledAlert = () => wrapper.findByTestId('token-disabled-alert');
const createMockApolloProvider = (requestHandlers) => {
return createMockApollo(requestHandlers);
@@ -80,7 +80,7 @@ describe('TokenAccess component', () => {
});
describe('toggle', () => {
- it('the toggle should be enabled and the token section should show', async () => {
+ it('the toggle is on and the alert is hidden', async () => {
createComponent([
[getCIJobTokenScopeQuery, enabledJobTokenScopeHandler],
[getProjectsWithCIJobTokenScopeQuery, getProjectsWithScope],
@@ -89,10 +89,10 @@ describe('TokenAccess component', () => {
await waitForPromises();
expect(findToggle().props('value')).toBe(true);
- expect(findTokenSection().exists()).toBe(true);
+ expect(findTokenDisabledAlert().exists()).toBe(false);
});
- it('the toggle should be disabled and the token section should show', async () => {
+ it('the toggle is off and the alert is visible', async () => {
createComponent([
[getCIJobTokenScopeQuery, disabledJobTokenScopeHandler],
[getProjectsWithCIJobTokenScopeQuery, getProjectsWithScope],
@@ -101,7 +101,7 @@ describe('TokenAccess component', () => {
await waitForPromises();
expect(findToggle().props('value')).toBe(false);
- expect(findTokenSection().exists()).toBe(true);
+ expect(findTokenDisabledAlert().exists()).toBe(true);
});
});
@@ -144,7 +144,7 @@ describe('TokenAccess component', () => {
await waitForPromises();
- expect(createFlash).toHaveBeenCalled();
+ expect(createAlert).toHaveBeenCalled();
});
});
@@ -187,7 +187,7 @@ describe('TokenAccess component', () => {
await waitForPromises();
- expect(createFlash).toHaveBeenCalled();
+ expect(createAlert).toHaveBeenCalled();
});
});
});
diff --git a/spec/frontend/tooltips/components/tooltips_spec.js b/spec/frontend/tooltips/components/tooltips_spec.js
index 998bb2a9ea2..d5a63a99601 100644
--- a/spec/frontend/tooltips/components/tooltips_spec.js
+++ b/spec/frontend/tooltips/components/tooltips_spec.js
@@ -49,7 +49,7 @@ describe('tooltips/components/tooltips.vue', () => {
await nextTick();
- expect(wrapper.find(GlTooltip).props('target')).toBe(target);
+ expect(wrapper.findComponent(GlTooltip).props('target')).toBe(target);
});
it('does not attach a tooltip to a target with empty title', async () => {
@@ -59,7 +59,7 @@ describe('tooltips/components/tooltips.vue', () => {
await nextTick();
- expect(wrapper.find(GlTooltip).exists()).toBe(false);
+ expect(wrapper.findComponent(GlTooltip).exists()).toBe(false);
});
it('does not attach a tooltip twice to the same element', async () => {
@@ -76,7 +76,7 @@ describe('tooltips/components/tooltips.vue', () => {
await nextTick();
- expect(wrapper.find(GlTooltip).text()).toBe(target.getAttribute('title'));
+ expect(wrapper.findComponent(GlTooltip).text()).toBe(target.getAttribute('title'));
});
it('supports HTML content', async () => {
@@ -88,7 +88,7 @@ describe('tooltips/components/tooltips.vue', () => {
await nextTick();
- expect(wrapper.find(GlTooltip).html()).toContain(target.getAttribute('title'));
+ expect(wrapper.findComponent(GlTooltip).html()).toContain(target.getAttribute('title'));
});
it('sets the configuration values passed in the config object', async () => {
@@ -96,7 +96,7 @@ describe('tooltips/components/tooltips.vue', () => {
target = createTooltipTarget();
wrapper.vm.addTooltips([target], config);
await nextTick();
- expect(wrapper.find(GlTooltip).props()).toMatchObject(config);
+ expect(wrapper.findComponent(GlTooltip).props()).toMatchObject(config);
});
it.each`
@@ -113,7 +113,7 @@ describe('tooltips/components/tooltips.vue', () => {
await nextTick();
- expect(wrapper.find(GlTooltip).props(prop)).toBe(value);
+ expect(wrapper.findComponent(GlTooltip).props(prop)).toBe(value);
},
);
});
@@ -180,7 +180,7 @@ describe('tooltips/components/tooltips.vue', () => {
wrapper.vm.triggerEvent(target, event);
- expect(wrapper.find(GlTooltip).emitted(event)).toHaveLength(1);
+ expect(wrapper.findComponent(GlTooltip).emitted(event)).toHaveLength(1);
});
});
@@ -198,14 +198,14 @@ describe('tooltips/components/tooltips.vue', () => {
await nextTick();
- expect(wrapper.find(GlTooltip).text()).toBe(currentTitle);
+ expect(wrapper.findComponent(GlTooltip).text()).toBe(currentTitle);
target.setAttribute('title', newTitle);
wrapper.vm.fixTitle(target);
await nextTick();
- expect(wrapper.find(GlTooltip).text()).toBe(newTitle);
+ expect(wrapper.findComponent(GlTooltip).text()).toBe(newTitle);
});
});
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 941c8244247..5f067d9de3c 100644
--- a/spec/frontend/user_lists/components/edit_user_list_spec.js
+++ b/spec/frontend/user_lists/components/edit_user_list_spec.js
@@ -47,7 +47,7 @@ describe('user_lists/components/edit_user_list', () => {
});
it('should show a loading icon', () => {
- expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
+ expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
});
});
@@ -60,7 +60,7 @@ describe('user_lists/components/edit_user_list', () => {
factory();
await waitForPromises();
- alert = wrapper.find(GlAlert);
+ alert = wrapper.findComponent(GlAlert);
});
it('should show a flash with the error respopnse', () => {
@@ -72,7 +72,7 @@ describe('user_lists/components/edit_user_list', () => {
});
it('should not show a user list form', () => {
- expect(wrapper.find(UserListForm).exists()).toBe(false);
+ expect(wrapper.findComponent(UserListForm).exists()).toBe(false);
});
});
@@ -129,7 +129,7 @@ describe('user_lists/components/edit_user_list', () => {
clickSave();
await waitForPromises();
- alert = wrapper.find(GlAlert);
+ alert = wrapper.findComponent(GlAlert);
});
it('should show a flash with the error respopnse', () => {
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 ace4a284347..8683cf2463c 100644
--- a/spec/frontend/user_lists/components/new_user_list_spec.js
+++ b/spec/frontend/user_lists/components/new_user_list_spec.js
@@ -72,7 +72,7 @@ describe('user_lists/components/new_user_list', () => {
await waitForPromises();
- alert = wrapper.find(GlAlert);
+ alert = wrapper.findComponent(GlAlert);
});
it('should show a flash with the error respopnse', () => {
diff --git a/spec/frontend/user_lists/components/user_list_spec.js b/spec/frontend/user_lists/components/user_list_spec.js
index f126c733dd5..e02862cad2b 100644
--- a/spec/frontend/user_lists/components/user_list_spec.js
+++ b/spec/frontend/user_lists/components/user_list_spec.js
@@ -50,7 +50,7 @@ describe('User List', () => {
});
it('shows a loading icon', () => {
- expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
+ expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
});
});
@@ -157,7 +157,7 @@ describe('User List', () => {
});
describe('error', () => {
- const findAlert = () => wrapper.find(GlAlert);
+ const findAlert = () => wrapper.findComponent(GlAlert);
beforeEach(async () => {
Api.fetchFeatureFlagUserList.mockRejectedValue();
@@ -190,7 +190,7 @@ describe('User List', () => {
});
it('displays an empty state', () => {
- expect(wrapper.find(GlEmptyState).exists()).toBe(true);
+ expect(wrapper.findComponent(GlEmptyState).exists()).toBe(true);
});
});
});
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 fb5093eb065..3324b040b86 100644
--- a/spec/frontend/user_lists/components/user_lists_table_spec.js
+++ b/spec/frontend/user_lists/components/user_lists_table_spec.js
@@ -59,7 +59,7 @@ describe('User Lists Table', () => {
describe('delete button', () => {
it('should display the confirmation modal', async () => {
- const modal = wrapper.find(GlModal);
+ const modal = wrapper.findComponent(GlModal);
wrapper.find('[data-testid="delete-user-list"]').trigger('click');
@@ -73,7 +73,7 @@ describe('User Lists Table', () => {
let modal;
beforeEach(async () => {
- modal = wrapper.find(GlModal);
+ modal = wrapper.findComponent(GlModal);
wrapper.find('button').trigger('click');
diff --git a/spec/frontend/user_popovers_spec.js b/spec/frontend/user_popovers_spec.js
index 0530569c9df..8ce071c075f 100644
--- a/spec/frontend/user_popovers_spec.js
+++ b/spec/frontend/user_popovers_spec.js
@@ -188,8 +188,8 @@ describe('User Popovers', () => {
});
it('removes title attribute from user links', () => {
- expect(userLink.getAttribute('title')).toBeFalsy();
- expect(userLink.dataset.originalTitle).toBeFalsy();
+ expect(userLink.getAttribute('title')).toBe('');
+ expect(userLink.dataset.originalTitle).toBe('');
});
it('fetches user info and status from the user cache', () => {
diff --git a/spec/frontend/vue_merge_request_widget/components/approvals/approvals_spec.js b/spec/frontend/vue_merge_request_widget/components/approvals/approvals_spec.js
index 05cd1bb5b3d..1f3b6dce620 100644
--- a/spec/frontend/vue_merge_request_widget/components/approvals/approvals_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/approvals/approvals_spec.js
@@ -1,7 +1,7 @@
import { nextTick } from 'vue';
import { GlButton, GlSprintf } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import Approvals from '~/vue_merge_request_widget/components/approvals/approvals.vue';
import ApprovalsSummary from '~/vue_merge_request_widget/components/approvals/approvals_summary.vue';
import ApprovalsSummaryOptional from '~/vue_merge_request_widget/components/approvals/approvals_summary_optional.vue';
@@ -49,7 +49,7 @@ describe('MRWidget approvals', () => {
});
};
- const findAction = () => wrapper.find(GlButton);
+ const findAction = () => wrapper.findComponent(GlButton);
const findActionData = () => {
const action = findAction();
@@ -61,8 +61,8 @@ describe('MRWidget approvals', () => {
text: action.text(),
};
};
- const findSummary = () => wrapper.find(ApprovalsSummary);
- const findOptionalSummary = () => wrapper.find(ApprovalsSummaryOptional);
+ const findSummary = () => wrapper.findComponent(ApprovalsSummary);
+ const findOptionalSummary = () => wrapper.findComponent(ApprovalsSummaryOptional);
const findInvalidRules = () => wrapper.find('[data-testid="invalid-rules"]');
beforeEach(() => {
@@ -129,7 +129,7 @@ describe('MRWidget approvals', () => {
});
it('flashes error', () => {
- expect(createFlash).toHaveBeenCalledWith({ message: FETCH_ERROR });
+ expect(createAlert).toHaveBeenCalledWith({ message: FETCH_ERROR });
});
});
@@ -268,7 +268,7 @@ describe('MRWidget approvals', () => {
});
it('flashes error message', () => {
- expect(createFlash).toHaveBeenCalledWith({ message: APPROVE_ERROR });
+ expect(createAlert).toHaveBeenCalledWith({ message: APPROVE_ERROR });
});
});
});
@@ -319,7 +319,7 @@ describe('MRWidget approvals', () => {
});
it('flashes error message', () => {
- expect(createFlash).toHaveBeenCalledWith({ message: UNAPPROVE_ERROR });
+ expect(createAlert).toHaveBeenCalledWith({ message: UNAPPROVE_ERROR });
});
});
});
diff --git a/spec/frontend/vue_merge_request_widget/components/approvals/approvals_summary_optional_spec.js b/spec/frontend/vue_merge_request_widget/components/approvals/approvals_summary_optional_spec.js
index 65cafc647e0..e6fb0495947 100644
--- a/spec/frontend/vue_merge_request_widget/components/approvals/approvals_summary_optional_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/approvals/approvals_summary_optional_spec.js
@@ -18,7 +18,7 @@ describe('MRWidget approvals summary optional', () => {
wrapper = null;
});
- const findHelpLink = () => wrapper.find(GlLink);
+ const findHelpLink = () => wrapper.findComponent(GlLink);
describe('when can approve', () => {
beforeEach(() => {
diff --git a/spec/frontend/vue_merge_request_widget/components/approvals/approvals_summary_spec.js b/spec/frontend/vue_merge_request_widget/components/approvals/approvals_summary_spec.js
index c2606346292..f4234083346 100644
--- a/spec/frontend/vue_merge_request_widget/components/approvals/approvals_summary_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/approvals/approvals_summary_spec.js
@@ -29,7 +29,7 @@ describe('MRWidget approvals summary', () => {
});
};
- const findAvatars = () => wrapper.find(UserAvatarList);
+ const findAvatars = () => wrapper.findComponent(UserAvatarList);
afterEach(() => {
wrapper.destroy();
@@ -136,7 +136,7 @@ describe('MRWidget approvals summary', () => {
});
it('does not render avatar list', () => {
- expect(wrapper.find(UserAvatarList).exists()).toBe(false);
+ expect(wrapper.findComponent(UserAvatarList).exists()).toBe(false);
});
});
});
diff --git a/spec/frontend/vue_merge_request_widget/components/artifacts_list_app_spec.js b/spec/frontend/vue_merge_request_widget/components/artifacts_list_app_spec.js
index e2386bc7f2b..73fa4b7b08f 100644
--- a/spec/frontend/vue_merge_request_widget/components/artifacts_list_app_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/artifacts_list_app_spec.js
@@ -60,7 +60,7 @@ describe('Merge Requests Artifacts list app', () => {
});
it('renders a loading icon', () => {
- const loadingIcon = wrapper.find(GlLoadingIcon);
+ const loadingIcon = wrapper.findComponent(GlLoadingIcon);
expect(loadingIcon.exists()).toBe(true);
});
diff --git a/spec/frontend/vue_merge_request_widget/components/artifacts_list_spec.js b/spec/frontend/vue_merge_request_widget/components/artifacts_list_spec.js
index d519ad2cdb0..b7bf72cd215 100644
--- a/spec/frontend/vue_merge_request_widget/components/artifacts_list_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/artifacts_list_spec.js
@@ -31,11 +31,11 @@ describe('Artifacts List', () => {
});
it('renders link for the artifact', () => {
- expect(wrapper.find(GlLink).attributes('href')).toEqual(data.artifacts[0].url);
+ expect(wrapper.findComponent(GlLink).attributes('href')).toEqual(data.artifacts[0].url);
});
it('renders artifact name', () => {
- expect(wrapper.find(GlLink).text()).toEqual(data.artifacts[0].text);
+ expect(wrapper.findComponent(GlLink).text()).toEqual(data.artifacts[0].text);
});
it('renders job url', () => {
diff --git a/spec/frontend/vue_merge_request_widget/components/mr_collapsible_extension_spec.js b/spec/frontend/vue_merge_request_widget/components/mr_collapsible_extension_spec.js
index 01fbcb2154f..c253dc63f23 100644
--- a/spec/frontend/vue_merge_request_widget/components/mr_collapsible_extension_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/mr_collapsible_extension_spec.js
@@ -23,7 +23,7 @@ describe('Merge Request Collapsible Extension', () => {
const findTitle = () => wrapper.find('[data-testid="mr-collapsible-title"]');
const findErrorMessage = () => wrapper.find('.js-error-state');
- const findIcon = () => wrapper.find(GlIcon);
+ const findIcon = () => wrapper.findComponent(GlIcon);
afterEach(() => {
wrapper.destroy();
@@ -77,7 +77,7 @@ describe('Merge Request Collapsible Extension', () => {
});
it('renders loading spinner', () => {
- expect(wrapper.find(GlLoadingIcon).isVisible()).toBe(true);
+ expect(wrapper.findComponent(GlLoadingIcon).isVisible()).toBe(true);
});
});
diff --git a/spec/frontend/vue_merge_request_widget/components/mr_widget_author_time_spec.js b/spec/frontend/vue_merge_request_widget/components/mr_widget_author_time_spec.js
index 8fd93809e01..90a29d15488 100644
--- a/spec/frontend/vue_merge_request_widget/components/mr_widget_author_time_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/mr_widget_author_time_spec.js
@@ -32,7 +32,9 @@ describe('MrWidgetAuthorTime', () => {
});
it('renders author', () => {
- expect(wrapper.find(MrWidgetAuthor).props('author')).toStrictEqual(defaultProps.author);
+ expect(wrapper.findComponent(MrWidgetAuthor).props('author')).toStrictEqual(
+ defaultProps.author,
+ );
});
it('renders provided time', () => {
diff --git a/spec/frontend/vue_merge_request_widget/components/mr_widget_expandable_section_spec.js b/spec/frontend/vue_merge_request_widget/components/mr_widget_expandable_section_spec.js
index 631aef412a6..8eaed998eb5 100644
--- a/spec/frontend/vue_merge_request_widget/components/mr_widget_expandable_section_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/mr_widget_expandable_section_spec.js
@@ -6,8 +6,8 @@ import MrCollapsibleSection from '~/vue_merge_request_widget/components/mr_widge
describe('MrWidgetExpanableSection', () => {
let wrapper;
- const findButton = () => wrapper.find(GlButton);
- const findCollapse = () => wrapper.find(GlCollapse);
+ const findButton = () => wrapper.findComponent(GlButton);
+ const findCollapse = () => wrapper.findComponent(GlCollapse);
beforeEach(() => {
wrapper = shallowMount(MrCollapsibleSection, {
@@ -19,7 +19,7 @@ describe('MrWidgetExpanableSection', () => {
});
it('renders Icon', () => {
- expect(wrapper.find(GlIcon).exists()).toBe(true);
+ expect(wrapper.findComponent(GlIcon).exists()).toBe(true);
});
it('renders header slot', () => {
diff --git a/spec/frontend/vue_merge_request_widget/components/mr_widget_icon_spec.js b/spec/frontend/vue_merge_request_widget/components/mr_widget_icon_spec.js
index ebd10f31fa7..6a9b019fb4f 100644
--- a/spec/frontend/vue_merge_request_widget/components/mr_widget_icon_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/mr_widget_icon_spec.js
@@ -21,6 +21,6 @@ describe('MrWidgetIcon', () => {
it('renders icon and container', () => {
expect(wrapper.element.className).toContain('circle-icon-container');
- expect(wrapper.find(GlIcon).props('name')).toEqual(TEST_ICON);
+ expect(wrapper.findComponent(GlIcon).props('name')).toEqual(TEST_ICON);
});
});
diff --git a/spec/frontend/vue_merge_request_widget/components/mr_widget_pipeline_container_spec.js b/spec/frontend/vue_merge_request_widget/components/mr_widget_pipeline_container_spec.js
index efe2bf75c3f..c3f6331e560 100644
--- a/spec/frontend/vue_merge_request_widget/components/mr_widget_pipeline_container_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/mr_widget_pipeline_container_spec.js
@@ -41,8 +41,8 @@ describe('MrWidgetPipelineContainer', () => {
});
it('renders pipeline', () => {
- expect(wrapper.find(MrWidgetPipeline).exists()).toBe(true);
- expect(wrapper.find(MrWidgetPipeline).props()).toMatchObject({
+ expect(wrapper.findComponent(MrWidgetPipeline).exists()).toBe(true);
+ expect(wrapper.findComponent(MrWidgetPipeline).props()).toMatchObject({
pipeline: mockStore.pipeline,
pipelineCoverageDelta: mockStore.pipelineCoverageDelta,
ciStatus: mockStore.ciStatus,
@@ -82,9 +82,9 @@ describe('MrWidgetPipelineContainer', () => {
});
it('renders pipeline', () => {
- expect(wrapper.find(MrWidgetPipeline).exists()).toBe(true);
+ expect(wrapper.findComponent(MrWidgetPipeline).exists()).toBe(true);
expect(findCIErrorMessage().exists()).toBe(false);
- expect(wrapper.find(MrWidgetPipeline).props()).toMatchObject({
+ expect(wrapper.findComponent(MrWidgetPipeline).props()).toMatchObject({
pipeline: mockStore.mergePipeline,
pipelineCoverageDelta: mockStore.pipelineCoverageDelta,
ciStatus: mockStore.mergePipeline.details.status.text,
@@ -102,7 +102,7 @@ describe('MrWidgetPipelineContainer', () => {
targetBranch: 'Foo<script>alert("XSS")</script>',
},
});
- expect(wrapper.find(MrWidgetPipeline).props().sourceBranchLink).toBe('Foo');
+ expect(wrapper.findComponent(MrWidgetPipeline).props().sourceBranchLink).toBe('Foo');
});
it('renders deployments', () => {
@@ -125,7 +125,7 @@ describe('MrWidgetPipelineContainer', () => {
it('renders the artifacts app', () => {
factory();
- expect(wrapper.find(ArtifactsApp).isVisible()).toBe(true);
+ expect(wrapper.findComponent(ArtifactsApp).isVisible()).toBe(true);
});
});
});
diff --git a/spec/frontend/vue_merge_request_widget/components/mr_widget_suggest_pipeline_spec.js b/spec/frontend/vue_merge_request_widget/components/mr_widget_suggest_pipeline_spec.js
index d6c67dab381..73358edee78 100644
--- a/spec/frontend/vue_merge_request_widget/components/mr_widget_suggest_pipeline_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/mr_widget_suggest_pipeline_spec.js
@@ -57,7 +57,7 @@ describe('MRWidgetSuggestPipeline', () => {
});
it('renders widget icon', () => {
- const icon = wrapper.find(MrWidgetIcon);
+ const icon = wrapper.findComponent(MrWidgetIcon);
expect(icon.exists()).toBe(true);
expect(icon.props()).toEqual(
@@ -115,7 +115,7 @@ describe('MRWidgetSuggestPipeline', () => {
});
describe('dismissible', () => {
- const findDismissContainer = () => wrapper.find(dismissibleContainer);
+ const findDismissContainer = () => wrapper.findComponent(dismissibleContainer);
beforeEach(() => {
wrapper = shallowMount(suggestPipelineComponent, { propsData: suggestProps });
diff --git a/spec/frontend/vue_merge_request_widget/components/states/__snapshots__/mr_widget_auto_merge_enabled_spec.js.snap b/spec/frontend/vue_merge_request_widget/components/states/__snapshots__/mr_widget_auto_merge_enabled_spec.js.snap
index 635ef0f6b0d..5f383c468d8 100644
--- a/spec/frontend/vue_merge_request_widget/components/states/__snapshots__/mr_widget_auto_merge_enabled_spec.js.snap
+++ b/spec/frontend/vue_merge_request_widget/components/states/__snapshots__/mr_widget_auto_merge_enabled_spec.js.snap
@@ -72,11 +72,14 @@ exports[`MRWidgetAutoMergeEnabled when graphql is disabled template should have
<div
class="gl-display-flex gl-md-display-block gl-font-size-0 gl-ml-auto"
>
- <div>
+ <div
+ class="gl-display-flex gl-align-items-flex-start"
+ >
<div
class="dropdown b-dropdown gl-new-dropdown gl-display-block gl-md-display-none! btn-group"
lazy=""
no-caret=""
+ title="Options"
>
<!---->
<button
@@ -246,11 +249,14 @@ exports[`MRWidgetAutoMergeEnabled when graphql is enabled template should have c
<div
class="gl-display-flex gl-md-display-block gl-font-size-0 gl-ml-auto"
>
- <div>
+ <div
+ class="gl-display-flex gl-align-items-flex-start"
+ >
<div
class="dropdown b-dropdown gl-new-dropdown gl-display-block gl-md-display-none! btn-group"
lazy=""
no-caret=""
+ title="Options"
>
<!---->
<button
diff --git a/spec/frontend/vue_merge_request_widget/components/states/merge_checks_failed_spec.js b/spec/frontend/vue_merge_request_widget/components/states/merge_checks_failed_spec.js
index 1900b53ac11..d85574262fe 100644
--- a/spec/frontend/vue_merge_request_widget/components/states/merge_checks_failed_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/states/merge_checks_failed_spec.js
@@ -15,9 +15,9 @@ describe('Merge request widget merge checks failed state component', () => {
});
it.each`
- mrState | displayText
- ${{ approvals: true, isApproved: false }} | ${'approvalNeeded'}
- ${{ blockingMergeRequests: { total_count: 1 } }} | ${'blockingMergeRequests'}
+ mrState | displayText
+ ${{ approvals: true, isApproved: false }} | ${'approvalNeeded'}
+ ${{ detailedMergeStatus: 'BLOCKED_STATUS' }} | ${'blockingMergeRequests'}
`('display $displayText text for $mrState', ({ mrState, displayText }) => {
factory({ mr: mrState });
diff --git a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed_spec.js b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed_spec.js
index 9320e733636..398a3912882 100644
--- a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed_spec.js
@@ -7,7 +7,7 @@ import eventHub from '~/vue_merge_request_widget/event_hub';
describe('MRWidgetAutoMergeFailed', () => {
let wrapper;
const mergeError = 'This is the merge error';
- const findButton = () => wrapper.find(GlButton);
+ const findButton = () => wrapper.findComponent(GlButton);
const createComponent = (props = {}, mergeRequestWidgetGraphql = false) => {
wrapper = mount(AutoMergeFailedComponent, {
@@ -61,7 +61,7 @@ describe('MRWidgetAutoMergeFailed', () => {
await nextTick();
expect(findButton().attributes('disabled')).toBe('disabled');
- expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
+ expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
});
});
});
diff --git a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_merged_spec.js b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_merged_spec.js
index 2606933450e..a3aa563b516 100644
--- a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_merged_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_merged_spec.js
@@ -1,180 +1,172 @@
import { getByRole } from '@testing-library/dom';
-import Vue from 'vue';
-import mountComponent from 'helpers/vue_mount_component_helper';
+import { nextTick } from 'vue';
+import { mount } from '@vue/test-utils';
import waitForPromises from 'helpers/wait_for_promises';
import { OPEN_REVERT_MODAL, OPEN_CHERRY_PICK_MODAL } from '~/projects/commit/constants';
import modalEventHub from '~/projects/commit/event_hub';
-import mergedComponent from '~/vue_merge_request_widget/components/states/mr_widget_merged.vue';
+import MergedComponent from '~/vue_merge_request_widget/components/states/mr_widget_merged.vue';
import eventHub from '~/vue_merge_request_widget/event_hub';
describe('MRWidgetMerged', () => {
- let vm;
+ let wrapper;
const targetBranch = 'foo';
-
- beforeEach(() => {
- jest.spyOn(document, 'dispatchEvent');
- const Component = Vue.extend(mergedComponent);
- const mr = {
- isRemovingSourceBranch: false,
- cherryPickInForkPath: false,
- canCherryPickInCurrentMR: true,
- revertInForkPath: false,
- canRevertInCurrentMR: true,
- canRemoveSourceBranch: true,
- sourceBranchRemoved: true,
- metrics: {
- mergedBy: {
- name: 'Administrator',
- username: 'root',
- webUrl: 'http://localhost:3000/root',
- avatarUrl:
- 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ const mr = {
+ isRemovingSourceBranch: false,
+ cherryPickInForkPath: false,
+ canCherryPickInCurrentMR: true,
+ revertInForkPath: false,
+ canRevertInCurrentMR: true,
+ canRemoveSourceBranch: true,
+ sourceBranchRemoved: true,
+ metrics: {
+ mergedBy: {
+ name: 'Administrator',
+ username: 'root',
+ webUrl: 'http://localhost:3000/root',
+ avatarUrl:
+ 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ },
+ mergedAt: 'Jan 24, 2018 1:02pm UTC',
+ readableMergedAt: '',
+ closedBy: {},
+ closedAt: 'Jan 24, 2018 1:02pm UTC',
+ readableClosedAt: '',
+ },
+ updatedAt: 'mergedUpdatedAt',
+ shortMergeCommitSha: '958c0475',
+ mergeCommitSha: '958c047516e182dfc52317f721f696e8a1ee85ed',
+ mergeCommitPath:
+ 'http://localhost:3000/root/nautilus/commit/f7ce827c314c9340b075657fd61c789fb01cf74d',
+ sourceBranch: 'bar',
+ targetBranch,
+ };
+
+ const service = {
+ removeSourceBranch: () => nextTick(),
+ };
+
+ const createComponent = (customMrFields = {}) => {
+ wrapper = mount(MergedComponent, {
+ propsData: {
+ mr: {
+ ...mr,
+ ...customMrFields,
},
- mergedAt: 'Jan 24, 2018 1:02pm UTC',
- readableMergedAt: '',
- closedBy: {},
- closedAt: 'Jan 24, 2018 1:02pm UTC',
- readableClosedAt: '',
+ service,
},
- updatedAt: 'mergedUpdatedAt',
- shortMergeCommitSha: '958c0475',
- mergeCommitSha: '958c047516e182dfc52317f721f696e8a1ee85ed',
- mergeCommitPath:
- 'http://localhost:3000/root/nautilus/commit/f7ce827c314c9340b075657fd61c789fb01cf74d',
- sourceBranch: 'bar',
- targetBranch,
- };
-
- const service = {
- removeSourceBranch() {},
- };
+ });
+ };
+ beforeEach(() => {
+ jest.spyOn(document, 'dispatchEvent');
jest.spyOn(eventHub, '$emit').mockImplementation(() => {});
-
- vm = mountComponent(Component, { mr, service });
});
afterEach(() => {
- vm.$destroy();
+ wrapper.destroy();
});
- describe('computed', () => {
- describe('shouldShowRemoveSourceBranch', () => {
- it('returns true when sourceBranchRemoved is false', () => {
- vm.mr.sourceBranchRemoved = false;
-
- expect(vm.shouldShowRemoveSourceBranch).toEqual(true);
- });
-
- it('returns false when sourceBranchRemoved is true', () => {
- vm.mr.sourceBranchRemoved = true;
-
- expect(vm.shouldShowRemoveSourceBranch).toEqual(false);
- });
-
- it('returns false when canRemoveSourceBranch is false', () => {
- vm.mr.sourceBranchRemoved = false;
- vm.mr.canRemoveSourceBranch = false;
-
- expect(vm.shouldShowRemoveSourceBranch).toEqual(false);
- });
-
- it('returns false when is making request', () => {
- vm.mr.canRemoveSourceBranch = true;
- vm.isMakingRequest = true;
-
- expect(vm.shouldShowRemoveSourceBranch).toEqual(false);
- });
+ const findButtonByText = (text) =>
+ wrapper.findAll('button').wrappers.find((w) => w.text() === text);
+ const findRemoveSourceBranchButton = () => findButtonByText('Delete source branch');
- it('returns true when all are true', () => {
- vm.mr.isRemovingSourceBranch = true;
- vm.mr.canRemoveSourceBranch = true;
- vm.isMakingRequest = true;
+ describe('remove source branch button', () => {
+ it('is displayed when sourceBranchRemoved is false', () => {
+ createComponent({ sourceBranchRemoved: false });
- expect(vm.shouldShowRemoveSourceBranch).toEqual(false);
- });
+ expect(findRemoveSourceBranchButton().exists()).toBe(true);
});
- describe('shouldShowSourceBranchRemoving', () => {
- it('should correct value when fields changed', () => {
- vm.mr.sourceBranchRemoved = false;
+ it('is not displayed when sourceBranchRemoved is true', () => {
+ createComponent({ sourceBranchRemoved: true });
- expect(vm.shouldShowSourceBranchRemoving).toEqual(false);
+ expect(findRemoveSourceBranchButton()).toBe(undefined);
+ });
- vm.mr.sourceBranchRemoved = true;
+ it('is not displayed when canRemoveSourceBranch is true', () => {
+ createComponent({ sourceBranchRemoved: false, canRemoveSourceBranch: false });
- expect(vm.shouldShowRemoveSourceBranch).toEqual(false);
+ expect(findRemoveSourceBranchButton()).toBe(undefined);
+ });
- vm.mr.sourceBranchRemoved = false;
- vm.isMakingRequest = true;
+ it('is not displayed when is making request', async () => {
+ createComponent({ sourceBranchRemoved: false, canRemoveSourceBranch: true });
- expect(vm.shouldShowSourceBranchRemoving).toEqual(true);
+ await findRemoveSourceBranchButton().trigger('click');
- vm.isMakingRequest = false;
- vm.mr.isRemovingSourceBranch = true;
+ expect(findRemoveSourceBranchButton()).toBe(undefined);
+ });
- expect(vm.shouldShowSourceBranchRemoving).toEqual(true);
+ it('is not displayed when all are true', () => {
+ createComponent({
+ isRemovingSourceBranch: true,
+ sourceBranchRemoved: false,
+ canRemoveSourceBranch: true,
});
+
+ expect(findRemoveSourceBranchButton()).toBe(undefined);
});
});
- describe('methods', () => {
- describe('removeSourceBranch', () => {
- it('should set flag and call service then request main component to update the widget', async () => {
- jest.spyOn(vm.service, 'removeSourceBranch').mockReturnValue(
- new Promise((resolve) => {
- resolve({
- data: {
- message: 'Branch was deleted',
- },
- });
- }),
- );
+ it('should set flag and call service then request main component to update the widget when branch is removed', async () => {
+ createComponent({ sourceBranchRemoved: false });
+ jest.spyOn(service, 'removeSourceBranch').mockResolvedValue({
+ data: {
+ message: 'Branch was deleted',
+ },
+ });
- vm.removeSourceBranch();
+ await findRemoveSourceBranchButton().trigger('click');
- await waitForPromises();
+ await waitForPromises();
- const args = eventHub.$emit.mock.calls[0];
+ const args = eventHub.$emit.mock.calls[0];
- expect(vm.isMakingRequest).toEqual(true);
- expect(args[0]).toEqual('MRWidgetUpdateRequested');
- expect(args[1]).not.toThrow();
- });
- });
+ expect(args[0]).toEqual('MRWidgetUpdateRequested');
+ expect(args[1]).not.toThrow();
});
it('calls dispatchDocumentEvent to load in the modal component', () => {
+ createComponent();
+
expect(document.dispatchEvent).toHaveBeenCalledWith(new CustomEvent('merged:UpdateActions'));
});
it('emits event to open the revert modal on revert button click', () => {
+ createComponent();
const eventHubSpy = jest.spyOn(modalEventHub, '$emit');
- getByRole(vm.$el, 'button', { name: /Revert/i }).click();
+ getByRole(wrapper.element, 'button', { name: /Revert/i }).click();
expect(eventHubSpy).toHaveBeenCalledWith(OPEN_REVERT_MODAL);
});
it('emits event to open the cherry-pick modal on cherry-pick button click', () => {
+ createComponent();
const eventHubSpy = jest.spyOn(modalEventHub, '$emit');
- getByRole(vm.$el, 'button', { name: /Cherry-pick/i }).click();
+ getByRole(wrapper.element, 'button', { name: /Cherry-pick/i }).click();
expect(eventHubSpy).toHaveBeenCalledWith(OPEN_CHERRY_PICK_MODAL);
});
it('has merged by information', () => {
- expect(vm.$el.textContent).toContain('Merged by');
- expect(vm.$el.textContent).toContain('Administrator');
+ createComponent();
+
+ expect(wrapper.text()).toContain('Merged by');
+ expect(wrapper.text()).toContain('Administrator');
});
it('shows revert and cherry-pick buttons', () => {
- expect(vm.$el.textContent).toContain('Revert');
- expect(vm.$el.textContent).toContain('Cherry-pick');
+ createComponent();
+
+ expect(wrapper.text()).toContain('Revert');
+ expect(wrapper.text()).toContain('Cherry-pick');
});
it('should use mergedEvent mergedAt as tooltip title', () => {
- expect(vm.$el.querySelector('time').getAttribute('title')).toBe('Jan 24, 2018 1:02pm UTC');
+ createComponent();
+
+ expect(wrapper.find('time').attributes('title')).toBe('Jan 24, 2018 1:02pm UTC');
});
});
diff --git a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_pipeline_failed_spec.js b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_pipeline_failed_spec.js
index d5619d4996d..bd158d59d74 100644
--- a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_pipeline_failed_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_pipeline_failed_spec.js
@@ -6,31 +6,42 @@ import StatusIcon from '~/vue_merge_request_widget/components/mr_widget_status_i
describe('PipelineFailed', () => {
let wrapper;
- const createComponent = () => {
+ const createComponent = (mr = {}) => {
wrapper = shallowMount(PipelineFailed, {
+ propsData: {
+ mr,
+ },
stubs: {
GlSprintf,
},
});
};
- beforeEach(() => {
- createComponent();
- });
-
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
it('should render error status icon', () => {
+ createComponent();
+
expect(wrapper.findComponent(StatusIcon).exists()).toBe(true);
expect(wrapper.findComponent(StatusIcon).props().status).toBe('failed');
});
it('should render error message with a disabled merge button', () => {
+ createComponent();
+
expect(wrapper.text()).toContain('Merge blocked: pipeline must succeed.');
expect(wrapper.text()).toContain('Push a commit that fixes the failure');
expect(wrapper.findComponent(GlLink).text()).toContain('learn about other solutions');
});
+
+ it('should render pipeline blocked message', () => {
+ createComponent({ isPipelineBlocked: true });
+
+ expect(wrapper.text()).toContain(
+ "Merge blocked: pipeline must succeed. It's waiting for a manual action to continue.",
+ );
+ });
});
diff --git a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_ready_to_merge_spec.js b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_ready_to_merge_spec.js
index 9a6bf66909e..48d3f15560b 100644
--- a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_ready_to_merge_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_ready_to_merge_spec.js
@@ -105,16 +105,17 @@ const createComponent = (
},
stubs: {
CommitEdit,
+ GlSprintf,
},
apolloProvider: createMockApollo([[readyToMergeQuery, readyToMergeResponseSpy]]),
});
};
-const findCheckboxElement = () => wrapper.find(SquashBeforeMerge);
+const findCheckboxElement = () => wrapper.findComponent(SquashBeforeMerge);
const findCommitEditElements = () => wrapper.findAllComponents(CommitEdit);
-const findCommitDropdownElement = () => wrapper.find(CommitMessageDropdown);
+const findCommitDropdownElement = () => wrapper.findComponent(CommitMessageDropdown);
const findFirstCommitEditLabel = () => findCommitEditElements().at(0).props('label');
-const findTipLink = () => wrapper.find(GlSprintf);
+const findTipLink = () => wrapper.findComponent(GlSprintf);
const findCommitEditWithInputId = (inputId) =>
findCommitEditElements().wrappers.find((x) => x.props('inputId') === inputId);
const findMergeCommitMessage = () => findCommitEditWithInputId('merge-message-edit').props('value');
@@ -300,6 +301,48 @@ describe('ReadyToMerge', () => {
expect(wrapper.vm.isMergeButtonDisabled).toBe(true);
});
});
+
+ describe('sourceBranchDeletedText', () => {
+ const should = 'Source branch will be deleted.';
+ const shouldNot = 'Source branch will not be deleted.';
+ const did = 'Deleted the source branch.';
+ const didNot = 'Did not delete the source branch.';
+ const scenarios = [
+ "the MR hasn't merged yet, and the backend-provided value expects to delete the branch",
+ "the MR hasn't merged yet, and the backend-provided value expects to leave the branch",
+ "the MR hasn't merged yet, and the backend-provided value is a non-boolean falsey value",
+ "the MR hasn't merged yet, and the backend-provided value is a non-boolean truthy value",
+ 'the MR has been merged, and the backend reports that the branch has been removed',
+ 'the MR has been merged, and the backend reports that the branch has not been removed',
+ 'the MR has been merged, and the backend reports a non-boolean falsey value',
+ 'the MR has been merged, and the backend reports a non-boolean truthy value',
+ ];
+
+ it.each`
+ describe | premerge | mrShould | mrRemoved | output
+ ${scenarios[0]} | ${true} | ${true} | ${null} | ${should}
+ ${scenarios[1]} | ${true} | ${false} | ${null} | ${shouldNot}
+ ${scenarios[2]} | ${true} | ${null} | ${null} | ${shouldNot}
+ ${scenarios[3]} | ${true} | ${'yeah'} | ${null} | ${should}
+ ${scenarios[4]} | ${false} | ${null} | ${true} | ${did}
+ ${scenarios[5]} | ${false} | ${null} | ${false} | ${didNot}
+ ${scenarios[6]} | ${false} | ${null} | ${null} | ${didNot}
+ ${scenarios[7]} | ${false} | ${null} | ${'yep'} | ${did}
+ `(
+ 'in the case that $describe, returns "$output"',
+ ({ premerge, mrShould, mrRemoved, output }) => {
+ createComponent({
+ mr: {
+ state: !premerge ? 'merged' : 'literally-anything-else',
+ shouldRemoveSourceBranch: mrShould,
+ sourceBranchRemoved: mrRemoved,
+ },
+ });
+
+ expect(wrapper.vm.sourceBranchDeletedText).toBe(output);
+ },
+ );
+ });
});
describe('methods', () => {
@@ -733,6 +776,34 @@ describe('ReadyToMerge', () => {
});
});
+ describe('source and target branches diverged', () => {
+ describe('when the MR is showing the Merge button', () => {
+ it('does not display the diverged commits message if the source branch is not behind the target', () => {
+ createComponent({ mr: { divergedCommitsCount: 0 } });
+
+ const textBody = wrapper.text();
+
+ expect(textBody).toEqual(
+ expect.not.stringContaining('The source branch is 0 commits behind the target branch'),
+ );
+ expect(textBody).toEqual(
+ expect.not.stringContaining('The source branch is 0 commit behind the target branch'),
+ );
+ expect(textBody).toEqual(
+ expect.not.stringContaining('The source branch is behind the target branch'),
+ );
+ });
+
+ it('shows the diverged commits text when the source branch is behind the target', () => {
+ createComponent({ mr: { divergedCommitsCount: 9001, canMerge: false } });
+
+ expect(wrapper.text()).toEqual(
+ expect.stringContaining('The source branch is 9001 commits behind the target branch'),
+ );
+ });
+ });
+ });
+
describe('Merge button when pipeline has failed', () => {
beforeEach(() => {
createComponent({
diff --git a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_squash_before_merge_spec.js b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_squash_before_merge_spec.js
index 6ea2e8675d3..c839fa17fe5 100644
--- a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_squash_before_merge_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_squash_before_merge_spec.js
@@ -18,7 +18,7 @@ describe('Squash before merge component', () => {
wrapper.destroy();
});
- const findCheckbox = () => wrapper.find(GlFormCheckbox);
+ const findCheckbox = () => wrapper.findComponent(GlFormCheckbox);
describe('checkbox', () => {
it('is unchecked if passed value prop is false', () => {
diff --git a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_wip_spec.js b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_wip_spec.js
index af52901f508..7259f210b6e 100644
--- a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_wip_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_wip_spec.js
@@ -38,7 +38,7 @@ describe('Wip', () => {
it('should have default data', () => {
const vm = createComponent();
- expect(vm.isMakingRequest).toBeFalsy();
+ expect(vm.isMakingRequest).toBe(false);
});
});
diff --git a/spec/frontend/vue_merge_request_widget/components/terraform/mr_widget_terraform_container_spec.js b/spec/frontend/vue_merge_request_widget/components/terraform/mr_widget_terraform_container_spec.js
deleted file mode 100644
index 7a868eb8cc9..00000000000
--- a/spec/frontend/vue_merge_request_widget/components/terraform/mr_widget_terraform_container_spec.js
+++ /dev/null
@@ -1,175 +0,0 @@
-import { GlSkeletonLoader, GlSprintf } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
-import MockAdapter from 'axios-mock-adapter';
-import { nextTick } from 'vue';
-import axios from '~/lib/utils/axios_utils';
-import Poll from '~/lib/utils/poll';
-import MrWidgetExpanableSection from '~/vue_merge_request_widget/components/mr_widget_expandable_section.vue';
-import MrWidgetTerraformContainer from '~/vue_merge_request_widget/components/terraform/mr_widget_terraform_container.vue';
-import TerraformPlan from '~/vue_merge_request_widget/components/terraform/terraform_plan.vue';
-import { invalidPlanWithName, plans, validPlanWithName } from './mock_data';
-
-describe('MrWidgetTerraformConainer', () => {
- let mock;
- let wrapper;
-
- const propsData = { endpoint: '/path/to/terraform/report.json' };
-
- const findHeader = () => wrapper.find('[data-testid="terraform-header-text"]');
- const findPlans = () =>
- wrapper.findAllComponents(TerraformPlan).wrappers.map((x) => x.props('plan'));
-
- const mockPollingApi = (response, body, header) => {
- mock.onGet(propsData.endpoint).reply(response, body, header);
- };
-
- const mountWrapper = () => {
- wrapper = shallowMount(MrWidgetTerraformContainer, {
- propsData,
- stubs: { MrWidgetExpanableSection, GlSprintf },
- });
- return axios.waitForAll();
- };
-
- beforeEach(() => {
- mock = new MockAdapter(axios);
- });
-
- afterEach(() => {
- wrapper.destroy();
- mock.restore();
- });
-
- describe('when data is loading', () => {
- beforeEach(async () => {
- mockPollingApi(200, plans, {});
-
- await mountWrapper();
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({ loading: true });
- await nextTick();
- });
-
- it('diplays loading skeleton', () => {
- expect(wrapper.findComponent(GlSkeletonLoader).exists()).toBe(true);
- expect(wrapper.find(MrWidgetExpanableSection).exists()).toBe(false);
- });
- });
-
- describe('when data has finished loading', () => {
- beforeEach(() => {
- mockPollingApi(200, plans, {});
- return mountWrapper();
- });
-
- it('displays terraform content', () => {
- expect(wrapper.findComponent(GlSkeletonLoader).exists()).toBe(false);
- expect(wrapper.find(MrWidgetExpanableSection).exists()).toBe(true);
- expect(findPlans()).toEqual(Object.values(plans));
- });
-
- describe('when data includes one invalid plan', () => {
- beforeEach(() => {
- const invalidPlanGroup = { bad_plan: invalidPlanWithName };
- mockPollingApi(200, invalidPlanGroup, {});
- return mountWrapper();
- });
-
- it('displays header text for one invalid plan', () => {
- expect(findHeader().text()).toBe('1 Terraform report failed to generate');
- });
- });
-
- describe('when data includes multiple invalid plans', () => {
- beforeEach(() => {
- const invalidPlanGroup = {
- bad_plan_one: invalidPlanWithName,
- bad_plan_two: invalidPlanWithName,
- };
-
- mockPollingApi(200, invalidPlanGroup, {});
- return mountWrapper();
- });
-
- it('displays header text for multiple invalid plans', () => {
- expect(findHeader().text()).toBe('2 Terraform reports failed to generate');
- });
- });
-
- describe('when data includes one valid plan', () => {
- beforeEach(() => {
- const validPlanGroup = { valid_plan: validPlanWithName };
- mockPollingApi(200, validPlanGroup, {});
- return mountWrapper();
- });
-
- it('displays header text for one valid plans', () => {
- expect(findHeader().text()).toBe('1 Terraform report was generated in your pipelines');
- });
- });
-
- describe('when data includes multiple valid plans', () => {
- beforeEach(() => {
- const validPlanGroup = {
- valid_plan_one: validPlanWithName,
- valid_plan_two: validPlanWithName,
- };
- mockPollingApi(200, validPlanGroup, {});
- return mountWrapper();
- });
-
- it('displays header text for multiple valid plans', () => {
- expect(findHeader().text()).toBe('2 Terraform reports were generated in your pipelines');
- });
- });
- });
-
- describe('polling', () => {
- let pollRequest;
- let pollStop;
-
- beforeEach(() => {
- pollRequest = jest.spyOn(Poll.prototype, 'makeRequest');
- pollStop = jest.spyOn(Poll.prototype, 'stop');
- });
-
- afterEach(() => {
- pollRequest.mockRestore();
- pollStop.mockRestore();
- });
-
- describe('successful poll', () => {
- beforeEach(() => {
- mockPollingApi(200, plans, {});
-
- return mountWrapper();
- });
-
- it('does not make additional requests after poll is successful', () => {
- expect(pollRequest).toHaveBeenCalledTimes(1);
- expect(pollStop).toHaveBeenCalledTimes(1);
- });
- });
-
- describe('polling fails', () => {
- beforeEach(() => {
- mockPollingApi(500, null, {});
- return mountWrapper();
- });
-
- it('stops loading', () => {
- expect(wrapper.findComponent(GlSkeletonLoader).exists()).toBe(false);
- });
-
- it('generates one broken plan', () => {
- expect(findPlans()).toEqual([{ tf_report_error: 'api_error' }]);
- });
-
- it('does not make additional requests after poll is unsuccessful', () => {
- expect(pollRequest).toHaveBeenCalledTimes(1);
- expect(pollStop).toHaveBeenCalledTimes(1);
- });
- });
- });
-});
diff --git a/spec/frontend/vue_merge_request_widget/components/terraform/terraform_plan_spec.js b/spec/frontend/vue_merge_request_widget/components/terraform/terraform_plan_spec.js
deleted file mode 100644
index 3c9f6c2e165..00000000000
--- a/spec/frontend/vue_merge_request_widget/components/terraform/terraform_plan_spec.js
+++ /dev/null
@@ -1,93 +0,0 @@
-import { GlLink, GlSprintf } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
-import TerraformPlan from '~/vue_merge_request_widget/components/terraform/terraform_plan.vue';
-import {
- invalidPlanWithName,
- invalidPlanWithoutName,
- validPlanWithName,
- validPlanWithoutName,
-} from './mock_data';
-
-describe('TerraformPlan', () => {
- let wrapper;
-
- const findIcon = () => wrapper.find('[data-testid="change-type-icon"]');
- const findLogButton = () => wrapper.find('[data-testid="terraform-report-link"]');
-
- const mountWrapper = (propsData) => {
- wrapper = shallowMount(TerraformPlan, { stubs: { GlLink, GlSprintf }, propsData });
- };
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- describe('valid plan with job_name', () => {
- beforeEach(() => {
- mountWrapper({ plan: validPlanWithName });
- });
-
- it('displays a document icon', () => {
- expect(findIcon().attributes('name')).toBe('doc-changes');
- });
-
- it('diplays the header text with a name', () => {
- expect(wrapper.text()).toContain(`The job ${validPlanWithName.job_name} generated a report.`);
- });
-
- it('diplays the reported changes', () => {
- expect(wrapper.text()).toContain(
- `Reported Resource Changes: ${validPlanWithName.create} to add, ${validPlanWithName.update} to change, ${validPlanWithName.delete} to delete`,
- );
- });
-
- it('renders button when url is found', () => {
- expect(findLogButton().exists()).toBe(true);
- expect(findLogButton().text()).toEqual('View full log');
- });
- });
-
- describe('valid plan without job_name', () => {
- beforeEach(() => {
- mountWrapper({ plan: validPlanWithoutName });
- });
-
- it('diplays the header text without a name', () => {
- expect(wrapper.text()).toContain('A report was generated in your pipelines.');
- });
- });
-
- describe('invalid plan with job_name', () => {
- beforeEach(() => {
- mountWrapper({ plan: invalidPlanWithName });
- });
-
- it('displays a warning icon', () => {
- expect(findIcon().attributes('name')).toBe('warning');
- });
-
- it('diplays the header text with a name', () => {
- expect(wrapper.text()).toContain(
- `The job ${invalidPlanWithName.job_name} failed to generate a report.`,
- );
- });
-
- it('diplays generic error since report values are missing', () => {
- expect(wrapper.text()).toContain('Generating the report caused an error.');
- });
- });
-
- describe('invalid plan with out job_name', () => {
- beforeEach(() => {
- mountWrapper({ plan: invalidPlanWithoutName });
- });
-
- it('diplays the header text without a name', () => {
- expect(wrapper.text()).toContain('A report failed to generate.');
- });
-
- it('does not render button because url is missing', () => {
- expect(findLogButton().exists()).toBe(false);
- });
- });
-});
diff --git a/spec/frontend/vue_merge_request_widget/components/widget/__snapshots__/dynamic_content_spec.js.snap b/spec/frontend/vue_merge_request_widget/components/widget/__snapshots__/dynamic_content_spec.js.snap
new file mode 100644
index 00000000000..08424077269
--- /dev/null
+++ b/spec/frontend/vue_merge_request_widget/components/widget/__snapshots__/dynamic_content_spec.js.snap
@@ -0,0 +1,35 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`~/vue_merge_request_widget/components/widget/dynamic_content.vue renders given data 1`] = `
+"<content-row-stub level=\\"2\\" statusiconname=\\"success\\" widgetname=\\"MyWidget\\" header=\\"This is a header,This is a subheader\\">
+ <div class=\\"gl-display-flex gl-flex-direction-column\\">
+ <div>
+ <p class=\\"gl-mb-0\\">Main text for the row</p>
+ <gl-link-stub href=\\"https://gitlab.com\\">Optional link to display after text</gl-link-stub>
+ <!---->
+ <gl-badge-stub size=\\"md\\" variant=\\"info\\">
+ Badge is optional. Text to be displayed inside badge
+ </gl-badge-stub>
+ <actions-stub widget=\\"MyWidget\\" tertiarybuttons=\\"\\" class=\\"gl-ml-auto gl-pl-3\\"></actions-stub>
+ <p class=\\"gl-m-0 gl-font-sm\\">Optional: Smaller sub-text to be displayed below the main text</p>
+ </div>
+ <ul class=\\"gl-m-0 gl-p-0 gl-list-style-none\\">
+ <li>
+ <content-row-stub level=\\"3\\" statusiconname=\\"\\" widgetname=\\"MyWidget\\" header=\\"Child row header\\" data-qa-selector=\\"child_content\\">
+ <div class=\\"gl-display-flex gl-flex-direction-column\\">
+ <div>
+ <p class=\\"gl-mb-0\\">This is recursive. It will be listed in level 3.</p>
+ <!---->
+ <!---->
+ <!---->
+ <actions-stub widget=\\"MyWidget\\" tertiarybuttons=\\"\\" class=\\"gl-ml-auto gl-pl-3\\"></actions-stub>
+ <!---->
+ </div>
+ <!---->
+ </div>
+ </content-row-stub>
+ </li>
+ </ul>
+ </div>
+</content-row-stub>"
+`;
diff --git a/spec/frontend/vue_merge_request_widget/components/widget/dynamic_content_spec.js b/spec/frontend/vue_merge_request_widget/components/widget/dynamic_content_spec.js
new file mode 100644
index 00000000000..b7753a58747
--- /dev/null
+++ b/spec/frontend/vue_merge_request_widget/components/widget/dynamic_content_spec.js
@@ -0,0 +1,52 @@
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { EXTENSION_ICONS } from '~/vue_merge_request_widget/constants';
+import DynamicContent from '~/vue_merge_request_widget/components/widget/dynamic_content.vue';
+
+describe('~/vue_merge_request_widget/components/widget/dynamic_content.vue', () => {
+ let wrapper;
+
+ const createComponent = ({ propsData } = {}) => {
+ wrapper = shallowMountExtended(DynamicContent, {
+ propsData: {
+ widgetName: 'MyWidget',
+ ...propsData,
+ },
+ stubs: {
+ DynamicContent,
+ },
+ });
+ };
+
+ it('renders given data', () => {
+ createComponent({
+ propsData: {
+ data: {
+ id: 'row-id',
+ header: ['This is a header', 'This is a subheader'],
+ text: 'Main text for the row',
+ subtext: 'Optional: Smaller sub-text to be displayed below the main text',
+ icon: {
+ name: EXTENSION_ICONS.success,
+ },
+ badge: {
+ text: 'Badge is optional. Text to be displayed inside badge',
+ variant: 'info',
+ },
+ link: {
+ text: 'Optional link to display after text',
+ href: 'https://gitlab.com',
+ },
+ children: [
+ {
+ id: 'row-id-2',
+ header: 'Child row header',
+ text: 'This is recursive. It will be listed in level 3.',
+ },
+ ],
+ },
+ },
+ });
+
+ expect(wrapper.html()).toMatchSnapshot();
+ });
+});
diff --git a/spec/frontend/vue_merge_request_widget/components/widget/widget_content_row_spec.js b/spec/frontend/vue_merge_request_widget/components/widget/widget_content_row_spec.js
new file mode 100644
index 00000000000..9eddd091ad0
--- /dev/null
+++ b/spec/frontend/vue_merge_request_widget/components/widget/widget_content_row_spec.js
@@ -0,0 +1,65 @@
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import WidgetContentRow from '~/vue_merge_request_widget/components/widget/widget_content_row.vue';
+import StatusIcon from '~/vue_merge_request_widget/components/widget/status_icon.vue';
+
+describe('~/vue_merge_request_widget/components/widget/widget_content_row.vue', () => {
+ let wrapper;
+
+ const findStatusIcon = () => wrapper.findComponent(StatusIcon);
+
+ const createComponent = ({ propsData, slots } = {}) => {
+ wrapper = shallowMountExtended(WidgetContentRow, {
+ propsData: {
+ widgetName: 'MyWidget',
+ level: 2,
+ ...propsData,
+ },
+ slots,
+ });
+ };
+
+ describe('body', () => {
+ it('renders the status icon when provided', () => {
+ createComponent({ propsData: { statusIconName: 'failed' } });
+ expect(findStatusIcon().exists()).toBe(true);
+ });
+
+ it('does not render the status icon when it is not provided', () => {
+ createComponent();
+ expect(findStatusIcon().exists()).toBe(false);
+ });
+
+ it('renders slots properly', () => {
+ createComponent({
+ propsData: {
+ statusIconName: 'success',
+ },
+ slots: {
+ header: '<span>this is a header</span>',
+ body: '<span>this is a body</span>',
+ },
+ });
+
+ expect(wrapper.findByText('this is a body').exists()).toBe(true);
+ expect(wrapper.findByText('this is a header').exists()).toBe(true);
+ });
+ });
+
+ describe('header', () => {
+ it('renders an array of header and subheader', () => {
+ createComponent({ propsData: { header: ['this is a header', 'this is a subheader'] } });
+ expect(wrapper.findByText('this is a header').exists()).toBe(true);
+ expect(wrapper.findByText('this is a subheader').exists()).toBe(true);
+ });
+
+ it('renders a string', () => {
+ createComponent({ propsData: { header: 'this is a header' } });
+ expect(wrapper.findByText('this is a header').exists()).toBe(true);
+ });
+
+ it('escapes html injection properly', () => {
+ createComponent({ propsData: { header: '<b role="header">this is a header</b>' } });
+ expect(wrapper.findByText('<b role="header">this is a header</b>').exists()).toBe(true);
+ });
+ });
+});
diff --git a/spec/frontend/vue_merge_request_widget/components/widget/widget_content_section_spec.js b/spec/frontend/vue_merge_request_widget/components/widget/widget_content_section_spec.js
deleted file mode 100644
index c2128d3ff33..00000000000
--- a/spec/frontend/vue_merge_request_widget/components/widget/widget_content_section_spec.js
+++ /dev/null
@@ -1,39 +0,0 @@
-import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import WidgetContentSection from '~/vue_merge_request_widget/components/widget/widget_content_section.vue';
-import StatusIcon from '~/vue_merge_request_widget/components/extensions/status_icon.vue';
-
-describe('~/vue_merge_request_widget/components/widget/widget_content_section.vue', () => {
- let wrapper;
-
- const findStatusIcon = () => wrapper.findComponent(StatusIcon);
-
- const createComponent = ({ propsData, slots } = {}) => {
- wrapper = shallowMountExtended(WidgetContentSection, {
- propsData: {
- widgetName: 'MyWidget',
- ...propsData,
- },
- slots,
- });
- };
-
- it('does not render the status icon when it is not provided', () => {
- createComponent();
- expect(findStatusIcon().exists()).toBe(false);
- });
-
- it('renders the status icon when provided', () => {
- createComponent({ propsData: { statusIconName: 'failed' } });
- expect(findStatusIcon().exists()).toBe(true);
- });
-
- it('renders the default slot', () => {
- createComponent({
- slots: {
- default: 'Hello world',
- },
- });
-
- expect(wrapper.findByText('Hello world').exists()).toBe(true);
- });
-});
diff --git a/spec/frontend/vue_merge_request_widget/components/widget/widget_spec.js b/spec/frontend/vue_merge_request_widget/components/widget/widget_spec.js
index b67b5703ad5..4826fecf98d 100644
--- a/spec/frontend/vue_merge_request_widget/components/widget/widget_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/widget/widget_spec.js
@@ -5,8 +5,9 @@ import waitForPromises from 'helpers/wait_for_promises';
import StatusIcon from '~/vue_merge_request_widget/components/extensions/status_icon.vue';
import ActionButtons from '~/vue_merge_request_widget/components/action_buttons.vue';
import Widget from '~/vue_merge_request_widget/components/widget/widget.vue';
+import WidgetContentRow from '~/vue_merge_request_widget/components/widget/widget_content_row.vue';
-describe('MR Widget', () => {
+describe('~/vue_merge_request_widget/components/widget/widget.vue', () => {
let wrapper;
const findStatusIcon = () => wrapper.findComponent(StatusIcon);
@@ -27,6 +28,10 @@ describe('MR Widget', () => {
...propsData,
},
slots,
+ stubs: {
+ StatusIcon,
+ ContentRow: WidgetContentRow,
+ },
});
};
diff --git a/spec/frontend/vue_merge_request_widget/deployment/deployment_action_button_spec.js b/spec/frontend/vue_merge_request_widget/deployment/deployment_action_button_spec.js
index 7e7438bcc0f..1bad5dacefa 100644
--- a/spec/frontend/vue_merge_request_widget/deployment/deployment_action_button_spec.js
+++ b/spec/frontend/vue_merge_request_widget/deployment/deployment_action_button_spec.js
@@ -41,7 +41,7 @@ describe('Deployment action button', () => {
});
it('renders prop icon correctly', () => {
- expect(wrapper.find(GlIcon).exists()).toBe(true);
+ expect(wrapper.findComponent(GlIcon).exists()).toBe(true);
});
});
@@ -59,7 +59,7 @@ describe('Deployment action button', () => {
});
it('renders slot and icon prop correctly', () => {
- expect(wrapper.find(GlIcon).exists()).toBe(true);
+ expect(wrapper.findComponent(GlIcon).exists()).toBe(true);
expect(wrapper.text()).toContain(actionButtonMocks[DEPLOYING].toString());
});
});
@@ -75,8 +75,8 @@ describe('Deployment action button', () => {
});
it('is disabled and shows the loading icon', () => {
- expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
- expect(wrapper.find(GlButton).props('disabled')).toBe(true);
+ expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
+ expect(wrapper.findComponent(GlButton).props('disabled')).toBe(true);
});
});
@@ -90,8 +90,8 @@ describe('Deployment action button', () => {
});
});
it('is disabled and does not show the loading icon', () => {
- expect(wrapper.find(GlLoadingIcon).exists()).toBe(false);
- expect(wrapper.find(GlButton).props('disabled')).toBe(true);
+ expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(false);
+ expect(wrapper.findComponent(GlButton).props('disabled')).toBe(true);
});
});
@@ -106,8 +106,8 @@ describe('Deployment action button', () => {
});
});
it('is disabled and does not show the loading icon', () => {
- expect(wrapper.find(GlLoadingIcon).exists()).toBe(false);
- expect(wrapper.find(GlButton).props('disabled')).toBe(true);
+ expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(false);
+ expect(wrapper.findComponent(GlButton).props('disabled')).toBe(true);
});
});
@@ -118,8 +118,8 @@ describe('Deployment action button', () => {
});
});
it('is not disabled nor does it show the loading icon', () => {
- expect(wrapper.find(GlLoadingIcon).exists()).toBe(false);
- expect(wrapper.find(GlButton).props('disabled')).toBe(false);
+ expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(false);
+ expect(wrapper.findComponent(GlButton).props('disabled')).toBe(false);
});
});
});
diff --git a/spec/frontend/vue_merge_request_widget/deployment/deployment_actions_spec.js b/spec/frontend/vue_merge_request_widget/deployment/deployment_actions_spec.js
index a8912405fa8..58dadb2c679 100644
--- a/spec/frontend/vue_merge_request_widget/deployment/deployment_actions_spec.js
+++ b/spec/frontend/vue_merge_request_widget/deployment/deployment_actions_spec.js
@@ -1,6 +1,6 @@
import { mount } from '@vue/test-utils';
import waitForPromises from 'helpers/wait_for_promises';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
import { visitUrl } from '~/lib/utils/url_utility';
import {
@@ -11,6 +11,7 @@ import {
REDEPLOYING,
STOPPING,
} from '~/vue_merge_request_widget/components/deployment/constants';
+import eventHub from '~/vue_merge_request_widget/event_hub';
import DeploymentActions from '~/vue_merge_request_widget/components/deployment/deployment_actions.vue';
import MRWidgetService from '~/vue_merge_request_widget/services/mr_widget_service';
import {
@@ -167,7 +168,7 @@ describe('DeploymentAction component', () => {
});
it('should not throw an error', () => {
- expect(createFlash).not.toHaveBeenCalled();
+ expect(createAlert).not.toHaveBeenCalled();
});
describe('response includes redirect_url', () => {
@@ -192,6 +193,7 @@ describe('DeploymentAction component', () => {
describe('it should call the executeAction method', () => {
beforeEach(async () => {
jest.spyOn(wrapper.vm, 'executeAction').mockImplementation();
+ jest.spyOn(eventHub, '$emit');
await waitForPromises();
@@ -206,11 +208,16 @@ describe('DeploymentAction component', () => {
actionButtonMocks[configConst],
);
});
+
+ it('emits the FetchDeployments event', () => {
+ expect(eventHub.$emit).toHaveBeenCalledWith('FetchDeployments');
+ });
});
describe('when executeInlineAction errors', () => {
beforeEach(async () => {
executeActionSpy.mockRejectedValueOnce();
+ jest.spyOn(eventHub, '$emit');
await waitForPromises();
@@ -218,12 +225,15 @@ describe('DeploymentAction component', () => {
finderFn().trigger('click');
});
- it('should call createFlash with error message', () => {
- expect(createFlash).toHaveBeenCalled();
- expect(createFlash).toHaveBeenCalledWith({
+ it('should call createAlert with error message', () => {
+ expect(createAlert).toHaveBeenCalledWith({
message: actionButtonMocks[configConst].errorMessage,
});
});
+
+ it('emits the FetchDeployments event', () => {
+ expect(eventHub.$emit).toHaveBeenCalledWith('FetchDeployments');
+ });
});
});
});
diff --git a/spec/frontend/vue_merge_request_widget/deployment/deployment_spec.js b/spec/frontend/vue_merge_request_widget/deployment/deployment_spec.js
index c27cbd8b781..f310f7669a9 100644
--- a/spec/frontend/vue_merge_request_widget/deployment/deployment_spec.js
+++ b/spec/frontend/vue_merge_request_widget/deployment/deployment_spec.js
@@ -37,7 +37,7 @@ describe('Deployment component', () => {
});
it('always renders DeploymentInfo', () => {
- expect(wrapper.find(DeploymentInfo).exists()).toBe(true);
+ expect(wrapper.findComponent(DeploymentInfo).exists()).toBe(true);
});
describe('status message and buttons', () => {
@@ -111,7 +111,7 @@ describe('Deployment component', () => {
});
it(`renders the text: ${text}`, () => {
- expect(wrapper.find(DeploymentInfo).text()).toContain(text);
+ expect(wrapper.findComponent(DeploymentInfo).text()).toContain(text);
});
if (actionButtons.length > 0) {
@@ -137,9 +137,11 @@ describe('Deployment component', () => {
if (actionButtons.includes(DeploymentViewButton)) {
it('renders the View button with expected text', () => {
if (status === SUCCESS) {
- expect(wrapper.find(DeploymentViewButton).text()).toContain('View app');
+ expect(wrapper.findComponent(DeploymentViewButton).text()).toContain('View app');
} else {
- expect(wrapper.find(DeploymentViewButton).text()).toContain('View latest app');
+ expect(wrapper.findComponent(DeploymentViewButton).text()).toContain(
+ 'View latest app',
+ );
}
});
}
@@ -150,7 +152,7 @@ describe('Deployment component', () => {
describe('hasExternalUrls', () => {
describe('when deployment has both external_url_formatted and external_url', () => {
it('should render the View Button', () => {
- expect(wrapper.find(DeploymentViewButton).exists()).toBe(true);
+ expect(wrapper.findComponent(DeploymentViewButton).exists()).toBe(true);
});
});
@@ -165,7 +167,7 @@ describe('Deployment component', () => {
});
it('should not render the View Button', () => {
- expect(wrapper.find(DeploymentViewButton).exists()).toBe(false);
+ expect(wrapper.findComponent(DeploymentViewButton).exists()).toBe(false);
});
});
@@ -180,7 +182,7 @@ describe('Deployment component', () => {
});
it('should not render the View Button', () => {
- expect(wrapper.find(DeploymentViewButton).exists()).toBe(false);
+ expect(wrapper.findComponent(DeploymentViewButton).exists()).toBe(false);
});
});
});
diff --git a/spec/frontend/vue_merge_request_widget/deployment/deployment_view_button_spec.js b/spec/frontend/vue_merge_request_widget/deployment/deployment_view_button_spec.js
index eb6e3711e2e..8994fa522d0 100644
--- a/spec/frontend/vue_merge_request_widget/deployment/deployment_view_button_spec.js
+++ b/spec/frontend/vue_merge_request_widget/deployment/deployment_view_button_spec.js
@@ -2,6 +2,7 @@ import { GlDropdown, GlLink } from '@gitlab/ui';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import DeploymentViewButton from '~/vue_merge_request_widget/components/deployment/deployment_view_button.vue';
import ReviewAppLink from '~/vue_merge_request_widget/components/review_app_link.vue';
+import ModalCopyButton from '~/vue_shared/components/modal_copy_button.vue';
import { deploymentMockData } from './deployment_mock_data';
const appButtonText = {
@@ -36,6 +37,7 @@ describe('Deployment View App button', () => {
const findMrWigdetDeploymentDropdownIcon = () =>
wrapper.findByTestId('mr-wigdet-deployment-dropdown-icon');
const findDeployUrlMenuItems = () => wrapper.findAllComponents(GlLink);
+ const findCopyButton = () => wrapper.findComponent(ModalCopyButton);
describe('text', () => {
it('renders text as passed', () => {
@@ -44,39 +46,93 @@ describe('Deployment View App button', () => {
});
describe('without changes', () => {
+ let deployment;
+
beforeEach(() => {
- createComponent({
- propsData: {
- deployment: { ...deploymentMockData, changes: null },
- appButtonText,
- },
+ deployment = { ...deploymentMockData, changes: null };
+ });
+
+ describe('with safe url', () => {
+ beforeEach(() => {
+ createComponent({
+ propsData: {
+ deployment,
+ appButtonText,
+ },
+ });
+ });
+
+ it('renders the link to the review app without dropdown', () => {
+ expect(findMrWigdetDeploymentDropdown().exists()).toBe(false);
+ expect(findReviewAppLink().attributes('href')).toBe(deployment.external_url);
});
});
- it('renders the link to the review app without dropdown', () => {
- expect(findMrWigdetDeploymentDropdown().exists()).toBe(false);
+ describe('without safe URL', () => {
+ beforeEach(() => {
+ deployment = { ...deployment, external_url: 'postgres://example' };
+ createComponent({
+ propsData: {
+ deployment,
+ appButtonText,
+ },
+ });
+ });
+
+ it('renders the link as a copy button', () => {
+ expect(findMrWigdetDeploymentDropdown().exists()).toBe(false);
+ expect(findCopyButton().props('text')).toBe(deployment.external_url);
+ });
});
});
describe('with a single change', () => {
+ let deployment;
+ let change;
+
beforeEach(() => {
- createComponent({
- propsData: {
- deployment: { ...deploymentMockData, changes: [deploymentMockData.changes[0]] },
- appButtonText,
- },
- });
+ [change] = deploymentMockData.changes;
+ deployment = { ...deploymentMockData, changes: [change] };
});
- it('renders the link to the review app without dropdown', () => {
- expect(findMrWigdetDeploymentDropdown().exists()).toBe(false);
- expect(findMrWigdetDeploymentDropdownIcon().exists()).toBe(false);
+ describe('with safe URL', () => {
+ beforeEach(() => {
+ createComponent({
+ propsData: {
+ deployment,
+ appButtonText,
+ },
+ });
+ });
+
+ it('renders the link to the review app without dropdown', () => {
+ expect(findMrWigdetDeploymentDropdown().exists()).toBe(false);
+ expect(findMrWigdetDeploymentDropdownIcon().exists()).toBe(false);
+ });
+
+ it('renders the link to the review app linked to to the first change', () => {
+ const expectedUrl = deploymentMockData.changes[0].external_url;
+
+ expect(findReviewAppLink().attributes('href')).toBe(expectedUrl);
+ });
});
- it('renders the link to the review app linked to to the first change', () => {
- const expectedUrl = deploymentMockData.changes[0].external_url;
+ describe('with unsafe URL', () => {
+ beforeEach(() => {
+ change = { ...change, external_url: 'postgres://example' };
+ deployment = { ...deployment, changes: [change] };
+ createComponent({
+ propsData: {
+ deployment,
+ appButtonText,
+ },
+ });
+ });
- expect(findReviewAppLink().attributes('href')).toBe(expectedUrl);
+ it('renders the link as a copy button', () => {
+ expect(findMrWigdetDeploymentDropdown().exists()).toBe(false);
+ expect(findCopyButton().props('text')).toBe(change.external_url);
+ });
});
});
diff --git a/spec/frontend/vue_merge_request_widget/extensions/test_report/index_spec.js b/spec/frontend/vue_merge_request_widget/extensions/test_report/index_spec.js
index 82743275739..05df66165dd 100644
--- a/spec/frontend/vue_merge_request_widget/extensions/test_report/index_spec.js
+++ b/spec/frontend/vue_merge_request_widget/extensions/test_report/index_spec.js
@@ -42,7 +42,7 @@ describe('Test report extension', () => {
const findFullReportLink = () => wrapper.findByTestId('full-report-link');
const findCopyFailedSpecsBtn = () => wrapper.findByTestId('copy-failed-specs-btn');
const findAllExtensionListItems = () => wrapper.findAllByTestId('extension-list-item');
- const findModal = () => wrapper.find(TestCaseDetails);
+ const findModal = () => wrapper.findComponent(TestCaseDetails);
const createComponent = () => {
wrapper = mountExtended(extensionsContainer, {
diff --git a/spec/frontend/vue_merge_request_widget/mr_widget_how_to_merge_modal_spec.js b/spec/frontend/vue_merge_request_widget/mr_widget_how_to_merge_modal_spec.js
index 295b9df30b9..d038660e6d3 100644
--- a/spec/frontend/vue_merge_request_widget/mr_widget_how_to_merge_modal_spec.js
+++ b/spec/frontend/vue_merge_request_widget/mr_widget_how_to_merge_modal_spec.js
@@ -24,7 +24,7 @@ describe('MRWidgetHowToMerge', () => {
mountComponent();
});
- const findModal = () => wrapper.find(GlModal);
+ const findModal = () => wrapper.findComponent(GlModal);
const findInstructionsFields = () =>
wrapper.findAll('[ data-testid="how-to-merge-instructions"]');
const findTipLink = () => wrapper.find("[data-testid='docs-tip']");
diff --git a/spec/frontend/vue_merge_request_widget/mr_widget_options_spec.js b/spec/frontend/vue_merge_request_widget/mr_widget_options_spec.js
index cc894f94f80..6622749da92 100644
--- a/spec/frontend/vue_merge_request_widget/mr_widget_options_spec.js
+++ b/spec/frontend/vue_merge_request_widget/mr_widget_options_spec.js
@@ -368,12 +368,13 @@ describe('MrWidgetOptions', () => {
describe('bindEventHubListeners', () => {
it.each`
- event | method | methodArgs
- ${'MRWidgetUpdateRequested'} | ${'checkStatus'} | ${(x) => [x]}
- ${'MRWidgetRebaseSuccess'} | ${'checkStatus'} | ${(x) => [x, true]}
- ${'FetchActionsContent'} | ${'fetchActionsContent'} | ${() => []}
- ${'EnablePolling'} | ${'resumePolling'} | ${() => []}
- ${'DisablePolling'} | ${'stopPolling'} | ${() => []}
+ event | method | methodArgs
+ ${'MRWidgetUpdateRequested'} | ${'checkStatus'} | ${(x) => [x]}
+ ${'MRWidgetRebaseSuccess'} | ${'checkStatus'} | ${(x) => [x, true]}
+ ${'FetchActionsContent'} | ${'fetchActionsContent'} | ${() => []}
+ ${'EnablePolling'} | ${'resumePolling'} | ${() => []}
+ ${'DisablePolling'} | ${'stopPolling'} | ${() => []}
+ ${'FetchDeployments'} | ${'fetchPreMergeDeployments'} | ${() => []}
`('should bind to $event', ({ event, method, methodArgs }) => {
jest.spyOn(wrapper.vm, method).mockImplementation();
@@ -771,34 +772,40 @@ describe('MrWidgetOptions', () => {
});
describe('security widget', () => {
- describe.each`
- context | hasPipeline | shouldRender
- ${'there is a pipeline'} | ${true} | ${true}
- ${'no pipeline'} | ${false} | ${false}
- `('given $context', ({ hasPipeline, shouldRender }) => {
- beforeEach(() => {
- const mrData = {
- ...mockData,
- ...(hasPipeline ? {} : { pipeline: null }),
- };
+ const setup = async (hasPipeline) => {
+ const mrData = {
+ ...mockData,
+ ...(hasPipeline ? {} : { pipeline: null }),
+ };
- // Override top-level mocked requests, which always use a fresh copy of
- // mockData, which always includes the full pipeline object.
- mock.onGet(mockData.merge_request_widget_path).reply(() => [200, mrData]);
- mock.onGet(mockData.merge_request_cached_widget_path).reply(() => [200, mrData]);
-
- return createComponent(mrData, {
- apolloProvider: createMockApollo([
- [
- securityReportMergeRequestDownloadPathsQuery,
- async () => ({ data: securityReportMergeRequestDownloadPathsQueryResponse }),
- ],
- ]),
- });
+ // Override top-level mocked requests, which always use a fresh copy of
+ // mockData, which always includes the full pipeline object.
+ mock.onGet(mockData.merge_request_widget_path).reply(() => [200, mrData]);
+ mock.onGet(mockData.merge_request_cached_widget_path).reply(() => [200, mrData]);
+
+ return createComponent(mrData, {
+ apolloProvider: createMockApollo([
+ [
+ securityReportMergeRequestDownloadPathsQuery,
+ async () => ({ data: securityReportMergeRequestDownloadPathsQueryResponse }),
+ ],
+ ]),
});
+ };
+
+ describe('with a pipeline', () => {
+ it('renders the security widget', async () => {
+ await setup(true);
+
+ expect(findSecurityMrWidget().exists()).toBe(true);
+ });
+ });
+
+ describe('with no pipeline', () => {
+ it('does not render the security widget', async () => {
+ await setup(false);
- it(shouldRender ? 'renders' : 'does not render', () => {
- expect(findSecurityMrWidget().exists()).toBe(shouldRender);
+ expect(findSecurityMrWidget().exists()).toBe(false);
});
});
});
@@ -881,7 +888,10 @@ describe('MrWidgetOptions', () => {
await nextTick();
expect(
- wrapper.find('[data-testid="widget-extension-top-level"]').find(GlDropdown).exists(),
+ wrapper
+ .find('[data-testid="widget-extension-top-level"]')
+ .findComponent(GlDropdown)
+ .exists(),
).toBe(false);
await nextTick();
@@ -891,19 +901,19 @@ describe('MrWidgetOptions', () => {
expect(collapsedSection.text()).toContain('Hello world');
// Renders icon in the row
- expect(collapsedSection.find(GlIcon).exists()).toBe(true);
- expect(collapsedSection.find(GlIcon).props('name')).toBe('status-failed');
+ expect(collapsedSection.findComponent(GlIcon).exists()).toBe(true);
+ expect(collapsedSection.findComponent(GlIcon).props('name')).toBe('status-failed');
// Renders badge in the row
- expect(collapsedSection.find(GlBadge).exists()).toBe(true);
- expect(collapsedSection.find(GlBadge).text()).toBe('Closed');
+ expect(collapsedSection.findComponent(GlBadge).exists()).toBe(true);
+ expect(collapsedSection.findComponent(GlBadge).text()).toBe('Closed');
// Renders a link in the row
- expect(collapsedSection.find(GlLink).exists()).toBe(true);
- expect(collapsedSection.find(GlLink).text()).toBe('GitLab.com');
+ expect(collapsedSection.findComponent(GlLink).exists()).toBe(true);
+ expect(collapsedSection.findComponent(GlLink).text()).toBe('GitLab.com');
- expect(collapsedSection.find(GlButton).exists()).toBe(true);
- expect(collapsedSection.find(GlButton).text()).toBe('Full report');
+ expect(collapsedSection.findComponent(GlButton).exists()).toBe(true);
+ expect(collapsedSection.findComponent(GlButton).text()).toBe('Full report');
});
it('extension polling is not called if enablePolling flag is not passed', () => {
@@ -994,7 +1004,7 @@ describe('MrWidgetOptions', () => {
await createComponent();
- expect(pollRequest).toHaveBeenCalledTimes(4);
+ expect(pollRequest).toHaveBeenCalledTimes(2);
});
});
@@ -1032,7 +1042,7 @@ describe('MrWidgetOptions', () => {
registerExtension(pollingErrorExtension);
await createComponent();
- expect(pollRequest).toHaveBeenCalledTimes(4);
+ expect(pollRequest).toHaveBeenCalledTimes(2);
});
it('captures sentry error and displays error when poll has failed', async () => {
@@ -1134,7 +1144,7 @@ describe('MrWidgetOptions', () => {
${'WidgetCodeQuality'} | ${'i_testing_code_quality_widget_total'}
${'WidgetTerraform'} | ${'i_testing_terraform_widget_total'}
${'WidgetIssues'} | ${'i_testing_issues_widget_total'}
- ${'WidgetTestReport'} | ${'i_testing_summary_widget_total'}
+ ${'WidgetTestSummary'} | ${'i_testing_summary_widget_total'}
`(
"sends non-standard events for the '$widgetName' widget",
async ({ widgetName, nonStandardEvent }) => {
diff --git a/spec/frontend/vue_merge_request_widget/stores/get_state_key_spec.js b/spec/frontend/vue_merge_request_widget/stores/get_state_key_spec.js
index 0246a8d4b0f..88d9d0b4cff 100644
--- a/spec/frontend/vue_merge_request_widget/stores/get_state_key_spec.js
+++ b/spec/frontend/vue_merge_request_widget/stores/get_state_key_spec.js
@@ -16,12 +16,13 @@ describe('getStateKey', () => {
commitsCount: 2,
hasConflicts: false,
draft: false,
+ detailedMergeStatus: null,
};
const bound = getStateKey.bind(context);
expect(bound()).toEqual(null);
- context.canBeMerged = true;
+ context.detailedMergeStatus = 'MERGEABLE';
expect(bound()).toEqual('readyToMerge');
@@ -36,21 +37,15 @@ describe('getStateKey', () => {
expect(bound()).toEqual('shaMismatch');
context.canMerge = false;
- context.isPipelineBlocked = true;
-
- expect(bound()).toEqual('pipelineBlocked');
-
- context.hasMergeableDiscussionsState = true;
- context.autoMergeEnabled = false;
+ context.detailedMergeStatus = 'DISCUSSIONS_NOT_RESOLVED';
expect(bound()).toEqual('unresolvedDiscussions');
- context.draft = true;
+ context.detailedMergeStatus = 'DRAFT_STATUS';
expect(bound()).toEqual('draft');
- context.onlyAllowMergeIfPipelineSucceeds = true;
- context.isPipelineFailed = true;
+ context.detailedMergeStatus = 'CI_MUST_PASS';
expect(bound()).toEqual('pipelineFailed');
@@ -62,7 +57,7 @@ describe('getStateKey', () => {
expect(bound()).toEqual('conflicts');
- context.mergeStatus = 'unchecked';
+ context.detailedMergeStatus = 'CHECKING';
expect(bound()).toEqual('checking');
diff --git a/spec/frontend/vue_shared/components/ci_badge_link_spec.js b/spec/frontend/vue_shared/components/ci_badge_link_spec.js
index 27b6718fb8e..07cbfe1e79b 100644
--- a/spec/frontend/vue_shared/components/ci_badge_link_spec.js
+++ b/spec/frontend/vue_shared/components/ci_badge_link_spec.js
@@ -1,7 +1,7 @@
+import { GlLink } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import CiBadge from '~/vue_shared/components/ci_badge_link.vue';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
-import { visitUrl } from '~/lib/utils/url_utility';
jest.mock('~/lib/utils/url_utility', () => ({
visitUrl: jest.fn(),
@@ -86,18 +86,14 @@ describe('CI Badge Link Component', () => {
wrapper.destroy();
});
- it.each(Object.keys(statuses))('should render badge for status: %s', async (status) => {
+ it.each(Object.keys(statuses))('should render badge for status: %s', (status) => {
createComponent({ status: statuses[status] });
- expect(wrapper.attributes('href')).toBe();
+ expect(wrapper.attributes('href')).toBe(statuses[status].details_path);
expect(wrapper.text()).toBe(statuses[status].text);
expect(wrapper.classes()).toContain('ci-status');
expect(wrapper.classes()).toContain(`ci-${statuses[status].group}`);
expect(findIcon().exists()).toBe(true);
-
- await wrapper.trigger('click');
-
- expect(visitUrl).toHaveBeenCalledWith(statuses[status].details_path);
});
it('should not render label', () => {
@@ -109,7 +105,7 @@ describe('CI Badge Link Component', () => {
it('should emit ciStatusBadgeClick event', async () => {
createComponent({ status: statuses.success });
- await wrapper.trigger('click');
+ await wrapper.findComponent(GlLink).vm.$emit('click');
expect(wrapper.emitted('ciStatusBadgeClick')).toEqual([[]]);
});
diff --git a/spec/frontend/vue_shared/components/color_select_dropdown/color_select_root_spec.js b/spec/frontend/vue_shared/components/color_select_dropdown/color_select_root_spec.js
index 441e21ee905..5b0772f6e34 100644
--- a/spec/frontend/vue_shared/components/color_select_dropdown/color_select_root_spec.js
+++ b/spec/frontend/vue_shared/components/color_select_dropdown/color_select_root_spec.js
@@ -3,7 +3,7 @@ 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 { createAlert } from '~/flash';
import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue';
import DropdownContents from '~/vue_shared/components/color_select_dropdown/dropdown_contents.vue';
import DropdownValue from '~/vue_shared/components/color_select_dropdown/dropdown_value.vue';
@@ -146,7 +146,7 @@ describe('LabelsSelectRoot', () => {
});
it('creates flash with error message', () => {
- expect(createFlash).toHaveBeenCalledWith({
+ expect(createAlert).toHaveBeenCalledWith({
captureError: true,
message: 'Error fetching epic color.',
});
@@ -186,7 +186,7 @@ describe('LabelsSelectRoot', () => {
findDropdownContents().vm.$emit('setColor', color);
await waitForPromises();
- expect(createFlash).toHaveBeenCalledWith({
+ expect(createAlert).toHaveBeenCalledWith({
captureError: true,
error: expect.anything(),
message: 'An error occurred while updating color.',
diff --git a/spec/frontend/vue_shared/components/date_time_picker/date_time_picker_lib_spec.js b/spec/frontend/vue_shared/components/date_time_picker/date_time_picker_lib_spec.js
index 10eacff630d..7a8f94b3746 100644
--- a/spec/frontend/vue_shared/components/date_time_picker/date_time_picker_lib_spec.js
+++ b/spec/frontend/vue_shared/components/date_time_picker/date_time_picker_lib_spec.js
@@ -121,7 +121,7 @@ describe('date time picker lib', () => {
const utcResult = '2019-09-08T01:01:01Z';
const localResult = '2019-09-08T08:01:01Z';
- test.each`
+ it.each`
val | locatTimezone | utc | result
${value} | ${'UTC'} | ${undefined} | ${utcResult}
${value} | ${'UTC'} | ${false} | ${utcResult}
@@ -167,7 +167,7 @@ describe('date time picker lib', () => {
const utcResult = '2019-09-08 08:01:01';
const localResult = '2019-09-08 01:01:01';
- test.each`
+ it.each`
val | locatTimezone | utc | result
${value} | ${'UTC'} | ${undefined} | ${utcResult}
${value} | ${'UTC'} | ${false} | ${utcResult}
diff --git a/spec/frontend/vue_shared/components/diff_stats_dropdown_spec.js b/spec/frontend/vue_shared/components/diff_stats_dropdown_spec.js
index 68684004b82..99c973bdd26 100644
--- a/spec/frontend/vue_shared/components/diff_stats_dropdown_spec.js
+++ b/spec/frontend/vue_shared/components/diff_stats_dropdown_spec.js
@@ -106,11 +106,11 @@ describe('Diff Stats Dropdown', () => {
expectedAddedDeletedExpanded,
expectedAddedDeletedCollapsed,
}) => {
- beforeAll(() => {
+ beforeEach(() => {
createComponent({ changed, added, deleted });
});
- afterAll(() => {
+ afterEach(() => {
wrapper.destroy();
});
diff --git a/spec/frontend/vue_shared/components/diff_viewer/diff_viewer_spec.js b/spec/frontend/vue_shared/components/diff_viewer/diff_viewer_spec.js
index 69964b2687d..6e0717c29d7 100644
--- a/spec/frontend/vue_shared/components/diff_viewer/diff_viewer_spec.js
+++ b/spec/frontend/vue_shared/components/diff_viewer/diff_viewer_spec.js
@@ -1,8 +1,6 @@
-import Vue, { nextTick } from 'vue';
-
-import mountComponent from 'helpers/vue_mount_component_helper';
+import { mount } from '@vue/test-utils';
import { GREEN_BOX_IMAGE_URL, RED_BOX_IMAGE_URL } from 'spec/test_constants';
-import diffViewer from '~/vue_shared/components/diff_viewer/diff_viewer.vue';
+import DiffViewer from '~/vue_shared/components/diff_viewer/diff_viewer.vue';
describe('DiffViewer', () => {
const requiredProps = {
@@ -14,37 +12,28 @@ describe('DiffViewer', () => {
oldPath: RED_BOX_IMAGE_URL,
oldSha: 'DEF',
};
- let vm;
-
- function createComponent(props) {
- const DiffViewer = Vue.extend(diffViewer);
+ let wrapper;
- vm = mountComponent(DiffViewer, props);
+ function createComponent(propsData) {
+ wrapper = mount(DiffViewer, { propsData });
}
afterEach(() => {
- vm.$destroy();
+ wrapper.destroy();
});
- it('renders image diff', async () => {
+ it('renders image diff', () => {
window.gon = {
relative_url_root: '',
};
createComponent({ ...requiredProps, projectPath: '' });
- await nextTick();
-
- expect(vm.$el.querySelector('.deleted img').getAttribute('src')).toBe(
- `//-/raw/DEF/${RED_BOX_IMAGE_URL}`,
- );
-
- expect(vm.$el.querySelector('.added img').getAttribute('src')).toBe(
- `//-/raw/ABC/${GREEN_BOX_IMAGE_URL}`,
- );
+ expect(wrapper.find('.deleted img').attributes('src')).toBe(`//-/raw/DEF/${RED_BOX_IMAGE_URL}`);
+ expect(wrapper.find('.added img').attributes('src')).toBe(`//-/raw/ABC/${GREEN_BOX_IMAGE_URL}`);
});
- it('renders fallback download diff display', async () => {
+ it('renders fallback download diff display', () => {
createComponent({
...requiredProps,
diffViewerMode: 'added',
@@ -52,18 +41,10 @@ describe('DiffViewer', () => {
oldPath: 'testold.abc',
});
- await nextTick();
-
- expect(vm.$el.querySelector('.deleted .file-info').textContent.trim()).toContain('testold.abc');
-
- expect(vm.$el.querySelector('.deleted .btn.btn-default').textContent.trim()).toContain(
- 'Download',
- );
-
- expect(vm.$el.querySelector('.added .file-info').textContent.trim()).toContain('test.abc');
- expect(vm.$el.querySelector('.added .btn.btn-default').textContent.trim()).toContain(
- 'Download',
- );
+ expect(wrapper.find('.deleted .file-info').text()).toContain('testold.abc');
+ expect(wrapper.find('.deleted .btn.btn-default').text()).toContain('Download');
+ expect(wrapper.find('.added .file-info').text()).toContain('test.abc');
+ expect(wrapper.find('.added .btn.btn-default').text()).toContain('Download');
});
describe('renamed file', () => {
@@ -85,7 +66,7 @@ describe('DiffViewer', () => {
oldPath: 'testold.abc',
});
- expect(vm.$el.textContent).toContain('File renamed with no changes.');
+ expect(wrapper.text()).toContain('File renamed with no changes.');
});
});
@@ -99,6 +80,6 @@ describe('DiffViewer', () => {
bMode: '321',
});
- expect(vm.$el.textContent).toContain('File mode changed from 123 to 321');
+ expect(wrapper.text()).toContain('File mode changed from 123 to 321');
});
});
diff --git a/spec/frontend/vue_shared/components/file_finder/item_spec.js b/spec/frontend/vue_shared/components/file_finder/item_spec.js
index b69c33055c1..f0998b1b5c6 100644
--- a/spec/frontend/vue_shared/components/file_finder/item_spec.js
+++ b/spec/frontend/vue_shared/components/file_finder/item_spec.js
@@ -1,127 +1,119 @@
-import Vue, { nextTick } from 'vue';
-import createComponent from 'helpers/vue_mount_component_helper';
+import { mount } from '@vue/test-utils';
import { file } from 'jest/ide/helpers';
import ItemComponent from '~/vue_shared/components/file_finder/item.vue';
describe('File finder item spec', () => {
- const Component = Vue.extend(ItemComponent);
- let vm;
- let localFile;
-
- beforeEach(() => {
- localFile = {
- ...file(),
- name: 'test file',
- path: 'test/file',
- };
-
- vm = createComponent(Component, {
- file: localFile,
- focused: true,
- searchText: '',
- index: 0,
+ let wrapper;
+
+ const createComponent = ({ file: customFileFields = {}, ...otherProps } = {}) => {
+ wrapper = mount(ItemComponent, {
+ propsData: {
+ file: {
+ ...file(),
+ name: 'test file',
+ path: 'test/file',
+ ...customFileFields,
+ },
+ focused: true,
+ searchText: '',
+ index: 0,
+ ...otherProps,
+ },
});
- });
+ };
afterEach(() => {
- vm.$destroy();
+ wrapper.destroy();
});
it('renders file name & path', () => {
- expect(vm.$el.textContent).toContain('test file');
- expect(vm.$el.textContent).toContain('test/file');
+ createComponent();
+
+ expect(wrapper.text()).toContain('test file');
+ expect(wrapper.text()).toContain('test/file');
});
describe('focused', () => {
it('adds is-focused class', () => {
- expect(vm.$el.classList).toContain('is-focused');
+ createComponent();
+
+ expect(wrapper.classes()).toContain('is-focused');
});
it('does not have is-focused class when not focused', async () => {
- vm.focused = false;
+ createComponent({ focused: false });
- await nextTick();
- expect(vm.$el.classList).not.toContain('is-focused');
+ expect(wrapper.classes()).not.toContain('is-focused');
});
});
describe('changed file icon', () => {
it('does not render when not a changed or temp file', () => {
- expect(vm.$el.querySelector('.diff-changed-stats')).toBe(null);
+ createComponent();
+
+ expect(wrapper.find('.diff-changed-stats').exists()).toBe(false);
});
it('renders when a changed file', async () => {
- vm.file.changed = true;
+ createComponent({ file: { changed: true } });
- await nextTick();
- expect(vm.$el.querySelector('.diff-changed-stats')).not.toBe(null);
+ expect(wrapper.find('.diff-changed-stats').exists()).toBe(true);
});
it('renders when a temp file', async () => {
- vm.file.tempFile = true;
+ createComponent({ file: { tempFile: true } });
- await nextTick();
- expect(vm.$el.querySelector('.diff-changed-stats')).not.toBe(null);
+ expect(wrapper.find('.diff-changed-stats').exists()).toBe(true);
});
});
- it('emits event when clicked', () => {
- jest.spyOn(vm, '$emit').mockImplementation(() => {});
+ it('emits event when clicked', async () => {
+ createComponent();
- vm.$el.click();
+ await wrapper.find('*').trigger('click');
- expect(vm.$emit).toHaveBeenCalledWith('click', vm.file);
+ expect(wrapper.emitted('click')[0]).toStrictEqual([wrapper.props('file')]);
});
describe('path', () => {
- let el;
-
- beforeEach(async () => {
- vm.searchText = 'file';
-
- el = vm.$el.querySelector('.diff-changed-file-path');
-
- nextTick();
- });
+ const findChangedFilePath = () => wrapper.find('.diff-changed-file-path');
it('highlights text', () => {
- expect(el.querySelectorAll('.highlighted').length).toBe(4);
+ createComponent({ searchText: 'file' });
+
+ expect(findChangedFilePath().findAll('.highlighted')).toHaveLength(4);
});
it('adds ellipsis to long text', async () => {
- vm.file.path = new Array(70)
+ const path = new Array(70)
.fill()
.map((_, i) => `${i}-`)
.join('');
- await nextTick();
- expect(el.textContent).toBe(`...${vm.file.path.substr(vm.file.path.length - 60)}`);
+ createComponent({ searchText: 'file', file: { path } });
+
+ expect(findChangedFilePath().text()).toBe(`...${path.substring(path.length - 60)}`);
});
});
describe('name', () => {
- let el;
-
- beforeEach(async () => {
- vm.searchText = 'file';
-
- el = vm.$el.querySelector('.diff-changed-file-name');
-
- await nextTick();
- });
+ const findChangedFileName = () => wrapper.find('.diff-changed-file-name');
it('highlights text', () => {
- expect(el.querySelectorAll('.highlighted').length).toBe(4);
+ createComponent({ searchText: 'file' });
+
+ expect(findChangedFileName().findAll('.highlighted')).toHaveLength(4);
});
it('does not add ellipsis to long text', async () => {
- vm.file.name = new Array(70)
+ const name = new Array(70)
.fill()
.map((_, i) => `${i}-`)
.join('');
- await nextTick();
- expect(el.textContent).not.toBe(`...${vm.file.name.substr(vm.file.name.length - 60)}`);
+ createComponent({ searchText: 'file', file: { name } });
+
+ expect(findChangedFileName().text()).not.toBe(`...${name.substring(name.length - 60)}`);
});
});
});
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/store/modules/filters/actions_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/store/modules/filters/actions_spec.js
index 4140ec09b4e..66ef473f368 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/store/modules/filters/actions_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/store/modules/filters/actions_spec.js
@@ -3,7 +3,7 @@ import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
import { mockBranches } from 'jest/vue_shared/components/filtered_search_bar/mock_data';
import Api from '~/api';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import httpStatusCodes from '~/lib/utils/http_status';
import * as actions from '~/vue_shared/components/filtered_search_bar/store/modules/filters/actions';
import * as types from '~/vue_shared/components/filtered_search_bar/store/modules/filters/mutation_types';
@@ -159,7 +159,7 @@ describe('Filters actions', () => {
},
],
[],
- ).then(() => expect(createFlash).toHaveBeenCalled());
+ ).then(() => expect(createAlert).toHaveBeenCalled());
});
});
});
@@ -233,7 +233,7 @@ describe('Filters actions', () => {
[],
).then(() => {
expect(mock.history.get[0].url).toBe('/api/v1/groups/fake_group_endpoint/members');
- expect(createFlash).toHaveBeenCalled();
+ expect(createAlert).toHaveBeenCalled();
});
});
@@ -252,7 +252,7 @@ describe('Filters actions', () => {
[],
).then(() => {
expect(mock.history.get[0].url).toBe('/api/v1/projects/fake_project_endpoint/users');
- expect(createFlash).toHaveBeenCalled();
+ expect(createAlert).toHaveBeenCalled();
});
});
});
@@ -298,7 +298,7 @@ describe('Filters actions', () => {
},
],
[],
- ).then(() => expect(createFlash).toHaveBeenCalled());
+ ).then(() => expect(createAlert).toHaveBeenCalled());
});
});
});
@@ -376,7 +376,7 @@ describe('Filters actions', () => {
[],
).then(() => {
expect(mock.history.get[0].url).toBe('/api/v1/groups/fake_group_endpoint/members');
- expect(createFlash).toHaveBeenCalled();
+ expect(createAlert).toHaveBeenCalled();
});
});
@@ -395,7 +395,7 @@ describe('Filters actions', () => {
[],
).then(() => {
expect(mock.history.get[0].url).toBe('/api/v1/projects/fake_project_endpoint/users');
- expect(createFlash).toHaveBeenCalled();
+ expect(createAlert).toHaveBeenCalled();
});
});
});
@@ -441,7 +441,7 @@ describe('Filters actions', () => {
},
],
[],
- ).then(() => expect(createFlash).toHaveBeenCalled());
+ ).then(() => expect(createAlert).toHaveBeenCalled());
});
});
});
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/author_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/author_token_spec.js
index 302dfabffb2..5371b9af475 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/author_token_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/author_token_spec.js
@@ -8,7 +8,7 @@ import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import { nextTick } from 'vue';
import waitForPromises from 'helpers/wait_for_promises';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { DEFAULT_NONE_ANY } from '~/vue_shared/components/filtered_search_bar/constants';
@@ -140,13 +140,13 @@ describe('AuthorToken', () => {
});
});
- it('calls `createFlash` with flash error message when request fails', () => {
+ it('calls `createAlert` with flash error message when request fails', () => {
jest.spyOn(wrapper.vm.config, 'fetchAuthors').mockRejectedValue({});
getBaseToken().vm.$emit('fetch-suggestions', 'root');
return waitForPromises().then(() => {
- expect(createFlash).toHaveBeenCalledWith({
+ expect(createAlert).toHaveBeenCalledWith({
message: 'There was a problem fetching users.',
});
});
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/branch_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/branch_token_spec.js
index 1de35daa3a5..05b42011fe1 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/branch_token_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/branch_token_spec.js
@@ -9,7 +9,7 @@ import MockAdapter from 'axios-mock-adapter';
import { nextTick } from 'vue';
import waitForPromises from 'helpers/wait_for_promises';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { DEFAULT_NONE_ANY } from '~/vue_shared/components/filtered_search_bar/constants';
import BranchToken from '~/vue_shared/components/filtered_search_bar/tokens/branch_token.vue';
@@ -87,13 +87,13 @@ describe('BranchToken', () => {
});
});
- it('calls `createFlash` with flash error message when request fails', () => {
+ it('calls `createAlert` with flash error message when request fails', () => {
jest.spyOn(wrapper.vm.config, 'fetchBranches').mockRejectedValue({});
wrapper.vm.fetchBranches('foo');
return waitForPromises().then(() => {
- expect(createFlash).toHaveBeenCalledWith({
+ expect(createAlert).toHaveBeenCalledWith({
message: 'There was a problem fetching branches.',
});
});
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/crm_contact_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/crm_contact_token_spec.js
index c9879987931..5b744521979 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/crm_contact_token_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/crm_contact_token_spec.js
@@ -8,7 +8,7 @@ import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { DEFAULT_NONE_ANY } from '~/vue_shared/components/filtered_search_bar/constants';
import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_token.vue';
@@ -94,7 +94,7 @@ describe('CrmContactToken', () => {
getBaseToken().vm.$emit('fetch-suggestions', 'foo');
await waitForPromises();
- expect(createFlash).not.toHaveBeenCalled();
+ expect(createAlert).not.toHaveBeenCalled();
expect(searchGroupCrmContactsQueryHandler).toHaveBeenCalledWith({
fullPath: 'group',
isProject: false,
@@ -108,7 +108,7 @@ describe('CrmContactToken', () => {
getBaseToken().vm.$emit('fetch-suggestions', '5');
await waitForPromises();
- expect(createFlash).not.toHaveBeenCalled();
+ expect(createAlert).not.toHaveBeenCalled();
expect(searchGroupCrmContactsQueryHandler).toHaveBeenCalledWith({
fullPath: 'group',
isProject: false,
@@ -134,7 +134,7 @@ describe('CrmContactToken', () => {
getBaseToken().vm.$emit('fetch-suggestions', 'foo');
await waitForPromises();
- expect(createFlash).not.toHaveBeenCalled();
+ expect(createAlert).not.toHaveBeenCalled();
expect(searchProjectCrmContactsQueryHandler).toHaveBeenCalledWith({
fullPath: 'project',
isProject: true,
@@ -148,7 +148,7 @@ describe('CrmContactToken', () => {
getBaseToken().vm.$emit('fetch-suggestions', '5');
await waitForPromises();
- expect(createFlash).not.toHaveBeenCalled();
+ expect(createAlert).not.toHaveBeenCalled();
expect(searchProjectCrmContactsQueryHandler).toHaveBeenCalledWith({
fullPath: 'project',
isProject: true,
@@ -159,7 +159,7 @@ describe('CrmContactToken', () => {
});
});
- it('calls `createFlash` with flash error message when request fails', async () => {
+ it('calls `createAlert` with flash error message when request fails', async () => {
mountComponent();
jest.spyOn(wrapper.vm.$apollo, 'query').mockRejectedValue({});
@@ -167,7 +167,7 @@ describe('CrmContactToken', () => {
getBaseToken().vm.$emit('fetch-suggestions');
await waitForPromises();
- expect(createFlash).toHaveBeenCalledWith({
+ expect(createAlert).toHaveBeenCalledWith({
message: 'There was a problem fetching CRM contacts.',
});
});
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/crm_organization_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/crm_organization_token_spec.js
index 16333b052e6..3a3e96032e8 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/crm_organization_token_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/crm_organization_token_spec.js
@@ -8,7 +8,7 @@ import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { DEFAULT_NONE_ANY } from '~/vue_shared/components/filtered_search_bar/constants';
import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_token.vue';
@@ -93,7 +93,7 @@ describe('CrmOrganizationToken', () => {
getBaseToken().vm.$emit('fetch-suggestions', 'foo');
await waitForPromises();
- expect(createFlash).not.toHaveBeenCalled();
+ expect(createAlert).not.toHaveBeenCalled();
expect(searchGroupCrmOrganizationsQueryHandler).toHaveBeenCalledWith({
fullPath: 'group',
isProject: false,
@@ -107,7 +107,7 @@ describe('CrmOrganizationToken', () => {
getBaseToken().vm.$emit('fetch-suggestions', '5');
await waitForPromises();
- expect(createFlash).not.toHaveBeenCalled();
+ expect(createAlert).not.toHaveBeenCalled();
expect(searchGroupCrmOrganizationsQueryHandler).toHaveBeenCalledWith({
fullPath: 'group',
isProject: false,
@@ -133,7 +133,7 @@ describe('CrmOrganizationToken', () => {
getBaseToken().vm.$emit('fetch-suggestions', 'foo');
await waitForPromises();
- expect(createFlash).not.toHaveBeenCalled();
+ expect(createAlert).not.toHaveBeenCalled();
expect(searchProjectCrmOrganizationsQueryHandler).toHaveBeenCalledWith({
fullPath: 'project',
isProject: true,
@@ -147,7 +147,7 @@ describe('CrmOrganizationToken', () => {
getBaseToken().vm.$emit('fetch-suggestions', '5');
await waitForPromises();
- expect(createFlash).not.toHaveBeenCalled();
+ expect(createAlert).not.toHaveBeenCalled();
expect(searchProjectCrmOrganizationsQueryHandler).toHaveBeenCalledWith({
fullPath: 'project',
isProject: true,
@@ -158,7 +158,7 @@ describe('CrmOrganizationToken', () => {
});
});
- it('calls `createFlash` with flash error message when request fails', async () => {
+ it('calls `createAlert` with flash error message when request fails', async () => {
mountComponent();
jest.spyOn(wrapper.vm.$apollo, 'query').mockRejectedValue({});
@@ -166,7 +166,7 @@ describe('CrmOrganizationToken', () => {
getBaseToken().vm.$emit('fetch-suggestions');
await waitForPromises();
- expect(createFlash).toHaveBeenCalledWith({
+ expect(createAlert).toHaveBeenCalledWith({
message: 'There was a problem fetching CRM organizations.',
});
});
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/emoji_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/emoji_token_spec.js
index bf4a6eb7635..e8436d2db17 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/emoji_token_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/emoji_token_spec.js
@@ -8,7 +8,7 @@ import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import { nextTick } from 'vue';
import waitForPromises from 'helpers/wait_for_promises';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import {
@@ -93,13 +93,13 @@ describe('EmojiToken', () => {
});
});
- it('calls `createFlash` with flash error message when request fails', () => {
+ it('calls `createAlert` with flash error message when request fails', () => {
jest.spyOn(wrapper.vm.config, 'fetchEmojis').mockRejectedValue({});
wrapper.vm.fetchEmojis('foo');
return waitForPromises().then(() => {
- expect(createFlash).toHaveBeenCalledWith({
+ expect(createAlert).toHaveBeenCalledWith({
message: 'There was a problem fetching emojis.',
});
});
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/label_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/label_token_spec.js
index 01e281884ed..8ca12afacec 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/label_token_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/label_token_spec.js
@@ -11,7 +11,7 @@ import {
mockRegularLabel,
mockLabels,
} from 'jest/vue_shared/components/sidebar/labels_select_vue/mock_data';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { DEFAULT_NONE_ANY } from '~/vue_shared/components/filtered_search_bar/constants';
@@ -116,13 +116,13 @@ describe('LabelToken', () => {
});
});
- it('calls `createFlash` with flash error message when request fails', () => {
+ it('calls `createAlert` with flash error message when request fails', () => {
jest.spyOn(wrapper.vm.config, 'fetchLabels').mockRejectedValue({});
wrapper.vm.fetchLabels('foo');
return waitForPromises().then(() => {
- expect(createFlash).toHaveBeenCalledWith({
+ expect(createAlert).toHaveBeenCalledWith({
message: 'There was a problem fetching labels.',
});
});
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/milestone_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/milestone_token_spec.js
index f71ba51fc5b..589697fe542 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/milestone_token_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/milestone_token_spec.js
@@ -8,7 +8,7 @@ import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import { nextTick } from 'vue';
import waitForPromises from 'helpers/wait_for_promises';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { sortMilestonesByDueDate } from '~/milestones/utils';
@@ -112,13 +112,13 @@ describe('MilestoneToken', () => {
});
});
- it('calls `createFlash` with flash error message when request fails', () => {
+ it('calls `createAlert` with flash error message when request fails', () => {
jest.spyOn(wrapper.vm.config, 'fetchMilestones').mockRejectedValue({});
wrapper.vm.fetchMilestones('foo');
return waitForPromises().then(() => {
- expect(createFlash).toHaveBeenCalledWith({
+ expect(createAlert).toHaveBeenCalledWith({
message: 'There was a problem fetching milestones.',
});
});
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/release_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/release_token_spec.js
index 4bbbaab9b7a..0e5fa0f66d4 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/release_token_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/release_token_spec.js
@@ -2,7 +2,7 @@ import { GlFilteredSearchToken, GlFilteredSearchTokenSegment } from '@gitlab/ui'
import { mount } from '@vue/test-utils';
import { nextTick } from 'vue';
import waitForPromises from 'helpers/wait_for_promises';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import ReleaseToken from '~/vue_shared/components/filtered_search_bar/tokens/release_token.vue';
import { mockReleaseToken } from '../mock_data';
@@ -73,7 +73,7 @@ describe('ReleaseToken', () => {
});
await waitForPromises();
- expect(createFlash).toHaveBeenCalledWith({
+ expect(createAlert).toHaveBeenCalledWith({
message: 'There was a problem fetching releases.',
});
});
diff --git a/spec/frontend/vue_shared/components/gitlab_version_check_spec.js b/spec/frontend/vue_shared/components/gitlab_version_check_spec.js
index 6699ae5fb69..38f28837cc1 100644
--- a/spec/frontend/vue_shared/components/gitlab_version_check_spec.js
+++ b/spec/frontend/vue_shared/components/gitlab_version_check_spec.js
@@ -1,7 +1,9 @@
import { GlBadge } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
+import { mockTracking } from 'helpers/tracking_helper';
+import { helpPagePath } from '~/helpers/help_page_helper';
import axios from '~/lib/utils/axios_utils';
import GitlabVersionCheck from '~/vue_shared/components/gitlab_version_check.vue';
@@ -9,6 +11,8 @@ describe('GitlabVersionCheck', () => {
let wrapper;
let mock;
+ const UPGRADE_DOCS_URL = helpPagePath('update/index');
+
const defaultResponse = {
code: 200,
res: { severity: 'success' },
@@ -23,7 +27,7 @@ describe('GitlabVersionCheck', () => {
mock = new MockAdapter(axios);
mock.onGet().replyOnce(response.code, response.res);
- wrapper = shallowMount(GitlabVersionCheck);
+ wrapper = shallowMountExtended(GitlabVersionCheck);
};
const dummyGon = {
@@ -38,6 +42,7 @@ describe('GitlabVersionCheck', () => {
window.gon = originalGon;
});
+ const findGlBadgeClickWrapper = () => wrapper.findByTestId('badge-click-wrapper');
const findGlBadge = () => wrapper.findComponent(GlBadge);
describe.each`
@@ -77,7 +82,8 @@ describe('GitlabVersionCheck', () => {
await waitForPromises(); // Ensure we wrap up the axios call
});
- it(`does${renders ? '' : ' not'} render GlBadge`, () => {
+ it(`does${renders ? '' : ' not'} render Badge Click Wrapper and GlBadge`, () => {
+ expect(findGlBadgeClickWrapper().exists()).toBe(renders);
expect(findGlBadge().exists()).toBe(renders);
});
});
@@ -90,8 +96,11 @@ describe('GitlabVersionCheck', () => {
${{ code: 200, res: { severity: 'danger' } }} | ${{ title: 'Update ASAP', variant: 'danger' }}
`('badge ui', ({ mockResponse, expectedUI }) => {
describe(`when response is ${mockResponse.res.severity}`, () => {
+ let trackingSpy;
+
beforeEach(async () => {
createComponent(mockResponse);
+ trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
await waitForPromises(); // Ensure we wrap up the axios call
});
@@ -102,6 +111,24 @@ describe('GitlabVersionCheck', () => {
it(`variant is ${expectedUI.variant}`, () => {
expect(findGlBadge().attributes('variant')).toBe(expectedUI.variant);
});
+
+ it(`tracks rendered_version_badge with label ${expectedUI.title}`, () => {
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, 'rendered_version_badge', {
+ label: expectedUI.title,
+ });
+ });
+
+ it(`link is ${UPGRADE_DOCS_URL}`, () => {
+ expect(findGlBadge().attributes('href')).toBe(UPGRADE_DOCS_URL);
+ });
+
+ it(`tracks click_version_badge with label ${expectedUI.title} when badge is clicked`, async () => {
+ await findGlBadgeClickWrapper().trigger('click');
+
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_version_badge', {
+ label: expectedUI.title,
+ });
+ });
});
});
});
diff --git a/spec/frontend/vue_shared/components/gl_countdown_spec.js b/spec/frontend/vue_shared/components/gl_countdown_spec.js
index 0d1d42082ab..af53d256236 100644
--- a/spec/frontend/vue_shared/components/gl_countdown_spec.js
+++ b/spec/frontend/vue_shared/components/gl_countdown_spec.js
@@ -1,10 +1,9 @@
import Vue, { nextTick } from 'vue';
-import mountComponent from 'helpers/vue_mount_component_helper';
+import { mount } from '@vue/test-utils';
import GlCountdown from '~/vue_shared/components/gl_countdown.vue';
describe('GlCountdown', () => {
- const Component = Vue.extend(GlCountdown);
- let vm;
+ let wrapper;
let now = '2000-01-01T00:00:00Z';
beforeEach(() => {
@@ -12,21 +11,20 @@ describe('GlCountdown', () => {
});
afterEach(() => {
- vm.$destroy();
- jest.clearAllTimers();
+ wrapper.destroy();
});
describe('when there is time remaining', () => {
beforeEach(async () => {
- vm = mountComponent(Component, {
- endDateString: '2000-01-01T01:02:03Z',
+ wrapper = mount(GlCountdown, {
+ propsData: {
+ endDateString: '2000-01-01T01:02:03Z',
+ },
});
-
- await nextTick();
});
it('displays remaining time', () => {
- expect(vm.$el.textContent).toContain('01:02:03');
+ expect(wrapper.text()).toContain('01:02:03');
});
it('updates remaining time', async () => {
@@ -34,21 +32,21 @@ describe('GlCountdown', () => {
jest.advanceTimersByTime(1000);
await nextTick();
- expect(vm.$el.textContent).toContain('01:02:02');
+ expect(wrapper.text()).toContain('01:02:02');
});
});
describe('when there is no time remaining', () => {
beforeEach(async () => {
- vm = mountComponent(Component, {
- endDateString: '1900-01-01T00:00:00Z',
+ wrapper = mount(GlCountdown, {
+ propsData: {
+ endDateString: '1900-01-01T00:00:00Z',
+ },
});
-
- await nextTick();
});
it('displays 00:00:00', () => {
- expect(vm.$el.textContent).toContain('00:00:00');
+ expect(wrapper.text()).toContain('00:00:00');
});
});
@@ -62,8 +60,10 @@ describe('GlCountdown', () => {
});
it('throws a validation error', () => {
- vm = mountComponent(Component, {
- endDateString: 'this is invalid',
+ wrapper = mount(GlCountdown, {
+ propsData: {
+ endDateString: 'this is invalid',
+ },
});
expect(Vue.config.warnHandler).toHaveBeenCalledTimes(1);
diff --git a/spec/frontend/vue_shared/components/group_select/utils_spec.js b/spec/frontend/vue_shared/components/group_select/utils_spec.js
new file mode 100644
index 00000000000..5188e1aabf1
--- /dev/null
+++ b/spec/frontend/vue_shared/components/group_select/utils_spec.js
@@ -0,0 +1,24 @@
+import { groupsPath } from '~/vue_shared/components/group_select/utils';
+
+describe('group_select utils', () => {
+ describe('groupsPath', () => {
+ it.each`
+ groupsFilter | parentGroupID | expectedPath
+ ${undefined} | ${undefined} | ${'/api/:version/groups.json'}
+ ${undefined} | ${1} | ${'/api/:version/groups.json'}
+ ${'descendant_groups'} | ${1} | ${'/api/:version/groups/1/descendant_groups'}
+ ${'subgroups'} | ${1} | ${'/api/:version/groups/1/subgroups'}
+ `(
+ 'returns $expectedPath with groupsFilter = $groupsFilter and parentGroupID = $parentGroupID',
+ ({ groupsFilter, parentGroupID, expectedPath }) => {
+ expect(groupsPath(groupsFilter, parentGroupID)).toBe(expectedPath);
+ },
+ );
+ });
+
+ it('throws if groupsFilter is passed but parentGroupID is undefined', () => {
+ expect(() => {
+ groupsPath('descendant_groups');
+ }).toThrow('Cannot use groupsFilter without a parentGroupID');
+ });
+});
diff --git a/spec/frontend/vue_shared/components/markdown/header_spec.js b/spec/frontend/vue_shared/components/markdown/header_spec.js
index 9831908f806..ed417097e1e 100644
--- a/spec/frontend/vue_shared/components/markdown/header_spec.js
+++ b/spec/frontend/vue_shared/components/markdown/header_spec.js
@@ -54,6 +54,8 @@ describe('Markdown field header component', () => {
'Add a bullet list',
'Add a numbered list',
'Add a checklist',
+ 'Indent line (⌘])',
+ 'Outdent line (⌘[)',
'Add a collapsible section',
'Add a table',
'Go full screen',
@@ -140,7 +142,7 @@ describe('Markdown field header component', () => {
const tableButton = findToolbarButtonByProp('icon', 'table');
expect(tableButton.props('tag')).toEqual(
- '| header | header |\n| ------ | ------ |\n| cell | cell |\n| cell | cell |',
+ '| header | header |\n| ------ | ------ |\n| | |\n| | |',
);
});
diff --git a/spec/frontend/vue_shared/components/markdown/markdown_editor_spec.js b/spec/frontend/vue_shared/components/markdown/markdown_editor_spec.js
new file mode 100644
index 00000000000..f7e93f45148
--- /dev/null
+++ b/spec/frontend/vue_shared/components/markdown/markdown_editor_spec.js
@@ -0,0 +1,289 @@
+import { GlSegmentedControl } from '@gitlab/ui';
+import axios from 'axios';
+import MockAdapter from 'axios-mock-adapter';
+import { nextTick } from 'vue';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import { EDITING_MODE_MARKDOWN_FIELD, EDITING_MODE_CONTENT_EDITOR } from '~/vue_shared/constants';
+import MarkdownEditor from '~/vue_shared/components/markdown/markdown_editor.vue';
+import ContentEditor from '~/content_editor/components/content_editor.vue';
+import BubbleMenu from '~/content_editor/components/bubble_menus/bubble_menu.vue';
+import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
+import MarkdownField from '~/vue_shared/components/markdown/field.vue';
+import { stubComponent } from 'helpers/stub_component';
+
+jest.mock('~/emoji');
+
+describe('vue_shared/component/markdown/markdown_editor', () => {
+ let wrapper;
+ const value = 'test markdown';
+ const renderMarkdownPath = '/api/markdown';
+ const markdownDocsPath = '/help/markdown';
+ const quickActionsDocsPath = '/help/quickactions';
+ const enableAutocomplete = true;
+ const enablePreview = false;
+ const formFieldId = 'markdown_field';
+ const formFieldName = 'form[markdown_field]';
+ const formFieldPlaceholder = 'Write some markdown';
+ const formFieldAriaLabel = 'Edit your content';
+ let mock;
+
+ const buildWrapper = ({ propsData = {}, attachTo } = {}) => {
+ wrapper = mountExtended(MarkdownEditor, {
+ attachTo,
+ propsData: {
+ value,
+ renderMarkdownPath,
+ markdownDocsPath,
+ quickActionsDocsPath,
+ enableAutocomplete,
+ enablePreview,
+ formFieldId,
+ formFieldName,
+ formFieldPlaceholder,
+ formFieldAriaLabel,
+ ...propsData,
+ },
+ stubs: {
+ BubbleMenu: stubComponent(BubbleMenu),
+ },
+ });
+ };
+ const findSegmentedControl = () => wrapper.findComponent(GlSegmentedControl);
+ const findMarkdownField = () => wrapper.findComponent(MarkdownField);
+ const findTextarea = () => wrapper.find('textarea');
+ const findLocalStorageSync = () => wrapper.findComponent(LocalStorageSync);
+ const findContentEditor = () => wrapper.findComponent(ContentEditor);
+
+ beforeEach(() => {
+ window.uploads_path = 'uploads';
+ mock = new MockAdapter(axios);
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ mock.restore();
+ });
+
+ it('displays markdown field by default', () => {
+ buildWrapper({ propsData: { supportsQuickActions: true } });
+
+ expect(findMarkdownField().props()).toEqual(
+ expect.objectContaining({
+ markdownPreviewPath: renderMarkdownPath,
+ quickActionsDocsPath,
+ canAttachFile: true,
+ enableAutocomplete,
+ textareaValue: value,
+ markdownDocsPath,
+ uploadsPath: window.uploads_path,
+ enablePreview,
+ }),
+ );
+ });
+
+ it('renders markdown field textarea', () => {
+ buildWrapper();
+
+ expect(findTextarea().attributes()).toEqual(
+ expect.objectContaining({
+ id: formFieldId,
+ name: formFieldName,
+ placeholder: formFieldPlaceholder,
+ 'aria-label': formFieldAriaLabel,
+ }),
+ );
+
+ expect(findTextarea().element.value).toBe(value);
+ });
+
+ it('renders switch segmented control', () => {
+ buildWrapper();
+
+ expect(findSegmentedControl().props()).toEqual({
+ checked: EDITING_MODE_MARKDOWN_FIELD,
+ options: [
+ {
+ text: expect.any(String),
+ value: EDITING_MODE_MARKDOWN_FIELD,
+ },
+ {
+ text: expect.any(String),
+ value: EDITING_MODE_CONTENT_EDITOR,
+ },
+ ],
+ });
+ });
+
+ describe.each`
+ editingMode
+ ${EDITING_MODE_CONTENT_EDITOR}
+ ${EDITING_MODE_MARKDOWN_FIELD}
+ `('when segmented control emits change event with $editingMode value', ({ editingMode }) => {
+ it(`emits ${editingMode} event`, () => {
+ buildWrapper();
+
+ findSegmentedControl().vm.$emit('change', editingMode);
+
+ expect(wrapper.emitted(editingMode)).toHaveLength(1);
+ });
+ });
+
+ describe(`when editingMode is ${EDITING_MODE_MARKDOWN_FIELD}`, () => {
+ it('emits input event when markdown field textarea changes', async () => {
+ buildWrapper();
+ const newValue = 'new value';
+
+ await findTextarea().setValue(newValue);
+
+ expect(wrapper.emitted('input')).toEqual([[newValue]]);
+ });
+
+ describe('when initOnAutofocus is true', () => {
+ beforeEach(async () => {
+ buildWrapper({ attachTo: document.body, propsData: { initOnAutofocus: true } });
+
+ await nextTick();
+ });
+
+ it('sets the markdown field as the active element in the document', () => {
+ expect(document.activeElement).toBe(findTextarea().element);
+ });
+ });
+
+ it('bubbles up keydown event', async () => {
+ buildWrapper();
+
+ await findTextarea().trigger('keydown');
+
+ expect(wrapper.emitted('keydown')).toHaveLength(1);
+ });
+
+ describe(`when segmented control triggers input event with ${EDITING_MODE_CONTENT_EDITOR} value`, () => {
+ beforeEach(() => {
+ buildWrapper();
+ findSegmentedControl().vm.$emit('input', EDITING_MODE_CONTENT_EDITOR);
+ findSegmentedControl().vm.$emit('change', EDITING_MODE_CONTENT_EDITOR);
+ });
+
+ it('displays the content editor', () => {
+ expect(findContentEditor().props()).toEqual(
+ expect.objectContaining({
+ renderMarkdown: expect.any(Function),
+ uploadsPath: window.uploads_path,
+ markdown: value,
+ autofocus: 'end',
+ }),
+ );
+ });
+
+ it('adds hidden field with current markdown', () => {
+ const hiddenField = wrapper.find(`#${formFieldId}`);
+
+ expect(hiddenField.attributes()).toEqual(
+ expect.objectContaining({
+ id: formFieldId,
+ name: formFieldName,
+ }),
+ );
+ expect(hiddenField.element.value).toBe(value);
+ });
+
+ it('hides the markdown field', () => {
+ expect(findMarkdownField().exists()).toBe(false);
+ });
+
+ it('updates localStorage value', () => {
+ expect(findLocalStorageSync().props().value).toBe(EDITING_MODE_CONTENT_EDITOR);
+ });
+ });
+ });
+
+ describe(`when editingMode is ${EDITING_MODE_CONTENT_EDITOR}`, () => {
+ beforeEach(() => {
+ buildWrapper();
+ findSegmentedControl().vm.$emit('input', EDITING_MODE_CONTENT_EDITOR);
+ });
+
+ describe('when initOnAutofocus is true', () => {
+ beforeEach(() => {
+ buildWrapper({ propsData: { initOnAutofocus: true } });
+ findLocalStorageSync().vm.$emit('input', EDITING_MODE_CONTENT_EDITOR);
+ });
+
+ it('sets the content editor autofocus property to end', () => {
+ expect(findContentEditor().props().autofocus).toBe('end');
+ });
+ });
+
+ it('emits input event when content editor emits change event', async () => {
+ const newValue = 'new value';
+
+ await findContentEditor().vm.$emit('change', { markdown: newValue });
+
+ expect(wrapper.emitted('input')).toEqual([[newValue]]);
+ });
+
+ it('bubbles up keydown event', () => {
+ const event = new Event('keydown');
+
+ findContentEditor().vm.$emit('keydown', event);
+
+ expect(wrapper.emitted('keydown')).toEqual([[event]]);
+ });
+
+ describe(`when segmented control triggers input event with ${EDITING_MODE_MARKDOWN_FIELD} value`, () => {
+ beforeEach(() => {
+ findSegmentedControl().vm.$emit('input', EDITING_MODE_MARKDOWN_FIELD);
+ });
+
+ it('hides the content editor', () => {
+ expect(findContentEditor().exists()).toBe(false);
+ });
+
+ it('shows the markdown field', () => {
+ expect(findMarkdownField().exists()).toBe(true);
+ });
+
+ it('updates localStorage value', () => {
+ expect(findLocalStorageSync().props().value).toBe(EDITING_MODE_MARKDOWN_FIELD);
+ });
+
+ it('sets the textarea as the activeElement in the document', async () => {
+ // The component should be rebuilt to attach it to the document body
+ buildWrapper({ attachTo: document.body });
+ await findSegmentedControl().vm.$emit('input', EDITING_MODE_CONTENT_EDITOR);
+
+ expect(findContentEditor().exists()).toBe(true);
+
+ await findSegmentedControl().vm.$emit('input', EDITING_MODE_MARKDOWN_FIELD);
+ await findSegmentedControl().vm.$emit('change', EDITING_MODE_MARKDOWN_FIELD);
+
+ expect(document.activeElement).toBe(findTextarea().element);
+ });
+ });
+
+ describe('when content editor emits loading event', () => {
+ beforeEach(() => {
+ findContentEditor().vm.$emit('loading');
+ });
+
+ it('disables switch editing mode control', () => {
+ // This is the only way that I found to check the segmented control is disabled
+ expect(findSegmentedControl().find('input[disabled]').exists()).toBe(true);
+ });
+
+ describe.each`
+ event
+ ${'loadingSuccess'}
+ ${'loadingError'}
+ `('when content editor emits $event event', ({ event }) => {
+ beforeEach(() => {
+ findContentEditor().vm.$emit(event);
+ });
+ it('enables the switch editing mode control', () => {
+ expect(findSegmentedControl().find('input[disabled]').exists()).toBe(false);
+ });
+ });
+ });
+ });
+});
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
index d792bd46ccd..9c91dc9b5fc 100644
--- 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
@@ -139,8 +139,7 @@ describe('Metrics upload item', () => {
closeModal();
await waitForPromises();
-
- expect(findModal().attributes('visible')).toBeFalsy();
+ expect(findModal().attributes('visible')).toBeUndefined();
});
it('should delete the image when selected', async () => {
@@ -189,8 +188,7 @@ describe('Metrics upload item', () => {
closeEditModal();
await waitForPromises();
-
- expect(findEditModal().attributes('visible')).toBeFalsy();
+ expect(findEditModal().attributes('visible')).toBeUndefined();
});
it('should delete the image when selected', async () => {
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
index 518cf354675..537367940e0 100644
--- a/spec/frontend/vue_shared/components/metric_images/store/actions_spec.js
+++ b/spec/frontend/vue_shared/components/metric_images/store/actions_spec.js
@@ -4,7 +4,7 @@ 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 { createAlert } from '~/flash';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { fileList, initialData } from '../mock_data';
@@ -35,7 +35,7 @@ describe('Metrics tab store actions', () => {
});
afterEach(() => {
- createFlash.mockClear();
+ createAlert.mockClear();
});
describe('fetching metric images', () => {
@@ -61,7 +61,7 @@ describe('Metrics tab store actions', () => {
[{ type: types.REQUEST_METRIC_IMAGES }, { type: types.RECEIVE_METRIC_IMAGES_ERROR }],
[],
);
- expect(createFlash).toHaveBeenCalled();
+ expect(createAlert).toHaveBeenCalled();
});
});
@@ -98,7 +98,7 @@ describe('Metrics tab store actions', () => {
[{ type: types.REQUEST_METRIC_UPLOAD }, { type: types.RECEIVE_METRIC_UPLOAD_ERROR }],
[],
);
- expect(createFlash).toHaveBeenCalled();
+ expect(createAlert).toHaveBeenCalled();
});
});
@@ -129,7 +129,7 @@ describe('Metrics tab store actions', () => {
[{ type: types.REQUEST_METRIC_UPLOAD }, { type: types.RECEIVE_METRIC_UPLOAD_ERROR }],
[],
);
- expect(createFlash).toHaveBeenCalled();
+ expect(createAlert).toHaveBeenCalled();
});
});
diff --git a/spec/frontend/vue_shared/components/modal_copy_button_spec.js b/spec/frontend/vue_shared/components/modal_copy_button_spec.js
index b57efc88d57..61e4e774420 100644
--- a/spec/frontend/vue_shared/components/modal_copy_button_spec.js
+++ b/spec/frontend/vue_shared/components/modal_copy_button_spec.js
@@ -17,9 +17,16 @@ describe('modal copy button', () => {
title: 'Copy this value',
id: 'test-id',
},
+ slots: {
+ default: 'test',
+ },
});
});
+ it('should show the default slot', () => {
+ expect(wrapper.text()).toBe('test');
+ });
+
describe('clipboard', () => {
it('should fire a `success` event on click', async () => {
const root = createWrapper(wrapper.vm.$root);
diff --git a/spec/frontend/vue_shared/components/namespace_select/namespace_select_spec.js b/spec/frontend/vue_shared/components/namespace_select/namespace_select_deprecated_spec.js
index 2c14d65186b..d930ef63dad 100644
--- a/spec/frontend/vue_shared/components/namespace_select/namespace_select_spec.js
+++ b/spec/frontend/vue_shared/components/namespace_select/namespace_select_deprecated_spec.js
@@ -11,14 +11,14 @@ import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import NamespaceSelect, {
i18n,
EMPTY_NAMESPACE_ID,
-} from '~/vue_shared/components/namespace_select/namespace_select.vue';
+} from '~/vue_shared/components/namespace_select/namespace_select_deprecated.vue';
import { userNamespaces, groupNamespaces } from './mock_data';
const FLAT_NAMESPACES = [...userNamespaces, ...groupNamespaces];
const EMPTY_NAMESPACE_TITLE = 'Empty namespace TEST';
const EMPTY_NAMESPACE_ITEM = { id: EMPTY_NAMESPACE_ID, humanName: EMPTY_NAMESPACE_TITLE };
-describe('Namespace Select', () => {
+describe('NamespaceSelectDeprecated', () => {
let wrapper;
const createComponent = (props = {}) =>
@@ -207,9 +207,9 @@ describe('Namespace Select', () => {
expect(wrapper.emitted('load-more-groups')).toEqual([[]]);
});
- describe('when `isLoadingMoreGroups` prop is `true`', () => {
+ describe('when `isLoading` prop is `true`', () => {
it('renders a loading icon', () => {
- wrapper = createComponent({ hasNextPageOfGroups: true, isLoadingMoreGroups: true });
+ wrapper = createComponent({ hasNextPageOfGroups: true, isLoading: true });
expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
});
@@ -223,4 +223,14 @@ describe('Namespace Select', () => {
expect(wrapper.findComponent(GlSearchBoxByType).props('isLoading')).toBe(true);
});
});
+
+ describe('when dropdown is opened', () => {
+ it('emits `show` event', () => {
+ wrapper = createComponent();
+
+ findDropdown().vm.$emit('show');
+
+ expect(wrapper.emitted('show')).toEqual([[]]);
+ });
+ });
});
diff --git a/spec/frontend/vue_shared/components/notes/__snapshots__/placeholder_note_spec.js.snap b/spec/frontend/vue_shared/components/notes/__snapshots__/placeholder_note_spec.js.snap
index bf6c8e8c704..3bac96069ec 100644
--- a/spec/frontend/vue_shared/components/notes/__snapshots__/placeholder_note_spec.js.snap
+++ b/spec/frontend/vue_shared/components/notes/__snapshots__/placeholder_note_spec.js.snap
@@ -2,13 +2,12 @@
exports[`Issue placeholder note component matches snapshot 1`] = `
<timeline-entry-item-stub
- class="note note-wrapper being-posted fade-in-half"
+ class="note note-wrapper note-comment being-posted fade-in-half"
>
<div
- class="timeline-icon"
+ class="timeline-avatar gl-float-left"
>
<gl-avatar-link-stub
- class="gl-mr-3"
href="/root"
>
<gl-avatar-stub
@@ -16,7 +15,7 @@ exports[`Issue placeholder note component matches snapshot 1`] = `
entityid="0"
entityname="root"
shape="circle"
- size="[object Object]"
+ size="32"
src="mock_path"
/>
</gl-avatar-link-stub>
@@ -50,16 +49,20 @@ exports[`Issue placeholder note component matches snapshot 1`] = `
</div>
<div
- class="note-body"
+ class="timeline-discussion-body"
>
<div
- class="note-text md"
+ class="note-body"
>
- <p>
- Foo
- </p>
-
+ <div
+ class="note-text md"
+ >
+ <p>
+ Foo
+ </p>
+
+ </div>
</div>
</div>
</div>
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 b86c8946e96..8f9f1bb336f 100644
--- a/spec/frontend/vue_shared/components/notes/placeholder_note_spec.js
+++ b/spec/frontend/vue_shared/components/notes/placeholder_note_spec.js
@@ -1,5 +1,4 @@
import { shallowMount } from '@vue/test-utils';
-import { GlAvatar } from '@gitlab/ui';
import Vue from 'vue';
import Vuex from 'vuex';
import IssuePlaceholderNote from '~/vue_shared/components/notes/placeholder_note.vue';
@@ -53,17 +52,4 @@ describe('Issue placeholder note component', () => {
expect(findNote().classes()).toContain('discussion');
});
-
- describe('avatar size', () => {
- it.each`
- size | line | isOverviewTab
- ${{ default: 24, md: 32 }} | ${null} | ${false}
- ${24} | ${{ line_code: '123' }} | ${false}
- ${{ default: 24, md: 32 }} | ${{ line_code: '123' }} | ${true}
- `('renders avatar $size for $line and $isOverviewTab', ({ size, line, isOverviewTab }) => {
- createComponent(false, { line, isOverviewTab });
-
- expect(wrapper.findComponent(GlAvatar).props('size')).toEqual(size);
- });
- });
});
diff --git a/spec/frontend/vue_shared/components/pagination_bar/pagination_bar_spec.js b/spec/frontend/vue_shared/components/pagination_bar/pagination_bar_spec.js
index b3be2f8a775..112cdaf74c6 100644
--- a/spec/frontend/vue_shared/components/pagination_bar/pagination_bar_spec.js
+++ b/spec/frontend/vue_shared/components/pagination_bar/pagination_bar_spec.js
@@ -2,6 +2,7 @@ import { GlPagination, GlDropdown, GlDropdownItem } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import PaginationBar from '~/vue_shared/components/pagination_bar/pagination_bar.vue';
import PaginationLinks from '~/vue_shared/components/pagination_links.vue';
+import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
describe('Pagination bar', () => {
const DEFAULT_PROPS = {
@@ -20,6 +21,7 @@ describe('Pagination bar', () => {
...DEFAULT_PROPS,
...propsData,
},
+ stubs: { LocalStorageSync: true },
});
};
@@ -90,4 +92,28 @@ describe('Pagination bar', () => {
'Showing 21 - 40 of 1000+',
);
});
+
+ describe('local storage sync', () => {
+ it('does not perform local storage sync when no storage key is provided', () => {
+ createComponent();
+
+ expect(wrapper.findComponent(LocalStorageSync).exists()).toBe(false);
+ });
+
+ it('passes current page size to local storage sync when storage key is provided', () => {
+ const STORAGE_KEY = 'fakeStorageKey';
+ createComponent({ storageKey: STORAGE_KEY });
+
+ expect(wrapper.getComponent(LocalStorageSync).props('storageKey')).toBe(STORAGE_KEY);
+ });
+
+ it('emits set-page event when local storage sync provides new value', () => {
+ const SAVED_SIZE = 50;
+ createComponent({ storageKey: 'some storage key' });
+
+ wrapper.getComponent(LocalStorageSync).vm.$emit('input', SAVED_SIZE);
+
+ expect(wrapper.emitted('set-page-size')).toEqual([[SAVED_SIZE]]);
+ });
+ });
});
diff --git a/spec/frontend/vue_shared/components/panel_resizer_spec.js b/spec/frontend/vue_shared/components/panel_resizer_spec.js
index d8b903e5bfd..0e261124cbf 100644
--- a/spec/frontend/vue_shared/components/panel_resizer_spec.js
+++ b/spec/frontend/vue_shared/components/panel_resizer_spec.js
@@ -1,12 +1,10 @@
-import Vue from 'vue';
-import mountComponent from 'helpers/vue_mount_component_helper';
-import panelResizer from '~/vue_shared/components/panel_resizer.vue';
+import { mount } from '@vue/test-utils';
+import PanelResizer from '~/vue_shared/components/panel_resizer.vue';
describe('Panel Resizer component', () => {
- let vm;
- let PanelResizer;
+ let wrapper;
- const triggerEvent = (eventName, el = vm.$el, clientX = 0) => {
+ const triggerEvent = (eventName, el = wrapper.element, clientX = 0) => {
const event = document.createEvent('MouseEvents');
event.initMouseEvent(
eventName,
@@ -29,57 +27,64 @@ describe('Panel Resizer component', () => {
el.dispatchEvent(event);
};
- beforeEach(() => {
- PanelResizer = Vue.extend(panelResizer);
- });
-
afterEach(() => {
- vm.$destroy();
+ wrapper.destroy();
});
it('should render a div element with the correct classes and styles', () => {
- vm = mountComponent(PanelResizer, {
- startSize: 100,
- side: 'left',
+ wrapper = mount(PanelResizer, {
+ propsData: {
+ startSize: 100,
+ side: 'left',
+ },
});
- expect(vm.$el.tagName).toEqual('DIV');
- expect(vm.$el.getAttribute('class')).toBe(
- 'position-absolute position-top-0 position-bottom-0 drag-handle position-left-0',
- );
+ expect(wrapper.element.tagName).toEqual('DIV');
+ expect(wrapper.classes().sort()).toStrictEqual([
+ 'drag-handle',
+ 'position-absolute',
+ 'position-bottom-0',
+ 'position-left-0',
+ 'position-top-0',
+ ]);
- expect(vm.$el.getAttribute('style')).toBe('cursor: ew-resize;');
+ expect(wrapper.element.getAttribute('style')).toBe('cursor: ew-resize;');
});
it('should render a div element with the correct classes for a right side panel', () => {
- vm = mountComponent(PanelResizer, {
- startSize: 100,
- side: 'right',
+ wrapper = mount(PanelResizer, {
+ propsData: {
+ startSize: 100,
+ side: 'right',
+ },
});
- expect(vm.$el.tagName).toEqual('DIV');
- expect(vm.$el.getAttribute('class')).toBe(
- 'position-absolute position-top-0 position-bottom-0 drag-handle position-right-0',
- );
+ expect(wrapper.element.tagName).toEqual('DIV');
+ expect(wrapper.classes().sort()).toStrictEqual([
+ 'drag-handle',
+ 'position-absolute',
+ 'position-bottom-0',
+ 'position-right-0',
+ 'position-top-0',
+ ]);
});
it('drag the resizer', () => {
- vm = mountComponent(PanelResizer, {
- startSize: 100,
- side: 'left',
+ wrapper = mount(PanelResizer, {
+ propsData: {
+ startSize: 100,
+ side: 'left',
+ },
});
- jest.spyOn(vm, '$emit').mockImplementation(() => {});
- triggerEvent('mousedown', vm.$el);
+ triggerEvent('mousedown');
triggerEvent('mousemove', document);
triggerEvent('mouseup', document);
- expect(vm.$emit.mock.calls).toEqual([
- ['resize-start', 100],
- ['update:size', 100],
- ['resize-end', 100],
- ]);
-
- expect(vm.size).toBe(100);
+ expect(wrapper.emitted()).toEqual({
+ 'resize-start': [[100]],
+ 'update:size': [[100]],
+ 'resize-end': [[100]],
+ });
});
});
diff --git a/spec/frontend/vue_shared/components/registry/__snapshots__/history_item_spec.js.snap b/spec/frontend/vue_shared/components/registry/__snapshots__/history_item_spec.js.snap
index 2abae33bc19..66cf2354bc7 100644
--- a/spec/frontend/vue_shared/components/registry/__snapshots__/history_item_spec.js.snap
+++ b/spec/frontend/vue_shared/components/registry/__snapshots__/history_item_spec.js.snap
@@ -2,7 +2,7 @@
exports[`History Item renders the correct markup 1`] = `
<li
- class="timeline-entry system-note note-wrapper gl-mb-6!"
+ class="timeline-entry system-note note-wrapper"
>
<div
class="timeline-entry-inner"
@@ -22,11 +22,13 @@ exports[`History Item renders the correct markup 1`] = `
<div
class="note-header"
>
- <span>
+ <div
+ class="note-header-info"
+ >
<div
data-testid="default-slot"
/>
- </span>
+ </div>
</div>
<div
diff --git a/spec/frontend/vue_shared/components/security_reports/artifact_downloads/merge_request_artifact_download_spec.js b/spec/frontend/vue_shared/components/security_reports/artifact_downloads/merge_request_artifact_download_spec.js
index c5672bc28cc..09b0b3d43ad 100644
--- a/spec/frontend/vue_shared/components/security_reports/artifact_downloads/merge_request_artifact_download_spec.js
+++ b/spec/frontend/vue_shared/components/security_reports/artifact_downloads/merge_request_artifact_download_spec.js
@@ -6,7 +6,7 @@ import {
expectedDownloadDropdownPropsWithTitle,
securityReportMergeRequestDownloadPathsQueryResponse,
} from 'jest/vue_shared/security_reports/mock_data';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import Component from '~/vue_shared/security_reports/components/artifact_downloads/merge_request_artifact_download.vue';
import SecurityReportDownloadDropdown from '~/vue_shared/security_reports/components/security_report_download_dropdown.vue';
import {
@@ -93,8 +93,8 @@ describe('Merge request artifact Download', () => {
});
});
- it('calls createFlash correctly', () => {
- expect(createFlash).toHaveBeenCalledWith({
+ it('calls createAlert correctly', () => {
+ expect(createAlert).toHaveBeenCalledWith({
message: Component.i18n.apiError,
captureError: true,
error: expect.any(Error),
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/labels_select_root_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/labels_select_root_spec.js
index 4c7ac6e9a6f..30c1a4b7d2f 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/labels_select_root_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/labels_select_root_spec.js
@@ -67,9 +67,9 @@ describe('LabelsSelectRoot', () => {
// We're utilizing `onDropdownClose` event emitted from the component to always include `touchedLabels`
// while the first param of the method is the labels list which were added/removed.
- expect(wrapper.emitted('updateSelectedLabels')).toBeTruthy();
+ expect(wrapper.emitted('updateSelectedLabels')).toHaveLength(1);
expect(wrapper.emitted('updateSelectedLabels')[0]).toEqual([touchedLabels]);
- expect(wrapper.emitted('onDropdownClose')).toBeTruthy();
+ expect(wrapper.emitted('onDropdownClose')).toHaveLength(1);
expect(wrapper.emitted('onDropdownClose')[0]).toEqual([touchedLabels]);
});
@@ -88,7 +88,7 @@ describe('LabelsSelectRoot', () => {
},
);
- expect(wrapper.emitted('updateSelectedLabels')).toBeTruthy();
+ expect(wrapper.emitted('updateSelectedLabels')).toHaveLength(1);
expect(wrapper.emitted('updateSelectedLabels')[0]).toEqual([
[
{
@@ -97,7 +97,7 @@ describe('LabelsSelectRoot', () => {
},
],
]);
- expect(wrapper.emitted('onDropdownClose')).toBeTruthy();
+ expect(wrapper.emitted('onDropdownClose')).toHaveLength(1);
expect(wrapper.emitted('onDropdownClose')[0]).toEqual([[]]);
});
});
@@ -106,8 +106,7 @@ describe('LabelsSelectRoot', () => {
it('emits `toggleCollapse` event on component', () => {
createComponent();
wrapper.vm.handleCollapsedValueClick();
-
- expect(wrapper.emitted().toggleCollapse).toBeTruthy();
+ expect(wrapper.emitted().toggleCollapse).toHaveLength(1);
});
});
});
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 2bc513e87bf..edd044bd754 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/actions_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/actions_spec.js
@@ -1,7 +1,7 @@
import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import * as actions from '~/vue_shared/components/sidebar/labels_select_vue/store/actions';
import * as types from '~/vue_shared/components/sidebar/labels_select_vue/store/mutation_types';
@@ -102,7 +102,7 @@ describe('LabelsSelect Actions', () => {
it('shows flash error', () => {
actions.receiveLabelsFailure({ commit: () => {} });
- expect(createFlash).toHaveBeenCalledWith({ message: 'Error fetching labels.' });
+ expect(createAlert).toHaveBeenCalledWith({ message: 'Error fetching labels.' });
});
});
@@ -186,7 +186,7 @@ describe('LabelsSelect Actions', () => {
it('shows flash error', () => {
actions.receiveCreateLabelFailure({ commit: () => {} });
- expect(createFlash).toHaveBeenCalledWith({ message: 'Error creating label.' });
+ expect(createAlert).toHaveBeenCalledWith({ message: 'Error creating label.' });
});
});
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/mutations_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/mutations_spec.js
index 1819e750324..2b2508b5e11 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/mutations_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/mutations_spec.js
@@ -189,10 +189,20 @@ describe('LabelsSelect Mutations', () => {
});
labelGroupIds.forEach((l) => {
- expect(state.labels[l.id - 1].touched).toBeFalsy();
+ expect(state.labels[l.id - 1].touched).toBeUndefined();
expect(state.labels[l.id - 1].set).toBe(false);
});
});
+ it('allows selection of multiple scoped labels', () => {
+ const state = { labels: cloneDeep(labels), allowMultipleScopedLabels: true };
+
+ mutations[types.UPDATE_SELECTED_LABELS](state, { labels: [{ id: labels[4].id }] });
+ mutations[types.UPDATE_SELECTED_LABELS](state, { labels: [{ id: labels[5].id }] });
+
+ expect(state.labels[4].set).toBe(true);
+ expect(state.labels[5].set).toBe(true);
+ expect(state.labels[6].set).toBe(true);
+ });
});
describe(`${types.UPDATE_LABELS_SET_STATE}`, () => {
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view_spec.js
index 9c29f304c71..237f174e048 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view_spec.js
@@ -4,7 +4,7 @@ import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { workspaceLabelsQueries } from '~/sidebar/constants';
import DropdownContentsCreateView from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view.vue';
import createLabelMutation from '~/vue_shared/components/sidebar/labels_select_widget/graphql/create_label.mutation.graphql';
@@ -202,7 +202,7 @@ describe('DropdownContentsCreateView', () => {
});
});
- it('calls createFlash is mutation has a user-recoverable error', async () => {
+ it('calls createAlert is mutation has a user-recoverable error', async () => {
createComponent({ mutationHandler: createLabelUserRecoverableErrorHandler });
fillLabelAttributes();
await nextTick();
@@ -210,10 +210,10 @@ describe('DropdownContentsCreateView', () => {
findCreateButton().vm.$emit('click');
await waitForPromises();
- expect(createFlash).toHaveBeenCalled();
+ expect(createAlert).toHaveBeenCalled();
});
- it('calls createFlash is mutation was rejected', async () => {
+ it('calls createAlert is mutation was rejected', async () => {
createComponent({ mutationHandler: createLabelErrorHandler });
fillLabelAttributes();
await nextTick();
@@ -221,7 +221,7 @@ describe('DropdownContentsCreateView', () => {
findCreateButton().vm.$emit('click');
await waitForPromises();
- expect(createFlash).toHaveBeenCalled();
+ expect(createAlert).toHaveBeenCalled();
});
it('displays error in alert if label title is already taken', async () => {
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view_spec.js
index 7f6770e0bea..5d8ad5ddee5 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view_spec.js
@@ -9,7 +9,7 @@ import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
import { DropdownVariant } from '~/vue_shared/components/sidebar/labels_select_widget/constants';
import DropdownContentsLabelsView from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view.vue';
@@ -143,13 +143,13 @@ describe('DropdownContentsLabelsView', () => {
expect(findNoResultsMessage().isVisible()).toBe(true);
});
- it('calls `createFlash` when fetching labels failed', async () => {
+ it('calls `createAlert` when fetching labels failed', async () => {
createComponent({ queryHandler: jest.fn().mockRejectedValue('Houston, we have a problem!') });
await makeObserverAppear();
jest.advanceTimersByTime(DEFAULT_DEBOUNCE_AND_THROTTLE_MS);
await waitForPromises();
- expect(createFlash).toHaveBeenCalled();
+ expect(createAlert).toHaveBeenCalled();
});
it('emits an `input` event on label click', async () => {
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 cad401e0013..b58c44645d6 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
@@ -3,7 +3,7 @@ import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { IssuableType } from '~/issues/constants';
import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue';
import DropdownContents from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_contents.vue';
@@ -151,7 +151,7 @@ describe('LabelsSelectRoot', () => {
it('creates flash with error message when query is rejected', async () => {
createComponent({ queryHandler: errorQueryHandler });
await waitForPromises();
- expect(createFlash).toHaveBeenCalledWith({ message: 'Error fetching labels.' });
+ expect(createAlert).toHaveBeenCalledWith({ message: 'Error fetching labels.' });
});
});
@@ -197,7 +197,7 @@ describe('LabelsSelectRoot', () => {
findDropdownContents().vm.$emit('setLabels', [label]);
await waitForPromises();
- expect(createFlash).toHaveBeenCalledWith({
+ expect(createAlert).toHaveBeenCalledWith({
captureError: true,
error: expect.anything(),
message: 'An error occurred while updating labels.',
diff --git a/spec/frontend/vue_shared/components/source_viewer/components/chunk_line_spec.js b/spec/frontend/vue_shared/components/source_viewer/components/chunk_line_spec.js
index fd3ff9ce892..f661bd6747a 100644
--- a/spec/frontend/vue_shared/components/source_viewer/components/chunk_line_spec.js
+++ b/spec/frontend/vue_shared/components/source_viewer/components/chunk_line_spec.js
@@ -1,10 +1,5 @@
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,
@@ -31,7 +26,6 @@ describe('Chunk Line component', () => {
const findLineLink = () => wrapper.find('.file-line-num');
const findBlameLink = () => wrapper.find('.file-line-blame');
const findContent = () => wrapper.findByTestId('content');
- const findWrappedBidiChars = () => wrapper.findAllByTestId('bidi-wrapper');
beforeEach(() => {
createComponent();
@@ -40,22 +34,6 @@ describe('Chunk Line component', () => {
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 blame link', () => {
expect(findBlameLink().attributes()).toMatchObject({
href: `${DEFAULT_PROPS.blamePath}#L${DEFAULT_PROPS.number}`,
diff --git a/spec/frontend/vue_shared/components/source_viewer/highlight_util_spec.js b/spec/frontend/vue_shared/components/source_viewer/highlight_util_spec.js
new file mode 100644
index 00000000000..4a995e2fde1
--- /dev/null
+++ b/spec/frontend/vue_shared/components/source_viewer/highlight_util_spec.js
@@ -0,0 +1,44 @@
+import hljs from 'highlight.js/lib/core';
+import languageLoader from '~/content_editor/services/highlight_js_language_loader';
+import { registerPlugins } from '~/vue_shared/components/source_viewer/plugins/index';
+import { highlight } from '~/vue_shared/components/source_viewer/workers/highlight_utils';
+
+jest.mock('highlight.js/lib/core', () => ({
+ highlight: jest.fn().mockReturnValue({}),
+ registerLanguage: jest.fn(),
+}));
+
+jest.mock('~/content_editor/services/highlight_js_language_loader', () => ({
+ javascript: jest.fn().mockReturnValue({ default: jest.fn() }),
+}));
+
+jest.mock('~/vue_shared/components/source_viewer/plugins/index', () => ({
+ registerPlugins: jest.fn(),
+}));
+
+const fileType = 'text';
+const content = 'function test() { return true };';
+const language = 'javascript';
+
+describe('Highlight utility', () => {
+ beforeEach(() => highlight(fileType, content, language));
+
+ it('loads the language', () => {
+ expect(languageLoader.javascript).toHaveBeenCalled();
+ });
+
+ it('registers the plugins', () => {
+ expect(registerPlugins).toHaveBeenCalled();
+ });
+
+ it('registers the language', () => {
+ expect(hljs.registerLanguage).toHaveBeenCalledWith(
+ language,
+ languageLoader[language]().default,
+ );
+ });
+
+ it('highlights the content', () => {
+ expect(hljs.highlight).toHaveBeenCalledWith(content, { language });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/source_viewer/plugins/index_spec.js b/spec/frontend/vue_shared/components/source_viewer/plugins/index_spec.js
index 83fdc5d669d..57045ca54ae 100644
--- a/spec/frontend/vue_shared/components/source_viewer/plugins/index_spec.js
+++ b/spec/frontend/vue_shared/components/source_viewer/plugins/index_spec.js
@@ -1,14 +1,18 @@
-import { registerPlugins } from '~/vue_shared/components/source_viewer/plugins/index';
-import { HLJS_ON_AFTER_HIGHLIGHT } from '~/vue_shared/components/source_viewer/constants';
-import wrapComments from '~/vue_shared/components/source_viewer/plugins/wrap_comments';
+import {
+ registerPlugins,
+ HLJS_ON_AFTER_HIGHLIGHT,
+} from '~/vue_shared/components/source_viewer/plugins/index';
+import wrapChildNodes from '~/vue_shared/components/source_viewer/plugins/wrap_child_nodes';
+import wrapBidiChars from '~/vue_shared/components/source_viewer/plugins/wrap_bidi_chars';
-jest.mock('~/vue_shared/components/source_viewer/plugins/wrap_comments');
+jest.mock('~/vue_shared/components/source_viewer/plugins/wrap_child_nodes');
const hljsMock = { addPlugin: jest.fn() };
describe('Highlight.js plugin registration', () => {
beforeEach(() => registerPlugins(hljsMock));
it('registers our plugins', () => {
- expect(hljsMock.addPlugin).toHaveBeenCalledWith({ [HLJS_ON_AFTER_HIGHLIGHT]: wrapComments });
+ expect(hljsMock.addPlugin).toHaveBeenCalledWith({ [HLJS_ON_AFTER_HIGHLIGHT]: wrapBidiChars });
+ expect(hljsMock.addPlugin).toHaveBeenCalledWith({ [HLJS_ON_AFTER_HIGHLIGHT]: wrapChildNodes });
});
});
diff --git a/spec/frontend/vue_shared/components/source_viewer/plugins/utils/dependency_linker_util_spec.js b/spec/frontend/vue_shared/components/source_viewer/plugins/utils/dependency_linker_util_spec.js
index 8079d5ad99a..e4ce07ec668 100644
--- a/spec/frontend/vue_shared/components/source_viewer/plugins/utils/dependency_linker_util_spec.js
+++ b/spec/frontend/vue_shared/components/source_viewer/plugins/utils/dependency_linker_util_spec.js
@@ -15,7 +15,7 @@ describe('createLink', () => {
it('escapes the user-controlled content', () => {
const unescapedXSS = '<script>XSS</script>';
const escapedPackageName = '&lt;script&gt;XSS&lt;/script&gt;';
- const escapedHref = '&amp;lt;script&amp;gt;XSS&amp;lt;/script&amp;gt;';
+ const escapedHref = '&lt;script&gt;XSS&lt;/script&gt;';
const href = `http://test.com/${unescapedXSS}`;
const innerText = `testing${unescapedXSS}`;
const result = `<a href="http://test.com/${escapedHref}" rel="nofollow noreferrer noopener">testing${escapedPackageName}</a>`;
diff --git a/spec/frontend/vue_shared/components/source_viewer/plugins/wrap_bidi_chars_spec.js b/spec/frontend/vue_shared/components/source_viewer/plugins/wrap_bidi_chars_spec.js
new file mode 100644
index 00000000000..f40f8b22627
--- /dev/null
+++ b/spec/frontend/vue_shared/components/source_viewer/plugins/wrap_bidi_chars_spec.js
@@ -0,0 +1,17 @@
+import wrapBidiChars from '~/vue_shared/components/source_viewer/plugins/wrap_bidi_chars';
+import {
+ BIDI_CHARS,
+ BIDI_CHARS_CLASS_LIST,
+ BIDI_CHAR_TOOLTIP,
+} from '~/vue_shared/components/source_viewer/constants';
+
+describe('Highlight.js plugin for wrapping BiDi characters', () => {
+ it.each(BIDI_CHARS)('wraps %s BiDi char', (bidiChar) => {
+ const inputValue = `// some content ${bidiChar} with BiDi chars`;
+ const outputValue = `// some content <span class="${BIDI_CHARS_CLASS_LIST}" title="${BIDI_CHAR_TOOLTIP}">${bidiChar}</span>`;
+ const hljsResultMock = { value: inputValue };
+
+ wrapBidiChars(hljsResultMock);
+ expect(hljsResultMock.value).toContain(outputValue);
+ });
+});
diff --git a/spec/frontend/vue_shared/components/source_viewer/plugins/wrap_child_nodes_spec.js b/spec/frontend/vue_shared/components/source_viewer/plugins/wrap_child_nodes_spec.js
new file mode 100644
index 00000000000..bc6df1a2565
--- /dev/null
+++ b/spec/frontend/vue_shared/components/source_viewer/plugins/wrap_child_nodes_spec.js
@@ -0,0 +1,22 @@
+import wrapChildNodes from '~/vue_shared/components/source_viewer/plugins/wrap_child_nodes';
+
+describe('Highlight.js plugin for wrapping _emitter nodes', () => {
+ it('mutates the input value by wrapping each node in a span tag', () => {
+ const hljsResultMock = {
+ _emitter: {
+ rootNode: {
+ children: [
+ { kind: 'string', children: ['Text 1'] },
+ { kind: 'string', children: ['Text 2', { kind: 'comment', children: ['Text 3'] }] },
+ 'Text4\nText5',
+ ],
+ },
+ },
+ };
+
+ const outputValue = `<span class="hljs-string">Text 1</span><span class="hljs-string"><span class="hljs-string">Text 2</span><span class="hljs-comment">Text 3</span></span><span class="">Text4</span>\n<span class="">Text5</span>`;
+
+ wrapChildNodes(hljsResultMock);
+ expect(hljsResultMock.value).toBe(outputValue);
+ });
+});
diff --git a/spec/frontend/vue_shared/components/source_viewer/plugins/wrap_comments_spec.js b/spec/frontend/vue_shared/components/source_viewer/plugins/wrap_comments_spec.js
deleted file mode 100644
index 5fd4182da29..00000000000
--- a/spec/frontend/vue_shared/components/source_viewer/plugins/wrap_comments_spec.js
+++ /dev/null
@@ -1,29 +0,0 @@
-import { HLJS_COMMENT_SELECTOR } from '~/vue_shared/components/source_viewer/constants';
-import wrapComments from '~/vue_shared/components/source_viewer/plugins/wrap_comments';
-
-describe('Highlight.js plugin for wrapping comments', () => {
- it('mutates the input value by wrapping each line in a span tag', () => {
- const inputValue = `<span class="${HLJS_COMMENT_SELECTOR}">/* Line 1 \n* Line 2 \n*/</span>`;
- const outputValue = `<span class="${HLJS_COMMENT_SELECTOR}">/* Line 1 \n<span class="${HLJS_COMMENT_SELECTOR}">* Line 2 </span>\n<span class="${HLJS_COMMENT_SELECTOR}">*/</span>`;
- const hljsResultMock = { value: inputValue };
-
- wrapComments(hljsResultMock);
- expect(hljsResultMock.value).toBe(outputValue);
- });
-
- it('does not mutate the input value if the hljs comment selector is not present', () => {
- const inputValue = '<span class="hljs-keyword">const</span>';
- const hljsResultMock = { value: inputValue };
-
- wrapComments(hljsResultMock);
- expect(hljsResultMock.value).toBe(inputValue);
- });
-
- it('does not mutate the input value if the hljs comment line includes a closing tag', () => {
- const inputValue = `<span class="${HLJS_COMMENT_SELECTOR}">/* Line 1 </span> \n* Line 2 \n*/`;
- const hljsResultMock = { value: inputValue };
-
- wrapComments(hljsResultMock);
- expect(hljsResultMock.value).toBe(inputValue);
- });
-});
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 e020d9a557e..6d319b37b02 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
@@ -22,10 +22,10 @@ jest.mock('~/vue_shared/components/source_viewer/plugins/index');
Vue.use(VueRouter);
const router = new VueRouter();
-const generateContent = (content, totalLines = 1) => {
+const generateContent = (content, totalLines = 1, delimiter = '\n') => {
let generatedContent = '';
for (let i = 0; i < totalLines; i += 1) {
- generatedContent += `Line: ${i + 1} = ${content}\n`;
+ generatedContent += `Line: ${i + 1} = ${content}${delimiter}`;
}
return generatedContent;
};
@@ -38,7 +38,9 @@ describe('Source Viewer component', () => {
const mappedLanguage = ROUGE_TO_HLJS_LANGUAGE_MAP[language];
const chunk1 = generateContent('// Some source code 1', 70);
const chunk2 = generateContent('// Some source code 2', 70);
- const content = chunk1 + chunk2;
+ const chunk3 = generateContent('// Some source code 3', 70, '\r\n');
+ const chunk3Result = generateContent('// Some source code 3', 70, '\n');
+ const content = chunk1 + chunk2 + chunk3;
const path = 'some/path.js';
const blamePath = 'some/blame/path.js';
const fileType = 'javascript';
@@ -152,6 +154,19 @@ describe('Source Viewer component', () => {
startingFrom: 70,
});
});
+
+ it('renders the third chunk', async () => {
+ const thirdChunk = findChunks().at(2);
+
+ expect(thirdChunk.props('content')).toContain(chunk3Result.trim());
+
+ expect(chunk3Result).toEqual(chunk3.replace(/\r?\n/g, '\n'));
+
+ expect(thirdChunk.props()).toMatchObject({
+ totalLines: 70,
+ startingFrom: 140,
+ });
+ });
});
it('emits showBlobInteractionZones on the eventHub when chunk appears', () => {
diff --git a/spec/frontend/vue_shared/components/stacked_progress_bar_spec.js b/spec/frontend/vue_shared/components/stacked_progress_bar_spec.js
index c6f01efa71a..79b1f17afa0 100644
--- a/spec/frontend/vue_shared/components/stacked_progress_bar_spec.js
+++ b/spec/frontend/vue_shared/components/stacked_progress_bar_spec.js
@@ -1,121 +1,109 @@
-import Vue from 'vue';
-
-import mountComponent from 'helpers/vue_mount_component_helper';
-import stackedProgressBarComponent from '~/vue_shared/components/stacked_progress_bar.vue';
-
-const createComponent = (config) => {
- const Component = Vue.extend(stackedProgressBarComponent);
- const defaultConfig = {
- successLabel: 'Synced',
- failureLabel: 'Failed',
- neutralLabel: 'Out of sync',
- successCount: 25,
- failureCount: 10,
- totalCount: 5000,
- ...config,
- };
-
- return mountComponent(Component, defaultConfig);
-};
+import { mount } from '@vue/test-utils';
+import StackedProgressBarComponent from '~/vue_shared/components/stacked_progress_bar.vue';
describe('StackedProgressBarComponent', () => {
- let vm;
-
- beforeEach(() => {
- vm = createComponent();
- });
+ let wrapper;
+
+ const createComponent = (config) => {
+ const defaultConfig = {
+ successLabel: 'Synced',
+ failureLabel: 'Failed',
+ neutralLabel: 'Out of sync',
+ successCount: 25,
+ failureCount: 10,
+ totalCount: 5000,
+ ...config,
+ };
+
+ wrapper = mount(StackedProgressBarComponent, { propsData: defaultConfig });
+ };
afterEach(() => {
- vm.$destroy();
+ wrapper.destroy();
});
- const findSuccessBarText = (wrapper) =>
- wrapper.$el.querySelector('.status-green').innerText.trim();
- const findNeutralBarText = (wrapper) =>
- wrapper.$el.querySelector('.status-neutral').innerText.trim();
- const findFailureBarText = (wrapper) => wrapper.$el.querySelector('.status-red').innerText.trim();
- const findUnavailableBarText = (wrapper) =>
- wrapper.$el.querySelector('.status-unavailable').innerText.trim();
-
- describe('computed', () => {
- describe('neutralCount', () => {
- it('returns neutralCount based on totalCount, successCount and failureCount', () => {
- expect(vm.neutralCount).toBe(4965); // 5000 - 25 - 10
- });
- });
- });
+ const findSuccessBar = () => wrapper.find('.status-green');
+ const findNeutralBar = () => wrapper.find('.status-neutral');
+ const findFailureBar = () => wrapper.find('.status-red');
+ const findUnavailableBar = () => wrapper.find('.status-unavailable');
describe('template', () => {
it('renders container element', () => {
- expect(vm.$el.classList.contains('stacked-progress-bar')).toBeTruthy();
+ createComponent();
+
+ expect(wrapper.classes()).toContain('stacked-progress-bar');
});
it('renders empty state when count is unavailable', () => {
- const vmX = createComponent({ totalCount: 0, successCount: 0, failureCount: 0 });
+ createComponent({ totalCount: 0, successCount: 0, failureCount: 0 });
- expect(findUnavailableBarText(vmX)).not.toBeUndefined();
+ expect(findUnavailableBar()).not.toBeUndefined();
});
it('renders bar elements when count is available', () => {
- expect(findSuccessBarText(vm)).not.toBeUndefined();
- expect(findNeutralBarText(vm)).not.toBeUndefined();
- expect(findFailureBarText(vm)).not.toBeUndefined();
+ createComponent();
+
+ expect(findSuccessBar().exists()).toBe(true);
+ expect(findNeutralBar().exists()).toBe(true);
+ expect(findFailureBar().exists()).toBe(true);
});
describe('getPercent', () => {
it('returns correct percentages from provided count based on `totalCount`', () => {
- vm = createComponent({ totalCount: 100, successCount: 25, failureCount: 10 });
+ createComponent({ totalCount: 100, successCount: 25, failureCount: 10 });
- expect(findSuccessBarText(vm)).toBe('25%');
- expect(findNeutralBarText(vm)).toBe('65%');
- expect(findFailureBarText(vm)).toBe('10%');
+ expect(findSuccessBar().text()).toBe('25%');
+ expect(findNeutralBar().text()).toBe('65%');
+ expect(findFailureBar().text()).toBe('10%');
});
it('returns percentage with decimal place when decimal is greater than 1', () => {
- vm = createComponent({ successCount: 67 });
+ createComponent({ successCount: 67 });
- expect(findSuccessBarText(vm)).toBe('1.3%');
+ expect(findSuccessBar().text()).toBe('1.3%');
});
it('returns percentage as `< 1%` from provided count based on `totalCount` when evaluated value is less than 1', () => {
- vm = createComponent({ successCount: 10 });
+ createComponent({ successCount: 10 });
- expect(findSuccessBarText(vm)).toBe('< 1%');
+ expect(findSuccessBar().text()).toBe('< 1%');
});
it('returns not available if totalCount is falsy', () => {
- vm = createComponent({ totalCount: 0 });
+ createComponent({ totalCount: 0 });
- expect(findUnavailableBarText(vm)).toBe('Not available');
+ expect(findUnavailableBar().text()).toBe('Not available');
});
it('returns 99.9% when numbers are extreme decimals', () => {
- vm = createComponent({ totalCount: 1000000 });
+ createComponent({ totalCount: 1000000 });
- expect(findNeutralBarText(vm)).toBe('99.9%');
+ expect(findNeutralBar().text()).toBe('99.9%');
});
});
- describe('barStyle', () => {
- it('returns style string based on percentage provided', () => {
- expect(vm.barStyle(50)).toBe('width: 50%;');
+ describe('bar style', () => {
+ it('renders width based on percentage provided', () => {
+ createComponent({ totalCount: 100, successCount: 25 });
+
+ expect(findSuccessBar().element.style.width).toBe('25%');
});
});
- describe('getTooltip', () => {
+ describe('tooltip', () => {
describe('when hideTooltips is false', () => {
it('returns label string based on label and count provided', () => {
- expect(vm.getTooltip('Synced', 10)).toBe('Synced: 10');
+ createComponent({ successCount: 10, successLabel: 'Synced', hideTooltips: false });
+
+ expect(findSuccessBar().attributes('title')).toBe('Synced: 10');
});
});
describe('when hideTooltips is true', () => {
- beforeEach(() => {
- vm = createComponent({ hideTooltips: true });
- });
-
it('returns an empty string', () => {
- expect(vm.getTooltip('Synced', 10)).toBe('');
+ createComponent({ successCount: 10, successLabel: 'Synced', hideTooltips: true });
+
+ expect(findSuccessBar().attributes('title')).toBe('');
});
});
});
diff --git a/spec/frontend/vue_shared/components/timezone_dropdown/helpers.js b/spec/frontend/vue_shared/components/timezone_dropdown/helpers.js
new file mode 100644
index 00000000000..dee4c92add4
--- /dev/null
+++ b/spec/frontend/vue_shared/components/timezone_dropdown/helpers.js
@@ -0,0 +1,6 @@
+import timezoneDataFixture from 'test_fixtures/timezones/short.json';
+
+export { timezoneDataFixture };
+
+export const findTzByName = (identifier = '') =>
+ timezoneDataFixture.find(({ name }) => name.toLowerCase() === identifier.toLowerCase());
diff --git a/spec/frontend/deploy_freeze/components/timezone_dropdown_spec.js b/spec/frontend/vue_shared/components/timezone_dropdown/timezone_dropdown_spec.js
index 567d18f8b92..e5f56c63031 100644
--- a/spec/frontend/deploy_freeze/components/timezone_dropdown_spec.js
+++ b/spec/frontend/vue_shared/components/timezone_dropdown/timezone_dropdown_spec.js
@@ -1,27 +1,20 @@
import { GlDropdownItem, GlDropdown } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
-import Vue from 'vue';
-import Vuex from 'vuex';
-import createStore from '~/deploy_freeze/store';
-import TimezoneDropdown from '~/vue_shared/components/timezone_dropdown.vue';
-import { findTzByName, formatTz, timezoneDataFixture } from '../helpers';
-
-Vue.use(Vuex);
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import TimezoneDropdown from '~/vue_shared/components/timezone_dropdown/timezone_dropdown.vue';
+import { formatTimezone } from '~/lib/utils/datetime_utility';
+import { findTzByName, timezoneDataFixture } from './helpers';
describe('Deploy freeze timezone dropdown', () => {
let wrapper;
let store;
const createComponent = (searchTerm, selectedTimezone) => {
- store = createStore({
- projectId: '8',
- timezoneData: timezoneDataFixture,
- });
- wrapper = shallowMount(TimezoneDropdown, {
+ wrapper = shallowMountExtended(TimezoneDropdown, {
store,
propsData: {
value: selectedTimezone,
timezoneData: timezoneDataFixture,
+ name: 'user[timezone]',
},
});
@@ -32,6 +25,8 @@ describe('Deploy freeze timezone dropdown', () => {
const findAllDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
const findDropdownItemByIndex = (index) => wrapper.findAllComponents(GlDropdownItem).at(index);
+ const findEmptyResultsItem = () => wrapper.findByTestId('noMatchingResults');
+ const findHiddenInput = () => wrapper.find('input');
afterEach(() => {
wrapper.destroy();
@@ -66,11 +61,11 @@ describe('Deploy freeze timezone dropdown', () => {
it('renders only the time zone searched for', () => {
const selectedTz = findTzByName('Alaska');
expect(findAllDropdownItems()).toHaveLength(1);
- expect(findDropdownItemByIndex(0).text()).toBe(formatTz(selectedTz));
+ expect(findDropdownItemByIndex(0).text()).toBe(formatTimezone(selectedTz));
});
it('should not display empty results message', () => {
- expect(wrapper.find('[data-testid="noMatchingResults"]').exists()).toBe(false);
+ expect(findEmptyResultsItem().exists()).toBe(false);
});
describe('Custom events', () => {
@@ -81,7 +76,7 @@ describe('Deploy freeze timezone dropdown', () => {
expect(wrapper.emitted('input')).toEqual([
[
{
- formattedTimezone: formatTz(selectedTz),
+ formattedTimezone: formatTimezone(selectedTz),
identifier: selectedTz.identifier,
},
],
@@ -90,13 +85,27 @@ describe('Deploy freeze timezone dropdown', () => {
});
});
- describe('Selected time zone', () => {
+ describe('Selected time zone not found', () => {
+ beforeEach(() => {
+ createComponent('', 'Berlin');
+ });
+
+ it('renders empty selections', () => {
+ expect(wrapper.findComponent(GlDropdown).props().text).toBe('Select timezone');
+ });
+
+ it('preserves initial value in the associated input', () => {
+ expect(findHiddenInput().attributes('value')).toBe('Berlin');
+ });
+ });
+
+ describe('Selected time zone found', () => {
beforeEach(() => {
- createComponent('', 'Alaska');
+ createComponent('', 'Europe/Berlin');
});
it('renders selected time zone as dropdown label', () => {
- expect(wrapper.findComponent(GlDropdown).vm.text).toBe('Alaska');
+ expect(wrapper.findComponent(GlDropdown).props().text).toBe('[UTC + 2] Berlin');
});
});
});
diff --git a/spec/frontend/vue_shared/components/url_sync_spec.js b/spec/frontend/vue_shared/components/url_sync_spec.js
index aefe6a5c3e8..acda1a64a75 100644
--- a/spec/frontend/vue_shared/components/url_sync_spec.js
+++ b/spec/frontend/vue_shared/components/url_sync_spec.js
@@ -1,10 +1,11 @@
import { shallowMount } from '@vue/test-utils';
import { historyPushState } from '~/lib/utils/common_utils';
-import { mergeUrlParams } from '~/lib/utils/url_utility';
-import UrlSyncComponent from '~/vue_shared/components/url_sync.vue';
+import { mergeUrlParams, setUrlParams } from '~/lib/utils/url_utility';
+import UrlSyncComponent, { URL_SET_PARAMS_STRATEGY } from '~/vue_shared/components/url_sync.vue';
jest.mock('~/lib/utils/url_utility', () => ({
- mergeUrlParams: jest.fn((query, url) => `urlParams: ${query} ${url}`),
+ mergeUrlParams: jest.fn((query, url) => `urlParams: ${JSON.stringify(query)} ${url}`),
+ setUrlParams: jest.fn((query, url) => `urlParams: ${JSON.stringify(query)} ${url}`),
}));
jest.mock('~/lib/utils/common_utils', () => ({
@@ -17,9 +18,14 @@ describe('url sync component', () => {
const findButton = () => wrapper.find('button');
- const createComponent = ({ query = mockQuery, scopedSlots, slots } = {}) => {
+ const createComponent = ({
+ query = mockQuery,
+ scopedSlots,
+ slots,
+ urlParamsUpdateStrategy,
+ } = {}) => {
wrapper = shallowMount(UrlSyncComponent, {
- propsData: { query },
+ propsData: { query, ...(urlParamsUpdateStrategy && { urlParamsUpdateStrategy }) },
scopedSlots,
slots,
});
@@ -29,21 +35,39 @@ describe('url sync component', () => {
wrapper.destroy();
});
- const expectUrlSync = (query, times, mergeUrlParamsReturnValue) => {
- expect(mergeUrlParams).toHaveBeenCalledTimes(times);
- expect(mergeUrlParams).toHaveBeenCalledWith(query, window.location.href, {
- spreadArrays: true,
- });
+ const expectUrlSyncFactory = (
+ query,
+ times,
+ urlParamsUpdateStrategy,
+ urlOptions,
+ urlReturnValue,
+ ) => {
+ expect(urlParamsUpdateStrategy).toHaveBeenCalledTimes(times);
+ expect(urlParamsUpdateStrategy).toHaveBeenCalledWith(query, window.location.href, urlOptions);
expect(historyPushState).toHaveBeenCalledTimes(times);
- expect(historyPushState).toHaveBeenCalledWith(mergeUrlParamsReturnValue);
+ expect(historyPushState).toHaveBeenCalledWith(urlReturnValue);
+ };
+
+ const expectUrlSyncWithMergeUrlParams = (query, times, mergeUrlParamsReturnValue) => {
+ expectUrlSyncFactory(
+ query,
+ times,
+ mergeUrlParams,
+ { spreadArrays: true },
+ mergeUrlParamsReturnValue,
+ );
+ };
+
+ const expectUrlSyncWithSetUrlParams = (query, times, setUrlParamsReturnValue) => {
+ expectUrlSyncFactory(query, times, setUrlParams, true, setUrlParamsReturnValue);
};
describe('with query as a props', () => {
it('immediately syncs the query to the URL', () => {
createComponent();
- expectUrlSync(mockQuery, 1, mergeUrlParams.mock.results[0].value);
+ expectUrlSyncWithMergeUrlParams(mockQuery, 1, mergeUrlParams.mock.results[0].value);
});
describe('when the query is modified', () => {
@@ -54,11 +78,21 @@ describe('url sync component', () => {
// using setProps to test the watcher
await wrapper.setProps({ query: newQuery });
- expectUrlSync(mockQuery, 2, mergeUrlParams.mock.results[1].value);
+ expectUrlSyncWithMergeUrlParams(mockQuery, 2, mergeUrlParams.mock.results[1].value);
});
});
});
+ describe('with url-params-update-strategy equals to URL_SET_PARAMS_STRATEGY', () => {
+ it('uses setUrlParams to generate URL', () => {
+ createComponent({
+ urlParamsUpdateStrategy: URL_SET_PARAMS_STRATEGY,
+ });
+
+ expectUrlSyncWithSetUrlParams(mockQuery, 1, setUrlParams.mock.results[0].value);
+ });
+ });
+
describe('with scoped slot', () => {
const scopedSlots = {
default: `
@@ -77,7 +111,7 @@ describe('url sync component', () => {
findButton().trigger('click');
- expectUrlSync({ bar: 'baz' }, 1, mergeUrlParams.mock.results[0].value);
+ expectUrlSyncWithMergeUrlParams({ bar: 'baz' }, 1, mergeUrlParams.mock.results[0].value);
});
});
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
deleted file mode 100644
index f87737ca86a..00000000000
--- a/spec/frontend/vue_shared/components/user_avatar/user_avatar_image_new_spec.js
+++ /dev/null
@@ -1,134 +0,0 @@
-import { shallowMount } from '@vue/test-utils';
-import { GlAvatar, GlTooltip } from '@gitlab/ui';
-import defaultAvatarUrl from 'images/no_avatar.png';
-import { placeholderImage } from '~/lazy_loader';
-import UserAvatarImage from '~/vue_shared/components/user_avatar/user_avatar_image_new.vue';
-
-jest.mock('images/no_avatar.png', () => 'default-avatar-url');
-
-const PROVIDED_PROPS = {
- size: 32,
- imgSrc: 'myavatarurl.com',
- imgAlt: 'mydisplayname',
- cssClasses: 'myextraavatarclass',
- tooltipText: 'tooltip text',
- tooltipPlacement: 'bottom',
-};
-
-describe('User Avatar Image Component', () => {
- let wrapper;
-
- const findAvatar = () => wrapper.findComponent(GlAvatar);
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- describe('Initialization', () => {
- beforeEach(() => {
- wrapper = shallowMount(UserAvatarImage, {
- propsData: {
- ...PROVIDED_PROPS,
- },
- });
- });
-
- it('should render `GlAvatar` and provide correct properties to it', () => {
- expect(findAvatar().attributes('data-src')).toBe(
- `${PROVIDED_PROPS.imgSrc}?width=${PROVIDED_PROPS.size}`,
- );
- expect(findAvatar().props()).toMatchObject({
- src: `${PROVIDED_PROPS.imgSrc}?width=${PROVIDED_PROPS.size}`,
- alt: PROVIDED_PROPS.imgAlt,
- size: PROVIDED_PROPS.size,
- });
- });
-
- it('should add correct CSS classes', () => {
- const classes = wrapper.findComponent(GlAvatar).classes();
- expect(classes).toContain(PROVIDED_PROPS.cssClasses);
- expect(classes).not.toContain('lazy');
- });
- });
-
- describe('Initialization when lazy', () => {
- beforeEach(() => {
- wrapper = shallowMount(UserAvatarImage, {
- propsData: {
- ...PROVIDED_PROPS,
- lazy: true,
- },
- });
- });
-
- it('should add lazy attributes', () => {
- expect(findAvatar().classes()).toContain('lazy');
- expect(findAvatar().attributes()).toMatchObject({
- src: placeholderImage,
- 'data-src': `${PROVIDED_PROPS.imgSrc}?width=${PROVIDED_PROPS.size}`,
- });
- });
-
- it('should use maximum number when size is provided as an object', () => {
- wrapper = shallowMount(UserAvatarImage, {
- propsData: {
- ...PROVIDED_PROPS,
- size: { default: 16, md: 64, lg: 24 },
- lazy: true,
- },
- });
-
- expect(findAvatar().attributes('data-src')).toBe(`${PROVIDED_PROPS.imgSrc}?width=${64}`);
- });
- });
-
- describe('Initialization without src', () => {
- beforeEach(() => {
- wrapper = shallowMount(UserAvatarImage, {
- propsData: {
- ...PROVIDED_PROPS,
- imgSrc: null,
- },
- });
- });
-
- it('should have default avatar image', () => {
- expect(findAvatar().props('src')).toBe(`${defaultAvatarUrl}?width=${PROVIDED_PROPS.size}`);
- });
- });
-
- describe('Dynamic tooltip content', () => {
- const slots = {
- default: ['Action!'],
- };
-
- describe('when `tooltipText` is provided and no default slot', () => {
- beforeEach(() => {
- wrapper = shallowMount(UserAvatarImage, {
- propsData: { ...PROVIDED_PROPS },
- });
- });
-
- it('renders the tooltip with `tooltipText` as content', () => {
- expect(wrapper.findComponent(GlTooltip).text()).toBe(PROVIDED_PROPS.tooltipText);
- });
- });
-
- 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
deleted file mode 100644
index 2c1be6ec47e..00000000000
--- a/spec/frontend/vue_shared/components/user_avatar/user_avatar_image_old_spec.js
+++ /dev/null
@@ -1,127 +0,0 @@
-import { shallowMount } from '@vue/test-utils';
-import { GlTooltip } from '@gitlab/ui';
-import defaultAvatarUrl from 'images/no_avatar.png';
-import { placeholderImage } from '~/lazy_loader';
-import UserAvatarImage from '~/vue_shared/components/user_avatar/user_avatar_image_old.vue';
-
-jest.mock('images/no_avatar.png', () => 'default-avatar-url');
-
-const PROVIDED_PROPS = {
- size: 32,
- imgSrc: 'myavatarurl.com',
- imgAlt: 'mydisplayname',
- cssClasses: 'myextraavatarclass',
- tooltipText: 'tooltip text',
- tooltipPlacement: 'bottom',
-};
-
-const DEFAULT_PROPS = {
- size: 20,
-};
-
-describe('User Avatar Image Component', () => {
- let wrapper;
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- describe('Initialization', () => {
- beforeEach(() => {
- wrapper = shallowMount(UserAvatarImage, {
- propsData: {
- ...PROVIDED_PROPS,
- },
- });
- });
-
- it('should have <img> as a child element', () => {
- const imageElement = wrapper.find('img');
-
- expect(imageElement.exists()).toBe(true);
- expect(imageElement.attributes('src')).toBe(
- `${PROVIDED_PROPS.imgSrc}?width=${PROVIDED_PROPS.size}`,
- );
- expect(imageElement.attributes('data-src')).toBe(
- `${PROVIDED_PROPS.imgSrc}?width=${PROVIDED_PROPS.size}`,
- );
- expect(imageElement.attributes('alt')).toBe(PROVIDED_PROPS.imgAlt);
- });
-
- it('should properly render img css', () => {
- const classes = wrapper.find('img').classes();
- expect(classes).toEqual(['avatar', 's32', PROVIDED_PROPS.cssClasses]);
- expect(classes).not.toContain('lazy');
- });
- });
-
- describe('Initialization when lazy', () => {
- beforeEach(() => {
- wrapper = shallowMount(UserAvatarImage, {
- propsData: {
- ...PROVIDED_PROPS,
- lazy: true,
- },
- });
- });
-
- it('should add lazy attributes', () => {
- const imageElement = wrapper.find('img');
-
- expect(imageElement.classes()).toContain('lazy');
- expect(imageElement.attributes('src')).toBe(placeholderImage);
- expect(imageElement.attributes('data-src')).toBe(
- `${PROVIDED_PROPS.imgSrc}?width=${PROVIDED_PROPS.size}`,
- );
- });
- });
-
- describe('Initialization without src', () => {
- beforeEach(() => {
- wrapper = shallowMount(UserAvatarImage);
- });
-
- it('should have default avatar image', () => {
- const imageElement = wrapper.find('img');
-
- expect(imageElement.attributes('src')).toBe(
- `${defaultAvatarUrl}?width=${DEFAULT_PROPS.size}`,
- );
- });
- });
-
- describe('Dynamic tooltip content', () => {
- const slots = {
- default: ['Action!'],
- };
-
- describe('when `tooltipText` is provided and no default slot', () => {
- beforeEach(() => {
- wrapper = shallowMount(UserAvatarImage, {
- propsData: { ...PROVIDED_PROPS },
- });
- });
-
- it('renders the tooltip with `tooltipText` as content', () => {
- expect(wrapper.findComponent(GlTooltip).text()).toBe(PROVIDED_PROPS.tooltipText);
- });
- });
-
- 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_spec.js b/spec/frontend/vue_shared/components/user_avatar/user_avatar_image_spec.js
index 6ad2ef226c2..d63b13981ac 100644
--- a/spec/frontend/vue_shared/components/user_avatar/user_avatar_image_spec.js
+++ b/spec/frontend/vue_shared/components/user_avatar/user_avatar_image_spec.js
@@ -1,7 +1,10 @@
import { shallowMount } from '@vue/test-utils';
+import { GlAvatar, GlTooltip } from '@gitlab/ui';
+import defaultAvatarUrl from 'images/no_avatar.png';
+import { placeholderImage } from '~/lazy_loader';
import UserAvatarImage from '~/vue_shared/components/user_avatar/user_avatar_image.vue';
-import UserAvatarImageNew from '~/vue_shared/components/user_avatar/user_avatar_image_new.vue';
-import UserAvatarImageOld from '~/vue_shared/components/user_avatar/user_avatar_image_old.vue';
+
+jest.mock('images/no_avatar.png', () => 'default-avatar-url');
const PROVIDED_PROPS = {
size: 32,
@@ -15,37 +18,117 @@ const PROVIDED_PROPS = {
describe('User Avatar Image Component', () => {
let wrapper;
- const createWrapper = (props = {}, { glAvatarForAllUserAvatars } = {}) => {
- wrapper = shallowMount(UserAvatarImage, {
- propsData: {
- ...PROVIDED_PROPS,
- ...props,
- },
- provide: {
- glFeatures: {
- glAvatarForAllUserAvatars,
- },
- },
- });
- };
+ const findAvatar = () => wrapper.findComponent(GlAvatar);
afterEach(() => {
wrapper.destroy();
});
- describe.each([
- [false, true, true],
- [true, false, true],
- [true, true, true],
- [false, false, false],
- ])(
- 'when glAvatarForAllUserAvatars=%s and enforceGlAvatar=%s',
- (glAvatarForAllUserAvatars, enforceGlAvatar, isUsingNewVersion) => {
- it(`will render ${isUsingNewVersion ? 'new' : 'old'} version`, () => {
- createWrapper({ enforceGlAvatar }, { glAvatarForAllUserAvatars });
- expect(wrapper.findComponent(UserAvatarImageNew).exists()).toBe(isUsingNewVersion);
- expect(wrapper.findComponent(UserAvatarImageOld).exists()).toBe(!isUsingNewVersion);
- });
- },
- );
+ describe('Initialization', () => {
+ beforeEach(() => {
+ wrapper = shallowMount(UserAvatarImage, {
+ propsData: {
+ ...PROVIDED_PROPS,
+ },
+ });
+ });
+
+ it('should render `GlAvatar` and provide correct properties to it', () => {
+ expect(findAvatar().attributes('data-src')).toBe(
+ `${PROVIDED_PROPS.imgSrc}?width=${PROVIDED_PROPS.size}`,
+ );
+ expect(findAvatar().props()).toMatchObject({
+ src: `${PROVIDED_PROPS.imgSrc}?width=${PROVIDED_PROPS.size}`,
+ alt: PROVIDED_PROPS.imgAlt,
+ size: PROVIDED_PROPS.size,
+ });
+ });
+
+ it('should add correct CSS classes', () => {
+ const classes = wrapper.findComponent(GlAvatar).classes();
+ expect(classes).toContain(PROVIDED_PROPS.cssClasses);
+ expect(classes).not.toContain('lazy');
+ });
+ });
+
+ describe('Initialization when lazy', () => {
+ beforeEach(() => {
+ wrapper = shallowMount(UserAvatarImage, {
+ propsData: {
+ ...PROVIDED_PROPS,
+ lazy: true,
+ },
+ });
+ });
+
+ it('should add lazy attributes', () => {
+ expect(findAvatar().classes()).toContain('lazy');
+ expect(findAvatar().attributes()).toMatchObject({
+ src: placeholderImage,
+ 'data-src': `${PROVIDED_PROPS.imgSrc}?width=${PROVIDED_PROPS.size}`,
+ });
+ });
+
+ it('should use maximum number when size is provided as an object', () => {
+ wrapper = shallowMount(UserAvatarImage, {
+ propsData: {
+ ...PROVIDED_PROPS,
+ size: { default: 16, md: 64, lg: 24 },
+ lazy: true,
+ },
+ });
+
+ expect(findAvatar().attributes('data-src')).toBe(`${PROVIDED_PROPS.imgSrc}?width=${64}`);
+ });
+ });
+
+ describe('Initialization without src', () => {
+ beforeEach(() => {
+ wrapper = shallowMount(UserAvatarImage, {
+ propsData: {
+ ...PROVIDED_PROPS,
+ imgSrc: null,
+ },
+ });
+ });
+
+ it('should have default avatar image', () => {
+ expect(findAvatar().props('src')).toBe(`${defaultAvatarUrl}?width=${PROVIDED_PROPS.size}`);
+ });
+ });
+
+ describe('Dynamic tooltip content', () => {
+ const slots = {
+ default: ['Action!'],
+ };
+
+ describe('when `tooltipText` is provided and no default slot', () => {
+ beforeEach(() => {
+ wrapper = shallowMount(UserAvatarImage, {
+ propsData: { ...PROVIDED_PROPS },
+ });
+ });
+
+ it('renders the tooltip with `tooltipText` as content', () => {
+ expect(wrapper.findComponent(GlTooltip).text()).toBe(PROVIDED_PROPS.tooltipText);
+ });
+ });
+
+ 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_link_new_spec.js b/spec/frontend/vue_shared/components/user_avatar/user_avatar_link_new_spec.js
deleted file mode 100644
index f485a14cfea..00000000000
--- a/spec/frontend/vue_shared/components/user_avatar/user_avatar_link_new_spec.js
+++ /dev/null
@@ -1,103 +0,0 @@
-import { GlAvatarLink } from '@gitlab/ui';
-import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import { TEST_HOST } from 'spec/test_constants';
-import UserAvatarImage from '~/vue_shared/components/user_avatar/user_avatar_image.vue';
-import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link_new.vue';
-
-describe('User Avatar Link Component', () => {
- let wrapper;
-
- const findUserName = () => wrapper.findByTestId('user-avatar-link-username');
-
- const defaultProps = {
- linkHref: `${TEST_HOST}/myavatarurl.com`,
- imgSize: 32,
- imgSrc: `${TEST_HOST}/myavatarurl.com`,
- imgAlt: 'mydisplayname',
- imgCssClasses: 'myextraavatarclass',
- tooltipText: 'tooltip text',
- tooltipPlacement: 'bottom',
- username: 'username',
- };
-
- const createWrapper = (props, slots) => {
- wrapper = shallowMountExtended(UserAvatarLink, {
- propsData: {
- ...defaultProps,
- ...props,
- ...slots,
- },
- });
- };
-
- beforeEach(() => {
- createWrapper();
- });
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- it('should render GlLink with correct props', () => {
- const link = wrapper.findComponent(GlAvatarLink);
- expect(link.exists()).toBe(true);
- expect(link.attributes('href')).toBe(defaultProps.linkHref);
- });
-
- it('should render UserAvatarImage and provide correct props to it', () => {
- expect(wrapper.findComponent(UserAvatarImage).exists()).toBe(true);
- expect(wrapper.findComponent(UserAvatarImage).props()).toEqual({
- cssClasses: defaultProps.imgCssClasses,
- imgAlt: defaultProps.imgAlt,
- imgSrc: defaultProps.imgSrc,
- lazy: false,
- size: defaultProps.imgSize,
- tooltipPlacement: defaultProps.tooltipPlacement,
- tooltipText: '',
- enforceGlAvatar: false,
- });
- });
-
- describe('when username provided', () => {
- beforeEach(() => {
- createWrapper({ username: defaultProps.username });
- });
-
- it('should render provided username', () => {
- expect(findUserName().text()).toBe(defaultProps.username);
- });
-
- it('should provide the tooltip data for the username', () => {
- expect(findUserName().attributes()).toEqual(
- expect.objectContaining({
- title: defaultProps.tooltipText,
- 'tooltip-placement': defaultProps.tooltipPlacement,
- }),
- );
- });
- });
-
- describe('when username is NOT provided', () => {
- beforeEach(() => {
- createWrapper({ username: '' });
- });
-
- it('should NOT render username', () => {
- expect(findUserName().exists()).toBe(false);
- });
- });
-
- describe('avatar-badge slot', () => {
- const badge = '<span>User badge</span>';
-
- beforeEach(() => {
- createWrapper(defaultProps, {
- 'avatar-badge': badge,
- });
- });
-
- it('should render provided `avatar-badge` slot content', () => {
- expect(wrapper.html()).toContain(badge);
- });
- });
-});
diff --git a/spec/frontend/vue_shared/components/user_avatar/user_avatar_link_old_spec.js b/spec/frontend/vue_shared/components/user_avatar/user_avatar_link_old_spec.js
deleted file mode 100644
index cf7a1025dba..00000000000
--- a/spec/frontend/vue_shared/components/user_avatar/user_avatar_link_old_spec.js
+++ /dev/null
@@ -1,103 +0,0 @@
-import { GlLink } from '@gitlab/ui';
-import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import { TEST_HOST } from 'spec/test_constants';
-import UserAvatarImage from '~/vue_shared/components/user_avatar/user_avatar_image.vue';
-import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link_old.vue';
-
-describe('User Avatar Link Component', () => {
- let wrapper;
-
- const findUserName = () => wrapper.find('[data-testid="user-avatar-link-username"]');
-
- const defaultProps = {
- linkHref: `${TEST_HOST}/myavatarurl.com`,
- imgSize: 32,
- imgSrc: `${TEST_HOST}/myavatarurl.com`,
- imgAlt: 'mydisplayname',
- imgCssClasses: 'myextraavatarclass',
- tooltipText: 'tooltip text',
- tooltipPlacement: 'bottom',
- username: 'username',
- };
-
- const createWrapper = (props, slots) => {
- wrapper = shallowMountExtended(UserAvatarLink, {
- propsData: {
- ...defaultProps,
- ...props,
- ...slots,
- },
- });
- };
-
- beforeEach(() => {
- createWrapper();
- });
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- it('should render GlLink with correct props', () => {
- const link = wrapper.findComponent(GlLink);
- expect(link.exists()).toBe(true);
- expect(link.attributes('href')).toBe(defaultProps.linkHref);
- });
-
- it('should render UserAvatarImage and povide correct props to it', () => {
- expect(wrapper.findComponent(UserAvatarImage).exists()).toBe(true);
- expect(wrapper.findComponent(UserAvatarImage).props()).toEqual({
- cssClasses: defaultProps.imgCssClasses,
- imgAlt: defaultProps.imgAlt,
- imgSrc: defaultProps.imgSrc,
- lazy: false,
- size: defaultProps.imgSize,
- tooltipPlacement: defaultProps.tooltipPlacement,
- tooltipText: '',
- enforceGlAvatar: false,
- });
- });
-
- describe('when username provided', () => {
- beforeEach(() => {
- createWrapper({ username: defaultProps.username });
- });
-
- it('should render provided username', () => {
- expect(findUserName().text()).toBe(defaultProps.username);
- });
-
- it('should provide the tooltip data for the username', () => {
- expect(findUserName().attributes()).toEqual(
- expect.objectContaining({
- title: defaultProps.tooltipText,
- 'tooltip-placement': defaultProps.tooltipPlacement,
- }),
- );
- });
- });
-
- describe('when username is NOT provided', () => {
- beforeEach(() => {
- createWrapper({ username: '' });
- });
-
- it('should NOT render username', () => {
- expect(findUserName().exists()).toBe(false);
- });
- });
-
- describe('avatar-badge slot', () => {
- const badge = '<span>User badge</span>';
-
- beforeEach(() => {
- createWrapper(defaultProps, {
- 'avatar-badge': badge,
- });
- });
-
- it('should render provided `avatar-badge` slot content', () => {
- expect(wrapper.html()).toContain(badge);
- });
- });
-});
diff --git a/spec/frontend/vue_shared/components/user_avatar/user_avatar_link_spec.js b/spec/frontend/vue_shared/components/user_avatar/user_avatar_link_spec.js
index fd3f59008ec..df7ce449678 100644
--- a/spec/frontend/vue_shared/components/user_avatar/user_avatar_link_spec.js
+++ b/spec/frontend/vue_shared/components/user_avatar/user_avatar_link_spec.js
@@ -1,51 +1,102 @@
-import { shallowMount } from '@vue/test-utils';
+import { GlAvatarLink } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { TEST_HOST } from 'spec/test_constants';
+import UserAvatarImage from '~/vue_shared/components/user_avatar/user_avatar_image.vue';
import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
-import UserAvatarLinkNew from '~/vue_shared/components/user_avatar/user_avatar_link_new.vue';
-import UserAvatarLinkOld from '~/vue_shared/components/user_avatar/user_avatar_link_old.vue';
-
-const PROVIDED_PROPS = {
- size: 32,
- imgSrc: 'myavatarurl.com',
- imgAlt: 'mydisplayname',
- cssClasses: 'myextraavatarclass',
- tooltipText: 'tooltip text',
- tooltipPlacement: 'bottom',
-};
describe('User Avatar Link Component', () => {
let wrapper;
- const createWrapper = (props = {}, { glAvatarForAllUserAvatars } = {}) => {
- wrapper = shallowMount(UserAvatarLink, {
+ const findUserName = () => wrapper.findByTestId('user-avatar-link-username');
+
+ const defaultProps = {
+ linkHref: `${TEST_HOST}/myavatarurl.com`,
+ imgSize: 32,
+ imgSrc: `${TEST_HOST}/myavatarurl.com`,
+ imgAlt: 'mydisplayname',
+ imgCssClasses: 'myextraavatarclass',
+ tooltipText: 'tooltip text',
+ tooltipPlacement: 'bottom',
+ username: 'username',
+ };
+
+ const createWrapper = (props, slots) => {
+ wrapper = shallowMountExtended(UserAvatarLink, {
propsData: {
- ...PROVIDED_PROPS,
+ ...defaultProps,
...props,
- },
- provide: {
- glFeatures: {
- glAvatarForAllUserAvatars,
- },
+ ...slots,
},
});
};
+ beforeEach(() => {
+ createWrapper();
+ });
+
afterEach(() => {
wrapper.destroy();
});
- describe.each([
- [false, true, true],
- [true, false, true],
- [true, true, true],
- [false, false, false],
- ])(
- 'when glAvatarForAllUserAvatars=%s and enforceGlAvatar=%s',
- (glAvatarForAllUserAvatars, enforceGlAvatar, isUsingNewVersion) => {
- it(`will render ${isUsingNewVersion ? 'new' : 'old'} version`, () => {
- createWrapper({ enforceGlAvatar }, { glAvatarForAllUserAvatars });
- expect(wrapper.findComponent(UserAvatarLinkNew).exists()).toBe(isUsingNewVersion);
- expect(wrapper.findComponent(UserAvatarLinkOld).exists()).toBe(!isUsingNewVersion);
+ it('should render GlLink with correct props', () => {
+ const link = wrapper.findComponent(GlAvatarLink);
+ expect(link.exists()).toBe(true);
+ expect(link.attributes('href')).toBe(defaultProps.linkHref);
+ });
+
+ it('should render UserAvatarImage and provide correct props to it', () => {
+ expect(wrapper.findComponent(UserAvatarImage).exists()).toBe(true);
+ expect(wrapper.findComponent(UserAvatarImage).props()).toEqual({
+ cssClasses: defaultProps.imgCssClasses,
+ imgAlt: defaultProps.imgAlt,
+ imgSrc: defaultProps.imgSrc,
+ lazy: false,
+ size: defaultProps.imgSize,
+ tooltipPlacement: defaultProps.tooltipPlacement,
+ tooltipText: '',
+ });
+ });
+
+ describe('when username provided', () => {
+ beforeEach(() => {
+ createWrapper({ username: defaultProps.username });
+ });
+
+ it('should render provided username', () => {
+ expect(findUserName().text()).toBe(defaultProps.username);
+ });
+
+ it('should provide the tooltip data for the username', () => {
+ expect(findUserName().attributes()).toEqual(
+ expect.objectContaining({
+ title: defaultProps.tooltipText,
+ 'tooltip-placement': defaultProps.tooltipPlacement,
+ }),
+ );
+ });
+ });
+
+ describe('when username is NOT provided', () => {
+ beforeEach(() => {
+ createWrapper({ username: '' });
+ });
+
+ it('should NOT render username', () => {
+ expect(findUserName().exists()).toBe(false);
+ });
+ });
+
+ describe('avatar-badge slot', () => {
+ const badge = '<span>User badge</span>';
+
+ beforeEach(() => {
+ createWrapper(defaultProps, {
+ 'avatar-badge': badge,
});
- },
- );
+ });
+
+ it('should render provided `avatar-badge` slot content', () => {
+ expect(wrapper.html()).toContain(badge);
+ });
+ });
});
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 b9accbf0373..1ad6d043399 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,29 +153,4 @@ 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_popover/user_popover_spec.js b/spec/frontend/vue_shared/components/user_popover/user_popover_spec.js
index 6d48000beb0..f6316af6ad8 100644
--- a/spec/frontend/vue_shared/components/user_popover/user_popover_spec.js
+++ b/spec/frontend/vue_shared/components/user_popover/user_popover_spec.js
@@ -8,10 +8,12 @@ import {
I18N_USER_BLOCKED,
I18N_USER_LEARN,
I18N_USER_FOLLOW,
+ I18N_ERROR_FOLLOW,
I18N_USER_UNFOLLOW,
+ I18N_ERROR_UNFOLLOW,
} from '~/vue_shared/components/user_popover/constants';
import axios from '~/lib/utils/axios_utils';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { followUser, unfollowUser } from '~/api/user_api';
import { mockTracking } from 'helpers/tracking_helper';
@@ -239,6 +241,18 @@ describe('User Popover Component', () => {
expect(wrapper.html()).toContain('<gl-emoji data-name="basketball_player"');
});
+ it('should show only emoji', () => {
+ const user = {
+ ...DEFAULT_PROPS.user,
+ status: { emoji: 'basketball_player' },
+ };
+
+ createWrapper({ user });
+
+ expect(findUserStatus().exists()).toBe(true);
+ expect(wrapper.html()).toContain('<gl-emoji data-name="basketball_player"');
+ });
+
it('hides the div when status is null', () => {
const user = { ...DEFAULT_PROPS.user, status: null };
@@ -367,27 +381,49 @@ describe('User Popover Component', () => {
itTracksToggleFollowButtonClick('follow_from_user_popover');
describe('when an error occurs', () => {
- beforeEach(() => {
- followUser.mockRejectedValue({});
+ describe('api send error message', () => {
+ const mockedMessage = sprintf(I18N_ERROR_UNFOLLOW, { limit: 300 });
+ const apiResponse = { response: { data: { message: mockedMessage } } };
- findToggleFollowButton().trigger('click');
- });
+ beforeEach(() => {
+ followUser.mockRejectedValue(apiResponse);
+ findToggleFollowButton().trigger('click');
+ });
- it('shows an error message', async () => {
- await axios.waitForAll();
+ it('show an error message from api response', async () => {
+ await axios.waitForAll();
- expect(createFlash).toHaveBeenCalledWith({
- message: 'An error occurred while trying to follow this user, please try again.',
- error: {},
- captureError: true,
+ expect(createAlert).toHaveBeenCalledWith({
+ message: mockedMessage,
+ error: apiResponse,
+ captureError: true,
+ });
});
});
- it('emits no events', async () => {
- await axios.waitForAll();
+ describe('api did not send error message', () => {
+ beforeEach(() => {
+ followUser.mockRejectedValue({});
- expect(wrapper.emitted().follow).toBeUndefined();
- expect(wrapper.emitted().unfollow).toBeUndefined();
+ findToggleFollowButton().trigger('click');
+ });
+
+ it('shows an error message', async () => {
+ await axios.waitForAll();
+
+ expect(createAlert).toHaveBeenCalledWith({
+ message: I18N_ERROR_FOLLOW,
+ error: {},
+ captureError: true,
+ });
+ });
+
+ it('emits no events', async () => {
+ await axios.waitForAll();
+
+ expect(wrapper.emitted().follow).toBeUndefined();
+ expect(wrapper.emitted().unfollow).toBeUndefined();
+ });
});
});
});
@@ -425,8 +461,8 @@ describe('User Popover Component', () => {
});
it('shows an error message', () => {
- expect(createFlash).toHaveBeenCalledWith({
- message: 'An error occurred while trying to unfollow this user, please try again.',
+ expect(createAlert).toHaveBeenCalledWith({
+ message: I18N_ERROR_UNFOLLOW,
error: {},
captureError: true,
});
diff --git a/spec/frontend/vue_shared/directives/safe_html_spec.js b/spec/frontend/vue_shared/directives/safe_html_spec.js
new file mode 100644
index 00000000000..ba1de8e4596
--- /dev/null
+++ b/spec/frontend/vue_shared/directives/safe_html_spec.js
@@ -0,0 +1,116 @@
+import { shallowMount } from '@vue/test-utils';
+import safeHtml from '~/vue_shared/directives/safe_html';
+import { defaultConfig } from '~/lib/dompurify';
+/* eslint-disable no-script-url */
+const invalidProtocolUrls = [
+ 'javascript:alert(1)',
+ 'jAvascript:alert(1)',
+ 'data:text/html,<script>alert(1);</script>',
+ ' javascript:',
+ 'javascript :',
+];
+/* eslint-enable no-script-url */
+const validProtocolUrls = ['slack://open', 'x-devonthink-item://90909', 'x-devonthink-item:90909'];
+
+describe('safe html directive', () => {
+ let wrapper;
+
+ const createComponent = ({ template, html, config } = {}) => {
+ const defaultTemplate = `<div v-safe-html="rawHtml"></div>`;
+ const defaultHtml = 'hello <script>alert(1)</script>world';
+
+ const component = {
+ directives: {
+ safeHtml,
+ },
+ data() {
+ return {
+ rawHtml: html || defaultHtml,
+ config: config || {},
+ };
+ },
+ template: template || defaultTemplate,
+ };
+
+ wrapper = shallowMount(component);
+ };
+
+ describe('default', () => {
+ it('should remove the script tag', () => {
+ createComponent();
+
+ expect(wrapper.html()).toEqual('<div>hello world</div>');
+ });
+
+ it('should remove javascript hrefs', () => {
+ createComponent({ html: '<a href="javascript:prompt(1)">click here</a>' });
+
+ expect(wrapper.html()).toEqual('<div><a>click here</a></div>');
+ });
+
+ it('should remove any existing children', () => {
+ createComponent({
+ template: `<div v-safe-html="rawHtml">foo <i>bar</i></div>`,
+ });
+
+ expect(wrapper.html()).toEqual('<div>hello world</div>');
+ });
+
+ describe('with non-http links', () => {
+ it.each(validProtocolUrls)('should allow %s', (url) => {
+ createComponent({
+ html: `<a href="${url}">internal link</a>`,
+ });
+ expect(wrapper.html()).toContain(`<a href="${url}">internal link</a>`);
+ });
+
+ it.each(invalidProtocolUrls)('should not allow %s', (url) => {
+ createComponent({
+ html: `<a href="${url}">internal link</a>`,
+ });
+ expect(wrapper.html()).toContain(`<a>internal link</a>`);
+ });
+ });
+
+ describe('handles data attributes correctly', () => {
+ const allowedDataAttrs = ['data-safe', 'data-random'];
+
+ it.each(defaultConfig.FORBID_ATTR)('removes dangerous `%s` attribute', (attr) => {
+ const html = `<a ${attr}="true"></a>`;
+ createComponent({ html });
+
+ expect(wrapper.html()).not.toContain(html);
+ });
+
+ it.each(allowedDataAttrs)('does not remove allowed `%s` attribute', (attr) => {
+ const html = `<a ${attr}="true"></a>`;
+ createComponent({ html });
+
+ expect(wrapper.html()).toContain(html);
+ });
+ });
+ });
+
+ describe('advance config', () => {
+ const template = '<div v-safe-html:[config]="rawHtml"></div>';
+ it('should only allow <b> tags', () => {
+ createComponent({
+ template,
+ html: '<a href="javascript:prompt(1)"><b>click here</b></a>',
+ config: { ALLOWED_TAGS: ['b'] },
+ });
+
+ expect(wrapper.html()).toEqual('<div><b>click here</b></div>');
+ });
+
+ it('should strip all html tags', () => {
+ createComponent({
+ template,
+ html: '<a href="javascript:prompt(1)"><u>click here</u></a>',
+ config: { ALLOWED_TAGS: [] },
+ });
+
+ expect(wrapper.html()).toEqual('<div>click here</div>');
+ });
+ });
+});
diff --git a/spec/frontend/boards/components/__snapshots__/board_blocked_icon_spec.js.snap b/spec/frontend/vue_shared/issuable/__snapshots__/issuable_blocked_icon_spec.js.snap
index 34e4f996ff0..dd011b9d84e 100644
--- a/spec/frontend/boards/components/__snapshots__/board_blocked_icon_spec.js.snap
+++ b/spec/frontend/vue_shared/issuable/__snapshots__/issuable_blocked_icon_spec.js.snap
@@ -1,23 +1,23 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`BoardBlockedIcon on mouseenter on blocked icon with more than three blocking issues matches the snapshot 1`] = `
-"<div class=\\"gl-display-inline\\"><svg data-testid=\\"issue-blocked-icon\\" role=\\"img\\" aria-hidden=\\"true\\" class=\\"issue-blocked-icon gl-mr-2 gl-cursor-pointer gl-text-red-500 gl-icon s16\\" id=\\"blocked-icon-uniqueId\\">
+exports[`IssuableBlockedIcon on mouseenter on blocked icon with more than three blocking issues matches the snapshot 1`] = `
+"<div class=\\"gl-display-inline\\"><svg data-testid=\\"issuable-blocked-icon\\" role=\\"img\\" aria-hidden=\\"true\\" class=\\"issuable-blocked-icon gl-mr-2 gl-cursor-pointer gl-text-red-500 gl-icon s16\\" id=\\"blocked-icon-uniqueId\\">
<use href=\\"#issue-block\\"></use>
</svg>
<div class=\\"gl-popover\\">
- <ul class=\\"gl-list-style-none gl-p-0\\">
+ <ul class=\\"gl-list-style-none gl-p-0 gl-mb-0\\">
<li><a href=\\"http://gdk.test:3000/gitlab-org/my-project-1/-/issues/6\\" class=\\"gl-link gl-text-blue-500! gl-font-sm\\">my-project-1#6</a>
- <p data-testid=\\"issuable-title\\" class=\\"gl-mb-3 gl-display-block!\\">
+ <p data-testid=\\"issuable-title\\" class=\\"gl-display-block! gl-mb-3\\">
blocking issue title 1
</p>
</li>
<li><a href=\\"http://gdk.test:3000/gitlab-org/my-project-1/-/issues/5\\" class=\\"gl-link gl-text-blue-500! gl-font-sm\\">my-project-1#5</a>
- <p data-testid=\\"issuable-title\\" class=\\"gl-mb-3 gl-display-block!\\">
+ <p data-testid=\\"issuable-title\\" class=\\"gl-display-block! gl-mb-3\\">
blocking issue title 2 + blocking issue title 2 + blocking issue title 2 + bloc…
</p>
</li>
<li><a href=\\"http://gdk.test:3000/gitlab-org/my-project-1/-/issues/4\\" class=\\"gl-link gl-text-blue-500! gl-font-sm\\">my-project-1#4</a>
- <p data-testid=\\"issuable-title\\" class=\\"gl-mb-3 gl-display-block!\\">
+ <p data-testid=\\"issuable-title\\" class=\\"gl-display-block! gl-mb-0\\">
blocking issue title 3
</p>
</li>
diff --git a/spec/frontend/boards/components/board_blocked_icon_spec.js b/spec/frontend/vue_shared/issuable/issuable_blocked_icon_spec.js
index ffdc0a7cecc..d59cbce6633 100644
--- a/spec/frontend/boards/components/board_blocked_icon_spec.js
+++ b/spec/frontend/vue_shared/issuable/issuable_blocked_icon_spec.js
@@ -5,8 +5,9 @@ import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import BoardBlockedIcon from '~/boards/components/board_blocked_icon.vue';
-import { blockingIssuablesQueries, issuableTypes } from '~/boards/constants';
+import IssuableBlockedIcon from '~/vue_shared/components/issuable_blocked_icon/issuable_blocked_icon.vue';
+import { blockingIssuablesQueries } from '~/vue_shared/components/issuable_blocked_icon/constants';
+import { issuableTypes } from '~/boards/constants';
import { truncate } from '~/lib/utils/text_utility';
import {
mockIssue,
@@ -21,9 +22,9 @@ import {
mockBlockedIssue2,
mockBlockedEpic1,
mockBlockingEpicIssuablesResponse1,
-} from '../mock_data';
+} from '../../boards/mock_data';
-describe('BoardBlockedIcon', () => {
+describe('IssuableBlockedIcon', () => {
let wrapper;
let mockApollo;
@@ -64,7 +65,7 @@ describe('BoardBlockedIcon', () => {
Vue.use(VueApollo);
wrapper = extendedWrapper(
- mount(BoardBlockedIcon, {
+ mount(IssuableBlockedIcon, {
apolloProvider: mockApollo,
propsData: {
item: {
@@ -88,7 +89,7 @@ describe('BoardBlockedIcon', () => {
issuableType = issuableTypes.issue,
} = {}) => {
wrapper = extendedWrapper(
- shallowMount(BoardBlockedIcon, {
+ shallowMount(IssuableBlockedIcon, {
propsData: {
item: {
...mockIssuable,
diff --git a/spec/frontend/vue_shared/issuable/show/components/issuable_body_spec.js b/spec/frontend/vue_shared/issuable/show/components/issuable_body_spec.js
index 39a76a51191..6b20f0c77a3 100644
--- a/spec/frontend/vue_shared/issuable/show/components/issuable_body_spec.js
+++ b/spec/frontend/vue_shared/issuable/show/components/issuable_body_spec.js
@@ -138,7 +138,7 @@ describe('IssuableBody', () => {
wrapper.vm.handleTaskListUpdateSuccess(updatedIssuable);
- expect(wrapper.emitted('task-list-update-success')).toBeTruthy();
+ expect(wrapper.emitted('task-list-update-success')).toHaveLength(1);
expect(wrapper.emitted('task-list-update-success')[0]).toEqual([updatedIssuable]);
});
});
@@ -147,7 +147,7 @@ describe('IssuableBody', () => {
it('emits `task-list-update-failure` event on component', () => {
wrapper.vm.handleTaskListUpdateFailure();
- expect(wrapper.emitted('task-list-update-failure')).toBeTruthy();
+ expect(wrapper.emitted('task-list-update-failure')).toHaveLength(1);
});
});
});
@@ -202,7 +202,7 @@ describe('IssuableBody', () => {
issuableTitle.vm.$emit('edit-issuable');
- expect(wrapper.emitted('edit-issuable')).toBeTruthy();
+ expect(wrapper.emitted('edit-issuable')).toHaveLength(1);
});
it.each(['keydown-title', 'keydown-description'])(
@@ -227,7 +227,7 @@ describe('IssuableBody', () => {
issuableEditForm.vm.$emit(eventName, eventObj, issuableMeta);
- expect(wrapper.emitted(eventName)).toBeTruthy();
+ expect(wrapper.emitted(eventName)).toHaveLength(1);
expect(wrapper.emitted(eventName)[0]).toMatchObject([eventObj, issuableMeta]);
},
);
diff --git a/spec/frontend/vue_shared/security_reports/security_reports_app_spec.js b/spec/frontend/vue_shared/security_reports/security_reports_app_spec.js
index a9651cf8bac..43ff68e30b5 100644
--- a/spec/frontend/vue_shared/security_reports/security_reports_app_spec.js
+++ b/spec/frontend/vue_shared/security_reports/security_reports_app_spec.js
@@ -14,7 +14,7 @@ import {
sastDiffSuccessMock,
secretDetectionDiffSuccessMock,
} from 'jest/vue_shared/security_reports/mock_data';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import HelpIcon from '~/vue_shared/security_reports/components/help_icon.vue';
import SecurityReportDownloadDropdown from '~/vue_shared/security_reports/components/security_report_download_dropdown.vue';
@@ -135,8 +135,8 @@ describe('Security reports app', () => {
});
});
- it('calls createFlash correctly', () => {
- expect(createFlash).toHaveBeenCalledWith({
+ it('calls createAlert correctly', () => {
+ expect(createAlert).toHaveBeenCalledWith({
message: SecurityReportsApp.i18n.apiError,
captureError: true,
error: expect.any(Error),
diff --git a/spec/frontend/webhooks/components/form_url_app_spec.js b/spec/frontend/webhooks/components/form_url_app_spec.js
new file mode 100644
index 00000000000..16e0a3f549e
--- /dev/null
+++ b/spec/frontend/webhooks/components/form_url_app_spec.js
@@ -0,0 +1,142 @@
+import { nextTick } from 'vue';
+import { GlFormRadio, GlFormRadioGroup, GlLink } from '@gitlab/ui';
+
+import FormUrlApp from '~/webhooks/components/form_url_app.vue';
+import FormUrlMaskItem from '~/webhooks/components/form_url_mask_item.vue';
+
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+
+describe('FormUrlApp', () => {
+ let wrapper;
+
+ const createComponent = ({ props } = {}) => {
+ wrapper = shallowMountExtended(FormUrlApp, {
+ propsData: { ...props },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ const findAllRadioButtons = () => wrapper.findAllComponents(GlFormRadio);
+ const findRadioGroup = () => wrapper.findComponent(GlFormRadioGroup);
+ const findUrlMaskDisable = () => findAllRadioButtons().at(0);
+ const findUrlMaskEnable = () => findAllRadioButtons().at(1);
+ const findAllUrlMaskItems = () => wrapper.findAllComponents(FormUrlMaskItem);
+ const findAddItem = () => wrapper.findComponent(GlLink);
+ const findFormUrl = () => wrapper.findByTestId('form-url');
+ const findFormUrlPreview = () => wrapper.findByTestId('form-url-preview');
+ const findUrlMaskSection = () => wrapper.findByTestId('url-mask-section');
+
+ describe('template', () => {
+ it('renders radio buttons for URL masking', () => {
+ createComponent();
+
+ expect(findAllRadioButtons()).toHaveLength(2);
+ expect(findUrlMaskDisable().text()).toBe(FormUrlApp.i18n.radioFullUrlText);
+ expect(findUrlMaskEnable().text()).toBe(FormUrlApp.i18n.radioMaskUrlText);
+ });
+
+ it('does not render mask section', () => {
+ createComponent();
+
+ expect(findUrlMaskSection().exists()).toBe(false);
+ });
+
+ describe('on radio select', () => {
+ beforeEach(async () => {
+ createComponent();
+
+ findRadioGroup().vm.$emit('input', true);
+ await nextTick();
+ });
+
+ it('renders mask section', () => {
+ expect(findUrlMaskSection().exists()).toBe(true);
+ });
+
+ it('renders an empty mask item by default', () => {
+ expect(findAllUrlMaskItems()).toHaveLength(1);
+
+ const firstItem = findAllUrlMaskItems().at(0);
+ expect(firstItem.props('itemKey')).toBeNull();
+ expect(firstItem.props('itemValue')).toBeNull();
+ });
+ });
+
+ describe('with mask items', () => {
+ const mockItem1 = { key: 'key1', value: 'value1' };
+ const mockItem2 = { key: 'key2', value: 'value2' };
+
+ beforeEach(() => {
+ createComponent({
+ props: { initialUrlVariables: [mockItem1, mockItem2] },
+ });
+ });
+
+ it('renders masked URL preview', async () => {
+ const mockUrl = 'https://test.host/value1?secret=value2';
+
+ findFormUrl().vm.$emit('input', mockUrl);
+ await nextTick();
+
+ expect(findFormUrlPreview().attributes('value')).toBe(
+ 'https://test.host/{key1}?secret={key2}',
+ );
+ });
+
+ it('renders mask items correctly', () => {
+ expect(findAllUrlMaskItems()).toHaveLength(2);
+
+ const firstItem = findAllUrlMaskItems().at(0);
+ expect(firstItem.props('itemKey')).toBe(mockItem1.key);
+ expect(firstItem.props('itemValue')).toBe(mockItem1.value);
+
+ const secondItem = findAllUrlMaskItems().at(1);
+ expect(secondItem.props('itemKey')).toBe(mockItem2.key);
+ expect(secondItem.props('itemValue')).toBe(mockItem2.value);
+ });
+
+ describe('on mask item input', () => {
+ const mockInput = { index: 0, key: 'display', value: 'secret' };
+
+ it('updates mask item', async () => {
+ const firstItem = findAllUrlMaskItems().at(0);
+ firstItem.vm.$emit('input', mockInput);
+ await nextTick();
+
+ expect(firstItem.props('itemKey')).toBe(mockInput.key);
+ expect(firstItem.props('itemValue')).toBe(mockInput.value);
+ });
+ });
+
+ describe('when add item is clicked', () => {
+ it('adds mask item', async () => {
+ findAddItem().vm.$emit('click');
+ await nextTick();
+
+ expect(findAllUrlMaskItems()).toHaveLength(3);
+
+ const lastItem = findAllUrlMaskItems().at(-1);
+ expect(lastItem.props('itemKey')).toBeNull();
+ expect(lastItem.props('itemValue')).toBeNull();
+ });
+ });
+
+ describe('when remove item is clicked', () => {
+ it('removes the correct mask item', async () => {
+ const firstItem = findAllUrlMaskItems().at(0);
+ firstItem.vm.$emit('remove');
+ await nextTick();
+
+ expect(findAllUrlMaskItems()).toHaveLength(1);
+
+ const newFirstItem = findAllUrlMaskItems().at(0);
+ expect(newFirstItem.props('itemKey')).toBe(mockItem2.key);
+ expect(newFirstItem.props('itemValue')).toBe(mockItem2.value);
+ });
+ });
+ });
+ });
+});
diff --git a/spec/frontend/webhooks/components/form_url_mask_item_spec.js b/spec/frontend/webhooks/components/form_url_mask_item_spec.js
new file mode 100644
index 00000000000..ab028ef2997
--- /dev/null
+++ b/spec/frontend/webhooks/components/form_url_mask_item_spec.js
@@ -0,0 +1,100 @@
+import { nextTick } from 'vue';
+import { GlButton, GlFormInput } from '@gitlab/ui';
+
+import FormUrlMaskItem from '~/webhooks/components/form_url_mask_item.vue';
+
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+
+describe('FormUrlMaskItem', () => {
+ let wrapper;
+
+ const defaultProps = {
+ index: 0,
+ };
+ const mockKey = 'key';
+ const mockValue = 'value';
+ const mockInput = 'input';
+
+ const createComponent = ({ props } = {}) => {
+ wrapper = shallowMountExtended(FormUrlMaskItem, {
+ propsData: { ...defaultProps, ...props },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ const findMaskItemKey = () => wrapper.findByTestId('mask-item-key');
+ const findMaskItemValue = () => wrapper.findByTestId('mask-item-value');
+ const findRemoveButton = () => wrapper.findComponent(GlButton);
+
+ describe('template', () => {
+ it('renders input for key and value', () => {
+ createComponent();
+
+ const keyInput = findMaskItemKey();
+ expect(keyInput.attributes('label')).toBe(FormUrlMaskItem.i18n.keyLabel);
+ expect(keyInput.findComponent(GlFormInput).attributes('name')).toBe(
+ 'hook[url_variables][][key]',
+ );
+
+ const valueInput = findMaskItemValue();
+ expect(valueInput.attributes('label')).toBe(FormUrlMaskItem.i18n.valueLabel);
+ expect(valueInput.findComponent(GlFormInput).attributes('name')).toBe(
+ 'hook[url_variables][][value]',
+ );
+ });
+
+ describe('on key input', () => {
+ beforeEach(async () => {
+ createComponent({ props: { itemKey: mockKey, itemValue: mockValue } });
+
+ findMaskItemKey().findComponent(GlFormInput).vm.$emit('input', mockInput);
+ await nextTick();
+ });
+
+ it('emits input event', () => {
+ expect(wrapper.emitted('input')).toEqual([
+ [{ index: defaultProps.index, key: mockInput, value: mockValue }],
+ ]);
+ });
+ });
+
+ describe('on value input', () => {
+ beforeEach(async () => {
+ createComponent({ props: { itemKey: mockKey, itemValue: mockValue } });
+
+ findMaskItemValue().findComponent(GlFormInput).vm.$emit('input', mockInput);
+ await nextTick();
+ });
+
+ it('emits input event', () => {
+ expect(wrapper.emitted('input')).toEqual([
+ [{ index: defaultProps.index, key: mockKey, value: mockInput }],
+ ]);
+ });
+ });
+
+ it('renders remove button', () => {
+ createComponent();
+
+ expect(findRemoveButton().props('icon')).toBe('remove');
+ });
+
+ describe('when remove button is clicked', () => {
+ const mockIndex = 5;
+
+ beforeEach(async () => {
+ createComponent({ props: { index: mockIndex } });
+
+ findRemoveButton().vm.$emit('click');
+ await nextTick();
+ });
+
+ it('emits remove event', () => {
+ expect(wrapper.emitted('remove')).toEqual([[mockIndex]]);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/whats_new/components/app_spec.js b/spec/frontend/whats_new/components/app_spec.js
index de5a814d3e7..da95b51c0b1 100644
--- a/spec/frontend/whats_new/components/app_spec.js
+++ b/spec/frontend/whats_new/components/app_spec.js
@@ -54,7 +54,7 @@ describe('App', () => {
});
};
- const findInfiniteScroll = () => wrapper.find(GlInfiniteScroll);
+ const findInfiniteScroll = () => wrapper.findComponent(GlInfiniteScroll);
const setup = async () => {
document.body.dataset.page = 'test-page';
@@ -80,7 +80,7 @@ describe('App', () => {
setup();
});
- const getDrawer = () => wrapper.find(GlDrawer);
+ const getDrawer = () => wrapper.findComponent(GlDrawer);
const getBackdrop = () => wrapper.find('.whats-new-modal-backdrop');
it('contains a drawer', () => {
@@ -173,7 +173,7 @@ describe('App', () => {
value();
- expect(getDrawerBodyHeight).toHaveBeenCalledWith(wrapper.find(GlDrawer).element);
+ expect(getDrawerBodyHeight).toHaveBeenCalledWith(wrapper.findComponent(GlDrawer).element);
expect(actions.setDrawerBodyHeight).toHaveBeenCalledWith(
expect.any(Object),
diff --git a/spec/frontend/work_items/components/work_item_assignees_spec.js b/spec/frontend/work_items/components/work_item_assignees_spec.js
index 28231fad108..1b204b6fd60 100644
--- a/spec/frontend/work_items/components/work_item_assignees_spec.js
+++ b/spec/frontend/work_items/components/work_item_assignees_spec.js
@@ -157,6 +157,14 @@ describe('WorkItemAssignees component', () => {
expect(findTokenSelector().props('viewOnly')).toBe(true);
});
+ it('has a label', () => {
+ createComponent();
+
+ expect(findTokenSelector().props('ariaLabelledby')).toEqual(
+ findAssigneesTitle().attributes('id'),
+ );
+ });
+
describe('when clicking outside the token selector', () => {
function arrange(args) {
createComponent(args);
diff --git a/spec/frontend/work_items/components/work_item_description_spec.js b/spec/frontend/work_items/components/work_item_description_spec.js
index d3165d8dc26..0691fe25e0d 100644
--- a/spec/frontend/work_items/components/work_item_description_spec.js
+++ b/spec/frontend/work_items/components/work_item_description_spec.js
@@ -4,6 +4,7 @@ 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 EditedAt from '~/issues/show/components/edited.vue';
import { updateDraft } from '~/lib/utils/autosave';
import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
import MarkdownField from '~/vue_shared/components/markdown/field.vue';
@@ -35,6 +36,7 @@ describe('WorkItemDescription', () => {
const findEditButton = () => wrapper.find('[data-testid="edit-description"]');
const findMarkdownField = () => wrapper.findComponent(MarkdownField);
+ const findEditedAt = () => wrapper.findComponent(EditedAt);
const editDescription = (newText) => wrapper.find('textarea').setValue(newText);
@@ -44,9 +46,9 @@ describe('WorkItemDescription', () => {
const createComponent = async ({
mutationHandler = mutationSuccessHandler,
canUpdate = true,
+ workItemResponse = workItemResponseFactory({ canUpdate }),
isEditing = false,
} = {}) => {
- const workItemResponse = workItemResponseFactory({ canUpdate });
const workItemResponseHandler = jest.fn().mockResolvedValue(workItemResponse);
const { id } = workItemQueryResponse.data.workItem;
@@ -100,6 +102,33 @@ describe('WorkItemDescription', () => {
});
describe('editing description', () => {
+ it('shows edited by text', async () => {
+ const lastEditedAt = '2022-09-21T06:18:42Z';
+ const lastEditedBy = {
+ name: 'Administrator',
+ webPath: '/root',
+ };
+
+ await createComponent({
+ workItemResponse: workItemResponseFactory({
+ lastEditedAt,
+ lastEditedBy,
+ }),
+ });
+
+ expect(findEditedAt().props()).toEqual({
+ updatedAt: lastEditedAt,
+ updatedByName: lastEditedBy.name,
+ updatedByPath: lastEditedBy.webPath,
+ });
+ });
+
+ it('does not show edited by text', async () => {
+ await createComponent();
+
+ expect(findEditedAt().exists()).toBe(false);
+ });
+
it('cancels when clicking cancel', async () => {
await createComponent({
isEditing: 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
index b047e0dc8d7..aae61b11196 100644
--- a/spec/frontend/work_items/components/work_item_detail_spec.js
+++ b/spec/frontend/work_items/components/work_item_detail_spec.js
@@ -1,8 +1,14 @@
-import { GlAlert, GlBadge, GlLoadingIcon, GlSkeletonLoader, GlButton } from '@gitlab/ui';
+import {
+ GlAlert,
+ GlBadge,
+ GlLoadingIcon,
+ GlSkeletonLoader,
+ GlButton,
+ GlEmptyState,
+} from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
-import workItemWeightSubscription from 'ee_component/work_items/graphql/work_item_weight.subscription.graphql';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
@@ -14,11 +20,13 @@ import WorkItemState from '~/work_items/components/work_item_state.vue';
import WorkItemTitle from '~/work_items/components/work_item_title.vue';
import WorkItemAssignees from '~/work_items/components/work_item_assignees.vue';
import WorkItemLabels from '~/work_items/components/work_item_labels.vue';
+import WorkItemMilestone from '~/work_items/components/work_item_milestone.vue';
import WorkItemInformation from '~/work_items/components/work_item_information.vue';
import { i18n } from '~/work_items/constants';
import workItemQuery from '~/work_items/graphql/work_item.query.graphql';
import workItemDatesSubscription from '~/work_items/graphql/work_item_dates.subscription.graphql';
import workItemTitleSubscription from '~/work_items/graphql/work_item_title.subscription.graphql';
+import workItemAssigneesSubscription from '~/work_items/graphql/work_item_assignees.subscription.graphql';
import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
import updateWorkItemTaskMutation from '~/work_items/graphql/update_work_item_task.mutation.graphql';
import { temporaryConfig } from '~/graphql_shared/issuable_client';
@@ -28,7 +36,7 @@ import {
workItemDatesSubscriptionResponse,
workItemResponseFactory,
workItemTitleSubscriptionResponse,
- workItemWeightSubscriptionResponse,
+ workItemAssigneesSubscriptionResponse,
} from '../mock_data';
describe('WorkItemDetail component', () => {
@@ -46,9 +54,12 @@ describe('WorkItemDetail component', () => {
const successHandler = jest.fn().mockResolvedValue(workItemQueryResponse);
const datesSubscriptionHandler = jest.fn().mockResolvedValue(workItemDatesSubscriptionResponse);
const titleSubscriptionHandler = jest.fn().mockResolvedValue(workItemTitleSubscriptionResponse);
- const weightSubscriptionHandler = jest.fn().mockResolvedValue(workItemWeightSubscriptionResponse);
+ const assigneesSubscriptionHandler = jest
+ .fn()
+ .mockResolvedValue(workItemAssigneesSubscriptionResponse);
const findAlert = () => wrapper.findComponent(GlAlert);
+ const findEmptyState = () => wrapper.findComponent(GlEmptyState);
const findSkeleton = () => wrapper.findComponent(GlSkeletonLoader);
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findWorkItemActions = () => wrapper.findComponent(WorkItemActions);
@@ -58,6 +69,7 @@ describe('WorkItemDetail component', () => {
const findWorkItemDueDate = () => wrapper.findComponent(WorkItemDueDate);
const findWorkItemAssignees = () => wrapper.findComponent(WorkItemAssignees);
const findWorkItemLabels = () => wrapper.findComponent(WorkItemLabels);
+ const findWorkItemMilestone = () => wrapper.findComponent(WorkItemMilestone);
const findParent = () => wrapper.find('[data-testid="work-item-parent"]');
const findParentButton = () => findParent().findComponent(GlButton);
const findCloseButton = () => wrapper.find('[data-testid="work-item-close"]');
@@ -72,21 +84,18 @@ describe('WorkItemDetail component', () => {
handler = successHandler,
subscriptionHandler = titleSubscriptionHandler,
confidentialityMock = [updateWorkItemMutation, jest.fn()],
- workItemsMvc2Enabled = false,
- includeWidgets = false,
error = undefined,
+ includeWidgets = false,
+ workItemsMvc2Enabled = false,
} = {}) => {
const handlers = [
[workItemQuery, handler],
[workItemTitleSubscription, subscriptionHandler],
[workItemDatesSubscription, datesSubscriptionHandler],
+ [workItemAssigneesSubscription, assigneesSubscriptionHandler],
confidentialityMock,
];
- if (IS_EE) {
- handlers.push([workItemWeightSubscription, weightSubscriptionHandler]);
- }
-
wrapper = shallowMount(WorkItemDetail, {
apolloProvider: createMockApollo(
handlers,
@@ -107,6 +116,12 @@ describe('WorkItemDetail component', () => {
workItemsMvc2: workItemsMvc2Enabled,
},
hasIssueWeightsFeature: true,
+ hasIterationsFeature: true,
+ projectNamespace: 'namespace',
+ },
+ stubs: {
+ WorkItemWeight: true,
+ WorkItemIteration: true,
},
});
};
@@ -384,13 +399,14 @@ describe('WorkItemDetail component', () => {
});
});
- it('shows an error message when the work item query was unsuccessful', async () => {
+ it('shows empty state with 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);
+ expect(findEmptyState().props('description')).toBe(i18n.fetchError);
+ expect(findWorkItemTitle().exists()).toBe(false);
});
it('shows an error message when WorkItemTitle emits an `error` event', async () => {
@@ -413,6 +429,30 @@ describe('WorkItemDetail component', () => {
});
});
+ describe('assignees subscription', () => {
+ describe('when the assignees widget exists', () => {
+ it('calls the assignees subscription', async () => {
+ createComponent();
+ await waitForPromises();
+
+ expect(assigneesSubscriptionHandler).toHaveBeenCalledWith({
+ issuableId: workItemQueryResponse.data.workItem.id,
+ });
+ });
+ });
+
+ describe('when the assignees widget does not exist', () => {
+ it('does not call the assignees subscription', async () => {
+ const response = workItemResponseFactory({ assigneesWidgetPresent: false });
+ const handler = jest.fn().mockResolvedValue(response);
+ createComponent({ handler });
+ await waitForPromises();
+
+ expect(assigneesSubscriptionHandler).not.toHaveBeenCalled();
+ });
+ });
+ });
+
describe('dates subscription', () => {
describe('when the due date widget exists', () => {
it('calls the dates subscription', async () => {
@@ -429,7 +469,7 @@ describe('WorkItemDetail component', () => {
it('does not call the dates subscription', async () => {
const response = workItemResponseFactory({ datesWidgetPresent: false });
const handler = jest.fn().mockResolvedValue(response);
- createComponent({ handler, workItemsMvc2Enabled: true });
+ createComponent({ handler });
await waitForPromises();
expect(datesSubscriptionHandler).not.toHaveBeenCalled();
@@ -440,9 +480,7 @@ describe('WorkItemDetail component', () => {
describe('assignees widget', () => {
it('renders assignees component when widget is returned from the API', async () => {
- createComponent({
- workItemsMvc2Enabled: true,
- });
+ createComponent();
await waitForPromises();
expect(findWorkItemAssignees().exists()).toBe(true);
@@ -450,7 +488,6 @@ describe('WorkItemDetail component', () => {
it('does not render assignees component when widget is not returned from the API', async () => {
createComponent({
- workItemsMvc2Enabled: true,
handler: jest
.fn()
.mockResolvedValue(workItemResponseFactory({ assigneesWidgetPresent: false })),
@@ -463,11 +500,13 @@ describe('WorkItemDetail component', () => {
describe('labels widget', () => {
it.each`
- description | includeWidgets | exists
- ${'renders when widget is returned from API'} | ${true} | ${true}
- ${'does not render when widget is not returned from API'} | ${false} | ${false}
- `('$description', async ({ includeWidgets, exists }) => {
- createComponent({ includeWidgets, workItemsMvc2Enabled: true });
+ description | labelsWidgetPresent | exists
+ ${'renders when widget is returned from API'} | ${true} | ${true}
+ ${'does not render when widget is not returned from API'} | ${false} | ${false}
+ `('$description', async ({ labelsWidgetPresent, exists }) => {
+ const response = workItemResponseFactory({ labelsWidgetPresent });
+ const handler = jest.fn().mockResolvedValue(response);
+ createComponent({ handler });
await waitForPromises();
expect(findWorkItemLabels().exists()).toBe(exists);
@@ -483,7 +522,7 @@ describe('WorkItemDetail component', () => {
it(`${datesWidgetPresent ? 'renders' : 'does not render'} due date component`, async () => {
const response = workItemResponseFactory({ datesWidgetPresent });
const handler = jest.fn().mockResolvedValue(response);
- createComponent({ handler, workItemsMvc2Enabled: true });
+ createComponent({ handler });
await waitForPromises();
expect(findWorkItemDueDate().exists()).toBe(exists);
@@ -491,7 +530,7 @@ describe('WorkItemDetail component', () => {
});
it('shows an error message when it emits an `error` event', async () => {
- createComponent({ workItemsMvc2Enabled: true });
+ createComponent();
await waitForPromises();
const updateError = 'Failed to update';
@@ -502,6 +541,19 @@ describe('WorkItemDetail component', () => {
});
});
+ describe('milestone widget', () => {
+ it.each`
+ description | includeWidgets | exists
+ ${'renders when widget is returned from API'} | ${true} | ${true}
+ ${'does not render when widget is not returned from API'} | ${false} | ${false}
+ `('$description', async ({ includeWidgets, exists }) => {
+ createComponent({ includeWidgets, workItemsMvc2Enabled: true });
+ await waitForPromises();
+
+ expect(findWorkItemMilestone().exists()).toBe(exists);
+ });
+ });
+
describe('work item information', () => {
beforeEach(() => {
createComponent();
diff --git a/spec/frontend/work_items/components/work_item_due_date_spec.js b/spec/frontend/work_items/components/work_item_due_date_spec.js
index 1d76154a1f0..701406b9588 100644
--- a/spec/frontend/work_items/components/work_item_due_date_spec.js
+++ b/spec/frontend/work_items/components/work_item_due_date_spec.js
@@ -62,7 +62,7 @@ describe('WorkItemDueDate component', () => {
createComponent({ canUpdate: true, startDate });
});
- it(exists ? 'renders' : 'does not render', () => {
+ it(`${exists ? 'renders' : 'does not render'}`, () => {
expect(findStartDateButton().exists()).toBe(exists);
});
});
@@ -172,7 +172,7 @@ describe('WorkItemDueDate component', () => {
createComponent({ canUpdate: true, dueDate });
});
- it(exists ? 'renders' : 'does not render', () => {
+ it(`${exists ? 'renders' : 'does not render'}`, () => {
expect(findDueDateButton().exists()).toBe(exists);
});
});
diff --git a/spec/frontend/work_items/components/work_item_labels_spec.js b/spec/frontend/work_items/components/work_item_labels_spec.js
index 1d976897c15..e6ff7e8502d 100644
--- a/spec/frontend/work_items/components/work_item_labels_spec.js
+++ b/spec/frontend/work_items/components/work_item_labels_spec.js
@@ -7,10 +7,18 @@ import { mountExtended } from 'helpers/vue_test_utils_helper';
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
import labelSearchQuery from '~/vue_shared/components/sidebar/labels_select_widget/graphql/project_labels.query.graphql';
import workItemQuery from '~/work_items/graphql/work_item.query.graphql';
+import workItemLabelsSubscription from 'ee_else_ce/work_items/graphql/work_item_labels.subscription.graphql';
+import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
import WorkItemLabels from '~/work_items/components/work_item_labels.vue';
-import { i18n } from '~/work_items/constants';
-import { temporaryConfig, resolvers } from '~/graphql_shared/issuable_client';
-import { projectLabelsResponse, mockLabels, workItemQueryResponse } from '../mock_data';
+import { i18n, I18N_WORK_ITEM_ERROR_FETCHING_LABELS } from '~/work_items/constants';
+import {
+ projectLabelsResponse,
+ mockLabels,
+ workItemQueryResponse,
+ workItemResponseFactory,
+ updateWorkItemMutationResponse,
+ workItemLabelsSubscriptionResponse,
+} from '../mock_data';
Vue.use(VueApollo);
@@ -21,32 +29,32 @@ describe('WorkItemLabels component', () => {
const findTokenSelector = () => wrapper.findComponent(GlTokenSelector);
const findSkeletonLoader = () => wrapper.findComponent(GlSkeletonLoader);
-
const findEmptyState = () => wrapper.findByTestId('empty-state');
+ const findLabelsTitle = () => wrapper.findByTestId('labels-title');
+ const workItemQuerySuccess = jest.fn().mockResolvedValue(workItemQueryResponse);
const successSearchQueryHandler = jest.fn().mockResolvedValue(projectLabelsResponse);
+ const successUpdateWorkItemMutationHandler = jest
+ .fn()
+ .mockResolvedValue(updateWorkItemMutationResponse);
+ const subscriptionHandler = jest.fn().mockResolvedValue(workItemLabelsSubscriptionResponse);
const errorHandler = jest.fn().mockRejectedValue('Houston, we have a problem');
const createComponent = ({
- labels = mockLabels,
canUpdate = true,
+ workItemQueryHandler = workItemQuerySuccess,
searchQueryHandler = successSearchQueryHandler,
+ updateWorkItemMutationHandler = successUpdateWorkItemMutationHandler,
} = {}) => {
- const apolloProvider = createMockApollo([[labelSearchQuery, searchQueryHandler]], resolvers, {
- typePolicies: temporaryConfig.cacheConfig.typePolicies,
- });
-
- apolloProvider.clients.defaultClient.writeQuery({
- query: workItemQuery,
- variables: {
- id: workItemId,
- },
- data: workItemQueryResponse.data,
- });
+ const apolloProvider = createMockApollo([
+ [workItemQuery, workItemQueryHandler],
+ [labelSearchQuery, searchQueryHandler],
+ [updateWorkItemMutation, updateWorkItemMutationHandler],
+ [workItemLabelsSubscription, subscriptionHandler],
+ ]);
wrapper = mountExtended(WorkItemLabels, {
propsData: {
- labels,
workItemId,
canUpdate,
fullPath: 'test-project-path',
@@ -60,6 +68,12 @@ describe('WorkItemLabels component', () => {
wrapper.destroy();
});
+ it('has a label', () => {
+ createComponent();
+
+ expect(findTokenSelector().props('ariaLabelledby')).toEqual(findLabelsTitle().attributes('id'));
+ });
+
it('focuses token selector on token selector input event', async () => {
createComponent();
findTokenSelector().vm.$emit('input', [mockLabels[0]]);
@@ -151,7 +165,7 @@ describe('WorkItemLabels component', () => {
findTokenSelector().vm.$emit('focus');
await waitForPromises();
- expect(wrapper.emitted('error')).toEqual([[i18n.fetchError]]);
+ expect(wrapper.emitted('error')).toEqual([[I18N_WORK_ITEM_ERROR_FETCHING_LABELS]]);
});
it('should search for with correct key after text input', async () => {
@@ -163,7 +177,53 @@ describe('WorkItemLabels component', () => {
await waitForPromises();
expect(successSearchQueryHandler).toHaveBeenCalledWith(
- expect.objectContaining({ search: searchKey }),
+ expect.objectContaining({ searchTerm: searchKey }),
);
});
+
+ describe('when clicking outside the token selector', () => {
+ it('calls a mutation with correct variables', () => {
+ createComponent();
+
+ findTokenSelector().vm.$emit('input', [mockLabels[0]]);
+ findTokenSelector().vm.$emit('blur', new FocusEvent({ relatedTarget: null }));
+
+ expect(successUpdateWorkItemMutationHandler).toHaveBeenCalledWith({
+ input: {
+ labelsWidget: { addLabelIds: [mockLabels[0].id], removeLabelIds: [] },
+ id: 'gid://gitlab/WorkItem/1',
+ },
+ });
+ });
+
+ it('emits an error and resets labels if mutation was rejected', async () => {
+ const workItemQueryHandler = jest.fn().mockResolvedValue(workItemResponseFactory());
+
+ createComponent({ updateWorkItemMutationHandler: errorHandler, workItemQueryHandler });
+
+ await waitForPromises();
+
+ const initialLabels = findTokenSelector().props('selectedTokens');
+
+ findTokenSelector().vm.$emit('input', [mockLabels[0]]);
+ findTokenSelector().vm.$emit('blur', new FocusEvent({ relatedTarget: null }));
+
+ await waitForPromises();
+
+ const updatedLabels = findTokenSelector().props('selectedTokens');
+
+ expect(wrapper.emitted('error')).toEqual([[i18n.updateError]]);
+ expect(updatedLabels).toEqual(initialLabels);
+ });
+
+ it('has a subscription', async () => {
+ createComponent();
+
+ await waitForPromises();
+
+ expect(subscriptionHandler).toHaveBeenCalledWith({
+ issuableId: workItemId,
+ });
+ });
+ });
});
diff --git a/spec/frontend/work_items/components/work_item_links/work_item_links_form_spec.js b/spec/frontend/work_items/components/work_item_links/work_item_links_form_spec.js
index 434c1db8a2c..ab3ea623e3e 100644
--- a/spec/frontend/work_items/components/work_item_links/work_item_links_form_spec.js
+++ b/spec/frontend/work_items/components/work_item_links/work_item_links_form_spec.js
@@ -28,6 +28,7 @@ describe('WorkItemLinksForm', () => {
listResponse = availableWorkItemsResponse,
typesResponse = projectWorkItemTypesQueryResponse,
parentConfidential = false,
+ hasIterationsFeature = false,
} = {}) => {
wrapper = shallowMountExtended(WorkItemLinksForm, {
apolloProvider: createMockApollo([
@@ -39,6 +40,7 @@ describe('WorkItemLinksForm', () => {
propsData: { issuableGid: 'gid://gitlab/WorkItem/1', parentConfidential },
provide: {
projectPath: 'project/path',
+ hasIterationsFeature,
},
});
diff --git a/spec/frontend/work_items/components/work_item_links/work_item_links_menu_spec.js b/spec/frontend/work_items/components/work_item_links/work_item_links_menu_spec.js
index 287ec022d3f..e3f3b74f296 100644
--- a/spec/frontend/work_items/components/work_item_links/work_item_links_menu_spec.js
+++ b/spec/frontend/work_items/components/work_item_links/work_item_links_menu_spec.js
@@ -10,8 +10,8 @@ describe('WorkItemLinksMenu', () => {
wrapper = shallowMountExtended(WorkItemLinksMenu);
};
- const findDropdown = () => wrapper.find(GlDropdown);
- const findRemoveDropdownItem = () => wrapper.find(GlDropdownItem);
+ const findDropdown = () => wrapper.findComponent(GlDropdown);
+ const findRemoveDropdownItem = () => wrapper.findComponent(GlDropdownItem);
beforeEach(async () => {
createComponent();
diff --git a/spec/frontend/work_items/components/work_item_links/work_item_links_spec.js b/spec/frontend/work_items/components/work_item_links/work_item_links_spec.js
index 876aedff08b..6961996f912 100644
--- a/spec/frontend/work_items/components/work_item_links/work_item_links_spec.js
+++ b/spec/frontend/work_items/components/work_item_links/work_item_links_spec.js
@@ -5,7 +5,7 @@ import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
-import issueConfidentialQuery from '~/sidebar/queries/issue_confidential.query.graphql';
+import issueDetailsQuery from 'ee_else_ce/work_items/graphql/get_issue_details.query.graphql';
import WorkItemLinks from '~/work_items/components/work_item_links/work_item_links.vue';
import WorkItemLinkChild from '~/work_items/components/work_item_links/work_item_link_child.vue';
import workItemQuery from '~/work_items/graphql/work_item.query.graphql';
@@ -21,16 +21,29 @@ import {
Vue.use(VueApollo);
-const issueConfidentialityResponse = (confidential = false) => ({
+const issueDetailsResponse = (confidential = false) => ({
data: {
workspace: {
- id: '1',
- __typename: 'Project',
+ id: 'gid://gitlab/Project/1',
issuable: {
- __typename: 'Issue',
id: 'gid://gitlab/Issue/4',
confidential,
+ iteration: {
+ id: 'gid://gitlab/Iteration/1124',
+ title: null,
+ startDate: '2022-06-22',
+ dueDate: '2022-07-19',
+ webUrl: 'http://127.0.0.1:3000/groups/gitlab-org/-/iterations/1124',
+ iterationCadence: {
+ id: 'gid://gitlab/Iterations::Cadence/1101',
+ title: 'Quod voluptates quidem ea eaque eligendi ex corporis.',
+ __typename: 'IterationCadence',
+ },
+ __typename: 'Iteration',
+ },
+ __typename: 'Issue',
},
+ __typename: 'Project',
},
},
});
@@ -55,14 +68,15 @@ describe('WorkItemLinks', () => {
data = {},
fetchHandler = jest.fn().mockResolvedValue(workItemHierarchyResponse),
mutationHandler = mutationChangeParentHandler,
- confidentialQueryHandler = jest.fn().mockResolvedValue(issueConfidentialityResponse()),
+ issueDetailsQueryHandler = jest.fn().mockResolvedValue(issueDetailsResponse()),
+ hasIterationsFeature = false,
} = {}) => {
mockApollo = createMockApollo(
[
[getWorkItemLinksQuery, fetchHandler],
[changeWorkItemParentMutation, mutationHandler],
[workItemQuery, childWorkItemQueryHandler],
- [issueConfidentialQuery, confidentialQueryHandler],
+ [issueDetailsQuery, issueDetailsQueryHandler],
],
{},
{ addTypename: true },
@@ -77,6 +91,7 @@ describe('WorkItemLinks', () => {
provide: {
projectPath: 'project/path',
iid: '1',
+ hasIterationsFeature,
},
propsData: { issuableId: 1 },
apolloProvider: mockApollo,
@@ -266,7 +281,7 @@ describe('WorkItemLinks', () => {
describe('when parent item is confidential', () => {
it('passes correct confidentiality status to form', async () => {
await createComponent({
- confidentialQueryHandler: jest.fn().mockResolvedValue(issueConfidentialityResponse(true)),
+ issueDetailsQueryHandler: jest.fn().mockResolvedValue(issueDetailsResponse(true)),
});
findToggleAddFormButton().vm.$emit('click');
await nextTick();
diff --git a/spec/frontend/work_items/components/work_item_milestone_spec.js b/spec/frontend/work_items/components/work_item_milestone_spec.js
new file mode 100644
index 00000000000..08cdf62ae52
--- /dev/null
+++ b/spec/frontend/work_items/components/work_item_milestone_spec.js
@@ -0,0 +1,247 @@
+import {
+ GlDropdown,
+ GlDropdownItem,
+ GlSearchBoxByType,
+ GlSkeletonLoader,
+ GlFormGroup,
+ GlDropdownText,
+} from '@gitlab/ui';
+import Vue, { nextTick } from 'vue';
+import VueApollo from 'vue-apollo';
+import WorkItemMilestone from '~/work_items/components/work_item_milestone.vue';
+import { resolvers, temporaryConfig } from '~/graphql_shared/issuable_client';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import { mockTracking } from 'helpers/tracking_helper';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import { TRACKING_CATEGORY_SHOW } from '~/work_items/constants';
+import projectMilestonesQuery from '~/sidebar/queries/project_milestones.query.graphql';
+import {
+ projectMilestonesResponse,
+ projectMilestonesResponseWithNoMilestones,
+ mockMilestoneWidgetResponse,
+ workItemResponseFactory,
+ updateWorkItemMutationErrorResponse,
+} from 'jest/work_items/mock_data';
+import workItemQuery from '~/work_items/graphql/work_item.query.graphql';
+
+describe('WorkItemMilestone component', () => {
+ Vue.use(VueApollo);
+
+ let wrapper;
+
+ const workItemId = 'gid://gitlab/WorkItem/1';
+ const workItemType = 'Task';
+ const fullPath = 'full-path';
+
+ const findDropdown = () => wrapper.findComponent(GlDropdown);
+ const findSearchBox = () => wrapper.findComponent(GlSearchBoxByType);
+ const findSkeletonLoader = () => wrapper.findComponent(GlSkeletonLoader);
+ const findNoMilestoneDropdownItem = () => wrapper.findByTestId('no-milestone');
+ const findDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
+ const findFirstDropdownItem = () => findDropdownItems().at(0);
+ const findDropdownTexts = () => wrapper.findAllComponents(GlDropdownText);
+ const findDropdownItemAtIndex = (index) => findDropdownItems().at(index);
+ const findDisabledTextSpan = () => wrapper.findByTestId('disabled-text');
+ const findDropdownTextAtIndex = (index) => findDropdownTexts().at(index);
+ const findInputGroup = () => wrapper.findComponent(GlFormGroup);
+
+ const workItemQueryResponse = workItemResponseFactory({ canUpdate: true, canDelete: true });
+
+ const networkResolvedValue = new Error();
+
+ const successSearchQueryHandler = jest.fn().mockResolvedValue(projectMilestonesResponse);
+ const successSearchWithNoMatchingMilestones = jest
+ .fn()
+ .mockResolvedValue(projectMilestonesResponseWithNoMilestones);
+
+ const showDropdown = () => {
+ findDropdown().vm.$emit('shown');
+ };
+
+ const hideDropdown = () => {
+ findDropdown().vm.$emit('hide');
+ };
+
+ const createComponent = ({
+ canUpdate = true,
+ milestone = mockMilestoneWidgetResponse,
+ searchQueryHandler = successSearchQueryHandler,
+ } = {}) => {
+ const apolloProvider = createMockApollo(
+ [[projectMilestonesQuery, searchQueryHandler]],
+ resolvers,
+ {
+ typePolicies: temporaryConfig.cacheConfig.typePolicies,
+ },
+ );
+
+ apolloProvider.clients.defaultClient.writeQuery({
+ query: workItemQuery,
+ variables: {
+ id: workItemId,
+ },
+ data: workItemQueryResponse.data,
+ });
+
+ wrapper = shallowMountExtended(WorkItemMilestone, {
+ apolloProvider,
+ propsData: {
+ canUpdate,
+ workItemMilestone: milestone,
+ workItemId,
+ workItemType,
+ fullPath,
+ },
+ stubs: {
+ GlDropdown,
+ GlSearchBoxByType,
+ },
+ });
+ };
+
+ it('has "Milestone" label', () => {
+ createComponent();
+
+ expect(findInputGroup().exists()).toBe(true);
+ expect(findInputGroup().attributes('label')).toBe(WorkItemMilestone.i18n.MILESTONE);
+ });
+
+ describe('Default text with canUpdate false and milestone value', () => {
+ describe.each`
+ description | milestone | value
+ ${'when no milestone'} | ${null} | ${WorkItemMilestone.i18n.NONE}
+ ${'when milestone set'} | ${mockMilestoneWidgetResponse} | ${mockMilestoneWidgetResponse.title}
+ `('$description', ({ milestone, value }) => {
+ it(`has a value of "${value}"`, () => {
+ createComponent({ canUpdate: false, milestone });
+
+ expect(findDisabledTextSpan().text()).toBe(value);
+ expect(findDropdown().exists()).toBe(false);
+ });
+ });
+ });
+
+ describe('Default text value when canUpdate true and no milestone set', () => {
+ it(`has a value of "Add to milestone"`, () => {
+ createComponent({ canUpdate: true, milestone: null });
+
+ expect(findDropdown().props('text')).toBe(WorkItemMilestone.i18n.MILESTONE_PLACEHOLDER);
+ });
+ });
+
+ describe('Dropdown search', () => {
+ it('has the search box', () => {
+ createComponent();
+
+ expect(findSearchBox().exists()).toBe(true);
+ });
+
+ it('shows no matching results when no items', () => {
+ createComponent({
+ searchQueryHandler: successSearchWithNoMatchingMilestones,
+ });
+
+ expect(findDropdownTextAtIndex(0).text()).toBe(WorkItemMilestone.i18n.NO_MATCHING_RESULTS);
+ expect(findDropdownItems()).toHaveLength(1);
+ expect(findDropdownTexts()).toHaveLength(1);
+ });
+ });
+
+ describe('Dropdown options', () => {
+ beforeEach(() => {
+ createComponent({ canUpdate: true });
+ });
+
+ it('shows the skeleton loader when the items are being fetched on click', async () => {
+ showDropdown();
+ await nextTick();
+
+ expect(findSkeletonLoader().exists()).toBe(true);
+ });
+
+ it('shows the milestones in dropdown when the items have finished fetching', async () => {
+ showDropdown();
+ await waitForPromises();
+
+ expect(findSkeletonLoader().exists()).toBe(false);
+ expect(findNoMilestoneDropdownItem().exists()).toBe(true);
+ expect(findDropdownItems()).toHaveLength(
+ projectMilestonesResponse.data.workspace.attributes.nodes.length + 1,
+ );
+ });
+
+ it('changes the milestone to null when clicked on no milestone', async () => {
+ showDropdown();
+ findFirstDropdownItem().vm.$emit('click');
+
+ hideDropdown();
+ await nextTick();
+ expect(findDropdown().props('loading')).toBe(true);
+
+ await waitForPromises();
+
+ expect(findDropdown().props('loading')).toBe(false);
+ expect(findDropdown().props('text')).toBe(WorkItemMilestone.i18n.MILESTONE_PLACEHOLDER);
+ });
+
+ it('changes the milestone to the selected milestone', async () => {
+ const milestoneIndex = 1;
+ /** the index is -1 since no matching results is also a dropdown item */
+ const milestoneAtIndex =
+ projectMilestonesResponse.data.workspace.attributes.nodes[milestoneIndex - 1];
+ showDropdown();
+
+ await waitForPromises();
+ findDropdownItemAtIndex(milestoneIndex).vm.$emit('click');
+
+ hideDropdown();
+ await waitForPromises();
+
+ expect(findDropdown().props('text')).toBe(milestoneAtIndex.title);
+ });
+ });
+
+ describe('Error handlers', () => {
+ it.each`
+ errorType | expectedErrorMessage | mockValue | resolveFunction
+ ${'graphql error'} | ${'Something went wrong while updating the task. Please try again.'} | ${updateWorkItemMutationErrorResponse} | ${'mockResolvedValue'}
+ ${'network error'} | ${'Something went wrong while updating the task. Please try again.'} | ${networkResolvedValue} | ${'mockRejectedValue'}
+ `(
+ 'emits an error when there is a $errorType',
+ async ({ mockValue, expectedErrorMessage, resolveFunction }) => {
+ createComponent({
+ mutationHandler: jest.fn()[resolveFunction](mockValue),
+ canUpdate: true,
+ });
+
+ showDropdown();
+ findFirstDropdownItem().vm.$emit('click');
+ hideDropdown();
+
+ await waitForPromises();
+
+ expect(wrapper.emitted('error')).toEqual([[expectedErrorMessage]]);
+ },
+ );
+ });
+
+ describe('Tracking event', () => {
+ it('tracks updating the milestone', async () => {
+ const trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
+ createComponent({ canUpdate: true });
+
+ showDropdown();
+ findFirstDropdownItem().vm.$emit('click');
+ hideDropdown();
+
+ await waitForPromises();
+
+ expect(trackingSpy).toHaveBeenCalledWith(TRACKING_CATEGORY_SHOW, 'updated_milestone', {
+ category: TRACKING_CATEGORY_SHOW,
+ label: 'item_milestone',
+ property: 'type_Task',
+ });
+ });
+ });
+});
diff --git a/spec/frontend/work_items/components/work_item_type_icon_spec.js b/spec/frontend/work_items/components/work_item_type_icon_spec.js
index 95ddfc3980e..182fb0f8cb6 100644
--- a/spec/frontend/work_items/components/work_item_type_icon_spec.js
+++ b/spec/frontend/work_items/components/work_item_type_icon_spec.js
@@ -51,7 +51,7 @@ describe('Work Item type component', () => {
});
it('renders the icon in gray color', () => {
- expect(findIcon().classes()).toContain('gl-text-gray-500');
+ expect(findIcon().classes()).toContain('gl-text-secondary');
});
it('shows tooltip on hover when props passed', () => {
diff --git a/spec/frontend/work_items/mock_data.js b/spec/frontend/work_items/mock_data.js
index e1bc8d2f6b7..ed90b11222a 100644
--- a/spec/frontend/work_items/mock_data.js
+++ b/spec/frontend/work_items/mock_data.js
@@ -17,6 +17,25 @@ export const mockAssignees = [
},
];
+export const mockLabels = [
+ {
+ __typename: 'Label',
+ id: 'gid://gitlab/Label/1',
+ title: 'Label 1',
+ description: '',
+ color: '#f00',
+ textColor: '#00f',
+ },
+ {
+ __typename: 'Label',
+ id: 'gid://gitlab/Label/2',
+ title: 'Label::2',
+ description: '',
+ color: '#b00',
+ textColor: '#00b',
+ },
+];
+
export const workItemQueryResponse = {
data: {
workItem: {
@@ -50,6 +69,8 @@ export const workItemQueryResponse = {
description: 'some **great** text',
descriptionHtml:
'<p data-sourcepos="1:1-1:19" dir="auto">some <strong>great</strong> text</p>',
+ lastEditedAt: null,
+ lastEditedBy: null,
},
{
__typename: 'WorkItemWidgetAssignees',
@@ -163,9 +184,15 @@ export const workItemResponseFactory = ({
allowsMultipleAssignees = true,
assigneesWidgetPresent = true,
datesWidgetPresent = true,
+ labelsWidgetPresent = true,
weightWidgetPresent = true,
+ milestoneWidgetPresent = true,
+ iterationWidgetPresent = true,
confidential = false,
canInviteMembers = false,
+ allowsScopedLabels = false,
+ lastEditedAt = null,
+ lastEditedBy = null,
parent = mockParent.parent,
} = {}) => ({
data: {
@@ -200,6 +227,8 @@ export const workItemResponseFactory = ({
description: 'some **great** text',
descriptionHtml:
'<p data-sourcepos="1:1-1:19" dir="auto">some <strong>great</strong> text</p>',
+ lastEditedAt,
+ lastEditedBy,
},
assigneesWidgetPresent
? {
@@ -212,6 +241,16 @@ export const workItemResponseFactory = ({
},
}
: { type: 'MOCK TYPE' },
+ labelsWidgetPresent
+ ? {
+ __typename: 'WorkItemWidgetLabels',
+ type: 'LABELS',
+ allowsScopedLabels,
+ labels: {
+ nodes: mockLabels,
+ },
+ }
+ : { type: 'MOCK TYPE' },
datesWidgetPresent
? {
__typename: 'WorkItemWidgetStartAndDueDate',
@@ -227,6 +266,30 @@ export const workItemResponseFactory = ({
weight: 0,
}
: { type: 'MOCK TYPE' },
+ iterationWidgetPresent
+ ? {
+ __typename: 'WorkItemWidgetIteration',
+ type: 'ITERATION',
+ iteration: {
+ description: null,
+ id: 'gid://gitlab/Iteration/1215',
+ iid: '182',
+ title: 'Iteration default title',
+ startDate: '2022-09-22',
+ dueDate: '2022-09-30',
+ },
+ }
+ : { type: 'MOCK TYPE' },
+ milestoneWidgetPresent
+ ? {
+ __typename: 'WorkItemWidgetMilestone',
+ dueDate: null,
+ expired: false,
+ id: 'gid://gitlab/Milestone/30',
+ title: 'v4.0',
+ type: 'MILESTONE',
+ }
+ : { type: 'MOCK TYPE' },
{
__typename: 'WorkItemWidgetHierarchy',
type: 'HIERARCHY',
@@ -331,6 +394,11 @@ export const createWorkItemFromTaskMutationResponse = {
type: 'DESCRIPTION',
description: 'New description',
descriptionHtml: '<p>New description</p>',
+ lastEditedAt: '2022-09-21T06:18:42Z',
+ lastEditedBy: {
+ name: 'Administrator',
+ webPath: '/root',
+ },
},
],
},
@@ -444,6 +512,61 @@ export const workItemWeightSubscriptionResponse = {
},
};
+export const workItemAssigneesSubscriptionResponse = {
+ data: {
+ issuableAssigneesUpdated: {
+ id: 'gid://gitlab/WorkItem/1',
+ widgets: [
+ {
+ __typename: 'WorkItemAssigneesWeight',
+ assignees: {
+ nodes: [mockAssignees[0]],
+ },
+ },
+ ],
+ },
+ },
+};
+
+export const workItemLabelsSubscriptionResponse = {
+ data: {
+ issuableLabelsUpdated: {
+ id: 'gid://gitlab/WorkItem/1',
+ widgets: [
+ {
+ __typename: 'WorkItemWidgetLabels',
+ type: 'LABELS',
+ allowsScopedLabels: false,
+ labels: {
+ nodes: mockLabels,
+ },
+ },
+ ],
+ },
+ },
+};
+
+export const workItemIterationSubscriptionResponse = {
+ data: {
+ issuableIterationUpdated: {
+ id: 'gid://gitlab/WorkItem/1',
+ widgets: [
+ {
+ __typename: 'WorkItemWidgetIteration',
+ iteration: {
+ description: 'Iteration description',
+ dueDate: '2022-07-29',
+ id: 'gid://gitlab/Iteration/1125',
+ iid: '95',
+ startDate: '2022-06-22',
+ title: 'Iteration subcription title',
+ },
+ },
+ ],
+ },
+ },
+};
+
export const workItemHierarchyEmptyResponse = {
data: {
workItem: {
@@ -857,25 +980,6 @@ export const currentUserNullResponse = {
},
};
-export const mockLabels = [
- {
- __typename: 'Label',
- id: 'gid://gitlab/Label/1',
- title: 'Label 1',
- description: '',
- color: '#f00',
- textColor: '#00f',
- },
- {
- __typename: 'Label',
- id: 'gid://gitlab/Label/2',
- title: 'Label 2',
- description: '',
- color: '#b00',
- textColor: '#00b',
- },
-];
-
export const projectLabelsResponse = {
data: {
workspace: {
@@ -887,3 +991,134 @@ export const projectLabelsResponse = {
},
},
};
+
+export const mockIterationWidgetResponse = {
+ description: 'Iteration description',
+ dueDate: '2022-07-19',
+ id: 'gid://gitlab/Iteration/1124',
+ iid: '91',
+ startDate: '2022-06-22',
+ title: 'Iteration title widget',
+};
+
+export const groupIterationsResponse = {
+ data: {
+ workspace: {
+ id: 'gid://gitlab/Group/22',
+ attributes: {
+ nodes: [
+ {
+ id: 'gid://gitlab/Iteration/1124',
+ title: null,
+ startDate: '2022-06-22',
+ dueDate: '2022-07-19',
+ webUrl: 'http://127.0.0.1:3000/groups/gitlab-org/-/iterations/1124',
+ iterationCadence: {
+ id: 'gid://gitlab/Iterations::Cadence/1101',
+ title: 'Quod voluptates quidem ea eaque eligendi ex corporis.',
+ __typename: 'IterationCadence',
+ },
+ __typename: 'Iteration',
+ state: 'current',
+ },
+ {
+ id: 'gid://gitlab/Iteration/1185',
+ title: null,
+ startDate: '2022-07-06',
+ dueDate: '2022-07-19',
+ webUrl: 'http://127.0.0.1:3000/groups/gitlab-org/-/iterations/1185',
+ iterationCadence: {
+ id: 'gid://gitlab/Iterations::Cadence/1144',
+ title: 'Quo velit perspiciatis saepe aut omnis voluptas ab eos.',
+ __typename: 'IterationCadence',
+ },
+ __typename: 'Iteration',
+ state: 'current',
+ },
+ {
+ id: 'gid://gitlab/Iteration/1194',
+ title: null,
+ startDate: '2022-07-06',
+ dueDate: '2022-07-19',
+ webUrl: 'http://127.0.0.1:3000/groups/gitlab-org/-/iterations/1194',
+ iterationCadence: {
+ id: 'gid://gitlab/Iterations::Cadence/1152',
+ title:
+ 'Minima aut consequatur magnam vero doloremque accusamus maxime repellat voluptatem qui.',
+ __typename: 'IterationCadence',
+ },
+ __typename: 'Iteration',
+ state: 'current',
+ },
+ ],
+ __typename: 'IterationConnection',
+ },
+ __typename: 'Group',
+ },
+ },
+};
+
+export const groupIterationsResponseWithNoIterations = {
+ data: {
+ workspace: {
+ id: 'gid://gitlab/Group/22',
+ attributes: {
+ nodes: [],
+ __typename: 'IterationConnection',
+ },
+ __typename: 'Group',
+ },
+ },
+};
+
+export const mockMilestoneWidgetResponse = {
+ dueDate: null,
+ expired: false,
+ id: 'gid://gitlab/Milestone/30',
+ title: 'v4.0',
+};
+
+export const projectMilestonesResponse = {
+ data: {
+ workspace: {
+ id: 'gid://gitlab/Project/1',
+ attributes: {
+ nodes: [
+ {
+ id: 'gid://gitlab/Milestone/5',
+ title: 'v4.0',
+ webUrl: '/gitlab-org/gitlab-test/-/milestones/5',
+ dueDate: null,
+ expired: false,
+ __typename: 'Milestone',
+ state: 'active',
+ },
+ {
+ id: 'gid://gitlab/Milestone/4',
+ title: 'v3.0',
+ webUrl: '/gitlab-org/gitlab-test/-/milestones/4',
+ dueDate: null,
+ expired: false,
+ __typename: 'Milestone',
+ state: 'active',
+ },
+ ],
+ __typename: 'MilestoneConnection',
+ },
+ __typename: 'Project',
+ },
+ },
+};
+
+export const projectMilestonesResponseWithNoMilestones = {
+ data: {
+ workspace: {
+ id: 'gid://gitlab/Project/1',
+ attributes: {
+ nodes: [],
+ __typename: 'MilestoneConnection',
+ },
+ __typename: 'Project',
+ },
+ },
+};
diff --git a/spec/frontend/work_items/router_spec.js b/spec/frontend/work_items/router_spec.js
index ab370e2ca8b..66a917d8052 100644
--- a/spec/frontend/work_items/router_spec.js
+++ b/spec/frontend/work_items/router_spec.js
@@ -4,15 +4,19 @@ import VueApollo from 'vue-apollo';
import workItemWeightSubscription from 'ee_component/work_items/graphql/work_item_weight.subscription.graphql';
import createMockApollo from 'helpers/mock_apollo_helper';
import {
+ workItemAssigneesSubscriptionResponse,
workItemDatesSubscriptionResponse,
workItemResponseFactory,
workItemTitleSubscriptionResponse,
workItemWeightSubscriptionResponse,
+ workItemLabelsSubscriptionResponse,
} from 'jest/work_items/mock_data';
import App from '~/work_items/components/app.vue';
import workItemQuery from '~/work_items/graphql/work_item.query.graphql';
import workItemDatesSubscription from '~/work_items/graphql/work_item_dates.subscription.graphql';
import workItemTitleSubscription from '~/work_items/graphql/work_item_title.subscription.graphql';
+import workItemAssigneesSubscription from '~/work_items/graphql/work_item_assignees.subscription.graphql';
+import workItemLabelsSubscription from 'ee_else_ce/work_items/graphql/work_item_labels.subscription.graphql';
import CreateWorkItem from '~/work_items/pages/create_work_item.vue';
import WorkItemsRoot from '~/work_items/pages/work_item_root.vue';
import { createRouter } from '~/work_items/router';
@@ -26,6 +30,10 @@ describe('Work items router', () => {
const datesSubscriptionHandler = jest.fn().mockResolvedValue(workItemDatesSubscriptionResponse);
const titleSubscriptionHandler = jest.fn().mockResolvedValue(workItemTitleSubscriptionResponse);
const weightSubscriptionHandler = jest.fn().mockResolvedValue(workItemWeightSubscriptionResponse);
+ const assigneesSubscriptionHandler = jest
+ .fn()
+ .mockResolvedValue(workItemAssigneesSubscriptionResponse);
+ const labelsSubscriptionHandler = jest.fn().mockResolvedValue(workItemLabelsSubscriptionResponse);
const createComponent = async (routeArg) => {
const router = createRouter('/work_item');
@@ -37,6 +45,8 @@ describe('Work items router', () => {
[workItemQuery, workItemQueryHandler],
[workItemDatesSubscription, datesSubscriptionHandler],
[workItemTitleSubscription, titleSubscriptionHandler],
+ [workItemAssigneesSubscription, assigneesSubscriptionHandler],
+ [workItemLabelsSubscription, labelsSubscriptionHandler],
];
if (IS_EE) {
diff --git a/spec/frontend/work_items_hierarchy/components/app_spec.js b/spec/frontend/work_items_hierarchy/components/app_spec.js
index 1426fbfab80..124ff5f1608 100644
--- a/spec/frontend/work_items_hierarchy/components/app_spec.js
+++ b/spec/frontend/work_items_hierarchy/components/app_spec.js
@@ -32,7 +32,7 @@ describe('WorkItemsHierarchy App', () => {
it('shows when the banner is visible', () => {
createComponent({}, { bannerVisible: true });
- expect(wrapper.find(GlBanner).exists()).toBe(true);
+ expect(wrapper.findComponent(GlBanner).exists()).toBe(true);
});
it('hide when close is called', async () => {
@@ -42,7 +42,7 @@ describe('WorkItemsHierarchy App', () => {
await nextTick();
- expect(wrapper.find(GlBanner).exists()).toBe(false);
+ expect(wrapper.findComponent(GlBanner).exists()).toBe(false);
});
});
diff --git a/spec/frontend/work_items_hierarchy/components/hierarchy_spec.js b/spec/frontend/work_items_hierarchy/components/hierarchy_spec.js
index dca016dc317..084aaa754ab 100644
--- a/spec/frontend/work_items_hierarchy/components/hierarchy_spec.js
+++ b/spec/frontend/work_items_hierarchy/components/hierarchy_spec.js
@@ -57,7 +57,7 @@ describe('WorkItemsHierarchy Hierarchy', () => {
});
it('does not render badges', () => {
- expect(wrapper.find(GlBadge).exists()).toBe(false);
+ expect(wrapper.findComponent(GlBadge).exists()).toBe(false);
});
});