summaryrefslogtreecommitdiff
path: root/spec/frontend
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2022-08-18 08:17:02 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2022-08-18 08:17:02 +0000
commitb39512ed755239198a9c294b6a45e65c05900235 (patch)
treed234a3efade1de67c46b9e5a38ce813627726aa7 /spec/frontend
parentd31474cf3b17ece37939d20082b07f6657cc79a9 (diff)
downloadgitlab-ce-b39512ed755239198a9c294b6a45e65c05900235.tar.gz
Add latest changes from gitlab-org/gitlab@15-3-stable-eev15.3.0-rc42
Diffstat (limited to 'spec/frontend')
-rw-r--r--spec/frontend/__helpers__/mock_apollo_helper.js1
-rw-r--r--spec/frontend/__helpers__/mock_dom_observer.js2
-rw-r--r--spec/frontend/__helpers__/mocks/axios_utils.js2
-rw-r--r--spec/frontend/__helpers__/stub_component.js8
-rw-r--r--spec/frontend/__helpers__/timeout.js59
-rw-r--r--spec/frontend/__helpers__/vue_mount_component_helper.js29
-rw-r--r--spec/frontend/__helpers__/vue_test_utils_helper.js19
-rw-r--r--spec/frontend/__helpers__/vue_test_utils_helper_spec.js57
-rw-r--r--spec/frontend/__helpers__/vuex_action_helper.js6
-rw-r--r--spec/frontend/__helpers__/vuex_action_helper_spec.js2
-rw-r--r--spec/frontend/__helpers__/wait_for_promises.js6
-rw-r--r--spec/frontend/__helpers__/web_worker_transformer.js2
-rw-r--r--spec/frontend/__mocks__/monaco-editor/index.js1
-rw-r--r--spec/frontend/access_tokens/components/__snapshots__/expires_at_field_spec.js.snap17
-rw-r--r--spec/frontend/access_tokens/components/expires_at_field_spec.js9
-rw-r--r--spec/frontend/access_tokens/components/projects_field_spec.js131
-rw-r--r--spec/frontend/access_tokens/components/projects_token_selector_spec.js266
-rw-r--r--spec/frontend/access_tokens/index_spec.js45
-rw-r--r--spec/frontend/add_context_commits_modal/components/add_context_commits_modal_spec.js4
-rw-r--r--spec/frontend/admin/analytics/devops_score/components/devops_score_spec.js2
-rw-r--r--spec/frontend/admin/signup_restrictions/components/signup_checkbox_spec.js2
-rw-r--r--spec/frontend/admin/signup_restrictions/components/signup_form_spec.js4
-rw-r--r--spec/frontend/admin/statistics_panel/components/app_spec.js2
-rw-r--r--spec/frontend/admin/users/components/actions/actions_spec.js2
-rw-r--r--spec/frontend/admin/users/components/app_spec.js2
-rw-r--r--spec/frontend/admin/users/components/modals/delete_user_modal_spec.js14
-rw-r--r--spec/frontend/admin/users/components/user_actions_spec.js4
-rw-r--r--spec/frontend/admin/users/components/user_avatar_spec.js6
-rw-r--r--spec/frontend/admin/users/components/users_table_spec.js8
-rw-r--r--spec/frontend/admin/users/index_spec.js4
-rw-r--r--spec/frontend/api/groups_api_spec.js46
-rw-r--r--spec/frontend/attention_requests/components/navigation_popover_spec.js88
-rw-r--r--spec/frontend/batch_comments/components/review_bar_spec.js51
-rw-r--r--spec/frontend/behaviors/components/json_table_spec.js162
-rw-r--r--spec/frontend/behaviors/gl_emoji_spec.js2
-rw-r--r--spec/frontend/behaviors/markdown/render_json_table_spec.js119
-rw-r--r--spec/frontend/blob/3d_viewer/mesh_object_spec.js4
-rw-r--r--spec/frontend/blob/blob_links_tracking_spec.js60
-rw-r--r--spec/frontend/blob/components/blob_content_spec.js22
-rw-r--r--spec/frontend/blob/components/blob_edit_content_spec.js2
-rw-r--r--spec/frontend/blob/components/blob_edit_header_spec.js6
-rw-r--r--spec/frontend/blob/components/blob_header_default_actions_spec.js8
-rw-r--r--spec/frontend/blob/components/blob_header_filepath_spec.js4
-rw-r--r--spec/frontend/blob/components/blob_header_spec.js14
-rw-r--r--spec/frontend/blob/components/blob_header_viewer_switcher_spec.js6
-rw-r--r--spec/frontend/blob/notebook/notebook_viever_spec.js8
-rw-r--r--spec/frontend/blob/pdf/pdf_viewer_spec.js6
-rw-r--r--spec/frontend/blob/pipeline_tour_success_modal_spec.js16
-rw-r--r--spec/frontend/blob/sketch/index_spec.js2
-rw-r--r--spec/frontend/blob/suggest_gitlab_ci_yml/components/popover_spec.js2
-rw-r--r--spec/frontend/boards/board_card_inner_spec.js6
-rw-r--r--spec/frontend/boards/board_list_spec.js2
-rw-r--r--spec/frontend/boards/components/board_add_new_column_form_spec.js30
-rw-r--r--spec/frontend/boards/components/board_add_new_column_trigger_spec.js4
-rw-r--r--spec/frontend/boards/components/board_blocked_icon_spec.js8
-rw-r--r--spec/frontend/boards/components/board_card_spec.js2
-rw-r--r--spec/frontend/boards/components/board_content_sidebar_spec.js2
-rw-r--r--spec/frontend/boards/components/board_content_spec.js12
-rw-r--r--spec/frontend/boards/components/board_list_header_spec.js2
-rw-r--r--spec/frontend/boards/components/board_new_item_spec.js8
-rw-r--r--spec/frontend/boards/components/board_settings_sidebar_spec.js16
-rw-r--r--spec/frontend/boards/components/boards_selector_spec.js2
-rw-r--r--spec/frontend/boards/components/new_board_button_spec.js6
-rw-r--r--spec/frontend/boards/components/sidebar/board_editable_item_spec.js2
-rw-r--r--spec/frontend/boards/components/sidebar/board_sidebar_time_tracker_spec.js2
-rw-r--r--spec/frontend/boards/components/sidebar/board_sidebar_title_spec.js8
-rw-r--r--spec/frontend/boards/project_select_spec.js9
-rw-r--r--spec/frontend/boards/stores/mutations_spec.js15
-rw-r--r--spec/frontend/captcha/wait_for_captcha_to_be_solved_spec.js5
-rw-r--r--spec/frontend/ci_variable_list/components/ci_admin_variables_spec.js178
-rw-r--r--spec/frontend/ci_variable_list/components/ci_environments_dropdown_spec.js139
-rw-r--r--spec/frontend/ci_variable_list/components/ci_group_variables_spec.js183
-rw-r--r--spec/frontend/ci_variable_list/components/ci_variable_modal_spec.js383
-rw-r--r--spec/frontend/ci_variable_list/components/ci_variable_settings_spec.js128
-rw-r--r--spec/frontend/ci_variable_list/components/ci_variable_table_spec.js98
-rw-r--r--spec/frontend/ci_variable_list/components/legacy_ci_variable_modal_spec.js8
-rw-r--r--spec/frontend/ci_variable_list/mocks.js109
-rw-r--r--spec/frontend/ci_variable_list/utils_spec.js78
-rw-r--r--spec/frontend/clusters/agents/components/activity_history_item_spec.js2
-rw-r--r--spec/frontend/clusters/agents/components/create_token_modal_spec.js2
-rw-r--r--spec/frontend/clusters/agents/components/token_table_spec.js4
-rw-r--r--spec/frontend/clusters/clusters_bundle_spec.js45
-rw-r--r--spec/frontend/clusters/components/new_cluster_spec.js4
-rw-r--r--spec/frontend/clusters/forms/components/integration_form_spec.js4
-rw-r--r--spec/frontend/clusters_list/components/install_agent_modal_spec.js1
-rw-r--r--spec/frontend/commit/pipelines/pipelines_table_spec.js39
-rw-r--r--spec/frontend/content_editor/components/bubble_menus/link_spec.js2
-rw-r--r--spec/frontend/content_editor/components/bubble_menus/media_spec.js2
-rw-r--r--spec/frontend/content_editor/components/content_editor_spec.js14
-rw-r--r--spec/frontend/content_editor/components/toolbar_more_dropdown_spec.js21
-rw-r--r--spec/frontend/content_editor/components/top_toolbar_spec.js2
-rw-r--r--spec/frontend/content_editor/components/wrappers/__snapshots__/table_of_contents_spec.js.snap115
-rw-r--r--spec/frontend/content_editor/components/wrappers/table_cell_base_spec.js4
-rw-r--r--spec/frontend/content_editor/components/wrappers/table_of_contents_spec.js84
-rw-r--r--spec/frontend/content_editor/extensions/image_spec.js2
-rw-r--r--spec/frontend/content_editor/markdown_processing_spec_helper.js4
-rw-r--r--spec/frontend/content_editor/remark_markdown_processing_spec.js238
-rw-r--r--spec/frontend/content_editor/render_html_and_json_for_all_examples.js4
-rw-r--r--spec/frontend/content_editor/services/markdown_serializer_spec.js144
-rw-r--r--spec/frontend/content_editor/services/table_of_contents_utils_spec.js96
-rw-r--r--spec/frontend/crm/contact_form_wrapper_spec.js9
-rw-r--r--spec/frontend/crm/contacts_root_spec.js56
-rw-r--r--spec/frontend/crm/form_spec.js38
-rw-r--r--spec/frontend/crm/mock_data.js32
-rw-r--r--spec/frontend/crm/organization_form_wrapper_spec.js2
-rw-r--r--spec/frontend/cycle_analytics/base_spec.js20
-rw-r--r--spec/frontend/cycle_analytics/mock_data.js18
-rw-r--r--spec/frontend/cycle_analytics/store/actions_spec.js56
-rw-r--r--spec/frontend/cycle_analytics/store/mutations_spec.js43
-rw-r--r--spec/frontend/design_management/components/delete_button_spec.js4
-rw-r--r--spec/frontend/design_management/components/design_notes/design_discussion_spec.js14
-rw-r--r--spec/frontend/design_management/components/design_notes/design_note_spec.js2
-rw-r--r--spec/frontend/design_management/components/design_notes/design_reply_form_spec.js18
-rw-r--r--spec/frontend/design_management/components/design_notes/toggle_replies_widget_spec.js8
-rw-r--r--spec/frontend/design_management/components/design_scaler_spec.js2
-rw-r--r--spec/frontend/design_management/components/design_sidebar_spec.js8
-rw-r--r--spec/frontend/design_management/components/design_todo_button_spec.js2
-rw-r--r--spec/frontend/design_management/components/image_spec.js2
-rw-r--r--spec/frontend/design_management/components/list/item_spec.js10
-rw-r--r--spec/frontend/design_management/components/toolbar/index_spec.js10
-rw-r--r--spec/frontend/design_management/components/upload/button_spec.js2
-rw-r--r--spec/frontend/design_management/components/upload/design_version_dropdown_spec.js12
-rw-r--r--spec/frontend/design_management/pages/design/index_spec.js12
-rw-r--r--spec/frontend/design_management/pages/index_spec.js14
-rw-r--r--spec/frontend/design_management/router_spec.js4
-rw-r--r--spec/frontend/diffs/components/diff_file_header_spec.js2
-rw-r--r--spec/frontend/diffs/components/diff_row_spec.js10
-rw-r--r--spec/frontend/diffs/store/utils_spec.js8
-rw-r--r--spec/frontend/dropzone_input_spec.js4
-rw-r--r--spec/frontend/editor/schema/ci/ci_schema_spec.js10
-rw-r--r--spec/frontend/editor/schema/ci/yaml_tests/negative_tests/artifacts.yml18
-rw-r--r--spec/frontend/editor/schema/ci/yaml_tests/negative_tests/cache.yml8
-rw-r--r--spec/frontend/editor/schema/ci/yaml_tests/negative_tests/include.yml8
-rw-r--r--spec/frontend/editor/schema/ci/yaml_tests/negative_tests/rules.yml14
-rw-r--r--spec/frontend/editor/schema/ci/yaml_tests/positive_tests/artifacts.yml25
-rw-r--r--spec/frontend/editor/schema/ci/yaml_tests/positive_tests/cache.yml3
-rw-r--r--spec/frontend/editor/schema/ci/yaml_tests/positive_tests/filter.yml10
-rw-r--r--spec/frontend/editor/schema/ci/yaml_tests/positive_tests/include.yml15
-rw-r--r--spec/frontend/editor/schema/ci/yaml_tests/positive_tests/rules.yml23
-rw-r--r--spec/frontend/editor/source_editor_instance_spec.js2
-rw-r--r--spec/frontend/editor/source_editor_spec.js7
-rw-r--r--spec/frontend/environment.js1
-rw-r--r--spec/frontend/environments/canary_ingress_spec.js10
-rw-r--r--spec/frontend/environments/canary_update_modal_spec.js4
-rw-r--r--spec/frontend/environments/confirm_rollback_modal_spec.js14
-rw-r--r--spec/frontend/environments/deploy_board_component_spec.js8
-rw-r--r--spec/frontend/environments/edit_environment_spec.js2
-rw-r--r--spec/frontend/environments/environment_actions_spec.js12
-rw-r--r--spec/frontend/environments/environment_delete_spec.js2
-rw-r--r--spec/frontend/environments/environment_item_spec.js12
-rw-r--r--spec/frontend/environments/environment_pin_spec.js4
-rw-r--r--spec/frontend/environments/environment_rollback_spec.js4
-rw-r--r--spec/frontend/environments/environment_stop_spec.js2
-rw-r--r--spec/frontend/environments/environment_table_spec.js4
-rw-r--r--spec/frontend/environments/environments_detail_header_spec.js12
-rw-r--r--spec/frontend/environments/folder/environments_folder_view_spec.js6
-rw-r--r--spec/frontend/environments/new_environment_spec.js2
-rw-r--r--spec/frontend/error_tracking/components/error_details_spec.js44
-rw-r--r--spec/frontend/error_tracking/components/error_tracking_actions_spec.js2
-rw-r--r--spec/frontend/error_tracking/components/error_tracking_list_spec.js24
-rw-r--r--spec/frontend/error_tracking/components/stacktrace_entry_spec.js10
-rw-r--r--spec/frontend/error_tracking/components/stacktrace_spec.js4
-rw-r--r--spec/frontend/error_tracking_settings/components/project_dropdown_spec.js24
-rw-r--r--spec/frontend/feature_flags/components/configure_feature_flags_modal_spec.js4
-rw-r--r--spec/frontend/feature_flags/components/empty_state_spec.js14
-rw-r--r--spec/frontend/feature_flags/components/environments_dropdown_spec.js10
-rw-r--r--spec/frontend/feature_flags/components/feature_flags_table_spec.js4
-rw-r--r--spec/frontend/feature_flags/components/form_spec.js18
-rw-r--r--spec/frontend/feature_flags/components/new_environments_dropdown_spec.js24
-rw-r--r--spec/frontend/feature_flags/components/new_feature_flag_spec.js6
-rw-r--r--spec/frontend/feature_flags/components/strategies/flexible_rollout_spec.js12
-rw-r--r--spec/frontend/feature_flags/components/strategies/gitlab_user_list_spec.js8
-rw-r--r--spec/frontend/feature_flags/components/strategies/parameter_form_group_spec.js2
-rw-r--r--spec/frontend/feature_flags/components/strategies/percent_rollout_spec.js12
-rw-r--r--spec/frontend/feature_flags/components/strategies/users_with_id_spec.js2
-rw-r--r--spec/frontend/feature_flags/components/strategy_parameters_spec.js6
-rw-r--r--spec/frontend/feature_flags/components/strategy_spec.js40
-rw-r--r--spec/frontend/fixtures/integrations.rb2
-rw-r--r--spec/frontend/fixtures/issues.rb40
-rw-r--r--spec/frontend/fixtures/namespaces.rb46
-rw-r--r--spec/frontend/fixtures/prometheus_integration.rb2
-rw-r--r--spec/frontend/fixtures/runner.rb44
-rw-r--r--spec/frontend/frequent_items/components/app_spec.js13
-rw-r--r--spec/frontend/gfm_auto_complete/mock_data.js34
-rw-r--r--spec/frontend/gfm_auto_complete_spec.js48
-rw-r--r--spec/frontend/group_settings/components/shared_runners_form_spec.js29
-rw-r--r--spec/frontend/groups/components/app_spec.js24
-rw-r--r--spec/frontend/groups/components/group_item_spec.js49
-rw-r--r--spec/frontend/groups/components/group_name_and_path_spec.js75
-rw-r--r--spec/frontend/groups/components/groups_spec.js4
-rw-r--r--spec/frontend/groups/components/transfer_group_form_spec.js1
-rw-r--r--spec/frontend/header_search/components/app_spec.js72
-rw-r--r--spec/frontend/helpers/diffs_helper_spec.js32
-rw-r--r--spec/frontend/ide/components/activity_bar_spec.js72
-rw-r--r--spec/frontend/ide/components/branches/item_spec.js6
-rw-r--r--spec/frontend/ide/components/branches/search_list_spec.js6
-rw-r--r--spec/frontend/ide/components/commit_sidebar/editor_header_spec.js4
-rw-r--r--spec/frontend/ide/components/commit_sidebar/form_spec.js6
-rw-r--r--spec/frontend/ide/components/error_message_spec.js4
-rw-r--r--spec/frontend/ide/components/file_templates/dropdown_spec.js2
-rw-r--r--spec/frontend/ide/components/ide_file_row_spec.js4
-rw-r--r--spec/frontend/ide/components/ide_project_header_spec.js2
-rw-r--r--spec/frontend/ide/components/ide_review_spec.js4
-rw-r--r--spec/frontend/ide/components/ide_side_bar_spec.js34
-rw-r--r--spec/frontend/ide/components/ide_sidebar_nav_spec.js6
-rw-r--r--spec/frontend/ide/components/ide_spec.js2
-rw-r--r--spec/frontend/ide/components/ide_status_bar_spec.js124
-rw-r--r--spec/frontend/ide/components/ide_status_list_spec.js4
-rw-r--r--spec/frontend/ide/components/ide_status_mr_spec.js4
-rw-r--r--spec/frontend/ide/components/ide_tree_spec.js2
-rw-r--r--spec/frontend/ide/components/jobs/detail/scroll_button_spec.js2
-rw-r--r--spec/frontend/ide/components/jobs/list_spec.js10
-rw-r--r--spec/frontend/ide/components/jobs/stage_spec.js8
-rw-r--r--spec/frontend/ide/components/merge_requests/list_spec.js8
-rw-r--r--spec/frontend/ide/components/new_dropdown/index_spec.js82
-rw-r--r--spec/frontend/ide/components/panes/collapsible_sidebar_spec.js2
-rw-r--r--spec/frontend/ide/components/panes/right_spec.js8
-rw-r--r--spec/frontend/ide/components/pipelines/empty_state_spec.js2
-rw-r--r--spec/frontend/ide/components/pipelines/list_spec.js12
-rw-r--r--spec/frontend/ide/components/preview/clientside_spec.js2
-rw-r--r--spec/frontend/ide/components/preview/navigator_spec.js4
-rw-r--r--spec/frontend/ide/components/repo_commit_section_spec.js12
-rw-r--r--spec/frontend/ide/components/repo_editor_spec.js5
-rw-r--r--spec/frontend/ide/components/repo_tab_spec.js6
-rw-r--r--spec/frontend/ide/components/resizable_panel_spec.js2
-rw-r--r--spec/frontend/ide/components/shared/commit_message_field_spec.js2
-rw-r--r--spec/frontend/ide/components/terminal/empty_state_spec.js10
-rw-r--r--spec/frontend/ide/components/terminal/session_spec.js4
-rw-r--r--spec/frontend/ide/components/terminal/terminal_controls_spec.js2
-rw-r--r--spec/frontend/ide/components/terminal/terminal_spec.js16
-rw-r--r--spec/frontend/ide/components/terminal/view_spec.js8
-rw-r--r--spec/frontend/ide/components/terminal_sync/terminal_sync_status_safe_spec.js4
-rw-r--r--spec/frontend/ide/components/terminal_sync/terminal_sync_status_spec.js8
-rw-r--r--spec/frontend/ide/lib/common/model_manager_spec.js4
-rw-r--r--spec/frontend/ide/lib/diff/diff_spec.js8
-rw-r--r--spec/frontend/ide/stores/actions/file_spec.js8
-rw-r--r--spec/frontend/ide/stores/actions/tree_spec.js2
-rw-r--r--spec/frontend/ide/stores/getters_spec.js6
-rw-r--r--spec/frontend/ide/stores/modules/commit/getters_spec.js38
-rw-r--r--spec/frontend/ide/stores/mutations/file_spec.js20
-rw-r--r--spec/frontend/ide/stores/mutations/merge_request_spec.js2
-rw-r--r--spec/frontend/ide/utils_spec.js6
-rw-r--r--spec/frontend/integrations/edit/components/dynamic_field_spec.js2
-rw-r--r--spec/frontend/integrations/overrides/components/integration_overrides_spec.js5
-rw-r--r--spec/frontend/invite_members/components/invite_members_modal_spec.js44
-rw-r--r--spec/frontend/invite_members/components/members_token_select_spec.js34
-rw-r--r--spec/frontend/invite_members/components/user_limit_notification_spec.js21
-rw-r--r--spec/frontend/invite_members/mock_data/api_responses.js15
-rw-r--r--spec/frontend/invite_members/mock_data/member_modal.js5
-rw-r--r--spec/frontend/issuable/components/related_issuable_item_spec.js233
-rw-r--r--spec/frontend/issuable/popover/components/issue_popover_spec.js62
-rw-r--r--spec/frontend/issuable/related_issues/components/add_issuable_form_spec.js4
-rw-r--r--spec/frontend/issuable/related_issues/components/related_issues_block_spec.js71
-rw-r--r--spec/frontend/issuable/related_issues/components/related_issues_root_spec.js367
-rw-r--r--spec/frontend/issues/list/components/issues_list_app_spec.js87
-rw-r--r--spec/frontend/issues/list/mock_data.js1
-rw-r--r--spec/frontend/issues/show/components/app_spec.js4
-rw-r--r--spec/frontend/issues/show/components/description_spec.js14
-rw-r--r--spec/frontend/issues/show/components/edit_actions_spec.js2
-rw-r--r--spec/frontend/issues/show/components/incidents/create_timeline_events_form_spec.js189
-rw-r--r--spec/frontend/issues/show/components/incidents/mock_data.js28
-rw-r--r--spec/frontend/issues/show/components/incidents/timeline_events_form_spec.js117
-rw-r--r--spec/frontend/issues/show/components/incidents/timeline_events_item_spec.js (renamed from spec/frontend/issues/show/components/incidents/timeline_events_list_item_spec.js)4
-rw-r--r--spec/frontend/issues/show/components/incidents/timeline_events_list_spec.js2
-rw-r--r--spec/frontend/issues/show/components/incidents/timeline_events_tab_spec.js14
-rw-r--r--spec/frontend/issues/show/components/incidents/utils_spec.js2
-rw-r--r--spec/frontend/jira_connect/branches/components/project_dropdown_spec.js2
-rw-r--r--spec/frontend/jira_connect/subscriptions/components/add_namespace_button_spec.js1
-rw-r--r--spec/frontend/jira_connect/subscriptions/components/sign_in_oauth_button_spec.js6
-rw-r--r--spec/frontend/jira_connect/subscriptions/pages/sign_in/sign_in_page_spec.js2
-rw-r--r--spec/frontend/jobs/components/job_log_controllers_spec.js148
-rw-r--r--spec/frontend/jobs/components/sidebar_detail_row_spec.js2
-rw-r--r--spec/frontend/labels/labels_select_spec.js6
-rw-r--r--spec/frontend/lib/dompurify_spec.js46
-rw-r--r--spec/frontend/lib/gfm/index_spec.js156
-rw-r--r--spec/frontend/lib/utils/common_utils_spec.js23
-rw-r--r--spec/frontend/lib/utils/confirm_via_gl_modal/confirm_modal_spec.js4
-rw-r--r--spec/frontend/lib/utils/rails_ujs_spec.js12
-rw-r--r--spec/frontend/lib/utils/recurrence_spec.js5
-rw-r--r--spec/frontend/lib/utils/sticky_spec.js6
-rw-r--r--spec/frontend/lib/utils/text_markdown_spec.js136
-rw-r--r--spec/frontend/lib/utils/url_utility_spec.js66
-rw-r--r--spec/frontend/members/components/action_buttons/access_request_action_buttons_spec.js4
-rw-r--r--spec/frontend/members/components/action_buttons/approve_access_request_button_spec.js4
-rw-r--r--spec/frontend/members/components/action_buttons/invite_action_buttons_spec.js4
-rw-r--r--spec/frontend/members/components/action_buttons/leave_button_spec.js4
-rw-r--r--spec/frontend/members/components/action_buttons/remove_group_link_button_spec.js2
-rw-r--r--spec/frontend/members/components/action_buttons/resend_invite_button_spec.js2
-rw-r--r--spec/frontend/members/components/action_buttons/user_action_buttons_spec.js4
-rw-r--r--spec/frontend/members/components/app_spec.js4
-rw-r--r--spec/frontend/members/components/avatars/group_avatar_spec.js2
-rw-r--r--spec/frontend/members/components/avatars/user_avatar_spec.js4
-rw-r--r--spec/frontend/members/components/filter_sort/filter_sort_container_spec.js4
-rw-r--r--spec/frontend/members/components/filter_sort/members_filtered_search_bar_spec.js2
-rw-r--r--spec/frontend/members/components/filter_sort/sort_dropdown_spec.js4
-rw-r--r--spec/frontend/members/components/modals/remove_group_link_modal_spec.js4
-rw-r--r--spec/frontend/members/components/modals/remove_member_modal_spec.js2
-rw-r--r--spec/frontend/members/components/table/created_at_spec.js2
-rw-r--r--spec/frontend/members/components/table/expiration_datepicker_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/components/table/members_table_spec.js20
-rw-r--r--spec/frontend/members/components/table/role_dropdown_spec.js4
-rw-r--r--spec/frontend/members/index_spec.js2
-rw-r--r--spec/frontend/monitoring/components/charts/anomaly_spec.js2
-rw-r--r--spec/frontend/monitoring/components/charts/bar_spec.js2
-rw-r--r--spec/frontend/monitoring/components/charts/column_spec.js2
-rw-r--r--spec/frontend/monitoring/components/charts/gauge_spec.js2
-rw-r--r--spec/frontend/monitoring/components/charts/heatmap_spec.js2
-rw-r--r--spec/frontend/monitoring/components/charts/single_stat_spec.js2
-rw-r--r--spec/frontend/monitoring/components/charts/stacked_column_spec.js4
-rw-r--r--spec/frontend/monitoring/components/charts/time_series_spec.js29
-rw-r--r--spec/frontend/monitoring/components/dashboard_actions_menu_spec.js4
-rw-r--r--spec/frontend/monitoring/components/dashboard_header_spec.js19
-rw-r--r--spec/frontend/monitoring/components/dashboard_panel_builder_spec.js22
-rw-r--r--spec/frontend/monitoring/components/dashboard_panel_spec.js43
-rw-r--r--spec/frontend/monitoring/components/dashboard_spec.js36
-rw-r--r--spec/frontend/monitoring/components/dashboard_url_time_spec.js3
-rw-r--r--spec/frontend/monitoring/components/dashboards_dropdown_spec.js14
-rw-r--r--spec/frontend/monitoring/components/duplicate_dashboard_form_spec.js2
-rw-r--r--spec/frontend/monitoring/components/duplicate_dashboard_modal_spec.js12
-rw-r--r--spec/frontend/monitoring/components/embeds/embed_group_spec.js20
-rw-r--r--spec/frontend/monitoring/components/embeds/metric_embed_spec.js8
-rw-r--r--spec/frontend/monitoring/components/empty_state_spec.js4
-rw-r--r--spec/frontend/monitoring/components/graph_group_spec.js8
-rw-r--r--spec/frontend/monitoring/components/group_empty_state_spec.js2
-rw-r--r--spec/frontend/monitoring/components/links_section_spec.js2
-rw-r--r--spec/frontend/monitoring/components/refresh_button_spec.js6
-rw-r--r--spec/frontend/monitoring/components/variables/dropdown_field_spec.js4
-rw-r--r--spec/frontend/monitoring/components/variables_section_spec.js4
-rw-r--r--spec/frontend/monitoring/pages/panel_new_page_spec.js4
-rw-r--r--spec/frontend/monitoring/router_spec.js10
-rw-r--r--spec/frontend/monitoring/store/utils_spec.js8
-rw-r--r--spec/frontend/nav/components/top_nav_dropdown_menu_spec.js2
-rw-r--r--spec/frontend/notes/components/comment_field_layout_spec.js4
-rw-r--r--spec/frontend/notes/components/diff_discussion_header_spec.js29
-rw-r--r--spec/frontend/notes/components/discussion_actions_spec.js22
-rw-r--r--spec/frontend/notes/components/discussion_counter_spec.js8
-rw-r--r--spec/frontend/notes/components/discussion_filter_note_spec.js4
-rw-r--r--spec/frontend/notes/components/discussion_notes_spec.js4
-rw-r--r--spec/frontend/notes/components/discussion_reply_placeholder_spec.js2
-rw-r--r--spec/frontend/notes/components/discussion_resolve_button_spec.js8
-rw-r--r--spec/frontend/notes/components/discussion_resolve_with_issue_button_spec.js2
-rw-r--r--spec/frontend/notes/components/note_actions/reply_button_spec.js2
-rw-r--r--spec/frontend/notes/components/note_actions_spec.js37
-rw-r--r--spec/frontend/notes/components/note_attachment_spec.js4
-rw-r--r--spec/frontend/notes/components/note_body_spec.js21
-rw-r--r--spec/frontend/notes/components/note_form_spec.js20
-rw-r--r--spec/frontend/notes/components/note_header_spec.js22
-rw-r--r--spec/frontend/notes/components/noteable_discussion_spec.js14
-rw-r--r--spec/frontend/notes/components/noteable_note_spec.js18
-rw-r--r--spec/frontend/notes/components/notes_app_spec.js7
-rw-r--r--spec/frontend/notes/components/sort_discussion_spec.js2
-rw-r--r--spec/frontend/notes/components/timeline_toggle_spec.js2
-rw-r--r--spec/frontend/notes/deprecated_notes_spec.js22
-rw-r--r--spec/frontend/operation_settings/components/metrics_settings_spec.js11
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/tags_list_spec.js5
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/cleanup_status_spec.js14
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/mock_data.js2
-rw-r--r--spec/frontend/packages_and_registries/dependency_proxy/app_spec.js18
-rw-r--r--spec/frontend/packages_and_registries/infrastructure_registry/components/shared/package_list_row_spec.js2
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/package_title_spec.js.snap29
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/pypi_installation_spec.js.snap196
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/package_files_spec.js192
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/package_history_spec.js32
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/package_title_spec.js20
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/pypi_installation_spec.js16
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/list/package_list_row_spec.js2
-rw-r--r--spec/frontend/packages_and_registries/package_registry/mock_data.js42
-rw-r--r--spec/frontend/packages_and_registries/package_registry/pages/details_spec.js187
-rw-r--r--spec/frontend/packages_and_registries/settings/project/settings/components/__snapshots__/container_expiration_policy_form_spec.js.snap3
-rw-r--r--spec/frontend/packages_and_registries/settings/project/settings/mock_data.js4
-rw-r--r--spec/frontend/pages/admin/application_settings/account_and_limits_spec.js8
-rw-r--r--spec/frontend/pages/groups/new/components/app_spec.js39
-rw-r--r--spec/frontend/pages/groups/new/components/create_group_description_details_spec.js57
-rw-r--r--spec/frontend/pages/projects/graphs/__snapshots__/code_coverage_spec.js.snap28
-rw-r--r--spec/frontend/pages/projects/graphs/code_coverage_spec.js21
-rw-r--r--spec/frontend/pages/projects/pipeline_schedules/shared/components/timezone_dropdown_spec.js1
-rw-r--r--spec/frontend/pages/projects/shared/permissions/components/settings_panel_spec.js20
-rw-r--r--spec/frontend/pages/shared/wikis/components/wiki_form_spec.js64
-rw-r--r--spec/frontend/pipeline_editor/components/drawer/cards/first_pipeline_card_spec.js12
-rw-r--r--spec/frontend/pipeline_editor/components/lint/ci_lint_spec.js72
-rw-r--r--spec/frontend/pipeline_editor/components/pipeline_editor_tabs_spec.js187
-rw-r--r--spec/frontend/pipeline_editor/components/validate/ci_validate_spec.js39
-rw-r--r--spec/frontend/pipeline_editor/pipeline_editor_home_spec.js6
-rw-r--r--spec/frontend/pipeline_new/components/pipeline_new_form_spec.js4
-rw-r--r--spec/frontend/pipeline_schedules/components/take_ownership_modal_spec.js54
-rw-r--r--spec/frontend/pipeline_wizard/components/editor_spec.js4
-rw-r--r--spec/frontend/pipeline_wizard/components/step_spec.js4
-rw-r--r--spec/frontend/pipeline_wizard/components/widgets/checklist_spec.js13
-rw-r--r--spec/frontend/pipelines/components/pipeline_tabs_spec.js4
-rw-r--r--spec/frontend/pipelines/components/pipelines_filtered_search_spec.js2
-rw-r--r--spec/frontend/pipelines/components/pipelines_list/pipeline_stage_spec.js59
-rw-r--r--spec/frontend/pipelines/graph/linked_pipeline_spec.js2
-rw-r--r--spec/frontend/pipelines/performance_insights_modal_spec.js25
-rw-r--r--spec/frontend/pipelines/pipeline_multi_actions_spec.js13
-rw-r--r--spec/frontend/pipelines/pipeline_url_spec.js4
-rw-r--r--spec/frontend/pipelines/pipelines_spec.js11
-rw-r--r--spec/frontend/pipelines/test_reports/test_reports_spec.js4
-rw-r--r--spec/frontend/projects/commits/components/author_select_spec.js10
-rw-r--r--spec/frontend/projects/compare/components/app_spec.js3
-rw-r--r--spec/frontend/projects/compare/components/mock_data.js25
-rw-r--r--spec/frontend/projects/project_new_spec.js67
-rw-r--r--spec/frontend/projects/settings/branch_rules/branch_dropdown_spec.js17
-rw-r--r--spec/frontend/projects/settings/branch_rules/components/protections/index_spec.js57
-rw-r--r--spec/frontend/projects/settings/branch_rules/components/protections/merge_protections_spec.js53
-rw-r--r--spec/frontend/projects/settings/branch_rules/components/protections/push_protections_spec.js50
-rw-r--r--spec/frontend/projects/settings/branch_rules/mock_data.js10
-rw-r--r--spec/frontend/projects/settings/branch_rules/rule_edit_spec.js61
-rw-r--r--spec/frontend/projects/settings/components/transfer_project_form_spec.js164
-rw-r--r--spec/frontend/prometheus_metrics/custom_metrics_spec.js48
-rw-r--r--spec/frontend/prometheus_metrics/prometheus_metrics_spec.js34
-rw-r--r--spec/frontend/releases/__snapshots__/util_spec.js.snap10
-rw-r--r--spec/frontend/releases/components/release_block_header_spec.js39
-rw-r--r--spec/frontend/reports/components/report_section_spec.js212
-rw-r--r--spec/frontend/repository/components/blob_content_viewer_spec.js93
-rw-r--r--spec/frontend/repository/mock_data.js1
-rw-r--r--spec/frontend/right_sidebar_spec.js2
-rw-r--r--spec/frontend/runner/admin_runner_edit/admin_runner_edit_app_spec.js2
-rw-r--r--spec/frontend/runner/admin_runner_show/admin_runner_show_app_spec.js13
-rw-r--r--spec/frontend/runner/admin_runners/admin_runners_app_spec.js214
-rw-r--r--spec/frontend/runner/components/cells/runner_summary_cell_spec.js13
-rw-r--r--spec/frontend/runner/components/registration/registration_token_spec.js27
-rw-r--r--spec/frontend/runner/components/runner_assigned_item_spec.js16
-rw-r--r--spec/frontend/runner/components/runner_bulk_delete_checkbox_spec.js101
-rw-r--r--spec/frontend/runner/components/runner_bulk_delete_spec.js176
-rw-r--r--spec/frontend/runner/components/runner_filtered_search_bar_spec.js6
-rw-r--r--spec/frontend/runner/components/runner_jobs_spec.js3
-rw-r--r--spec/frontend/runner/components/runner_list_spec.js8
-rw-r--r--spec/frontend/runner/components/runner_pagination_spec.js138
-rw-r--r--spec/frontend/runner/components/runner_projects_spec.js1
-rw-r--r--spec/frontend/runner/components/stat/runner_count_spec.js4
-rw-r--r--spec/frontend/runner/components/stat/runner_single_stat_spec.js61
-rw-r--r--spec/frontend/runner/components/stat/runner_stats_spec.js37
-rw-r--r--spec/frontend/runner/components/stat/runner_status_stat_spec.js67
-rw-r--r--spec/frontend/runner/graphql/local_state_spec.js62
-rw-r--r--spec/frontend/runner/group_runner_show/group_runner_show_app_spec.js9
-rw-r--r--spec/frontend/runner/group_runners/group_runners_app_spec.js56
-rw-r--r--spec/frontend/runner/mock_data.js43
-rw-r--r--spec/frontend/runner/runner_search_utils_spec.js40
-rw-r--r--spec/frontend/security_configuration/components/training_provider_list_spec.js2
-rw-r--r--spec/frontend/sidebar/assignees_spec.js2
-rw-r--r--spec/frontend/sidebar/components/assignees/sidebar_assignees_widget_spec.js1
-rw-r--r--spec/frontend/sidebar/components/attention_requested_toggle_spec.js121
-rw-r--r--spec/frontend/sidebar/components/confidential/sidebar_confidentiality_form_spec.js25
-rw-r--r--spec/frontend/sidebar/components/confidential/sidebar_confidentiality_widget_spec.js5
-rw-r--r--spec/frontend/sidebar/components/incidents/escalation_status_spec.js5
-rw-r--r--spec/frontend/sidebar/components/participants/sidebar_participants_widget_spec.js5
-rw-r--r--spec/frontend/sidebar/components/reviewers/uncollapsed_reviewer_list_spec.js36
-rw-r--r--spec/frontend/sidebar/mock_data.js12
-rw-r--r--spec/frontend/sidebar/reviewer_title_spec.js4
-rw-r--r--spec/frontend/sidebar/reviewers_spec.js64
-rw-r--r--spec/frontend/sidebar/sidebar_mediator_spec.js92
-rw-r--r--spec/frontend/snippets/components/__snapshots__/snippet_blob_edit_spec.js.snap1
-rw-r--r--spec/frontend/surveys/merge_request_performance/app_spec.js9
-rw-r--r--spec/frontend/test_setup.js5
-rw-r--r--spec/frontend/user_popovers_spec.js218
-rw-r--r--spec/frontend/vue_merge_request_widget/components/action_buttons.js (renamed from spec/frontend/vue_mr_widget/components/extensions/actions_spec.js)2
-rw-r--r--spec/frontend/vue_merge_request_widget/components/added_commit_message_spec.js (renamed from spec/frontend/vue_mr_widget/components/added_commit_message_spec.js)5
-rw-r--r--spec/frontend/vue_merge_request_widget/components/approvals/approvals_spec.js (renamed from spec/frontend/vue_mr_widget/components/approvals/approvals_spec.js)0
-rw-r--r--spec/frontend/vue_merge_request_widget/components/approvals/approvals_summary_optional_spec.js (renamed from spec/frontend/vue_mr_widget/components/approvals/approvals_summary_optional_spec.js)0
-rw-r--r--spec/frontend/vue_merge_request_widget/components/approvals/approvals_summary_spec.js (renamed from spec/frontend/vue_mr_widget/components/approvals/approvals_summary_spec.js)0
-rw-r--r--spec/frontend/vue_merge_request_widget/components/approvals/humanized_text_spec.js (renamed from spec/frontend/vue_mr_widget/components/approvals/humanized_text_spec.js)0
-rw-r--r--spec/frontend/vue_merge_request_widget/components/artifacts_list_app_spec.js (renamed from spec/frontend/vue_mr_widget/components/artifacts_list_app_spec.js)0
-rw-r--r--spec/frontend/vue_merge_request_widget/components/artifacts_list_spec.js (renamed from spec/frontend/vue_mr_widget/components/artifacts_list_spec.js)0
-rw-r--r--spec/frontend/vue_merge_request_widget/components/extensions/child_content_spec.js (renamed from spec/frontend/vue_mr_widget/components/extensions/child_content_spec.js)0
-rw-r--r--spec/frontend/vue_merge_request_widget/components/extensions/index_spec.js (renamed from spec/frontend/vue_mr_widget/components/extensions/index_spec.js)0
-rw-r--r--spec/frontend/vue_merge_request_widget/components/extensions/status_icon_spec.js (renamed from spec/frontend/vue_mr_widget/components/extensions/status_icon_spec.js)0
-rw-r--r--spec/frontend/vue_merge_request_widget/components/extensions/utils_spec.js (renamed from spec/frontend/vue_mr_widget/components/extensions/utils_spec.js)0
-rw-r--r--spec/frontend/vue_merge_request_widget/components/mr_collapsible_extension_spec.js (renamed from spec/frontend/vue_mr_widget/components/mr_collapsible_extension_spec.js)0
-rw-r--r--spec/frontend/vue_merge_request_widget/components/mr_widget_alert_message_spec.js (renamed from spec/frontend/vue_mr_widget/components/mr_widget_alert_message_spec.js)0
-rw-r--r--spec/frontend/vue_merge_request_widget/components/mr_widget_author_spec.js (renamed from spec/frontend/vue_mr_widget/components/mr_widget_author_spec.js)0
-rw-r--r--spec/frontend/vue_merge_request_widget/components/mr_widget_author_time_spec.js (renamed from spec/frontend/vue_mr_widget/components/mr_widget_author_time_spec.js)0
-rw-r--r--spec/frontend/vue_merge_request_widget/components/mr_widget_container_spec.js (renamed from spec/frontend/vue_mr_widget/components/mr_widget_container_spec.js)0
-rw-r--r--spec/frontend/vue_merge_request_widget/components/mr_widget_expandable_section_spec.js (renamed from spec/frontend/vue_mr_widget/components/mr_widget_expandable_section_spec.js)0
-rw-r--r--spec/frontend/vue_merge_request_widget/components/mr_widget_icon_spec.js (renamed from spec/frontend/vue_mr_widget/components/mr_widget_icon_spec.js)0
-rw-r--r--spec/frontend/vue_merge_request_widget/components/mr_widget_memory_usage_spec.js (renamed from spec/frontend/vue_mr_widget/components/mr_widget_memory_usage_spec.js)12
-rw-r--r--spec/frontend/vue_merge_request_widget/components/mr_widget_pipeline_container_spec.js (renamed from spec/frontend/vue_mr_widget/components/mr_widget_pipeline_container_spec.js)0
-rw-r--r--spec/frontend/vue_merge_request_widget/components/mr_widget_pipeline_spec.js (renamed from spec/frontend/vue_mr_widget/components/mr_widget_pipeline_spec.js)0
-rw-r--r--spec/frontend/vue_merge_request_widget/components/mr_widget_rebase_spec.js (renamed from spec/frontend/vue_mr_widget/components/mr_widget_rebase_spec.js)61
-rw-r--r--spec/frontend/vue_merge_request_widget/components/mr_widget_related_links_spec.js (renamed from spec/frontend/vue_mr_widget/components/mr_widget_related_links_spec.js)0
-rw-r--r--spec/frontend/vue_merge_request_widget/components/mr_widget_status_icon_spec.js (renamed from spec/frontend/vue_mr_widget/components/mr_widget_status_icon_spec.js)17
-rw-r--r--spec/frontend/vue_merge_request_widget/components/mr_widget_suggest_pipeline_spec.js (renamed from spec/frontend/vue_mr_widget/components/mr_widget_suggest_pipeline_spec.js)0
-rw-r--r--spec/frontend/vue_merge_request_widget/components/pipeline_tour_mock_data.js (renamed from spec/frontend/vue_mr_widget/components/pipeline_tour_mock_data.js)0
-rw-r--r--spec/frontend/vue_merge_request_widget/components/review_app_link_spec.js (renamed from spec/frontend/vue_mr_widget/components/review_app_link_spec.js)0
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/__snapshots__/mr_widget_auto_merge_enabled_spec.js.snap241
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/__snapshots__/mr_widget_pipeline_failed_spec.js.snap (renamed from spec/frontend/vue_mr_widget/components/states/__snapshots__/mr_widget_pipeline_failed_spec.js.snap)4
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/__snapshots__/new_ready_to_merge_spec.js.snap (renamed from spec/frontend/vue_mr_widget/components/states/__snapshots__/new_ready_to_merge_spec.js.snap)0
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/commit_edit_spec.js (renamed from spec/frontend/vue_mr_widget/components/states/commit_edit_spec.js)0
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/merge_checks_failed_spec.js (renamed from spec/frontend/vue_mr_widget/components/states/merge_checks_failed_spec.js)0
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/merge_failed_pipeline_confirmation_dialog_spec.js (renamed from spec/frontend/vue_mr_widget/components/states/merge_failed_pipeline_confirmation_dialog_spec.js)6
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/mr_widget_archived_spec.js (renamed from spec/frontend/vue_mr_widget/components/states/mr_widget_archived_spec.js)5
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled_spec.js (renamed from spec/frontend/vue_mr_widget/components/states/mr_widget_auto_merge_enabled_spec.js)116
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed_spec.js (renamed from spec/frontend/vue_mr_widget/components/states/mr_widget_auto_merge_failed_spec.js)6
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/mr_widget_checking_spec.js (renamed from spec/frontend/vue_mr_widget/components/states/mr_widget_checking_spec.js)4
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/mr_widget_closed_spec.js (renamed from spec/frontend/vue_mr_widget/components/states/mr_widget_closed_spec.js)24
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/mr_widget_commit_message_dropdown_spec.js (renamed from spec/frontend/vue_mr_widget/components/states/mr_widget_commit_message_dropdown_spec.js)0
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/mr_widget_commits_header_spec.js (renamed from spec/frontend/vue_mr_widget/components/states/mr_widget_commits_header_spec.js)7
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/mr_widget_conflicts_spec.js (renamed from spec/frontend/vue_mr_widget/components/states/mr_widget_conflicts_spec.js)4
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/mr_widget_failed_to_merge_spec.js (renamed from spec/frontend/vue_mr_widget/components/states/mr_widget_failed_to_merge_spec.js)28
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/mr_widget_merged_spec.js (renamed from spec/frontend/vue_mr_widget/components/states/mr_widget_merged_spec.js)57
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/mr_widget_merging_spec.js (renamed from spec/frontend/vue_mr_widget/components/states/mr_widget_merging_spec.js)13
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/mr_widget_missing_branch_spec.js (renamed from spec/frontend/vue_mr_widget/components/states/mr_widget_missing_branch_spec.js)0
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/mr_widget_not_allowed_spec.js (renamed from spec/frontend/vue_mr_widget/components/states/mr_widget_not_allowed_spec.js)0
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/mr_widget_nothing_to_merge_spec.js (renamed from spec/frontend/vue_mr_widget/components/states/mr_widget_nothing_to_merge_spec.js)2
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked_spec.js (renamed from spec/frontend/vue_mr_widget/components/states/mr_widget_pipeline_blocked_spec.js)0
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/mr_widget_pipeline_failed_spec.js (renamed from spec/frontend/vue_mr_widget/components/states/mr_widget_pipeline_failed_spec.js)7
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/mr_widget_ready_to_merge_spec.js (renamed from spec/frontend/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js)164
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/mr_widget_sha_mismatch_spec.js (renamed from spec/frontend/vue_mr_widget/components/states/mr_widget_sha_mismatch_spec.js)0
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/mr_widget_squash_before_merge_spec.js (renamed from spec/frontend/vue_mr_widget/components/states/mr_widget_squash_before_merge_spec.js)0
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/mr_widget_unresolved_discussions_spec.js (renamed from spec/frontend/vue_mr_widget/components/states/mr_widget_unresolved_discussions_spec.js)0
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/mr_widget_wip_spec.js (renamed from spec/frontend/vue_mr_widget/components/states/mr_widget_wip_spec.js)16
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/new_ready_to_merge_spec.js (renamed from spec/frontend/vue_mr_widget/components/states/new_ready_to_merge_spec.js)0
-rw-r--r--spec/frontend/vue_merge_request_widget/components/terraform/mock_data.js (renamed from spec/frontend/vue_mr_widget/components/terraform/mock_data.js)0
-rw-r--r--spec/frontend/vue_merge_request_widget/components/terraform/mr_widget_terraform_container_spec.js (renamed from spec/frontend/vue_mr_widget/components/terraform/mr_widget_terraform_container_spec.js)0
-rw-r--r--spec/frontend/vue_merge_request_widget/components/terraform/terraform_plan_spec.js (renamed from spec/frontend/vue_mr_widget/components/terraform/terraform_plan_spec.js)0
-rw-r--r--spec/frontend/vue_merge_request_widget/components/widget/app_spec.js19
-rw-r--r--spec/frontend/vue_merge_request_widget/components/widget/widget_spec.js167
-rw-r--r--spec/frontend/vue_merge_request_widget/deployment/deployment_action_button_spec.js (renamed from spec/frontend/vue_mr_widget/deployment/deployment_action_button_spec.js)0
-rw-r--r--spec/frontend/vue_merge_request_widget/deployment/deployment_actions_spec.js (renamed from spec/frontend/vue_mr_widget/deployment/deployment_actions_spec.js)0
-rw-r--r--spec/frontend/vue_merge_request_widget/deployment/deployment_list_spec.js (renamed from spec/frontend/vue_mr_widget/deployment/deployment_list_spec.js)0
-rw-r--r--spec/frontend/vue_merge_request_widget/deployment/deployment_mock_data.js (renamed from spec/frontend/vue_mr_widget/deployment/deployment_mock_data.js)0
-rw-r--r--spec/frontend/vue_merge_request_widget/deployment/deployment_spec.js (renamed from spec/frontend/vue_mr_widget/deployment/deployment_spec.js)0
-rw-r--r--spec/frontend/vue_merge_request_widget/deployment/deployment_view_button_spec.js (renamed from spec/frontend/vue_mr_widget/deployment/deployment_view_button_spec.js)0
-rw-r--r--spec/frontend/vue_merge_request_widget/extensions/test_report/index_spec.js (renamed from spec/frontend/vue_mr_widget/extensions/test_report/index_spec.js)0
-rw-r--r--spec/frontend/vue_merge_request_widget/extensions/test_report/utils_spec.js (renamed from spec/frontend/vue_mr_widget/extensions/test_report/utils_spec.js)0
-rw-r--r--spec/frontend/vue_merge_request_widget/extentions/accessibility/index_spec.js (renamed from spec/frontend/vue_mr_widget/extentions/accessibility/index_spec.js)0
-rw-r--r--spec/frontend/vue_merge_request_widget/extentions/accessibility/mock_data.js (renamed from spec/frontend/vue_mr_widget/extentions/accessibility/mock_data.js)0
-rw-r--r--spec/frontend/vue_merge_request_widget/extentions/code_quality/index_spec.js (renamed from spec/frontend/vue_mr_widget/extentions/code_quality/index_spec.js)0
-rw-r--r--spec/frontend/vue_merge_request_widget/extentions/code_quality/mock_data.js (renamed from spec/frontend/vue_mr_widget/extentions/code_quality/mock_data.js)0
-rw-r--r--spec/frontend/vue_merge_request_widget/extentions/terraform/index_spec.js (renamed from spec/frontend/vue_mr_widget/extentions/terraform/index_spec.js)0
-rw-r--r--spec/frontend/vue_merge_request_widget/mock_data.js (renamed from spec/frontend/vue_mr_widget/mock_data.js)0
-rw-r--r--spec/frontend/vue_merge_request_widget/mr_widget_how_to_merge_modal_spec.js (renamed from spec/frontend/vue_mr_widget/mr_widget_how_to_merge_modal_spec.js)0
-rw-r--r--spec/frontend/vue_merge_request_widget/mr_widget_options_spec.js (renamed from spec/frontend/vue_mr_widget/mr_widget_options_spec.js)82
-rw-r--r--spec/frontend/vue_merge_request_widget/stores/artifacts_list/actions_spec.js (renamed from spec/frontend/vue_mr_widget/stores/artifacts_list/actions_spec.js)0
-rw-r--r--spec/frontend/vue_merge_request_widget/stores/artifacts_list/getters_spec.js (renamed from spec/frontend/vue_mr_widget/stores/artifacts_list/getters_spec.js)0
-rw-r--r--spec/frontend/vue_merge_request_widget/stores/artifacts_list/mutations_spec.js (renamed from spec/frontend/vue_mr_widget/stores/artifacts_list/mutations_spec.js)0
-rw-r--r--spec/frontend/vue_merge_request_widget/stores/get_state_key_spec.js (renamed from spec/frontend/vue_mr_widget/stores/get_state_key_spec.js)22
-rw-r--r--spec/frontend/vue_merge_request_widget/stores/mr_widget_store_spec.js (renamed from spec/frontend/vue_mr_widget/stores/mr_widget_store_spec.js)0
-rw-r--r--spec/frontend/vue_merge_request_widget/test_extensions.js (renamed from spec/frontend/vue_mr_widget/test_extensions.js)0
-rw-r--r--spec/frontend/vue_mr_widget/components/states/__snapshots__/mr_widget_auto_merge_enabled_spec.js.snap145
-rw-r--r--spec/frontend/vue_shared/alert_details/alert_details_spec.js6
-rw-r--r--spec/frontend/vue_shared/alert_details/alert_metrics_spec.js4
-rw-r--r--spec/frontend/vue_shared/alert_details/alert_status_spec.js2
-rw-r--r--spec/frontend/vue_shared/alert_details/sidebar/alert_sidebar_assignees_spec.js4
-rw-r--r--spec/frontend/vue_shared/alert_details/sidebar/alert_sidebar_spec.js4
-rw-r--r--spec/frontend/vue_shared/alert_details/system_notes/alert_management_system_note_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/actions_button_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/alert_details_table_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/blob_viewers/rich_viewer_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/changed_file_icon_spec.js6
-rw-r--r--spec/frontend/vue_shared/components/ci_icon_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/clipboard_button_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/clone_dropdown_spec.js12
-rw-r--r--spec/frontend/vue_shared/components/color_picker/color_picker_spec.js10
-rw-r--r--spec/frontend/vue_shared/components/commit_spec.js6
-rw-r--r--spec/frontend/vue_shared/components/confirm_modal_spec.js6
-rw-r--r--spec/frontend/vue_shared/components/dismissible_alert_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/dismissible_container_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/dropdown/dropdown_button_spec.js77
-rw-r--r--spec/frontend/vue_shared/components/dropdown/dropdown_widget_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/expand_button_spec.js22
-rw-r--r--spec/frontend/vue_shared/components/file_icon_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/file_row_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/file_tree_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_bar_root_spec.js44
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/mock_data.js8
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/author_token_spec.js12
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/branch_token_spec.js14
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/crm_contact_token_spec.js18
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/crm_organization_token_spec.js18
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/emoji_token_spec.js22
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/label_token_spec.js18
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/milestone_token_spec.js18
-rw-r--r--spec/frontend/vue_shared/components/gitlab_version_check_spec.js31
-rw-r--r--spec/frontend/vue_shared/components/gl_modal_vuex_spec.js10
-rw-r--r--spec/frontend/vue_shared/components/help_popover_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/integration_help_text_spec.js12
-rw-r--r--spec/frontend/vue_shared/components/markdown/field_spec.js7
-rw-r--r--spec/frontend/vue_shared/components/markdown/header_spec.js17
-rw-r--r--spec/frontend/vue_shared/components/markdown/suggestion_diff_header_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/markdown/suggestion_diff_spec.js6
-rw-r--r--spec/frontend/vue_shared/components/markdown/toolbar_button_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/memory_graph_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/metric_images/metric_images_tab_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/namespace_select/namespace_select_spec.js81
-rw-r--r--spec/frontend/vue_shared/components/navigation_tabs_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/notes/noteable_warning_spec.js13
-rw-r--r--spec/frontend/vue_shared/components/notes/placeholder_note_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs_spec.js18
-rw-r--r--spec/frontend/vue_shared/components/pagination_bar/pagination_bar_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/pagination_links_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/project_avatar_spec.js36
-rw-r--r--spec/frontend/vue_shared/components/project_selector/project_list_item_spec.js1
-rw-r--r--spec/frontend/vue_shared/components/project_selector/project_selector_spec.js6
-rw-r--r--spec/frontend/vue_shared/components/registry/code_instruction_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/registry/details_row_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/registry/history_item_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/registry/list_item_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/registry/metadata_item_spec.js6
-rw-r--r--spec/frontend/vue_shared/components/registry/registry_search_spec.js6
-rw-r--r--spec/frontend/vue_shared/components/registry/title_area_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/rich_timestamp_tooltip_spec.js41
-rw-r--r--spec/frontend/vue_shared/components/runner_instructions/runner_instructions_modal_spec.js6
-rw-r--r--spec/frontend/vue_shared/components/security_reports/artifact_downloads/merge_request_artifact_download_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/security_reports/help_icon_spec.js6
-rw-r--r--spec/frontend/vue_shared/components/sidebar/issuable_move_dropdown_spec.js32
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_button_spec.js6
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_create_view_spec.js16
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view_spec.js16
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_title_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_vue/label_item_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_vue/labels_select_root_spec.js14
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_widget/labels_select_root_spec.js1
-rw-r--r--spec/frontend/vue_shared/components/sidebar/todo_button_spec.js12
-rw-r--r--spec/frontend/vue_shared/components/source_editor_spec.js26
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/components/chunk_line_spec.js29
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/components/chunk_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/plugins/link_dependencies_spec.js9
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/plugins/mock_data.js2
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/plugins/utils/dependency_linker_util_spec.js5
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/plugins/utils/gemspec_linker_spec.js14
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/source_viewer_spec.js3
-rw-r--r--spec/frontend/vue_shared/components/split_button_spec.js5
-rw-r--r--spec/frontend/vue_shared/components/table_pagination_spec.js6
-rw-r--r--spec/frontend/vue_shared/components/tooltip_on_truncate_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/upload_dropzone/upload_dropzone_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/user_access_role_badge_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/user_avatar/user_avatar_image_new_spec.js45
-rw-r--r--spec/frontend/vue_shared/components/user_avatar/user_avatar_image_spec.js64
-rw-r--r--spec/frontend/vue_shared/components/user_avatar/user_avatar_link_new_spec.js1
-rw-r--r--spec/frontend/vue_shared/components/user_avatar/user_avatar_link_old_spec.js1
-rw-r--r--spec/frontend/vue_shared/components/user_avatar/user_avatar_link_spec.js64
-rw-r--r--spec/frontend/vue_shared/components/user_avatar/user_avatar_list_spec.js14
-rw-r--r--spec/frontend/vue_shared/components/user_popover/user_popover_spec.js39
-rw-r--r--spec/frontend/vue_shared/components/user_select_spec.js95
-rw-r--r--spec/frontend/vue_shared/components/web_ide_link_spec.js10
-rw-r--r--spec/frontend/vue_shared/issuable/create/components/issuable_create_root_spec.js4
-rw-r--r--spec/frontend/vue_shared/issuable/create/components/issuable_form_spec.js14
-rw-r--r--spec/frontend/vue_shared/issuable/list/components/issuable_item_spec.js48
-rw-r--r--spec/frontend/vue_shared/issuable/list/components/issuable_list_root_spec.js12
-rw-r--r--spec/frontend/vue_shared/issuable/list/mock_data.js1
-rw-r--r--spec/frontend/vue_shared/issuable/show/components/issuable_body_spec.js10
-rw-r--r--spec/frontend/vue_shared/issuable/show/components/issuable_edit_form_spec.js6
-rw-r--r--spec/frontend/vue_shared/issuable/show/components/issuable_header_spec.js12
-rw-r--r--spec/frontend/vue_shared/issuable/show/components/issuable_show_root_spec.js22
-rw-r--r--spec/frontend/vue_shared/security_reports/components/security_report_download_dropdown_spec.js4
-rw-r--r--spec/frontend/vue_shared/security_reports/security_reports_app_spec.js4
-rw-r--r--spec/frontend/whats_new/components/app_spec.js4
-rw-r--r--spec/frontend/whats_new/components/feature_spec.js9
-rw-r--r--spec/frontend/work_items/components/item_state_spec.js16
-rw-r--r--spec/frontend/work_items/components/item_title_spec.js2
-rw-r--r--spec/frontend/work_items/components/work_item_actions_spec.js85
-rw-r--r--spec/frontend/work_items/components/work_item_assignees_spec.js120
-rw-r--r--spec/frontend/work_items/components/work_item_detail_modal_spec.js134
-rw-r--r--spec/frontend/work_items/components/work_item_links/work_item_links_form_spec.js87
-rw-r--r--spec/frontend/work_items/components/work_item_links/work_item_links_menu_spec.js119
-rw-r--r--spec/frontend/work_items/components/work_item_links/work_item_links_spec.js205
-rw-r--r--spec/frontend/work_items/components/work_item_state_spec.js16
-rw-r--r--spec/frontend/work_items/components/work_item_title_spec.js21
-rw-r--r--spec/frontend/work_items/components/work_item_type_icon_spec.js47
-rw-r--r--spec/frontend/work_items/components/work_item_weight_spec.js87
-rw-r--r--spec/frontend/work_items/mock_data.js218
-rw-r--r--spec/frontend/work_items/pages/work_item_detail_spec.js213
-rw-r--r--spec/frontend/work_items_hierarchy/components/app_spec.js8
-rw-r--r--spec/frontend/work_items_hierarchy/components/hierarchy_spec.js7
667 files changed, 10900 insertions, 5552 deletions
diff --git a/spec/frontend/__helpers__/mock_apollo_helper.js b/spec/frontend/__helpers__/mock_apollo_helper.js
index bae9f33be87..e0739df7086 100644
--- a/spec/frontend/__helpers__/mock_apollo_helper.js
+++ b/spec/frontend/__helpers__/mock_apollo_helper.js
@@ -8,7 +8,6 @@ export function createMockClient(handlers = [], resolvers = {}, cacheOptions = {
const cache = new InMemoryCache({
possibleTypes,
typePolicies,
- addTypename: false,
...cacheOptions,
});
diff --git a/spec/frontend/__helpers__/mock_dom_observer.js b/spec/frontend/__helpers__/mock_dom_observer.js
index bc2646be4c2..8c9c435041e 100644
--- a/spec/frontend/__helpers__/mock_dom_observer.js
+++ b/spec/frontend/__helpers__/mock_dom_observer.js
@@ -22,14 +22,12 @@ class MockObserver {
takeRecords() {}
- // eslint-disable-next-line camelcase
$_triggerObserve(node, { entry = {}, options = {} } = {}) {
if (this.$_hasObserver(node, options)) {
this.$_cb([{ target: node, ...entry }]);
}
}
- // eslint-disable-next-line camelcase
$_hasObserver(node, options = {}) {
return this.$_observers.some(
([obvNode, obvOptions]) => node === obvNode && isMatch(options, obvOptions),
diff --git a/spec/frontend/__helpers__/mocks/axios_utils.js b/spec/frontend/__helpers__/mocks/axios_utils.js
index b1efd29dc8d..60644c84a57 100644
--- a/spec/frontend/__helpers__/mocks/axios_utils.js
+++ b/spec/frontend/__helpers__/mocks/axios_utils.js
@@ -1,4 +1,6 @@
import EventEmitter from 'events';
+// eslint-disable-next-line no-restricted-syntax
+import { setImmediate } from 'timers';
const axios = jest.requireActual('~/lib/utils/axios_utils').default;
diff --git a/spec/frontend/__helpers__/stub_component.js b/spec/frontend/__helpers__/stub_component.js
index 96fe3a8bc45..4f9d1ee6f5d 100644
--- a/spec/frontend/__helpers__/stub_component.js
+++ b/spec/frontend/__helpers__/stub_component.js
@@ -22,6 +22,14 @@ const createStubbedMethods = (methods = {}) => {
);
};
+export const RENDER_ALL_SLOTS_TEMPLATE = `<div>
+ <template v-for="(_, name) in $scopedSlots">
+ <div :data-testid="'slot-' + name">
+ <slot :name="name" />
+ </div>
+ </template>
+</div>`;
+
export function stubComponent(Component, options = {}) {
return {
props: Component.props,
diff --git a/spec/frontend/__helpers__/timeout.js b/spec/frontend/__helpers__/timeout.js
deleted file mode 100644
index 8688625a95e..00000000000
--- a/spec/frontend/__helpers__/timeout.js
+++ /dev/null
@@ -1,59 +0,0 @@
-const NS_PER_SEC = 1e9;
-const NS_PER_MS = 1e6;
-const IS_DEBUGGING = process.execArgv.join(' ').includes('--inspect-brk');
-
-let testTimeoutNS;
-
-export const setTestTimeout = (newTimeoutMS) => {
- const newTimeoutNS = newTimeoutMS * NS_PER_MS;
- // never accept a smaller timeout than the default
- if (newTimeoutNS < testTimeoutNS) {
- return;
- }
-
- testTimeoutNS = newTimeoutNS;
- jest.setTimeout(newTimeoutMS);
-};
-
-// Allows slow tests to set their own timeout.
-// Useful for tests with jQuery, which is very slow in big DOMs.
-let temporaryTimeoutNS = null;
-export const setTestTimeoutOnce = (newTimeoutMS) => {
- const newTimeoutNS = newTimeoutMS * NS_PER_MS;
- // never accept a smaller timeout than the default
- if (newTimeoutNS < testTimeoutNS) {
- return;
- }
-
- temporaryTimeoutNS = newTimeoutNS;
-};
-
-export const initializeTestTimeout = (defaultTimeoutMS) => {
- setTestTimeout(defaultTimeoutMS);
-
- let testStartTime;
-
- // https://github.com/facebook/jest/issues/6947
- beforeEach(() => {
- testStartTime = process.hrtime();
- });
-
- afterEach(() => {
- let timeoutNS = testTimeoutNS;
- if (Number.isFinite(temporaryTimeoutNS)) {
- timeoutNS = temporaryTimeoutNS;
- temporaryTimeoutNS = null;
- }
-
- const [seconds, remainingNs] = process.hrtime(testStartTime);
- const elapsedNS = seconds * NS_PER_SEC + remainingNs;
-
- // Disable the timeout error when debugging. It is meaningless because
- // debugging always takes longer than the test timeout.
- if (elapsedNS > timeoutNS && !IS_DEBUGGING) {
- throw new Error(
- `Test took too long (${elapsedNS / NS_PER_MS}ms > ${timeoutNS / NS_PER_MS}ms)!`,
- );
- }
- });
-};
diff --git a/spec/frontend/__helpers__/vue_mount_component_helper.js b/spec/frontend/__helpers__/vue_mount_component_helper.js
index 615ff69a01c..ed43355ea5b 100644
--- a/spec/frontend/__helpers__/vue_mount_component_helper.js
+++ b/spec/frontend/__helpers__/vue_mount_component_helper.js
@@ -1,5 +1,3 @@
-import Vue from 'vue';
-
/**
* Deprecated. Please do not use.
* Please see https://gitlab.com/groups/gitlab-org/-/epics/2445
@@ -33,31 +31,4 @@ export const mountComponentWithStore = (Component, { el, props, store }) =>
* Deprecated. Please do not use.
* Please see https://gitlab.com/groups/gitlab-org/-/epics/2445
*/
-export const mountComponentWithSlots = (Component, { props, slots }) => {
- const component = new Component({
- propsData: props || {},
- });
-
- component.$slots = slots;
-
- return component.$mount();
-};
-
-/**
- * Mount a component with the given render method.
- *
- * -----------------------------
- * Deprecated. Please do not use.
- * Please see https://gitlab.com/groups/gitlab-org/-/epics/2445
- * -----------------------------
- *
- * This helps with inserting slots that need to be compiled.
- */
-export const mountComponentWithRender = (render, el = null) =>
- mountComponent(Vue.extend({ render }), {}, 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.js b/spec/frontend/__helpers__/vue_test_utils_helper.js
index 2aae91f8a39..75bd5df8cbf 100644
--- a/spec/frontend/__helpers__/vue_test_utils_helper.js
+++ b/spec/frontend/__helpers__/vue_test_utils_helper.js
@@ -7,6 +7,20 @@ const vNodeContainsText = (vnode, text) =>
(vnode.children && vnode.children.filter((child) => vNodeContainsText(child, text)).length);
/**
+ * Create a VTU wrapper from an element.
+ *
+ * If a Vue instance manages the element, the wrapper is created
+ * with that Vue instance.
+ *
+ * @param {HTMLElement} element
+ * @param {Object} options
+ * @returns VTU wrapper
+ */
+const createWrapperFromElement = (element, options) =>
+ // eslint-disable-next-line no-underscore-dangle
+ createWrapper(element.__vue__ || element, options || {});
+
+/**
* Determines whether a `shallowMount` Wrapper contains text
* within one of it's slots. This will also work on Wrappers
* acquired with `find()`, but only if it's parent Wrapper
@@ -85,8 +99,7 @@ export const extendedWrapper = (wrapper) => {
if (!elements.length) {
return new ErrorWrapper(query);
}
-
- return createWrapper(elements[0], this.options || {});
+ return createWrapperFromElement(elements[0], this.options);
},
},
};
@@ -104,7 +117,7 @@ export const extendedWrapper = (wrapper) => {
);
const wrappers = elements.map((element) => {
- const elementWrapper = createWrapper(element, this.options || {});
+ const elementWrapper = createWrapperFromElement(element, this.options);
elementWrapper.selector = text;
return elementWrapper;
diff --git a/spec/frontend/__helpers__/vue_test_utils_helper_spec.js b/spec/frontend/__helpers__/vue_test_utils_helper_spec.js
index 3bb228f94b8..ae180c3b49d 100644
--- a/spec/frontend/__helpers__/vue_test_utils_helper_spec.js
+++ b/spec/frontend/__helpers__/vue_test_utils_helper_spec.js
@@ -6,6 +6,7 @@ import {
WrapperArray as VTUWrapperArray,
ErrorWrapper as VTUErrorWrapper,
} from '@vue/test-utils';
+import Vue from 'vue';
import {
extendedWrapper,
shallowMountExtended,
@@ -139,9 +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 wrapper;
beforeEach(() => {
+ jest.spyOn(vtu, 'createWrapper');
+
wrapper = extendedWrapper(
shallowMount({
template: `<div>foo bar</div>`,
@@ -164,7 +168,6 @@ describe('Vue test utils helpers', () => {
describe('when element is found', () => {
beforeEach(() => {
jest.spyOn(testingLibrary, expectedQuery).mockImplementation(() => [mockDiv]);
- jest.spyOn(vtu, 'createWrapper');
});
it('returns a VTU wrapper', () => {
@@ -172,14 +175,27 @@ describe('Vue test utils helpers', () => {
expect(vtu.createWrapper).toHaveBeenCalledWith(mockDiv, wrapper.options);
expect(result).toBeInstanceOf(VTUWrapper);
+ expect(result.vm).toBeUndefined();
});
});
+ describe('when a Vue instance element is found', () => {
+ beforeEach(() => {
+ jest.spyOn(testingLibrary, expectedQuery).mockImplementation(() => [mockVm.$el]);
+ });
+
+ it('returns a VTU wrapper', () => {
+ const result = wrapper[findMethod](text, options);
+
+ expect(vtu.createWrapper).toHaveBeenCalledWith(mockVm, wrapper.options);
+ expect(result).toBeInstanceOf(VTUWrapper);
+ expect(result.vm).toBeInstanceOf(Vue);
+ });
+ });
describe('when multiple elements are found', () => {
beforeEach(() => {
const mockSpan = document.createElement('span');
jest.spyOn(testingLibrary, expectedQuery).mockImplementation(() => [mockDiv, mockSpan]);
- jest.spyOn(vtu, 'createWrapper');
});
it('returns the first element as a VTU wrapper', () => {
@@ -187,6 +203,24 @@ describe('Vue test utils helpers', () => {
expect(vtu.createWrapper).toHaveBeenCalledWith(mockDiv, wrapper.options);
expect(result).toBeInstanceOf(VTUWrapper);
+ expect(result.vm).toBeUndefined();
+ });
+ });
+
+ describe('when multiple Vue instances are found', () => {
+ beforeEach(() => {
+ const mockVm2 = new Vue({ render: (h) => h('span') }).$mount();
+ jest
+ .spyOn(testingLibrary, expectedQuery)
+ .mockImplementation(() => [mockVm.$el, mockVm2.$el]);
+ });
+
+ it('returns the first element as a VTU wrapper', () => {
+ const result = wrapper[findMethod](text, options);
+
+ expect(vtu.createWrapper).toHaveBeenCalledWith(mockVm, wrapper.options);
+ expect(result).toBeInstanceOf(VTUWrapper);
+ expect(result.vm).toBeInstanceOf(Vue);
});
});
@@ -211,12 +245,17 @@ describe('Vue test utils helpers', () => {
${'findAllByAltText'} | ${'queryAllByAltText'}
`('$findMethod', ({ findMethod, expectedQuery }) => {
const text = 'foo bar';
- const options = { selector: 'div' };
+ const options = { selector: 'li' };
const mockElements = [
document.createElement('li'),
document.createElement('li'),
document.createElement('li'),
];
+ const mockVms = [
+ new Vue({ render: (h) => h('li') }).$mount(),
+ new Vue({ render: (h) => h('li') }).$mount(),
+ new Vue({ render: (h) => h('li') }).$mount(),
+ ];
let wrapper;
beforeEach(() => {
@@ -245,9 +284,13 @@ describe('Vue test utils helpers', () => {
);
});
- describe('when elements are found', () => {
+ describe.each`
+ case | mockResult | isVueInstance
+ ${'HTMLElements'} | ${mockElements} | ${false}
+ ${'Vue instance elements'} | ${mockVms} | ${true}
+ `('when $case are found', ({ mockResult, isVueInstance }) => {
beforeEach(() => {
- jest.spyOn(testingLibrary, expectedQuery).mockImplementation(() => mockElements);
+ jest.spyOn(testingLibrary, expectedQuery).mockImplementation(() => mockResult);
});
it('returns a VTU wrapper array', () => {
@@ -257,7 +300,9 @@ describe('Vue test utils helpers', () => {
expect(
result.wrappers.every(
(resultWrapper) =>
- resultWrapper instanceof VTUWrapper && resultWrapper.options === wrapper.options,
+ resultWrapper instanceof VTUWrapper &&
+ resultWrapper.vm instanceof Vue === isVueInstance &&
+ resultWrapper.options === wrapper.options,
),
).toBe(true);
expect(result.length).toBe(3);
diff --git a/spec/frontend/__helpers__/vuex_action_helper.js b/spec/frontend/__helpers__/vuex_action_helper.js
index ab2637d6024..bdd5a0a9034 100644
--- a/spec/frontend/__helpers__/vuex_action_helper.js
+++ b/spec/frontend/__helpers__/vuex_action_helper.js
@@ -1,5 +1,7 @@
-/**
- * Helper for testing action with expected mutations inspired in
+// eslint-disable-next-line no-restricted-syntax
+import { setImmediate } from 'timers';
+
+/** Helper for testing action with expected mutations inspired in
* https://vuex.vuejs.org/en/testing.html
*
* @param {(Function|Object)} action to be tested, or object of named parameters
diff --git a/spec/frontend/__helpers__/vuex_action_helper_spec.js b/spec/frontend/__helpers__/vuex_action_helper_spec.js
index 5bb2b3b26e2..182aea9c1c5 100644
--- a/spec/frontend/__helpers__/vuex_action_helper_spec.js
+++ b/spec/frontend/__helpers__/vuex_action_helper_spec.js
@@ -76,7 +76,7 @@ describe.each([testActionFn, testActionFnWithOptionsArg])(
const promise = testAction(() => {}, null, {}, assertion.mutations, assertion.actions);
- originalExpect(promise instanceof Promise).toBeTruthy();
+ originalExpect(promise instanceof Promise).toBe(true);
return promise;
});
diff --git a/spec/frontend/__helpers__/wait_for_promises.js b/spec/frontend/__helpers__/wait_for_promises.js
index 753c3c5d92b..5a15b8b74b5 100644
--- a/spec/frontend/__helpers__/wait_for_promises.js
+++ b/spec/frontend/__helpers__/wait_for_promises.js
@@ -1,4 +1,2 @@
-export default () =>
- new Promise((resolve) => {
- requestAnimationFrame(resolve);
- });
+// eslint-disable-next-line no-restricted-syntax
+export default () => new Promise(jest.requireActual('timers').setImmediate);
diff --git a/spec/frontend/__helpers__/web_worker_transformer.js b/spec/frontend/__helpers__/web_worker_transformer.js
index 5b2f7d77947..767ab3f5675 100644
--- a/spec/frontend/__helpers__/web_worker_transformer.js
+++ b/spec/frontend/__helpers__/web_worker_transformer.js
@@ -6,7 +6,7 @@ const babelJestTransformer = require('babel-jest');
// [1]: https://webpack.js.org/loaders/worker-loader/
module.exports = {
process: (contentArg, filename, ...args) => {
- const { code: content } = babelJestTransformer.process(contentArg, filename, ...args);
+ const { code: content } = babelJestTransformer.default.process(contentArg, filename, ...args);
return `const { FakeWebWorker } = require("helpers/web_worker_fake");
module.exports = class JestTransformedWorker extends FakeWebWorker {
diff --git a/spec/frontend/__mocks__/monaco-editor/index.js b/spec/frontend/__mocks__/monaco-editor/index.js
index 18b7df32f9b..384f9993150 100644
--- a/spec/frontend/__mocks__/monaco-editor/index.js
+++ b/spec/frontend/__mocks__/monaco-editor/index.js
@@ -15,4 +15,3 @@ 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';
-export default global.monaco;
diff --git a/spec/frontend/access_tokens/components/__snapshots__/expires_at_field_spec.js.snap b/spec/frontend/access_tokens/components/__snapshots__/expires_at_field_spec.js.snap
index 36003154b58..2bd2b17a12d 100644
--- a/spec/frontend/access_tokens/components/__snapshots__/expires_at_field_spec.js.snap
+++ b/spec/frontend/access_tokens/components/__snapshots__/expires_at_field_spec.js.snap
@@ -11,22 +11,17 @@ exports[`~/access_tokens/components/expires_at_field should render datepicker wi
arialabel=""
autocomplete=""
container=""
+ data-qa-selector="expiry_date_field"
+ defaultdate="Wed Aug 05 2020 00:00:00 GMT+0000 (Greenwich Mean Time)"
displayfield="true"
firstday="0"
+ inputid="personal_access_token_expires_at"
inputlabel="Enter date"
+ inputname="personal_access_token[expires_at]"
mindate="Mon Jul 06 2020 00:00:00 GMT+0000 (Greenwich Mean Time)"
placeholder="YYYY-MM-DD"
+ showclearbutton="true"
theme=""
- >
- <gl-form-input-stub
- autocomplete="off"
- class="datepicker gl-datepicker-input"
- data-qa-selector="expiry_date_field"
- id="personal_access_token_expires_at"
- inputmode="none"
- name="personal_access_token[expires_at]"
- placeholder="YYYY-MM-DD"
- />
- </gl-datepicker-stub>
+ />
</gl-form-group-stub>
`;
diff --git a/spec/frontend/access_tokens/components/expires_at_field_spec.js b/spec/frontend/access_tokens/components/expires_at_field_spec.js
index cb899d10ba7..646dc0d703f 100644
--- a/spec/frontend/access_tokens/components/expires_at_field_spec.js
+++ b/spec/frontend/access_tokens/components/expires_at_field_spec.js
@@ -1,6 +1,7 @@
import { shallowMount } from '@vue/test-utils';
import { GlDatepicker } from '@gitlab/ui';
import ExpiresAtField from '~/access_tokens/components/expires_at_field.vue';
+import { getDateInFuture } from '~/lib/utils/datetime_utility';
describe('~/access_tokens/components/expires_at_field', () => {
let wrapper;
@@ -49,4 +50,12 @@ describe('~/access_tokens/components/expires_at_field', () => {
expect(findDatepicker().props('maxDate')).toStrictEqual(maxDate);
});
+
+ it('should set the default expiration date to be 30 days', () => {
+ const today = new Date();
+ const future = getDateInFuture(today, 30);
+ createComponent();
+
+ expect(findDatepicker().props('defaultDate')).toStrictEqual(future);
+ });
});
diff --git a/spec/frontend/access_tokens/components/projects_field_spec.js b/spec/frontend/access_tokens/components/projects_field_spec.js
deleted file mode 100644
index 1c4fe7bb168..00000000000
--- a/spec/frontend/access_tokens/components/projects_field_spec.js
+++ /dev/null
@@ -1,131 +0,0 @@
-import { nextTick } from 'vue';
-import { within, fireEvent } from '@testing-library/dom';
-import { mount } from '@vue/test-utils';
-import ProjectsField from '~/access_tokens/components/projects_field.vue';
-import ProjectsTokenSelector from '~/access_tokens/components/projects_token_selector.vue';
-
-describe('ProjectsField', () => {
- let wrapper;
-
- const createComponent = ({ inputAttrsValue = '' } = {}) => {
- wrapper = mount(ProjectsField, {
- propsData: {
- inputAttrs: {
- id: 'projects',
- name: 'projects',
- value: inputAttrsValue,
- },
- },
- });
- };
-
- const queryByLabelText = (text) => within(wrapper.element).queryByLabelText(text);
- const queryByText = (text) => within(wrapper.element).queryByText(text);
- const findAllProjectsRadio = () => queryByLabelText('All projects');
- const findSelectedProjectsRadio = () => queryByLabelText('Selected projects');
- const findProjectsTokenSelector = () => wrapper.findComponent(ProjectsTokenSelector);
- const findHiddenInput = () => wrapper.find('input[type="hidden"]');
-
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
- it('renders label and sub-label', () => {
- createComponent();
-
- expect(queryByText('Projects')).not.toBe(null);
- expect(queryByText('Set access permissions for this token.')).not.toBe(null);
- });
-
- describe('when `inputAttrs.value` is empty', () => {
- beforeEach(() => {
- createComponent();
- });
-
- it('renders "All projects" radio as checked', () => {
- expect(findAllProjectsRadio().checked).toBe(true);
- });
-
- it('renders "Selected projects" radio as unchecked', () => {
- expect(findSelectedProjectsRadio().checked).toBe(false);
- });
-
- it('sets `projects-token-selector` `initialProjectIds` prop to an empty array', () => {
- expect(findProjectsTokenSelector().props('initialProjectIds')).toEqual([]);
- });
- });
-
- describe('when `inputAttrs.value` is a comma separated list of project IDs', () => {
- beforeEach(() => {
- createComponent({ inputAttrsValue: '1,2' });
- });
-
- it('renders "All projects" radio as unchecked', () => {
- expect(findAllProjectsRadio().checked).toBe(false);
- });
-
- it('renders "Selected projects" radio as checked', () => {
- expect(findSelectedProjectsRadio().checked).toBe(true);
- });
-
- it('sets `projects-token-selector` `initialProjectIds` prop to an array of project IDs', () => {
- expect(findProjectsTokenSelector().props('initialProjectIds')).toEqual(['1', '2']);
- });
- });
-
- it('renders `projects-token-selector` component', () => {
- createComponent();
-
- expect(findProjectsTokenSelector().exists()).toBe(true);
- });
-
- it('renders hidden input with correct `name` and `id` attributes', () => {
- createComponent();
-
- expect(findHiddenInput().attributes()).toEqual(
- expect.objectContaining({
- id: 'projects',
- name: 'projects',
- }),
- );
- });
-
- describe('when `projects-token-selector` is focused', () => {
- beforeEach(() => {
- createComponent();
-
- findProjectsTokenSelector().vm.$emit('focus');
- });
-
- it('auto selects the "Selected projects" radio', () => {
- expect(findSelectedProjectsRadio().checked).toBe(true);
- });
-
- describe('when `projects-token-selector` is changed', () => {
- beforeEach(() => {
- findProjectsTokenSelector().vm.$emit('input', [
- {
- id: 1,
- },
- {
- id: 2,
- },
- ]);
- });
-
- it('updates the hidden input value to a comma separated list of project IDs', () => {
- expect(findHiddenInput().attributes('value')).toBe('1,2');
- });
-
- describe('when radio is changed back to "All projects"', () => {
- it('removes the hidden input value', async () => {
- fireEvent.change(findAllProjectsRadio());
- await nextTick();
-
- expect(findHiddenInput().attributes('value')).toBe('');
- });
- });
- });
- });
-});
diff --git a/spec/frontend/access_tokens/components/projects_token_selector_spec.js b/spec/frontend/access_tokens/components/projects_token_selector_spec.js
deleted file mode 100644
index 40aaf16d41f..00000000000
--- a/spec/frontend/access_tokens/components/projects_token_selector_spec.js
+++ /dev/null
@@ -1,266 +0,0 @@
-import {
- GlAvatar,
- GlAvatarLabeled,
- GlIntersectionObserver,
- GlToken,
- GlTokenSelector,
- GlLoadingIcon,
-} from '@gitlab/ui';
-import { mount } from '@vue/test-utils';
-import produce from 'immer';
-import Vue from 'vue';
-import VueApollo from 'vue-apollo';
-
-import getProjectsQueryResponse from 'test_fixtures/graphql/projects/access_tokens/get_projects.query.graphql.json';
-import createMockApollo from 'helpers/mock_apollo_helper';
-import { extendedWrapper } from 'helpers/vue_test_utils_helper';
-import waitForPromises from 'helpers/wait_for_promises';
-import ProjectsTokenSelector from '~/access_tokens/components/projects_token_selector.vue';
-import getProjectsQuery from '~/access_tokens/graphql/queries/get_projects.query.graphql';
-import { getIdFromGraphQLId } from '~/graphql_shared/utils';
-
-describe('ProjectsTokenSelector', () => {
- const getProjectsQueryResponsePage2 = produce(
- getProjectsQueryResponse,
- (getProjectsQueryResponseDraft) => {
- /* eslint-disable no-param-reassign */
- getProjectsQueryResponseDraft.data.projects.pageInfo.hasNextPage = false;
- getProjectsQueryResponseDraft.data.projects.pageInfo.endCursor = null;
- getProjectsQueryResponseDraft.data.projects.nodes.splice(1, 1);
- getProjectsQueryResponseDraft.data.projects.nodes[0].id = 'gid://gitlab/Project/100';
- /* eslint-enable no-param-reassign */
- },
- );
-
- const runDebounce = () => jest.runAllTimers();
-
- const { pageInfo, nodes: projects } = getProjectsQueryResponse.data.projects;
- const project1 = projects[0];
- const project2 = projects[1];
-
- let wrapper;
-
- let resolveGetProjectsQuery;
- let resolveGetInitialProjectsQuery;
- const getProjectsQueryRequestHandler = jest.fn(
- ({ ids }) =>
- new Promise((resolve) => {
- if (ids) {
- resolveGetInitialProjectsQuery = resolve;
- } else {
- resolveGetProjectsQuery = resolve;
- }
- }),
- );
-
- const createComponent = ({
- propsData = {},
- apolloProvider = createMockApollo([[getProjectsQuery, getProjectsQueryRequestHandler]]),
- resolveQueries = true,
- } = {}) => {
- Vue.use(VueApollo);
-
- wrapper = extendedWrapper(
- mount(ProjectsTokenSelector, {
- apolloProvider,
- propsData: {
- selectedProjects: [],
- initialProjectIds: [],
- ...propsData,
- },
- stubs: ['gl-intersection-observer'],
- }),
- );
-
- runDebounce();
-
- if (resolveQueries) {
- resolveGetProjectsQuery(getProjectsQueryResponse);
-
- return waitForPromises();
- }
-
- return Promise.resolve();
- };
-
- const findTokenSelector = () => wrapper.findComponent(GlTokenSelector);
- const findTokenSelectorInput = () => findTokenSelector().find('input[type="text"]');
- const findIntersectionObserver = () => wrapper.findComponent(GlIntersectionObserver);
-
- it('renders dropdown items with project avatars', async () => {
- await createComponent();
-
- wrapper.findAllComponents(GlAvatarLabeled).wrappers.forEach((avatarLabeledWrapper, index) => {
- const project = projects[index];
-
- expect(avatarLabeledWrapper.attributes()).toEqual(
- expect.objectContaining({
- 'entity-id': `${getIdFromGraphQLId(project.id)}`,
- 'entity-name': project.name,
- ...(project.avatarUrl && { src: project.avatarUrl }),
- }),
- );
-
- expect(avatarLabeledWrapper.props()).toEqual(
- expect.objectContaining({
- label: project.name,
- subLabel: project.nameWithNamespace,
- }),
- );
- });
- });
-
- it('renders tokens with project avatars', () => {
- createComponent({
- propsData: {
- selectedProjects: [{ ...project2, id: getIdFromGraphQLId(project2.id) }],
- },
- });
-
- const token = wrapper.findComponent(GlToken);
- const avatar = token.findComponent(GlAvatar);
-
- expect(token.text()).toContain(project2.nameWithNamespace);
- expect(avatar.attributes('src')).toBe(project2.avatarUrl);
- expect(avatar.props()).toEqual(
- expect.objectContaining({
- entityId: getIdFromGraphQLId(project2.id),
- entityName: project2.name,
- }),
- );
- });
-
- describe('when `enter` key is pressed', () => {
- it('calls `preventDefault` so form is not submitted when user selects a project from the dropdown', () => {
- createComponent();
-
- const event = {
- preventDefault: jest.fn(),
- };
-
- findTokenSelectorInput().trigger('keydown.enter', event);
-
- expect(event.preventDefault).toHaveBeenCalled();
- });
- });
-
- describe('when text input is typed in', () => {
- const searchTerm = 'foo bar';
-
- beforeEach(async () => {
- await createComponent();
-
- await findTokenSelectorInput().setValue(searchTerm);
- runDebounce();
- });
-
- it('makes GraphQL request with `search` variable set', async () => {
- expect(getProjectsQueryRequestHandler).toHaveBeenLastCalledWith({
- search: searchTerm,
- after: null,
- first: 20,
- ids: null,
- });
- });
-
- it('sets loading state while waiting for GraphQL request to resolve', async () => {
- expect(findTokenSelector().props('loading')).toBe(true);
-
- resolveGetProjectsQuery(getProjectsQueryResponse);
- await waitForPromises();
-
- expect(findTokenSelector().props('loading')).toBe(false);
- });
- });
-
- describe('when there is a next page of projects and user scrolls to the bottom of the dropdown', () => {
- beforeEach(async () => {
- await createComponent();
-
- findIntersectionObserver().vm.$emit('appear');
- });
-
- it('makes GraphQL request with `after` variable set', async () => {
- expect(getProjectsQueryRequestHandler).toHaveBeenLastCalledWith({
- after: pageInfo.endCursor,
- first: 20,
- search: '',
- ids: null,
- });
- });
-
- it('displays loading icon while waiting for GraphQL request to resolve', async () => {
- expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
-
- resolveGetProjectsQuery(getProjectsQueryResponsePage2);
- await waitForPromises();
-
- expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(false);
- });
- });
-
- describe('when there is not a next page of projects', () => {
- it('does not render `GlIntersectionObserver`', async () => {
- createComponent({ resolveQueries: false });
-
- resolveGetProjectsQuery(getProjectsQueryResponsePage2);
- await waitForPromises();
-
- expect(findIntersectionObserver().exists()).toBe(false);
- });
- });
-
- describe('when `GlTokenSelector` emits `input` event', () => {
- it('emits `input` event used by `v-model`', () => {
- findTokenSelector().vm.$emit('input', project1);
-
- expect(wrapper.emitted('input')[0]).toEqual([project1]);
- });
- });
-
- describe('when `GlTokenSelector` emits `focus` event', () => {
- it('emits `focus` event', () => {
- const event = { fakeEvent: 'foo' };
- findTokenSelector().vm.$emit('focus', event);
-
- expect(wrapper.emitted('focus')[0]).toEqual([event]);
- });
- });
-
- describe('when `initialProjectIds` is an empty array', () => {
- it('does not request initial projects', async () => {
- await createComponent();
-
- expect(getProjectsQueryRequestHandler).toHaveBeenCalledTimes(1);
- expect(getProjectsQueryRequestHandler).toHaveBeenCalledWith(
- expect.objectContaining({
- ids: null,
- }),
- );
- });
- });
-
- describe('when `initialProjectIds` is an array of project IDs', () => {
- it('requests those projects and emits `input` event with result', async () => {
- await createComponent({
- propsData: {
- initialProjectIds: [getIdFromGraphQLId(project1.id), getIdFromGraphQLId(project2.id)],
- },
- });
-
- resolveGetInitialProjectsQuery(getProjectsQueryResponse);
- await waitForPromises();
-
- expect(getProjectsQueryRequestHandler).toHaveBeenCalledWith({
- after: '',
- first: null,
- search: '',
- ids: [project1.id, project2.id],
- });
- expect(wrapper.emitted('input')[0][0]).toEqual([
- { ...project1, id: getIdFromGraphQLId(project1.id) },
- { ...project2, id: getIdFromGraphQLId(project2.id) },
- ]);
- });
- });
-});
diff --git a/spec/frontend/access_tokens/index_spec.js b/spec/frontend/access_tokens/index_spec.js
index b6119f1d167..0c611a4a512 100644
--- a/spec/frontend/access_tokens/index_spec.js
+++ b/spec/frontend/access_tokens/index_spec.js
@@ -8,13 +8,11 @@ import {
initAccessTokenTableApp,
initExpiresAtField,
initNewAccessTokenApp,
- initProjectsField,
initTokensApp,
} from '~/access_tokens';
import * as AccessTokenTableApp from '~/access_tokens/components/access_token_table_app.vue';
-import * as ExpiresAtField from '~/access_tokens/components/expires_at_field.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 ProjectsField from '~/access_tokens/components/projects_field.vue';
import * as TokensApp from '~/access_tokens/components/tokens_app.vue';
import { FEED_TOKEN, INCOMING_EMAIL_TOKEN, STATIC_OBJECT_TOKEN } from '~/access_tokens/constants';
import { __, sprintf } from '~/locale';
@@ -115,49 +113,28 @@ describe('access tokens', () => {
});
});
- describe.each`
- initFunction | mountSelector | fieldName | expectedComponent
- ${initExpiresAtField} | ${'js-access-tokens-expires-at'} | ${'expiresAt'} | ${ExpiresAtField}
- ${initProjectsField} | ${'js-access-tokens-projects'} | ${'projects'} | ${ProjectsField}
- `('$initFunction', ({ initFunction, mountSelector, fieldName, expectedComponent }) => {
+ describe('initExpiresAtField', () => {
describe('when mount element exists', () => {
- const FakeComponent = Vue.component('FakeComponent', {
- props: ['inputAttrs'],
- render: () => null,
- });
-
- const nameAttribute = `access_tokens[${fieldName}]`;
- const idAttribute = `access_tokens_${fieldName}`;
+ const nameAttribute = 'access_tokens[expires_at]';
+ const idAttribute = 'access_tokens_expires_at';
beforeEach(() => {
- window.gon = { features: { personalAccessTokensScopedToProjects: true } };
-
setHTMLFixture(
- `<div class="${mountSelector}">
+ `<div class="js-access-tokens-expires-at">
<input
- name="${nameAttribute}"
- data-js-name="${fieldName}"
- id="${idAttribute}"
+ name="access_tokens[expires_at]"
+ data-js-name="expiresAt"
+ id="access_tokens_expires_at"
placeholder="Foo bar"
value="1,2"
/>
</div>`,
);
-
- // Mock component so we don't have to deal with mocking Apollo
- // eslint-disable-next-line no-param-reassign
- expectedComponent.default = FakeComponent;
- });
-
- afterEach(() => {
- delete window.gon;
});
it('mounts component and sets `inputAttrs` prop', async () => {
- const vueInstance = await initFunction();
-
- wrapper = createWrapper(vueInstance);
- const component = wrapper.findComponent(FakeComponent);
+ wrapper = createWrapper(initExpiresAtField());
+ const component = wrapper.findComponent(ExpiresAtField);
expect(component.exists()).toBe(true);
expect(component.props('inputAttrs')).toEqual({
@@ -171,7 +148,7 @@ describe('access tokens', () => {
describe('when mount element does not exist', () => {
it('returns `null`', () => {
- expect(initFunction()).toBe(null);
+ expect(initExpiresAtField()).toBe(null);
});
});
});
diff --git a/spec/frontend/add_context_commits_modal/components/add_context_commits_modal_spec.js b/spec/frontend/add_context_commits_modal/components/add_context_commits_modal_spec.js
index 9b93fd26fa0..bffadbde087 100644
--- a/spec/frontend/add_context_commits_modal/components/add_context_commits_modal_spec.js
+++ b/spec/frontend/add_context_commits_modal/components/add_context_commits_modal_spec.js
@@ -87,7 +87,7 @@ describe('AddContextCommitsModal', () => {
it('enabled ok button when atleast one row is selected', async () => {
wrapper.vm.$store.state.selectedCommits = [{ ...commit, isSelected: true }];
await nextTick();
- expect(findModal().attributes('ok-disabled')).toBeFalsy();
+ expect(findModal().attributes('ok-disabled')).toBe(undefined);
});
});
@@ -102,7 +102,7 @@ describe('AddContextCommitsModal', () => {
it('an enabled ok button when atleast one row is selected', async () => {
wrapper.vm.$store.state.selectedCommits = [{ ...commit, isSelected: true }];
await nextTick();
- expect(findModal().attributes('ok-disabled')).toBeFalsy();
+ expect(findModal().attributes('ok-disabled')).toBe(undefined);
});
it('a disabled ok button in first tab, when row is selected in second tab', () => {
diff --git a/spec/frontend/admin/analytics/devops_score/components/devops_score_spec.js b/spec/frontend/admin/analytics/devops_score/components/devops_score_spec.js
index d6c5c5f963a..534af2a3033 100644
--- a/spec/frontend/admin/analytics/devops_score/components/devops_score_spec.js
+++ b/spec/frontend/admin/analytics/devops_score/components/devops_score_spec.js
@@ -129,7 +129,7 @@ describe('DevopsScore', () => {
});
it('displays the correct badge', () => {
- const badge = findUsageCol().find(GlBadge);
+ const badge = findUsageCol().findComponent(GlBadge);
expect(badge.exists()).toBe(true);
expect(badge.props('variant')).toBe('muted');
diff --git a/spec/frontend/admin/signup_restrictions/components/signup_checkbox_spec.js b/spec/frontend/admin/signup_restrictions/components/signup_checkbox_spec.js
index ae9b6f57ee0..eecc21e206b 100644
--- a/spec/frontend/admin/signup_restrictions/components/signup_checkbox_spec.js
+++ b/spec/frontend/admin/signup_restrictions/components/signup_checkbox_spec.js
@@ -24,7 +24,7 @@ describe('Signup Form', () => {
const findByTestId = (id) => wrapper.find(`[data-testid="${id}"]`);
const findHiddenInput = () => findByTestId('input');
- const findCheckbox = () => wrapper.find(GlFormCheckbox);
+ const findCheckbox = () => wrapper.findComponent(GlFormCheckbox);
const findCheckboxLabel = () => findByTestId('label');
const findHelpText = () => findByTestId('helpText');
diff --git a/spec/frontend/admin/signup_restrictions/components/signup_form_spec.js b/spec/frontend/admin/signup_restrictions/components/signup_form_spec.js
index 31a0c2b07e4..411126d0c89 100644
--- a/spec/frontend/admin/signup_restrictions/components/signup_form_spec.js
+++ b/spec/frontend/admin/signup_restrictions/components/signup_form_spec.js
@@ -28,7 +28,7 @@ describe('Signup Form', () => {
const findForm = () => wrapper.findByTestId('form');
const findInputCsrf = () => findForm().find('[name="authenticity_token"]');
- const findFormSubmitButton = () => findForm().find(GlButton);
+ const findFormSubmitButton = () => findForm().findComponent(GlButton);
const findDenyListRawRadio = () => queryByLabelText('Enter denylist manually');
const findDenyListFileRadio = () => queryByLabelText('Upload denylist file');
@@ -36,7 +36,7 @@ describe('Signup Form', () => {
const findDenyListRawInputGroup = () => wrapper.findByTestId('domain-denylist-raw-input-group');
const findDenyListFileInputGroup = () => wrapper.findByTestId('domain-denylist-file-input-group');
const findUserCapInput = () => wrapper.findByTestId('user-cap-input');
- const findModal = () => wrapper.find(GlModal);
+ const findModal = () => wrapper.findComponent(GlModal);
afterEach(() => {
wrapper.destroy();
diff --git a/spec/frontend/admin/statistics_panel/components/app_spec.js b/spec/frontend/admin/statistics_panel/components/app_spec.js
index bac542e72fb..190f0eb94a0 100644
--- a/spec/frontend/admin/statistics_panel/components/app_spec.js
+++ b/spec/frontend/admin/statistics_panel/components/app_spec.js
@@ -41,7 +41,7 @@ describe('Admin statistics app', () => {
store.dispatch('requestStatistics');
createComponent();
- expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
+ expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
});
});
diff --git a/spec/frontend/admin/users/components/actions/actions_spec.js b/spec/frontend/admin/users/components/actions/actions_spec.js
index b758c15a91a..4967753b91c 100644
--- a/spec/frontend/admin/users/components/actions/actions_spec.js
+++ b/spec/frontend/admin/users/components/actions/actions_spec.js
@@ -12,7 +12,7 @@ import { paths } from '../../mock_data';
describe('Action components', () => {
let wrapper;
- const findDropdownItem = () => wrapper.find(GlDropdownItem);
+ const findDropdownItem = () => wrapper.findComponent(GlDropdownItem);
const initComponent = ({ component, props } = {}) => {
wrapper = shallowMount(component, {
diff --git a/spec/frontend/admin/users/components/app_spec.js b/spec/frontend/admin/users/components/app_spec.js
index 65b13e3a40d..913732aae42 100644
--- a/spec/frontend/admin/users/components/app_spec.js
+++ b/spec/frontend/admin/users/components/app_spec.js
@@ -28,7 +28,7 @@ describe('AdminUsersApp component', () => {
});
it('renders the admin users table with props', () => {
- expect(wrapper.find(AdminUsersTable).props()).toEqual({
+ expect(wrapper.findComponent(AdminUsersTable).props()).toEqual({
users,
paths,
});
diff --git a/spec/frontend/admin/users/components/modals/delete_user_modal_spec.js b/spec/frontend/admin/users/components/modals/delete_user_modal_spec.js
index 09a345ac826..70ed9eeb3e1 100644
--- a/spec/frontend/admin/users/components/modals/delete_user_modal_spec.js
+++ b/spec/frontend/admin/users/components/modals/delete_user_modal_spec.js
@@ -17,7 +17,7 @@ describe('Delete user modal', () => {
const findButton = (variant, category) =>
wrapper
- .findAll(GlButton)
+ .findAllComponents(GlButton)
.filter((w) => w.attributes('variant') === variant && w.attributes('category') === category)
.at(0);
const findForm = () => wrapper.find('form');
@@ -87,8 +87,8 @@ describe('Delete user modal', () => {
});
it('has disabled buttons', () => {
- expect(findPrimaryButton().attributes('disabled')).toBeTruthy();
- expect(findSecondaryButton().attributes('disabled')).toBeTruthy();
+ expect(findPrimaryButton().attributes('disabled')).toBe('true');
+ expect(findSecondaryButton().attributes('disabled')).toBe('true');
});
});
@@ -105,8 +105,8 @@ describe('Delete user modal', () => {
});
it('has disabled buttons', () => {
- expect(findPrimaryButton().attributes('disabled')).toBeTruthy();
- expect(findSecondaryButton().attributes('disabled')).toBeTruthy();
+ expect(findPrimaryButton().attributes('disabled')).toBe('true');
+ expect(findSecondaryButton().attributes('disabled')).toBe('true');
});
});
@@ -123,8 +123,8 @@ describe('Delete user modal', () => {
});
it('has enabled buttons', () => {
- expect(findPrimaryButton().attributes('disabled')).toBeFalsy();
- expect(findSecondaryButton().attributes('disabled')).toBeFalsy();
+ expect(findPrimaryButton().attributes('disabled')).toBeUndefined();
+ expect(findSecondaryButton().attributes('disabled')).toBeUndefined();
});
describe('when primary action is clicked', () => {
diff --git a/spec/frontend/admin/users/components/user_actions_spec.js b/spec/frontend/admin/users/components/user_actions_spec.js
index e04c43ae3f2..ffc05e744c8 100644
--- a/spec/frontend/admin/users/components/user_actions_spec.js
+++ b/spec/frontend/admin/users/components/user_actions_spec.js
@@ -83,7 +83,7 @@ describe('AdminUserActions component', () => {
});
it.each(CONFIRMATION_ACTIONS)('renders an action component item for "%s"', (action) => {
- const component = wrapper.find(Actions[capitalizeFirstCharacter(action)]);
+ const component = wrapper.findComponent(Actions[capitalizeFirstCharacter(action)]);
expect(component.props('username')).toBe(user.name);
expect(component.props('path')).toBe(userPaths[action]);
@@ -119,7 +119,7 @@ describe('AdminUserActions component', () => {
});
it.each(DELETE_ACTIONS)('renders a delete action component item for "%s"', (action) => {
- const component = wrapper.find(Actions[capitalizeFirstCharacter(action)]);
+ const component = wrapper.findComponent(Actions[capitalizeFirstCharacter(action)]);
expect(component.props('username')).toBe(user.name);
expect(component.props('paths')).toEqual(userPaths);
diff --git a/spec/frontend/admin/users/components/user_avatar_spec.js b/spec/frontend/admin/users/components/user_avatar_spec.js
index 8bbfb89bec1..94fac875fbe 100644
--- a/spec/frontend/admin/users/components/user_avatar_spec.js
+++ b/spec/frontend/admin/users/components/user_avatar_spec.js
@@ -12,10 +12,10 @@ describe('AdminUserAvatar component', () => {
const user = users[0];
const adminUserPath = paths.adminUser;
- const findNote = () => wrapper.find(GlIcon);
- const findAvatar = () => wrapper.find(GlAvatarLabeled);
+ const findNote = () => wrapper.findComponent(GlIcon);
+ const findAvatar = () => wrapper.findComponent(GlAvatarLabeled);
const findUserLink = () => wrapper.find('.js-user-link');
- const findAllBadges = () => wrapper.findAll(GlBadge);
+ const findAllBadges = () => wrapper.findAllComponents(GlBadge);
const findTooltip = () => getBinding(findNote().element, 'gl-tooltip');
const initComponent = (props = {}) => {
diff --git a/spec/frontend/admin/users/components/users_table_spec.js b/spec/frontend/admin/users/components/users_table_spec.js
index ad1c45495b5..fe07f0fce00 100644
--- a/spec/frontend/admin/users/components/users_table_spec.js
+++ b/spec/frontend/admin/users/components/users_table_spec.js
@@ -30,10 +30,10 @@ describe('AdminUsersTable component', () => {
const fetchGroupCountsResponse = createFetchGroupCount([{ id: user.id, groupCount: 5 }]);
const findUserGroupCount = (id) => wrapper.findByTestId(`user-group-count-${id}`);
- const findUserGroupCountLoader = (id) => findUserGroupCount(id).find(GlSkeletonLoader);
+ const findUserGroupCountLoader = (id) => findUserGroupCount(id).findComponent(GlSkeletonLoader);
const getCellByLabel = (trIdx, label) => {
return wrapper
- .find(GlTable)
+ .findComponent(GlTable)
.find('tbody')
.findAll('tr')
.at(trIdx)
@@ -72,7 +72,7 @@ describe('AdminUsersTable component', () => {
});
it('renders the user actions', () => {
- expect(wrapper.find(AdminUserActions).exists()).toBe(true);
+ expect(wrapper.findComponent(AdminUserActions).exists()).toBe(true);
});
it.each`
@@ -81,7 +81,7 @@ describe('AdminUsersTable component', () => {
${AdminUserDate} | ${'Created on'}
${AdminUserDate} | ${'Last activity'}
`('renders the component for column $label', ({ component, label }) => {
- expect(getCellByLabel(0, label).find(component).exists()).toBe(true);
+ expect(getCellByLabel(0, label).findComponent(component).exists()).toBe(true);
});
});
diff --git a/spec/frontend/admin/users/index_spec.js b/spec/frontend/admin/users/index_spec.js
index 961fa96acdd..b51858d5129 100644
--- a/spec/frontend/admin/users/index_spec.js
+++ b/spec/frontend/admin/users/index_spec.js
@@ -8,7 +8,7 @@ describe('initAdminUsersApp', () => {
let wrapper;
let el;
- const findApp = () => wrapper.find(AdminUsersApp);
+ const findApp = () => wrapper.findComponent(AdminUsersApp);
beforeEach(() => {
el = document.createElement('div');
@@ -36,7 +36,7 @@ describe('initAdminUserActions', () => {
let wrapper;
let el;
- const findUserActions = () => wrapper.find(UserActions);
+ const findUserActions = () => wrapper.findComponent(UserActions);
beforeEach(() => {
el = document.createElement('div');
diff --git a/spec/frontend/api/groups_api_spec.js b/spec/frontend/api/groups_api_spec.js
new file mode 100644
index 00000000000..e14ead0b8eb
--- /dev/null
+++ b/spec/frontend/api/groups_api_spec.js
@@ -0,0 +1,46 @@
+import MockAdapter from 'axios-mock-adapter';
+import httpStatus from '~/lib/utils/http_status';
+import axios from '~/lib/utils/axios_utils';
+import { updateGroup } from '~/api/groups_api';
+
+const mockApiVersion = 'v4';
+const mockUrlRoot = '/gitlab';
+
+describe('GroupsApi', () => {
+ let originalGon;
+ let mock;
+
+ const dummyGon = {
+ api_version: mockApiVersion,
+ relative_url_root: mockUrlRoot,
+ };
+
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+ originalGon = window.gon;
+ window.gon = { ...dummyGon };
+ });
+
+ afterEach(() => {
+ mock.restore();
+ window.gon = originalGon;
+ });
+
+ describe('updateGroup', () => {
+ const mockGroupId = '99';
+ const mockData = { attr: 'value' };
+ const expectedUrl = `${mockUrlRoot}/api/${mockApiVersion}/groups/${mockGroupId}`;
+
+ beforeEach(() => {
+ mock.onPut(expectedUrl).reply(({ data }) => {
+ return [httpStatus.OK, { id: mockGroupId, ...JSON.parse(data) }];
+ });
+ });
+
+ it('updates group', async () => {
+ const res = await updateGroup(mockGroupId, mockData);
+
+ expect(res.data).toMatchObject({ id: mockGroupId, ...mockData });
+ });
+ });
+});
diff --git a/spec/frontend/attention_requests/components/navigation_popover_spec.js b/spec/frontend/attention_requests/components/navigation_popover_spec.js
deleted file mode 100644
index e4d53d5dbdb..00000000000
--- a/spec/frontend/attention_requests/components/navigation_popover_spec.js
+++ /dev/null
@@ -1,88 +0,0 @@
-import { shallowMount } from '@vue/test-utils';
-import { GlPopover, GlButton, GlSprintf, GlIcon } from '@gitlab/ui';
-import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
-import NavigationPopover from '~/attention_requests/components/navigation_popover.vue';
-import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
-import { makeMockUserCalloutDismisser } from 'helpers/mock_user_callout_dismisser';
-
-let wrapper;
-let dismiss;
-
-function createComponent(provideData = {}, shouldShowCallout = true) {
- wrapper = shallowMount(NavigationPopover, {
- provide: {
- message: ['Test'],
- observerElSelector: '.js-test',
- observerElToggledClass: 'show',
- featureName: 'attention_requests',
- popoverTarget: '.js-test-popover',
- ...provideData,
- },
- stubs: {
- UserCalloutDismisser: makeMockUserCalloutDismisser({
- dismiss,
- shouldShowCallout,
- }),
- GlSprintf,
- },
- });
-}
-
-describe('Attention requests navigation popover', () => {
- beforeEach(() => {
- setHTMLFixture('<div><div class="js-test-popover"></div><div class="js-test"></div></div>');
- dismiss = jest.fn();
- });
-
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- resetHTMLFixture();
- });
-
- it('hides popover if callout is disabled', () => {
- createComponent({}, false);
-
- expect(wrapper.findComponent(GlPopover).exists()).toBe(false);
- });
-
- it('shows popover if callout is enabled', () => {
- createComponent();
-
- expect(wrapper.findComponent(GlPopover).exists()).toBe(true);
- });
-
- it.each`
- isDesktop | device | expectedPlacement
- ${true} | ${'desktop'} | ${'left'}
- ${false} | ${'mobile'} | ${'bottom'}
- `(
- 'sets popover position to $expectedPlacement on $device',
- ({ isDesktop, expectedPlacement }) => {
- jest.spyOn(bp, 'isDesktop').mockReturnValue(isDesktop);
-
- createComponent();
-
- expect(wrapper.findComponent(GlPopover).props('placement')).toBe(expectedPlacement);
- },
- );
-
- it('calls dismiss when clicking action button', () => {
- createComponent();
-
- wrapper
- .findComponent(GlButton)
- .vm.$emit('click', { preventDefault() {}, stopPropagation() {} });
-
- expect(dismiss).toHaveBeenCalled();
- });
-
- it('shows icon in text', () => {
- createComponent({ showAttentionIcon: true, message: ['%{strongStart}Test%{strongEnd}'] });
-
- const icon = wrapper.findComponent(GlIcon);
-
- expect(icon.exists()).toBe(true);
- expect(icon.props('name')).toBe('attention');
- });
-});
diff --git a/spec/frontend/batch_comments/components/review_bar_spec.js b/spec/frontend/batch_comments/components/review_bar_spec.js
index f50db6ab210..f98e0a4c64a 100644
--- a/spec/frontend/batch_comments/components/review_bar_spec.js
+++ b/spec/frontend/batch_comments/components/review_bar_spec.js
@@ -6,6 +6,8 @@ import createStore from '../create_batch_comments_store';
describe('Batch comments review bar component', () => {
let store;
let wrapper;
+ let addEventListenerSpy;
+ let removeEventListenerSpy;
const createComponent = (propsData = {}) => {
store = createStore();
@@ -18,25 +20,58 @@ describe('Batch comments review bar component', () => {
beforeEach(() => {
document.body.className = '';
+
+ addEventListenerSpy = jest.spyOn(window, 'addEventListener');
+ removeEventListenerSpy = jest.spyOn(window, 'removeEventListener');
});
afterEach(() => {
+ addEventListenerSpy.mockRestore();
+ removeEventListenerSpy.mockRestore();
wrapper.destroy();
});
- it('it adds review-bar-visible class to body when review bar is mounted', async () => {
- expect(document.body.classList.contains(REVIEW_BAR_VISIBLE_CLASS_NAME)).toBe(false);
+ describe('when mounted', () => {
+ it('it adds review-bar-visible class to body', async () => {
+ expect(document.body.classList.contains(REVIEW_BAR_VISIBLE_CLASS_NAME)).toBe(false);
+
+ createComponent();
+
+ expect(document.body.classList.contains(REVIEW_BAR_VISIBLE_CLASS_NAME)).toBe(true);
+ });
- createComponent();
+ it('it adds a blocking handler to the `beforeunload` window event', () => {
+ expect(addEventListenerSpy).not.toBeCalled();
- expect(document.body.classList.contains(REVIEW_BAR_VISIBLE_CLASS_NAME)).toBe(true);
+ createComponent();
+
+ expect(addEventListenerSpy).toHaveBeenCalledTimes(1);
+ expect(addEventListenerSpy).toBeCalledWith('beforeunload', expect.any(Function), {
+ capture: true,
+ });
+ });
});
- it('it removes review-bar-visible class to body when review bar is destroyed', async () => {
- createComponent();
+ describe('before destroyed', () => {
+ it('it removes review-bar-visible class to body', async () => {
+ createComponent();
- wrapper.destroy();
+ wrapper.destroy();
- expect(document.body.classList.contains(REVIEW_BAR_VISIBLE_CLASS_NAME)).toBe(false);
+ expect(document.body.classList.contains(REVIEW_BAR_VISIBLE_CLASS_NAME)).toBe(false);
+ });
+
+ it('it removes the blocking handler from the `beforeunload` window event', () => {
+ createComponent();
+
+ expect(removeEventListenerSpy).not.toBeCalled();
+
+ wrapper.destroy();
+
+ expect(removeEventListenerSpy).toHaveBeenCalledTimes(1);
+ expect(removeEventListenerSpy).toBeCalledWith('beforeunload', expect.any(Function), {
+ capture: true,
+ });
+ });
});
});
diff --git a/spec/frontend/behaviors/components/json_table_spec.js b/spec/frontend/behaviors/components/json_table_spec.js
new file mode 100644
index 00000000000..42b4a051d4d
--- /dev/null
+++ b/spec/frontend/behaviors/components/json_table_spec.js
@@ -0,0 +1,162 @@
+import { GlTable, GlFormInput } from '@gitlab/ui';
+import { nextTick } from 'vue';
+import { merge } from 'lodash';
+import { shallowMountExtended, mountExtended } from 'helpers/vue_test_utils_helper';
+import { stubComponent, RENDER_ALL_SLOTS_TEMPLATE } from 'helpers/stub_component';
+import JSONTable from '~/behaviors/components/json_table.vue';
+
+const TEST_FIELDS = [
+ 'A',
+ {
+ key: 'B',
+ label: 'Second',
+ sortable: true,
+ other: 'foo',
+ },
+ {
+ key: 'C',
+ label: 'Third',
+ },
+ 'D',
+];
+const TEST_ITEMS = [
+ { A: 1, B: 'lorem', C: 2, D: null, E: 'dne' },
+ { A: 2, B: 'ipsum', C: 2, D: null, E: 'dne' },
+ { A: 3, B: 'dolar', C: 2, D: null, E: 'dne' },
+];
+
+describe('behaviors/components/json_table', () => {
+ let wrapper;
+
+ const buildWrapper = ({
+ fields = [],
+ items = [],
+ filter = undefined,
+ caption = undefined,
+ } = {}) => {
+ wrapper = shallowMountExtended(JSONTable, {
+ propsData: {
+ fields,
+ items,
+ hasFilter: filter,
+ caption,
+ },
+ stubs: {
+ GlTable: merge(stubComponent(GlTable), {
+ props: {
+ fields: {
+ type: Array,
+ required: true,
+ },
+ items: {
+ type: Array,
+ required: true,
+ },
+ },
+ template: RENDER_ALL_SLOTS_TEMPLATE,
+ }),
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ const findTable = () => wrapper.findComponent(GlTable);
+ const findTableCaption = () => wrapper.findByTestId('slot-table-caption');
+ const findFilterInput = () => wrapper.findComponent(GlFormInput);
+
+ describe('default', () => {
+ beforeEach(() => {
+ buildWrapper();
+ });
+
+ it('renders gltable', () => {
+ expect(findTable().props()).toEqual({
+ fields: [],
+ items: [],
+ });
+ expect(findTable().attributes()).toMatchObject({
+ filter: '',
+ 'show-empty': '',
+ });
+ });
+
+ it('does not render filter input', () => {
+ expect(findFilterInput().exists()).toBe(false);
+ });
+
+ it('renders caption', () => {
+ expect(findTableCaption().text()).toBe('Generated with JSON data');
+ });
+ });
+
+ describe('with filter', () => {
+ beforeEach(() => {
+ buildWrapper({
+ filter: true,
+ });
+ });
+
+ it('renders filter input', () => {
+ expect(findFilterInput().attributes()).toMatchObject({
+ value: '',
+ placeholder: 'Type to search',
+ });
+ });
+
+ it('when input is changed, updates table filter', async () => {
+ findFilterInput().vm.$emit('input', 'New value!');
+
+ await nextTick();
+
+ expect(findTable().attributes('filter')).toBe('New value!');
+ });
+ });
+
+ describe('with fields', () => {
+ beforeEach(() => {
+ buildWrapper({
+ fields: TEST_FIELDS,
+ items: TEST_ITEMS,
+ });
+ });
+
+ it('passes cleaned fields and items to table', () => {
+ expect(findTable().props()).toEqual({
+ fields: [
+ 'A',
+ {
+ key: 'B',
+ label: 'Second',
+ sortable: true,
+ },
+ {
+ key: 'C',
+ label: 'Third',
+ sortable: false,
+ },
+ 'D',
+ ],
+ items: TEST_ITEMS,
+ });
+ });
+ });
+
+ describe('with full mount', () => {
+ beforeEach(() => {
+ wrapper = mountExtended(JSONTable, {
+ propsData: {
+ fields: [],
+ items: [],
+ },
+ });
+ });
+
+ // We want to make sure all the props are passed down nicely in integration
+ it('renders table without errors', () => {
+ expect(findTable().exists()).toBe(true);
+ });
+ });
+});
diff --git a/spec/frontend/behaviors/gl_emoji_spec.js b/spec/frontend/behaviors/gl_emoji_spec.js
index 8842ad636ec..722327e94ba 100644
--- a/spec/frontend/behaviors/gl_emoji_spec.js
+++ b/spec/frontend/behaviors/gl_emoji_spec.js
@@ -121,7 +121,7 @@ describe('gl_emoji', () => {
window.gon.emoji_sprites_css_path = testPath;
expect(document.head.querySelector(`link[href="${testPath}"]`)).toBe(null);
- expect(window.gon.emoji_sprites_css_added).toBeFalsy();
+ expect(window.gon.emoji_sprites_css_added).toBe(undefined);
markupToDomElement(
'<gl-emoji data-fallback-sprite-class="emoji-bomb" data-name="bomb"></gl-emoji>',
diff --git a/spec/frontend/behaviors/markdown/render_json_table_spec.js b/spec/frontend/behaviors/markdown/render_json_table_spec.js
new file mode 100644
index 00000000000..488492479f3
--- /dev/null
+++ b/spec/frontend/behaviors/markdown/render_json_table_spec.js
@@ -0,0 +1,119 @@
+import { nextTick } from 'vue';
+import { renderJSONTable } from '~/behaviors/markdown/render_json_table';
+
+describe('behaviors/markdown/render_json_table', () => {
+ let element;
+
+ const TEST_DATA = {
+ fields: [
+ { label: 'Field 1', key: 'a' },
+ { label: 'F 2', key: 'b' },
+ { label: 'F 3', key: 'c' },
+ ],
+ items: [
+ {
+ a: '1',
+ b: 'b',
+ c: 'c',
+ },
+ {
+ a: '2',
+ b: 'd',
+ c: 'e',
+ },
+ ],
+ };
+ const TEST_LABELS = TEST_DATA.fields.map((x) => x.label);
+
+ const tableAsData = (table) => ({
+ head: Array.from(table.querySelectorAll('thead th')).map((td) => td.textContent),
+ body: Array.from(table.querySelectorAll('tbody > tr')).map((tr) =>
+ Array.from(tr.querySelectorAll('td')).map((x) => x.textContent),
+ ),
+ });
+
+ const createTestSubject = async (json) => {
+ if (element) {
+ throw new Error('element has already been initialized');
+ }
+
+ const parent = document.createElement('div');
+ const pre = document.createElement('pre');
+
+ pre.textContent = json;
+ parent.appendChild(pre);
+
+ document.body.appendChild(parent);
+ renderJSONTable([parent]);
+
+ element = parent;
+
+ jest.runAllTimers();
+
+ await nextTick();
+ };
+
+ const findPres = () => document.querySelectorAll('pre');
+ const findTables = () => document.querySelectorAll('table');
+ const findAlerts = () => document.querySelectorAll('.gl-alert');
+ const findInputs = () => document.querySelectorAll('.gl-form-input');
+
+ afterEach(() => {
+ document.body.innerHTML = '';
+ element = null;
+ });
+
+ describe('default', () => {
+ beforeEach(async () => {
+ await createTestSubject(JSON.stringify(TEST_DATA, null, 2));
+ });
+
+ it('removes pre', () => {
+ expect(findPres()).toHaveLength(0);
+ });
+
+ it('replaces pre with table', () => {
+ const tables = findTables();
+
+ expect(tables).toHaveLength(1);
+ expect(tableAsData(tables[0])).toEqual({
+ head: TEST_LABELS,
+ body: [
+ ['1', 'b', 'c'],
+ ['2', 'd', 'e'],
+ ],
+ });
+ });
+
+ it('does not show filter', () => {
+ expect(findInputs()).toHaveLength(0);
+ });
+ });
+
+ describe('with invalid json', () => {
+ beforeEach(() => {
+ createTestSubject('funky but not json');
+ });
+
+ it('preserves pre', () => {
+ expect(findPres()).toHaveLength(1);
+ });
+
+ it('shows alert', () => {
+ const alerts = findAlerts();
+
+ expect(alerts).toHaveLength(1);
+ expect(alerts[0].textContent).toMatchInterpolatedText('Unable to parse JSON');
+ });
+ });
+
+ describe('with filter set', () => {
+ beforeEach(() => {
+ createTestSubject(JSON.stringify({ ...TEST_DATA, filter: true }));
+ });
+
+ it('shows filter', () => {
+ expect(findInputs()).toHaveLength(1);
+ });
+ });
+});
diff --git a/spec/frontend/blob/3d_viewer/mesh_object_spec.js b/spec/frontend/blob/3d_viewer/mesh_object_spec.js
index 60be285039f..3014af073f5 100644
--- a/spec/frontend/blob/3d_viewer/mesh_object_spec.js
+++ b/spec/frontend/blob/3d_viewer/mesh_object_spec.js
@@ -5,7 +5,7 @@ describe('Mesh object', () => {
it('defaults to non-wireframe material', () => {
const object = new MeshObject(new BoxGeometry(10, 10, 10));
- expect(object.material.wireframe).toBeFalsy();
+ expect(object.material.wireframe).toBe(false);
});
it('changes to wirefame material', () => {
@@ -13,7 +13,7 @@ describe('Mesh object', () => {
object.changeMaterial('wireframe');
- expect(object.material.wireframe).toBeTruthy();
+ expect(object.material.wireframe).toBe(true);
});
it('scales object down', () => {
diff --git a/spec/frontend/blob/blob_links_tracking_spec.js b/spec/frontend/blob/blob_links_tracking_spec.js
new file mode 100644
index 00000000000..22e087bc180
--- /dev/null
+++ b/spec/frontend/blob/blob_links_tracking_spec.js
@@ -0,0 +1,60 @@
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
+import addBlobLinksTracking from '~/blob/blob_links_tracking';
+import Tracking from '~/tracking';
+
+describe('Blob links Tracking', () => {
+ const eventName = 'click_link';
+ const label = 'file_line_action';
+
+ const eventsToTrack = [
+ { selector: '.file-line-blame', property: 'blame' },
+ { selector: '.file-line-num', property: 'link' },
+ ];
+
+ const [blameLinkClickEvent, numLinkClickEvent] = eventsToTrack;
+
+ beforeEach(() => {
+ setHTMLFixture(`
+ <div id="blob-content-holder">
+ <div class="line-links diff-line-num">
+ <a href="#L5" class="file-line-blame"></a>
+ <a id="L5" href="#L5" data-line-number="5" class="file-line-num">5</a>
+ </div>
+ <pre id="LC5">Line 5 content</pre>
+ </div>
+ `);
+ addBlobLinksTracking('#blob-content-holder', eventsToTrack);
+ jest.spyOn(Tracking, 'event');
+ });
+
+ afterEach(() => {
+ resetHTMLFixture();
+ });
+
+ it('tracks blame link click event', () => {
+ const blameButton = document.querySelector(blameLinkClickEvent.selector);
+ blameButton.click();
+
+ expect(Tracking.event).toHaveBeenCalledWith(undefined, eventName, {
+ label,
+ property: blameLinkClickEvent.property,
+ });
+ });
+
+ it('tracks num link click event', () => {
+ const numLinkButton = document.querySelector(numLinkClickEvent.selector);
+ numLinkButton.click();
+
+ expect(Tracking.event).toHaveBeenCalledWith(undefined, eventName, {
+ label,
+ property: numLinkClickEvent.property,
+ });
+ });
+
+ it("doesn't fire tracking if the user clicks on any element that is not a link", () => {
+ const codeLine = document.querySelector('#LC5');
+ codeLine.click();
+
+ expect(Tracking.event).not.toHaveBeenCalled();
+ });
+});
diff --git a/spec/frontend/blob/components/blob_content_spec.js b/spec/frontend/blob/components/blob_content_spec.js
index 8450c6b9332..788ee0a86ab 100644
--- a/spec/frontend/blob/components/blob_content_spec.js
+++ b/spec/frontend/blob/components/blob_content_spec.js
@@ -36,20 +36,20 @@ describe('Blob Content component', () => {
describe('rendering', () => {
it('renders loader if `loading: true`', () => {
createComponent({ loading: true });
- expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
- expect(wrapper.find(BlobContentError).exists()).toBe(false);
- expect(wrapper.find(RichViewer).exists()).toBe(false);
- expect(wrapper.find(SimpleViewer).exists()).toBe(false);
+ expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
+ expect(wrapper.findComponent(BlobContentError).exists()).toBe(false);
+ expect(wrapper.findComponent(RichViewer).exists()).toBe(false);
+ expect(wrapper.findComponent(SimpleViewer).exists()).toBe(false);
});
it('renders error if there is any in the viewer', () => {
const renderError = 'Oops';
const viewer = { ...SimpleViewerMock, renderError };
createComponent({}, viewer);
- expect(wrapper.find(GlLoadingIcon).exists()).toBe(false);
- expect(wrapper.find(BlobContentError).exists()).toBe(true);
- expect(wrapper.find(RichViewer).exists()).toBe(false);
- expect(wrapper.find(SimpleViewer).exists()).toBe(false);
+ expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(false);
+ expect(wrapper.findComponent(BlobContentError).exists()).toBe(true);
+ expect(wrapper.findComponent(RichViewer).exists()).toBe(false);
+ expect(wrapper.findComponent(SimpleViewer).exists()).toBe(false);
});
it.each`
@@ -60,7 +60,7 @@ describe('Blob Content component', () => {
'renders $type viewer when activeViewer is $type and no loading or error detected',
({ mock, viewer }) => {
createComponent({}, mock);
- expect(wrapper.find(viewer).exists()).toBe(true);
+ expect(wrapper.findComponent(viewer).exists()).toBe(true);
},
);
@@ -70,13 +70,13 @@ describe('Blob Content component', () => {
${RichBlobContentMock.richData} | ${RichViewerMock} | ${RichViewer}
`('renders correct content that is passed to the component', ({ content, mock, viewer }) => {
createComponent({ content }, mock);
- expect(wrapper.find(viewer).html()).toContain(content);
+ expect(wrapper.findComponent(viewer).html()).toContain(content);
});
});
describe('functionality', () => {
describe('render error', () => {
- const findErrorEl = () => wrapper.find(BlobContentError);
+ const findErrorEl = () => wrapper.findComponent(BlobContentError);
const renderError = BLOB_RENDER_ERRORS.REASONS.COLLAPSED.id;
const viewer = { ...SimpleViewerMock, renderError };
diff --git a/spec/frontend/blob/components/blob_edit_content_spec.js b/spec/frontend/blob/components/blob_edit_content_spec.js
index 9fc2356c018..5017b624292 100644
--- a/spec/frontend/blob/components/blob_edit_content_spec.js
+++ b/spec/frontend/blob/components/blob_edit_content_spec.js
@@ -69,7 +69,7 @@ describe('Blob Header Editing', () => {
});
it('initialises Source Editor', () => {
- const el = wrapper.find({ ref: 'editor' }).element;
+ const el = wrapper.findComponent({ ref: 'editor' }).element;
expect(utils.initSourceEditor).toHaveBeenCalledWith({
el,
blobPath: fileName,
diff --git a/spec/frontend/blob/components/blob_edit_header_spec.js b/spec/frontend/blob/components/blob_edit_header_spec.js
index b1ce0e9a4c5..c84b5896348 100644
--- a/spec/frontend/blob/components/blob_edit_header_spec.js
+++ b/spec/frontend/blob/components/blob_edit_header_spec.js
@@ -16,7 +16,7 @@ describe('Blob Header Editing', () => {
});
};
const findDeleteButton = () =>
- wrapper.findAll(GlButton).wrappers.find((x) => x.text() === 'Delete file');
+ wrapper.findAllComponents(GlButton).wrappers.find((x) => x.text() === 'Delete file');
beforeEach(() => {
createComponent();
@@ -32,7 +32,7 @@ describe('Blob Header Editing', () => {
});
it('contains a form input field', () => {
- expect(wrapper.find(GlFormInput).exists()).toBe(true);
+ expect(wrapper.findComponent(GlFormInput).exists()).toBe(true);
});
it('does not show delete button', () => {
@@ -42,7 +42,7 @@ describe('Blob Header Editing', () => {
describe('functionality', () => {
it('emits input event when the blob name is changed', async () => {
- const inputComponent = wrapper.find(GlFormInput);
+ const inputComponent = wrapper.findComponent(GlFormInput);
const newValue = 'bar.txt';
// setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
diff --git a/spec/frontend/blob/components/blob_header_default_actions_spec.js b/spec/frontend/blob/components/blob_header_default_actions_spec.js
index aa538facae2..0f015715dc2 100644
--- a/spec/frontend/blob/components/blob_header_default_actions_spec.js
+++ b/spec/frontend/blob/components/blob_header_default_actions_spec.js
@@ -30,8 +30,8 @@ describe('Blob Header Default Actions', () => {
beforeEach(() => {
createComponent();
- btnGroup = wrapper.find(GlButtonGroup);
- buttons = wrapper.findAll(GlButton);
+ btnGroup = wrapper.findComponent(GlButtonGroup);
+ buttons = wrapper.findAllComponents(GlButton);
});
afterEach(() => {
@@ -69,9 +69,9 @@ describe('Blob Header Default Actions', () => {
createComponent({
activeViewer: RICH_BLOB_VIEWER,
});
- buttons = wrapper.findAll(GlButton);
+ buttons = wrapper.findAllComponents(GlButton);
- expect(buttons.at(0).attributes('disabled')).toBeTruthy();
+ expect(buttons.at(0).attributes('disabled')).toBe('true');
});
it('does not render the copy button if a rendering error is set', () => {
diff --git a/spec/frontend/blob/components/blob_header_filepath_spec.js b/spec/frontend/blob/components/blob_header_filepath_spec.js
index 8220b598ff6..8c32cba1ba4 100644
--- a/spec/frontend/blob/components/blob_header_filepath_spec.js
+++ b/spec/frontend/blob/components/blob_header_filepath_spec.js
@@ -25,7 +25,7 @@ describe('Blob Header Filepath', () => {
wrapper.destroy();
});
- const findBadge = () => wrapper.find(GlBadge);
+ const findBadge = () => wrapper.findComponent(GlBadge);
describe('rendering', () => {
it('matches the snapshot', () => {
@@ -46,7 +46,7 @@ describe('Blob Header Filepath', () => {
it('renders copy-to-clipboard icon that copies path of the Blob', () => {
createComponent();
- const btn = wrapper.find(ClipboardButton);
+ const btn = wrapper.findComponent(ClipboardButton);
expect(btn.exists()).toBe(true);
expect(btn.vm.text).toBe(MockBlob.path);
});
diff --git a/spec/frontend/blob/components/blob_header_spec.js b/spec/frontend/blob/components/blob_header_spec.js
index ee42c2387ae..46740958090 100644
--- a/spec/frontend/blob/components/blob_header_spec.js
+++ b/spec/frontend/blob/components/blob_header_spec.js
@@ -31,7 +31,7 @@ describe('Blob Header Default Actions', () => {
});
describe('rendering', () => {
- const findDefaultActions = () => wrapper.find(DefaultActions);
+ const findDefaultActions = () => wrapper.findComponent(DefaultActions);
const slots = {
prepend: 'Foo Prepend',
@@ -45,17 +45,17 @@ describe('Blob Header Default Actions', () => {
it('renders all components', () => {
createComponent();
- expect(wrapper.find(TableContents).exists()).toBe(true);
- expect(wrapper.find(ViewerSwitcher).exists()).toBe(true);
+ expect(wrapper.findComponent(TableContents).exists()).toBe(true);
+ expect(wrapper.findComponent(ViewerSwitcher).exists()).toBe(true);
expect(findDefaultActions().exists()).toBe(true);
- expect(wrapper.find(BlobFilepath).exists()).toBe(true);
+ expect(wrapper.findComponent(BlobFilepath).exists()).toBe(true);
});
it('does not render viewer switcher if the blob has only the simple viewer', () => {
createComponent({
richViewer: null,
});
- expect(wrapper.find(ViewerSwitcher).exists()).toBe(false);
+ expect(wrapper.findComponent(ViewerSwitcher).exists()).toBe(false);
});
it('does not render viewer switcher if a corresponding prop is passed', () => {
@@ -66,7 +66,7 @@ describe('Blob Header Default Actions', () => {
hideViewerSwitcher: true,
},
);
- expect(wrapper.find(ViewerSwitcher).exists()).toBe(false);
+ expect(wrapper.findComponent(ViewerSwitcher).exists()).toBe(false);
});
it('does not render default actions is corresponding prop is passed', () => {
@@ -77,7 +77,7 @@ describe('Blob Header Default Actions', () => {
hideDefaultActions: true,
},
);
- expect(wrapper.find(DefaultActions).exists()).toBe(false);
+ expect(wrapper.findComponent(DefaultActions).exists()).toBe(false);
});
Object.keys(slots).forEach((slot) => {
diff --git a/spec/frontend/blob/components/blob_header_viewer_switcher_spec.js b/spec/frontend/blob/components/blob_header_viewer_switcher_spec.js
index 91baaf3ea69..1eac0733646 100644
--- a/spec/frontend/blob/components/blob_header_viewer_switcher_spec.js
+++ b/spec/frontend/blob/components/blob_header_viewer_switcher_spec.js
@@ -35,8 +35,8 @@ describe('Blob Header Viewer Switcher', () => {
beforeEach(() => {
createComponent();
- btnGroup = wrapper.find(GlButtonGroup);
- buttons = wrapper.findAll(GlButton);
+ btnGroup = wrapper.findComponent(GlButtonGroup);
+ buttons = wrapper.findAllComponents(GlButton);
});
it('renders gl-button-group component', () => {
@@ -58,7 +58,7 @@ describe('Blob Header Viewer Switcher', () => {
function factory(propsData = {}) {
createComponent(propsData);
- buttons = wrapper.findAll(GlButton);
+ buttons = wrapper.findAllComponents(GlButton);
simpleBtn = buttons.at(0);
richBtn = buttons.at(1);
diff --git a/spec/frontend/blob/notebook/notebook_viever_spec.js b/spec/frontend/blob/notebook/notebook_viever_spec.js
index 93406db2675..ea4badc03fb 100644
--- a/spec/frontend/blob/notebook/notebook_viever_spec.js
+++ b/spec/frontend/blob/notebook/notebook_viever_spec.js
@@ -31,10 +31,10 @@ describe('iPython notebook renderer', () => {
wrapper = shallowMount(component, { propsData: { endpoint, relativeRawPath } });
};
- const findLoading = () => wrapper.find(GlLoadingIcon);
- const findNotebookLab = () => wrapper.find(NotebookLab);
- const findLoadErrorMessage = () => wrapper.find({ ref: 'loadErrorMessage' });
- const findParseErrorMessage = () => wrapper.find({ ref: 'parsingErrorMessage' });
+ const findLoading = () => wrapper.findComponent(GlLoadingIcon);
+ const findNotebookLab = () => wrapper.findComponent(NotebookLab);
+ const findLoadErrorMessage = () => wrapper.findComponent({ ref: 'loadErrorMessage' });
+ const findParseErrorMessage = () => wrapper.findComponent({ ref: 'parsingErrorMessage' });
beforeEach(() => {
mock = new MockAdapter(axios);
diff --git a/spec/frontend/blob/pdf/pdf_viewer_spec.js b/spec/frontend/blob/pdf/pdf_viewer_spec.js
index e332ea49fa6..23227df6357 100644
--- a/spec/frontend/blob/pdf/pdf_viewer_spec.js
+++ b/spec/frontend/blob/pdf/pdf_viewer_spec.js
@@ -18,9 +18,9 @@ describe('PDF renderer', () => {
});
};
- const findLoading = () => wrapper.find(GlLoadingIcon);
- const findPdfLab = () => wrapper.find(PdfLab);
- const findLoadError = () => wrapper.find({ ref: 'loadError' });
+ const findLoading = () => wrapper.findComponent(GlLoadingIcon);
+ const findPdfLab = () => wrapper.findComponent(PdfLab);
+ const findLoadError = () => wrapper.findComponent({ ref: 'loadError' });
beforeEach(() => {
mountComponent();
diff --git a/spec/frontend/blob/pipeline_tour_success_modal_spec.js b/spec/frontend/blob/pipeline_tour_success_modal_spec.js
index 750dd8f0a72..81b38cfc278 100644
--- a/spec/frontend/blob/pipeline_tour_success_modal_spec.js
+++ b/spec/frontend/blob/pipeline_tour_success_modal_spec.js
@@ -52,7 +52,7 @@ describe('PipelineTourSuccessModal', () => {
});
it('renders the path from the commit cookie for back to the merge request button', () => {
- const goToMrBtn = wrapper.find({ ref: 'goToMergeRequest' });
+ const goToMrBtn = wrapper.findComponent({ ref: 'goToMergeRequest' });
expect(goToMrBtn.attributes('href')).toBe(expectedMrPath);
});
@@ -67,16 +67,16 @@ describe('PipelineTourSuccessModal', () => {
});
it('renders the path from projectMergeRequestsPath for back to the merge request button', () => {
- const goToMrBtn = wrapper.find({ ref: 'goToMergeRequest' });
+ const goToMrBtn = wrapper.findComponent({ ref: 'goToMergeRequest' });
expect(goToMrBtn.attributes('href')).toBe(expectedMrPath);
});
});
it('has expected structure', () => {
- const modal = wrapper.find(GlModal);
- const sprintf = modal.find(GlSprintf);
- const emoji = modal.find(GlEmoji);
+ const modal = wrapper.findComponent(GlModal);
+ const sprintf = modal.findComponent(GlSprintf);
+ const emoji = modal.findComponent(GlEmoji);
expect(wrapper.text()).toContain("That's it, well done!");
expect(sprintf.exists()).toBe(true);
@@ -84,7 +84,7 @@ describe('PipelineTourSuccessModal', () => {
});
it('renders the link for codeQualityLink', () => {
- expect(wrapper.find(GlLink).attributes('href')).toBe('/code-quality-link');
+ expect(wrapper.findComponent(GlLink).attributes('href')).toBe('/code-quality-link');
});
it('calls to remove cookie', () => {
@@ -103,7 +103,7 @@ describe('PipelineTourSuccessModal', () => {
it('send an event when go to pipelines is clicked', () => {
trackingSpy = mockTracking('_category_', wrapper.element, jest.spyOn);
- const goToBtn = wrapper.find({ ref: 'goToPipelines' });
+ const goToBtn = wrapper.findComponent({ ref: 'goToPipelines' });
triggerEvent(goToBtn.element);
expect(trackingSpy).toHaveBeenCalledWith('_category_', 'click_button', {
@@ -115,7 +115,7 @@ describe('PipelineTourSuccessModal', () => {
it('sends an event when back to the merge request is clicked', () => {
trackingSpy = mockTracking('_category_', wrapper.element, jest.spyOn);
- const goToBtn = wrapper.find({ ref: 'goToMergeRequest' });
+ const goToBtn = wrapper.findComponent({ ref: 'goToMergeRequest' });
triggerEvent(goToBtn.element);
expect(trackingSpy).toHaveBeenCalledWith('_category_', 'click_button', {
diff --git a/spec/frontend/blob/sketch/index_spec.js b/spec/frontend/blob/sketch/index_spec.js
index 5e1922a24f4..e8d1f724c4b 100644
--- a/spec/frontend/blob/sketch/index_spec.js
+++ b/spec/frontend/blob/sketch/index_spec.js
@@ -69,7 +69,7 @@ describe('Sketch viewer', () => {
const img = document.querySelector('#js-sketch-viewer img');
expect(img).not.toBeNull();
- expect(img.classList.contains('img-fluid')).toBeTruthy();
+ expect(img.classList.contains('img-fluid')).toBe(true);
});
it('renders link to image', () => {
diff --git a/spec/frontend/blob/suggest_gitlab_ci_yml/components/popover_spec.js b/spec/frontend/blob/suggest_gitlab_ci_yml/components/popover_spec.js
index 7e13994f2b7..6b329dc078a 100644
--- a/spec/frontend/blob/suggest_gitlab_ci_yml/components/popover_spec.js
+++ b/spec/frontend/blob/suggest_gitlab_ci_yml/components/popover_spec.js
@@ -98,7 +98,7 @@ describe('Suggest gitlab-ci.yml Popover', () => {
const expectedAction = 'click_button';
const expectedProperty = 'owner';
const expectedValue = '10';
- const dismissButton = wrapper.find(GlButton);
+ const dismissButton = wrapper.findComponent(GlButton);
trackingSpy = mockTracking('_category_', wrapper.element, jest.spyOn);
triggerEvent(dismissButton.element);
diff --git a/spec/frontend/boards/board_card_inner_spec.js b/spec/frontend/boards/board_card_inner_spec.js
index c6de3ee69f3..985902b4a3b 100644
--- a/spec/frontend/boards/board_card_inner_spec.js
+++ b/spec/frontend/boards/board_card_inner_spec.js
@@ -238,7 +238,7 @@ describe('Board card component', () => {
});
it('renders assignee', () => {
- expect(wrapper.find('.board-card-assignee .avatar').exists()).toBe(true);
+ expect(wrapper.find('.board-card-assignee .gl-avatar').exists()).toBe(true);
});
it('sets title', () => {
@@ -336,7 +336,7 @@ describe('Board card component', () => {
});
it('renders all three assignees', () => {
- expect(wrapper.findAll('.board-card-assignee .avatar').length).toEqual(3);
+ expect(wrapper.findAll('.board-card-assignee .gl-avatar').length).toEqual(3);
});
describe('more than three assignees', () => {
@@ -362,7 +362,7 @@ describe('Board card component', () => {
});
it('renders two assignees', () => {
- expect(wrapper.findAll('.board-card-assignee .avatar').length).toEqual(2);
+ expect(wrapper.findAll('.board-card-assignee .gl-avatar').length).toEqual(2);
});
it('renders 99+ avatar counter', async () => {
diff --git a/spec/frontend/boards/board_list_spec.js b/spec/frontend/boards/board_list_spec.js
index fd9d2b6823d..9b0c0b93ffb 100644
--- a/spec/frontend/boards/board_list_spec.js
+++ b/spec/frontend/boards/board_list_spec.js
@@ -56,7 +56,7 @@ describe('Board list component', () => {
});
it('renders issues', () => {
- expect(wrapper.findAll(BoardCard).length).toBe(1);
+ expect(wrapper.findAllComponents(BoardCard).length).toBe(1);
});
it('sets data attribute with issue id', () => {
diff --git a/spec/frontend/boards/components/board_add_new_column_form_spec.js b/spec/frontend/boards/components/board_add_new_column_form_spec.js
index 3b26ca57d6f..0b3c6cb24c4 100644
--- a/spec/frontend/boards/components/board_add_new_column_form_spec.js
+++ b/spec/frontend/boards/components/board_add_new_column_form_spec.js
@@ -60,8 +60,8 @@ describe('Board card layout', () => {
});
const formTitle = () => wrapper.findByTestId('board-add-column-form-title').text();
- const findSearchInput = () => wrapper.find(GlSearchBoxByType);
- const findSearchLabel = () => wrapper.find(GlFormGroup);
+ const findSearchInput = () => wrapper.findComponent(GlSearchBoxByType);
+ const findSearchLabelFormGroup = () => wrapper.findComponent(GlFormGroup);
const cancelButton = () => wrapper.findByTestId('cancelAddNewColumn');
const submitButton = () => wrapper.findByTestId('addNewColumnButton');
const findDropdown = () => wrapper.findComponent(GlDropdown);
@@ -121,10 +121,17 @@ describe('Board card layout', () => {
mountComponent(props);
- expect(findSearchLabel().attributes('label')).toEqual(props.searchLabel);
+ expect(findSearchLabelFormGroup().attributes('label')).toEqual(props.searchLabel);
expect(findSearchInput().attributes('placeholder')).toEqual(props.searchPlaceholder);
});
+ it('does not show the dropdown as invalid by default', () => {
+ mountComponent();
+
+ expect(findSearchLabelFormGroup().attributes('state')).toBe('true');
+ expect(findDropdown().props('toggleClass')).not.toContain('gl-inset-border-1-red-400!');
+ });
+
it('emits filter event on input', () => {
mountComponent();
@@ -137,13 +144,13 @@ describe('Board card layout', () => {
});
describe('Add list button', () => {
- it('is disabled if no item is selected', () => {
+ it('is enabled by default', () => {
mountComponent();
- expect(submitButton().props('disabled')).toBe(true);
+ expect(submitButton().props('disabled')).toBe(false);
});
- it('emits add-list event on click', () => {
+ it('emits add-list event on click when an ID is selected', () => {
mountComponent({
selectedId: mockLabelList.label.id,
});
@@ -152,5 +159,16 @@ describe('Board card layout', () => {
expect(wrapper.emitted('add-list')).toEqual([[]]);
});
+
+ it('does not emit the add-list event on click and shows the dropdown as invalid when no ID is selected', async () => {
+ mountComponent();
+
+ await submitButton().vm.$emit('click');
+
+ expect(findSearchLabelFormGroup().attributes('state')).toBeUndefined();
+ expect(findDropdown().props('toggleClass')).toContain('gl-inset-border-1-red-400!');
+
+ expect(wrapper.emitted('add-list')).toBeUndefined();
+ });
});
});
diff --git a/spec/frontend/boards/components/board_add_new_column_trigger_spec.js b/spec/frontend/boards/components/board_add_new_column_trigger_spec.js
index 7dd02bf1d35..354eb7bff16 100644
--- a/spec/frontend/boards/components/board_add_new_column_trigger_spec.js
+++ b/spec/frontend/boards/components/board_add_new_column_trigger_spec.js
@@ -39,7 +39,7 @@ describe('BoardAddNewColumnTrigger', () => {
});
it('renders an enabled button', () => {
- const button = wrapper.find(GlButton);
+ const button = wrapper.findComponent(GlButton);
expect(button.props('disabled')).toBe(false);
});
@@ -47,7 +47,7 @@ describe('BoardAddNewColumnTrigger', () => {
describe('when button is disabled', () => {
it('shows the tooltip', async () => {
- wrapper.find(GlButton).vm.$emit('click');
+ wrapper.findComponent(GlButton).vm.$emit('click');
await nextTick();
diff --git a/spec/frontend/boards/components/board_blocked_icon_spec.js b/spec/frontend/boards/components/board_blocked_icon_spec.js
index 7a5c49bd488..cf4ba07da16 100644
--- a/spec/frontend/boards/components/board_blocked_icon_spec.js
+++ b/spec/frontend/boards/components/board_blocked_icon_spec.js
@@ -23,9 +23,9 @@ describe('BoardBlockedIcon', () => {
let wrapper;
let mockApollo;
- const findGlIcon = () => wrapper.find(GlIcon);
- const findGlPopover = () => wrapper.find(GlPopover);
- const findGlLink = () => wrapper.find(GlLink);
+ const findGlIcon = () => wrapper.findComponent(GlIcon);
+ const findGlPopover = () => wrapper.findComponent(GlPopover);
+ const findGlLink = () => wrapper.findComponent(GlLink);
const findPopoverTitle = () => wrapper.findByTestId('popover-title');
const findIssuableTitle = () => wrapper.findByTestId('issuable-title');
const findHiddenBlockingCount = () => wrapper.findByTestId('hidden-blocking-count');
@@ -114,7 +114,7 @@ describe('BoardBlockedIcon', () => {
it('should display a loading spinner while loading', () => {
createWrapper({ loading: true });
- expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
+ expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
});
it('should not query for blocking issuables by default', async () => {
diff --git a/spec/frontend/boards/components/board_card_spec.js b/spec/frontend/boards/components/board_card_spec.js
index 17a5383a31e..bb1e63a581e 100644
--- a/spec/frontend/boards/components/board_card_spec.js
+++ b/spec/frontend/boards/components/board_card_spec.js
@@ -88,7 +88,7 @@ describe('Board card', () => {
createStore({ initialState: { isShowingLabels: true } });
mountComponent({ mountFn: mount, stubs: {} });
- wrapper.find(GlLabel).trigger('mouseup');
+ wrapper.findComponent(GlLabel).trigger('mouseup');
expect(mockActions.toggleBoardItem).toHaveBeenCalledTimes(0);
});
diff --git a/spec/frontend/boards/components/board_content_sidebar_spec.js b/spec/frontend/boards/components/board_content_sidebar_spec.js
index 368c7d561f8..7e35c39cd48 100644
--- a/spec/frontend/boards/components/board_content_sidebar_spec.js
+++ b/spec/frontend/boards/components/board_content_sidebar_spec.js
@@ -108,7 +108,7 @@ describe('BoardContentSidebar', () => {
createStore({ mockGetters: { isSidebarOpen: () => false } });
createComponent();
- expect(wrapper.findComponent(GlDrawer).exists()).toBe(false);
+ expect(wrapper.findComponent(GlDrawer).props('open')).toBe(false);
});
it('applies an open attribute', () => {
diff --git a/spec/frontend/boards/components/board_content_spec.js b/spec/frontend/boards/components/board_content_spec.js
index f535679b8a0..97d9e08f5d4 100644
--- a/spec/frontend/boards/components/board_content_spec.js
+++ b/spec/frontend/boards/components/board_content_spec.js
@@ -67,12 +67,12 @@ describe('BoardContent', () => {
});
it('renders BoardContentSidebar', () => {
- expect(wrapper.find(BoardContentSidebar).exists()).toBe(true);
+ expect(wrapper.findComponent(BoardContentSidebar).exists()).toBe(true);
});
it('does not display EpicsSwimlanes component', () => {
- expect(wrapper.find(EpicsSwimlanes).exists()).toBe(false);
- expect(wrapper.find(GlAlert).exists()).toBe(false);
+ expect(wrapper.findComponent(EpicsSwimlanes).exists()).toBe(false);
+ expect(wrapper.findComponent(GlAlert).exists()).toBe(false);
});
});
@@ -82,7 +82,7 @@ describe('BoardContent', () => {
});
it('does not render BoardContentSidebar', () => {
- expect(wrapper.find(BoardContentSidebar).exists()).toBe(false);
+ expect(wrapper.findComponent(BoardContentSidebar).exists()).toBe(false);
});
});
@@ -92,7 +92,7 @@ describe('BoardContent', () => {
});
it('renders draggable component', () => {
- expect(wrapper.find(Draggable).exists()).toBe(true);
+ expect(wrapper.findComponent(Draggable).exists()).toBe(true);
});
});
@@ -102,7 +102,7 @@ describe('BoardContent', () => {
});
it('does not render draggable component', () => {
- expect(wrapper.find(Draggable).exists()).toBe(false);
+ expect(wrapper.findComponent(Draggable).exists()).toBe(false);
});
});
});
diff --git a/spec/frontend/boards/components/board_list_header_spec.js b/spec/frontend/boards/components/board_list_header_spec.js
index 2f9677680eb..50901f3fe84 100644
--- a/spec/frontend/boards/components/board_list_header_spec.js
+++ b/spec/frontend/boards/components/board_list_header_spec.js
@@ -83,7 +83,7 @@ describe('Board List Header Component', () => {
const isCollapsed = () => wrapper.vm.list.collapsed;
- const findAddIssueButton = () => wrapper.find({ ref: 'newIssueBtn' });
+ const findAddIssueButton = () => wrapper.findComponent({ ref: 'newIssueBtn' });
const findTitle = () => wrapper.find('.board-title');
const findCaret = () => wrapper.findByTestId('board-title-caret');
diff --git a/spec/frontend/boards/components/board_new_item_spec.js b/spec/frontend/boards/components/board_new_item_spec.js
index 86cebc8a719..f4e9901aad2 100644
--- a/spec/frontend/boards/components/board_new_item_spec.js
+++ b/spec/frontend/boards/components/board_new_item_spec.js
@@ -44,7 +44,7 @@ describe('BoardNewItem', () => {
it('finds an enabled create button', async () => {
expect(wrapper.findByTestId('create-button').props('disabled')).toBe(true);
- wrapper.find(GlFormInput).vm.$emit('input', 'hello');
+ wrapper.findComponent(GlFormInput).vm.$emit('input', 'hello');
await nextTick();
expect(wrapper.findByTestId('create-button').props('disabled')).toBe(false);
@@ -53,7 +53,7 @@ describe('BoardNewItem', () => {
describe('when the user types in a string with only spaces', () => {
it('disables the Create Issue button', async () => {
- wrapper.find(GlFormInput).vm.$emit('input', ' ');
+ wrapper.findComponent(GlFormInput).vm.$emit('input', ' ');
await nextTick();
@@ -93,7 +93,7 @@ describe('BoardNewItem', () => {
titleInput().setValue('Foo');
await glForm().trigger('submit');
- expect(wrapper.emitted('form-submit')).toBeTruthy();
+ expect(wrapper.emitted('form-submit')).toHaveLength(1);
expect(wrapper.emitted('form-submit')[0]).toEqual([
{
title: 'Foo',
@@ -131,7 +131,7 @@ describe('BoardNewItem', () => {
await glForm().trigger('reset');
expect(titleInput().element.value).toBe('');
- expect(wrapper.emitted('form-cancel')).toBeTruthy();
+ expect(wrapper.emitted('form-cancel')).toHaveLength(1);
});
});
});
diff --git a/spec/frontend/boards/components/board_settings_sidebar_spec.js b/spec/frontend/boards/components/board_settings_sidebar_spec.js
index 7f40c426b30..4171a6236de 100644
--- a/spec/frontend/boards/components/board_settings_sidebar_spec.js
+++ b/spec/frontend/boards/components/board_settings_sidebar_spec.js
@@ -57,10 +57,10 @@ describe('BoardSettingsSidebar', () => {
}),
);
};
- const findLabel = () => wrapper.find(GlLabel);
- const findDrawer = () => wrapper.find(GlDrawer);
- const findModal = () => wrapper.find(GlModal);
- const findRemoveButton = () => wrapper.find(GlButton);
+ const findLabel = () => wrapper.findComponent(GlLabel);
+ const findDrawer = () => wrapper.findComponent(GlDrawer);
+ const findModal = () => wrapper.findComponent(GlModal);
+ const findRemoveButton = () => wrapper.findComponent(GlButton);
afterEach(() => {
jest.restoreAllMocks();
@@ -71,7 +71,7 @@ describe('BoardSettingsSidebar', () => {
it('finds a MountingPortal component', () => {
createComponent();
- expect(wrapper.find(MountingPortal).props()).toMatchObject({
+ expect(wrapper.findComponent(MountingPortal).props()).toMatchObject({
mountTo: '#js-right-sidebar-portal',
append: true,
name: 'board-settings-sidebar',
@@ -93,7 +93,7 @@ describe('BoardSettingsSidebar', () => {
await nextTick();
- expect(wrapper.find(GlDrawer).exists()).toBe(false);
+ expect(wrapper.findComponent(GlDrawer).props('open')).toBe(false);
});
it('closes the sidebar when emitting the correct event', async () => {
@@ -103,7 +103,7 @@ describe('BoardSettingsSidebar', () => {
await nextTick();
- expect(wrapper.find(GlDrawer).exists()).toBe(false);
+ expect(wrapper.findComponent(GlDrawer).props('open')).toBe(false);
});
});
@@ -150,7 +150,7 @@ describe('BoardSettingsSidebar', () => {
it('does not render GlDrawer', () => {
createComponent({ sidebarType: '' });
- expect(findDrawer().exists()).toBe(false);
+ expect(findDrawer().props('open')).toBe(false);
});
});
diff --git a/spec/frontend/boards/components/boards_selector_spec.js b/spec/frontend/boards/components/boards_selector_spec.js
index d91e81fe4d0..f3be66db36f 100644
--- a/spec/frontend/boards/components/boards_selector_spec.js
+++ b/spec/frontend/boards/components/boards_selector_spec.js
@@ -53,7 +53,7 @@ describe('BoardsSelector', () => {
};
const fillSearchBox = (filterTerm) => {
- const searchBox = wrapper.find({ ref: 'searchBox' });
+ const searchBox = wrapper.findComponent({ ref: 'searchBox' });
const searchBoxInput = searchBox.find('input');
searchBoxInput.setValue(filterTerm);
searchBoxInput.trigger('input');
diff --git a/spec/frontend/boards/components/new_board_button_spec.js b/spec/frontend/boards/components/new_board_button_spec.js
index 075fe225ec2..2bbd3797abf 100644
--- a/spec/frontend/boards/components/new_board_button_spec.js
+++ b/spec/frontend/boards/components/new_board_button_spec.js
@@ -53,13 +53,13 @@ describe('NewBoardButton', () => {
it('renders nothing when `canAdminBoard` is `false`', () => {
wrapper = createComponent({ canAdminBoard: false });
- expect(wrapper.find(GlButton).exists()).toBe(false);
+ expect(wrapper.findComponent(GlButton).exists()).toBe(false);
});
it('renders nothing when `multipleIssueBoardsAvailable` is `false`', () => {
wrapper = createComponent({ multipleIssueBoardsAvailable: false });
- expect(wrapper.find(GlButton).exists()).toBe(false);
+ expect(wrapper.findComponent(GlButton).exists()).toBe(false);
});
it('emits `showBoardModal` when button is clicked', () => {
@@ -67,7 +67,7 @@ describe('NewBoardButton', () => {
wrapper = createComponent();
- wrapper.find(GlButton).vm.$emit('click', { preventDefault: () => {} });
+ wrapper.findComponent(GlButton).vm.$emit('click', { preventDefault: () => {} });
expect(eventHub.$emit).toHaveBeenCalledWith('showBoardModal', 'new');
});
diff --git a/spec/frontend/boards/components/sidebar/board_editable_item_spec.js b/spec/frontend/boards/components/sidebar/board_editable_item_spec.js
index 0c76c711b3a..5e2222ac3d7 100644
--- a/spec/frontend/boards/components/sidebar/board_editable_item_spec.js
+++ b/spec/frontend/boards/components/sidebar/board_editable_item_spec.js
@@ -6,7 +6,7 @@ import BoardSidebarItem from '~/boards/components/sidebar/board_editable_item.vu
describe('boards sidebar remove issue', () => {
let wrapper;
- const findLoader = () => wrapper.find(GlLoadingIcon);
+ const findLoader = () => wrapper.findComponent(GlLoadingIcon);
const findEditButton = () => wrapper.find('[data-testid="edit-button"]');
const findTitle = () => wrapper.find('[data-testid="title"]');
const findCollapsed = () => wrapper.find('[data-testid="collapsed-content"]');
diff --git a/spec/frontend/boards/components/sidebar/board_sidebar_time_tracker_spec.js b/spec/frontend/boards/components/sidebar/board_sidebar_time_tracker_spec.js
index 7c8996be0b8..5c435643425 100644
--- a/spec/frontend/boards/components/sidebar/board_sidebar_time_tracker_spec.js
+++ b/spec/frontend/boards/components/sidebar/board_sidebar_time_tracker_spec.js
@@ -47,7 +47,7 @@ describe('BoardSidebarTimeTracker', () => {
(timeTrackingLimitToHours) => {
createComponent({ provide: { timeTrackingLimitToHours } });
- expect(wrapper.find(IssuableTimeTracker).props()).toEqual({
+ expect(wrapper.findComponent(IssuableTimeTracker).props()).toEqual({
limitToHours: timeTrackingLimitToHours,
showCollapsed: false,
issuableId: '1',
diff --git a/spec/frontend/boards/components/sidebar/board_sidebar_title_spec.js b/spec/frontend/boards/components/sidebar/board_sidebar_title_spec.js
index 5364d929c38..cc1e5de15c1 100644
--- a/spec/frontend/boards/components/sidebar/board_sidebar_title_spec.js
+++ b/spec/frontend/boards/components/sidebar/board_sidebar_title_spec.js
@@ -46,10 +46,10 @@ describe('~/boards/components/sidebar/board_sidebar_title.vue', () => {
});
};
- const findForm = () => wrapper.find(GlForm);
- const findAlert = () => wrapper.find(GlAlert);
- const findFormInput = () => wrapper.find(GlFormInput);
- const findEditableItem = () => wrapper.find(BoardEditableItem);
+ const findForm = () => wrapper.findComponent(GlForm);
+ const findAlert = () => wrapper.findComponent(GlAlert);
+ const findFormInput = () => wrapper.findComponent(GlFormInput);
+ const findEditableItem = () => wrapper.findComponent(BoardEditableItem);
const findCancelButton = () => wrapper.find('[data-testid="cancel-button"]');
const findTitle = () => wrapper.find('[data-testid="item-title"]');
const findCollapsed = () => wrapper.find('[data-testid="collapsed-content"]');
diff --git a/spec/frontend/boards/project_select_spec.js b/spec/frontend/boards/project_select_spec.js
index c45cd545155..7ff34ffdf9e 100644
--- a/spec/frontend/boards/project_select_spec.js
+++ b/spec/frontend/boards/project_select_spec.js
@@ -10,7 +10,6 @@ import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import ProjectSelect from '~/boards/components/project_select.vue';
import defaultState from '~/boards/stores/state';
-import waitForPromises from 'helpers/wait_for_promises';
import { mockList, mockActiveGroupProjects } from './mock_data';
@@ -23,9 +22,9 @@ describe('ProjectSelect component', () => {
const findLabel = () => wrapper.find("[data-testid='header-label']");
const findGlDropdown = () => wrapper.findComponent(GlDropdown);
const findGlDropdownLoadingIcon = () =>
- findGlDropdown().find('button:first-child').find(GlLoadingIcon);
- const findGlSearchBoxByType = () => wrapper.find(GlSearchBoxByType);
- const findGlDropdownItems = () => wrapper.findAll(GlDropdownItem);
+ findGlDropdown().find('button:first-child').findComponent(GlLoadingIcon);
+ const findGlSearchBoxByType = () => wrapper.findComponent(GlSearchBoxByType);
+ const findGlDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
const findFirstGlDropdownItem = () => findGlDropdownItems().at(0);
const findInMenuLoadingIcon = () => wrapper.find("[data-testid='dropdown-text-loading-icon']");
const findEmptySearchMessage = () => wrapper.find("[data-testid='empty-result-message']");
@@ -133,7 +132,7 @@ describe('ProjectSelect component', () => {
const dropdownToggle = findGlDropdown().find('.dropdown-toggle');
await dropdownToggle.trigger('click');
- await waitForPromises();
+ jest.runOnlyPendingTimers();
await nextTick();
const searchInput = findGlDropdown().findComponent(GlFormInput).element;
diff --git a/spec/frontend/boards/stores/mutations_spec.js b/spec/frontend/boards/stores/mutations_spec.js
index 7d79993a0ee..1606ca09d8f 100644
--- a/spec/frontend/boards/stores/mutations_spec.js
+++ b/spec/frontend/boards/stores/mutations_spec.js
@@ -518,17 +518,6 @@ describe('Board Store Mutations', () => {
expect(state.boardItemsByListId[payload.listId]).toEqual(listState);
});
-
- it("updates the list's items count", () => {
- expect(state.boardLists['gid://gitlab/List/1'].issuesCount).toBe(1);
-
- mutations.ADD_BOARD_ITEM_TO_LIST(state, {
- itemId: mockIssue2.id,
- listId: mockList.id,
- });
-
- expect(state.boardLists['gid://gitlab/List/1'].issuesCount).toBe(2);
- });
});
describe('REMOVE_BOARD_ITEM_FROM_LIST', () => {
@@ -536,8 +525,7 @@ describe('Board Store Mutations', () => {
setBoardsListsState();
});
- it("removes an item from a list and updates the list's items count", () => {
- expect(state.boardLists['gid://gitlab/List/1'].issuesCount).toBe(1);
+ it('removes an item from a list', () => {
expect(state.boardItemsByListId['gid://gitlab/List/1']).toContain(mockIssue.id);
mutations.REMOVE_BOARD_ITEM_FROM_LIST(state, {
@@ -546,7 +534,6 @@ describe('Board Store Mutations', () => {
});
expect(state.boardItemsByListId['gid://gitlab/List/1']).not.toContain(mockIssue.id);
- expect(state.boardLists['gid://gitlab/List/1'].issuesCount).toBe(0);
});
});
diff --git a/spec/frontend/captcha/wait_for_captcha_to_be_solved_spec.js b/spec/frontend/captcha/wait_for_captcha_to_be_solved_spec.js
index 08d031a4fa7..2263d2bbeed 100644
--- a/spec/frontend/captcha/wait_for_captcha_to_be_solved_spec.js
+++ b/spec/frontend/captcha/wait_for_captcha_to_be_solved_spec.js
@@ -1,3 +1,4 @@
+import { nextTick } from 'vue';
import CaptchaModal from '~/captcha/captcha_modal.vue';
import { waitForCaptchaToBeSolved } from '~/captcha/wait_for_captcha_to_be_solved';
@@ -15,7 +16,7 @@ describe('waitForCaptchaToBeSolved', () => {
it('opens a modal, resolves with captcha response on success', async () => {
CaptchaModal.mounted.mockImplementationOnce(function mounted() {
- requestAnimationFrame(() => {
+ return nextTick().then(() => {
this.$emit('receivedCaptchaResponse', response);
this.$emit('hidden');
});
@@ -36,7 +37,7 @@ describe('waitForCaptchaToBeSolved', () => {
it("opens a modal, rejects with error in case the captcha isn't solved", async () => {
CaptchaModal.mounted.mockImplementationOnce(function mounted() {
- requestAnimationFrame(() => {
+ return nextTick().then(() => {
this.$emit('receivedCaptchaResponse', null);
this.$emit('hidden');
});
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
new file mode 100644
index 00000000000..920ceaefb70
--- /dev/null
+++ b/spec/frontend/ci_variable_list/components/ci_admin_variables_spec.js
@@ -0,0 +1,178 @@
+import Vue, { nextTick } from 'vue';
+import VueApollo from 'vue-apollo';
+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 ciAdminVariables from '~/ci_variable_list/components/ci_admin_variables.vue';
+import ciVariableSettings from '~/ci_variable_list/components/ci_variable_settings.vue';
+import ciVariableTable from '~/ci_variable_list/components/ci_variable_table.vue';
+import getAdminVariables from '~/ci_variable_list/graphql/queries/variables.query.graphql';
+
+import addAdminVariable from '~/ci_variable_list/graphql/mutations/admin_add_variable.mutation.graphql';
+import deleteAdminVariable from '~/ci_variable_list/graphql/mutations/admin_delete_variable.mutation.graphql';
+import updateAdminVariable from '~/ci_variable_list/graphql/mutations/admin_update_variable.mutation.graphql';
+
+import { genericMutationErrorText, variableFetchErrorText } from '~/ci_variable_list/constants';
+
+import { mockAdminVariables, newVariable } from '../mocks';
+
+jest.mock('~/flash');
+
+Vue.use(VueApollo);
+
+const mockProvide = {
+ endpoint: '/variables',
+};
+
+describe('Ci Admin Variable list', () => {
+ let wrapper;
+
+ let mockApollo;
+ let mockVariables;
+
+ const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
+ const findCiTable = () => wrapper.findComponent(GlTable);
+ const findCiSettings = () => wrapper.findComponent(ciVariableSettings);
+
+ // eslint-disable-next-line consistent-return
+ const createComponentWithApollo = async ({ isLoading = false } = {}) => {
+ const handlers = [[getAdminVariables, mockVariables]];
+
+ mockApollo = createMockApollo(handlers, resolvers);
+
+ wrapper = shallowMount(ciAdminVariables, {
+ provide: mockProvide,
+ apolloProvider: mockApollo,
+ stubs: { ciVariableSettings, ciVariableTable },
+ });
+
+ if (!isLoading) {
+ return waitForPromises();
+ }
+ };
+
+ beforeEach(() => {
+ mockVariables = jest.fn();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('while queries are being fetch', () => {
+ beforeEach(() => {
+ createComponentWithApollo({ isLoading: true });
+ });
+
+ it('shows a loading icon', () => {
+ expect(findLoadingIcon().exists()).toBe(true);
+ expect(findCiTable().exists()).toBe(false);
+ });
+ });
+
+ describe('when queries are resolved', () => {
+ describe('successfuly', () => {
+ beforeEach(async () => {
+ mockVariables.mockResolvedValue(mockAdminVariables);
+
+ await createComponentWithApollo();
+ });
+
+ it('passes down the expected environments as props', () => {
+ expect(findCiSettings().props('environments')).toEqual([]);
+ });
+
+ it('passes down the expected variables as props', () => {
+ expect(findCiSettings().props('variables')).toEqual(
+ mockAdminVariables.data.ciVariables.nodes,
+ );
+ });
+
+ it('createFlash was not called', () => {
+ expect(createFlash).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('with an error for variables', () => {
+ beforeEach(async () => {
+ mockVariables.mockRejectedValue();
+
+ await createComponentWithApollo();
+ });
+
+ it('calls createFlash with the expected error message', () => {
+ expect(createFlash).toHaveBeenCalledWith({ message: variableFetchErrorText });
+ });
+ });
+ });
+
+ describe('mutations', () => {
+ beforeEach(async () => {
+ mockVariables.mockResolvedValue(mockAdminVariables);
+
+ await createComponentWithApollo();
+ });
+ it.each`
+ actionName | mutation | event
+ ${'add'} | ${addAdminVariable} | ${'add-variable'}
+ ${'update'} | ${updateAdminVariable} | ${'update-variable'}
+ ${'delete'} | ${deleteAdminVariable} | ${'delete-variable'}
+ `(
+ 'calls the right mutation when user performs $actionName variable',
+ async ({ event, mutation }) => {
+ jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue();
+ await findCiSettings().vm.$emit(event, newVariable);
+
+ expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({
+ mutation,
+ variables: {
+ endpoint: mockProvide.endpoint,
+ variable: newVariable,
+ },
+ });
+ },
+ );
+
+ it.each`
+ actionName | event | mutationName
+ ${'add'} | ${'add-variable'} | ${'addAdminVariable'}
+ ${'update'} | ${'update-variable'} | ${'updateAdminVariable'}
+ ${'delete'} | ${'delete-variable'} | ${'deleteAdminVariable'}
+ `(
+ 'throws with the specific graphql error if present when user performs $actionName variable',
+ async ({ event, mutationName }) => {
+ const graphQLErrorMessage = 'There is a problem with this graphQL action';
+ jest
+ .spyOn(wrapper.vm.$apollo, 'mutate')
+ .mockResolvedValue({ data: { [mutationName]: { errors: [graphQLErrorMessage] } } });
+ await findCiSettings().vm.$emit(event, newVariable);
+ await nextTick();
+
+ expect(wrapper.vm.$apollo.mutate).toHaveBeenCalled();
+ expect(createFlash).toHaveBeenCalledWith({ message: graphQLErrorMessage });
+ },
+ );
+
+ it.each`
+ actionName | event
+ ${'add'} | ${'add-variable'}
+ ${'update'} | ${'update-variable'}
+ ${'delete'} | ${'delete-variable'}
+ `(
+ 'throws generic error when the mutation fails with no graphql errors and user performs $actionName variable',
+ async ({ event }) => {
+ jest.spyOn(wrapper.vm.$apollo, 'mutate').mockImplementationOnce(() => {
+ throw new Error();
+ });
+ await findCiSettings().vm.$emit(event, newVariable);
+
+ expect(wrapper.vm.$apollo.mutate).toHaveBeenCalled();
+ expect(createFlash).toHaveBeenCalledWith({ message: genericMutationErrorText });
+ },
+ );
+ });
+});
diff --git a/spec/frontend/ci_variable_list/components/ci_environments_dropdown_spec.js b/spec/frontend/ci_variable_list/components/ci_environments_dropdown_spec.js
new file mode 100644
index 00000000000..e9966576cab
--- /dev/null
+++ b/spec/frontend/ci_variable_list/components/ci_environments_dropdown_spec.js
@@ -0,0 +1,139 @@
+import { GlDropdown, GlDropdownItem, GlIcon, GlSearchBoxByType } from '@gitlab/ui';
+import { mount } from '@vue/test-utils';
+import { nextTick } from 'vue';
+import { allEnvironments } from '~/ci_variable_list/constants';
+import CiEnvironmentsDropdown from '~/ci_variable_list/components/ci_environments_dropdown.vue';
+
+describe('Ci environments dropdown', () => {
+ let wrapper;
+
+ const envs = ['dev', 'prod', 'staging'];
+ const defaultProps = { environments: envs, selectedEnvironmentScope: '' };
+
+ const findDropdownText = () => wrapper.findComponent(GlDropdown).text();
+ const findAllDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
+ const findDropdownItemByIndex = (index) => wrapper.findAllComponents(GlDropdownItem).at(index);
+ const findActiveIconByIndex = (index) => findDropdownItemByIndex(index).findComponent(GlIcon);
+ const findSearchBox = () => wrapper.findComponent(GlSearchBoxByType);
+
+ const createComponent = ({ props = {}, searchTerm = '' } = {}) => {
+ wrapper = mount(CiEnvironmentsDropdown, {
+ propsData: {
+ ...defaultProps,
+ ...props,
+ },
+ });
+
+ findSearchBox().vm.$emit('input', searchTerm);
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('No environments found', () => {
+ beforeEach(() => {
+ createComponent({ searchTerm: 'stable' });
+ });
+
+ it('renders create button with search term if environments do not contain search term', () => {
+ expect(findAllDropdownItems()).toHaveLength(2);
+ expect(findDropdownItemByIndex(1).text()).toBe('Create wildcard: stable');
+ });
+
+ it('renders empty results message', () => {
+ expect(findDropdownItemByIndex(0).text()).toBe('No matching results');
+ });
+ });
+
+ describe('Search term is empty', () => {
+ beforeEach(() => {
+ createComponent({ props: { environments: envs } });
+ });
+
+ it('renders all environments when search term is empty', () => {
+ expect(findAllDropdownItems()).toHaveLength(3);
+ expect(findDropdownItemByIndex(0).text()).toBe(envs[0]);
+ expect(findDropdownItemByIndex(1).text()).toBe(envs[1]);
+ expect(findDropdownItemByIndex(2).text()).toBe(envs[2]);
+ });
+
+ it('should not display active checkmark on the inactive stage', () => {
+ expect(findActiveIconByIndex(0).classes('gl-visibility-hidden')).toBe(true);
+ });
+ });
+
+ describe('when `*` is the value of selectedEnvironmentScope props', () => {
+ const wildcardScope = '*';
+
+ beforeEach(() => {
+ createComponent({ props: { selectedEnvironmentScope: wildcardScope } });
+ });
+
+ it('shows the `All environments` text and not the wildcard', () => {
+ expect(findDropdownText()).toContain(allEnvironments.text);
+ expect(findDropdownText()).not.toContain(wildcardScope);
+ });
+ });
+
+ describe('Environments found', () => {
+ const currentEnv = envs[2];
+
+ beforeEach(async () => {
+ createComponent({ searchTerm: currentEnv });
+ await nextTick();
+ });
+
+ it('renders only the environment searched for', () => {
+ expect(findAllDropdownItems()).toHaveLength(1);
+ expect(findDropdownItemByIndex(0).text()).toBe(currentEnv);
+ });
+
+ it('should not display create button', () => {
+ const environments = findAllDropdownItems().filter((env) => env.text().startsWith('Create'));
+ expect(environments).toHaveLength(0);
+ expect(findAllDropdownItems()).toHaveLength(1);
+ });
+
+ it('should not display empty results message', () => {
+ expect(wrapper.findComponent({ ref: 'noMatchingResults' }).exists()).toBe(false);
+ });
+
+ it('should clear the search term when showing the dropdown', () => {
+ wrapper.findComponent(GlDropdown).trigger('click');
+
+ expect(findSearchBox().text()).toBe('');
+ });
+
+ describe('Custom events', () => {
+ describe('when clicking on an environment', () => {
+ const itemIndex = 0;
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('should emit `select-environment` if an environment is clicked', async () => {
+ await nextTick();
+
+ await findDropdownItemByIndex(itemIndex).vm.$emit('click');
+
+ expect(wrapper.emitted('select-environment')).toEqual([[envs[itemIndex]]]);
+ });
+ });
+
+ describe('when creating a new environment from a search term', () => {
+ const search = 'new-env';
+ beforeEach(() => {
+ createComponent({ searchTerm: search });
+ });
+
+ it('should emit createClicked if an environment is clicked', async () => {
+ await nextTick();
+ findDropdownItemByIndex(1).vm.$emit('click');
+ expect(wrapper.emitted('create-environment-scope')).toEqual([[search]]);
+ });
+ });
+ });
+ });
+});
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
new file mode 100644
index 00000000000..e45656acfd8
--- /dev/null
+++ b/spec/frontend/ci_variable_list/components/ci_group_variables_spec.js
@@ -0,0 +1,183 @@
+import Vue, { nextTick } from 'vue';
+import VueApollo from 'vue-apollo';
+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 { convertToGraphQLId } from '~/graphql_shared/utils';
+
+import ciGroupVariables from '~/ci_variable_list/components/ci_group_variables.vue';
+import ciVariableSettings from '~/ci_variable_list/components/ci_variable_settings.vue';
+import ciVariableTable from '~/ci_variable_list/components/ci_variable_table.vue';
+import getGroupVariables from '~/ci_variable_list/graphql/queries/group_variables.query.graphql';
+
+import addGroupVariable from '~/ci_variable_list/graphql/mutations/group_add_variable.mutation.graphql';
+import deleteGroupVariable from '~/ci_variable_list/graphql/mutations/group_delete_variable.mutation.graphql';
+import updateGroupVariable from '~/ci_variable_list/graphql/mutations/group_update_variable.mutation.graphql';
+
+import { genericMutationErrorText, variableFetchErrorText } from '~/ci_variable_list/constants';
+
+import { mockGroupVariables, newVariable } from '../mocks';
+
+jest.mock('~/flash');
+
+Vue.use(VueApollo);
+
+const mockProvide = {
+ endpoint: '/variables',
+ groupPath: '/namespace/group',
+ groupId: 1,
+};
+
+describe('Ci Group Variable list', () => {
+ let wrapper;
+
+ let mockApollo;
+ let mockVariables;
+
+ const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
+ const findCiTable = () => wrapper.findComponent(GlTable);
+ const findCiSettings = () => wrapper.findComponent(ciVariableSettings);
+
+ // eslint-disable-next-line consistent-return
+ const createComponentWithApollo = async ({ isLoading = false } = {}) => {
+ const handlers = [[getGroupVariables, mockVariables]];
+
+ mockApollo = createMockApollo(handlers, resolvers);
+
+ wrapper = shallowMount(ciGroupVariables, {
+ provide: mockProvide,
+ apolloProvider: mockApollo,
+ stubs: { ciVariableSettings, ciVariableTable },
+ });
+
+ if (!isLoading) {
+ return waitForPromises();
+ }
+ };
+
+ beforeEach(() => {
+ mockVariables = jest.fn();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('while queries are being fetch', () => {
+ beforeEach(() => {
+ createComponentWithApollo({ isLoading: true });
+ });
+
+ it('shows a loading icon', () => {
+ expect(findLoadingIcon().exists()).toBe(true);
+ expect(findCiTable().exists()).toBe(false);
+ });
+ });
+
+ describe('when queries are resolved', () => {
+ describe('successfuly', () => {
+ beforeEach(async () => {
+ mockVariables.mockResolvedValue(mockGroupVariables);
+
+ await createComponentWithApollo();
+ });
+
+ it('passes down the expected environments as props', () => {
+ expect(findCiSettings().props('environments')).toEqual([]);
+ });
+
+ it('passes down the expected variables as props', () => {
+ expect(findCiSettings().props('variables')).toEqual(
+ mockGroupVariables.data.group.ciVariables.nodes,
+ );
+ });
+
+ it('createFlash was not called', () => {
+ expect(createFlash).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('with an error for variables', () => {
+ beforeEach(async () => {
+ mockVariables.mockRejectedValue();
+
+ await createComponentWithApollo();
+ });
+
+ it('calls createFlash with the expected error message', () => {
+ expect(createFlash).toHaveBeenCalledWith({ message: variableFetchErrorText });
+ });
+ });
+ });
+
+ describe('mutations', () => {
+ beforeEach(async () => {
+ mockVariables.mockResolvedValue(mockGroupVariables);
+
+ await createComponentWithApollo();
+ });
+ it.each`
+ actionName | mutation | event
+ ${'add'} | ${addGroupVariable} | ${'add-variable'}
+ ${'update'} | ${updateGroupVariable} | ${'update-variable'}
+ ${'delete'} | ${deleteGroupVariable} | ${'delete-variable'}
+ `(
+ 'calls the right mutation when user performs $actionName variable',
+ async ({ event, mutation }) => {
+ jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue();
+ await findCiSettings().vm.$emit(event, newVariable);
+
+ expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({
+ mutation,
+ variables: {
+ endpoint: mockProvide.endpoint,
+ fullPath: mockProvide.groupPath,
+ groupId: convertToGraphQLId('Group', mockProvide.groupId),
+ variable: newVariable,
+ },
+ });
+ },
+ );
+
+ it.each`
+ actionName | event | mutationName
+ ${'add'} | ${'add-variable'} | ${'addGroupVariable'}
+ ${'update'} | ${'update-variable'} | ${'updateGroupVariable'}
+ ${'delete'} | ${'delete-variable'} | ${'deleteGroupVariable'}
+ `(
+ 'throws with the specific graphql error if present when user performs $actionName variable',
+ async ({ event, mutationName }) => {
+ const graphQLErrorMessage = 'There is a problem with this graphQL action';
+ jest
+ .spyOn(wrapper.vm.$apollo, 'mutate')
+ .mockResolvedValue({ data: { [mutationName]: { errors: [graphQLErrorMessage] } } });
+ await findCiSettings().vm.$emit(event, newVariable);
+ await nextTick();
+
+ expect(wrapper.vm.$apollo.mutate).toHaveBeenCalled();
+ expect(createFlash).toHaveBeenCalledWith({ message: graphQLErrorMessage });
+ },
+ );
+
+ it.each`
+ actionName | event
+ ${'add'} | ${'add-variable'}
+ ${'update'} | ${'update-variable'}
+ ${'delete'} | ${'delete-variable'}
+ `(
+ 'throws generic error when the mutation fails with no graphql errors and user performs $actionName variable',
+ async ({ event }) => {
+ jest.spyOn(wrapper.vm.$apollo, 'mutate').mockImplementationOnce(() => {
+ throw new Error();
+ });
+ await findCiSettings().vm.$emit(event, newVariable);
+
+ expect(wrapper.vm.$apollo.mutate).toHaveBeenCalled();
+ expect(createFlash).toHaveBeenCalledWith({ message: genericMutationErrorText });
+ },
+ );
+ });
+});
diff --git a/spec/frontend/ci_variable_list/components/ci_variable_modal_spec.js b/spec/frontend/ci_variable_list/components/ci_variable_modal_spec.js
new file mode 100644
index 00000000000..e5019e3261e
--- /dev/null
+++ b/spec/frontend/ci_variable_list/components/ci_variable_modal_spec.js
@@ -0,0 +1,383 @@
+import { GlButton, GlFormInput } from '@gitlab/ui';
+import { mockTracking } from 'helpers/tracking_helper';
+import { shallowMountExtended, mountExtended } from 'helpers/vue_test_utils_helper';
+import CiEnvironmentsDropdown from '~/ci_variable_list/components/ci_environments_dropdown.vue';
+import CiVariableModal from '~/ci_variable_list/components/ci_variable_modal.vue';
+import {
+ ADD_VARIABLE_ACTION,
+ AWS_ACCESS_KEY_ID,
+ EDIT_VARIABLE_ACTION,
+ EVENT_LABEL,
+ EVENT_ACTION,
+ ENVIRONMENT_SCOPE_LINK_TITLE,
+ instanceString,
+} from '~/ci_variable_list/constants';
+import { mockVariablesWithScopes } from '../mocks';
+import ModalStub from '../stubs';
+
+describe('Ci variable modal', () => {
+ let wrapper;
+ let trackingSpy;
+
+ const maskableRegex = '^[a-zA-Z0-9_+=/@:.~-]{8,}$';
+ const mockVariables = mockVariablesWithScopes(instanceString);
+
+ const defaultProvide = {
+ awsLogoSvgPath: '/logo',
+ awsTipCommandsLink: '/tips',
+ awsTipDeployLink: '/deploy',
+ awsTipLearnLink: '/learn-link',
+ containsVariableReferenceLink: '/reference',
+ environmentScopeLink: '/help/environments',
+ isProtectedByDefault: false,
+ maskedEnvironmentVariablesLink: '/variables-link',
+ maskableRegex,
+ protectedEnvironmentVariablesLink: '/protected-link',
+ };
+
+ const defaultProps = {
+ areScopedVariablesAvailable: true,
+ environments: [],
+ mode: ADD_VARIABLE_ACTION,
+ selectedVariable: {},
+ variable: [],
+ };
+
+ const createComponent = ({ mountFn = shallowMountExtended, props = {}, provide = {} } = {}) => {
+ wrapper = mountFn(CiVariableModal, {
+ attachTo: document.body,
+ provide: { ...defaultProvide, ...provide },
+ propsData: {
+ ...defaultProps,
+ ...props,
+ },
+ stubs: {
+ GlModal: ModalStub,
+ },
+ });
+ };
+
+ const findCiEnvironmentsDropdown = () => wrapper.find(CiEnvironmentsDropdown);
+ const findReferenceWarning = () => wrapper.findByTestId('contains-variable-reference');
+ const findModal = () => wrapper.find(ModalStub);
+ const findAWSTip = () => wrapper.findByTestId('aws-guidance-tip');
+ const findAddorUpdateButton = () => wrapper.findByTestId('ciUpdateOrAddVariableBtn');
+ const deleteVariableButton = () =>
+ findModal()
+ .findAll(GlButton)
+ .wrappers.find((button) => button.props('variant') === 'danger');
+ const findProtectedVariableCheckbox = () =>
+ wrapper.findByTestId('ci-variable-protected-checkbox');
+ const findMaskedVariableCheckbox = () => wrapper.findByTestId('ci-variable-masked-checkbox');
+ const findValueField = () => wrapper.find('#ci-variable-value');
+ const findEnvScopeLink = () => wrapper.findByTestId('environment-scope-link');
+ const findEnvScopeInput = () => wrapper.findByTestId('environment-scope').find(GlFormInput);
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('Adding a variable', () => {
+ describe('when no key/value pair are present', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('shows the submit button as disabled ', () => {
+ expect(findAddorUpdateButton().attributes('disabled')).toBe('true');
+ });
+ });
+
+ describe('when a key/value pair is present', () => {
+ beforeEach(() => {
+ createComponent({ props: { selectedVariable: mockVariables[0] } });
+ });
+
+ it('shows the submit button as enabled ', () => {
+ expect(findAddorUpdateButton().attributes('disabled')).toBeUndefined();
+ });
+ });
+
+ describe('events', () => {
+ const [currentVariable] = mockVariables;
+
+ beforeEach(() => {
+ createComponent({ props: { selectedVariable: currentVariable } });
+ jest.spyOn(wrapper.vm, '$emit');
+ });
+
+ it('Dispatches `add-variable` action on submit', () => {
+ findAddorUpdateButton().vm.$emit('click');
+ expect(wrapper.emitted('add-variable')).toEqual([[currentVariable]]);
+ });
+
+ it('Dispatches the `hideModal` event when dismissing', () => {
+ findModal().vm.$emit('hidden');
+ expect(wrapper.emitted('hideModal')).toEqual([[]]);
+ });
+ });
+ });
+
+ describe('when protected by default', () => {
+ describe('when adding a new variable', () => {
+ beforeEach(() => {
+ createComponent({ provide: { isProtectedByDefault: true } });
+ findModal().vm.$emit('shown');
+ });
+
+ it('updates the protected value to true', () => {
+ expect(findProtectedVariableCheckbox().attributes('data-is-protected-checked')).toBe(
+ 'true',
+ );
+ });
+ });
+
+ describe('when editing a variable', () => {
+ beforeEach(() => {
+ createComponent({
+ provide: { isProtectedByDefault: false },
+ props: {
+ selectedVariable: {},
+ mode: EDIT_VARIABLE_ACTION,
+ },
+ });
+ findModal().vm.$emit('shown');
+ });
+
+ it('keeps the value as false', async () => {
+ expect(
+ findProtectedVariableCheckbox().attributes('data-is-protected-checked'),
+ ).toBeUndefined();
+ });
+ });
+ });
+
+ describe('Adding a new non-AWS variable', () => {
+ beforeEach(() => {
+ const [variable] = mockVariables;
+ createComponent({ mountFn: mountExtended, props: { selectedVariable: variable } });
+ });
+
+ it('does not show AWS guidance tip', () => {
+ const tip = findAWSTip();
+ expect(tip.exists()).toBe(true);
+ expect(tip.isVisible()).toBe(false);
+ });
+ });
+
+ describe('Adding a new AWS variable', () => {
+ beforeEach(() => {
+ const [variable] = mockVariables;
+ const AWSKeyVariable = {
+ ...variable,
+ key: AWS_ACCESS_KEY_ID,
+ value: 'AKIAIOSFODNN7EXAMPLEjdhy',
+ };
+ createComponent({ mountFn: mountExtended, props: { selectedVariable: AWSKeyVariable } });
+ });
+
+ it('shows AWS guidance tip', () => {
+ const tip = findAWSTip();
+ expect(tip.exists()).toBe(true);
+ expect(tip.isVisible()).toBe(true);
+ });
+ });
+
+ describe('Reference warning when adding a variable', () => {
+ describe('with a $ character', () => {
+ beforeEach(() => {
+ const [variable] = mockVariables;
+ const variableWithDollarSign = {
+ ...variable,
+ value: 'valueWith$',
+ };
+ createComponent({
+ mountFn: mountExtended,
+ props: { selectedVariable: variableWithDollarSign },
+ });
+ });
+
+ it(`renders the variable reference warning`, () => {
+ expect(findReferenceWarning().exists()).toBe(true);
+ });
+ });
+
+ describe('without a $ character', () => {
+ beforeEach(() => {
+ const [variable] = mockVariables;
+ createComponent({
+ mountFn: mountExtended,
+ props: { selectedVariable: variable },
+ });
+ });
+
+ it(`does not render the variable reference warning`, () => {
+ expect(findReferenceWarning().exists()).toBe(false);
+ });
+ });
+ });
+
+ describe('Editing a variable', () => {
+ const [variable] = mockVariables;
+
+ beforeEach(() => {
+ createComponent({ props: { selectedVariable: variable, mode: EDIT_VARIABLE_ACTION } });
+ jest.spyOn(wrapper.vm, '$emit');
+ });
+
+ it('button text is Update variable when updating', () => {
+ expect(findAddorUpdateButton().text()).toBe('Update variable');
+ });
+
+ it('Update variable button dispatches updateVariable with correct variable', () => {
+ findAddorUpdateButton().vm.$emit('click');
+ expect(wrapper.emitted('update-variable')).toEqual([[variable]]);
+ });
+
+ it('Propagates the `hideModal` event', () => {
+ findModal().vm.$emit('hidden');
+ expect(wrapper.emitted('hideModal')).toEqual([[]]);
+ });
+
+ it('dispatches `delete-variable` with correct variable to delete', () => {
+ deleteVariableButton().vm.$emit('click');
+ expect(wrapper.emitted('delete-variable')).toEqual([[variable]]);
+ });
+ });
+
+ describe('Environment scope', () => {
+ describe('when feature is available', () => {
+ it('renders the environment dropdown', () => {
+ createComponent({
+ mountFn: mountExtended,
+ props: {
+ areScopedVariablesAvailable: true,
+ },
+ });
+
+ expect(findCiEnvironmentsDropdown().exists()).toBe(true);
+ expect(findCiEnvironmentsDropdown().isVisible()).toBe(true);
+ });
+
+ it('renders a link to documentation on scopes', () => {
+ createComponent({ mountFn: mountExtended });
+
+ const link = findEnvScopeLink();
+
+ expect(link.attributes('title')).toBe(ENVIRONMENT_SCOPE_LINK_TITLE);
+ expect(link.attributes('href')).toBe(defaultProvide.environmentScopeLink);
+ });
+ });
+
+ describe('when feature is not available', () => {
+ it('disables the dropdown', () => {
+ createComponent({
+ mountFn: mountExtended,
+ props: {
+ areScopedVariablesAvailable: false,
+ },
+ });
+
+ expect(findCiEnvironmentsDropdown().exists()).toBe(false);
+ expect(findEnvScopeInput().attributes('readonly')).toBe('readonly');
+ });
+ });
+ });
+
+ describe('Validations', () => {
+ const maskError = 'This variable can not be masked.';
+
+ describe('when the mask state is invalid', () => {
+ beforeEach(async () => {
+ const [variable] = mockVariables;
+ const invalidMaskVariable = {
+ ...variable,
+ value: 'd:;',
+ masked: false,
+ };
+ createComponent({
+ mountFn: mountExtended,
+ props: { selectedVariable: invalidMaskVariable },
+ });
+ trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
+ await findMaskedVariableCheckbox().trigger('click');
+ });
+
+ it('disables the submit button', () => {
+ expect(findAddorUpdateButton().attributes('disabled')).toBe('disabled');
+ });
+
+ it('shows the correct error text', () => {
+ expect(findModal().text()).toContain(maskError);
+ });
+
+ it('sends the correct tracking event', () => {
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, EVENT_ACTION, {
+ label: EVENT_LABEL,
+ property: ';',
+ });
+ });
+ });
+
+ describe.each`
+ value | masked | eventSent | trackingErrorProperty
+ ${'secretValue'} | ${false} | ${0} | ${null}
+ ${'short'} | ${true} | ${0} | ${null}
+ ${'dollar$ign'} | ${false} | ${1} | ${'$'}
+ ${'dollar$ign'} | ${true} | ${1} | ${'$'}
+ ${'unsupported|char'} | ${true} | ${1} | ${'|'}
+ ${'unsupported|char'} | ${false} | ${0} | ${null}
+ `('Adding a new variable', ({ value, masked, eventSent, trackingErrorProperty }) => {
+ beforeEach(async () => {
+ const [variable] = mockVariables;
+ const invalidKeyVariable = {
+ ...variable,
+ value: '',
+ masked: false,
+ };
+ createComponent({
+ mountFn: mountExtended,
+ props: { selectedVariable: invalidKeyVariable },
+ });
+ trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
+ await findValueField().vm.$emit('input', value);
+ if (masked) {
+ await findMaskedVariableCheckbox().trigger('click');
+ }
+ });
+
+ it(`${
+ eventSent > 0 ? 'sends the correct' : 'does not send the'
+ } variable validation tracking event with ${value}`, () => {
+ expect(trackingSpy).toHaveBeenCalledTimes(eventSent);
+
+ if (eventSent > 0) {
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, EVENT_ACTION, {
+ label: EVENT_LABEL,
+ property: trackingErrorProperty,
+ });
+ }
+ });
+ });
+
+ describe('when masked variable has acceptable value', () => {
+ beforeEach(() => {
+ const [variable] = mockVariables;
+ const validMaskandKeyVariable = {
+ ...variable,
+ key: AWS_ACCESS_KEY_ID,
+ value: '12345678',
+ masked: true,
+ };
+ createComponent({
+ mountFn: mountExtended,
+ props: { selectedVariable: validMaskandKeyVariable },
+ });
+ });
+
+ it('does not disable the submit button', () => {
+ expect(findAddorUpdateButton().attributes('disabled')).toBeUndefined();
+ });
+ });
+ });
+});
diff --git a/spec/frontend/ci_variable_list/components/ci_variable_settings_spec.js b/spec/frontend/ci_variable_list/components/ci_variable_settings_spec.js
new file mode 100644
index 00000000000..5c77ce71b41
--- /dev/null
+++ b/spec/frontend/ci_variable_list/components/ci_variable_settings_spec.js
@@ -0,0 +1,128 @@
+import { nextTick } from 'vue';
+import { shallowMount } from '@vue/test-utils';
+import CiVariableSettings from '~/ci_variable_list/components/ci_variable_settings.vue';
+import ciVariableModal from '~/ci_variable_list/components/ci_variable_modal.vue';
+import ciVariableTable from '~/ci_variable_list/components/ci_variable_table.vue';
+import {
+ ADD_VARIABLE_ACTION,
+ EDIT_VARIABLE_ACTION,
+ projectString,
+} from '~/ci_variable_list/constants';
+import { mapEnvironmentNames } from '~/ci_variable_list/utils';
+
+import { mockEnvs, mockVariablesWithScopes, newVariable } from '../mocks';
+
+describe('Ci variable table', () => {
+ let wrapper;
+
+ const defaultProps = {
+ areScopedVariablesAvailable: true,
+ environments: mapEnvironmentNames(mockEnvs),
+ isLoading: false,
+ variables: mockVariablesWithScopes(projectString),
+ };
+
+ const findCiVariableTable = () => wrapper.findComponent(ciVariableTable);
+ const findCiVariableModal = () => wrapper.findComponent(ciVariableModal);
+
+ const createComponent = () => {
+ wrapper = shallowMount(CiVariableSettings, {
+ propsData: {
+ ...defaultProps,
+ },
+ });
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('props passing', () => {
+ it('passes props down correctly to the ci table', () => {
+ expect(findCiVariableTable().props()).toEqual({
+ isLoading: defaultProps.isLoading,
+ variables: defaultProps.variables,
+ });
+ });
+
+ it('passes props down correctly to the ci modal', async () => {
+ findCiVariableTable().vm.$emit('set-selected-variable');
+ await nextTick();
+
+ expect(findCiVariableModal().props()).toEqual({
+ areScopedVariablesAvailable: defaultProps.areScopedVariablesAvailable,
+ environments: defaultProps.environments,
+ variables: defaultProps.variables,
+ mode: ADD_VARIABLE_ACTION,
+ selectedVariable: {},
+ });
+ });
+ });
+
+ describe('modal mode', () => {
+ it('passes down ADD mode when receiving an empty variable', async () => {
+ findCiVariableTable().vm.$emit('set-selected-variable');
+ await nextTick();
+
+ expect(findCiVariableModal().props('mode')).toBe(ADD_VARIABLE_ACTION);
+ });
+
+ it('passes down EDIT mode when receiving a variable', async () => {
+ findCiVariableTable().vm.$emit('set-selected-variable', newVariable);
+ await nextTick();
+
+ expect(findCiVariableModal().props('mode')).toBe(EDIT_VARIABLE_ACTION);
+ });
+ });
+
+ describe('variable modal', () => {
+ it('is hidden by default', () => {
+ expect(findCiVariableModal().exists()).toBe(false);
+ });
+
+ it('shows modal when adding a new variable', async () => {
+ findCiVariableTable().vm.$emit('set-selected-variable');
+ await nextTick();
+
+ expect(findCiVariableModal().exists()).toBe(true);
+ });
+
+ it('shows modal when updating a variable', async () => {
+ findCiVariableTable().vm.$emit('set-selected-variable', newVariable);
+ await nextTick();
+
+ expect(findCiVariableModal().exists()).toBe(true);
+ });
+
+ it('hides modal when receiving the event from the modal', async () => {
+ findCiVariableTable().vm.$emit('set-selected-variable');
+ await nextTick();
+
+ findCiVariableModal().vm.$emit('hideModal');
+ await nextTick();
+
+ expect(findCiVariableModal().exists()).toBe(false);
+ });
+ });
+
+ describe('variable events', () => {
+ it.each`
+ eventName
+ ${'add-variable'}
+ ${'update-variable'}
+ ${'delete-variable'}
+ `('bubbles up the $eventName event', async ({ eventName }) => {
+ findCiVariableTable().vm.$emit('set-selected-variable');
+ await nextTick();
+
+ findCiVariableModal().vm.$emit(eventName, newVariable);
+ await nextTick();
+
+ expect(wrapper.emitted(eventName)).toEqual([[newVariable]]);
+ });
+ });
+});
diff --git a/spec/frontend/ci_variable_list/components/ci_variable_table_spec.js b/spec/frontend/ci_variable_list/components/ci_variable_table_spec.js
new file mode 100644
index 00000000000..8a4c35173ec
--- /dev/null
+++ b/spec/frontend/ci_variable_list/components/ci_variable_table_spec.js
@@ -0,0 +1,98 @@
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import CiVariableTable from '~/ci_variable_list/components/ci_variable_table.vue';
+import { projectString } from '~/ci_variable_list/constants';
+import { mockVariables } from '../mocks';
+
+describe('Ci variable table', () => {
+ let wrapper;
+
+ const defaultProps = {
+ isLoading: false,
+ variables: mockVariables(projectString),
+ };
+
+ const createComponent = ({ props = {} } = {}) => {
+ wrapper = mountExtended(CiVariableTable, {
+ attachTo: document.body,
+ propsData: {
+ ...defaultProps,
+ ...props,
+ },
+ });
+ };
+
+ const findRevealButton = () => wrapper.findByText('Reveal values');
+ const findAddButton = () => wrapper.findByLabelText('Add');
+ const findEditButton = () => wrapper.findByLabelText('Edit');
+ const findEmptyVariablesPlaceholder = () => wrapper.findByText('There are no variables yet.');
+ const findHiddenValues = () => wrapper.findAll('[data-testid="hiddenValue"]');
+ const findRevealedValues = () => wrapper.findAll('[data-testid="revealedValue"]');
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('When table is empty', () => {
+ beforeEach(() => {
+ createComponent({ props: { variables: [] } });
+ });
+
+ it('displays empty message', () => {
+ expect(findEmptyVariablesPlaceholder().exists()).toBe(true);
+ });
+
+ it('hides the reveal button', () => {
+ expect(findRevealButton().exists()).toBe(false);
+ });
+ });
+
+ describe('When table has variables', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('does not display the empty message', () => {
+ expect(findEmptyVariablesPlaceholder().exists()).toBe(false);
+ });
+
+ it('displays the reveal button', () => {
+ expect(findRevealButton().exists()).toBe(true);
+ });
+
+ it('displays the correct amount of variables', async () => {
+ expect(wrapper.findAll('.js-ci-variable-row')).toHaveLength(defaultProps.variables.length);
+ });
+ });
+
+ describe('Table click actions', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('reveals secret values when button is clicked', async () => {
+ expect(findHiddenValues()).toHaveLength(defaultProps.variables.length);
+ expect(findRevealedValues()).toHaveLength(0);
+
+ await findRevealButton().trigger('click');
+
+ expect(findHiddenValues()).toHaveLength(0);
+ expect(findRevealedValues()).toHaveLength(defaultProps.variables.length);
+ });
+
+ it('dispatches `setSelectedVariable` with correct variable to edit', async () => {
+ await findEditButton().trigger('click');
+
+ expect(wrapper.emitted('set-selected-variable')).toEqual([[defaultProps.variables[0]]]);
+ });
+
+ it('dispatches `setSelectedVariable` with no variable when adding a new one', async () => {
+ await findAddButton().trigger('click');
+
+ expect(wrapper.emitted('set-selected-variable')).toEqual([[null]]);
+ });
+ });
+});
diff --git a/spec/frontend/ci_variable_list/components/legacy_ci_variable_modal_spec.js b/spec/frontend/ci_variable_list/components/legacy_ci_variable_modal_spec.js
index 42c6501dcce..6681ab91a4a 100644
--- a/spec/frontend/ci_variable_list/components/legacy_ci_variable_modal_spec.js
+++ b/spec/frontend/ci_variable_list/components/legacy_ci_variable_modal_spec.js
@@ -58,7 +58,7 @@ describe('Ci variable modal', () => {
});
it('button is disabled when no key/value pair are present', () => {
- expect(findAddorUpdateButton().attributes('disabled')).toBeTruthy();
+ expect(findAddorUpdateButton().attributes('disabled')).toBe('true');
});
});
@@ -71,7 +71,7 @@ describe('Ci variable modal', () => {
});
it('button is enabled when key/value pair are present', () => {
- expect(findAddorUpdateButton().attributes('disabled')).toBeFalsy();
+ expect(findAddorUpdateButton().attributes('disabled')).toBeUndefined();
});
it('Add variable button dispatches addVariable action', () => {
@@ -249,7 +249,7 @@ describe('Ci variable modal', () => {
});
it('disables the submit button', () => {
- expect(findAddorUpdateButton().attributes('disabled')).toBeTruthy();
+ expect(findAddorUpdateButton().attributes('disabled')).toBe('disabled');
});
it('shows the correct error text', () => {
@@ -316,7 +316,7 @@ describe('Ci variable modal', () => {
});
it('does not disable the submit button', () => {
- expect(findAddorUpdateButton().attributes('disabled')).toBeFalsy();
+ expect(findAddorUpdateButton().attributes('disabled')).toBeUndefined();
});
});
});
diff --git a/spec/frontend/ci_variable_list/mocks.js b/spec/frontend/ci_variable_list/mocks.js
new file mode 100644
index 00000000000..89ba77858dc
--- /dev/null
+++ b/spec/frontend/ci_variable_list/mocks.js
@@ -0,0 +1,109 @@
+import { variableTypes, groupString, instanceString } from '~/ci_variable_list/constants';
+
+export const devName = 'dev';
+export const prodName = 'prod';
+
+export const mockVariables = (kind) => {
+ return [
+ {
+ __typename: `Ci${kind}Variable`,
+ id: 1,
+ key: 'my-var',
+ masked: false,
+ protected: true,
+ value: 'env_val',
+ variableType: variableTypes.variableType,
+ },
+ {
+ __typename: `Ci${kind}Variable`,
+ id: 2,
+ key: 'secret',
+ masked: true,
+ protected: false,
+ value: 'the_secret_value',
+ variableType: variableTypes.fileType,
+ },
+ ];
+};
+
+export const mockVariablesWithScopes = (kind) =>
+ mockVariables(kind).map((variable) => {
+ return { ...variable, environmentScope: '*' };
+ });
+
+const createDefaultVars = ({ withScope = true, kind } = {}) => {
+ let base = mockVariables(kind);
+
+ if (withScope) {
+ base = mockVariablesWithScopes(kind);
+ }
+
+ return {
+ __typename: `Ci${kind}VariableConnection`,
+ nodes: base,
+ };
+};
+
+const defaultEnvs = {
+ __typename: 'EnvironmentConnection',
+ nodes: [
+ {
+ __typename: 'Environment',
+ id: 1,
+ name: prodName,
+ },
+ {
+ __typename: 'Environment',
+ id: 2,
+ name: devName,
+ },
+ ],
+};
+
+export const mockEnvs = defaultEnvs.nodes;
+
+export const mockProjectEnvironments = {
+ data: {
+ project: {
+ __typename: 'Project',
+ id: 1,
+ environments: defaultEnvs,
+ },
+ },
+};
+
+export const mockProjectVariables = {
+ data: {
+ project: {
+ __typename: 'Project',
+ id: 1,
+ ciVariables: createDefaultVars(),
+ },
+ },
+};
+
+export const mockGroupVariables = {
+ data: {
+ group: {
+ __typename: 'Group',
+ id: 1,
+ ciVariables: createDefaultVars({ kind: groupString }),
+ },
+ },
+};
+
+export const mockAdminVariables = {
+ data: {
+ ciVariables: createDefaultVars({ withScope: false, kind: instanceString }),
+ },
+};
+
+export const newVariable = {
+ id: 3,
+ environmentScope: 'new',
+ key: 'AWS_RANDOM_THING',
+ masked: true,
+ protected: false,
+ value: 'devops',
+ variableType: variableTypes.variableType,
+};
diff --git a/spec/frontend/ci_variable_list/utils_spec.js b/spec/frontend/ci_variable_list/utils_spec.js
new file mode 100644
index 00000000000..081c399792f
--- /dev/null
+++ b/spec/frontend/ci_variable_list/utils_spec.js
@@ -0,0 +1,78 @@
+import {
+ createJoinedEnvironments,
+ convertEnvironmentScope,
+ mapEnvironmentNames,
+} from '~/ci_variable_list/utils';
+import { allEnvironments } from '~/ci_variable_list/constants';
+
+describe('utils', () => {
+ const environments = ['dev', 'prod'];
+ const newEnvironments = ['staging'];
+
+ describe('createJoinedEnvironments', () => {
+ it('returns only `environments` if `variables` argument is undefined', () => {
+ const variables = undefined;
+
+ expect(createJoinedEnvironments(variables, environments, [])).toEqual(environments);
+ });
+
+ it('returns a list of environments and environment scopes taken from variables in alphabetical order', () => {
+ const envScope1 = 'new1';
+ const envScope2 = 'new2';
+
+ const variables = [{ environmentScope: envScope1 }, { environmentScope: envScope2 }];
+
+ expect(createJoinedEnvironments(variables, environments, [])).toEqual([
+ environments[0],
+ envScope1,
+ envScope2,
+ environments[1],
+ ]);
+ });
+
+ it('returns combined list with new environments included', () => {
+ const variables = undefined;
+
+ expect(createJoinedEnvironments(variables, environments, newEnvironments)).toEqual([
+ ...environments,
+ ...newEnvironments,
+ ]);
+ });
+
+ it('removes duplicate environments', () => {
+ const envScope1 = environments[0];
+ const envScope2 = 'new2';
+
+ const variables = [{ environmentScope: envScope1 }, { environmentScope: envScope2 }];
+
+ expect(createJoinedEnvironments(variables, environments, [])).toEqual([
+ environments[0],
+ envScope2,
+ environments[1],
+ ]);
+ });
+ });
+
+ describe('convertEnvironmentScope', () => {
+ it('converts the * to the `All environments` text', () => {
+ expect(convertEnvironmentScope('*')).toBe(allEnvironments.text);
+ });
+
+ it('returns the environment as is if not the *', () => {
+ expect(convertEnvironmentScope('prod')).toBe('prod');
+ });
+ });
+
+ describe('mapEnvironmentNames', () => {
+ const envName = 'dev';
+ const envName2 = 'prod';
+
+ const nodes = [
+ { name: envName, otherProp: {} },
+ { name: envName2, otherProp: {} },
+ ];
+ it('flatten a nodes array with only their names', () => {
+ expect(mapEnvironmentNames(nodes)).toEqual([envName, envName2]);
+ });
+ });
+});
diff --git a/spec/frontend/clusters/agents/components/activity_history_item_spec.js b/spec/frontend/clusters/agents/components/activity_history_item_spec.js
index 100a280d0cc..68f6f11aa8f 100644
--- a/spec/frontend/clusters/agents/components/activity_history_item_spec.js
+++ b/spec/frontend/clusters/agents/components/activity_history_item_spec.js
@@ -23,7 +23,7 @@ describe('ActivityHistoryItem', () => {
};
const findHistoryItem = () => wrapper.findComponent(HistoryItem);
- const findTimeAgo = () => wrapper.find(TimeAgoTooltip);
+ const findTimeAgo = () => wrapper.findComponent(TimeAgoTooltip);
afterEach(() => {
wrapper.destroy();
diff --git a/spec/frontend/clusters/agents/components/create_token_modal_spec.js b/spec/frontend/clusters/agents/components/create_token_modal_spec.js
index ad48afe10b6..0d10801e80e 100644
--- a/spec/frontend/clusters/agents/components/create_token_modal_spec.js
+++ b/spec/frontend/clusters/agents/components/create_token_modal_spec.js
@@ -55,7 +55,7 @@ describe('CreateTokenModal', () => {
const findAgentInstructions = () => findModal().findComponent(AgentToken);
const findButtonByVariant = (variant) =>
findModal()
- .findAll(GlButton)
+ .findAllComponents(GlButton)
.wrappers.find((button) => button.props('variant') === variant);
const findActionButton = () => findButtonByVariant('confirm');
const findCancelButton = () => wrapper.findByTestId('agent-token-close-button');
diff --git a/spec/frontend/clusters/agents/components/token_table_spec.js b/spec/frontend/clusters/agents/components/token_table_spec.js
index 6caeaf5c192..334615f1818 100644
--- a/spec/frontend/clusters/agents/components/token_table_spec.js
+++ b/spec/frontend/clusters/agents/components/token_table_spec.js
@@ -136,8 +136,8 @@ describe('ClusterAgentTokenTable', () => {
const token = tokens.at(lineNumber);
expect(token.text()).toContain(description);
- expect(token.find(GlTruncate).exists()).toBe(truncatesText);
- expect(token.find(GlTooltip).exists()).toBe(hasTooltip);
+ expect(token.findComponent(GlTruncate).exists()).toBe(truncatesText);
+ expect(token.findComponent(GlTooltip).exists()).toBe(hasTooltip);
},
);
diff --git a/spec/frontend/clusters/clusters_bundle_spec.js b/spec/frontend/clusters/clusters_bundle_spec.js
index b5345ea8915..ad2aa4acbaf 100644
--- a/spec/frontend/clusters/clusters_bundle_spec.js
+++ b/spec/frontend/clusters/clusters_bundle_spec.js
@@ -1,7 +1,6 @@
import MockAdapter from 'axios-mock-adapter';
import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import { useMockLocationHelper } from 'helpers/mock_window_location_helper';
-import { setTestTimeout } from 'helpers/timeout';
import Clusters from '~/clusters/clusters_bundle';
import axios from '~/lib/utils/axios_utils';
import initProjectSelectDropdown from '~/project_select';
@@ -12,8 +11,6 @@ jest.mock('~/project_select');
useMockLocationHelper();
describe('Clusters', () => {
- setTestTimeout(1000);
-
let cluster;
let mock;
@@ -60,9 +57,9 @@ describe('Clusters', () => {
it('should show the creating container', () => {
cluster.updateContainer(null, 'creating');
- expect(cluster.creatingContainer.classList.contains('hidden')).toBeFalsy();
- expect(cluster.successContainer.classList.contains('hidden')).toBeTruthy();
- expect(cluster.errorContainer.classList.contains('hidden')).toBeTruthy();
+ expect(cluster.creatingContainer.classList.contains('hidden')).toBe(false);
+ expect(cluster.successContainer.classList.contains('hidden')).toBe(true);
+ expect(cluster.errorContainer.classList.contains('hidden')).toBe(true);
expect(window.location.reload).not.toHaveBeenCalled();
});
@@ -70,9 +67,9 @@ describe('Clusters', () => {
cluster.updateContainer(null, 'creating');
cluster.updateContainer('creating', 'creating');
- expect(cluster.creatingContainer.classList.contains('hidden')).toBeFalsy();
- expect(cluster.successContainer.classList.contains('hidden')).toBeTruthy();
- expect(cluster.errorContainer.classList.contains('hidden')).toBeTruthy();
+ expect(cluster.creatingContainer.classList.contains('hidden')).toBe(false);
+ expect(cluster.successContainer.classList.contains('hidden')).toBe(true);
+ expect(cluster.errorContainer.classList.contains('hidden')).toBe(true);
expect(window.location.reload).not.toHaveBeenCalled();
});
});
@@ -83,9 +80,9 @@ describe('Clusters', () => {
cluster.updateContainer(null, 'creating');
cluster.updateContainer('creating', 'created');
- expect(cluster.creatingContainer.classList.contains('hidden')).toBeTruthy();
- expect(cluster.successContainer.classList.contains('hidden')).toBeTruthy();
- expect(cluster.errorContainer.classList.contains('hidden')).toBeTruthy();
+ expect(cluster.creatingContainer.classList.contains('hidden')).toBe(true);
+ expect(cluster.successContainer.classList.contains('hidden')).toBe(true);
+ expect(cluster.errorContainer.classList.contains('hidden')).toBe(true);
expect(window.location.reload).toHaveBeenCalled();
expect(cluster.setClusterNewlyCreated).toHaveBeenCalledWith(true);
});
@@ -97,9 +94,9 @@ describe('Clusters', () => {
cluster.updateContainer(null, 'created');
cluster.updateContainer('created', 'created');
- expect(cluster.creatingContainer.classList.contains('hidden')).toBeTruthy();
- expect(cluster.successContainer.classList.contains('hidden')).toBeFalsy();
- expect(cluster.errorContainer.classList.contains('hidden')).toBeTruthy();
+ expect(cluster.creatingContainer.classList.contains('hidden')).toBe(true);
+ expect(cluster.successContainer.classList.contains('hidden')).toBe(false);
+ expect(cluster.errorContainer.classList.contains('hidden')).toBe(true);
expect(window.location.reload).not.toHaveBeenCalled();
expect(cluster.setClusterNewlyCreated).toHaveBeenCalledWith(false);
});
@@ -111,9 +108,9 @@ describe('Clusters', () => {
cluster.updateContainer(null, 'created');
cluster.updateContainer('created', 'created');
- expect(cluster.creatingContainer.classList.contains('hidden')).toBeTruthy();
- expect(cluster.successContainer.classList.contains('hidden')).toBeTruthy();
- expect(cluster.errorContainer.classList.contains('hidden')).toBeTruthy();
+ expect(cluster.creatingContainer.classList.contains('hidden')).toBe(true);
+ expect(cluster.successContainer.classList.contains('hidden')).toBe(true);
+ expect(cluster.errorContainer.classList.contains('hidden')).toBe(true);
expect(window.location.reload).not.toHaveBeenCalled();
expect(cluster.setClusterNewlyCreated).not.toHaveBeenCalled();
});
@@ -123,11 +120,11 @@ describe('Clusters', () => {
it('should show the error container', () => {
cluster.updateContainer(null, 'errored', 'this is an error');
- expect(cluster.creatingContainer.classList.contains('hidden')).toBeTruthy();
+ expect(cluster.creatingContainer.classList.contains('hidden')).toBe(true);
- expect(cluster.successContainer.classList.contains('hidden')).toBeTruthy();
+ expect(cluster.successContainer.classList.contains('hidden')).toBe(true);
- expect(cluster.errorContainer.classList.contains('hidden')).toBeFalsy();
+ expect(cluster.errorContainer.classList.contains('hidden')).toBe(false);
expect(cluster.errorReasonContainer.textContent).toContain('this is an error');
});
@@ -135,11 +132,11 @@ describe('Clusters', () => {
it('should show `error` banner when previously `creating`', () => {
cluster.updateContainer('creating', 'errored');
- expect(cluster.creatingContainer.classList.contains('hidden')).toBeTruthy();
+ expect(cluster.creatingContainer.classList.contains('hidden')).toBe(true);
- expect(cluster.successContainer.classList.contains('hidden')).toBeTruthy();
+ expect(cluster.successContainer.classList.contains('hidden')).toBe(true);
- expect(cluster.errorContainer.classList.contains('hidden')).toBeFalsy();
+ expect(cluster.errorContainer.classList.contains('hidden')).toBe(false);
});
});
diff --git a/spec/frontend/clusters/components/new_cluster_spec.js b/spec/frontend/clusters/components/new_cluster_spec.js
index f9df70b9f87..ef39c90aaef 100644
--- a/spec/frontend/clusters/components/new_cluster_spec.js
+++ b/spec/frontend/clusters/components/new_cluster_spec.js
@@ -12,9 +12,9 @@ describe('NewCluster', () => {
await nextTick();
};
- const findDescription = () => wrapper.find(GlSprintf);
+ const findDescription = () => wrapper.findComponent(GlSprintf);
- const findLink = () => wrapper.find(GlLink);
+ const findLink = () => wrapper.findComponent(GlLink);
beforeEach(() => {
return createWrapper();
diff --git a/spec/frontend/clusters/forms/components/integration_form_spec.js b/spec/frontend/clusters/forms/components/integration_form_spec.js
index 67d442bfdc5..b17886a5826 100644
--- a/spec/frontend/clusters/forms/components/integration_form_spec.js
+++ b/spec/frontend/clusters/forms/components/integration_form_spec.js
@@ -32,8 +32,8 @@ describe('ClusterIntegrationForm', () => {
wrapper = null;
};
- const findSubmitButton = () => wrapper.find(GlButton);
- const findGlToggle = () => wrapper.find(GlToggle);
+ const findSubmitButton = () => wrapper.findComponent(GlButton);
+ const findGlToggle = () => wrapper.findComponent(GlToggle);
afterEach(() => {
destroyWrapper();
diff --git a/spec/frontend/clusters_list/components/install_agent_modal_spec.js b/spec/frontend/clusters_list/components/install_agent_modal_spec.js
index 29884675b24..964dd005a27 100644
--- a/spec/frontend/clusters_list/components/install_agent_modal_spec.js
+++ b/spec/frontend/clusters_list/components/install_agent_modal_spec.js
@@ -150,7 +150,6 @@ describe('InstallAgentModal', () => {
});
it("doesn't render agent installation instructions", () => {
- expect(findModal().text()).not.toContain(i18n.basicInstallTitle);
expect(findModal().findComponent(GlFormInputGroup).exists()).toBe(false);
expect(findModal().findComponent(GlAlert).exists()).toBe(false);
});
diff --git a/spec/frontend/commit/pipelines/pipelines_table_spec.js b/spec/frontend/commit/pipelines/pipelines_table_spec.js
index 9b01af1e585..71ee12cf02d 100644
--- a/spec/frontend/commit/pipelines/pipelines_table_spec.js
+++ b/spec/frontend/commit/pipelines/pipelines_table_spec.js
@@ -1,4 +1,4 @@
-import { GlEmptyState, GlLoadingIcon, GlModal, GlTableLite } from '@gitlab/ui';
+import { GlLoadingIcon, GlModal, GlTableLite } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import { nextTick } from 'vue';
@@ -8,7 +8,7 @@ import waitForPromises from 'helpers/wait_for_promises';
import Api from '~/api';
import PipelinesTable from '~/commit/pipelines/pipelines_table.vue';
import httpStatusCodes from '~/lib/utils/http_status';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { TOAST_MESSAGE } from '~/pipelines/constants';
import axios from '~/lib/utils/axios_utils';
@@ -26,10 +26,12 @@ describe('Pipelines table in Commits and Merge requests', () => {
const findRunPipelineBtn = () => wrapper.findByTestId('run_pipeline_button');
const findRunPipelineBtnMobile = () => wrapper.findByTestId('run_pipeline_button_mobile');
const findLoadingState = () => wrapper.findComponent(GlLoadingIcon);
- const findEmptyState = () => wrapper.findComponent(GlEmptyState);
+ const findErrorEmptyState = () => wrapper.findByTestId('pipeline-error-empty-state');
+ const findEmptyState = () => wrapper.findByTestId('pipeline-empty-state');
const findTable = () => wrapper.findComponent(GlTableLite);
const findTableRows = () => wrapper.findAllByTestId('pipeline-table-row');
const findModal = () => wrapper.findComponent(GlModal);
+ const findMrPipelinesDocsLink = () => wrapper.findByTestId('mr-pipelines-docs-link');
const createComponent = (props = {}) => {
wrapper = extendedWrapper(
@@ -73,7 +75,18 @@ describe('Pipelines table in Commits and Merge requests', () => {
it('should render the empty state', () => {
expect(findTableRows()).toHaveLength(0);
expect(findLoadingState().exists()).toBe(false);
- expect(findEmptyState().exists()).toBe(false);
+ expect(findErrorEmptyState().exists()).toBe(false);
+ expect(findEmptyState().exists()).toBe(true);
+ });
+
+ it('should render correct empty state content', () => {
+ expect(findRunPipelineBtn().exists()).toBe(true);
+ expect(findMrPipelinesDocsLink().attributes('href')).toBe(
+ '/help/ci/pipelines/merge_request_pipelines.md#prerequisites',
+ );
+ expect(findEmptyState().text()).toContain(
+ 'To run a merge request pipeline, the jobs in the CI/CD configuration file must be configured to run in merge request pipelines.',
+ );
});
});
@@ -90,7 +103,7 @@ describe('Pipelines table in Commits and Merge requests', () => {
expect(findTable().exists()).toBe(true);
expect(findTableRows()).toHaveLength(1);
expect(findLoadingState().exists()).toBe(false);
- expect(findEmptyState().exists()).toBe(false);
+ expect(findErrorEmptyState().exists()).toBe(false);
});
describe('with pagination', () => {
@@ -226,12 +239,14 @@ describe('Pipelines table in Commits and Merge requests', () => {
describe('failure', () => {
const permissionsMsg = 'You do not have permission to run a pipeline on this branch.';
+ const defaultMsg =
+ 'An error occurred while trying to run a new pipeline for this merge request.';
it.each`
status | message
- ${httpStatusCodes.BAD_REQUEST} | ${permissionsMsg}
+ ${httpStatusCodes.BAD_REQUEST} | ${defaultMsg}
${httpStatusCodes.UNAUTHORIZED} | ${permissionsMsg}
- ${httpStatusCodes.INTERNAL_SERVER_ERROR} | ${'An error occurred while trying to run a new pipeline for this merge request.'}
+ ${httpStatusCodes.INTERNAL_SERVER_ERROR} | ${defaultMsg}
`('displays permissions error message', async ({ status, message }) => {
const response = { response: { status } };
@@ -243,7 +258,13 @@ describe('Pipelines table in Commits and Merge requests', () => {
await waitForPromises();
- expect(createFlash).toHaveBeenCalledWith({ message });
+ expect(createAlert).toHaveBeenCalledWith({
+ message,
+ primaryButton: {
+ text: 'Learn more',
+ link: '/help/ci/pipelines/merge_request_pipelines.md',
+ },
+ });
});
});
});
@@ -293,7 +314,7 @@ describe('Pipelines table in Commits and Merge requests', () => {
});
it('should render error state', () => {
- expect(findEmptyState().text()).toBe(
+ expect(findErrorEmptyState().text()).toBe(
'There was an error fetching the pipelines. Try again in a few moments or contact your support team.',
);
});
diff --git a/spec/frontend/content_editor/components/bubble_menus/link_spec.js b/spec/frontend/content_editor/components/bubble_menus/link_spec.js
index ba6d8da9584..93204deb68c 100644
--- a/spec/frontend/content_editor/components/bubble_menus/link_spec.js
+++ b/spec/frontend/content_editor/components/bubble_menus/link_spec.js
@@ -182,7 +182,7 @@ describe('content_editor/components/bubble_menus/link', () => {
it('updates prosemirror doc with new link', async () => {
expect(tiptapEditor.getHTML()).toBe(
- '<p>Download <a target="_blank" rel="noopener noreferrer nofollow" href="https://google.com" title="Search Google" canonicalsrc="https://google.com">PDF File</a></p>',
+ '<p>Download <a target="_blank" rel="noopener noreferrer nofollow" href="https://google.com" title="Search Google">PDF File</a></p>',
);
});
diff --git a/spec/frontend/content_editor/components/bubble_menus/media_spec.js b/spec/frontend/content_editor/components/bubble_menus/media_spec.js
index 8839caea80e..fada4f06743 100644
--- a/spec/frontend/content_editor/components/bubble_menus/media_spec.js
+++ b/spec/frontend/content_editor/components/bubble_menus/media_spec.js
@@ -14,7 +14,7 @@ import {
} from '../../test_constants';
const TIPTAP_IMAGE_HTML = `<p>
- <img src="https://gitlab.com/favicon.png" alt="gitlab favicon" title="gitlab favicon" data-canonical-src="https://gitlab.com/favicon.png">
+ <img src="https://gitlab.com/favicon.png" alt="gitlab favicon" title="gitlab favicon">
</p>`;
const TIPTAP_AUDIO_HTML = `<p>
diff --git a/spec/frontend/content_editor/components/content_editor_spec.js b/spec/frontend/content_editor/components/content_editor_spec.js
index 9ee3b017831..0ba2672100b 100644
--- a/spec/frontend/content_editor/components/content_editor_spec.js
+++ b/spec/frontend/content_editor/components/content_editor_spec.js
@@ -19,6 +19,7 @@ describe('ContentEditor', () => {
const findEditorElement = () => wrapper.findByTestId('content-editor');
const findEditorContent = () => wrapper.findComponent(EditorContent);
+ const findEditorStateObserver = () => wrapper.findComponent(EditorStateObserver);
const createWrapper = (propsData = {}) => {
renderMarkdown = jest.fn();
@@ -119,4 +120,17 @@ describe('ContentEditor', () => {
expect(wrapper.findComponent(FormattingBubbleMenu).exists()).toBe(true);
});
+
+ it.each`
+ event
+ ${'loading'}
+ ${'loadingSuccess'}
+ ${'loadingError'}
+ `('broadcasts $event event triggered by editor-state-observer component', ({ event }) => {
+ createWrapper();
+
+ findEditorStateObserver().vm.$emit(event);
+
+ expect(wrapper.emitted(event)).toHaveLength(1);
+ });
});
diff --git a/spec/frontend/content_editor/components/toolbar_more_dropdown_spec.js b/spec/frontend/content_editor/components/toolbar_more_dropdown_spec.js
index 351fd967719..62fec8d4e72 100644
--- a/spec/frontend/content_editor/components/toolbar_more_dropdown_spec.js
+++ b/spec/frontend/content_editor/components/toolbar_more_dropdown_spec.js
@@ -37,16 +37,17 @@ describe('content_editor/components/toolbar_more_dropdown', () => {
});
describe.each`
- name | contentType | command | params
- ${'Code block'} | ${'codeBlock'} | ${'setNode'} | ${['codeBlock']}
- ${'Details block'} | ${'details'} | ${'toggleList'} | ${['details', 'detailsContent']}
- ${'Bullet list'} | ${'bulletList'} | ${'toggleList'} | ${['bulletList', 'listItem']}
- ${'Ordered list'} | ${'orderedList'} | ${'toggleList'} | ${['orderedList', 'listItem']}
- ${'Task list'} | ${'taskList'} | ${'toggleList'} | ${['taskList', 'taskItem']}
- ${'Mermaid diagram'} | ${'diagram'} | ${'setNode'} | ${['diagram', { language: 'mermaid' }]}
- ${'PlantUML diagram'} | ${'diagram'} | ${'setNode'} | ${['diagram', { language: 'plantuml' }]}
- ${'Horizontal rule'} | ${'horizontalRule'} | ${'setHorizontalRule'} | ${[]}
- `('when option $label is clicked', ({ name, command, contentType, params }) => {
+ name | contentType | command | params
+ ${'Code block'} | ${'codeBlock'} | ${'setNode'} | ${['codeBlock']}
+ ${'Details block'} | ${'details'} | ${'toggleList'} | ${['details', 'detailsContent']}
+ ${'Bullet list'} | ${'bulletList'} | ${'toggleList'} | ${['bulletList', 'listItem']}
+ ${'Ordered list'} | ${'orderedList'} | ${'toggleList'} | ${['orderedList', 'listItem']}
+ ${'Task list'} | ${'taskList'} | ${'toggleList'} | ${['taskList', 'taskItem']}
+ ${'Mermaid diagram'} | ${'diagram'} | ${'setNode'} | ${['diagram', { language: 'mermaid' }]}
+ ${'PlantUML diagram'} | ${'diagram'} | ${'setNode'} | ${['diagram', { language: 'plantuml' }]}
+ ${'Table of contents'} | ${'tableOfContents'} | ${'insertTableOfContents'} | ${[]}
+ ${'Horizontal rule'} | ${'horizontalRule'} | ${'setHorizontalRule'} | ${[]}
+ `('when option $name is clicked', ({ name, command, contentType, params }) => {
let commands;
let btn;
diff --git a/spec/frontend/content_editor/components/top_toolbar_spec.js b/spec/frontend/content_editor/components/top_toolbar_spec.js
index 2acb6e14ce0..8f194ff32e2 100644
--- a/spec/frontend/content_editor/components/top_toolbar_spec.js
+++ b/spec/frontend/content_editor/components/top_toolbar_spec.js
@@ -32,7 +32,7 @@ describe('content_editor/components/top_toolbar', () => {
${'link'} | ${{}}
${'bullet-list'} | ${{ contentType: 'bulletList', iconName: 'list-bulleted', label: 'Add a bullet list', editorCommand: 'toggleBulletList' }}
${'ordered-list'} | ${{ contentType: 'orderedList', iconName: 'list-numbered', label: 'Add a numbered list', editorCommand: 'toggleOrderedList' }}
- ${'task-list'} | ${{ contentType: 'taskList', iconName: 'list-task', label: 'Add a task list', editorCommand: 'toggleTaskList' }}
+ ${'task-list'} | ${{ contentType: 'taskList', iconName: 'list-task', label: 'Add a checklist', editorCommand: 'toggleTaskList' }}
${'image'} | ${{}}
${'table'} | ${{}}
${'more'} | ${{}}
diff --git a/spec/frontend/content_editor/components/wrappers/__snapshots__/table_of_contents_spec.js.snap b/spec/frontend/content_editor/components/wrappers/__snapshots__/table_of_contents_spec.js.snap
new file mode 100644
index 00000000000..fb091419ad9
--- /dev/null
+++ b/spec/frontend/content_editor/components/wrappers/__snapshots__/table_of_contents_spec.js.snap
@@ -0,0 +1,115 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`content/components/wrappers/table_of_contents collects all headings and renders a nested list of headings 1`] = `
+<div
+ class="table-of-contents gl-border-1 gl-border-solid gl-border-gray-100 gl-mb-5 gl-p-4!"
+ data-testid="table-of-contents"
+>
+
+ Table of contents
+
+ <li>
+ <a
+ href="#"
+ >
+
+ Heading 1
+
+ </a>
+
+ <ul>
+ <li>
+ <a
+ href="#"
+ >
+
+ Heading 1.1
+
+ </a>
+
+ <ul>
+ <li>
+ <a
+ href="#"
+ >
+
+ Heading 1.1.1
+
+ </a>
+
+ <!---->
+ </li>
+ </ul>
+ </li>
+ <li>
+ <a
+ href="#"
+ >
+
+ Heading 1.2
+
+ </a>
+
+ <ul>
+ <li>
+ <a
+ href="#"
+ >
+
+ Heading 1.2.1
+
+ </a>
+
+ <!---->
+ </li>
+ </ul>
+ </li>
+ <li>
+ <a
+ href="#"
+ >
+
+ Heading 1.3
+
+ </a>
+
+ <!---->
+ </li>
+ <li>
+ <a
+ href="#"
+ >
+
+ Heading 1.4
+
+ </a>
+
+ <ul>
+ <li>
+ <a
+ href="#"
+ >
+
+ Heading 1.4.1
+
+ </a>
+
+ <!---->
+ </li>
+ </ul>
+ </li>
+ </ul>
+ </li>
+ <li>
+ <a
+ href="#"
+ >
+
+ Heading 2
+
+ </a>
+
+ <!---->
+ </li>
+</div>
+`;
diff --git a/spec/frontend/content_editor/components/wrappers/table_cell_base_spec.js b/spec/frontend/content_editor/components/wrappers/table_cell_base_spec.js
index 6017a145a87..1fdddce3962 100644
--- a/spec/frontend/content_editor/components/wrappers/table_cell_base_spec.js
+++ b/spec/frontend/content_editor/components/wrappers/table_cell_base_spec.js
@@ -1,12 +1,12 @@
import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
import { NodeViewWrapper } from '@tiptap/vue-2';
-import { selectedRect as getSelectedRect } from 'prosemirror-tables';
+import { selectedRect as getSelectedRect } from '@_ueberdosis/prosemirror-tables';
import { nextTick } from 'vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import TableCellBaseWrapper from '~/content_editor/components/wrappers/table_cell_base.vue';
import { createTestEditor, mockChainedCommands, emitEditorEvent } from '../../test_utils';
-jest.mock('prosemirror-tables');
+jest.mock('@_ueberdosis/prosemirror-tables');
describe('content/components/wrappers/table_cell_base', () => {
let wrapper;
diff --git a/spec/frontend/content_editor/components/wrappers/table_of_contents_spec.js b/spec/frontend/content_editor/components/wrappers/table_of_contents_spec.js
new file mode 100644
index 00000000000..bfda89a8b09
--- /dev/null
+++ b/spec/frontend/content_editor/components/wrappers/table_of_contents_spec.js
@@ -0,0 +1,84 @@
+import { nextTick } from 'vue';
+import { NodeViewWrapper } from '@tiptap/vue-2';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import { stubComponent } from 'helpers/stub_component';
+import eventHubFactory from '~/helpers/event_hub_factory';
+import Heading from '~/content_editor/extensions/heading';
+import Diagram from '~/content_editor/extensions/diagram';
+import TableOfContentsWrapper from '~/content_editor/components/wrappers/table_of_contents.vue';
+import { createTestEditor, createDocBuilder, emitEditorEvent } from '../../test_utils';
+
+describe('content/components/wrappers/table_of_contents', () => {
+ let wrapper;
+ let tiptapEditor;
+ let contentEditor;
+ let eventHub;
+
+ const buildEditor = () => {
+ tiptapEditor = createTestEditor({ extensions: [Heading, Diagram] });
+ contentEditor = { renderDiagram: jest.fn().mockResolvedValue('url/to/some/diagram') };
+ eventHub = eventHubFactory();
+ };
+
+ const createWrapper = async () => {
+ wrapper = mountExtended(TableOfContentsWrapper, {
+ propsData: {
+ editor: tiptapEditor,
+ node: {
+ attrs: {},
+ },
+ },
+ stubs: {
+ NodeViewWrapper: stubComponent(NodeViewWrapper),
+ },
+ provide: {
+ contentEditor,
+ tiptapEditor,
+ eventHub,
+ },
+ });
+ };
+
+ beforeEach(async () => {
+ buildEditor();
+ createWrapper();
+
+ const {
+ builders: { heading, doc },
+ } = createDocBuilder({
+ tiptapEditor,
+ names: {
+ heading: { nodeType: Heading.name },
+ },
+ });
+
+ const initialDoc = doc(
+ heading({ level: 1 }, 'Heading 1'),
+ heading({ level: 2 }, 'Heading 1.1'),
+ heading({ level: 3 }, 'Heading 1.1.1'),
+ heading({ level: 2 }, 'Heading 1.2'),
+ heading({ level: 3 }, 'Heading 1.2.1'),
+ heading({ level: 2 }, 'Heading 1.3'),
+ heading({ level: 2 }, 'Heading 1.4'),
+ heading({ level: 3 }, 'Heading 1.4.1'),
+ heading({ level: 1 }, 'Heading 2'),
+ );
+
+ tiptapEditor.commands.setContent(initialDoc.toJSON());
+
+ await emitEditorEvent({ event: 'update', tiptapEditor });
+ await nextTick();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('renders a node-view-wrapper as a ul element', () => {
+ expect(wrapper.findComponent(NodeViewWrapper).props().as).toBe('ul');
+ });
+
+ it('collects all headings and renders a nested list of headings', () => {
+ expect(wrapper.findComponent(NodeViewWrapper).element).toMatchSnapshot();
+ });
+});
diff --git a/spec/frontend/content_editor/extensions/image_spec.js b/spec/frontend/content_editor/extensions/image_spec.js
index 256f7bad309..f73b0143fd9 100644
--- a/spec/frontend/content_editor/extensions/image_spec.js
+++ b/spec/frontend/content_editor/extensions/image_spec.js
@@ -35,7 +35,7 @@ describe('content_editor/extensions/image', () => {
tiptapEditor.commands.setContent(initialDoc.toJSON());
expect(tiptapEditor.getHTML()).toEqual(
- '<p><img src="/-/wikis/uploads/image.jpg" alt="image" title="this is an image" data-canonical-src="uploads/image.jpg"></p>',
+ '<p><img src="/-/wikis/uploads/image.jpg" alt="image" title="this is an image"></p>',
);
});
});
diff --git a/spec/frontend/content_editor/markdown_processing_spec_helper.js b/spec/frontend/content_editor/markdown_processing_spec_helper.js
index 41442dd8388..228d009e42c 100644
--- a/spec/frontend/content_editor/markdown_processing_spec_helper.js
+++ b/spec/frontend/content_editor/markdown_processing_spec_helper.js
@@ -2,7 +2,6 @@ import fs from 'fs';
import jsYaml from 'js-yaml';
import { memoize } from 'lodash';
import { createContentEditor } from '~/content_editor';
-import { setTestTimeoutOnce } from 'helpers/timeout';
const getFocusedMarkdownExamples = memoize(
() => process.env.FOCUSED_MARKDOWN_EXAMPLES?.split(',') || [],
@@ -76,9 +75,6 @@ export const describeMarkdownProcessing = (description, markdownYamlPath) => {
}
it(exampleName, async () => {
- if (name === 'frontmatter_toml') {
- setTestTimeoutOnce(2000);
- }
await testSerializesHtmlToMarkdownForElement(example);
});
});
diff --git a/spec/frontend/content_editor/remark_markdown_processing_spec.js b/spec/frontend/content_editor/remark_markdown_processing_spec.js
index 48adceaab58..7ae0a7c13c1 100644
--- a/spec/frontend/content_editor/remark_markdown_processing_spec.js
+++ b/spec/frontend/content_editor/remark_markdown_processing_spec.js
@@ -5,6 +5,7 @@ import Code from '~/content_editor/extensions/code';
import CodeBlockHighlight from '~/content_editor/extensions/code_block_highlight';
import FootnoteDefinition from '~/content_editor/extensions/footnote_definition';
import FootnoteReference from '~/content_editor/extensions/footnote_reference';
+import Frontmatter from '~/content_editor/extensions/frontmatter';
import HardBreak from '~/content_editor/extensions/hard_break';
import HTMLNodes from '~/content_editor/extensions/html_nodes';
import Heading from '~/content_editor/extensions/heading';
@@ -15,6 +16,7 @@ import Link from '~/content_editor/extensions/link';
import ListItem from '~/content_editor/extensions/list_item';
import OrderedList from '~/content_editor/extensions/ordered_list';
import Paragraph from '~/content_editor/extensions/paragraph';
+import ReferenceDefinition from '~/content_editor/extensions/reference_definition';
import Sourcemap from '~/content_editor/extensions/sourcemap';
import Strike from '~/content_editor/extensions/strike';
import Table from '~/content_editor/extensions/table';
@@ -37,6 +39,7 @@ const tiptapEditor = createTestEditor({
CodeBlockHighlight,
FootnoteDefinition,
FootnoteReference,
+ Frontmatter,
HardBreak,
Heading,
HorizontalRule,
@@ -45,6 +48,7 @@ const tiptapEditor = createTestEditor({
Link,
ListItem,
OrderedList,
+ ReferenceDefinition,
Sourcemap,
Strike,
Table,
@@ -69,6 +73,7 @@ const {
div,
footnoteDefinition,
footnoteReference,
+ frontmatter,
hardBreak,
heading,
horizontalRule,
@@ -78,6 +83,7 @@ const {
listItem,
orderedList,
pre,
+ referenceDefinition,
strike,
table,
tableRow,
@@ -96,6 +102,7 @@ const {
codeBlock: { nodeType: CodeBlockHighlight.name },
footnoteDefinition: { nodeType: FootnoteDefinition.name },
footnoteReference: { nodeType: FootnoteReference.name },
+ frontmatter: { nodeType: Frontmatter.name },
hardBreak: { nodeType: HardBreak.name },
heading: { nodeType: Heading.name },
horizontalRule: { nodeType: HorizontalRule.name },
@@ -105,6 +112,7 @@ const {
listItem: { nodeType: ListItem.name },
orderedList: { nodeType: OrderedList.name },
paragraph: { nodeType: Paragraph.name },
+ referenceDefinition: { nodeType: ReferenceDefinition.name },
strike: { nodeType: Strike.name },
table: { nodeType: Table.name },
tableCell: { nodeType: TableCell.name },
@@ -253,7 +261,12 @@ describe('Client side Markdown processing', () => {
expectedDoc: doc(
paragraph(
source('<img src="bar" alt="foo" />'),
- image({ ...source('<img src="bar" alt="foo" />'), alt: 'foo', src: 'bar' }),
+ image({
+ ...source('<img src="bar" alt="foo" />'),
+ alt: 'foo',
+ canonicalSrc: 'bar',
+ src: 'bar',
+ }),
),
),
},
@@ -271,7 +284,12 @@ describe('Client side Markdown processing', () => {
),
paragraph(
source('<img src="bar" alt="foo" />'),
- image({ ...source('<img src="bar" alt="foo" />'), alt: 'foo', src: 'bar' }),
+ image({
+ ...source('<img src="bar" alt="foo" />'),
+ alt: 'foo',
+ src: 'bar',
+ canonicalSrc: 'bar',
+ }),
),
),
},
@@ -284,6 +302,7 @@ describe('Client side Markdown processing', () => {
{
...source('[GitLab](https://gitlab.com "Go to GitLab")'),
href: 'https://gitlab.com',
+ canonicalSrc: 'https://gitlab.com',
title: 'Go to GitLab',
},
'GitLab',
@@ -302,6 +321,7 @@ describe('Client side Markdown processing', () => {
{
...source('[GitLab](https://gitlab.com "Go to GitLab")'),
href: 'https://gitlab.com',
+ canonicalSrc: 'https://gitlab.com',
title: 'Go to GitLab',
},
'GitLab',
@@ -318,6 +338,7 @@ describe('Client side Markdown processing', () => {
link(
{
...source('www.commonmark.org'),
+ canonicalSrc: 'http://www.commonmark.org',
href: 'http://www.commonmark.org',
},
'www.commonmark.org',
@@ -334,6 +355,7 @@ describe('Client side Markdown processing', () => {
link(
{
...source('www.commonmark.org/help'),
+ canonicalSrc: 'http://www.commonmark.org/help',
href: 'http://www.commonmark.org/help',
},
'www.commonmark.org/help',
@@ -351,6 +373,7 @@ describe('Client side Markdown processing', () => {
link(
{
...source('hello+xyz@mail.example'),
+ canonicalSrc: 'mailto:hello+xyz@mail.example',
href: 'mailto:hello+xyz@mail.example',
},
'hello+xyz@mail.example',
@@ -369,6 +392,7 @@ describe('Client side Markdown processing', () => {
{
sourceMapKey: null,
sourceMarkdown: null,
+ canonicalSrc: 'https://gitlab.com',
href: 'https://gitlab.com',
},
'https://gitlab.com',
@@ -398,6 +422,7 @@ hard line break`,
image({
...source('![GitLab Logo](https://gitlab.com/logo.png "GitLab Logo")'),
alt: 'GitLab Logo',
+ canonicalSrc: 'https://gitlab.com/logo.png',
src: 'https://gitlab.com/logo.png',
title: 'GitLab Logo',
}),
@@ -591,7 +616,12 @@ two
paragraph(
source('List item with an image ![bar](foo.png)'),
'List item with an image',
- image({ ...source('![bar](foo.png)'), alt: 'bar', src: 'foo.png' }),
+ image({
+ ...source('![bar](foo.png)'),
+ alt: 'bar',
+ canonicalSrc: 'foo.png',
+ src: 'foo.png',
+ }),
),
),
),
@@ -940,8 +970,17 @@ Paragraph
paragraph(
source('[![moon](moon.jpg)](/uri)'),
link(
- { ...source('[![moon](moon.jpg)](/uri)'), href: '/uri' },
- image({ ...source('![moon](moon.jpg)'), src: 'moon.jpg', alt: 'moon' }),
+ {
+ ...source('[![moon](moon.jpg)](/uri)'),
+ canonicalSrc: '/uri',
+ href: '/uri',
+ },
+ image({
+ ...source('![moon](moon.jpg)'),
+ canonicalSrc: 'moon.jpg',
+ src: 'moon.jpg',
+ alt: 'moon',
+ }),
),
),
),
@@ -971,12 +1010,26 @@ Paragraph
source('~[moon](moon.jpg) and [sun](sun.jpg)~'),
strike(
source('~[moon](moon.jpg) and [sun](sun.jpg)~'),
- link({ ...source('[moon](moon.jpg)'), href: 'moon.jpg' }, 'moon'),
+ link(
+ {
+ ...source('[moon](moon.jpg)'),
+ canonicalSrc: 'moon.jpg',
+ href: 'moon.jpg',
+ },
+ 'moon',
+ ),
),
strike(source('~[moon](moon.jpg) and [sun](sun.jpg)~'), ' and '),
strike(
source('~[moon](moon.jpg) and [sun](sun.jpg)~'),
- link({ ...source('[sun](sun.jpg)'), href: 'sun.jpg' }, 'sun'),
+ link(
+ {
+ ...source('[sun](sun.jpg)'),
+ href: 'sun.jpg',
+ canonicalSrc: 'sun.jpg',
+ },
+ 'sun',
+ ),
),
),
),
@@ -1079,6 +1132,107 @@ _world_.
),
),
},
+ {
+ markdown: `
+[GitLab][gitlab-url]
+
+[gitlab-url]: https://gitlab.com "GitLab"
+
+ `,
+ expectedDoc: doc(
+ paragraph(
+ source('[GitLab][gitlab-url]'),
+ link(
+ {
+ ...source('[GitLab][gitlab-url]'),
+ href: 'https://gitlab.com',
+ canonicalSrc: 'gitlab-url',
+ title: 'GitLab',
+ isReference: true,
+ },
+ 'GitLab',
+ ),
+ ),
+ referenceDefinition(
+ {
+ ...source('[gitlab-url]: https://gitlab.com "GitLab"'),
+ identifier: 'gitlab-url',
+ url: 'https://gitlab.com',
+ title: 'GitLab',
+ },
+ '[gitlab-url]: https://gitlab.com "GitLab"',
+ ),
+ ),
+ },
+ {
+ markdown: `
+![GitLab Logo][gitlab-logo]
+
+[gitlab-logo]: https://gitlab.com/gitlab-logo.png "GitLab Logo"
+
+ `,
+ expectedDoc: doc(
+ paragraph(
+ source('![GitLab Logo][gitlab-logo]'),
+ image({
+ ...source('![GitLab Logo][gitlab-logo]'),
+ src: 'https://gitlab.com/gitlab-logo.png',
+ canonicalSrc: 'gitlab-logo',
+ alt: 'GitLab Logo',
+ title: 'GitLab Logo',
+ isReference: true,
+ }),
+ ),
+ referenceDefinition(
+ {
+ ...source('[gitlab-logo]: https://gitlab.com/gitlab-logo.png "GitLab Logo"'),
+ identifier: 'gitlab-logo',
+ url: 'https://gitlab.com/gitlab-logo.png',
+ title: 'GitLab Logo',
+ },
+ '[gitlab-logo]: https://gitlab.com/gitlab-logo.png "GitLab Logo"',
+ ),
+ ),
+ },
+ {
+ markdown: `
+---
+title: 'layout'
+---
+ `,
+ expectedDoc: doc(
+ frontmatter(
+ { ...source("---\ntitle: 'layout'\n---"), language: 'yaml' },
+ "title: 'layout'",
+ ),
+ ),
+ },
+ {
+ markdown: `
++++
+title: 'layout'
++++
+ `,
+ expectedDoc: doc(
+ frontmatter(
+ { ...source("+++\ntitle: 'layout'\n+++"), language: 'toml' },
+ "title: 'layout'",
+ ),
+ ),
+ },
+ {
+ markdown: `
+;;;
+{ title: 'layout' }
+;;;
+ `,
+ expectedDoc: doc(
+ frontmatter(
+ { ...source(";;;\n{ title: 'layout' }\n;;;"), language: 'json' },
+ "{ title: 'layout' }",
+ ),
+ ),
+ },
];
const runOnly = examples.find((example) => example.only === true);
@@ -1090,7 +1244,7 @@ _world_.
const trimmed = markdown.trim();
const document = await deserialize(trimmed);
- expect(expectedDoc).not.toBeFalsy();
+ expect(expectedDoc).not.toBe(false);
expect(document.toJSON()).toEqual(expectedDoc.toJSON());
expect(serialize(document)).toEqual(expectedMarkdown ?? trimmed);
},
@@ -1155,4 +1309,72 @@ body {
expect(tiptapEditor.getHTML()).toEqual(expectedHtml);
},
);
+
+ describe('attribute sanitization', () => {
+ // eslint-disable-next-line no-script-url
+ const protocolBasedInjectionSimpleNoSpaces = "javascript:alert('XSS');";
+ // eslint-disable-next-line no-script-url
+ const protocolBasedInjectionSimpleSpacesBefore = "javascript: alert('XSS');";
+
+ const docWithImageFactory = (urlInput, urlOutput) => {
+ const input = `<img src="${urlInput}">`;
+
+ return {
+ input,
+ expectedDoc: doc(
+ paragraph(
+ source(input),
+ image({
+ ...source(input),
+ src: urlOutput,
+ canonicalSrc: urlOutput,
+ }),
+ ),
+ ),
+ };
+ };
+
+ const docWithLinkFactory = (urlInput, urlOutput) => {
+ const input = `<a href="${urlInput}">foo</a>`;
+
+ return {
+ input,
+ expectedDoc: doc(
+ paragraph(
+ source(input),
+ link({ ...source(input), href: urlOutput, canonicalSrc: urlOutput }, 'foo'),
+ ),
+ ),
+ };
+ };
+
+ it.each`
+ desc | urlInput | urlOutput
+ ${'protocol-based JS injection: simple, no spaces'} | ${protocolBasedInjectionSimpleNoSpaces} | ${null}
+ ${'protocol-based JS injection: simple, spaces before'} | ${"javascript :alert('XSS');"} | ${null}
+ ${'protocol-based JS injection: simple, spaces after'} | ${protocolBasedInjectionSimpleSpacesBefore} | ${null}
+ ${'protocol-based JS injection: simple, spaces before and after'} | ${"javascript : alert('XSS');"} | ${null}
+ ${'protocol-based JS injection: UTF-8 encoding'} | ${'javascript&#58;'} | ${null}
+ ${'protocol-based JS injection: long UTF-8 encoding'} | ${'javascript&#0058;'} | ${null}
+ ${'protocol-based JS injection: long UTF-8 encoding without semicolons'} | ${'&#0000106&#0000097&#0000118&#0000097&#0000115&#0000099&#0000114&#0000105&#0000112&#0000116&#0000058&#0000097&#0000108&#0000101&#0000114&#0000116&#0000040&#0000039&#0000088&#0000083&#0000083&#0000039&#0000041'} | ${null}
+ ${'protocol-based JS injection: hex encoding'} | ${'javascript&#x3A;'} | ${null}
+ ${'protocol-based JS injection: long hex encoding'} | ${'javascript&#x003A;'} | ${null}
+ ${'protocol-based JS injection: hex encoding without semicolons'} | ${'&#x6A&#x61&#x76&#x61&#x73&#x63&#x72&#x69&#x70&#x74&#x3A&#x61&#x6C&#x65&#x72&#x74&#x28&#x27&#x58&#x53&#x53&#x27&#x29'} | ${null}
+ ${'protocol-based JS injection: Unicode'} | ${"\u0001java\u0003script:alert('XSS')"} | ${null}
+ ${'protocol-based JS injection: spaces and entities'} | ${"&#14; javascript:alert('XSS');"} | ${null}
+ ${'vbscript'} | ${'vbscript:alert(document.domain)'} | ${null}
+ ${'protocol-based JS injection: preceding colon'} | ${":javascript:alert('XSS');"} | ${":javascript:alert('XSS');"}
+ ${'protocol-based JS injection: null char'} | ${"java\0script:alert('XSS')"} | ${"java�script:alert('XSS')"}
+ ${'protocol-based JS injection: invalid URL char'} | ${"java\\script:alert('XSS')"} | ${"java\\script:alert('XSS')"}
+ `('sanitize $desc:\n\tURL "$urlInput" becomes "$urlOutput"', ({ urlInput, urlOutput }) => {
+ const exampleFactories = [docWithImageFactory, docWithLinkFactory];
+
+ exampleFactories.forEach(async (exampleFactory) => {
+ const { input, expectedDoc } = exampleFactory(urlInput, urlOutput);
+ const document = await deserialize(input);
+
+ expect(document.toJSON()).toEqual(expectedDoc.toJSON());
+ });
+ });
+ });
});
diff --git a/spec/frontend/content_editor/render_html_and_json_for_all_examples.js b/spec/frontend/content_editor/render_html_and_json_for_all_examples.js
index 116a26cf7d5..4a57c7b1942 100644
--- a/spec/frontend/content_editor/render_html_and_json_for_all_examples.js
+++ b/spec/frontend/content_editor/render_html_and_json_for_all_examples.js
@@ -16,6 +16,7 @@ import FigureCaption from '~/content_editor/extensions/figure_caption';
import FootnoteDefinition from '~/content_editor/extensions/footnote_definition';
import FootnoteReference from '~/content_editor/extensions/footnote_reference';
import FootnotesSection from '~/content_editor/extensions/footnotes_section';
+import Frontmatter from '~/content_editor/extensions/frontmatter';
import HardBreak from '~/content_editor/extensions/hard_break';
import Heading from '~/content_editor/extensions/heading';
import HorizontalRule from '~/content_editor/extensions/horizontal_rule';
@@ -26,6 +27,7 @@ import Italic from '~/content_editor/extensions/italic';
import Link from '~/content_editor/extensions/link';
import ListItem from '~/content_editor/extensions/list_item';
import OrderedList from '~/content_editor/extensions/ordered_list';
+import ReferenceDefinition from '~/content_editor/extensions/reference_definition';
import Strike from '~/content_editor/extensions/strike';
import Table from '~/content_editor/extensions/table';
import TableCell from '~/content_editor/extensions/table_cell';
@@ -51,6 +53,7 @@ const tiptapEditor = createTestEditor({
FootnoteDefinition,
FootnoteReference,
FootnotesSection,
+ Frontmatter,
Figure,
FigureCaption,
HardBreak,
@@ -63,6 +66,7 @@ const tiptapEditor = createTestEditor({
Link,
ListItem,
OrderedList,
+ ReferenceDefinition,
Strike,
Table,
TableCell,
diff --git a/spec/frontend/content_editor/services/markdown_serializer_spec.js b/spec/frontend/content_editor/services/markdown_serializer_spec.js
index 509cda3046c..0e5281be9bf 100644
--- a/spec/frontend/content_editor/services/markdown_serializer_spec.js
+++ b/spec/frontend/content_editor/services/markdown_serializer_spec.js
@@ -24,6 +24,7 @@ import Link from '~/content_editor/extensions/link';
import ListItem from '~/content_editor/extensions/list_item';
import OrderedList from '~/content_editor/extensions/ordered_list';
import Paragraph from '~/content_editor/extensions/paragraph';
+import ReferenceDefinition from '~/content_editor/extensions/reference_definition';
import Sourcemap from '~/content_editor/extensions/sourcemap';
import Strike from '~/content_editor/extensions/strike';
import Table from '~/content_editor/extensions/table';
@@ -63,6 +64,7 @@ const tiptapEditor = createTestEditor({
Link,
ListItem,
OrderedList,
+ ReferenceDefinition,
Sourcemap,
Strike,
Table,
@@ -104,6 +106,7 @@ const {
listItem,
orderedList,
paragraph,
+ referenceDefinition,
strike,
table,
tableCell,
@@ -139,6 +142,7 @@ const {
listItem: { nodeType: ListItem.name },
orderedList: { nodeType: OrderedList.name },
paragraph: { nodeType: Paragraph.name },
+ referenceDefinition: { nodeType: ReferenceDefinition.name },
strike: { markType: Strike.name },
table: { nodeType: Table.name },
tableCell: { nodeType: TableCell.name },
@@ -243,6 +247,37 @@ describe('markdownSerializer', () => {
).toBe('[download file](file.zip "click here to download")');
});
+ it('correctly serializes link references', () => {
+ expect(
+ serialize(
+ paragraph(
+ link(
+ {
+ href: 'gitlab-url',
+ isReference: true,
+ },
+ 'GitLab',
+ ),
+ ),
+ ),
+ ).toBe('[GitLab][gitlab-url]');
+ });
+
+ it('correctly serializes image references', () => {
+ expect(
+ serialize(
+ paragraph(
+ image({
+ canonicalSrc: 'gitlab-url',
+ src: 'image.svg',
+ alt: 'GitLab',
+ isReference: true,
+ }),
+ ),
+ ),
+ ).toBe('![GitLab][gitlab-url]');
+ });
+
it('correctly serializes strikethrough', () => {
expect(serialize(paragraph(strike('deleted content')))).toBe('~~deleted content~~');
});
@@ -1163,6 +1198,38 @@ Oranges are orange [^1]
);
});
+ it('correctly serializes reference definition', () => {
+ expect(
+ serialize(
+ referenceDefinition('[gitlab]: https://gitlab.com'),
+ referenceDefinition('[foobar]: foobar.com'),
+ ),
+ ).toBe(
+ `
+[gitlab]: https://gitlab.com
+[foobar]: foobar.com`.trimLeft(),
+ );
+ });
+
+ it('correctly adds a space between a reference definition and a block content', () => {
+ expect(
+ serialize(
+ paragraph('paragraph'),
+ referenceDefinition('[gitlab]: https://gitlab.com'),
+ referenceDefinition('[foobar]: foobar.com'),
+ heading({ level: 2 }, 'heading'),
+ ),
+ ).toBe(
+ `
+paragraph
+
+[gitlab]: https://gitlab.com
+[foobar]: foobar.com
+
+## heading`.trimLeft(),
+ );
+ });
+
const defaultEditAction = (initialContent) => {
tiptapEditor.chain().setContent(initialContent.toJSON()).insertContent(' modified').run();
};
@@ -1177,42 +1244,49 @@ Oranges are orange [^1]
};
it.each`
- mark | markdown | modifiedMarkdown | editAction
- ${'bold'} | ${'**bold**'} | ${'**bold modified**'} | ${defaultEditAction}
- ${'bold'} | ${'__bold__'} | ${'__bold modified__'} | ${defaultEditAction}
- ${'bold'} | ${'<strong>bold</strong>'} | ${'<strong>bold modified</strong>'} | ${defaultEditAction}
- ${'bold'} | ${'<b>bold</b>'} | ${'<b>bold modified</b>'} | ${defaultEditAction}
- ${'italic'} | ${'_italic_'} | ${'_italic modified_'} | ${defaultEditAction}
- ${'italic'} | ${'*italic*'} | ${'*italic modified*'} | ${defaultEditAction}
- ${'italic'} | ${'<em>italic</em>'} | ${'<em>italic modified</em>'} | ${defaultEditAction}
- ${'italic'} | ${'<i>italic</i>'} | ${'<i>italic modified</i>'} | ${defaultEditAction}
- ${'link'} | ${'[gitlab](https://gitlab.com)'} | ${'[gitlab modified](https://gitlab.com)'} | ${defaultEditAction}
- ${'link'} | ${'<a href="https://gitlab.com">link</a>'} | ${'<a href="https://gitlab.com">link modified</a>'} | ${defaultEditAction}
- ${'link'} | ${'link www.gitlab.com'} | ${'modified link www.gitlab.com'} | ${prependContentEditAction}
- ${'link'} | ${'link https://www.gitlab.com'} | ${'modified link https://www.gitlab.com'} | ${prependContentEditAction}
- ${'link'} | ${'link(https://www.gitlab.com)'} | ${'modified link(https://www.gitlab.com)'} | ${prependContentEditAction}
- ${'link'} | ${'link(engineering@gitlab.com)'} | ${'modified link(engineering@gitlab.com)'} | ${prependContentEditAction}
- ${'link'} | ${'link <https://www.gitlab.com>'} | ${'modified link <https://www.gitlab.com>'} | ${prependContentEditAction}
- ${'link'} | ${'link [https://www.gitlab.com>'} | ${'modified link \\[https://www.gitlab.com>'} | ${prependContentEditAction}
- ${'link'} | ${'link <https://www.gitlab.com'} | ${'modified link <https://www.gitlab.com'} | ${prependContentEditAction}
- ${'link'} | ${'link https://www.gitlab.com>'} | ${'modified link https://www.gitlab.com>'} | ${prependContentEditAction}
- ${'link'} | ${'link **https://www.gitlab.com]**'} | ${'modified link [**https://www.gitlab.com\\]**](https://www.gitlab.com%5D)'} | ${prependContentEditAction}
- ${'code'} | ${'`code`'} | ${'`code modified`'} | ${defaultEditAction}
- ${'code'} | ${'<code>code</code>'} | ${'<code>code modified</code>'} | ${defaultEditAction}
- ${'strike'} | ${'~~striked~~'} | ${'~~striked modified~~'} | ${defaultEditAction}
- ${'strike'} | ${'<del>striked</del>'} | ${'<del>striked modified</del>'} | ${defaultEditAction}
- ${'strike'} | ${'<strike>striked</strike>'} | ${'<strike>striked modified</strike>'} | ${defaultEditAction}
- ${'strike'} | ${'<s>striked</s>'} | ${'<s>striked modified</s>'} | ${defaultEditAction}
- ${'list'} | ${'- list item'} | ${'- list item modified'} | ${defaultEditAction}
- ${'list'} | ${'* list item'} | ${'* list item modified'} | ${defaultEditAction}
- ${'list'} | ${'+ list item'} | ${'+ list item modified'} | ${defaultEditAction}
- ${'list'} | ${'- list item 1\n- list item 2'} | ${'- list item 1\n- list item 2 modified'} | ${defaultEditAction}
- ${'list'} | ${'2) list item'} | ${'2) list item modified'} | ${defaultEditAction}
- ${'list'} | ${'1. list item'} | ${'1. list item modified'} | ${defaultEditAction}
- ${'taskList'} | ${'2) [ ] task list item'} | ${'2) [ ] task list item modified'} | ${defaultEditAction}
- ${'taskList'} | ${'2) [x] task list item'} | ${'2) [x] task list item modified'} | ${defaultEditAction}
+ mark | markdown | modifiedMarkdown | editAction
+ ${'bold'} | ${'**bold**'} | ${'**bold modified**'} | ${defaultEditAction}
+ ${'bold'} | ${'__bold__'} | ${'__bold modified__'} | ${defaultEditAction}
+ ${'bold'} | ${'<strong>bold</strong>'} | ${'<strong>bold modified</strong>'} | ${defaultEditAction}
+ ${'bold'} | ${'<b>bold</b>'} | ${'<b>bold modified</b>'} | ${defaultEditAction}
+ ${'italic'} | ${'_italic_'} | ${'_italic modified_'} | ${defaultEditAction}
+ ${'italic'} | ${'*italic*'} | ${'*italic modified*'} | ${defaultEditAction}
+ ${'italic'} | ${'<em>italic</em>'} | ${'<em>italic modified</em>'} | ${defaultEditAction}
+ ${'italic'} | ${'<i>italic</i>'} | ${'<i>italic modified</i>'} | ${defaultEditAction}
+ ${'link'} | ${'[gitlab](https://gitlab.com)'} | ${'[gitlab modified](https://gitlab.com)'} | ${defaultEditAction}
+ ${'link'} | ${'<a href="https://gitlab.com">link</a>'} | ${'<a href="https://gitlab.com">link modified</a>'} | ${defaultEditAction}
+ ${'link'} | ${'link www.gitlab.com'} | ${'modified link www.gitlab.com'} | ${prependContentEditAction}
+ ${'link'} | ${'link https://www.gitlab.com'} | ${'modified link https://www.gitlab.com'} | ${prependContentEditAction}
+ ${'link'} | ${'link(https://www.gitlab.com)'} | ${'modified link(https://www.gitlab.com)'} | ${prependContentEditAction}
+ ${'link'} | ${'link(engineering@gitlab.com)'} | ${'modified link(engineering@gitlab.com)'} | ${prependContentEditAction}
+ ${'link'} | ${'link <https://www.gitlab.com>'} | ${'modified link <https://www.gitlab.com>'} | ${prependContentEditAction}
+ ${'link'} | ${'link [https://www.gitlab.com>'} | ${'modified link \\[https://www.gitlab.com>'} | ${prependContentEditAction}
+ ${'link'} | ${'link <https://www.gitlab.com'} | ${'modified link <https://www.gitlab.com'} | ${prependContentEditAction}
+ ${'link'} | ${'link https://www.gitlab.com>'} | ${'modified link https://www.gitlab.com>'} | ${prependContentEditAction}
+ ${'link'} | ${'link https://www.gitlab.com/path'} | ${'modified link https://www.gitlab.com/path'} | ${prependContentEditAction}
+ ${'link'} | ${'link https://www.gitlab.com?query=search'} | ${'modified link https://www.gitlab.com?query=search'} | ${prependContentEditAction}
+ ${'link'} | ${'link https://www.gitlab.com/#fragment'} | ${'modified link https://www.gitlab.com/#fragment'} | ${prependContentEditAction}
+ ${'link'} | ${'link https://www.gitlab.com/?query=search'} | ${'modified link https://www.gitlab.com/?query=search'} | ${prependContentEditAction}
+ ${'link'} | ${'link https://www.gitlab.com#fragment'} | ${'modified link https://www.gitlab.com#fragment'} | ${prependContentEditAction}
+ ${'link'} | ${'link **https://www.gitlab.com]**'} | ${'modified link **https://www.gitlab.com\\]**'} | ${prependContentEditAction}
+ ${'code'} | ${'`code`'} | ${'`code modified`'} | ${defaultEditAction}
+ ${'code'} | ${'<code>code</code>'} | ${'<code>code modified</code>'} | ${defaultEditAction}
+ ${'strike'} | ${'~~striked~~'} | ${'~~striked modified~~'} | ${defaultEditAction}
+ ${'strike'} | ${'<del>striked</del>'} | ${'<del>striked modified</del>'} | ${defaultEditAction}
+ ${'strike'} | ${'<strike>striked</strike>'} | ${'<strike>striked modified</strike>'} | ${defaultEditAction}
+ ${'strike'} | ${'<s>striked</s>'} | ${'<s>striked modified</s>'} | ${defaultEditAction}
+ ${'list'} | ${'- list item'} | ${'- list item modified'} | ${defaultEditAction}
+ ${'list'} | ${'* list item'} | ${'* list item modified'} | ${defaultEditAction}
+ ${'list'} | ${'+ list item'} | ${'+ list item modified'} | ${defaultEditAction}
+ ${'list'} | ${'- list item 1\n- list item 2'} | ${'- list item 1\n- list item 2 modified'} | ${defaultEditAction}
+ ${'list'} | ${'2) list item'} | ${'2) list item modified'} | ${defaultEditAction}
+ ${'list'} | ${'1. list item'} | ${'1. list item modified'} | ${defaultEditAction}
+ ${'taskList'} | ${'2) [ ] task list item'} | ${'2) [ ] task list item modified'} | ${defaultEditAction}
+ ${'taskList'} | ${'2) [x] task list item'} | ${'2) [x] task list item modified'} | ${defaultEditAction}
+ ${'image'} | ${'![image](image.png)'} | ${'![image](image.png) modified'} | ${defaultEditAction}
+ ${'footnoteReference'} | ${'[^1] footnote\n\n[^1]: footnote definition'} | ${'modified [^1] footnote\n\n[^1]: footnote definition'} | ${prependContentEditAction}
`(
- 'preserves original $mark syntax when sourceMarkdown is available for $content',
+ 'preserves original $mark syntax when sourceMarkdown is available for $markdown',
async ({ markdown, modifiedMarkdown, editAction }) => {
const { document } = await remarkMarkdownDeserializer().deserialize({
schema: tiptapEditor.schema,
diff --git a/spec/frontend/content_editor/services/table_of_contents_utils_spec.js b/spec/frontend/content_editor/services/table_of_contents_utils_spec.js
new file mode 100644
index 00000000000..7f63c2171c2
--- /dev/null
+++ b/spec/frontend/content_editor/services/table_of_contents_utils_spec.js
@@ -0,0 +1,96 @@
+import Heading from '~/content_editor/extensions/heading';
+import { toTree, getHeadings } from '~/content_editor/services/table_of_contents_utils';
+import { createTestEditor, createDocBuilder } from '../test_utils';
+
+describe('content_editor/services/table_of_content_utils', () => {
+ describe('toTree', () => {
+ it('should fills in gaps in heading levels and convert headings to a tree', () => {
+ expect(
+ toTree([
+ { level: 3, text: '3' },
+ { level: 2, text: '2' },
+ ]),
+ ).toEqual([
+ expect.objectContaining({
+ level: 1,
+ text: '',
+ subHeadings: [
+ expect.objectContaining({
+ level: 2,
+ text: '',
+ subHeadings: [expect.objectContaining({ level: 3, text: '3', subHeadings: [] })],
+ }),
+ expect.objectContaining({ level: 2, text: '2', subHeadings: [] }),
+ ],
+ }),
+ ]);
+ });
+ });
+
+ describe('getHeadings', () => {
+ const tiptapEditor = createTestEditor({
+ extensions: [Heading],
+ });
+
+ const {
+ builders: { heading, doc },
+ } = createDocBuilder({
+ tiptapEditor,
+ names: {
+ heading: { nodeType: Heading.name },
+ },
+ });
+
+ it('gets all headings as a tree in a tiptap document', () => {
+ const initialDoc = doc(
+ heading({ level: 1 }, 'Heading 1'),
+ heading({ level: 2 }, 'Heading 1.1'),
+ heading({ level: 3 }, 'Heading 1.1.1'),
+ heading({ level: 2 }, 'Heading 1.2'),
+ heading({ level: 3 }, 'Heading 1.2.1'),
+ heading({ level: 2 }, 'Heading 1.3'),
+ heading({ level: 2 }, 'Heading 1.4'),
+ heading({ level: 3 }, 'Heading 1.4.1'),
+ heading({ level: 1 }, 'Heading 2'),
+ );
+
+ tiptapEditor.commands.setContent(initialDoc.toJSON());
+
+ expect(getHeadings(tiptapEditor)).toEqual([
+ expect.objectContaining({
+ level: 1,
+ text: 'Heading 1',
+ subHeadings: [
+ expect.objectContaining({
+ level: 2,
+ text: 'Heading 1.1',
+ subHeadings: [
+ expect.objectContaining({ level: 3, text: 'Heading 1.1.1', subHeadings: [] }),
+ ],
+ }),
+ expect.objectContaining({
+ level: 2,
+ text: 'Heading 1.2',
+ subHeadings: [
+ expect.objectContaining({ level: 3, text: 'Heading 1.2.1', subHeadings: [] }),
+ ],
+ }),
+ expect.objectContaining({ level: 2, text: 'Heading 1.3', subHeadings: [] }),
+ expect.objectContaining({
+ level: 2,
+ text: 'Heading 1.4',
+ subHeadings: [
+ expect.objectContaining({ level: 3, text: 'Heading 1.4.1', subHeadings: [] }),
+ ],
+ }),
+ ],
+ }),
+ expect.objectContaining({
+ level: 1,
+ text: 'Heading 2',
+ subHeadings: [],
+ }),
+ ]);
+ });
+ });
+});
diff --git a/spec/frontend/crm/contact_form_wrapper_spec.js b/spec/frontend/crm/contact_form_wrapper_spec.js
index 5e1743701e4..e49b553e4b5 100644
--- a/spec/frontend/crm/contact_form_wrapper_spec.js
+++ b/spec/frontend/crm/contact_form_wrapper_spec.js
@@ -56,8 +56,9 @@ describe('Customer relations contact form wrapper', () => {
${'edit'} | ${'Edit contact'} | ${'Contact has been updated.'} | ${updateContactMutation} | ${contacts[0].id}
${'create'} | ${'New contact'} | ${'Contact has been added.'} | ${createContactMutation} | ${null}
`('in $mode mode', ({ mode, title, successMessage, mutation, existingId }) => {
+ const isEditMode = mode === 'edit';
+
beforeEach(() => {
- const isEditMode = mode === 'edit';
mountComponent({ isEditMode });
return waitForPromises();
@@ -82,7 +83,7 @@ describe('Customer relations contact form wrapper', () => {
});
it('renders correct fields prop', () => {
- expect(findContactForm().props('fields')).toEqual([
+ const fields = [
{ name: 'firstName', label: 'First name', required: true },
{ name: 'lastName', label: 'Last name', required: true },
{ name: 'email', label: 'Email', required: true },
@@ -98,7 +99,9 @@ describe('Customer relations contact form wrapper', () => {
],
},
{ name: 'description', label: 'Description' },
- ]);
+ ];
+ if (isEditMode) fields.push({ name: 'active', label: 'Active', required: true, bool: true });
+ expect(findContactForm().props('fields')).toEqual(fields);
});
it('renders correct title prop', () => {
diff --git a/spec/frontend/crm/contacts_root_spec.js b/spec/frontend/crm/contacts_root_spec.js
index 3a6989a00f1..7aaaf480c44 100644
--- a/spec/frontend/crm/contacts_root_spec.js
+++ b/spec/frontend/crm/contacts_root_spec.js
@@ -1,14 +1,16 @@
-import { GlAlert, GlLoadingIcon } from '@gitlab/ui';
+import { GlLoadingIcon } from '@gitlab/ui';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import VueRouter from 'vue-router';
-import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import ContactsRoot from '~/crm/contacts/components/contacts_root.vue';
import getGroupContactsQuery from '~/crm/contacts/components/graphql/get_group_contacts.query.graphql';
+import getGroupContactsCountByStateQuery from '~/crm/contacts/components/graphql/get_group_contacts_count_by_state.graphql';
import routes from '~/crm/contacts/routes';
-import { getGroupContactsQueryResponse } from './mock_data';
+import PaginatedTableWithSearchAndTabs from '~/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs.vue';
+import { getGroupContactsQueryResponse, getGroupContactsCountQueryResponse } from './mock_data';
describe('Customer relations contacts root app', () => {
Vue.use(VueApollo);
@@ -21,24 +23,30 @@ describe('Customer relations contacts root app', () => {
const findRowByName = (rowName) => wrapper.findAllByRole('row', { name: rowName });
const findIssuesLinks = () => wrapper.findAllByTestId('issues-link');
const findNewContactButton = () => wrapper.findByTestId('new-contact-button');
- const findError = () => wrapper.findComponent(GlAlert);
+ const findTable = () => wrapper.findComponent(PaginatedTableWithSearchAndTabs);
const successQueryHandler = jest.fn().mockResolvedValue(getGroupContactsQueryResponse);
+ const successCountQueryHandler = jest.fn().mockResolvedValue(getGroupContactsCountQueryResponse);
const basePath = '/groups/flightjs/-/crm/contacts';
const mountComponent = ({
queryHandler = successQueryHandler,
- mountFunction = shallowMountExtended,
+ countQueryHandler = successCountQueryHandler,
canAdminCrmContact = true,
+ textQuery = null,
} = {}) => {
- fakeApollo = createMockApollo([[getGroupContactsQuery, queryHandler]]);
- wrapper = mountFunction(ContactsRoot, {
+ fakeApollo = createMockApollo([
+ [getGroupContactsQuery, queryHandler],
+ [getGroupContactsCountByStateQuery, countQueryHandler],
+ ]);
+ wrapper = mountExtended(ContactsRoot, {
router,
provide: {
groupFullPath: 'flightjs',
groupId: 26,
groupIssuesPath: '/issues',
canAdminCrmContact,
+ textQuery,
},
apolloProvider: fakeApollo,
});
@@ -58,9 +66,33 @@ describe('Customer relations contacts root app', () => {
router = null;
});
- it('should render loading spinner', () => {
+ it('should render table with default props and loading state', () => {
mountComponent();
+ expect(findTable().props()).toMatchObject({
+ items: [],
+ itemsCount: {},
+ pageInfo: {},
+ statusTabs: [
+ { title: 'Active', status: 'ACTIVE', filters: 'active' },
+ { title: 'Inactive', status: 'INACTIVE', filters: 'inactive' },
+ { title: 'All', status: 'ALL', filters: 'all' },
+ ],
+ showItems: true,
+ showErrorMsg: false,
+ trackViewsOptions: { category: 'Customer Relations', action: 'view_contacts_list' },
+ i18n: {
+ emptyText: 'No contacts found',
+ issuesButtonLabel: 'View issues',
+ editButtonLabel: 'Edit',
+ title: 'Customer relations contacts',
+ newContact: 'New contact',
+ errorText: 'Something went wrong. Please try again.',
+ },
+ serverErrorMessage: '',
+ filterSearchKey: 'contacts',
+ filterSearchTokens: [],
+ });
expect(findLoadingIcon().exists()).toBe(true);
});
@@ -83,7 +115,7 @@ describe('Customer relations contacts root app', () => {
mountComponent({ queryHandler: jest.fn().mockRejectedValue('ERROR') });
await waitForPromises();
- expect(findError().exists()).toBe(true);
+ expect(wrapper.text()).toContain('Something went wrong. Please try again.');
});
});
@@ -92,11 +124,11 @@ describe('Customer relations contacts root app', () => {
mountComponent();
await waitForPromises();
- expect(findError().exists()).toBe(false);
+ expect(wrapper.text()).not.toContain('Something went wrong. Please try again.');
});
it('renders correct results', async () => {
- mountComponent({ mountFunction: mountExtended });
+ mountComponent();
await waitForPromises();
expect(findRowByName(/Marty/i)).toHaveLength(1);
@@ -105,7 +137,7 @@ describe('Customer relations contacts root app', () => {
const issueLink = findIssuesLinks().at(0);
expect(issueLink.exists()).toBe(true);
- expect(issueLink.attributes('href')).toBe('/issues?crm_contact_id=16');
+ expect(issueLink.attributes('href')).toBe('/issues?crm_contact_id=12');
});
});
});
diff --git a/spec/frontend/crm/form_spec.js b/spec/frontend/crm/form_spec.js
index d39f0795f5f..f0e9150cada 100644
--- a/spec/frontend/crm/form_spec.js
+++ b/spec/frontend/crm/form_spec.js
@@ -1,4 +1,4 @@
-import { GlAlert, GlFormInput, GlFormSelect, GlFormGroup } from '@gitlab/ui';
+import { GlAlert, GlFormCheckbox, GlFormInput, GlFormSelect, GlFormGroup } from '@gitlab/ui';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import VueRouter from 'vue-router';
@@ -78,6 +78,7 @@ describe('Reusable form component', () => {
const findSaveButton = () => wrapper.findByTestId('save-button');
const findForm = () => wrapper.find('form');
const findError = () => wrapper.findComponent(GlAlert);
+ const findFormGroup = (at) => wrapper.findAllComponents(GlFormGroup).at(at);
const mountComponent = (propsData) => {
wrapper = shallowMountExtended(Form, {
@@ -92,7 +93,7 @@ describe('Reusable form component', () => {
});
};
- const mountContact = ({ propsData } = {}) => {
+ const mountContact = ({ propsData, extraFields = [] } = {}) => {
mountComponent({
fields: [
{ name: 'firstName', label: 'First name', required: true },
@@ -108,6 +109,7 @@ describe('Reusable form component', () => {
{ key: 'gid://gitlab/CustomerRelations::Organization/2', value: 'ABC Corp' },
],
},
+ ...extraFields,
],
getQuery: {
query: getGroupContactsQuery,
@@ -136,7 +138,8 @@ describe('Reusable form component', () => {
mutation: updateContactMutation,
existingId: 'gid://gitlab/CustomerRelations::Contact/12',
};
- mountContact({ propsData });
+ const extraFields = [{ name: 'active', label: 'Active', required: true, bool: true }];
+ mountContact({ propsData, extraFields });
};
const mountOrganization = ({ propsData } = {}) => {
@@ -285,18 +288,16 @@ describe('Reusable form component', () => {
});
it.each`
- index | id | componentName | value
- ${0} | ${'firstName'} | ${'GlFormInput'} | ${'Marty'}
- ${1} | ${'lastName'} | ${'GlFormInput'} | ${'McFly'}
- ${2} | ${'email'} | ${'GlFormInput'} | ${'example@gitlab.com'}
- ${4} | ${'description'} | ${'GlFormInput'} | ${undefined}
- ${3} | ${'phone'} | ${'GlFormInput'} | ${undefined}
- ${5} | ${'organizationId'} | ${'GlFormSelect'} | ${'gid://gitlab/CustomerRelations::Organization/2'}
+ index | id | component | value
+ ${0} | ${'firstName'} | ${GlFormInput} | ${'Marty'}
+ ${1} | ${'lastName'} | ${GlFormInput} | ${'McFly'}
+ ${2} | ${'email'} | ${GlFormInput} | ${'example@gitlab.com'}
+ ${4} | ${'description'} | ${GlFormInput} | ${undefined}
+ ${3} | ${'phone'} | ${GlFormInput} | ${undefined}
+ ${5} | ${'organizationId'} | ${GlFormSelect} | ${'gid://gitlab/CustomerRelations::Organization/2'}
`(
- 'should render a $componentName for #$id with the value "$value"',
- ({ index, id, componentName, value }) => {
- const component = componentName === 'GlFormInput' ? GlFormInput : GlFormSelect;
- const findFormGroup = (at) => wrapper.findAllComponents(GlFormGroup).at(at);
+ 'should render the correct component for #$id with the value "$value"',
+ ({ index, id, component, value }) => {
const findFormElement = () => findFormGroup(index).find(component);
expect(findFormElement().attributes('id')).toBe(id);
@@ -304,6 +305,14 @@ describe('Reusable form component', () => {
},
);
+ it('should render a checked GlFormCheckbox for #active', () => {
+ const activeCheckboxIndex = 6;
+ const findFormElement = () => findFormGroup(activeCheckboxIndex).find(GlFormCheckbox);
+
+ expect(findFormElement().attributes('id')).toBe('active');
+ expect(findFormElement().attributes('checked')).toBe('true');
+ });
+
it('should include updated values in update mutation', () => {
wrapper.find('#firstName').vm.$emit('input', 'Michael');
wrapper
@@ -314,6 +323,7 @@ describe('Reusable form component', () => {
expect(handler).toHaveBeenCalledWith('updateContact', {
input: {
+ active: true,
description: null,
email: 'example@gitlab.com',
firstName: 'Michael',
diff --git a/spec/frontend/crm/mock_data.js b/spec/frontend/crm/mock_data.js
index 35bc7fb69b4..a2e2e88ac60 100644
--- a/spec/frontend/crm/mock_data.js
+++ b/spec/frontend/crm/mock_data.js
@@ -13,6 +13,7 @@ export const getGroupContactsQueryResponse = {
email: 'example@gitlab.com',
phone: null,
description: null,
+ active: true,
organization: {
__typename: 'CustomerRelationsOrganization',
id: 'gid://gitlab/CustomerRelations::Organization/2',
@@ -27,6 +28,7 @@ export const getGroupContactsQueryResponse = {
email: null,
phone: null,
description: null,
+ active: true,
organization: null,
},
{
@@ -37,9 +39,32 @@ export const getGroupContactsQueryResponse = {
email: 'jd@gitlab.com',
phone: '+44 44 4444 4444',
description: 'Vice President',
+ active: true,
organization: null,
},
],
+ pageInfo: {
+ __typename: 'PageInfo',
+ hasNextPage: false,
+ endCursor: 'eyJsYXN0X25hbWUiOiJMZWRuZXIiLCJpZCI6IjE3OSJ9',
+ hasPreviousPage: false,
+ startCursor: 'eyJsYXN0X25hbWUiOiJCYXJ0b24iLCJpZCI6IjE5MyJ9',
+ },
+ },
+ },
+ },
+};
+
+export const getGroupContactsCountQueryResponse = {
+ data: {
+ group: {
+ __typename: 'Group',
+ id: 'gid://gitlab/Group/26',
+ contactStateCounts: {
+ all: 241,
+ active: 239,
+ inactive: 2,
+ __typename: 'ContactStateCountsType',
},
},
},
@@ -58,6 +83,7 @@ export const getGroupOrganizationsQueryResponse = {
name: 'Test Inc',
defaultRate: 100,
description: null,
+ active: true,
},
{
__typename: 'CustomerRelationsOrganization',
@@ -65,6 +91,7 @@ export const getGroupOrganizationsQueryResponse = {
name: 'ABC Company',
defaultRate: 110,
description: 'VIP',
+ active: true,
},
{
__typename: 'CustomerRelationsOrganization',
@@ -72,6 +99,7 @@ export const getGroupOrganizationsQueryResponse = {
name: 'GitLab',
defaultRate: 120,
description: null,
+ active: true,
},
],
},
@@ -91,6 +119,7 @@ export const createContactMutationResponse = {
phone: null,
description: null,
organization: null,
+ active: true,
},
errors: [],
},
@@ -119,6 +148,7 @@ export const updateContactMutationResponse = {
phone: null,
description: null,
organization: null,
+ active: true,
},
errors: [],
},
@@ -143,6 +173,7 @@ export const createOrganizationMutationResponse = {
name: 'A',
defaultRate: null,
description: null,
+ active: true,
},
errors: [],
},
@@ -168,6 +199,7 @@ export const updateOrganizationMutationResponse = {
name: 'A',
defaultRate: null,
description: null,
+ active: true,
},
errors: [],
},
diff --git a/spec/frontend/crm/organization_form_wrapper_spec.js b/spec/frontend/crm/organization_form_wrapper_spec.js
index 1a5a7c6ca5d..9f26b9157e6 100644
--- a/spec/frontend/crm/organization_form_wrapper_spec.js
+++ b/spec/frontend/crm/organization_form_wrapper_spec.js
@@ -49,7 +49,7 @@ describe('Customer relations organization form wrapper', () => {
mountComponent({ isEditMode: true });
const organizationForm = findOrganizationForm();
- expect(organizationForm.props('fields')).toHaveLength(3);
+ expect(organizationForm.props('fields')).toHaveLength(4);
expect(organizationForm.props('title')).toBe('Edit organization');
expect(organizationForm.props('successMessage')).toBe('Organization has been updated.');
expect(organizationForm.props('mutation')).toBe(updateOrganizationMutation);
diff --git a/spec/frontend/cycle_analytics/base_spec.js b/spec/frontend/cycle_analytics/base_spec.js
index 7b1ef71da63..ea3da86c7b2 100644
--- a/spec/frontend/cycle_analytics/base_spec.js
+++ b/spec/frontend/cycle_analytics/base_spec.js
@@ -11,7 +11,6 @@ import ValueStreamFilters from '~/cycle_analytics/components/value_stream_filter
import { NOT_ENOUGH_DATA_ERROR } from '~/cycle_analytics/constants';
import initState from '~/cycle_analytics/store/state';
import {
- permissions,
transformedProjectStagePathData,
selectedStage,
issueEvents,
@@ -34,7 +33,6 @@ let wrapper;
const { id: groupId, path: groupPath } = currentGroup;
const defaultState = {
- permissions,
currentGroup,
createdBefore,
createdAfter,
@@ -240,24 +238,6 @@ describe('Value stream analytics component', () => {
});
});
- describe('without enough permissions', () => {
- beforeEach(() => {
- wrapper = createComponent({
- initialState: {
- selectedStage,
- permissions: {
- ...permissions,
- [selectedStage.id]: false,
- },
- },
- });
- });
-
- it('renders the empty stage with `You need permission.` message', () => {
- expect(findEmptyStageTitle()).toBe('You need permission.');
- });
- });
-
describe('without a selected stage', () => {
beforeEach(() => {
wrapper = createComponent({
diff --git a/spec/frontend/cycle_analytics/mock_data.js b/spec/frontend/cycle_analytics/mock_data.js
index 1fe1dbbb75c..02666260cdb 100644
--- a/spec/frontend/cycle_analytics/mock_data.js
+++ b/spec/frontend/cycle_analytics/mock_data.js
@@ -101,30 +101,12 @@ export const selectedStage = {
...issueStage,
value: null,
active: false,
- isUserAllowed: true,
emptyStageText:
'The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage.',
slug: 'issue',
};
-export const stats = [issueStage, planStage, codeStage, testStage, reviewStage, stagingStage];
-
-export const permissions = {
- issue: true,
- plan: true,
- code: true,
- test: true,
- review: true,
- staging: true,
-};
-
-export const rawData = {
- summary,
- stats,
- permissions,
-};
-
export const convertedData = {
summary: [
{ value: '20', title: 'New Issues' },
diff --git a/spec/frontend/cycle_analytics/store/actions_spec.js b/spec/frontend/cycle_analytics/store/actions_spec.js
index e775e941b4c..94b6de85a5c 100644
--- a/spec/frontend/cycle_analytics/store/actions_spec.js
+++ b/spec/frontend/cycle_analytics/store/actions_spec.js
@@ -153,6 +153,19 @@ describe('Project Value Stream Analytics actions', () => {
});
});
});
+
+ describe('with no value stream stages available', () => {
+ it('will return SET_NO_ACCESS_ERROR', () => {
+ state = { ...state, stages: [] };
+ testAction({
+ action: actions.setInitialStage,
+ state,
+ payload: null,
+ expectedMutations: [{ type: 'SET_NO_ACCESS_ERROR' }],
+ expectedActions: [],
+ });
+ });
+ });
});
describe('updateStageTablePagination', () => {
@@ -170,46 +183,6 @@ describe('Project Value Stream Analytics actions', () => {
});
});
- describe('fetchCycleAnalyticsData', () => {
- beforeEach(() => {
- state = { ...defaultState, endpoints: mockEndpoints };
- mock = new MockAdapter(axios);
- mock.onGet(mockRequestPath).reply(httpStatusCodes.OK);
- });
-
- it(`dispatches the 'setSelectedStage' and 'fetchStageData' actions`, () =>
- testAction({
- action: actions.fetchCycleAnalyticsData,
- state,
- payload: {},
- expectedMutations: [
- { type: 'REQUEST_CYCLE_ANALYTICS_DATA' },
- { type: 'RECEIVE_CYCLE_ANALYTICS_DATA_SUCCESS' },
- ],
- expectedActions: [],
- }));
-
- describe('with a failing request', () => {
- beforeEach(() => {
- state = { endpoints: mockEndpoints };
- mock = new MockAdapter(axios);
- mock.onGet(mockRequestPath).reply(httpStatusCodes.BAD_REQUEST);
- });
-
- it(`commits the 'RECEIVE_CYCLE_ANALYTICS_DATA_ERROR' mutation`, () =>
- testAction({
- action: actions.fetchCycleAnalyticsData,
- state,
- payload: {},
- expectedMutations: [
- { type: 'REQUEST_CYCLE_ANALYTICS_DATA' },
- { type: 'RECEIVE_CYCLE_ANALYTICS_DATA_ERROR' },
- ],
- expectedActions: [],
- }));
- });
- });
-
describe('fetchStageData', () => {
const mockStagePath = /value_streams\/\w+\/stages\/\w+\/records/;
const headers = {
@@ -529,14 +502,13 @@ describe('Project Value Stream Analytics actions', () => {
});
describe('fetchValueStreamStageData', () => {
- it('will dispatch the fetchCycleAnalyticsData, fetchStageData, fetchStageMedians and fetchStageCountValues actions', () =>
+ it('will dispatch the fetchStageData, fetchStageMedians and fetchStageCountValues actions', () =>
testAction({
action: actions.fetchValueStreamStageData,
state,
payload: {},
expectedMutations: [],
expectedActions: [
- { type: 'fetchCycleAnalyticsData' },
{ type: 'fetchStageData' },
{ type: 'fetchStageMedians' },
{ type: 'fetchStageCountValues' },
diff --git a/spec/frontend/cycle_analytics/store/mutations_spec.js b/spec/frontend/cycle_analytics/store/mutations_spec.js
index 2670a390e9c..2e9e5d91471 100644
--- a/spec/frontend/cycle_analytics/store/mutations_spec.js
+++ b/spec/frontend/cycle_analytics/store/mutations_spec.js
@@ -38,31 +38,24 @@ describe('Project Value Stream Analytics mutations', () => {
});
it.each`
- mutation | stateKey | value
- ${types.REQUEST_VALUE_STREAMS} | ${'valueStreams'} | ${[]}
- ${types.RECEIVE_VALUE_STREAMS_ERROR} | ${'valueStreams'} | ${[]}
- ${types.REQUEST_VALUE_STREAM_STAGES} | ${'stages'} | ${[]}
- ${types.RECEIVE_VALUE_STREAM_STAGES_ERROR} | ${'stages'} | ${[]}
- ${types.REQUEST_CYCLE_ANALYTICS_DATA} | ${'isLoading'} | ${true}
- ${types.REQUEST_CYCLE_ANALYTICS_DATA} | ${'hasError'} | ${false}
- ${types.RECEIVE_CYCLE_ANALYTICS_DATA_SUCCESS} | ${'hasError'} | ${false}
- ${types.RECEIVE_CYCLE_ANALYTICS_DATA_ERROR} | ${'isLoading'} | ${false}
- ${types.RECEIVE_CYCLE_ANALYTICS_DATA_ERROR} | ${'hasError'} | ${true}
- ${types.REQUEST_STAGE_DATA} | ${'isLoadingStage'} | ${true}
- ${types.REQUEST_STAGE_DATA} | ${'isEmptyStage'} | ${false}
- ${types.REQUEST_STAGE_DATA} | ${'hasError'} | ${false}
- ${types.REQUEST_STAGE_DATA} | ${'selectedStageEvents'} | ${[]}
- ${types.RECEIVE_STAGE_DATA_SUCCESS} | ${'isLoadingStage'} | ${false}
- ${types.RECEIVE_STAGE_DATA_SUCCESS} | ${'selectedStageEvents'} | ${[]}
- ${types.RECEIVE_STAGE_DATA_SUCCESS} | ${'hasError'} | ${false}
- ${types.RECEIVE_STAGE_DATA_ERROR} | ${'isLoadingStage'} | ${false}
- ${types.RECEIVE_STAGE_DATA_ERROR} | ${'selectedStageEvents'} | ${[]}
- ${types.RECEIVE_STAGE_DATA_ERROR} | ${'hasError'} | ${true}
- ${types.RECEIVE_STAGE_DATA_ERROR} | ${'isEmptyStage'} | ${true}
- ${types.REQUEST_STAGE_MEDIANS} | ${'medians'} | ${{}}
- ${types.RECEIVE_STAGE_MEDIANS_ERROR} | ${'medians'} | ${{}}
- ${types.REQUEST_STAGE_COUNTS} | ${'stageCounts'} | ${{}}
- ${types.RECEIVE_STAGE_COUNTS_ERROR} | ${'stageCounts'} | ${{}}
+ mutation | stateKey | value
+ ${types.REQUEST_VALUE_STREAMS} | ${'valueStreams'} | ${[]}
+ ${types.RECEIVE_VALUE_STREAMS_ERROR} | ${'valueStreams'} | ${[]}
+ ${types.REQUEST_VALUE_STREAM_STAGES} | ${'stages'} | ${[]}
+ ${types.RECEIVE_VALUE_STREAM_STAGES_ERROR} | ${'stages'} | ${[]}
+ ${types.REQUEST_STAGE_DATA} | ${'isLoadingStage'} | ${true}
+ ${types.REQUEST_STAGE_DATA} | ${'isEmptyStage'} | ${false}
+ ${types.REQUEST_STAGE_DATA} | ${'selectedStageEvents'} | ${[]}
+ ${types.RECEIVE_STAGE_DATA_SUCCESS} | ${'isLoadingStage'} | ${false}
+ ${types.RECEIVE_STAGE_DATA_SUCCESS} | ${'selectedStageEvents'} | ${[]}
+ ${types.RECEIVE_STAGE_DATA_ERROR} | ${'isLoadingStage'} | ${false}
+ ${types.RECEIVE_STAGE_DATA_ERROR} | ${'selectedStageEvents'} | ${[]}
+ ${types.RECEIVE_STAGE_DATA_ERROR} | ${'isEmptyStage'} | ${true}
+ ${types.REQUEST_STAGE_MEDIANS} | ${'medians'} | ${{}}
+ ${types.RECEIVE_STAGE_MEDIANS_ERROR} | ${'medians'} | ${{}}
+ ${types.REQUEST_STAGE_COUNTS} | ${'stageCounts'} | ${{}}
+ ${types.RECEIVE_STAGE_COUNTS_ERROR} | ${'stageCounts'} | ${{}}
+ ${types.SET_NO_ACCESS_ERROR} | ${'hasNoAccessError'} | ${true}
`('$mutation will set $stateKey to $value', ({ mutation, stateKey, value }) => {
mutations[mutation](state);
diff --git a/spec/frontend/design_management/components/delete_button_spec.js b/spec/frontend/design_management/components/delete_button_spec.js
index e3907fdbe15..cee1eec792d 100644
--- a/spec/frontend/design_management/components/delete_button_spec.js
+++ b/spec/frontend/design_management/components/delete_button_spec.js
@@ -6,8 +6,8 @@ import BatchDeleteButton from '~/design_management/components/delete_button.vue'
describe('Batch delete button component', () => {
let wrapper;
- const findButton = () => wrapper.find(GlButton);
- const findModal = () => wrapper.find(GlModal);
+ const findButton = () => wrapper.findComponent(GlButton);
+ const findModal = () => wrapper.findComponent(GlModal);
function createComponent({ isDeleting = false } = {}, { slots = {} } = {}) {
wrapper = shallowMount(BatchDeleteButton, {
diff --git a/spec/frontend/design_management/components/design_notes/design_discussion_spec.js b/spec/frontend/design_management/components/design_notes/design_discussion_spec.js
index 77935fbde11..2091e1e08dd 100644
--- a/spec/frontend/design_management/components/design_notes/design_discussion_spec.js
+++ b/spec/frontend/design_management/components/design_notes/design_discussion_spec.js
@@ -26,13 +26,13 @@ describe('Design discussions component', () => {
const originalGon = window.gon;
let wrapper;
- const findDesignNotes = () => wrapper.findAll(DesignNote);
- const findReplyPlaceholder = () => wrapper.find(ReplyPlaceholder);
- const findReplyForm = () => wrapper.find(DesignReplyForm);
- const findRepliesWidget = () => wrapper.find(ToggleRepliesWidget);
+ const findDesignNotes = () => wrapper.findAllComponents(DesignNote);
+ const findReplyPlaceholder = () => wrapper.findComponent(ReplyPlaceholder);
+ const findReplyForm = () => wrapper.findComponent(DesignReplyForm);
+ const findRepliesWidget = () => wrapper.findComponent(ToggleRepliesWidget);
const findResolveButton = () => wrapper.find('[data-testid="resolve-button"]');
const findResolvedMessage = () => wrapper.find('[data-testid="resolved-message"]');
- const findResolveLoadingIcon = () => wrapper.find(GlLoadingIcon);
+ const findResolveLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findResolveCheckbox = () => wrapper.find('[data-testid="resolve-checkbox"]');
const findApolloMutation = () => wrapper.findComponent(ApolloMutation);
@@ -307,7 +307,7 @@ describe('Design discussions component', () => {
expect(
wrapper
- .findAll(DesignNote)
+ .findAllComponents(DesignNote)
.wrappers.every((designNote) => designNote.classes('gl-bg-blue-50')),
).toBe(true);
},
@@ -351,7 +351,7 @@ describe('Design discussions component', () => {
createComponent();
findReplyPlaceholder().vm.$emit('focus');
- expect(wrapper.emitted('open-form')).toBeTruthy();
+ expect(wrapper.emitted('open-form')).toHaveLength(1);
});
describe('when user is not logged in', () => {
diff --git a/spec/frontend/design_management/components/design_notes/design_note_spec.js b/spec/frontend/design_management/components/design_notes/design_note_spec.js
index 1f84fde9f7f..28833b4af5c 100644
--- a/spec/frontend/design_management/components/design_notes/design_note_spec.js
+++ b/spec/frontend/design_management/components/design_notes/design_note_spec.js
@@ -100,7 +100,7 @@ describe('Design note component', () => {
note,
});
- expect(wrapper.find(TimeAgoTooltip).exists()).toBe(true);
+ expect(wrapper.findComponent(TimeAgoTooltip).exists()).toBe(true);
});
it('should not render edit icon when user does not have a permission', () => {
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 d2d1fe6b2d8..f7ce742b933 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
@@ -15,9 +15,9 @@ describe('Design reply form component', () => {
let wrapper;
const findTextarea = () => wrapper.find('textarea');
- const findSubmitButton = () => wrapper.find({ ref: 'submitButton' });
- const findCancelButton = () => wrapper.find({ ref: 'cancelButton' });
- const findModal = () => wrapper.find({ ref: 'cancelCommentModal' });
+ 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, {
@@ -42,6 +42,18 @@ describe('Design reply form component', () => {
expect(findTextarea().element).toEqual(document.activeElement);
});
+ it('renders "Attach a file or image" button in markdown toolbar', () => {
+ createComponent();
+
+ expect(wrapper.find('[data-testid="button-attach-file"]').exists()).toBe(true);
+ });
+
+ it('renders file upload progress container', () => {
+ createComponent();
+
+ expect(wrapper.find('.comment-toolbar .uploading-container').exists()).toBe(true);
+ });
+
it('renders button text as "Comment" when creating a comment', () => {
createComponent();
diff --git a/spec/frontend/design_management/components/design_notes/toggle_replies_widget_spec.js b/spec/frontend/design_management/components/design_notes/toggle_replies_widget_spec.js
index f87228663b6..41129e2b58d 100644
--- a/spec/frontend/design_management/components/design_notes/toggle_replies_widget_spec.js
+++ b/spec/frontend/design_management/components/design_notes/toggle_replies_widget_spec.js
@@ -8,10 +8,10 @@ describe('Toggle replies widget component', () => {
let wrapper;
const findToggleWrapper = () => wrapper.find('[data-testid="toggle-comments-wrapper"]');
- const findIcon = () => wrapper.find(GlIcon);
- const findButton = () => wrapper.find(GlButton);
- const findAuthorLink = () => wrapper.find(GlLink);
- const findTimeAgo = () => wrapper.find(TimeAgoTooltip);
+ const findIcon = () => wrapper.findComponent(GlIcon);
+ const findButton = () => wrapper.findComponent(GlButton);
+ const findAuthorLink = () => wrapper.findComponent(GlLink);
+ const findTimeAgo = () => wrapper.findComponent(TimeAgoTooltip);
function createComponent(props = {}) {
wrapper = shallowMount(ToggleRepliesWidget, {
diff --git a/spec/frontend/design_management/components/design_scaler_spec.js b/spec/frontend/design_management/components/design_scaler_spec.js
index a04e2ebda5b..e1a66cea329 100644
--- a/spec/frontend/design_management/components/design_scaler_spec.js
+++ b/spec/frontend/design_management/components/design_scaler_spec.js
@@ -6,7 +6,7 @@ import DesignScaler from '~/design_management/components/design_scaler.vue';
describe('Design management design scaler component', () => {
let wrapper;
- const getButtons = () => wrapper.findAll(GlButton);
+ const getButtons = () => wrapper.findAllComponents(GlButton);
const getDecreaseScaleButton = () => getButtons().at(0);
const getResetScaleButton = () => getButtons().at(1);
const getIncreaseScaleButton = () => getButtons().at(2);
diff --git a/spec/frontend/design_management/components/design_sidebar_spec.js b/spec/frontend/design_management/components/design_sidebar_spec.js
index f13796138bd..af995f75ddc 100644
--- a/spec/frontend/design_management/components/design_sidebar_spec.js
+++ b/spec/frontend/design_management/components/design_sidebar_spec.js
@@ -32,12 +32,12 @@ describe('Design management design sidebar component', () => {
const originalGon = window.gon;
let wrapper;
- const findDiscussions = () => wrapper.findAll(DesignDiscussion);
+ const findDiscussions = () => wrapper.findAllComponents(DesignDiscussion);
const findFirstDiscussion = () => findDiscussions().at(0);
const findUnresolvedDiscussions = () => wrapper.findAll('[data-testid="unresolved-discussion"]');
const findResolvedDiscussions = () => wrapper.findAll('[data-testid="resolved-discussion"]');
- const findParticipants = () => wrapper.find(Participants);
- const findResolvedCommentsToggle = () => wrapper.find(GlAccordionItem);
+ const findParticipants = () => wrapper.findComponent(Participants);
+ const findResolvedCommentsToggle = () => wrapper.findComponent(GlAccordionItem);
const findNewDiscussionDisclaimer = () =>
wrapper.find('[data-testid="new-discussion-disclaimer"]');
@@ -87,7 +87,7 @@ describe('Design management design sidebar component', () => {
it('renders To-Do button', () => {
createComponent();
- expect(wrapper.find(DesignTodoButton).exists()).toBe(true);
+ expect(wrapper.findComponent(DesignTodoButton).exists()).toBe(true);
});
describe('when has no discussions', () => {
diff --git a/spec/frontend/design_management/components/design_todo_button_spec.js b/spec/frontend/design_management/components/design_todo_button_spec.js
index 73661c9fcb0..b3afcefe1ed 100644
--- a/spec/frontend/design_management/components/design_todo_button_spec.js
+++ b/spec/frontend/design_management/components/design_todo_button_spec.js
@@ -57,7 +57,7 @@ describe('Design management design todo button', () => {
});
it('renders TodoButton component', () => {
- expect(wrapper.find(TodoButton).exists()).toBe(true);
+ expect(wrapper.findComponent(TodoButton).exists()).toBe(true);
});
describe('when design has a pending todo', () => {
diff --git a/spec/frontend/design_management/components/image_spec.js b/spec/frontend/design_management/components/image_spec.js
index 65ee0ae6238..8163cb0d87a 100644
--- a/spec/frontend/design_management/components/image_spec.js
+++ b/spec/frontend/design_management/components/image_spec.js
@@ -71,7 +71,7 @@ describe('Design management large image component', () => {
image.trigger('error');
await nextTick();
expect(image.isVisible()).toBe(false);
- expect(wrapper.find(GlIcon).element).toMatchSnapshot();
+ expect(wrapper.findComponent(GlIcon).element).toMatchSnapshot();
});
describe('zoom', () => {
diff --git a/spec/frontend/design_management/components/list/item_spec.js b/spec/frontend/design_management/components/list/item_spec.js
index e00dda2015e..66d3f883960 100644
--- a/spec/frontend/design_management/components/list/item_spec.js
+++ b/spec/frontend/design_management/components/list/item_spec.js
@@ -23,8 +23,8 @@ describe('Design management list item component', () => {
const findDesignEvent = () => wrapper.findByTestId('design-event');
const findImgFilename = (id = imgId) => wrapper.findByTestId(`design-img-filename-${id}`);
- const findEventIcon = () => findDesignEvent().find(GlIcon);
- const findLoadingIcon = () => wrapper.find(GlLoadingIcon);
+ const findEventIcon = () => findDesignEvent().findComponent(GlIcon);
+ const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
function createComponent({
notesCount = 0,
@@ -74,7 +74,7 @@ describe('Design management list item component', () => {
beforeEach(async () => {
createComponent();
image = wrapper.find('img');
- glIntersectionObserver = wrapper.find(GlIntersectionObserver);
+ glIntersectionObserver = wrapper.findComponent(GlIntersectionObserver);
glIntersectionObserver.vm.$emit('appear');
await nextTick();
@@ -86,7 +86,7 @@ describe('Design management list item component', () => {
describe('before image is loaded', () => {
it('renders loading spinner', () => {
- expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
+ expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
});
});
@@ -105,7 +105,7 @@ describe('Design management list item component', () => {
image.trigger('error');
await nextTick();
expect(image.isVisible()).toBe(false);
- expect(wrapper.find(GlIcon).element).toMatchSnapshot();
+ expect(wrapper.findComponent(GlIcon).element).toMatchSnapshot();
});
describe('when imageV432x230 and image provided', () => {
diff --git a/spec/frontend/design_management/components/toolbar/index_spec.js b/spec/frontend/design_management/components/toolbar/index_spec.js
index 412f3de911e..b6137ba2eee 100644
--- a/spec/frontend/design_management/components/toolbar/index_spec.js
+++ b/spec/frontend/design_management/components/toolbar/index_spec.js
@@ -85,35 +85,35 @@ describe('Design management toolbar component', () => {
createComponent();
await nextTick();
- expect(wrapper.find(DeleteButton).exists()).toBe(true);
+ expect(wrapper.findComponent(DeleteButton).exists()).toBe(true);
});
it('does not render delete button on non-latest version', async () => {
createComponent(false, true, { isLatestVersion: false });
await nextTick();
- expect(wrapper.find(DeleteButton).exists()).toBe(false);
+ expect(wrapper.findComponent(DeleteButton).exists()).toBe(false);
});
it('does not render delete button when user is not logged in', async () => {
createComponent(false, false);
await nextTick();
- expect(wrapper.find(DeleteButton).exists()).toBe(false);
+ expect(wrapper.findComponent(DeleteButton).exists()).toBe(false);
});
it('emits `delete` event on deleteButton `delete-selected-designs` event', async () => {
createComponent();
await nextTick();
- wrapper.find(DeleteButton).vm.$emit('delete-selected-designs');
+ wrapper.findComponent(DeleteButton).vm.$emit('delete-selected-designs');
expect(wrapper.emitted().delete).toBeTruthy();
});
it('renders download button with correct link', () => {
createComponent();
- expect(wrapper.find(GlButton).attributes('href')).toBe(
+ expect(wrapper.findComponent(GlButton).attributes('href')).toBe(
'/-/designs/306/7f747adcd4693afadbe968d7ba7d983349b9012d',
);
});
diff --git a/spec/frontend/design_management/components/upload/button_spec.js b/spec/frontend/design_management/components/upload/button_spec.js
index d123db43ce6..59821218ab8 100644
--- a/spec/frontend/design_management/components/upload/button_spec.js
+++ b/spec/frontend/design_management/components/upload/button_spec.js
@@ -34,7 +34,7 @@ describe('Design management upload button component', () => {
it('Button `loading` prop is `true`', () => {
createComponent({ isSaving: true });
- const button = wrapper.find(GlButton);
+ const button = wrapper.findComponent(GlButton);
expect(button.exists()).toBe(true);
expect(button.props('loading')).toBe(true);
});
diff --git a/spec/frontend/design_management/components/upload/design_version_dropdown_spec.js b/spec/frontend/design_management/components/upload/design_version_dropdown_spec.js
index ec5db04bb80..7c26ab9739b 100644
--- a/spec/frontend/design_management/components/upload/design_version_dropdown_spec.js
+++ b/spec/frontend/design_management/components/upload/design_version_dropdown_spec.js
@@ -46,7 +46,7 @@ describe('Design management design version dropdown component', () => {
wrapper.destroy();
});
- const findVersionLink = (index) => wrapper.findAll(GlDropdownItem).at(index);
+ const findVersionLink = (index) => wrapper.findAllComponents(GlDropdownItem).at(index);
it('renders design version dropdown button', async () => {
createComponent();
@@ -76,35 +76,35 @@ describe('Design management design version dropdown component', () => {
createComponent();
await nextTick();
- expect(wrapper.find(GlDropdown).attributes('text')).toBe('Showing latest version');
+ expect(wrapper.findComponent(GlDropdown).attributes('text')).toBe('Showing latest version');
});
it('displays latest version text when only 1 version is present', async () => {
createComponent({ maxVersions: 1 });
await nextTick();
- expect(wrapper.find(GlDropdown).attributes('text')).toBe('Showing latest version');
+ expect(wrapper.findComponent(GlDropdown).attributes('text')).toBe('Showing latest version');
});
it('displays version text when the current version is not the latest', async () => {
createComponent({ $route: designRouteFactory(PREVIOUS_VERSION_ID) });
await nextTick();
- expect(wrapper.find(GlDropdown).attributes('text')).toBe(`Showing version #1`);
+ expect(wrapper.findComponent(GlDropdown).attributes('text')).toBe(`Showing version #1`);
});
it('displays latest version text when the current version is the latest', async () => {
createComponent({ $route: designRouteFactory(LATEST_VERSION_ID) });
await nextTick();
- expect(wrapper.find(GlDropdown).attributes('text')).toBe('Showing latest version');
+ expect(wrapper.findComponent(GlDropdown).attributes('text')).toBe('Showing latest version');
});
it('should have the same length as apollo query', async () => {
createComponent();
await nextTick();
- expect(wrapper.findAll(GlDropdownItem)).toHaveLength(wrapper.vm.allVersions.length);
+ expect(wrapper.findAllComponents(GlDropdownItem)).toHaveLength(wrapper.vm.allVersions.length);
});
it('should render TimeAgo', async () => {
diff --git a/spec/frontend/design_management/pages/design/index_spec.js b/spec/frontend/design_management/pages/design/index_spec.js
index 17a299c5de1..774e37a8b21 100644
--- a/spec/frontend/design_management/pages/design/index_spec.js
+++ b/spec/frontend/design_management/pages/design/index_spec.js
@@ -85,9 +85,9 @@ describe('Design management design index page', () => {
let wrapper;
let router;
- const findDiscussionForm = () => wrapper.find(DesignReplyForm);
- const findSidebar = () => wrapper.find(DesignSidebar);
- const findDesignPresentation = () => wrapper.find(DesignPresentation);
+ const findDiscussionForm = () => wrapper.findComponent(DesignReplyForm);
+ const findSidebar = () => wrapper.findComponent(DesignSidebar);
+ const findDesignPresentation = () => wrapper.findComponent(DesignPresentation);
function createComponent(
{ loading = false } = {},
@@ -181,15 +181,15 @@ describe('Design management design index page', () => {
it('sets loading state', () => {
createComponent({ loading: true });
- expect(wrapper.find(DesignPresentation).props('isLoading')).toBe(true);
- expect(wrapper.find(DesignSidebar).props('isLoading')).toBe(true);
+ expect(wrapper.findComponent(DesignPresentation).props('isLoading')).toBe(true);
+ expect(wrapper.findComponent(DesignSidebar).props('isLoading')).toBe(true);
});
it('renders design index', () => {
createComponent({ loading: false }, { data: { design } });
expect(wrapper.element).toMatchSnapshot();
- expect(wrapper.find(GlAlert).exists()).toBe(false);
+ expect(wrapper.findComponent(GlAlert).exists()).toBe(false);
});
it('passes correct props to sidebar component', () => {
diff --git a/spec/frontend/design_management/pages/index_spec.js b/spec/frontend/design_management/pages/index_spec.js
index 21be7bd148b..f90feaadfb0 100644
--- a/spec/frontend/design_management/pages/index_spec.js
+++ b/spec/frontend/design_management/pages/index_spec.js
@@ -111,8 +111,8 @@ describe('Design management index page', () => {
const findDropzoneWrapper = () => wrapper.findByTestId('design-dropzone-wrapper');
const findFirstDropzoneWithDesign = () => wrapper.findAllComponents(DesignDropzone).at(1);
const findDesignsWrapper = () => wrapper.findByTestId('designs-root');
- const findDesigns = () => wrapper.findAll(Design);
- const draggableAttributes = () => wrapper.find(VueDraggable).vm.$attrs;
+ const findDesigns = () => wrapper.findAllComponents(Design);
+ const draggableAttributes = () => wrapper.findComponent(VueDraggable).vm.$attrs;
const findDesignUploadButton = () => wrapper.findByTestId('design-upload-button');
const findDesignToolbarWrapper = () => wrapper.findByTestId('design-toolbar-wrapper');
const findDesignUpdateAlert = () => wrapper.findByTestId('design-update-alert');
@@ -120,8 +120,8 @@ describe('Design management index page', () => {
async function moveDesigns(localWrapper) {
await waitForPromises();
- localWrapper.find(VueDraggable).vm.$emit('input', reorderedDesigns);
- localWrapper.find(VueDraggable).vm.$emit('change', {
+ localWrapper.findComponent(VueDraggable).vm.$emit('input', reorderedDesigns);
+ localWrapper.findComponent(VueDraggable).vm.$emit('change', {
moved: {
newIndex: 0,
element: designToMove,
@@ -369,7 +369,7 @@ describe('Design management index page', () => {
findDropzone().vm.$emit('change', [{ name: 'test' }]);
expect(mutate).toHaveBeenCalledWith(mutationVariables);
expect(wrapper.vm.filesToBeSaved).toEqual([{ name: 'test' }]);
- expect(wrapper.vm.isSaving).toBeTruthy();
+ expect(wrapper.vm.isSaving).toBe(true);
expect(dropzoneClasses()).toContain('design-list-item');
expect(dropzoneClasses()).toContain('design-list-item-new');
});
@@ -399,7 +399,7 @@ describe('Design management index page', () => {
await nextTick();
expect(wrapper.vm.filesToBeSaved).toEqual([]);
- expect(wrapper.vm.isSaving).toBeFalsy();
+ expect(wrapper.vm.isSaving).toBe(false);
expect(wrapper.vm.isLatestVersion).toBe(true);
});
@@ -412,7 +412,7 @@ describe('Design management index page', () => {
wrapper.vm.onUploadDesignError();
await nextTick();
expect(wrapper.vm.filesToBeSaved).toEqual([]);
- expect(wrapper.vm.isSaving).toBeFalsy();
+ expect(wrapper.vm.isSaving).toBe(false);
expect(findDesignUpdateAlert().exists()).toBe(true);
expect(findDesignUpdateAlert().text()).toBe(UPLOAD_DESIGN_ERROR);
});
diff --git a/spec/frontend/design_management/router_spec.js b/spec/frontend/design_management/router_spec.js
index b9c62334223..b9edde559c8 100644
--- a/spec/frontend/design_management/router_spec.js
+++ b/spec/frontend/design_management/router_spec.js
@@ -44,7 +44,7 @@ describe('Design management router', () => {
it('pushes home component', () => {
const wrapper = factory(routeArg);
- expect(wrapper.find(Designs).exists()).toBe(true);
+ expect(wrapper.findComponent(Designs).exists()).toBe(true);
});
});
@@ -55,7 +55,7 @@ describe('Design management router', () => {
const wrapper = factory(routeArg);
return nextTick().then(() => {
- const detail = wrapper.find(DesignDetail);
+ const detail = wrapper.findComponent(DesignDetail);
expect(detail.exists()).toBe(true);
expect(detail.props('id')).toEqual('1');
});
diff --git a/spec/frontend/diffs/components/diff_file_header_spec.js b/spec/frontend/diffs/components/diff_file_header_spec.js
index d90afeb6b82..92b8b2d4aa3 100644
--- a/spec/frontend/diffs/components/diff_file_header_spec.js
+++ b/spec/frontend/diffs/components/diff_file_header_spec.js
@@ -263,7 +263,7 @@ describe('DiffFileHeader component', () => {
},
},
});
- expect(findModeChangedLine().exists()).toBeFalsy();
+ expect(findModeChangedLine().exists()).toBe(false);
},
);
diff --git a/spec/frontend/diffs/components/diff_row_spec.js b/spec/frontend/diffs/components/diff_row_spec.js
index be81508213b..a74013dc2d4 100644
--- a/spec/frontend/diffs/components/diff_row_spec.js
+++ b/spec/frontend/diffs/components/diff_row_spec.js
@@ -239,7 +239,7 @@ describe('DiffRow', () => {
const coverage = wrapper.find('.line-coverage.right-side');
expect(coverage.attributes('title')).toContain('Test coverage: 5 hits');
- expect(coverage.classes('coverage')).toBeTruthy();
+ expect(coverage.classes('coverage')).toBe(true);
});
it('for lines without coverage', () => {
@@ -248,7 +248,7 @@ describe('DiffRow', () => {
const coverage = wrapper.find('.line-coverage.right-side');
expect(coverage.attributes('title')).toContain('No test coverage');
- expect(coverage.classes('no-coverage')).toBeTruthy();
+ expect(coverage.classes('no-coverage')).toBe(true);
});
it('for unknown lines', () => {
@@ -256,9 +256,9 @@ describe('DiffRow', () => {
wrapper = createWrapper({ props, state: { coverageFiles } });
const coverage = wrapper.find('.line-coverage.right-side');
- expect(coverage.attributes('title')).toBeFalsy();
- expect(coverage.classes('coverage')).toBeFalsy();
- expect(coverage.classes('no-coverage')).toBeFalsy();
+ expect(coverage.attributes('title')).toBeUndefined();
+ expect(coverage.classes('coverage')).toBe(false);
+ expect(coverage.classes('no-coverage')).toBe(false);
});
});
diff --git a/spec/frontend/diffs/store/utils_spec.js b/spec/frontend/diffs/store/utils_spec.js
index 8852c6c62c5..3f870a98396 100644
--- a/spec/frontend/diffs/store/utils_spec.js
+++ b/spec/frontend/diffs/store/utils_spec.js
@@ -424,8 +424,8 @@ describe('DiffsStoreUtils', () => {
expect(firstChar).not.toBe('+');
expect(firstChar).not.toBe('-');
- expect(preparedDiff.diff_files[0].renderIt).toBeTruthy();
- expect(preparedDiff.diff_files[0].collapsed).toBeFalsy();
+ expect(preparedDiff.diff_files[0].renderIt).toBe(true);
+ expect(preparedDiff.diff_files[0].collapsed).toBe(false);
});
it('guarantees an empty array for both diff styles', () => {
@@ -506,8 +506,8 @@ describe('DiffsStoreUtils', () => {
});
it('sets the renderIt and collapsed attribute on files', () => {
- expect(preparedDiffFiles[0].renderIt).toBeTruthy();
- expect(preparedDiffFiles[0].collapsed).toBeFalsy();
+ expect(preparedDiffFiles[0].renderIt).toBe(true);
+ expect(preparedDiffFiles[0].collapsed).toBeUndefined();
});
it('guarantees an empty array of lines for both diff styles', () => {
diff --git a/spec/frontend/dropzone_input_spec.js b/spec/frontend/dropzone_input_spec.js
index a633de9ef56..0fe70bac6b7 100644
--- a/spec/frontend/dropzone_input_spec.js
+++ b/spec/frontend/dropzone_input_spec.js
@@ -29,7 +29,9 @@ describe('dropzone_input', () => {
it('returns valid dropzone when successfully initialize', () => {
const dropzone = dropzoneInput($(TEMPLATE));
- expect(dropzone.version).toBeTruthy();
+ expect(dropzone).toMatchObject({
+ version: expect.any(String),
+ });
});
describe('handlePaste', () => {
diff --git a/spec/frontend/editor/schema/ci/ci_schema_spec.js b/spec/frontend/editor/schema/ci/ci_schema_spec.js
index c59806a5d60..c9010fbec0c 100644
--- a/spec/frontend/editor/schema/ci/ci_schema_spec.js
+++ b/spec/frontend/editor/schema/ci/ci_schema_spec.js
@@ -2,7 +2,7 @@ import Ajv from 'ajv';
import AjvFormats from 'ajv-formats';
import CiSchema from '~/editor/schema/ci.json';
-// JSON POSITIVE TESTS
+// JSON POSITIVE TESTS (LEGACY)
import AllowFailureJson from './json_tests/positive_tests/allow_failure.json';
import EnvironmentJson from './json_tests/positive_tests/environment.json';
import GitlabCiDependenciesJson from './json_tests/positive_tests/gitlab-ci-dependencies.json';
@@ -14,7 +14,7 @@ import TerraformReportJson from './json_tests/positive_tests/terraform_report.js
import VariablesMixStringAndUserInputJson from './json_tests/positive_tests/variables_mix_string_and_user_input.json';
import VariablesJson from './json_tests/positive_tests/variables.json';
-// JSON NEGATIVE TESTS
+// JSON NEGATIVE TESTS (LEGACY)
import DefaultNoAdditionalPropertiesJson from './json_tests/negative_tests/default_no_additional_properties.json';
import InheritDefaultNoAdditionalPropertiesJson from './json_tests/negative_tests/inherit_default_no_additional_properties.json';
import JobVariablesMustNotContainObjectsJson from './json_tests/negative_tests/job_variables_must_not_contain_objects.json';
@@ -24,14 +24,17 @@ import ReleaseAssetsLinksMissingJson from './json_tests/negative_tests/release_a
import RetryUnknownWhenJson from './json_tests/negative_tests/retry_unknown_when.json';
// YAML POSITIVE TEST
+import ArtifactsYaml from './yaml_tests/positive_tests/artifacts.yml';
import CacheYaml from './yaml_tests/positive_tests/cache.yml';
import FilterYaml from './yaml_tests/positive_tests/filter.yml';
import IncludeYaml from './yaml_tests/positive_tests/include.yml';
import RulesYaml from './yaml_tests/positive_tests/rules.yml';
// YAML NEGATIVE TEST
+import 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';
const ajv = new Ajv({
strictTypes: false,
@@ -59,6 +62,7 @@ describe('positive tests', () => {
VariablesJson,
// YAML
+ ArtifactsYaml,
CacheYaml,
FilterYaml,
IncludeYaml,
@@ -82,8 +86,10 @@ describe('negative tests', () => {
RetryUnknownWhenJson,
// YAML
+ ArtifactsNegativeYaml,
CacheNegativeYaml,
IncludeNegativeYaml,
+ RulesNegativeYaml,
}),
)('schema validates %s', (_, input) => {
expect(input).not.toValidateJsonSchema(schema);
diff --git a/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/artifacts.yml b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/artifacts.yml
new file mode 100644
index 00000000000..f5670376efc
--- /dev/null
+++ b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/artifacts.yml
@@ -0,0 +1,18 @@
+# invalid artifact:reports:cyclonedx
+
+cyclonedx no paths:
+ artifacts:
+ reports:
+ cyclonedx:
+
+cyclonedx not a report:
+ artifacts:
+ cyclonedx: foo
+
+cyclonedx not an array or string:
+ artifacts:
+ reports:
+ cyclonedx:
+ paths:
+ - foo
+ - bar
diff --git a/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/cache.yml b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/cache.yml
index ee533f54d3b..04020c06753 100644
--- a/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/cache.yml
+++ b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/cache.yml
@@ -1,15 +1,13 @@
-# Covers https://gitlab.com/gitlab-org/gitlab/-/merge_requests/70779
stages:
- prepare
-# invalid cache:when value
-job1:
+# invalid cache:when values
+when no integer:
stage: prepare
cache:
when: 0
-# invalid cache:when value
-job2:
+when must be a reserved word:
stage: prepare
cache:
when: 'never'
diff --git a/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/include.yml b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/include.yml
index 287150a765f..1e16bb55405 100644
--- a/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/include.yml
+++ b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/include.yml
@@ -1,16 +1,14 @@
-# Covers https://gitlab.com/gitlab-org/gitlab/-/merge_requests/70779
stages:
- prepare
-# missing file property
-childPipeline:
+# invalid trigger:include
+trigger missing file property:
stage: prepare
trigger:
include:
- project: 'my-group/my-pipeline-library'
-# missing project property
-childPipeline2:
+trigger missing project property:
stage: prepare
trigger:
include:
diff --git a/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/rules.yml b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/rules.yml
new file mode 100644
index 00000000000..d74a681b23b
--- /dev/null
+++ b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/rules.yml
@@ -0,0 +1,14 @@
+# invalid rules:changes
+unnecessary ref declaration:
+ script: exit 0
+ rules:
+ - changes:
+ paths:
+ - README.md
+ compare_to: { ref: 'main' }
+
+wrong path declaration:
+ script: exit 0
+ rules:
+ - changes:
+ paths: { file: 'DOCKER' }
diff --git a/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/artifacts.yml b/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/artifacts.yml
new file mode 100644
index 00000000000..20c1fc2c50f
--- /dev/null
+++ b/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/artifacts.yml
@@ -0,0 +1,25 @@
+# valid artifact:reports:cyclonedx
+
+cyclonedx string path:
+ artifacts:
+ reports:
+ cyclonedx: foo
+
+cyclonedx glob path:
+ artifacts:
+ reports:
+ cyclonedx: "*.foo"
+
+cylonedx list of string paths:
+ artifacts:
+ reports:
+ cyclonedx:
+ - foo
+ - ./bar/baz
+
+cylonedx mixed list of string paths and globs:
+ artifacts:
+ reports:
+ cyclonedx:
+ - ./foo
+ - "bar/*.baz"
diff --git a/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/cache.yml b/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/cache.yml
index 436c7d72699..d83e14fdc6a 100644
--- a/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/cache.yml
+++ b/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/cache.yml
@@ -1,8 +1,7 @@
-# Covers https://gitlab.com/gitlab-org/gitlab/-/merge_requests/70779
stages:
- prepare
-# test for cache:when values
+# valid cache:when values
job1:
stage: prepare
script:
diff --git a/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/filter.yml b/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/filter.yml
index 2b29c24fa3c..f82ea71dcf3 100644
--- a/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/filter.yml
+++ b/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/filter.yml
@@ -1,5 +1,5 @@
-# Covers https://gitlab.com/gitlab-org/gitlab/-/merge_requests/79335
-deploy-template:
+# valid only/except values
+only and except as array of strings:
script:
- echo "hello world"
only:
@@ -7,12 +7,10 @@ deploy-template:
except:
- bar
-# null value allowed
-deploy-without-only:
+only as null value:
extends: deploy-template
only:
-# null value allowed
-deploy-without-except:
+except as null value:
extends: deploy-template
except:
diff --git a/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/include.yml b/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/include.yml
index 3497be28058..c00ab0d464a 100644
--- a/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/include.yml
+++ b/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/include.yml
@@ -1,17 +1,15 @@
-# Covers https://gitlab.com/gitlab-org/gitlab/-/merge_requests/70779
+stages:
+ - prepare
-# test for include:rules
+# valid include:rules
include:
- local: builds.yml
rules:
- if: '$INCLUDE_BUILDS == "true"'
when: always
-stages:
- - prepare
-
-# test for trigger:include
-childPipeline:
+# valid trigger:include
+trigger:include accepts project and file properties:
stage: prepare
script:
- echo 'creating pipeline...'
@@ -20,8 +18,7 @@ childPipeline:
- project: 'my-group/my-pipeline-library'
file: '.gitlab-ci.yml'
-# accepts optional ref property
-childPipeline2:
+trigger:include accepts optional ref property:
stage: prepare
script:
- echo 'creating pipeline...'
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 27a199cff13..37cae6b4264 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
@@ -1,13 +1,28 @@
-# Covers https://gitlab.com/gitlab-org/gitlab/-/merge_requests/74164
+# valid workflow:rules:changes
+rules:changes with paths and compare_to properties:
+ script: exit 0
+ rules:
+ - changes:
+ paths:
+ - README.md
+ compare_to: main
+
+rules:changes as array of strings:
+ script: exit 0
+ rules:
+ - changes:
+ - README.md
-# test for workflow:rules:changes and workflow:rules:exists
+# valid workflow:rules:exists
+# valid rules:changes:path
workflow:
rules:
+ - changes:
+ paths:
+ - README.md
- if: '$CI_PIPELINE_SOURCE == "schedule"'
exists:
- Dockerfile
- changes:
- - Dockerfile
variables:
IS_A_FEATURE: 'true'
when: always
diff --git a/spec/frontend/editor/source_editor_instance_spec.js b/spec/frontend/editor/source_editor_instance_spec.js
index 99c4ff4f3fa..1223fee320e 100644
--- a/spec/frontend/editor/source_editor_instance_spec.js
+++ b/spec/frontend/editor/source_editor_instance_spec.js
@@ -423,7 +423,7 @@ describe('Source Editor Instance', () => {
'changes language of an attached model to "$expectedLanguage" when filepath is "$path"',
({ path, expectedLanguage }) => {
seInstance.updateModelLanguage(path);
- expect(instanceModel.getLanguageIdentifier().language).toBe(expectedLanguage);
+ expect(instanceModel.getLanguageId()).toBe(expectedLanguage);
},
);
});
diff --git a/spec/frontend/editor/source_editor_spec.js b/spec/frontend/editor/source_editor_spec.js
index 74aae7b899b..6a8e7b296aa 100644
--- a/spec/frontend/editor/source_editor_spec.js
+++ b/spec/frontend/editor/source_editor_spec.js
@@ -267,7 +267,6 @@ describe('Base editor', () => {
let editorEl2;
let inst1;
let inst2;
- const readOnlyIndex = '78'; // readOnly option has the internal index of 78 in the editor's options
beforeEach(() => {
setHTMLFixture('<div id="editor1"></div><div id="editor2"></div>');
@@ -331,10 +330,10 @@ describe('Base editor', () => {
});
inst1 = editor.createInstance(inst1Args);
- expect(inst1.getOption(readOnlyIndex)).toBe(true);
+ expect(inst1.getRawOptions().readOnly).toBe(true);
inst2 = editor.createInstance(inst2Args);
- expect(inst2.getOption(readOnlyIndex)).toBe(true);
+ expect(inst2.getRawOptions().readOnly).toBe(true);
});
it('allows overriding editor options on the instance level', () => {
@@ -346,7 +345,7 @@ describe('Base editor', () => {
readOnly: false,
});
- expect(inst1.getOption(readOnlyIndex)).toBe(false);
+ expect(inst1.getRawOptions().readOnly).toBe(false);
});
it('disposes instances and relevant models independently from each other', () => {
diff --git a/spec/frontend/environment.js b/spec/frontend/environment.js
index dc1c1dfbe4a..1c84350bd8e 100644
--- a/spec/frontend/environment.js
+++ b/spec/frontend/environment.js
@@ -70,7 +70,6 @@ class CustomEnvironment extends JSDOMEnvironment {
//
// Monaco-related environment variables
//
- this.global.MonacoEnvironment = { globalAPI: true };
Object.defineProperty(this.global, 'matchMedia', {
writable: true,
value: (query) => ({
diff --git a/spec/frontend/environments/canary_ingress_spec.js b/spec/frontend/environments/canary_ingress_spec.js
index d58f9f9b8a2..340740e6499 100644
--- a/spec/frontend/environments/canary_ingress_spec.js
+++ b/spec/frontend/environments/canary_ingress_spec.js
@@ -10,7 +10,7 @@ describe('/environments/components/canary_ingress.vue', () => {
const setWeightTo = (weightWrapper, x) =>
weightWrapper
- .findAll(GlDropdownItem)
+ .findAllComponents(GlDropdownItem)
.at(x / 5)
.vm.$emit('click');
@@ -59,14 +59,14 @@ describe('/environments/components/canary_ingress.vue', () => {
});
it('lists options from 0 to 100 in increments of 5', () => {
- const options = stableWeightDropdown.findAll(GlDropdownItem);
+ const options = stableWeightDropdown.findAllComponents(GlDropdownItem);
expect(options).toHaveLength(21);
options.wrappers.forEach((w, i) => expect(w.text()).toBe((i * 5).toString()));
});
it('is set to open the change modal', () => {
stableWeightDropdown
- .findAll(GlDropdownItem)
+ .findAllComponents(GlDropdownItem)
.wrappers.forEach((w) =>
expect(getBinding(w.element, 'gl-modal')).toMatchObject({ value: CANARY_UPDATE_MODAL }),
);
@@ -92,13 +92,13 @@ describe('/environments/components/canary_ingress.vue', () => {
it('lists options from 0 to 100 in increments of 5', () => {
canaryWeightDropdown
- .findAll(GlDropdownItem)
+ .findAllComponents(GlDropdownItem)
.wrappers.forEach((w, i) => expect(w.text()).toBe((i * 5).toString()));
});
it('is set to open the change modal', () => {
canaryWeightDropdown
- .findAll(GlDropdownItem)
+ .findAllComponents(GlDropdownItem)
.wrappers.forEach((w) =>
expect(getBinding(w.element, 'gl-modal')).toMatchObject({ value: CANARY_UPDATE_MODAL }),
);
diff --git a/spec/frontend/environments/canary_update_modal_spec.js b/spec/frontend/environments/canary_update_modal_spec.js
index 16792dcda1e..31b1770da59 100644
--- a/spec/frontend/environments/canary_update_modal_spec.js
+++ b/spec/frontend/environments/canary_update_modal_spec.js
@@ -10,7 +10,7 @@ describe('/environments/components/canary_update_modal.vue', () => {
let modal;
let mutate;
- const findAlert = () => wrapper.find(GlAlert);
+ const findAlert = () => wrapper.findComponent(GlAlert);
const createComponent = () => {
mutate = jest.fn().mockResolvedValue();
@@ -27,7 +27,7 @@ describe('/environments/components/canary_update_modal.vue', () => {
$apollo: { mutate },
},
});
- modal = wrapper.find(GlModal);
+ modal = wrapper.findComponent(GlModal);
};
afterEach(() => {
diff --git a/spec/frontend/environments/confirm_rollback_modal_spec.js b/spec/frontend/environments/confirm_rollback_modal_spec.js
index c4763933468..2163814528a 100644
--- a/spec/frontend/environments/confirm_rollback_modal_spec.js
+++ b/spec/frontend/environments/confirm_rollback_modal_spec.js
@@ -73,7 +73,7 @@ describe('Confirm Rollback Modal Component', () => {
hasMultipleCommits,
retryUrl,
});
- const modal = component.find(GlModal);
+ const modal = component.findComponent(GlModal);
expect(modal.attributes('title')).toContain('Rollback');
expect(modal.attributes('title')).toContain('test');
@@ -92,7 +92,7 @@ describe('Confirm Rollback Modal Component', () => {
hasMultipleCommits,
});
- const modal = component.find(GlModal);
+ const modal = component.findComponent(GlModal);
expect(modal.attributes('title')).toContain('Re-deploy');
expect(modal.attributes('title')).toContain('test');
@@ -110,7 +110,7 @@ describe('Confirm Rollback Modal Component', () => {
});
const eventHubSpy = jest.spyOn(eventHub, '$emit');
- const modal = component.find(GlModal);
+ const modal = component.findComponent(GlModal);
modal.vm.$emit('ok');
expect(eventHubSpy).toHaveBeenCalledWith('rollbackEnvironment', env);
@@ -155,7 +155,7 @@ describe('Confirm Rollback Modal Component', () => {
},
{ apolloProvider },
);
- const modal = component.find(GlModal);
+ const modal = component.findComponent(GlModal);
expect(trimText(modal.text())).toContain('commit abc0123');
expect(modal.text()).toContain('Are you sure you want to continue?');
@@ -177,7 +177,7 @@ describe('Confirm Rollback Modal Component', () => {
},
{ apolloProvider },
);
- const modal = component.find(GlModal);
+ const modal = component.findComponent(GlModal);
expect(modal.attributes('title')).toContain('Rollback');
expect(modal.attributes('title')).toContain('test');
@@ -201,7 +201,7 @@ describe('Confirm Rollback Modal Component', () => {
{ apolloProvider },
);
- const modal = component.find(GlModal);
+ const modal = component.findComponent(GlModal);
expect(modal.attributes('title')).toContain('Re-deploy');
expect(modal.attributes('title')).toContain('test');
@@ -220,7 +220,7 @@ describe('Confirm Rollback Modal Component', () => {
{ apolloProvider },
);
- const modal = component.find(GlModal);
+ const modal = component.findComponent(GlModal);
modal.vm.$emit('ok');
await nextTick();
diff --git a/spec/frontend/environments/deploy_board_component_spec.js b/spec/frontend/environments/deploy_board_component_spec.js
index 4d63648dd48..c005ca22070 100644
--- a/spec/frontend/environments/deploy_board_component_spec.js
+++ b/spec/frontend/environments/deploy_board_component_spec.js
@@ -26,7 +26,9 @@ describe('Deploy Board', () => {
});
it('should render percentage with completion value provided', () => {
- expect(wrapper.find({ ref: 'percentage' }).text()).toBe(`${deployBoardMockData.completion}%`);
+ expect(wrapper.findComponent({ ref: 'percentage' }).text()).toBe(
+ `${deployBoardMockData.completion}%`,
+ );
});
it('should render total instance count', () => {
@@ -79,7 +81,9 @@ describe('Deploy Board', () => {
});
it('should render percentage with completion value provided', () => {
- expect(wrapper.find({ ref: 'percentage' }).text()).toBe(`${rolloutStatus.completion}%`);
+ expect(wrapper.findComponent({ ref: 'percentage' }).text()).toBe(
+ `${rolloutStatus.completion}%`,
+ );
});
it('should render total instance count', () => {
diff --git a/spec/frontend/environments/edit_environment_spec.js b/spec/frontend/environments/edit_environment_spec.js
index 2c8c054ccbd..0f2d6e95bf0 100644
--- a/spec/frontend/environments/edit_environment_spec.js
+++ b/spec/frontend/environments/edit_environment_spec.js
@@ -42,7 +42,7 @@ describe('~/environments/components/edit.vue', () => {
const findExternalUrlInput = () => wrapper.findByLabelText('External URL');
const findForm = () => wrapper.findByRole('form', { name: 'Edit environment' });
- const showsLoading = () => wrapper.find(GlLoadingIcon).exists();
+ const showsLoading = () => wrapper.findComponent(GlLoadingIcon).exists();
const submitForm = async (expected, response) => {
mock
diff --git a/spec/frontend/environments/environment_actions_spec.js b/spec/frontend/environments/environment_actions_spec.js
index ada79e2d415..68895b194a1 100644
--- a/spec/frontend/environments/environment_actions_spec.js
+++ b/spec/frontend/environments/environment_actions_spec.js
@@ -51,7 +51,7 @@ describe('EnvironmentActions Component', () => {
}
const findDropdownItem = (action) => {
- const buttons = wrapper.findAll(GlDropdownItem);
+ const buttons = wrapper.findAllComponents(GlDropdownItem);
return buttons.filter((button) => button.text().startsWith(action.name)).at(0);
};
@@ -62,12 +62,12 @@ describe('EnvironmentActions Component', () => {
it('should render a dropdown button with 2 icons', () => {
createComponent({}, { mountFn: mount });
- expect(wrapper.find(GlDropdown).findAll(GlIcon).length).toBe(2);
+ expect(wrapper.findComponent(GlDropdown).findAllComponents(GlIcon).length).toBe(2);
});
it('should render a dropdown button with aria-label description', () => {
createComponent();
- expect(wrapper.find(GlDropdown).attributes('aria-label')).toBe('Deploy to...');
+ expect(wrapper.findComponent(GlDropdown).attributes('aria-label')).toBe('Deploy to...');
});
it('should render a tooltip', () => {
@@ -98,11 +98,11 @@ describe('EnvironmentActions Component', () => {
});
it('should render a dropdown with the provided list of actions', () => {
- expect(wrapper.findAll(GlDropdownItem)).toHaveLength(actions.length);
+ expect(wrapper.findAllComponents(GlDropdownItem)).toHaveLength(actions.length);
});
it("should render a disabled action when it's not playable", () => {
- const dropdownItems = wrapper.findAll(GlDropdownItem);
+ const dropdownItems = wrapper.findAllComponents(GlDropdownItem);
const lastDropdownItem = dropdownItems.at(dropdownItems.length - 1);
expect(lastDropdownItem.attributes('disabled')).toBe('true');
});
@@ -136,7 +136,7 @@ describe('EnvironmentActions Component', () => {
});
it('should render a dropdown button with a loading icon', () => {
- expect(wrapper.find(GlLoadingIcon).isVisible()).toBe(true);
+ expect(wrapper.findComponent(GlLoadingIcon).isVisible()).toBe(true);
});
});
diff --git a/spec/frontend/environments/environment_delete_spec.js b/spec/frontend/environments/environment_delete_spec.js
index 057cb9858c4..530f9f55088 100644
--- a/spec/frontend/environments/environment_delete_spec.js
+++ b/spec/frontend/environments/environment_delete_spec.js
@@ -21,7 +21,7 @@ describe('External URL Component', () => {
});
};
- const findDropdownItem = () => wrapper.find(GlDropdownItem);
+ const findDropdownItem = () => wrapper.findComponent(GlDropdownItem);
describe('event hub', () => {
beforeEach(() => {
diff --git a/spec/frontend/environments/environment_item_spec.js b/spec/frontend/environments/environment_item_spec.js
index 1c86a66d9b8..dd909cf4473 100644
--- a/spec/frontend/environments/environment_item_spec.js
+++ b/spec/frontend/environments/environment_item_spec.js
@@ -88,11 +88,11 @@ describe('Environment item', () => {
it('should render user avatar with link to profile', () => {
const avatarLink = findLastDeploymentAvatarLink();
const avatar = findLastDeploymentAvatar();
- const { username, avatar_url, web_url } = environment.last_deployment.user;
+ const { username, avatar_url: src, web_url } = environment.last_deployment.user;
expect(avatarLink.attributes('href')).toBe(web_url);
expect(avatar.props()).toMatchObject({
- src: avatar_url,
+ src,
entityName: username,
});
expect(avatar.attributes()).toMatchObject({
@@ -127,12 +127,12 @@ describe('Environment item', () => {
it('should render the build ID and user', () => {
const avatarLink = findUpcomingDeploymentAvatarLink();
const avatar = findUpcomingDeploymentAvatar();
- const { username, avatar_url, web_url } = environment.upcoming_deployment.user;
+ const { username, avatar_url: src, web_url } = environment.upcoming_deployment.user;
expect(findUpcomingDeploymentContent().text()).toMatchInterpolatedText('#27 by');
expect(avatarLink.attributes('href')).toBe(web_url);
expect(avatar.props()).toMatchObject({
- src: avatar_url,
+ src,
entityName: username,
});
});
@@ -166,12 +166,12 @@ describe('Environment item', () => {
it('should still render the build ID and user avatar', () => {
const avatarLink = findUpcomingDeploymentAvatarLink();
const avatar = findUpcomingDeploymentAvatar();
- const { username, avatar_url, web_url } = environment.upcoming_deployment.user;
+ const { username, avatar_url: src, web_url } = environment.upcoming_deployment.user;
expect(findUpcomingDeploymentContent().text()).toMatchInterpolatedText('#27 by');
expect(avatarLink.attributes('href')).toBe(web_url);
expect(avatar.props()).toMatchObject({
- src: avatar_url,
+ src,
entityName: username,
});
});
diff --git a/spec/frontend/environments/environment_pin_spec.js b/spec/frontend/environments/environment_pin_spec.js
index 669c974ea4f..170036b5b00 100644
--- a/spec/frontend/environments/environment_pin_spec.js
+++ b/spec/frontend/environments/environment_pin_spec.js
@@ -41,7 +41,7 @@ describe('Pin Component', () => {
it('should emit onPinClick when clicked', () => {
const eventHubSpy = jest.spyOn(eventHub, '$emit');
- const item = wrapper.find(GlDropdownItem);
+ const item = wrapper.findComponent(GlDropdownItem);
item.vm.$emit('click');
@@ -74,7 +74,7 @@ describe('Pin Component', () => {
it('should emit onPinClick when clicked', () => {
jest.spyOn(mockApollo.defaultClient, 'mutate');
- const item = wrapper.find(GlDropdownItem);
+ const item = wrapper.findComponent(GlDropdownItem);
item.vm.$emit('click');
diff --git a/spec/frontend/environments/environment_rollback_spec.js b/spec/frontend/environments/environment_rollback_spec.js
index 7eff46baaf7..be61c6fcc90 100644
--- a/spec/frontend/environments/environment_rollback_spec.js
+++ b/spec/frontend/environments/environment_rollback_spec.js
@@ -44,7 +44,7 @@ describe('Rollback Component', () => {
},
},
});
- const button = wrapper.find(GlDropdownItem);
+ const button = wrapper.findComponent(GlDropdownItem);
button.vm.$emit('click');
@@ -71,7 +71,7 @@ describe('Rollback Component', () => {
},
apolloProvider,
});
- const button = wrapper.find(GlDropdownItem);
+ const button = wrapper.findComponent(GlDropdownItem);
button.vm.$emit('click');
expect(apolloProvider.defaultClient.mutate).toHaveBeenCalledWith({
diff --git a/spec/frontend/environments/environment_stop_spec.js b/spec/frontend/environments/environment_stop_spec.js
index 358abca2f77..851e24c22cc 100644
--- a/spec/frontend/environments/environment_stop_spec.js
+++ b/spec/frontend/environments/environment_stop_spec.js
@@ -22,7 +22,7 @@ describe('Stop Component', () => {
});
};
- const findButton = () => wrapper.find(GlButton);
+ const findButton = () => wrapper.findComponent(GlButton);
describe('eventHub', () => {
beforeEach(() => {
diff --git a/spec/frontend/environments/environment_table_spec.js b/spec/frontend/environments/environment_table_spec.js
index aff6b1327f0..49a643aaac8 100644
--- a/spec/frontend/environments/environment_table_spec.js
+++ b/spec/frontend/environments/environment_table_spec.js
@@ -177,10 +177,10 @@ describe('Environment table', () => {
},
});
- wrapper.find(DeployBoard).vm.$emit('changeCanaryWeight', 40);
+ wrapper.findComponent(DeployBoard).vm.$emit('changeCanaryWeight', 40);
await nextTick();
- expect(wrapper.find(CanaryUpdateModal).props()).toMatchObject({
+ expect(wrapper.findComponent(CanaryUpdateModal).props()).toMatchObject({
weight: 40,
environment: mockItem,
});
diff --git a/spec/frontend/environments/environments_detail_header_spec.js b/spec/frontend/environments/environments_detail_header_spec.js
index 305e7385b43..4687119127d 100644
--- a/spec/frontend/environments/environments_detail_header_spec.js
+++ b/spec/frontend/environments/environments_detail_header_spec.js
@@ -1,5 +1,6 @@
import { GlSprintf } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
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';
@@ -43,6 +44,9 @@ describe('Environments detail header component', () => {
GlSprintf,
TimeAgo,
},
+ directives: {
+ GlTooltip: createMockDirective(),
+ },
propsData: {
canAdminEnvironment: false,
canUpdateEnvironment: false,
@@ -185,6 +189,14 @@ describe('Environments detail header component', () => {
it('displays the metrics button with correct path', () => {
expect(findMetricsButton().attributes('href')).toBe(metricsPath);
});
+
+ it('uses a gl tooltip for the title', () => {
+ const button = findMetricsButton();
+ const tooltip = getBinding(button.element, 'gl-tooltip');
+
+ expect(tooltip).toBeDefined();
+ expect(button.attributes('title')).toBe('See metrics');
+ });
});
describe('when has all admin rights', () => {
diff --git a/spec/frontend/environments/folder/environments_folder_view_spec.js b/spec/frontend/environments/folder/environments_folder_view_spec.js
index 9eb57b2682f..f8b8465cf6f 100644
--- a/spec/frontend/environments/folder/environments_folder_view_spec.js
+++ b/spec/frontend/environments/folder/environments_folder_view_spec.js
@@ -65,7 +65,7 @@ describe('Environments Folder View', () => {
});
it('should render a table with environments', () => {
- const table = wrapper.find(EnvironmentTable);
+ const table = wrapper.findComponent(EnvironmentTable);
expect(table.exists()).toBe(true);
expect(table.find('.environment-name').text()).toEqual(environmentsList[0].name);
@@ -93,7 +93,7 @@ describe('Environments Folder View', () => {
describe('pagination', () => {
it('should render pagination', () => {
- expect(wrapper.find(GlPagination).exists()).toBe(true);
+ expect(wrapper.findComponent(GlPagination).exists()).toBe(true);
});
it('should make an API request when changing page', () => {
@@ -126,7 +126,7 @@ describe('Environments Folder View', () => {
});
it('should not render a table', () => {
- expect(wrapper.find(EnvironmentTable).exists()).toBe(false);
+ expect(wrapper.findComponent(EnvironmentTable).exists()).toBe(false);
});
it('should render available tab with count 0', () => {
diff --git a/spec/frontend/environments/new_environment_spec.js b/spec/frontend/environments/new_environment_spec.js
index f6d970e02d8..5a1c1c7714c 100644
--- a/spec/frontend/environments/new_environment_spec.js
+++ b/spec/frontend/environments/new_environment_spec.js
@@ -40,7 +40,7 @@ describe('~/environments/components/new.vue', () => {
wrapper.destroy();
});
- const showsLoading = () => wrapper.find(GlLoadingIcon).exists();
+ const showsLoading = () => wrapper.findComponent(GlLoadingIcon).exists();
const submitForm = async (expected, response) => {
mock
diff --git a/spec/frontend/error_tracking/components/error_details_spec.js b/spec/frontend/error_tracking/components/error_details_spec.js
index 4273da6c735..732eff65495 100644
--- a/spec/frontend/error_tracking/components/error_details_spec.js
+++ b/spec/frontend/error_tracking/components/error_details_spec.js
@@ -35,7 +35,9 @@ describe('ErrorDetails', () => {
const externalUrl = 'https://sentry.io/organizations/test-sentry-nk/issues/1/?project=1';
const findInput = (name) => {
- const inputs = wrapper.findAll(GlFormInput).filter((c) => c.attributes('name') === name);
+ const inputs = wrapper
+ .findAllComponents(GlFormInput)
+ .filter((c) => c.attributes('name') === name);
return inputs.length ? inputs.at(0) : inputs;
};
@@ -44,7 +46,7 @@ describe('ErrorDetails', () => {
const findUpdateResolveStatusButton = () =>
wrapper.find('[data-testid="update-resolve-status-btn"]');
const findExternalUrl = () => wrapper.find('[data-testid="external-url-link"]');
- const findAlert = () => wrapper.find(GlAlert);
+ const findAlert = () => wrapper.findComponent(GlAlert);
function mountComponent() {
wrapper = shallowMount(ErrorDetails, {
@@ -119,9 +121,9 @@ describe('ErrorDetails', () => {
});
it('should show spinner while loading', () => {
- expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
- expect(wrapper.find(GlLink).exists()).toBe(false);
- expect(wrapper.find(Stacktrace).exists()).toBe(false);
+ expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
+ expect(wrapper.findComponent(GlLink).exists()).toBe(false);
+ expect(wrapper.findComponent(Stacktrace).exists()).toBe(false);
});
});
@@ -141,7 +143,7 @@ describe('ErrorDetails', () => {
wrapper.vm.onNoApolloResult();
await nextTick();
- expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
+ expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
expect(createFlash).not.toHaveBeenCalled();
expect(mocks.$apollo.queries.error.stopPolling).not.toHaveBeenCalled();
});
@@ -152,8 +154,8 @@ describe('ErrorDetails', () => {
wrapper.vm.onNoApolloResult();
await nextTick();
- expect(wrapper.find(GlLoadingIcon).exists()).toBe(false);
- expect(wrapper.find(GlLink).exists()).toBe(false);
+ expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(false);
+ expect(wrapper.findComponent(GlLink).exists()).toBe(false);
expect(createFlash).toHaveBeenCalledWith({
message: 'Could not connect to Sentry. Refresh the page to try again.',
type: 'warning',
@@ -186,11 +188,11 @@ describe('ErrorDetails', () => {
});
it('should show Sentry error details without stacktrace', () => {
- expect(wrapper.find(GlLink).exists()).toBe(true);
- expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
- expect(wrapper.find(Stacktrace).exists()).toBe(false);
- expect(wrapper.find(GlBadge).exists()).toBe(false);
- expect(wrapper.findAll(GlButton)).toHaveLength(3);
+ expect(wrapper.findComponent(GlLink).exists()).toBe(true);
+ expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
+ expect(wrapper.findComponent(Stacktrace).exists()).toBe(false);
+ expect(wrapper.findComponent(GlBadge).exists()).toBe(false);
+ expect(wrapper.findAllComponents(GlButton)).toHaveLength(3);
});
describe('unsafe chars for culprit field', () => {
@@ -227,7 +229,7 @@ describe('ErrorDetails', () => {
},
});
await nextTick();
- expect(wrapper.findAll(GlBadge).length).toBe(2);
+ expect(wrapper.findAllComponents(GlBadge).length).toBe(2);
});
it('should NOT show the badge if the tag is not present', async () => {
@@ -239,7 +241,7 @@ describe('ErrorDetails', () => {
},
});
await nextTick();
- expect(wrapper.findAll(GlBadge).length).toBe(1);
+ expect(wrapper.findAllComponents(GlBadge).length).toBe(1);
});
it.each(Object.keys(severityLevel))(
@@ -253,7 +255,7 @@ describe('ErrorDetails', () => {
},
});
await nextTick();
- expect(wrapper.find(GlBadge).props('variant')).toEqual(
+ expect(wrapper.findComponent(GlBadge).props('variant')).toEqual(
severityLevelVariant[severityLevel[level]],
);
},
@@ -268,7 +270,7 @@ describe('ErrorDetails', () => {
},
});
await nextTick();
- expect(wrapper.find(GlBadge).props('variant')).toEqual(
+ expect(wrapper.findComponent(GlBadge).props('variant')).toEqual(
severityLevelVariant[severityLevel.ERROR],
);
});
@@ -278,8 +280,8 @@ describe('ErrorDetails', () => {
it('should show stacktrace', async () => {
store.state.details.loadingStacktrace = false;
await nextTick();
- expect(wrapper.find(GlLoadingIcon).exists()).toBe(false);
- expect(wrapper.find(Stacktrace).exists()).toBe(true);
+ expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(false);
+ expect(wrapper.findComponent(Stacktrace).exists()).toBe(true);
expect(findAlert().exists()).toBe(false);
});
@@ -287,8 +289,8 @@ describe('ErrorDetails', () => {
store.state.details.loadingStacktrace = false;
store.getters = { 'details/sentryUrl': () => 'sentry.io', 'details/stacktrace': () => [] };
await nextTick();
- expect(wrapper.find(GlLoadingIcon).exists()).toBe(false);
- expect(wrapper.find(Stacktrace).exists()).toBe(false);
+ expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(false);
+ expect(wrapper.findComponent(Stacktrace).exists()).toBe(false);
expect(findAlert().text()).toBe('No stack trace for this error');
});
});
diff --git a/spec/frontend/error_tracking/components/error_tracking_actions_spec.js b/spec/frontend/error_tracking/components/error_tracking_actions_spec.js
index 7ed4e5f6b05..5f6c9ddb4d7 100644
--- a/spec/frontend/error_tracking/components/error_tracking_actions_spec.js
+++ b/spec/frontend/error_tracking/components/error_tracking_actions_spec.js
@@ -35,7 +35,7 @@ describe('Error Tracking Actions', () => {
}
});
- const findButtons = () => wrapper.findAll(GlButton);
+ const findButtons = () => wrapper.findAllComponents(GlButton);
describe('when error status is unresolved', () => {
it('renders the correct actions buttons to allow ignore and resolve', async () => {
diff --git a/spec/frontend/error_tracking/components/error_tracking_list_spec.js b/spec/frontend/error_tracking/components/error_tracking_list_spec.js
index 23d448f3964..b7dffbbec04 100644
--- a/spec/frontend/error_tracking/components/error_tracking_list_spec.js
+++ b/spec/frontend/error_tracking/components/error_tracking_list_spec.js
@@ -19,13 +19,13 @@ describe('ErrorTrackingList', () => {
const findErrorListTable = () => wrapper.find('table');
const findErrorListRows = () => wrapper.findAll('tbody tr');
- const dropdownsArray = () => wrapper.findAll(GlDropdown);
- const findRecentSearchesDropdown = () => dropdownsArray().at(0).find(GlDropdown);
- const findStatusFilterDropdown = () => dropdownsArray().at(1).find(GlDropdown);
- const findSortDropdown = () => dropdownsArray().at(2).find(GlDropdown);
- const findLoadingIcon = () => wrapper.find(GlLoadingIcon);
- const findPagination = () => wrapper.find(GlPagination);
- const findErrorActions = () => wrapper.find(ErrorTrackingActions);
+ const dropdownsArray = () => wrapper.findAllComponents(GlDropdown);
+ const findRecentSearchesDropdown = () => dropdownsArray().at(0).findComponent(GlDropdown);
+ const findStatusFilterDropdown = () => dropdownsArray().at(1).findComponent(GlDropdown);
+ const findSortDropdown = () => dropdownsArray().at(2).findComponent(GlDropdown);
+ const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
+ const findPagination = () => wrapper.findComponent(GlPagination);
+ const findErrorActions = () => wrapper.findComponent(ErrorTrackingActions);
const findIntegratedDisabledAlert = () => wrapper.findByTestId('integrated-disabled-alert');
function mountComponent({
@@ -152,12 +152,12 @@ describe('ErrorTrackingList', () => {
it('each error in the list should have an action button set', () => {
findErrorListRows().wrappers.forEach((row) => {
- expect(row.find(ErrorTrackingActions).exists()).toBe(true);
+ expect(row.findComponent(ErrorTrackingActions).exists()).toBe(true);
});
});
describe('filtering', () => {
- const findSearchBox = () => wrapper.find(GlFormInput);
+ const findSearchBox = () => wrapper.findComponent(GlFormInput);
it('shows search box & sort dropdown', () => {
expect(findSearchBox().exists()).toBe(true);
@@ -222,7 +222,7 @@ describe('ErrorTrackingList', () => {
});
it('shows empty state', () => {
- expect(wrapper.find(GlEmptyState).exists()).toBe(true);
+ expect(wrapper.findComponent(GlEmptyState).exists()).toBe(true);
expect(findLoadingIcon().exists()).toBe(false);
expect(findErrorListTable().exists()).toBe(false);
expect(dropdownsArray().length).toBe(0);
@@ -327,7 +327,7 @@ describe('ErrorTrackingList', () => {
});
it('shows empty state', () => {
- expect(wrapper.find(GlEmptyState).isVisible()).toBe(true);
+ expect(wrapper.findComponent(GlEmptyState).isVisible()).toBe(true);
});
});
@@ -358,7 +358,7 @@ describe('ErrorTrackingList', () => {
});
describe('clear', () => {
- const clearRecentButton = () => wrapper.find({ ref: 'clearRecentSearches' });
+ const clearRecentButton = () => wrapper.findComponent({ ref: 'clearRecentSearches' });
it('is hidden when list empty', () => {
store.state.list.recentSearches = [];
diff --git a/spec/frontend/error_tracking/components/stacktrace_entry_spec.js b/spec/frontend/error_tracking/components/stacktrace_entry_spec.js
index 0b43167c19b..693fcff50ca 100644
--- a/spec/frontend/error_tracking/components/stacktrace_entry_spec.js
+++ b/spec/frontend/error_tracking/components/stacktrace_entry_spec.js
@@ -36,10 +36,10 @@ describe('Stacktrace Entry', () => {
it('should render stacktrace entry collapsed', () => {
mountComponent({ lines });
- expect(wrapper.find(StackTraceEntry).exists()).toBe(true);
- expect(wrapper.find(ClipboardButton).exists()).toBe(true);
- expect(wrapper.find(GlIcon).exists()).toBe(true);
- expect(wrapper.find(FileIcon).exists()).toBe(true);
+ expect(wrapper.findComponent(StackTraceEntry).exists()).toBe(true);
+ expect(wrapper.findComponent(ClipboardButton).exists()).toBe(true);
+ expect(wrapper.findComponent(GlIcon).exists()).toBe(true);
+ expect(wrapper.findComponent(FileIcon).exists()).toBe(true);
expect(wrapper.find('table').exists()).toBe(false);
});
@@ -56,7 +56,7 @@ describe('Stacktrace Entry', () => {
it('should hide collapse icon and render error fn name and error line when there is no code block', () => {
const extraInfo = { errorLine: 34, errorFn: 'errorFn', errorColumn: 77 };
mountComponent({ expanded: false, lines: [], ...extraInfo });
- expect(wrapper.find(GlIcon).exists()).toBe(false);
+ expect(wrapper.findComponent(GlIcon).exists()).toBe(false);
expect(trimText(findFileHeaderContent())).toContain(
`in ${extraInfo.errorFn} at line ${extraInfo.errorLine}:${extraInfo.errorColumn}`,
);
diff --git a/spec/frontend/error_tracking/components/stacktrace_spec.js b/spec/frontend/error_tracking/components/stacktrace_spec.js
index 4f4a60acba4..cd5a57f5683 100644
--- a/spec/frontend/error_tracking/components/stacktrace_spec.js
+++ b/spec/frontend/error_tracking/components/stacktrace_spec.js
@@ -33,13 +33,13 @@ describe('ErrorDetails', () => {
it('should render single Stacktrace entry', () => {
mountComponent([stackTraceEntry]);
- expect(wrapper.findAll(StackTraceEntry).length).toBe(1);
+ expect(wrapper.findAllComponents(StackTraceEntry).length).toBe(1);
});
it('should render multiple Stacktrace entry', () => {
const entriesNum = 3;
mountComponent(new Array(entriesNum).fill(stackTraceEntry));
- expect(wrapper.findAll(StackTraceEntry).length).toBe(entriesNum);
+ expect(wrapper.findAllComponents(StackTraceEntry).length).toBe(entriesNum);
});
});
});
diff --git a/spec/frontend/error_tracking_settings/components/project_dropdown_spec.js b/spec/frontend/error_tracking_settings/components/project_dropdown_spec.js
index 1ba5a505f57..b44af547658 100644
--- a/spec/frontend/error_tracking_settings/components/project_dropdown_spec.js
+++ b/spec/frontend/error_tracking_settings/components/project_dropdown_spec.js
@@ -41,23 +41,23 @@ describe('error tracking settings project dropdown', () => {
describe('empty project list', () => {
it('renders the dropdown', () => {
- expect(wrapper.find('#project-dropdown').exists()).toBeTruthy();
- expect(wrapper.find(GlDropdown).exists()).toBeTruthy();
+ expect(wrapper.find('#project-dropdown').exists()).toBe(true);
+ expect(wrapper.find(GlDropdown).exists()).toBe(true);
});
it('shows helper text', () => {
- expect(wrapper.find('.js-project-dropdown-label').exists()).toBeTruthy();
+ expect(wrapper.find('.js-project-dropdown-label').exists()).toBe(true);
expect(wrapper.find('.js-project-dropdown-label').text()).toContain(
'To enable project selection',
);
});
it('does not show an error', () => {
- expect(wrapper.find('.js-project-dropdown-error').exists()).toBeFalsy();
+ expect(wrapper.find('.js-project-dropdown-error').exists()).toBe(false);
});
it('does not contain any dropdown items', () => {
- expect(wrapper.find(GlDropdownItem).exists()).toBeFalsy();
+ expect(wrapper.find(GlDropdownItem).exists()).toBe(false);
expect(wrapper.find(GlDropdown).props('text')).toBe('No projects available');
});
});
@@ -70,12 +70,12 @@ describe('error tracking settings project dropdown', () => {
});
it('renders the dropdown', () => {
- expect(wrapper.find('#project-dropdown').exists()).toBeTruthy();
- expect(wrapper.find(GlDropdown).exists()).toBeTruthy();
+ expect(wrapper.find('#project-dropdown').exists()).toBe(true);
+ expect(wrapper.find(GlDropdown).exists()).toBe(true);
});
it('contains a number of dropdown items', () => {
- expect(wrapper.find(GlDropdownItem).exists()).toBeTruthy();
+ expect(wrapper.find(GlDropdownItem).exists()).toBe(true);
expect(wrapper.findAll(GlDropdownItem).length).toBe(2);
});
});
@@ -89,8 +89,8 @@ describe('error tracking settings project dropdown', () => {
});
it('does not show helper text', () => {
- expect(wrapper.find('.js-project-dropdown-label').exists()).toBeFalsy();
- expect(wrapper.find('.js-project-dropdown-error').exists()).toBeFalsy();
+ expect(wrapper.find('.js-project-dropdown-label').exists()).toBe(false);
+ expect(wrapper.find('.js-project-dropdown-error').exists()).toBe(false);
});
});
@@ -105,8 +105,8 @@ describe('error tracking settings project dropdown', () => {
});
it('displays a error', () => {
- expect(wrapper.find('.js-project-dropdown-label').exists()).toBeFalsy();
- expect(wrapper.find('.js-project-dropdown-error').exists()).toBeTruthy();
+ expect(wrapper.find('.js-project-dropdown-label').exists()).toBe(false);
+ expect(wrapper.find('.js-project-dropdown-error').exists()).toBe(true);
});
});
});
diff --git a/spec/frontend/feature_flags/components/configure_feature_flags_modal_spec.js b/spec/frontend/feature_flags/components/configure_feature_flags_modal_spec.js
index 4a0242b4a46..c1051a14a08 100644
--- a/spec/frontend/feature_flags/components/configure_feature_flags_modal_spec.js
+++ b/spec/frontend/feature_flags/components/configure_feature_flags_modal_spec.js
@@ -39,7 +39,7 @@ describe('Configure Feature Flags Modal', () => {
const findSecondaryAction = () => findGlModal().props('actionSecondary');
const findProjectNameInput = () => wrapper.find('#project_name_verification');
const findDangerGlAlert = () =>
- wrapper.findAll(GlAlert).filter((c) => c.props('variant') === 'danger');
+ wrapper.findAllComponents(GlAlert).filter((c) => c.props('variant') === 'danger');
describe('idle', () => {
afterEach(() => wrapper.destroy());
@@ -157,7 +157,7 @@ describe('Configure Feature Flags Modal', () => {
beforeEach(factory.bind(null, { isRotating: true }));
it('should disable the project name input', async () => {
- expect(findProjectNameInput().attributes('disabled')).toBeTruthy();
+ expect(findProjectNameInput().attributes('disabled')).toBe('true');
});
});
});
diff --git a/spec/frontend/feature_flags/components/empty_state_spec.js b/spec/frontend/feature_flags/components/empty_state_spec.js
index 4ac82ae44a6..e3cc6f703c4 100644
--- a/spec/frontend/feature_flags/components/empty_state_spec.js
+++ b/spec/frontend/feature_flags/components/empty_state_spec.js
@@ -57,7 +57,7 @@ describe('feature_flags/components/feature_flags_tab.vue', () => {
beforeEach(() => {
wrapper = factory();
- alerts = wrapper.findAll(GlAlert);
+ alerts = wrapper.findAllComponents(GlAlert);
});
it('should show any alerts', () => {
@@ -68,7 +68,7 @@ describe('feature_flags/components/feature_flags_tab.vue', () => {
it('should emit a dismiss event for a dismissed alert', () => {
alerts.at(0).vm.$emit('dismiss');
- expect(wrapper.find(EmptyState).emitted('dismissAlert')).toEqual([[0]]);
+ expect(wrapper.findComponent(EmptyState).emitted('dismissAlert')).toEqual([[0]]);
});
});
@@ -78,8 +78,8 @@ describe('feature_flags/components/feature_flags_tab.vue', () => {
});
it('should show a loading icon and nothing else', () => {
- expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
- expect(wrapper.findAll(GlEmptyState)).toHaveLength(0);
+ expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
+ expect(wrapper.findAllComponents(GlEmptyState)).toHaveLength(0);
});
});
@@ -88,7 +88,7 @@ describe('feature_flags/components/feature_flags_tab.vue', () => {
beforeEach(() => {
wrapper = factory({ errorState: true });
- emptyState = wrapper.find(GlEmptyState);
+ emptyState = wrapper.findComponent(GlEmptyState);
});
it('should show an error state if there has been an error', () => {
@@ -106,8 +106,8 @@ describe('feature_flags/components/feature_flags_tab.vue', () => {
beforeEach(() => {
wrapper = factory({ emptyState: true });
- emptyState = wrapper.find(GlEmptyState);
- emptyStateLink = emptyState.find(GlLink);
+ emptyState = wrapper.findComponent(GlEmptyState);
+ emptyStateLink = emptyState.findComponent(GlLink);
});
it('should show an empty state if it is empty', () => {
diff --git a/spec/frontend/feature_flags/components/environments_dropdown_spec.js b/spec/frontend/feature_flags/components/environments_dropdown_spec.js
index cca472012e9..e8103df78bc 100644
--- a/spec/frontend/feature_flags/components/environments_dropdown_spec.js
+++ b/spec/frontend/feature_flags/components/environments_dropdown_spec.js
@@ -23,7 +23,7 @@ describe('Feature flags > Environments dropdown ', () => {
});
};
- const findEnvironmentSearchInput = () => wrapper.find(GlSearchBoxByType);
+ const findEnvironmentSearchInput = () => wrapper.findComponent(GlSearchBoxByType);
const findDropdownMenu = () => wrapper.find('.dropdown-menu');
afterEach(() => {
@@ -91,7 +91,7 @@ describe('Feature flags > Environments dropdown ', () => {
describe('with received data', () => {
it('sets is loading to false', () => {
expect(wrapper.vm.isLoading).toBe(false);
- expect(wrapper.find(GlLoadingIcon).exists()).toBe(false);
+ expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(false);
});
it('shows the suggestions', () => {
@@ -100,7 +100,7 @@ describe('Feature flags > Environments dropdown ', () => {
it('emits event when a suggestion is clicked', async () => {
const button = wrapper
- .findAll(GlButton)
+ .findAllComponents(GlButton)
.filter((b) => b.text() === 'production')
.at(0);
button.vm.$emit('click');
@@ -111,7 +111,7 @@ describe('Feature flags > Environments dropdown ', () => {
describe('on click clear button', () => {
beforeEach(async () => {
- wrapper.find(GlButton).vm.$emit('click');
+ wrapper.findComponent(GlButton).vm.$emit('click');
await nextTick();
});
@@ -137,7 +137,7 @@ describe('Feature flags > Environments dropdown ', () => {
});
it('emits create event', async () => {
- wrapper.findAll(GlButton).at(0).vm.$emit('click');
+ wrapper.findAllComponents(GlButton).at(0).vm.$emit('click');
await nextTick();
expect(wrapper.emitted('createClicked')).toEqual([['production']]);
});
diff --git a/spec/frontend/feature_flags/components/feature_flags_table_spec.js b/spec/frontend/feature_flags/components/feature_flags_table_spec.js
index 99864a95f59..47f12f70056 100644
--- a/spec/frontend/feature_flags/components/feature_flags_table_spec.js
+++ b/spec/frontend/feature_flags/components/feature_flags_table_spec.js
@@ -119,7 +119,7 @@ describe('Feature flag table', () => {
it('should render an environments specs badge with active class', () => {
const envColumn = wrapper.find('.js-feature-flag-environments');
- expect(trimText(envColumn.find(GlBadge).text())).toBe('All Users: All Environments');
+ expect(trimText(envColumn.findComponent(GlBadge).text())).toBe('All Users: All Environments');
});
it('should render an actions column', () => {
@@ -137,7 +137,7 @@ describe('Feature flag table', () => {
beforeEach(() => {
props.featureFlags[0].update_path = props.featureFlags[0].destroy_path;
createWrapper(props);
- toggle = wrapper.find(GlToggle);
+ toggle = wrapper.findComponent(GlToggle);
spy = mockTracking('_category_', toggle.element, jest.spyOn);
});
diff --git a/spec/frontend/feature_flags/components/form_spec.js b/spec/frontend/feature_flags/components/form_spec.js
index 3ad1225906b..7dd7c709c94 100644
--- a/spec/frontend/feature_flags/components/form_spec.js
+++ b/spec/frontend/feature_flags/components/form_spec.js
@@ -61,7 +61,7 @@ describe('feature flag form', () => {
it('does not render the related issues widget without the featureFlagIssuesEndpoint', () => {
factory(requiredProps);
- expect(wrapper.find(RelatedIssuesRoot).exists()).toBe(false);
+ expect(wrapper.findComponent(RelatedIssuesRoot).exists()).toBe(false);
});
it('renders the related issues widget when the featureFlagIssuesEndpoint is provided', () => {
@@ -73,7 +73,7 @@ describe('feature flag form', () => {
},
);
- expect(wrapper.find(RelatedIssuesRoot).exists()).toBe(true);
+ expect(wrapper.findComponent(RelatedIssuesRoot).exists()).toBe(true);
});
describe('without provided data', () => {
@@ -114,7 +114,7 @@ describe('feature flag form', () => {
});
it('should show the strategy component', () => {
- const strategy = wrapper.find(Strategy);
+ const strategy = wrapper.findComponent(Strategy);
expect(strategy.exists()).toBe(true);
expect(strategy.props('strategy')).toEqual({
type: ROLLOUT_STRATEGY_PERCENT_ROLLOUT,
@@ -124,14 +124,14 @@ describe('feature flag form', () => {
});
it('should show one strategy component per strategy', () => {
- expect(wrapper.findAll(Strategy)).toHaveLength(2);
+ expect(wrapper.findAllComponents(Strategy)).toHaveLength(2);
});
it('adds an all users strategy when clicking the Add button', async () => {
- wrapper.find(GlButton).vm.$emit('click');
+ wrapper.findComponent(GlButton).vm.$emit('click');
await nextTick();
- const strategies = wrapper.findAll(Strategy);
+ const strategies = wrapper.findAllComponents(Strategy);
expect(strategies).toHaveLength(3);
expect(strategies.at(2).props('strategy')).toEqual(allUsersStrategy);
@@ -143,10 +143,10 @@ describe('feature flag form', () => {
parameters: { percentage: '30' },
scopes: [],
};
- wrapper.find(Strategy).vm.$emit('delete');
+ wrapper.findComponent(Strategy).vm.$emit('delete');
await nextTick();
- expect(wrapper.findAll(Strategy)).toHaveLength(1);
- expect(wrapper.find(Strategy).props('strategy')).not.toEqual(strategy);
+ expect(wrapper.findAllComponents(Strategy)).toHaveLength(1);
+ expect(wrapper.findComponent(Strategy).props('strategy')).not.toEqual(strategy);
});
});
});
diff --git a/spec/frontend/feature_flags/components/new_environments_dropdown_spec.js b/spec/frontend/feature_flags/components/new_environments_dropdown_spec.js
index 63fa5d19982..1c0c444c296 100644
--- a/spec/frontend/feature_flags/components/new_environments_dropdown_spec.js
+++ b/spec/frontend/feature_flags/components/new_environments_dropdown_spec.js
@@ -31,17 +31,17 @@ describe('New Environments Dropdown', () => {
describe('before results', () => {
it('should show a loading icon', () => {
axiosMock.onGet(TEST_HOST).reply(() => {
- expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
+ expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
});
- wrapper.find(GlSearchBoxByType).vm.$emit('focus');
+ wrapper.findComponent(GlSearchBoxByType).vm.$emit('focus');
return axios.waitForAll();
});
it('should not show any dropdown items', () => {
axiosMock.onGet(TEST_HOST).reply(() => {
- expect(wrapper.findAll(GlDropdownItem)).toHaveLength(0);
+ expect(wrapper.findAllComponents(GlDropdownItem)).toHaveLength(0);
});
- wrapper.find(GlSearchBoxByType).vm.$emit('focus');
+ wrapper.findComponent(GlSearchBoxByType).vm.$emit('focus');
return axios.waitForAll();
});
});
@@ -50,11 +50,11 @@ describe('New Environments Dropdown', () => {
let item;
beforeEach(async () => {
axiosMock.onGet(TEST_HOST).reply(200, []);
- wrapper.find(GlSearchBoxByType).vm.$emit('focus');
- wrapper.find(GlSearchBoxByType).vm.$emit('input', TEST_SEARCH);
+ wrapper.findComponent(GlSearchBoxByType).vm.$emit('focus');
+ wrapper.findComponent(GlSearchBoxByType).vm.$emit('input', TEST_SEARCH);
await axios.waitForAll();
await nextTick();
- item = wrapper.find(GlDropdownItem);
+ item = wrapper.findComponent(GlDropdownItem);
});
it('should display a Create item label', () => {
@@ -62,7 +62,7 @@ describe('New Environments Dropdown', () => {
});
it('should display that no matching items are found', () => {
- expect(wrapper.find({ ref: 'noResults' }).exists()).toBe(true);
+ expect(wrapper.findComponent({ ref: 'noResults' }).exists()).toBe(true);
});
it('should emit a new scope when selected', () => {
@@ -75,10 +75,10 @@ describe('New Environments Dropdown', () => {
let items;
beforeEach(() => {
axiosMock.onGet(TEST_HOST).reply(httpStatusCodes.OK, ['prod', 'production']);
- wrapper.find(GlSearchBoxByType).vm.$emit('focus');
- wrapper.find(GlSearchBoxByType).vm.$emit('input', 'prod');
+ wrapper.findComponent(GlSearchBoxByType).vm.$emit('focus');
+ wrapper.findComponent(GlSearchBoxByType).vm.$emit('input', 'prod');
return axios.waitForAll().then(() => {
- items = wrapper.findAll(GlDropdownItem);
+ items = wrapper.findAllComponents(GlDropdownItem);
});
});
@@ -97,7 +97,7 @@ describe('New Environments Dropdown', () => {
});
it('should not display a message about no results', () => {
- expect(wrapper.find({ ref: 'noResults' }).exists()).toBe(false);
+ expect(wrapper.findComponent({ ref: 'noResults' }).exists()).toBe(false);
});
});
});
diff --git a/spec/frontend/feature_flags/components/new_feature_flag_spec.js b/spec/frontend/feature_flags/components/new_feature_flag_spec.js
index 688ba54f919..300d0e47082 100644
--- a/spec/frontend/feature_flags/components/new_feature_flag_spec.js
+++ b/spec/frontend/feature_flags/components/new_feature_flag_spec.js
@@ -40,7 +40,7 @@ describe('New feature flag form', () => {
};
const findWarningGlAlert = () =>
- wrapper.findAll(GlAlert).filter((c) => c.props('variant') === 'warning');
+ wrapper.findAllComponents(GlAlert).filter((c) => c.props('variant') === 'warning');
beforeEach(() => {
factory();
@@ -65,11 +65,11 @@ describe('New feature flag form', () => {
});
it('should render feature flag form', () => {
- expect(wrapper.find(Form).exists()).toEqual(true);
+ expect(wrapper.findComponent(Form).exists()).toEqual(true);
});
it('has an all users strategy by default', () => {
- const strategies = wrapper.find(Form).props('strategies');
+ const strategies = wrapper.findComponent(Form).props('strategies');
expect(strategies).toEqual([allUsersStrategy]);
});
diff --git a/spec/frontend/feature_flags/components/strategies/flexible_rollout_spec.js b/spec/frontend/feature_flags/components/strategies/flexible_rollout_spec.js
index 56b14d80ab3..70a9156b5a9 100644
--- a/spec/frontend/feature_flags/components/strategies/flexible_rollout_spec.js
+++ b/spec/frontend/feature_flags/components/strategies/flexible_rollout_spec.js
@@ -34,12 +34,12 @@ describe('feature_flags/components/strategies/flexible_rollout.vue', () => {
percentageFormGroup = wrapper
.find('[data-testid="strategy-flexible-rollout-percentage"]')
- .find(ParameterFormGroup);
- percentageInput = percentageFormGroup.find(GlFormInput);
+ .findComponent(ParameterFormGroup);
+ percentageInput = percentageFormGroup.findComponent(GlFormInput);
stickinessFormGroup = wrapper
.find('[data-testid="strategy-flexible-rollout-stickiness"]')
- .find(ParameterFormGroup);
- stickinessSelect = stickinessFormGroup.find(GlFormSelect);
+ .findComponent(ParameterFormGroup);
+ stickinessSelect = stickinessFormGroup.findComponent(GlFormSelect);
});
it('displays the current percentage value', () => {
@@ -94,7 +94,7 @@ describe('feature_flags/components/strategies/flexible_rollout.vue', () => {
it('shows errors', () => {
const formGroup = wrapper
.find('[data-testid="strategy-flexible-rollout-percentage"]')
- .find(ParameterFormGroup);
+ .findComponent(ParameterFormGroup);
expect(formGroup.attributes('state')).toBeUndefined();
});
@@ -108,7 +108,7 @@ describe('feature_flags/components/strategies/flexible_rollout.vue', () => {
it('shows errors', () => {
const formGroup = wrapper
.find('[data-testid="strategy-flexible-rollout-percentage"]')
- .find(ParameterFormGroup);
+ .findComponent(ParameterFormGroup);
expect(formGroup.attributes('state')).toBeUndefined();
});
diff --git a/spec/frontend/feature_flags/components/strategies/gitlab_user_list_spec.js b/spec/frontend/feature_flags/components/strategies/gitlab_user_list_spec.js
index 3b69194494f..96b9434f3ec 100644
--- a/spec/frontend/feature_flags/components/strategies/gitlab_user_list_spec.js
+++ b/spec/frontend/feature_flags/components/strategies/gitlab_user_list_spec.js
@@ -24,10 +24,10 @@ describe('~/feature_flags/components/strategies/gitlab_user_list.vue', () => {
propsData: { ...DEFAULT_PROPS, ...props },
});
- const findDropdown = () => wrapper.find(GlDropdown);
+ const findDropdown = () => wrapper.findComponent(GlDropdown);
describe('with user lists', () => {
- const findDropdownItem = () => wrapper.find(GlDropdownItem);
+ const findDropdownItem = () => wrapper.findComponent(GlDropdownItem);
beforeEach(() => {
Api.searchFeatureFlagUserLists.mockResolvedValue({ data: [userList] });
@@ -69,10 +69,10 @@ describe('~/feature_flags/components/strategies/gitlab_user_list.vue', () => {
r = resolve;
}),
);
- const searchWrapper = wrapper.find(GlSearchBoxByType);
+ const searchWrapper = wrapper.findComponent(GlSearchBoxByType);
searchWrapper.vm.$emit('input', 'new');
await nextTick();
- const loadingIcon = wrapper.find(GlLoadingIcon);
+ const loadingIcon = wrapper.findComponent(GlLoadingIcon);
expect(loadingIcon.exists()).toBe(true);
expect(Api.searchFeatureFlagUserLists).toHaveBeenCalledWith('1', 'new');
diff --git a/spec/frontend/feature_flags/components/strategies/parameter_form_group_spec.js b/spec/frontend/feature_flags/components/strategies/parameter_form_group_spec.js
index 33696064d55..23ad0d3a08d 100644
--- a/spec/frontend/feature_flags/components/strategies/parameter_form_group_spec.js
+++ b/spec/frontend/feature_flags/components/strategies/parameter_form_group_spec.js
@@ -20,7 +20,7 @@ describe('~/feature_flags/strategies/parameter_form_group.vue', () => {
},
});
- formGroup = wrapper.find(GlFormGroup);
+ formGroup = wrapper.findComponent(GlFormGroup);
slot = wrapper.find('[data-testid="slot"]');
});
diff --git a/spec/frontend/feature_flags/components/strategies/percent_rollout_spec.js b/spec/frontend/feature_flags/components/strategies/percent_rollout_spec.js
index 180697e93e4..cb422a018f9 100644
--- a/spec/frontend/feature_flags/components/strategies/percent_rollout_spec.js
+++ b/spec/frontend/feature_flags/components/strategies/percent_rollout_spec.js
@@ -30,8 +30,8 @@ describe('~/feature_flags/components/strategies/percent_rollout.vue', () => {
beforeEach(() => {
wrapper = factory();
- input = wrapper.find(GlFormInput);
- formGroup = wrapper.find(ParameterFormGroup);
+ input = wrapper.findComponent(GlFormInput);
+ formGroup = wrapper.findComponent(ParameterFormGroup);
});
it('displays the current value', () => {
@@ -55,8 +55,8 @@ describe('~/feature_flags/components/strategies/percent_rollout.vue', () => {
beforeEach(() => {
wrapper = factory({ strategy: { parameters: { percentage: '101' } } });
- input = wrapper.find(GlFormInput);
- formGroup = wrapper.find(ParameterFormGroup);
+ input = wrapper.findComponent(GlFormInput);
+ formGroup = wrapper.findComponent(ParameterFormGroup);
});
it('shows errors', () => {
@@ -68,8 +68,8 @@ describe('~/feature_flags/components/strategies/percent_rollout.vue', () => {
beforeEach(() => {
wrapper = factory({ strategy: { parameters: { percentage: '3.14' } } });
- input = wrapper.find(GlFormInput);
- formGroup = wrapper.find(ParameterFormGroup);
+ input = wrapper.findComponent(GlFormInput);
+ formGroup = wrapper.findComponent(ParameterFormGroup);
});
it('shows errors', () => {
diff --git a/spec/frontend/feature_flags/components/strategies/users_with_id_spec.js b/spec/frontend/feature_flags/components/strategies/users_with_id_spec.js
index 745fbca00fe..0a72714c22a 100644
--- a/spec/frontend/feature_flags/components/strategies/users_with_id_spec.js
+++ b/spec/frontend/feature_flags/components/strategies/users_with_id_spec.js
@@ -15,7 +15,7 @@ describe('~/feature_flags/components/users_with_id.vue', () => {
beforeEach(() => {
wrapper = factory();
- textarea = wrapper.find(GlFormTextarea);
+ textarea = wrapper.findComponent(GlFormTextarea);
});
afterEach(() => {
diff --git a/spec/frontend/feature_flags/components/strategy_parameters_spec.js b/spec/frontend/feature_flags/components/strategy_parameters_spec.js
index 979ca255b08..d0f1f7d0e2a 100644
--- a/spec/frontend/feature_flags/components/strategy_parameters_spec.js
+++ b/spec/frontend/feature_flags/components/strategy_parameters_spec.js
@@ -51,11 +51,11 @@ describe('~/feature_flags/components/strategy_parameters.vue', () => {
});
it('should show the correct component', () => {
- expect(wrapper.find(component).exists()).toBe(true);
+ expect(wrapper.findComponent(component).exists()).toBe(true);
});
it('should emit changes from the lower component', () => {
- const strategyParameterWrapper = wrapper.find(component);
+ const strategyParameterWrapper = wrapper.findComponent(component);
strategyParameterWrapper.vm.$emit('change', { parameters: { foo: 'bar' } });
@@ -77,7 +77,7 @@ describe('~/feature_flags/components/strategy_parameters.vue', () => {
strategy,
});
- expect(wrapper.find(UsersWithId).props('strategy')).toEqual(strategy);
+ expect(wrapper.findComponent(UsersWithId).props('strategy')).toEqual(strategy);
});
});
});
diff --git a/spec/frontend/feature_flags/components/strategy_spec.js b/spec/frontend/feature_flags/components/strategy_spec.js
index aee3873721c..84d4180fe63 100644
--- a/spec/frontend/feature_flags/components/strategy_spec.js
+++ b/spec/frontend/feature_flags/components/strategy_spec.js
@@ -32,8 +32,8 @@ Vue.use(Vuex);
describe('Feature flags strategy', () => {
let wrapper;
- const findStrategyParameters = () => wrapper.find(StrategyParameters);
- const findDocsLinks = () => wrapper.findAll(GlLink);
+ const findStrategyParameters = () => wrapper.findComponent(StrategyParameters);
+ const findDocsLinks = () => wrapper.findAllComponents(GlLink);
const factory = (
opts = {
@@ -93,7 +93,7 @@ describe('Feature flags strategy', () => {
});
it('should set the select to match the strategy name', () => {
- expect(wrapper.find(GlFormSelect).element.value).toBe(name);
+ expect(wrapper.findComponent(GlFormSelect).element.value).toBe(name);
});
it('should emit a change if the parameters component does', () => {
@@ -118,7 +118,7 @@ describe('Feature flags strategy', () => {
});
it('shows an alert asking users to consider using flexibleRollout instead', () => {
- expect(wrapper.find(GlAlert).text()).toContain(
+ expect(wrapper.findComponent(GlAlert).text()).toContain(
'Consider using the more flexible "Percent rollout" strategy instead.',
);
});
@@ -139,10 +139,10 @@ describe('Feature flags strategy', () => {
});
it('should revert to all-environments scope when last scope is removed', async () => {
- const token = wrapper.find(GlToken);
+ const token = wrapper.findComponent(GlToken);
token.vm.$emit('close');
await nextTick();
- expect(wrapper.findAll(GlToken)).toHaveLength(0);
+ expect(wrapper.findAllComponents(GlToken)).toHaveLength(0);
expect(last(wrapper.emitted('change'))).toEqual([
{
name: ROLLOUT_STRATEGY_PERCENT_ROLLOUT,
@@ -167,7 +167,7 @@ describe('Feature flags strategy', () => {
});
it('should change the parameters if a different strategy is chosen', async () => {
- const select = wrapper.find(GlFormSelect);
+ const select = wrapper.findComponent(GlFormSelect);
select.setValue(ROLLOUT_STRATEGY_ALL_USERS);
await nextTick();
expect(last(wrapper.emitted('change'))).toEqual([
@@ -180,26 +180,26 @@ describe('Feature flags strategy', () => {
});
it('should display selected scopes', async () => {
- const dropdown = wrapper.find(NewEnvironmentsDropdown);
+ const dropdown = wrapper.findComponent(NewEnvironmentsDropdown);
dropdown.vm.$emit('add', 'production');
await nextTick();
- expect(wrapper.findAll(GlToken)).toHaveLength(1);
- expect(wrapper.find(GlToken).text()).toBe('production');
+ expect(wrapper.findAllComponents(GlToken)).toHaveLength(1);
+ expect(wrapper.findComponent(GlToken).text()).toBe('production');
});
it('should display all selected scopes', async () => {
- const dropdown = wrapper.find(NewEnvironmentsDropdown);
+ const dropdown = wrapper.findComponent(NewEnvironmentsDropdown);
dropdown.vm.$emit('add', 'production');
dropdown.vm.$emit('add', 'staging');
await nextTick();
- const tokens = wrapper.findAll(GlToken);
+ const tokens = wrapper.findAllComponents(GlToken);
expect(tokens).toHaveLength(2);
expect(tokens.at(0).text()).toBe('production');
expect(tokens.at(1).text()).toBe('staging');
});
it('should emit selected scopes', async () => {
- const dropdown = wrapper.find(NewEnvironmentsDropdown);
+ const dropdown = wrapper.findComponent(NewEnvironmentsDropdown);
dropdown.vm.$emit('add', 'production');
await nextTick();
expect(last(wrapper.emitted('change'))).toEqual([
@@ -215,7 +215,7 @@ describe('Feature flags strategy', () => {
});
it('should emit a delete if the delete button is clicked', () => {
- wrapper.find(GlButton).vm.$emit('click');
+ wrapper.findComponent(GlButton).vm.$emit('click');
expect(wrapper.emitted('delete')).toEqual([[]]);
});
});
@@ -232,26 +232,26 @@ describe('Feature flags strategy', () => {
});
it('should display selected scopes', async () => {
- const dropdown = wrapper.find(NewEnvironmentsDropdown);
+ const dropdown = wrapper.findComponent(NewEnvironmentsDropdown);
dropdown.vm.$emit('add', 'production');
await nextTick();
- expect(wrapper.findAll(GlToken)).toHaveLength(1);
- expect(wrapper.find(GlToken).text()).toBe('production');
+ expect(wrapper.findAllComponents(GlToken)).toHaveLength(1);
+ expect(wrapper.findComponent(GlToken).text()).toBe('production');
});
it('should display all selected scopes', async () => {
- const dropdown = wrapper.find(NewEnvironmentsDropdown);
+ const dropdown = wrapper.findComponent(NewEnvironmentsDropdown);
dropdown.vm.$emit('add', 'production');
dropdown.vm.$emit('add', 'staging');
await nextTick();
- const tokens = wrapper.findAll(GlToken);
+ const tokens = wrapper.findAllComponents(GlToken);
expect(tokens).toHaveLength(2);
expect(tokens.at(0).text()).toBe('production');
expect(tokens.at(1).text()).toBe('staging');
});
it('should emit selected scopes', async () => {
- const dropdown = wrapper.find(NewEnvironmentsDropdown);
+ const dropdown = wrapper.findComponent(NewEnvironmentsDropdown);
dropdown.vm.$emit('add', 'production');
await nextTick();
expect(last(wrapper.emitted('change'))).toEqual([
diff --git a/spec/frontend/fixtures/integrations.rb b/spec/frontend/fixtures/integrations.rb
index 1bafb0bfe78..45d1c400f5d 100644
--- a/spec/frontend/fixtures/integrations.rb
+++ b/spec/frontend/fixtures/integrations.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe Projects::Settings::IntegrationsController, '(JavaScript fixtures)', type: :controller do
include JavaScriptFixturesHelpers
- let(:namespace) { create(:namespace, name: 'frontend-fixtures' )}
+ let(:namespace) { create(:namespace, name: 'frontend-fixtures' ) }
let(:project) { create(:project_empty_repo, namespace: namespace, path: 'integrations-project') }
let!(:service) { create(:custom_issue_tracker_integration, project: project) }
let(:user) { project.first_owner }
diff --git a/spec/frontend/fixtures/issues.rb b/spec/frontend/fixtures/issues.rb
index 8bedb802242..cde796497d4 100644
--- a/spec/frontend/fixtures/issues.rb
+++ b/spec/frontend/fixtures/issues.rb
@@ -106,3 +106,43 @@ RSpec.describe API::Issues, '(JavaScript fixtures)', type: :request do
expect(response).to be_successful
end
end
+
+RSpec.describe GraphQL::Query, type: :request do
+ include ApiHelpers
+ include GraphqlHelpers
+ include JavaScriptFixturesHelpers
+
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project) }
+ let_it_be(:issue_type) { 'issue' }
+
+ before_all do
+ project.add_reporter(user)
+ end
+
+ issue_popover_query_path = 'issuable/popover/queries/issue.query.graphql'
+
+ it "graphql/#{issue_popover_query_path}.json" do
+ query = get_graphql_query_as_string(issue_popover_query_path, ee: Gitlab.ee?)
+
+ issue = create(
+ :issue,
+ project: project,
+ confidential: true,
+ created_at: Time.parse('2020-07-01T04:08:01Z'),
+ due_date: Date.new(2020, 7, 5),
+ milestone: create(
+ :milestone,
+ project: project,
+ title: '15.2',
+ start_date: Date.new(2020, 7, 1),
+ due_date: Date.new(2020, 7, 30)
+ ),
+ issue_type: issue_type
+ )
+
+ post_graphql(query, current_user: user, variables: { projectPath: project.full_path, iid: issue.iid.to_s })
+
+ expect_graphql_errors_to_be_empty
+ end
+end
diff --git a/spec/frontend/fixtures/namespaces.rb b/spec/frontend/fixtures/namespaces.rb
new file mode 100644
index 00000000000..b11f661fe09
--- /dev/null
+++ b/spec/frontend/fixtures/namespaces.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Jobs (JavaScript fixtures)' do
+ include ApiHelpers
+ include JavaScriptFixturesHelpers
+ include GraphqlHelpers
+
+ describe GraphQL::Query, 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
+
+ query_name = 'search_namespaces_where_user_can_transfer_projects'
+ query_extension = '.query.graphql'
+
+ full_input_path = "projects/settings/graphql/queries/#{query_name}#{query_extension}"
+ base_output_path = "graphql/projects/settings/#{query_name}"
+
+ it "#{base_output_path}_page_1#{query_extension}.json" do
+ query = get_graphql_query_as_string(full_input_path)
+
+ post_graphql(query, current_user: user, variables: { first: 2 })
+
+ expect_graphql_errors_to_be_empty
+ end
+
+ it "#{base_output_path}_page_2#{query_extension}.json" do
+ query = get_graphql_query_as_string(full_input_path)
+
+ post_graphql(query, current_user: user, variables: { first: 2 })
+
+ post_graphql(
+ query,
+ current_user: user,
+ variables: { first: 2, after: graphql_data_at('currentUser', 'groups', 'pageInfo', 'endCursor') }
+ )
+
+ expect_graphql_errors_to_be_empty
+ end
+ end
+end
diff --git a/spec/frontend/fixtures/prometheus_integration.rb b/spec/frontend/fixtures/prometheus_integration.rb
index 883dbb929a2..250c50bc8bb 100644
--- a/spec/frontend/fixtures/prometheus_integration.rb
+++ b/spec/frontend/fixtures/prometheus_integration.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe Projects::Settings::IntegrationsController, '(JavaScript fixtures)', type: :controller do
include JavaScriptFixturesHelpers
- let(:namespace) { create(:namespace, name: 'frontend-fixtures' )}
+ let(:namespace) { create(:namespace, name: 'frontend-fixtures' ) }
let(:project) { create(:project_empty_repo, namespace: namespace, path: 'integrations-project') }
let!(:integration) { create(:prometheus_integration, project: project) }
let(:user) { project.first_owner }
diff --git a/spec/frontend/fixtures/runner.rb b/spec/frontend/fixtures/runner.rb
index 36281af0219..b523650dda5 100644
--- a/spec/frontend/fixtures/runner.rb
+++ b/spec/frontend/fixtures/runner.rb
@@ -13,11 +13,12 @@ RSpec.describe 'Runner (JavaScript fixtures)' do
let_it_be(:project) { create(:project, :repository, :public) }
let_it_be(:project_2) { create(:project, :repository, :public) }
- let_it_be(:instance_runner) { create(:ci_runner, :instance, version: '1.0.0', revision: '123', description: 'Instance runner', ip_address: '127.0.0.1') }
- let_it_be(:group_runner) { create(:ci_runner, :group, groups: [group], active: false, version: '2.0.0', revision: '456', description: 'Group runner', ip_address: '127.0.0.1') }
- let_it_be(:group_runner_2) { create(:ci_runner, :group, groups: [group], active: false, version: '2.0.0', revision: '456', description: 'Group runner 2', ip_address: '127.0.0.1') }
- let_it_be(:project_runner) { create(:ci_runner, :project, projects: [project, project_2], active: false, version: '2.0.0', revision: '456', description: 'Project runner', ip_address: '127.0.0.1') }
- let_it_be(:build) { create(:ci_build, runner: instance_runner) }
+ let_it_be(:runner) { create(:ci_runner, :instance, description: 'My Runner', version: '1.0.0') }
+ let_it_be(:group_runner) { create(:ci_runner, :group, groups: [group], version: '2.0.0') }
+ let_it_be(:group_runner_2) { create(:ci_runner, :group, groups: [group], version: '2.0.0') }
+ let_it_be(:project_runner) { create(:ci_runner, :project, projects: [project, project_2], version: '2.0.0') }
+
+ let_it_be(:build) { create(:ci_build, runner: runner) }
query_path = 'runner/graphql/'
fixtures_path = 'graphql/runner/'
@@ -27,18 +28,19 @@ RSpec.describe 'Runner (JavaScript fixtures)' do
end
before do
- allow(Gitlab::Ci::RunnerUpgradeCheck.instance)
- .to receive(:check_runner_upgrade_status)
- .and_return({ not_available: nil })
+ allow_next_instance_of(::Gitlab::Ci::RunnerUpgradeCheck) do |instance|
+ allow(instance).to receive(:check_runner_upgrade_suggestion)
+ .and_return([nil, :not_available])
+ end
end
- describe do
+ describe 'as admin', GraphQL::Query do
before do
sign_in(admin)
enable_admin_mode!(admin)
end
- describe GraphQL::Query, type: :request do
+ describe 'all_runners.query.graphql', type: :request do
all_runners_query = 'list/all_runners.query.graphql'
let_it_be(:query) do
@@ -58,7 +60,7 @@ RSpec.describe 'Runner (JavaScript fixtures)' do
end
end
- describe GraphQL::Query, type: :request do
+ describe 'all_runners_count.query.graphql', type: :request do
all_runners_count_query = 'list/all_runners_count.query.graphql'
let_it_be(:query) do
@@ -72,7 +74,7 @@ RSpec.describe 'Runner (JavaScript fixtures)' do
end
end
- describe GraphQL::Query, type: :request do
+ describe 'runner.query.graphql', type: :request do
runner_query = 'show/runner.query.graphql'
let_it_be(:query) do
@@ -81,7 +83,7 @@ RSpec.describe 'Runner (JavaScript fixtures)' do
it "#{fixtures_path}#{runner_query}.json" do
post_graphql(query, current_user: admin, variables: {
- id: instance_runner.to_global_id.to_s
+ id: runner.to_global_id.to_s
})
expect_graphql_errors_to_be_empty
@@ -96,7 +98,7 @@ RSpec.describe 'Runner (JavaScript fixtures)' do
end
end
- describe GraphQL::Query, type: :request do
+ describe 'runner_projects.query.graphql', type: :request do
runner_projects_query = 'show/runner_projects.query.graphql'
let_it_be(:query) do
@@ -112,7 +114,7 @@ RSpec.describe 'Runner (JavaScript fixtures)' do
end
end
- describe GraphQL::Query, type: :request do
+ describe 'runner_jobs.query.graphql', type: :request do
runner_jobs_query = 'show/runner_jobs.query.graphql'
let_it_be(:query) do
@@ -121,14 +123,14 @@ RSpec.describe 'Runner (JavaScript fixtures)' do
it "#{fixtures_path}#{runner_jobs_query}.json" do
post_graphql(query, current_user: admin, variables: {
- id: instance_runner.to_global_id.to_s
+ id: runner.to_global_id.to_s
})
expect_graphql_errors_to_be_empty
end
end
- describe GraphQL::Query, type: :request do
+ describe 'runner_form.query.graphql', type: :request do
runner_jobs_query = 'edit/runner_form.query.graphql'
let_it_be(:query) do
@@ -137,7 +139,7 @@ RSpec.describe 'Runner (JavaScript fixtures)' do
it "#{fixtures_path}#{runner_jobs_query}.json" do
post_graphql(query, current_user: admin, variables: {
- id: instance_runner.to_global_id.to_s
+ id: runner.to_global_id.to_s
})
expect_graphql_errors_to_be_empty
@@ -145,14 +147,14 @@ RSpec.describe 'Runner (JavaScript fixtures)' do
end
end
- describe do
+ describe 'as group owner', GraphQL::Query do
let_it_be(:group_owner) { create(:user) }
before do
group.add_owner(group_owner)
end
- describe GraphQL::Query, type: :request do
+ describe 'group_runners.query.graphql', type: :request do
group_runners_query = 'list/group_runners.query.graphql'
let_it_be(:query) do
@@ -177,7 +179,7 @@ RSpec.describe 'Runner (JavaScript fixtures)' do
end
end
- describe GraphQL::Query, type: :request do
+ describe 'group_runners_count.query.graphql', type: :request do
group_runners_count_query = 'list/group_runners_count.query.graphql'
let_it_be(:query) do
diff --git a/spec/frontend/frequent_items/components/app_spec.js b/spec/frontend/frequent_items/components/app_spec.js
index 32c66c0d288..c201bbf4af2 100644
--- a/spec/frontend/frequent_items/components/app_spec.js
+++ b/spec/frontend/frequent_items/components/app_spec.js
@@ -145,11 +145,16 @@ describe('Frequent Items App Component', () => {
expect(findFrequentItemsList().props()).toEqual(
expect.objectContaining({
items: mockSearchedProjects.data.map(
- ({ avatar_url, web_url, name_with_namespace, ...item }) => ({
+ ({
+ avatar_url: avatarUrl,
+ web_url: webUrl,
+ name_with_namespace: namespace,
+ ...item
+ }) => ({
...item,
- avatarUrl: avatar_url,
- webUrl: web_url,
- namespace: name_with_namespace,
+ avatarUrl,
+ webUrl,
+ namespace,
}),
),
namespace: TEST_NAMESPACE,
diff --git a/spec/frontend/gfm_auto_complete/mock_data.js b/spec/frontend/gfm_auto_complete/mock_data.js
new file mode 100644
index 00000000000..86795ffd0a5
--- /dev/null
+++ b/spec/frontend/gfm_auto_complete/mock_data.js
@@ -0,0 +1,34 @@
+export const eventlistenersMockDefaultMap = [
+ {
+ key: 'shown',
+ namespace: 'atwho',
+ },
+ {
+ key: 'shown-users',
+ namespace: 'atwho',
+ },
+ {
+ key: 'shown-issues',
+ namespace: 'atwho',
+ },
+ {
+ key: 'shown-milestones',
+ namespace: 'atwho',
+ },
+ {
+ key: 'shown-mergerequests',
+ namespace: 'atwho',
+ },
+ {
+ key: 'shown-labels',
+ namespace: 'atwho',
+ },
+ {
+ key: 'shown-snippets',
+ namespace: 'atwho',
+ },
+ {
+ key: 'shown-contacts',
+ namespace: 'atwho',
+ },
+];
diff --git a/spec/frontend/gfm_auto_complete_spec.js b/spec/frontend/gfm_auto_complete_spec.js
index 072cf34d0ef..c3dfc4570f9 100644
--- a/spec/frontend/gfm_auto_complete_spec.js
+++ b/spec/frontend/gfm_auto_complete_spec.js
@@ -10,6 +10,7 @@ import { TEST_HOST } from 'helpers/test_constants';
import waitForPromises from 'helpers/wait_for_promises';
import AjaxCache from '~/lib/utils/ajax_cache';
import axios from '~/lib/utils/axios_utils';
+import { eventlistenersMockDefaultMap } from 'ee_else_ce_jest/gfm_auto_complete/mock_data';
describe('GfmAutoComplete', () => {
const fetchDataMock = { fetchData: jest.fn() };
@@ -457,12 +458,12 @@ describe('GfmAutoComplete', () => {
it('should be false with actual array data', () => {
expect(
- GfmAutoComplete.isLoading([{ title: 'Foo' }, { title: 'Bar' }, { title: 'Qux' }]),
+ GfmAutoComplete.isLoading([{ title: 'events' }, { title: 'Bar' }, { title: 'Qux' }]),
).toBe(false);
});
it('should be false with actual data item', () => {
- expect(GfmAutoComplete.isLoading({ title: 'Foo' })).toBe(false);
+ expect(GfmAutoComplete.isLoading({ title: 'events' })).toBe(false);
});
});
@@ -884,4 +885,47 @@ describe('GfmAutoComplete', () => {
).toBe(`<li><small>${escapedPayload} ${escapedPayload}</small> ${escapedPayload}</li>`);
});
});
+
+ describe('autocomplete show eventlisteners', () => {
+ let $textarea;
+
+ beforeEach(() => {
+ setHTMLFixture('<textarea></textarea>');
+ $textarea = $('textarea');
+ });
+
+ it('sets correct eventlisteners when autocomplete features are enabled', () => {
+ const autocomplete = new GfmAutoComplete({});
+ autocomplete.setup($textarea);
+ autocomplete.setupAtWho($textarea);
+ /* eslint-disable-next-line no-underscore-dangle */
+ const events = $._data($textarea[0], 'events');
+ expect(
+ Object.keys(events)
+ .filter((x) => {
+ return x.startsWith('shown');
+ })
+ .map((e) => {
+ return { key: e, namespace: events[e][0].namespace };
+ }),
+ ).toEqual(expect.arrayContaining(eventlistenersMockDefaultMap));
+ });
+
+ it('sets no eventlisteners when features are disabled', () => {
+ const autocomplete = new GfmAutoComplete({});
+ autocomplete.setup($textarea, {});
+ autocomplete.setupAtWho($textarea);
+ /* eslint-disable-next-line no-underscore-dangle */
+ const events = $._data($textarea[0], 'events');
+ expect(
+ Object.keys(events)
+ .filter((x) => {
+ return x.startsWith('shown');
+ })
+ .map((e) => {
+ return { key: e, namespace: events[e][0].namespace };
+ }),
+ ).toStrictEqual([]);
+ });
+ });
});
diff --git a/spec/frontend/group_settings/components/shared_runners_form_spec.js b/spec/frontend/group_settings/components/shared_runners_form_spec.js
index 70a22c86e62..5282c0ed839 100644
--- a/spec/frontend/group_settings/components/shared_runners_form_spec.js
+++ b/spec/frontend/group_settings/components/shared_runners_form_spec.js
@@ -1,24 +1,24 @@
import { GlAlert } from '@gitlab/ui';
-import MockAxiosAdapter from 'axios-mock-adapter';
import { nextTick } from 'vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import SharedRunnersForm from '~/group_settings/components/shared_runners_form.vue';
-import axios from '~/lib/utils/axios_utils';
+import { updateGroup } from '~/api/groups_api';
-const UPDATE_PATH = '/test/update';
+jest.mock('~/api/groups_api');
+
+const GROUP_ID = '99';
const RUNNER_ENABLED_VALUE = 'enabled';
const RUNNER_DISABLED_VALUE = 'disabled_and_unoverridable';
const RUNNER_ALLOW_OVERRIDE_VALUE = 'disabled_with_override';
describe('group_settings/components/shared_runners_form', () => {
let wrapper;
- let mock;
const createComponent = (provide = {}) => {
wrapper = shallowMountExtended(SharedRunnersForm, {
provide: {
- updatePath: UPDATE_PATH,
+ groupId: GROUP_ID,
sharedRunnersSetting: RUNNER_ENABLED_VALUE,
parentSharedRunnersSetting: null,
runnerEnabledValue: RUNNER_ENABLED_VALUE,
@@ -36,18 +36,19 @@ describe('group_settings/components/shared_runners_form', () => {
.at(0);
const findSharedRunnersToggle = () => wrapper.findByTestId('shared-runners-toggle');
const findOverrideToggle = () => wrapper.findByTestId('override-runners-toggle');
- const getSharedRunnersSetting = () => JSON.parse(mock.history.put[0].data).shared_runners_setting;
+ const getSharedRunnersSetting = () => {
+ return updateGroup.mock.calls[0][1].shared_runners_setting;
+ };
beforeEach(() => {
- mock = new MockAxiosAdapter(axios);
- mock.onPut(UPDATE_PATH).reply(200);
+ updateGroup.mockResolvedValue({});
});
afterEach(() => {
wrapper.destroy();
wrapper = null;
- mock.restore();
+ updateGroup.mockReset();
});
describe('default state', () => {
@@ -115,7 +116,7 @@ describe('group_settings/components/shared_runners_form', () => {
findSharedRunnersToggle().vm.$emit('change', false);
await waitForPromises();
- expect(mock.history.put.length).toBe(1);
+ expect(updateGroup).toHaveBeenCalledTimes(1);
});
it('is not loading state after completed request', async () => {
@@ -170,12 +171,14 @@ describe('group_settings/components/shared_runners_form', () => {
});
describe.each`
- errorObj | message
+ errorData | message
${{}} | ${'An error occurred while updating configuration. Refresh the page and try again.'}
${{ error: 'Undefined error' }} | ${'Undefined error Refresh the page and try again.'}
- `(`with error $errorObj`, ({ errorObj, message }) => {
+ `(`with error $errorObj`, ({ errorData, message }) => {
beforeEach(async () => {
- mock.onPut(UPDATE_PATH).reply(500, errorObj);
+ updateGroup.mockRejectedValue({
+ response: { data: errorData },
+ });
createComponent();
findSharedRunnersToggle().vm.$emit('change', false);
diff --git a/spec/frontend/groups/components/app_spec.js b/spec/frontend/groups/components/app_spec.js
index 9e4666ffc70..a6bbea648d2 100644
--- a/spec/frontend/groups/components/app_spec.js
+++ b/spec/frontend/groups/components/app_spec.js
@@ -85,30 +85,6 @@ describe('AppComponent', () => {
await nextTick();
});
- describe('computed', () => {
- describe('groups', () => {
- it('should return list of groups from store', () => {
- jest.spyOn(vm.store, 'getGroups').mockImplementation(() => {});
-
- const { groups } = vm;
-
- expect(vm.store.getGroups).toHaveBeenCalled();
- expect(groups).not.toBeDefined();
- });
- });
-
- describe('pageInfo', () => {
- it('should return pagination info from store', () => {
- jest.spyOn(vm.store, 'getPaginationInfo').mockImplementation(() => {});
-
- const { pageInfo } = vm;
-
- expect(vm.store.getPaginationInfo).toHaveBeenCalled();
- expect(pageInfo).not.toBeDefined();
- });
- });
- });
-
describe('methods', () => {
describe('fetchGroups', () => {
it('should call `getGroups` with all the params provided', () => {
diff --git a/spec/frontend/groups/components/group_item_spec.js b/spec/frontend/groups/components/group_item_spec.js
index 0bc80df6535..9906f62878f 100644
--- a/spec/frontend/groups/components/group_item_spec.js
+++ b/spec/frontend/groups/components/group_item_spec.js
@@ -6,19 +6,20 @@ import ItemActions from '~/groups/components/item_actions.vue';
import eventHub from '~/groups/event_hub';
import { getGroupItemMicrodata } from '~/groups/store/utils';
import * as urlUtilities from '~/lib/utils/url_utility';
+import { ITEM_TYPE } from '~/groups/constants';
import {
- ITEM_TYPE,
- VISIBILITY_INTERNAL,
- VISIBILITY_PRIVATE,
- VISIBILITY_PUBLIC,
-} from '~/groups/constants';
-import { mountExtended } from 'helpers/vue_test_utils_helper';
+ VISIBILITY_LEVEL_PRIVATE,
+ VISIBILITY_LEVEL_INTERNAL,
+ VISIBILITY_LEVEL_PUBLIC,
+} from '~/visibility_level/constants';
+import { helpPagePath } from '~/helpers/help_page_helper';
+import { mountExtended, extendedWrapper } from 'helpers/vue_test_utils_helper';
import { mockParentGroupItem, mockChildren } from '../mock_data';
const createComponent = (
propsData = { group: mockParentGroupItem, parentGroup: mockChildren[0] },
provide = {
- currentGroupVisibility: VISIBILITY_PRIVATE,
+ currentGroupVisibility: VISIBILITY_LEVEL_PRIVATE,
},
) => {
return mountExtended(GroupItem, {
@@ -289,7 +290,7 @@ describe('GroupItemComponent', () => {
});
describe('visibility warning popover', () => {
- const findPopover = () => wrapper.findComponent(GlPopover);
+ const findPopover = () => extendedWrapper(wrapper.findComponent(GlPopover));
const itDoesNotRenderVisibilityWarningPopover = () => {
it('does not render visibility warning popover', () => {
@@ -319,13 +320,16 @@ describe('GroupItemComponent', () => {
describe('when showing projects', () => {
describe.each`
- itemVisibility | currentGroupVisibility | isPopoverShown
- ${VISIBILITY_PRIVATE} | ${VISIBILITY_PUBLIC} | ${false}
- ${VISIBILITY_INTERNAL} | ${VISIBILITY_PUBLIC} | ${false}
- ${VISIBILITY_PUBLIC} | ${VISIBILITY_PUBLIC} | ${false}
- ${VISIBILITY_PRIVATE} | ${VISIBILITY_PRIVATE} | ${false}
- ${VISIBILITY_INTERNAL} | ${VISIBILITY_PRIVATE} | ${true}
- ${VISIBILITY_PUBLIC} | ${VISIBILITY_PRIVATE} | ${true}
+ itemVisibility | currentGroupVisibility | isPopoverShown
+ ${VISIBILITY_LEVEL_PRIVATE} | ${VISIBILITY_LEVEL_PUBLIC} | ${false}
+ ${VISIBILITY_LEVEL_INTERNAL} | ${VISIBILITY_LEVEL_PUBLIC} | ${false}
+ ${VISIBILITY_LEVEL_PUBLIC} | ${VISIBILITY_LEVEL_PUBLIC} | ${false}
+ ${VISIBILITY_LEVEL_PRIVATE} | ${VISIBILITY_LEVEL_PRIVATE} | ${false}
+ ${VISIBILITY_LEVEL_INTERNAL} | ${VISIBILITY_LEVEL_PRIVATE} | ${true}
+ ${VISIBILITY_LEVEL_PUBLIC} | ${VISIBILITY_LEVEL_PRIVATE} | ${true}
+ ${VISIBILITY_LEVEL_PRIVATE} | ${VISIBILITY_LEVEL_INTERNAL} | ${false}
+ ${VISIBILITY_LEVEL_INTERNAL} | ${VISIBILITY_LEVEL_INTERNAL} | ${false}
+ ${VISIBILITY_LEVEL_PUBLIC} | ${VISIBILITY_LEVEL_INTERNAL} | ${true}
`(
'when item visibility is $itemVisibility and parent group visibility is $currentGroupVisibility',
({ itemVisibility, currentGroupVisibility, isPopoverShown }) => {
@@ -347,8 +351,17 @@ describe('GroupItemComponent', () => {
});
if (isPopoverShown) {
- it('renders visibility warning popover', () => {
- expect(findPopover().exists()).toBe(true);
+ it('renders visibility warning popover with `Learn more` link', () => {
+ const popover = findPopover();
+
+ expect(popover.exists()).toBe(true);
+ expect(
+ popover.findByRole('link', { name: GroupItem.i18n.learnMore }).attributes('href'),
+ ).toBe(
+ helpPagePath('user/project/members/share_project_with_groups', {
+ anchor: 'sharing-projects-with-groups-of-a-higher-restrictive-visibility-level',
+ }),
+ );
});
} else {
itDoesNotRenderVisibilityWarningPopover();
@@ -361,7 +374,7 @@ describe('GroupItemComponent', () => {
wrapper = createComponent({
group: {
...mockParentGroupItem,
- visibility: VISIBILITY_PUBLIC,
+ visibility: VISIBILITY_LEVEL_PUBLIC,
type: ITEM_TYPE.PROJECT,
},
parentGroup: mockChildren[0],
diff --git a/spec/frontend/groups/components/group_name_and_path_spec.js b/spec/frontend/groups/components/group_name_and_path_spec.js
index 9c9bdead6fa..823d2ed286a 100644
--- a/spec/frontend/groups/components/group_name_and_path_spec.js
+++ b/spec/frontend/groups/components/group_name_and_path_spec.js
@@ -1,18 +1,23 @@
-import { nextTick } from 'vue';
+import Vue, { nextTick } from 'vue';
+import VueApollo from 'vue-apollo';
import { merge } from 'lodash';
-import { GlAlert } from '@gitlab/ui';
+import { GlAlert, GlDropdown, GlTruncate, GlDropdownItem } from '@gitlab/ui';
import { mountExtended, extendedWrapper } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
+import createMockApollo from 'helpers/mock_apollo_helper';
import GroupNameAndPath from '~/groups/components/group_name_and_path.vue';
import { getGroupPathAvailability } from '~/rest_api';
import { createAlert } from '~/flash';
import { helpPagePath } from '~/helpers/help_page_helper';
+import searchGroupsWhereUserCanCreateSubgroups from '~/groups/queries/search_groups_where_user_can_create_subgroups.query.graphql';
jest.mock('~/flash');
jest.mock('~/rest_api', () => ({
getGroupPathAvailability: jest.fn(),
}));
+Vue.use(VueApollo);
+
describe('GroupNameAndPath', () => {
let wrapper;
@@ -20,6 +25,17 @@ describe('GroupNameAndPath', () => {
const mockGroupUrl = 'my-awesome-group';
const mockGroupUrlSuggested = 'my-awesome-group1';
+ const mockQueryResponse = jest.fn().mockResolvedValue({
+ data: {
+ currentUser: {
+ id: '1',
+ groups: {
+ nodes: [{ id: '2', fullPath: '/path2' }],
+ },
+ },
+ },
+ });
+
const defaultProvide = {
basePath: 'http://gitlab.com/',
fields: {
@@ -32,13 +48,20 @@ describe('GroupNameAndPath', () => {
pattern: '[a-zA-Z0-9_\\.][a-zA-Z0-9_\\-\\.]*[a-zA-Z0-9_\\-]|[a-zA-Z0-9_]',
},
parentId: { name: 'group[parent_id]', id: 'group_parent_id', value: '1' },
+ parentFullPath: { name: 'group[parent_full_path]', id: 'group_full_path', value: '/path1' },
groupId: { name: 'group[id]', id: 'group_id', value: '' },
},
+ newSubgroup: false,
mattermostEnabled: false,
};
const createComponent = ({ provide = {} } = {}) => {
- wrapper = mountExtended(GroupNameAndPath, { provide: merge({}, defaultProvide, provide) });
+ wrapper = mountExtended(GroupNameAndPath, {
+ provide: merge({}, defaultProvide, provide),
+ apolloProvider: createMockApollo([
+ [searchGroupsWhereUserCanCreateSubgroups, mockQueryResponse],
+ ]),
+ });
};
const createComponentEditGroup = ({ path = mockGroupUrl } = {}) => {
createComponent({
@@ -46,8 +69,11 @@ describe('GroupNameAndPath', () => {
});
};
- const findGroupNameField = () => wrapper.findByLabelText(GroupNameAndPath.i18n.inputs.name.label);
- const findGroupUrlField = () => wrapper.findByLabelText(GroupNameAndPath.i18n.inputs.path.label);
+ const findGroupNameField = () => wrapper.findByLabelText('Group name');
+ const findGroupUrlField = () => wrapper.findByLabelText('Group URL');
+ const findSubgroupNameField = () => wrapper.findByLabelText('Subgroup name');
+ const findSubgroupSlugField = () => wrapper.findByLabelText('Subgroup slug');
+ const findSelectedGroup = () => wrapper.findComponent(GlTruncate);
const findAlert = () => extendedWrapper(wrapper.findComponent(GlAlert));
const apiMockAvailablePath = () => {
@@ -79,6 +105,41 @@ describe('GroupNameAndPath', () => {
});
});
+ describe('when creating a new subgroup', () => {
+ beforeEach(() => {
+ createComponent({ provide: { newSubgroup: true } });
+ });
+
+ it('updates `Subgroup slug` field as user types', async () => {
+ await findSubgroupNameField().setValue(mockGroupName);
+
+ expect(findSubgroupSlugField().element.value).toBe(mockGroupUrl);
+ });
+
+ describe('when user selects parent group', () => {
+ it('updates `Subgroup URL` dropdown and calls API', async () => {
+ expect(findSelectedGroup().text()).toContain('/path1');
+
+ await findSubgroupNameField().setValue(mockGroupName);
+
+ wrapper.findComponent(GlDropdown).vm.$emit('shown');
+ await wrapper.vm.$apollo.queries.currentUserGroups.refetch();
+ jest.runOnlyPendingTimers();
+ await waitForPromises();
+
+ wrapper.findComponent(GlDropdownItem).vm.$emit('click');
+ await nextTick();
+
+ expect(findSelectedGroup().text()).toContain('/path2');
+ expect(getGroupPathAvailability).toHaveBeenCalled();
+
+ expect(wrapper.findByText(GroupNameAndPath.i18n.inputs.path.validFeedback).exists()).toBe(
+ true,
+ );
+ });
+ });
+ });
+
describe('when editing a group', () => {
it('does not update `Group URL` field and does not call API', async () => {
const groupUrl = 'foo-bar';
@@ -346,9 +407,7 @@ describe('GroupNameAndPath', () => {
it('shows `Group ID` field', () => {
createComponentEditGroup();
- expect(
- wrapper.findByLabelText(GroupNameAndPath.i18n.inputs.groupId.label).element.value,
- ).toBe('1');
+ expect(wrapper.findByLabelText('Group ID').element.value).toBe('1');
});
});
});
diff --git a/spec/frontend/groups/components/groups_spec.js b/spec/frontend/groups/components/groups_spec.js
index 48a2319cf96..6c1eb373b7e 100644
--- a/spec/frontend/groups/components/groups_spec.js
+++ b/spec/frontend/groups/components/groups_spec.js
@@ -6,7 +6,7 @@ import GroupItemComponent from '~/groups/components/group_item.vue';
import PaginationLinks from '~/vue_shared/components/pagination_links.vue';
import GroupsComponent from '~/groups/components/groups.vue';
import eventHub from '~/groups/event_hub';
-import { VISIBILITY_PRIVATE } from '~/groups/constants';
+import { VISIBILITY_LEVEL_PRIVATE } from '~/visibility_level/constants';
import { mockGroups, mockPageInfo } from '../mock_data';
describe('GroupsComponent', () => {
@@ -26,7 +26,7 @@ describe('GroupsComponent', () => {
...propsData,
},
provide: {
- currentGroupVisibility: VISIBILITY_PRIVATE,
+ currentGroupVisibility: VISIBILITY_LEVEL_PRIVATE,
},
});
};
diff --git a/spec/frontend/groups/components/transfer_group_form_spec.js b/spec/frontend/groups/components/transfer_group_form_spec.js
index 6dc760f4f7c..8cfe8ce8e18 100644
--- a/spec/frontend/groups/components/transfer_group_form_spec.js
+++ b/spec/frontend/groups/components/transfer_group_form_spec.js
@@ -82,7 +82,6 @@ describe('Transfer group form', () => {
it('sets the confirm danger properties', () => {
expect(findConfirmDanger().props()).toMatchObject({
- buttonClass: 'qa-transfer-button',
disabled: true,
buttonText: confirmButtonText,
phrase: confirmationPhrase,
diff --git a/spec/frontend/header_search/components/app_spec.js b/spec/frontend/header_search/components/app_spec.js
index d89218f5542..6a138f9a247 100644
--- a/spec/frontend/header_search/components/app_spec.js
+++ b/spec/frontend/header_search/components/app_spec.js
@@ -15,6 +15,10 @@ import {
ICON_GROUP,
ICON_SUBGROUP,
SCOPE_TOKEN_MAX_LENGTH,
+ IS_SEARCHING,
+ IS_NOT_FOCUSED,
+ IS_FOCUSED,
+ SEARCH_SHORTCUTS_MIN_CHARACTERS,
} from '~/header_search/constants';
import DropdownKeyboardNavigation from '~/vue_shared/components/dropdown_keyboard_navigation.vue';
import { ENTER_KEY } from '~/lib/utils/keys';
@@ -170,6 +174,14 @@ describe('HeaderSearchApp', () => {
it(`should render the Dropdown Navigation Component`, () => {
expect(findDropdownKeyboardNavigation().exists()).toBe(true);
});
+
+ it(`should close the dropdown when press escape key`, async () => {
+ findHeaderSearchInput().vm.$emit('keydown', new KeyboardEvent({ key: 27 }));
+ await nextTick();
+ expect(findHeaderSearchDropdown().exists()).toBe(false);
+ // only one event emmited from findHeaderSearchInput().vm.$emit('click');
+ expect(wrapper.emitted().expandSearchBar.length).toBe(1);
+ });
});
});
@@ -245,6 +257,7 @@ describe('HeaderSearchApp', () => {
searchOptions: () => searchOptions,
},
);
+ findHeaderSearchInput().vm.$emit('click');
});
it(`${hasToken ? 'is' : 'is NOT'} rendered when data set has type "${
@@ -263,47 +276,43 @@ describe('HeaderSearchApp', () => {
});
});
- describe('form wrapper', () => {
+ describe('form', () => {
describe.each`
- searchContext | search | searchOptions
- ${MOCK_SEARCH_CONTEXT_FULL} | ${null} | ${[]}
- ${MOCK_SEARCH_CONTEXT_FULL} | ${MOCK_SEARCH} | ${[]}
- ${MOCK_SEARCH_CONTEXT_FULL} | ${MOCK_SEARCH} | ${MOCK_SCOPED_SEARCH_OPTIONS}
- ${null} | ${MOCK_SEARCH} | ${MOCK_SCOPED_SEARCH_OPTIONS}
- ${null} | ${null} | ${MOCK_SCOPED_SEARCH_OPTIONS}
- ${null} | ${null} | ${[]}
- `('', ({ searchContext, search, searchOptions }) => {
+ searchContext | search | searchOptions | isFocused
+ ${MOCK_SEARCH_CONTEXT_FULL} | ${null} | ${[]} | ${true}
+ ${MOCK_SEARCH_CONTEXT_FULL} | ${MOCK_SEARCH} | ${[]} | ${true}
+ ${MOCK_SEARCH_CONTEXT_FULL} | ${MOCK_SEARCH} | ${MOCK_SCOPED_SEARCH_OPTIONS} | ${true}
+ ${MOCK_SEARCH_CONTEXT_FULL} | ${MOCK_SEARCH} | ${MOCK_SCOPED_SEARCH_OPTIONS} | ${false}
+ ${null} | ${MOCK_SEARCH} | ${MOCK_SCOPED_SEARCH_OPTIONS} | ${true}
+ ${null} | ${null} | ${MOCK_SCOPED_SEARCH_OPTIONS} | ${true}
+ ${null} | ${null} | ${[]} | ${true}
+ `('wrapper', ({ searchContext, search, searchOptions, isFocused }) => {
beforeEach(() => {
window.gon.current_username = MOCK_USERNAME;
-
createComponent({ search, searchContext }, { searchOptions: () => searchOptions });
-
- findHeaderSearchInput().vm.$emit('click');
+ if (isFocused) {
+ findHeaderSearchInput().vm.$emit('click');
+ }
});
- const hasIcon = Boolean(searchContext?.group);
- const isSearching = Boolean(search);
- const isActive = Boolean(searchOptions.length > 0);
+ const isSearching = search?.length > SEARCH_SHORTCUTS_MIN_CHARACTERS;
- it(`${hasIcon ? 'with' : 'without'} search context classes contain "${
- hasIcon ? 'has-icon' : 'has-no-icon'
- }"`, () => {
- const iconClassRegex = hasIcon ? 'has-icon' : 'has-no-icon';
- expect(findHeaderSearchForm().classes()).toContain(iconClassRegex);
+ it(`classes ${isSearching ? 'contain' : 'do not contain'} "${IS_SEARCHING}"`, () => {
+ if (isSearching) {
+ expect(findHeaderSearchForm().classes()).toContain(IS_SEARCHING);
+ return;
+ }
+ if (!isSearching) {
+ expect(findHeaderSearchForm().classes()).not.toContain(IS_SEARCHING);
+ }
});
- it(`${isSearching ? 'with' : 'without'} search string classes contain "${
- isSearching ? 'is-searching' : 'is-not-searching'
+ it(`classes ${isSearching ? 'contain' : 'do not contain'} "${
+ isFocused ? IS_FOCUSED : IS_NOT_FOCUSED
}"`, () => {
- const iconClassRegex = isSearching ? 'is-searching' : 'is-not-searching';
- expect(findHeaderSearchForm().classes()).toContain(iconClassRegex);
- });
-
- it(`${isActive ? 'with' : 'without'} search results classes contain "${
- isActive ? 'is-active' : 'is-not-active'
- }"`, () => {
- const iconClassRegex = isActive ? 'is-active' : 'is-not-active';
- expect(findHeaderSearchForm().classes()).toContain(iconClassRegex);
+ expect(findHeaderSearchForm().classes()).toContain(
+ isFocused ? IS_FOCUSED : IS_NOT_FOCUSED,
+ );
});
});
});
@@ -323,6 +332,7 @@ describe('HeaderSearchApp', () => {
searchOptions: () => searchOptions,
},
);
+ findHeaderSearchInput().vm.$emit('click');
});
it(`icon for data set type "${searchOptions[0]?.html_id}" ${
diff --git a/spec/frontend/helpers/diffs_helper_spec.js b/spec/frontend/helpers/diffs_helper_spec.js
index b223d48bf5c..c1ac7fac3fd 100644
--- a/spec/frontend/helpers/diffs_helper_spec.js
+++ b/spec/frontend/helpers/diffs_helper_spec.js
@@ -14,45 +14,45 @@ describe('diffs helper', () => {
describe('hasInlineLines', () => {
it('is false when the file does not exist', () => {
- expect(diffsHelper.hasInlineLines()).toBeFalsy();
+ expect(diffsHelper.hasInlineLines()).toBe(false);
});
it('is false when the file does not have the highlighted_diff_lines property', () => {
const missingInline = getDiffFile({ highlighted_diff_lines: undefined });
- expect(diffsHelper.hasInlineLines(missingInline)).toBeFalsy();
+ expect(diffsHelper.hasInlineLines(missingInline)).toBe(false);
});
it('is false when the file has zero highlighted_diff_lines', () => {
const emptyInline = getDiffFile({ highlighted_diff_lines: [] });
- expect(diffsHelper.hasInlineLines(emptyInline)).toBeFalsy();
+ expect(diffsHelper.hasInlineLines(emptyInline)).toBe(false);
});
it('is true when the file has at least 1 highlighted_diff_lines', () => {
- expect(diffsHelper.hasInlineLines(getDiffFile())).toBeTruthy();
+ expect(diffsHelper.hasInlineLines(getDiffFile())).toBe(true);
});
});
describe('hasParallelLines', () => {
it('is false when the file does not exist', () => {
- expect(diffsHelper.hasParallelLines()).toBeFalsy();
+ expect(diffsHelper.hasParallelLines()).toBe(false);
});
it('is false when the file does not have the parallel_diff_lines property', () => {
const missingInline = getDiffFile({ parallel_diff_lines: undefined });
- expect(diffsHelper.hasParallelLines(missingInline)).toBeFalsy();
+ expect(diffsHelper.hasParallelLines(missingInline)).toBe(false);
});
it('is false when the file has zero parallel_diff_lines', () => {
const emptyInline = getDiffFile({ parallel_diff_lines: [] });
- expect(diffsHelper.hasParallelLines(emptyInline)).toBeFalsy();
+ expect(diffsHelper.hasParallelLines(emptyInline)).toBe(false);
});
it('is true when the file has at least 1 parallel_diff_lines', () => {
- expect(diffsHelper.hasParallelLines(getDiffFile())).toBeTruthy();
+ expect(diffsHelper.hasParallelLines(getDiffFile())).toBe(true);
});
});
@@ -61,16 +61,16 @@ describe('diffs helper', () => {
const noParallelLines = getDiffFile({ parallel_diff_lines: undefined });
const emptyParallelLines = getDiffFile({ parallel_diff_lines: [] });
- expect(diffsHelper.isSingleViewStyle(noParallelLines)).toBeTruthy();
- expect(diffsHelper.isSingleViewStyle(emptyParallelLines)).toBeTruthy();
+ expect(diffsHelper.isSingleViewStyle(noParallelLines)).toBe(true);
+ expect(diffsHelper.isSingleViewStyle(emptyParallelLines)).toBe(true);
});
it('is true when the file has at least 1 parallel line but no inline lines for any reason', () => {
const noInlineLines = getDiffFile({ highlighted_diff_lines: undefined });
const emptyInlineLines = getDiffFile({ highlighted_diff_lines: [] });
- expect(diffsHelper.isSingleViewStyle(noInlineLines)).toBeTruthy();
- expect(diffsHelper.isSingleViewStyle(emptyInlineLines)).toBeTruthy();
+ expect(diffsHelper.isSingleViewStyle(noInlineLines)).toBe(true);
+ expect(diffsHelper.isSingleViewStyle(emptyInlineLines)).toBe(true);
});
it('is true when the file does not have any inline lines or parallel lines for any reason', () => {
@@ -83,13 +83,13 @@ describe('diffs helper', () => {
parallel_diff_lines: [],
});
- expect(diffsHelper.isSingleViewStyle(noLines)).toBeTruthy();
- expect(diffsHelper.isSingleViewStyle(emptyLines)).toBeTruthy();
- expect(diffsHelper.isSingleViewStyle()).toBeTruthy();
+ expect(diffsHelper.isSingleViewStyle(noLines)).toBe(true);
+ expect(diffsHelper.isSingleViewStyle(emptyLines)).toBe(true);
+ expect(diffsHelper.isSingleViewStyle()).toBe(true);
});
it('is false when the file has both inline and parallel lines', () => {
- expect(diffsHelper.isSingleViewStyle(getDiffFile())).toBeFalsy();
+ expect(diffsHelper.isSingleViewStyle(getDiffFile())).toBe(false);
});
});
diff --git a/spec/frontend/ide/components/activity_bar_spec.js b/spec/frontend/ide/components/activity_bar_spec.js
index 39fe2c7e723..a97e883a8bf 100644
--- a/spec/frontend/ide/components/activity_bar_spec.js
+++ b/spec/frontend/ide/components/activity_bar_spec.js
@@ -1,86 +1,82 @@
-import Vue, { nextTick } from 'vue';
-import { createComponentWithStore } from 'helpers/vue_mount_component_helper';
+import { GlBadge } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
import ActivityBar from '~/ide/components/activity_bar.vue';
import { leftSidebarViews } from '~/ide/constants';
import { createStore } from '~/ide/stores';
-describe('IDE activity bar', () => {
- const Component = Vue.extend(ActivityBar);
- let vm;
+describe('IDE ActivityBar component', () => {
+ let wrapper;
let store;
- const findChangesBadge = () => vm.$el.querySelector('.badge');
+ const findChangesBadge = () => wrapper.findComponent(GlBadge);
- beforeEach(() => {
+ const mountComponent = (state) => {
store = createStore();
-
- Vue.set(store.state.projects, 'abcproject', {
- web_url: 'testing',
+ store.replaceState({
+ ...store.state,
+ projects: { abcproject: { web_url: 'testing' } },
+ currentProjectId: 'abcproject',
+ ...state,
});
- Vue.set(store.state, 'currentProjectId', 'abcproject');
- vm = createComponentWithStore(Component, store);
- });
+ wrapper = shallowMount(ActivityBar, { store });
+ };
afterEach(() => {
- vm.$destroy();
+ wrapper.destroy();
});
describe('updateActivityBarView', () => {
beforeEach(() => {
- jest.spyOn(vm, 'updateActivityBarView').mockImplementation(() => {});
-
- vm.$mount();
+ mountComponent();
+ jest.spyOn(wrapper.vm, 'updateActivityBarView').mockImplementation(() => {});
});
it('calls updateActivityBarView with edit value on click', () => {
- vm.$el.querySelector('.js-ide-edit-mode').click();
+ wrapper.find('.js-ide-edit-mode').trigger('click');
- expect(vm.updateActivityBarView).toHaveBeenCalledWith(leftSidebarViews.edit.name);
+ expect(wrapper.vm.updateActivityBarView).toHaveBeenCalledWith(leftSidebarViews.edit.name);
});
it('calls updateActivityBarView with commit value on click', () => {
- vm.$el.querySelector('.js-ide-commit-mode').click();
+ wrapper.find('.js-ide-commit-mode').trigger('click');
- expect(vm.updateActivityBarView).toHaveBeenCalledWith(leftSidebarViews.commit.name);
+ expect(wrapper.vm.updateActivityBarView).toHaveBeenCalledWith(leftSidebarViews.commit.name);
});
it('calls updateActivityBarView with review value on click', () => {
- vm.$el.querySelector('.js-ide-review-mode').click();
+ wrapper.find('.js-ide-review-mode').trigger('click');
- expect(vm.updateActivityBarView).toHaveBeenCalledWith(leftSidebarViews.review.name);
+ expect(wrapper.vm.updateActivityBarView).toHaveBeenCalledWith(leftSidebarViews.review.name);
});
});
describe('active item', () => {
- beforeEach(() => {
- vm.$mount();
- });
-
it('sets edit item active', () => {
- expect(vm.$el.querySelector('.js-ide-edit-mode').classList).toContain('active');
+ mountComponent();
+
+ expect(wrapper.find('.js-ide-edit-mode').classes()).toContain('active');
});
- it('sets commit item active', async () => {
- vm.$store.state.currentActivityView = leftSidebarViews.commit.name;
+ it('sets commit item active', () => {
+ mountComponent({ currentActivityView: leftSidebarViews.commit.name });
- await nextTick();
- expect(vm.$el.querySelector('.js-ide-commit-mode').classList).toContain('active');
+ expect(wrapper.find('.js-ide-commit-mode').classes()).toContain('active');
});
});
describe('changes badge', () => {
it('is rendered when files are staged', () => {
- store.state.stagedFiles = [{ path: '/path/to/file' }];
- vm.$mount();
+ mountComponent({ stagedFiles: [{ path: '/path/to/file' }] });
- expect(findChangesBadge()).toBeTruthy();
- expect(findChangesBadge().textContent.trim()).toBe('1');
+ expect(findChangesBadge().exists()).toBe(true);
+ expect(findChangesBadge().text()).toBe('1');
});
it('is not rendered when no changes are present', () => {
- vm.$mount();
- expect(findChangesBadge()).toBeFalsy();
+ mountComponent();
+
+ expect(findChangesBadge().exists()).toBe(false);
});
});
});
diff --git a/spec/frontend/ide/components/branches/item_spec.js b/spec/frontend/ide/components/branches/item_spec.js
index 271d0600e16..3dbd1210916 100644
--- a/spec/frontend/ide/components/branches/item_spec.js
+++ b/spec/frontend/ide/components/branches/item_spec.js
@@ -44,8 +44,8 @@ describe('IDE branch item', () => {
});
it('renders branch name and timeago', () => {
expect(wrapper.text()).toContain(TEST_BRANCH.name);
- expect(wrapper.find(Timeago).props('time')).toBe(TEST_BRANCH.committedDate);
- expect(wrapper.find(GlIcon).exists()).toBe(false);
+ expect(wrapper.findComponent(Timeago).props('time')).toBe(TEST_BRANCH.committedDate);
+ expect(wrapper.findComponent(GlIcon).exists()).toBe(false);
});
it('renders link to branch', () => {
@@ -60,6 +60,6 @@ describe('IDE branch item', () => {
it('renders icon if is not active', () => {
createComponent({ isActive: true });
- expect(wrapper.find(GlIcon).exists()).toBe(true);
+ expect(wrapper.findComponent(GlIcon).exists()).toBe(true);
});
});
diff --git a/spec/frontend/ide/components/branches/search_list_spec.js b/spec/frontend/ide/components/branches/search_list_spec.js
index b6e3274153a..bbde45d700f 100644
--- a/spec/frontend/ide/components/branches/search_list_spec.js
+++ b/spec/frontend/ide/components/branches/search_list_spec.js
@@ -47,7 +47,7 @@ describe('IDE branches search list', () => {
it('renders loading icon when `isLoading` is true', () => {
createComponent({ isLoading: true });
- expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
+ expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
});
it('renders branches not found when search is not empty and branches list is empty', async () => {
@@ -61,7 +61,7 @@ describe('IDE branches search list', () => {
describe('with branches', () => {
it('renders list', () => {
createComponent({ branches });
- const items = wrapper.findAll(Item);
+ const items = wrapper.findAllComponents(Item);
expect(items.length).toBe(branches.length);
});
@@ -69,7 +69,7 @@ describe('IDE branches search list', () => {
it('renders check next to active branch', () => {
const activeBranch = 'regular';
createComponent({ branches }, activeBranch);
- const items = wrapper.findAll(Item).filter((w) => w.props('isActive'));
+ const items = wrapper.findAllComponents(Item).filter((w) => w.props('isActive'));
expect(items.length).toBe(1);
expect(items.at(0).props('item').name).toBe(activeBranch);
diff --git a/spec/frontend/ide/components/commit_sidebar/editor_header_spec.js b/spec/frontend/ide/components/commit_sidebar/editor_header_spec.js
index d77e8e3d04c..f6d5833edee 100644
--- a/spec/frontend/ide/components/commit_sidebar/editor_header_spec.js
+++ b/spec/frontend/ide/components/commit_sidebar/editor_header_spec.js
@@ -26,8 +26,8 @@ describe('IDE commit editor header', () => {
});
};
- const findDiscardModal = () => wrapper.find({ ref: 'discardModal' });
- const findDiscardButton = () => wrapper.find({ ref: 'discardButton' });
+ const findDiscardModal = () => wrapper.findComponent({ ref: 'discardModal' });
+ const findDiscardButton = () => wrapper.findComponent({ ref: 'discardButton' });
beforeEach(() => {
store = createStore();
diff --git a/spec/frontend/ide/components/commit_sidebar/form_spec.js b/spec/frontend/ide/components/commit_sidebar/form_spec.js
index 28f62a9775a..a8ee81afa0b 100644
--- a/spec/frontend/ide/components/commit_sidebar/form_spec.js
+++ b/spec/frontend/ide/components/commit_sidebar/form_spec.js
@@ -58,7 +58,7 @@ describe('IDE commit form', () => {
});
const findForm = () => wrapper.find('form');
const submitForm = () => findForm().trigger('submit');
- const findCommitMessageInput = () => wrapper.find(CommitMessageField);
+ const findCommitMessageInput = () => wrapper.findComponent(CommitMessageField);
const setCommitMessageInput = (val) => findCommitMessageInput().vm.$emit('input', val);
const findDiscardDraftButton = () => wrapper.find('[data-testid="discard-draft"]');
@@ -302,7 +302,7 @@ describe('IDE commit form', () => {
${() => createCodeownersCommitError('test message')} | ${{ actionPrimary: { text: 'Create new branch' } }}
${createUnexpectedCommitError} | ${{ actionPrimary: null }}
`('opens error modal if commitError with $error', async ({ createError, props }) => {
- const modal = wrapper.find(GlModal);
+ const modal = wrapper.findComponent(GlModal);
modal.vm.show = jest.fn();
const error = createError();
@@ -343,7 +343,7 @@ describe('IDE commit form', () => {
await nextTick();
- wrapper.find(GlModal).vm.$emit('ok');
+ wrapper.findComponent(GlModal).vm.$emit('ok');
await waitForPromises();
diff --git a/spec/frontend/ide/components/error_message_spec.js b/spec/frontend/ide/components/error_message_spec.js
index 17568158131..204d39de741 100644
--- a/spec/frontend/ide/components/error_message_spec.js
+++ b/spec/frontend/ide/components/error_message_spec.js
@@ -105,7 +105,7 @@ describe('IDE error message component', () => {
findActionButton().trigger('click');
await nextTick();
- expect(wrapper.find(GlLoadingIcon).isVisible()).toBe(true);
+ expect(wrapper.findComponent(GlLoadingIcon).isVisible()).toBe(true);
resolveAction();
});
@@ -113,7 +113,7 @@ describe('IDE error message component', () => {
findActionButton().trigger('click');
await actionMock();
await nextTick();
- expect(wrapper.find(GlLoadingIcon).isVisible()).toBe(false);
+ expect(wrapper.findComponent(GlLoadingIcon).isVisible()).toBe(false);
});
});
});
diff --git a/spec/frontend/ide/components/file_templates/dropdown_spec.js b/spec/frontend/ide/components/file_templates/dropdown_spec.js
index e54b322b9db..ee90d87357c 100644
--- a/spec/frontend/ide/components/file_templates/dropdown_spec.js
+++ b/spec/frontend/ide/components/file_templates/dropdown_spec.js
@@ -94,7 +94,7 @@ describe('IDE file templates dropdown component', () => {
it('shows loader when isLoading is true', () => {
createComponent({ props: defaultAsyncProps, state: { isLoading: true } });
- expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
+ expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
});
it('renders templates', () => {
diff --git a/spec/frontend/ide/components/ide_file_row_spec.js b/spec/frontend/ide/components/ide_file_row_spec.js
index baf3d7cca9d..aa66224fa19 100644
--- a/spec/frontend/ide/components/ide_file_row_spec.js
+++ b/spec/frontend/ide/components/ide_file_row_spec.js
@@ -39,8 +39,8 @@ describe('Ide File Row component', () => {
wrapper = null;
});
- const findFileRowExtra = () => wrapper.find(FileRowExtra);
- const findFileRow = () => wrapper.find(FileRow);
+ const findFileRowExtra = () => wrapper.findComponent(FileRowExtra);
+ const findFileRow = () => wrapper.findComponent(FileRow);
const hasDropdownOpen = () => findFileRowExtra().props('dropdownOpen');
it('fileRow component has listeners', async () => {
diff --git a/spec/frontend/ide/components/ide_project_header_spec.js b/spec/frontend/ide/components/ide_project_header_spec.js
index fc39651c661..d0636352a3f 100644
--- a/spec/frontend/ide/components/ide_project_header_spec.js
+++ b/spec/frontend/ide/components/ide_project_header_spec.js
@@ -3,6 +3,7 @@ import IDEProjectHeader from '~/ide/components/ide_project_header.vue';
import ProjectAvatar from '~/vue_shared/components/project_avatar.vue';
const mockProject = {
+ id: 1,
name: 'test proj',
avatar_url: 'https://gitlab.com',
path_with_namespace: 'path/with-namespace',
@@ -30,6 +31,7 @@ describe('IDE project header', () => {
it('renders ProjectAvatar with correct props', () => {
expect(findProjectAvatar().props()).toMatchObject({
+ projectId: mockProject.id,
projectName: mockProject.name,
projectAvatarUrl: mockProject.avatar_url,
});
diff --git a/spec/frontend/ide/components/ide_review_spec.js b/spec/frontend/ide/components/ide_review_spec.js
index 13d20761263..0759f957374 100644
--- a/spec/frontend/ide/components/ide_review_spec.js
+++ b/spec/frontend/ide/components/ide_review_spec.js
@@ -42,7 +42,7 @@ describe('IDE review mode', () => {
let inititializeSpy;
beforeEach(async () => {
- inititializeSpy = jest.spyOn(wrapper.find(IdeReview).vm, 'initialize');
+ inititializeSpy = jest.spyOn(wrapper.findComponent(IdeReview).vm, 'initialize');
store.state.viewer = 'editor';
await wrapper.vm.reactivate();
@@ -85,7 +85,7 @@ describe('IDE review mode', () => {
});
it('renders edit dropdown', () => {
- expect(wrapper.find(EditorModeDropdown).exists()).toBe(true);
+ expect(wrapper.findComponent(EditorModeDropdown).exists()).toBe(true);
});
it('renders merge request link & IID', async () => {
diff --git a/spec/frontend/ide/components/ide_side_bar_spec.js b/spec/frontend/ide/components/ide_side_bar_spec.js
index 4469c3fc901..4784d6c516f 100644
--- a/spec/frontend/ide/components/ide_side_bar_spec.js
+++ b/spec/frontend/ide/components/ide_side_bar_spec.js
@@ -47,32 +47,32 @@ describe('IdeSidebar', () => {
await nextTick();
- expect(wrapper.findAll(GlSkeletonLoader)).toHaveLength(3);
+ expect(wrapper.findAllComponents(GlSkeletonLoader)).toHaveLength(3);
});
describe('deferred rendering components', () => {
it('fetches components on demand', async () => {
wrapper = createComponent();
- expect(wrapper.find(IdeTree).exists()).toBe(true);
- expect(wrapper.find(IdeReview).exists()).toBe(false);
- expect(wrapper.find(RepoCommitSection).exists()).toBe(false);
+ expect(wrapper.findComponent(IdeTree).exists()).toBe(true);
+ expect(wrapper.findComponent(IdeReview).exists()).toBe(false);
+ expect(wrapper.findComponent(RepoCommitSection).exists()).toBe(false);
store.state.currentActivityView = leftSidebarViews.review.name;
await waitForPromises();
await nextTick();
- expect(wrapper.find(IdeTree).exists()).toBe(false);
- expect(wrapper.find(IdeReview).exists()).toBe(true);
- expect(wrapper.find(RepoCommitSection).exists()).toBe(false);
+ expect(wrapper.findComponent(IdeTree).exists()).toBe(false);
+ expect(wrapper.findComponent(IdeReview).exists()).toBe(true);
+ expect(wrapper.findComponent(RepoCommitSection).exists()).toBe(false);
store.state.currentActivityView = leftSidebarViews.commit.name;
await waitForPromises();
await nextTick();
- expect(wrapper.find(IdeTree).exists()).toBe(false);
- expect(wrapper.find(IdeReview).exists()).toBe(false);
- expect(wrapper.find(RepoCommitSection).exists()).toBe(true);
+ expect(wrapper.findComponent(IdeTree).exists()).toBe(false);
+ expect(wrapper.findComponent(IdeReview).exists()).toBe(false);
+ expect(wrapper.findComponent(RepoCommitSection).exists()).toBe(true);
});
it.each`
view | tree | review | commit
@@ -86,23 +86,23 @@ describe('IdeSidebar', () => {
await waitForPromises();
await nextTick();
- expect(wrapper.find(IdeTree).exists()).toBe(tree);
- expect(wrapper.find(IdeReview).exists()).toBe(review);
- expect(wrapper.find(RepoCommitSection).exists()).toBe(commit);
+ expect(wrapper.findComponent(IdeTree).exists()).toBe(tree);
+ expect(wrapper.findComponent(IdeReview).exists()).toBe(review);
+ expect(wrapper.findComponent(RepoCommitSection).exists()).toBe(commit);
});
});
it('keeps the current activity view components alive', async () => {
wrapper = createComponent();
- const ideTreeComponent = wrapper.find(IdeTree).element;
+ const ideTreeComponent = wrapper.findComponent(IdeTree).element;
store.state.currentActivityView = leftSidebarViews.commit.name;
await waitForPromises();
await nextTick();
- expect(wrapper.find(IdeTree).exists()).toBe(false);
- expect(wrapper.find(RepoCommitSection).exists()).toBe(true);
+ expect(wrapper.findComponent(IdeTree).exists()).toBe(false);
+ expect(wrapper.findComponent(RepoCommitSection).exists()).toBe(true);
store.state.currentActivityView = leftSidebarViews.edit.name;
@@ -110,6 +110,6 @@ describe('IdeSidebar', () => {
await nextTick();
// reference to the elements remains the same, meaning the components were kept alive
- expect(wrapper.find(IdeTree).element).toEqual(ideTreeComponent);
+ expect(wrapper.findComponent(IdeTree).element).toEqual(ideTreeComponent);
});
});
diff --git a/spec/frontend/ide/components/ide_sidebar_nav_spec.js b/spec/frontend/ide/components/ide_sidebar_nav_spec.js
index 2ea0c250794..80e8aba4072 100644
--- a/spec/frontend/ide/components/ide_sidebar_nav_spec.js
+++ b/spec/frontend/ide/components/ide_sidebar_nav_spec.js
@@ -8,12 +8,12 @@ import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants';
const TEST_TABS = [
{
title: 'Lorem',
- icon: 'angle-up',
+ icon: 'chevron-lg-up',
views: [{ name: 'lorem-1' }, { name: 'lorem-2' }],
},
{
title: 'Ipsum',
- icon: 'angle-down',
+ icon: 'chevron-lg-down',
views: [{ name: 'ipsum-1' }, { name: 'ipsum-2' }],
},
];
@@ -55,7 +55,7 @@ describe('ide/components/ide_sidebar_nav', () => {
ariaLabel: button.attributes('aria-label'),
classes: button.classes(),
qaSelector: button.attributes('data-qa-selector'),
- icon: button.find(GlIcon).props('name'),
+ icon: button.findComponent(GlIcon).props('name'),
tooltip: getBinding(button.element, 'tooltip').value,
};
});
diff --git a/spec/frontend/ide/components/ide_spec.js b/spec/frontend/ide/components/ide_spec.js
index 9172c69b10e..48c670757a2 100644
--- a/spec/frontend/ide/components/ide_spec.js
+++ b/spec/frontend/ide/components/ide_spec.js
@@ -82,7 +82,7 @@ describe('WebIDE', () => {
await waitForPromises();
- expect(wrapper.find(ErrorMessage).exists()).toBe(exists);
+ expect(wrapper.findComponent(ErrorMessage).exists()).toBe(exists);
},
);
});
diff --git a/spec/frontend/ide/components/ide_status_bar_spec.js b/spec/frontend/ide/components/ide_status_bar_spec.js
index 17a5aa17b1f..e6e0ebaf1e8 100644
--- a/spec/frontend/ide/components/ide_status_bar_spec.js
+++ b/spec/frontend/ide/components/ide_status_bar_spec.js
@@ -1,8 +1,8 @@
+import { mount } from '@vue/test-utils';
import _ from 'lodash';
-import Vue, { nextTick } from 'vue';
import { TEST_HOST } from 'helpers/test_constants';
-import { createComponentWithStore } from 'helpers/vue_mount_component_helper';
import IdeStatusBar from '~/ide/components/ide_status_bar.vue';
+import IdeStatusMR from '~/ide/components/ide_status_mr.vue';
import { rightSidebarViews } from '~/ide/constants';
import { createStore } from '~/ide/stores';
import { projectData } from '../mock_data';
@@ -13,42 +13,48 @@ const TEST_MERGE_REQUEST_URL = `${TEST_HOST}merge-requests/${TEST_MERGE_REQUEST_
jest.mock('~/lib/utils/poll');
-describe('ideStatusBar', () => {
- let store;
- let vm;
+describe('IdeStatusBar component', () => {
+ let wrapper;
+
+ const findMRStatus = () => wrapper.findComponent(IdeStatusMR);
+
+ const mountComponent = (state = {}) => {
+ const store = createStore();
+ store.replaceState({
+ ...store.state,
+ currentBranchId: 'main',
+ currentProjectId: TEST_PROJECT_ID,
+ projects: {
+ ...store.state.projects,
+ [TEST_PROJECT_ID]: _.clone(projectData),
+ },
+ ...state,
+ });
- const createComponent = () => {
- vm = createComponentWithStore(Vue.extend(IdeStatusBar), store).$mount();
+ wrapper = mount(IdeStatusBar, { store });
};
- const findMRStatus = () => vm.$el.querySelector('.js-ide-status-mr');
-
- beforeEach(() => {
- store = createStore();
- store.state.currentProjectId = TEST_PROJECT_ID;
- store.state.projects[TEST_PROJECT_ID] = _.clone(projectData);
- store.state.currentBranchId = 'main';
- });
afterEach(() => {
- vm.$destroy();
+ wrapper.destroy();
});
describe('default', () => {
- beforeEach(() => {
- createComponent();
- });
-
it('triggers a setInterval', () => {
- expect(vm.intervalId).not.toBe(null);
+ mountComponent();
+
+ expect(wrapper.vm.intervalId).not.toBe(null);
});
it('renders the statusbar', () => {
- expect(vm.$el.className).toBe('ide-status-bar');
+ mountComponent();
+
+ expect(wrapper.classes()).toEqual(['ide-status-bar']);
});
describe('commitAgeUpdate', () => {
beforeEach(() => {
- jest.spyOn(vm, 'commitAgeUpdate').mockImplementation(() => {});
+ mountComponent();
+ jest.spyOn(wrapper.vm, 'commitAgeUpdate').mockImplementation(() => {});
});
afterEach(() => {
@@ -56,70 +62,82 @@ describe('ideStatusBar', () => {
});
it('gets called every second', () => {
- expect(vm.commitAgeUpdate).not.toHaveBeenCalled();
+ expect(wrapper.vm.commitAgeUpdate).not.toHaveBeenCalled();
jest.advanceTimersByTime(1000);
- expect(vm.commitAgeUpdate.mock.calls.length).toEqual(1);
+ expect(wrapper.vm.commitAgeUpdate.mock.calls).toHaveLength(1);
jest.advanceTimersByTime(1000);
- expect(vm.commitAgeUpdate.mock.calls.length).toEqual(2);
+ expect(wrapper.vm.commitAgeUpdate.mock.calls).toHaveLength(2);
});
});
describe('getCommitPath', () => {
it('returns the path to the commit details', () => {
- expect(vm.getCommitPath('abc123de')).toBe('/commit/abc123de');
+ mountComponent();
+
+ expect(wrapper.vm.getCommitPath('abc123de')).toBe('/commit/abc123de');
});
});
describe('pipeline status', () => {
- it('opens right sidebar on clicking icon', async () => {
- jest.spyOn(vm, 'openRightPane').mockImplementation(() => {});
- Vue.set(vm.$store.state.pipelines, 'latestPipeline', {
- details: {
- status: {
- text: 'success',
- details_path: 'test',
- icon: 'status_success',
+ it('opens right sidebar on clicking icon', () => {
+ const pipelines = {
+ latestPipeline: {
+ details: {
+ status: {
+ text: 'success',
+ details_path: 'test',
+ icon: 'status_success',
+ },
+ },
+ commit: {
+ author_gravatar_url: 'www',
},
},
- commit: {
- author_gravatar_url: 'www',
- },
- });
+ };
+ mountComponent({ pipelines });
+ jest.spyOn(wrapper.vm, 'openRightPane').mockImplementation(() => {});
- await nextTick();
- vm.$el.querySelector('.ide-status-pipeline button').click();
+ wrapper.find('button').trigger('click');
- expect(vm.openRightPane).toHaveBeenCalledWith(rightSidebarViews.pipelines);
+ expect(wrapper.vm.openRightPane).toHaveBeenCalledWith(rightSidebarViews.pipelines);
});
});
it('does not show merge request status', () => {
- expect(findMRStatus()).toBe(null);
+ mountComponent();
+
+ expect(findMRStatus().exists()).toBe(false);
});
});
describe('with merge request in store', () => {
beforeEach(() => {
- store.state.projects[TEST_PROJECT_ID].mergeRequests = {
- [TEST_MERGE_REQUEST_ID]: {
- web_url: TEST_MERGE_REQUEST_URL,
- references: {
- short: `!${TEST_MERGE_REQUEST_ID}`,
+ const state = {
+ currentMergeRequestId: TEST_MERGE_REQUEST_ID,
+ projects: {
+ [TEST_PROJECT_ID]: {
+ ..._.clone(projectData),
+ mergeRequests: {
+ [TEST_MERGE_REQUEST_ID]: {
+ web_url: TEST_MERGE_REQUEST_URL,
+ references: {
+ short: `!${TEST_MERGE_REQUEST_ID}`,
+ },
+ },
+ },
},
},
};
- store.state.currentMergeRequestId = TEST_MERGE_REQUEST_ID;
-
- createComponent();
+ mountComponent(state);
});
it('shows merge request status', () => {
- expect(findMRStatus().textContent.trim()).toEqual(`Merge request !${TEST_MERGE_REQUEST_ID}`);
- expect(findMRStatus().querySelector('a').href).toEqual(TEST_MERGE_REQUEST_URL);
+ expect(findMRStatus().text()).toBe(`Merge request !${TEST_MERGE_REQUEST_ID}`);
+ expect(findMRStatus().find('a').attributes('href')).toBe(TEST_MERGE_REQUEST_URL);
});
});
});
diff --git a/spec/frontend/ide/components/ide_status_list_spec.js b/spec/frontend/ide/components/ide_status_list_spec.js
index 371fbc6becd..0b54e8b6afb 100644
--- a/spec/frontend/ide/components/ide_status_list_spec.js
+++ b/spec/frontend/ide/components/ide_status_list_spec.js
@@ -25,7 +25,7 @@ describe('ide/components/ide_status_list', () => {
let store;
let wrapper;
- const findLink = () => wrapper.find(GlLink);
+ const findLink = () => wrapper.findComponent(GlLink);
const createComponent = (options = {}) => {
store = new Vuex.Store({
getters: {
@@ -98,6 +98,6 @@ describe('ide/components/ide_status_list', () => {
it('renders terminal sync status', () => {
createComponent();
- expect(wrapper.find(TerminalSyncStatusSafe).exists()).toBe(true);
+ expect(wrapper.findComponent(TerminalSyncStatusSafe).exists()).toBe(true);
});
});
diff --git a/spec/frontend/ide/components/ide_status_mr_spec.js b/spec/frontend/ide/components/ide_status_mr_spec.js
index 0526d4653f8..0b9111c0e2a 100644
--- a/spec/frontend/ide/components/ide_status_mr_spec.js
+++ b/spec/frontend/ide/components/ide_status_mr_spec.js
@@ -14,8 +14,8 @@ describe('ide/components/ide_status_mr', () => {
propsData: props,
});
};
- const findIcon = () => wrapper.find(GlIcon);
- const findLink = () => wrapper.find(GlLink);
+ const findIcon = () => wrapper.findComponent(GlIcon);
+ const findLink = () => wrapper.findComponent(GlLink);
afterEach(() => {
wrapper.destroy();
diff --git a/spec/frontend/ide/components/ide_tree_spec.js b/spec/frontend/ide/components/ide_tree_spec.js
index 8465ef9f5f3..f00017a2736 100644
--- a/spec/frontend/ide/components/ide_tree_spec.js
+++ b/spec/frontend/ide/components/ide_tree_spec.js
@@ -41,7 +41,7 @@ describe('IdeTree', () => {
let inititializeSpy;
beforeEach(async () => {
- inititializeSpy = jest.spyOn(wrapper.find(IdeTree).vm, 'initialize');
+ inititializeSpy = jest.spyOn(wrapper.findComponent(IdeTree).vm, 'initialize');
store.state.viewer = 'diff';
await wrapper.vm.reactivate();
diff --git a/spec/frontend/ide/components/jobs/detail/scroll_button_spec.js b/spec/frontend/ide/components/jobs/detail/scroll_button_spec.js
index d632a34266a..5eb66f75978 100644
--- a/spec/frontend/ide/components/jobs/detail/scroll_button_spec.js
+++ b/spec/frontend/ide/components/jobs/detail/scroll_button_spec.js
@@ -27,7 +27,7 @@ describe('IDE job log scroll button', () => {
beforeEach(() => createComponent({ direction }));
it('returns proper icon name', () => {
- expect(wrapper.find(GlIcon).props('name')).toBe(icon);
+ expect(wrapper.findComponent(GlIcon).props('name')).toBe(icon);
});
it('returns proper title', () => {
diff --git a/spec/frontend/ide/components/jobs/list_spec.js b/spec/frontend/ide/components/jobs/list_spec.js
index cb2c9f8f04f..b4c7eb51781 100644
--- a/spec/frontend/ide/components/jobs/list_spec.js
+++ b/spec/frontend/ide/components/jobs/list_spec.js
@@ -58,29 +58,29 @@ describe('IDE stages list', () => {
it('renders loading icon when no stages & loading', () => {
createComponent({ loading: true, stages: [] });
- expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
+ expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
});
it('renders stages components for each stage', () => {
createComponent({ stages });
- expect(wrapper.findAll(Stage).length).toBe(stages.length);
+ expect(wrapper.findAllComponents(Stage).length).toBe(stages.length);
});
it('triggers fetchJobs action when stage emits fetch event', () => {
createComponent({ stages });
- wrapper.find(Stage).vm.$emit('fetch');
+ wrapper.findComponent(Stage).vm.$emit('fetch');
expect(storeActions.fetchJobs).toHaveBeenCalled();
});
it('triggers toggleStageCollapsed action when stage emits toggleCollapsed event', () => {
createComponent({ stages });
- wrapper.find(Stage).vm.$emit('toggleCollapsed');
+ wrapper.findComponent(Stage).vm.$emit('toggleCollapsed');
expect(storeActions.toggleStageCollapsed).toHaveBeenCalled();
});
it('triggers setDetailJob action when stage emits clickViewLog event', () => {
createComponent({ stages });
- wrapper.find(Stage).vm.$emit('clickViewLog');
+ wrapper.findComponent(Stage).vm.$emit('clickViewLog');
expect(storeActions.setDetailJob).toHaveBeenCalled();
});
diff --git a/spec/frontend/ide/components/jobs/stage_spec.js b/spec/frontend/ide/components/jobs/stage_spec.js
index f158c59cd32..1d5e5743a4d 100644
--- a/spec/frontend/ide/components/jobs/stage_spec.js
+++ b/spec/frontend/ide/components/jobs/stage_spec.js
@@ -18,8 +18,8 @@ describe('IDE pipeline stage', () => {
},
};
- const findHeader = () => wrapper.find({ ref: 'cardHeader' });
- const findJobList = () => wrapper.find({ ref: 'jobList' });
+ const findHeader = () => wrapper.findComponent({ ref: 'cardHeader' });
+ const findJobList = () => wrapper.findComponent({ ref: 'jobList' });
const createComponent = (props) => {
wrapper = shallowMount(Stage, {
@@ -45,7 +45,7 @@ describe('IDE pipeline stage', () => {
stage: { ...defaultProps.stage, isLoading: true, jobs: [] },
});
- expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
+ expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
});
it('emits toggleCollaped event with stage id when clicking header', async () => {
@@ -60,7 +60,7 @@ describe('IDE pipeline stage', () => {
it('emits clickViewLog entity with job', async () => {
const [job] = defaultProps.stage.jobs;
createComponent();
- wrapper.findAll(Item).at(0).vm.$emit('clickViewLog', job);
+ wrapper.findAllComponents(Item).at(0).vm.$emit('clickViewLog', job);
await nextTick();
expect(wrapper.emitted().clickViewLog[0][0]).toBe(job);
});
diff --git a/spec/frontend/ide/components/merge_requests/list_spec.js b/spec/frontend/ide/components/merge_requests/list_spec.js
index 583671a0af6..ea6e2741a85 100644
--- a/spec/frontend/ide/components/merge_requests/list_spec.js
+++ b/spec/frontend/ide/components/merge_requests/list_spec.js
@@ -14,7 +14,7 @@ describe('IDE merge requests list', () => {
let fetchMergeRequestsMock;
const findSearchTypeButtons = () => wrapper.findAll('button');
- const findTokenedInput = () => wrapper.find(TokenedInput);
+ const findTokenedInput = () => wrapper.findComponent(TokenedInput);
const createComponent = (state = {}) => {
const { mergeRequests = {}, ...restOfState } = state;
@@ -63,7 +63,7 @@ describe('IDE merge requests list', () => {
it('renders loading icon when merge request is loading', () => {
createComponent({ mergeRequests: { isLoading: true } });
- expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
+ expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
});
it('renders no search results text when search is not empty', async () => {
@@ -107,8 +107,8 @@ describe('IDE merge requests list', () => {
it('renders list', () => {
createComponent(defaultStateWithMergeRequests);
- expect(wrapper.findAll(Item).length).toBe(1);
- expect(wrapper.find(Item).props('item')).toBe(
+ expect(wrapper.findAllComponents(Item).length).toBe(1);
+ expect(wrapper.findComponent(Item).props('item')).toBe(
defaultStateWithMergeRequests.mergeRequests.mergeRequests[0],
);
});
diff --git a/spec/frontend/ide/components/new_dropdown/index_spec.js b/spec/frontend/ide/components/new_dropdown/index_spec.js
index 19dcd9569b3..747c099db33 100644
--- a/spec/frontend/ide/components/new_dropdown/index_spec.js
+++ b/spec/frontend/ide/components/new_dropdown/index_spec.js
@@ -1,70 +1,66 @@
-import Vue, { nextTick } from 'vue';
-import { createComponentWithStore } from 'helpers/vue_mount_component_helper';
-import newDropdown from '~/ide/components/new_dropdown/index.vue';
+import { mount } from '@vue/test-utils';
+import NewDropdown from '~/ide/components/new_dropdown/index.vue';
+import Button from '~/ide/components/new_dropdown/button.vue';
import { createStore } from '~/ide/stores';
describe('new dropdown component', () => {
- let store;
- let vm;
-
- beforeEach(() => {
- store = createStore();
-
- const component = Vue.extend(newDropdown);
-
- vm = createComponentWithStore(component, store, {
- branch: 'main',
- path: '',
- mouseOver: false,
- type: 'tree',
+ let wrapper;
+
+ const findAllButtons = () => wrapper.findAllComponents(Button);
+
+ const mountComponent = () => {
+ const store = createStore();
+ store.state.currentProjectId = 'abcproject';
+ store.state.path = '';
+ store.state.trees['abcproject/mybranch'] = { tree: [] };
+
+ wrapper = mount(NewDropdown, {
+ store,
+ propsData: {
+ branch: 'main',
+ path: '',
+ mouseOver: false,
+ type: 'tree',
+ },
});
+ };
- vm.$store.state.currentProjectId = 'abcproject';
- vm.$store.state.path = '';
- vm.$store.state.trees['abcproject/mybranch'] = {
- tree: [],
- };
-
- vm.$mount();
-
- jest.spyOn(vm.$refs.newModal, 'open').mockImplementation(() => {});
+ beforeEach(() => {
+ mountComponent();
+ jest.spyOn(wrapper.vm.$refs.newModal, 'open').mockImplementation(() => {});
});
afterEach(() => {
- vm.$destroy();
+ wrapper.destroy();
});
it('renders new file, upload and new directory links', () => {
- const buttons = vm.$el.querySelectorAll('.dropdown-menu button');
-
- expect(buttons[0].textContent.trim()).toBe('New file');
- expect(buttons[1].textContent.trim()).toBe('Upload file');
- expect(buttons[2].textContent.trim()).toBe('New directory');
+ expect(findAllButtons().at(0).text()).toBe('New file');
+ expect(findAllButtons().at(1).text()).toBe('Upload file');
+ expect(findAllButtons().at(2).text()).toBe('New directory');
});
describe('createNewItem', () => {
it('opens modal for a blob when new file is clicked', () => {
- vm.$el.querySelectorAll('.dropdown-menu button')[0].click();
+ findAllButtons().at(0).trigger('click');
- expect(vm.$refs.newModal.open).toHaveBeenCalledWith('blob', '');
+ expect(wrapper.vm.$refs.newModal.open).toHaveBeenCalledWith('blob', '');
});
it('opens modal for a tree when new directory is clicked', () => {
- vm.$el.querySelectorAll('.dropdown-menu button')[2].click();
+ findAllButtons().at(2).trigger('click');
- expect(vm.$refs.newModal.open).toHaveBeenCalledWith('tree', '');
+ expect(wrapper.vm.$refs.newModal.open).toHaveBeenCalledWith('tree', '');
});
});
describe('isOpen', () => {
it('scrolls dropdown into view', async () => {
- jest.spyOn(vm.$refs.dropdownMenu, 'scrollIntoView').mockImplementation(() => {});
-
- vm.isOpen = true;
+ jest.spyOn(wrapper.vm.$refs.dropdownMenu, 'scrollIntoView').mockImplementation(() => {});
- await nextTick();
+ await wrapper.setProps({ isOpen: true });
- expect(vm.$refs.dropdownMenu.scrollIntoView).toHaveBeenCalledWith({
+ expect(wrapper.vm.$refs.dropdownMenu.scrollIntoView).toHaveBeenCalledWith({
block: 'nearest',
});
});
@@ -72,11 +68,11 @@ describe('new dropdown component', () => {
describe('delete entry', () => {
it('calls delete action', () => {
- jest.spyOn(vm, 'deleteEntry').mockImplementation(() => {});
+ jest.spyOn(wrapper.vm, 'deleteEntry').mockImplementation(() => {});
- vm.$el.querySelectorAll('.dropdown-menu button')[4].click();
+ findAllButtons().at(4).trigger('click');
- expect(vm.deleteEntry).toHaveBeenCalledWith('');
+ expect(wrapper.vm.deleteEntry).toHaveBeenCalledWith('');
});
});
});
diff --git a/spec/frontend/ide/components/panes/collapsible_sidebar_spec.js b/spec/frontend/ide/components/panes/collapsible_sidebar_spec.js
index 7f2ee0fe7d9..1d38231a767 100644
--- a/spec/frontend/ide/components/panes/collapsible_sidebar_spec.js
+++ b/spec/frontend/ide/components/panes/collapsible_sidebar_spec.js
@@ -27,7 +27,7 @@ describe('ide/components/panes/collapsible_sidebar.vue', () => {
});
};
- const findSidebarNav = () => wrapper.find(IdeSidebarNav);
+ const findSidebarNav = () => wrapper.findComponent(IdeSidebarNav);
beforeEach(() => {
store = createStore();
diff --git a/spec/frontend/ide/components/panes/right_spec.js b/spec/frontend/ide/components/panes/right_spec.js
index d12acd6dc4c..4555f519bc2 100644
--- a/spec/frontend/ide/components/panes/right_spec.js
+++ b/spec/frontend/ide/components/panes/right_spec.js
@@ -37,7 +37,7 @@ describe('ide/components/panes/right.vue', () => {
it('is always shown', () => {
createComponent();
- expect(wrapper.find(CollapsibleSidebar).props('extensionTabs')).toEqual(
+ expect(wrapper.findComponent(CollapsibleSidebar).props('extensionTabs')).toEqual(
expect.arrayContaining([
expect.objectContaining({
show: true,
@@ -65,7 +65,7 @@ describe('ide/components/panes/right.vue', () => {
createComponent();
- expect(wrapper.find(CollapsibleSidebar).props('extensionTabs')).toEqual(
+ expect(wrapper.findComponent(CollapsibleSidebar).props('extensionTabs')).toEqual(
expect.arrayContaining([
expect.objectContaining({
show: true,
@@ -90,7 +90,7 @@ describe('ide/components/panes/right.vue', () => {
store.state.terminal.isVisible = true;
await nextTick();
- expect(wrapper.find(CollapsibleSidebar).props('extensionTabs')).toEqual(
+ expect(wrapper.findComponent(CollapsibleSidebar).props('extensionTabs')).toEqual(
expect.arrayContaining([
expect.objectContaining({
show: true,
@@ -103,7 +103,7 @@ describe('ide/components/panes/right.vue', () => {
it('hides terminal tab when not visible', () => {
store.state.terminal.isVisible = false;
- expect(wrapper.find(CollapsibleSidebar).props('extensionTabs')).toEqual(
+ expect(wrapper.findComponent(CollapsibleSidebar).props('extensionTabs')).toEqual(
expect.arrayContaining([
expect.objectContaining({
show: false,
diff --git a/spec/frontend/ide/components/pipelines/empty_state_spec.js b/spec/frontend/ide/components/pipelines/empty_state_spec.js
index f7409fc36be..31081e8f9d5 100644
--- a/spec/frontend/ide/components/pipelines/empty_state_spec.js
+++ b/spec/frontend/ide/components/pipelines/empty_state_spec.js
@@ -32,7 +32,7 @@ describe('~/ide/components/pipelines/empty_state.vue', () => {
});
it('renders empty state', () => {
- expect(wrapper.find(GlEmptyState).props()).toMatchObject({
+ expect(wrapper.findComponent(GlEmptyState).props()).toMatchObject({
title: EmptyState.i18n.title,
description: EmptyState.i18n.description,
primaryButtonText: EmptyState.i18n.primaryButtonText,
diff --git a/spec/frontend/ide/components/pipelines/list_spec.js b/spec/frontend/ide/components/pipelines/list_spec.js
index 8a3606e27eb..545924c9c11 100644
--- a/spec/frontend/ide/components/pipelines/list_spec.js
+++ b/spec/frontend/ide/components/pipelines/list_spec.js
@@ -99,7 +99,7 @@ describe('IDE pipelines list', () => {
},
);
- expect(wrapper.find(GlLoadingIcon).exists()).toBe(false);
+ expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(false);
});
it('renders loading state', () => {
@@ -111,7 +111,7 @@ describe('IDE pipelines list', () => {
},
);
- expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
+ expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
});
});
@@ -128,7 +128,7 @@ describe('IDE pipelines list', () => {
it('renders empty state when no latestPipeline', () => {
createComponent({}, { ...defaultPipelinesLoadedState, latestPipeline: null });
- expect(wrapper.find(EmptyState).exists()).toBe(true);
+ expect(wrapper.findComponent(EmptyState).exists()).toBe(true);
expect(wrapper.element).toMatchSnapshot();
});
@@ -144,7 +144,7 @@ describe('IDE pipelines list', () => {
it('renders ci icon', () => {
createComponent({}, withLatestPipelineState);
- expect(wrapper.find(CiIcon).exists()).toBe(true);
+ expect(wrapper.findComponent(CiIcon).exists()).toBe(true);
});
it('renders pipeline data', () => {
@@ -158,7 +158,7 @@ describe('IDE pipelines list', () => {
const isLoadingJobs = true;
createComponent({}, { ...withLatestPipelineState, stages, isLoadingJobs });
- const jobProps = wrapper.findAll(GlTab).at(0).find(JobsList).props();
+ const jobProps = wrapper.findAllComponents(GlTab).at(0).findComponent(JobsList).props();
expect(jobProps.stages).toBe(stages);
expect(jobProps.loading).toBe(isLoadingJobs);
});
@@ -169,7 +169,7 @@ describe('IDE pipelines list', () => {
const isLoadingJobs = true;
createComponent({}, { ...withLatestPipelineState, isLoadingJobs });
- const jobProps = wrapper.findAll(GlTab).at(1).find(JobsList).props();
+ const jobProps = wrapper.findAllComponents(GlTab).at(1).findComponent(JobsList).props();
expect(jobProps.stages).toBe(failedStages);
expect(jobProps.loading).toBe(isLoadingJobs);
});
diff --git a/spec/frontend/ide/components/preview/clientside_spec.js b/spec/frontend/ide/components/preview/clientside_spec.js
index 426fbd5c04c..cf768114e70 100644
--- a/spec/frontend/ide/components/preview/clientside_spec.js
+++ b/spec/frontend/ide/components/preview/clientside_spec.js
@@ -396,7 +396,7 @@ describe('IDE clientside preview', () => {
wrapper.setData({ loading: true });
await nextTick();
- expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
+ expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
});
});
diff --git a/spec/frontend/ide/components/preview/navigator_spec.js b/spec/frontend/ide/components/preview/navigator_spec.js
index a199f4704f7..9c4f825ccf5 100644
--- a/spec/frontend/ide/components/preview/navigator_spec.js
+++ b/spec/frontend/ide/components/preview/navigator_spec.js
@@ -37,13 +37,13 @@ describe('IDE clientside preview navigator', () => {
});
it('renders loading icon by default', () => {
- expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
+ expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
});
it('removes loading icon when done event is fired', async () => {
listenHandler({ type: 'done' });
await nextTick();
- expect(wrapper.find(GlLoadingIcon).exists()).toBe(false);
+ expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(false);
});
it('does not count visiting same url multiple times', async () => {
diff --git a/spec/frontend/ide/components/repo_commit_section_spec.js b/spec/frontend/ide/components/repo_commit_section_spec.js
index db4181395d3..d3312358402 100644
--- a/spec/frontend/ide/components/repo_commit_section_spec.js
+++ b/spec/frontend/ide/components/repo_commit_section_spec.js
@@ -77,8 +77,10 @@ describe('RepoCommitSection', () => {
});
it('renders no changes text', () => {
- expect(wrapper.find(EmptyState).text().trim()).toContain('No changes');
- expect(wrapper.find(EmptyState).find('img').attributes('src')).toBe(TEST_NO_CHANGES_SVG);
+ expect(wrapper.findComponent(EmptyState).text().trim()).toContain('No changes');
+ expect(wrapper.findComponent(EmptyState).find('img').attributes('src')).toBe(
+ TEST_NO_CHANGES_SVG,
+ );
});
});
@@ -111,7 +113,7 @@ describe('RepoCommitSection', () => {
});
it('does not show empty state', () => {
- expect(wrapper.find(EmptyState).exists()).toBe(false);
+ expect(wrapper.findComponent(EmptyState).exists()).toBe(false);
});
});
@@ -157,7 +159,7 @@ describe('RepoCommitSection', () => {
});
it('does not show empty state', () => {
- expect(wrapper.find(EmptyState).exists()).toBe(false);
+ expect(wrapper.findComponent(EmptyState).exists()).toBe(false);
});
});
@@ -167,7 +169,7 @@ describe('RepoCommitSection', () => {
beforeEach(async () => {
createComponent();
- inititializeSpy = jest.spyOn(wrapper.find(RepoCommitSection).vm, 'initialize');
+ inititializeSpy = jest.spyOn(wrapper.findComponent(RepoCommitSection).vm, 'initialize');
store.state.viewer = 'diff';
await wrapper.vm.reactivate();
diff --git a/spec/frontend/ide/components/repo_editor_spec.js b/spec/frontend/ide/components/repo_editor_spec.js
index 7a0bcda1b7a..9921d8cba18 100644
--- a/spec/frontend/ide/components/repo_editor_spec.js
+++ b/spec/frontend/ide/components/repo_editor_spec.js
@@ -145,8 +145,7 @@ describe('RepoEditor', () => {
jest.clearAllMocks();
// create a new model each time, otherwise tests conflict with each other
// because of same model being used in multiple tests
- // eslint-disable-next-line no-undef
- monaco.editor.getModels().forEach((model) => model.dispose());
+ monacoEditor.getModels().forEach((model) => model.dispose());
wrapper.destroy();
wrapper = null;
});
@@ -212,7 +211,7 @@ describe('RepoEditor', () => {
it('renders markdown for tempFile', async () => {
findPreviewTab().vm.$emit('click');
await waitForPromises();
- expect(wrapper.find(ContentViewer).html()).toContain(dummyFile.text.content);
+ expect(wrapper.findComponent(ContentViewer).html()).toContain(dummyFile.text.content);
});
describe('when file changes to non-markdown file', () => {
diff --git a/spec/frontend/ide/components/repo_tab_spec.js b/spec/frontend/ide/components/repo_tab_spec.js
index b16fd8f80ba..b26edc5a85b 100644
--- a/spec/frontend/ide/components/repo_tab_spec.js
+++ b/spec/frontend/ide/components/repo_tab_spec.js
@@ -19,7 +19,7 @@ describe('RepoTab', () => {
let store;
let router;
- const findTab = () => wrapper.find(GlTabStub);
+ const findTab = () => wrapper.findComponent(GlTabStub);
function createComponent(propsData) {
wrapper = mount(RepoTab, {
@@ -164,7 +164,7 @@ describe('RepoTab', () => {
await wrapper.find('.multi-file-tab-close').trigger('click');
- expect(tab.opened).toBeFalsy();
+ expect(tab.opened).toBe(false);
expect(wrapper.vm.$store.state.changedFiles).toHaveLength(1);
});
@@ -180,7 +180,7 @@ describe('RepoTab', () => {
await wrapper.find('.multi-file-tab-close').trigger('click');
- expect(tab.opened).toBeFalsy();
+ expect(tab.opened).toBe(false);
});
});
});
diff --git a/spec/frontend/ide/components/resizable_panel_spec.js b/spec/frontend/ide/components/resizable_panel_spec.js
index 55b9423aba8..fe2a128c9c8 100644
--- a/spec/frontend/ide/components/resizable_panel_spec.js
+++ b/spec/frontend/ide/components/resizable_panel_spec.js
@@ -35,7 +35,7 @@ describe('~/ide/components/resizable_panel', () => {
store,
});
};
- const findResizer = () => wrapper.find(PanelResizer);
+ const findResizer = () => wrapper.findComponent(PanelResizer);
const findInlineStyle = () => wrapper.element.style.cssText;
const createInlineStyle = (width) => `width: ${width}px;`;
diff --git a/spec/frontend/ide/components/shared/commit_message_field_spec.js b/spec/frontend/ide/components/shared/commit_message_field_spec.js
index f4f9b95b233..94da06f4cb2 100644
--- a/spec/frontend/ide/components/shared/commit_message_field_spec.js
+++ b/spec/frontend/ide/components/shared/commit_message_field_spec.js
@@ -79,7 +79,7 @@ describe('CommitMessageField', () => {
await fillText(text);
expect(findHighlightsText().text()).toEqual(text);
- expect(findHighlightsMark().text()).toBeFalsy();
+ expect(findHighlightsMark().text()).toBe('');
});
it('highlights characters over 50 length', async () => {
diff --git a/spec/frontend/ide/components/terminal/empty_state_spec.js b/spec/frontend/ide/components/terminal/empty_state_spec.js
index 57c816747aa..15fb0fe9013 100644
--- a/spec/frontend/ide/components/terminal/empty_state_spec.js
+++ b/spec/frontend/ide/components/terminal/empty_state_spec.js
@@ -46,7 +46,7 @@ describe('IDE TerminalEmptyState', () => {
},
});
- expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
+ expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
});
it('when not loading, does not show loading icon', () => {
@@ -56,7 +56,7 @@ describe('IDE TerminalEmptyState', () => {
},
});
- expect(wrapper.find(GlLoadingIcon).exists()).toBe(false);
+ expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(false);
});
describe('when valid', () => {
@@ -71,7 +71,7 @@ describe('IDE TerminalEmptyState', () => {
},
});
- button = wrapper.find(GlButton);
+ button = wrapper.findComponent(GlButton);
});
it('shows button', () => {
@@ -100,7 +100,7 @@ describe('IDE TerminalEmptyState', () => {
},
});
- expect(wrapper.find(GlButton).props('disabled')).toBe(true);
- expect(wrapper.find(GlAlert).html()).toContain(TEST_HTML_MESSAGE);
+ expect(wrapper.findComponent(GlButton).props('disabled')).toBe(true);
+ expect(wrapper.findComponent(GlAlert).html()).toContain(TEST_HTML_MESSAGE);
});
});
diff --git a/spec/frontend/ide/components/terminal/session_spec.js b/spec/frontend/ide/components/terminal/session_spec.js
index 6a70ddb46a8..7e4a56b0610 100644
--- a/spec/frontend/ide/components/terminal/session_spec.js
+++ b/spec/frontend/ide/components/terminal/session_spec.js
@@ -38,7 +38,7 @@ describe('IDE TerminalSession', () => {
});
};
- const findButton = () => wrapper.find(GlButton);
+ const findButton = () => wrapper.findComponent(GlButton);
beforeEach(() => {
state = {
@@ -60,7 +60,7 @@ describe('IDE TerminalSession', () => {
it('shows terminal', () => {
factory();
- expect(wrapper.find(Terminal).props()).toEqual({
+ expect(wrapper.findComponent(Terminal).props()).toEqual({
terminalPath: TEST_TERMINAL_PATH,
status: RUNNING,
});
diff --git a/spec/frontend/ide/components/terminal/terminal_controls_spec.js b/spec/frontend/ide/components/terminal/terminal_controls_spec.js
index 71ec0dca89d..c18934f0f3b 100644
--- a/spec/frontend/ide/components/terminal/terminal_controls_spec.js
+++ b/spec/frontend/ide/components/terminal/terminal_controls_spec.js
@@ -12,7 +12,7 @@ describe('IDE TerminalControls', () => {
...options,
});
- buttons = wrapper.findAll(ScrollButton);
+ buttons = wrapper.findAllComponents(ScrollButton);
};
it('shows an up and down scroll button', () => {
diff --git a/spec/frontend/ide/components/terminal/terminal_spec.js b/spec/frontend/ide/components/terminal/terminal_spec.js
index afc49e22c83..4da3e1910e9 100644
--- a/spec/frontend/ide/components/terminal/terminal_spec.js
+++ b/spec/frontend/ide/components/terminal/terminal_spec.js
@@ -68,7 +68,7 @@ describe('IDE Terminal', () => {
it(`shows when starting (${status})`, () => {
factory({ status });
- expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
+ expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
expect(wrapper.find('.top-bar').text()).toBe('Starting...');
});
});
@@ -76,7 +76,7 @@ describe('IDE Terminal', () => {
it(`shows when stopping`, () => {
factory({ status: STOPPING });
- expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
+ expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
expect(wrapper.find('.top-bar').text()).toBe('Stopping...');
});
@@ -84,7 +84,7 @@ describe('IDE Terminal', () => {
it('hides when not loading', () => {
factory({ status });
- expect(wrapper.find(GlLoadingIcon).exists()).toBe(false);
+ expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(false);
expect(wrapper.find('.top-bar').text()).toBe('');
});
});
@@ -107,23 +107,23 @@ describe('IDE Terminal', () => {
});
it('is visible if terminal is created', () => {
- expect(wrapper.find(TerminalControls).exists()).toBe(true);
+ expect(wrapper.findComponent(TerminalControls).exists()).toBe(true);
});
it('scrolls glterminal on scroll-up', () => {
- wrapper.find(TerminalControls).vm.$emit('scroll-up');
+ wrapper.findComponent(TerminalControls).vm.$emit('scroll-up');
expect(wrapper.vm.glterminal.scrollToTop).toHaveBeenCalled();
});
it('scrolls glterminal on scroll-down', () => {
- wrapper.find(TerminalControls).vm.$emit('scroll-down');
+ wrapper.findComponent(TerminalControls).vm.$emit('scroll-down');
expect(wrapper.vm.glterminal.scrollToBottom).toHaveBeenCalled();
});
it('has props set', () => {
- expect(wrapper.find(TerminalControls).props()).toEqual({
+ expect(wrapper.findComponent(TerminalControls).props()).toEqual({
canScrollUp: false,
canScrollDown: false,
});
@@ -133,7 +133,7 @@ describe('IDE Terminal', () => {
wrapper.setData({ canScrollUp: true, canScrollDown: true });
return nextTick().then(() => {
- expect(wrapper.find(TerminalControls).props()).toEqual({
+ expect(wrapper.findComponent(TerminalControls).props()).toEqual({
canScrollUp: true,
canScrollDown: true,
});
diff --git a/spec/frontend/ide/components/terminal/view_spec.js b/spec/frontend/ide/components/terminal/view_spec.js
index 49f9513d2ac..57c8da9f5b7 100644
--- a/spec/frontend/ide/components/terminal/view_spec.js
+++ b/spec/frontend/ide/components/terminal/view_spec.js
@@ -66,7 +66,7 @@ describe('IDE TerminalView', () => {
it('renders empty state', async () => {
await factory();
- expect(wrapper.find(TerminalEmptyState).props()).toEqual({
+ expect(wrapper.findComponent(TerminalEmptyState).props()).toEqual({
helpPath: TEST_HELP_PATH,
illustrationPath: TEST_SVG_PATH,
...getters.allCheck(),
@@ -79,7 +79,7 @@ describe('IDE TerminalView', () => {
expect(actions.startSession).not.toHaveBeenCalled();
expect(actions.hideSplash).not.toHaveBeenCalled();
- wrapper.find(TerminalEmptyState).vm.$emit('start');
+ wrapper.findComponent(TerminalEmptyState).vm.$emit('start');
expect(actions.startSession).toHaveBeenCalled();
expect(actions.hideSplash).toHaveBeenCalled();
@@ -89,7 +89,7 @@ describe('IDE TerminalView', () => {
state.isShowSplash = false;
await factory();
- expect(wrapper.find(TerminalEmptyState).exists()).toBe(false);
- expect(wrapper.find(TerminalSession).exists()).toBe(true);
+ expect(wrapper.findComponent(TerminalEmptyState).exists()).toBe(false);
+ expect(wrapper.findComponent(TerminalSession).exists()).toBe(true);
});
});
diff --git a/spec/frontend/ide/components/terminal_sync/terminal_sync_status_safe_spec.js b/spec/frontend/ide/components/terminal_sync/terminal_sync_status_safe_spec.js
index f921037d744..5b1502cc190 100644
--- a/spec/frontend/ide/components/terminal_sync/terminal_sync_status_safe_spec.js
+++ b/spec/frontend/ide/components/terminal_sync/terminal_sync_status_safe_spec.js
@@ -34,13 +34,13 @@ describe('ide/components/terminal_sync/terminal_sync_status_safe', () => {
});
it('renders terminal sync status', () => {
- expect(wrapper.find(TerminalSyncStatus).exists()).toBe(true);
+ expect(wrapper.findComponent(TerminalSyncStatus).exists()).toBe(true);
});
});
describe('without terminal sync module', () => {
it('does not render terminal sync status', () => {
- expect(wrapper.find(TerminalSyncStatus).exists()).toBe(false);
+ expect(wrapper.findComponent(TerminalSyncStatus).exists()).toBe(false);
});
});
});
diff --git a/spec/frontend/ide/components/terminal_sync/terminal_sync_status_spec.js b/spec/frontend/ide/components/terminal_sync/terminal_sync_status_spec.js
index 3a326b08fff..147235abc8e 100644
--- a/spec/frontend/ide/components/terminal_sync/terminal_sync_status_spec.js
+++ b/spec/frontend/ide/components/terminal_sync/terminal_sync_status_spec.js
@@ -78,19 +78,19 @@ describe('ide/components/terminal_sync/terminal_sync_status', () => {
if (!icon) {
it('does not render icon', () => {
- expect(wrapper.find(GlIcon).exists()).toBe(false);
+ expect(wrapper.findComponent(GlIcon).exists()).toBe(false);
});
it('renders loading icon', () => {
- expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
+ expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
});
} else {
it('renders icon', () => {
- expect(wrapper.find(GlIcon).props('name')).toEqual(icon);
+ expect(wrapper.findComponent(GlIcon).props('name')).toEqual(icon);
});
it('does not render loading icon', () => {
- expect(wrapper.find(GlLoadingIcon).exists()).toBe(false);
+ expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(false);
});
}
});
diff --git a/spec/frontend/ide/lib/common/model_manager_spec.js b/spec/frontend/ide/lib/common/model_manager_spec.js
index 08e4ab0f113..e485873e8da 100644
--- a/spec/frontend/ide/lib/common/model_manager_spec.js
+++ b/spec/frontend/ide/lib/common/model_manager_spec.js
@@ -59,7 +59,7 @@ describe('Multi-file editor library model manager', () => {
describe('hasCachedModel', () => {
it('returns false when no models exist', () => {
- expect(instance.hasCachedModel('path')).toBeFalsy();
+ expect(instance.hasCachedModel('path')).toBe(false);
});
it('returns true when model exists', () => {
@@ -67,7 +67,7 @@ describe('Multi-file editor library model manager', () => {
instance.addModel(f);
- expect(instance.hasCachedModel(f.key)).toBeTruthy();
+ expect(instance.hasCachedModel(f.key)).toBe(true);
});
});
diff --git a/spec/frontend/ide/lib/diff/diff_spec.js b/spec/frontend/ide/lib/diff/diff_spec.js
index 901f9e7cfd1..208ed9bf759 100644
--- a/spec/frontend/ide/lib/diff/diff_spec.js
+++ b/spec/frontend/ide/lib/diff/diff_spec.js
@@ -18,8 +18,8 @@ describe('Multi-file editor library diff calculator', () => {
({ originalContent, newContent, lineNumber }) => {
const diff = computeDiff(originalContent, newContent)[0];
- expect(diff.added).toBeTruthy();
- expect(diff.modified).toBeTruthy();
+ expect(diff.added).toBe(true);
+ expect(diff.modified).toBe(true);
expect(diff.removed).toBeUndefined();
expect(diff.lineNumber).toBe(lineNumber);
},
@@ -36,7 +36,7 @@ describe('Multi-file editor library diff calculator', () => {
({ originalContent, newContent, lineNumber }) => {
const diff = computeDiff(originalContent, newContent)[0];
- expect(diff.added).toBeTruthy();
+ expect(diff.added).toBe(true);
expect(diff.modified).toBeUndefined();
expect(diff.removed).toBeUndefined();
expect(diff.lineNumber).toBe(lineNumber);
@@ -56,7 +56,7 @@ describe('Multi-file editor library diff calculator', () => {
expect(diff.added).toBeUndefined();
expect(diff.modified).toBe(modified);
- expect(diff.removed).toBeTruthy();
+ expect(diff.removed).toBe(true);
expect(diff.lineNumber).toBe(lineNumber);
},
);
diff --git a/spec/frontend/ide/stores/actions/file_spec.js b/spec/frontend/ide/stores/actions/file_spec.js
index 6c1dee1e5ca..d1c31cd412b 100644
--- a/spec/frontend/ide/stores/actions/file_spec.js
+++ b/spec/frontend/ide/stores/actions/file_spec.js
@@ -60,8 +60,8 @@ describe('IDE store file actions', () => {
it('closes open files', () => {
return store.dispatch('closeFile', localFile).then(() => {
- expect(localFile.opened).toBeFalsy();
- expect(localFile.active).toBeFalsy();
+ expect(localFile.opened).toBe(false);
+ expect(localFile.active).toBe(false);
expect(store.state.openFiles.length).toBe(0);
});
});
@@ -269,7 +269,7 @@ describe('IDE store file actions', () => {
it('sets the file as active', () => {
return store.dispatch('getFileData', { path: localFile.path }).then(() => {
- expect(localFile.active).toBeTruthy();
+ expect(localFile.active).toBe(true);
});
});
@@ -277,7 +277,7 @@ describe('IDE store file actions', () => {
return store
.dispatch('getFileData', { path: localFile.path, makeFileActive: false })
.then(() => {
- expect(localFile.active).toBeFalsy();
+ expect(localFile.active).toBe(false);
});
});
diff --git a/spec/frontend/ide/stores/actions/tree_spec.js b/spec/frontend/ide/stores/actions/tree_spec.js
index d43393875eb..6e8a03b47ad 100644
--- a/spec/frontend/ide/stores/actions/tree_spec.js
+++ b/spec/frontend/ide/stores/actions/tree_spec.js
@@ -134,7 +134,7 @@ describe('Multi-file store tree actions', () => {
it('toggles the tree open', async () => {
await store.dispatch('toggleTreeOpen', tree.path);
- expect(tree.opened).toBeTruthy();
+ expect(tree.opened).toBe(true);
});
});
diff --git a/spec/frontend/ide/stores/getters_spec.js b/spec/frontend/ide/stores/getters_spec.js
index 53d161ae5c9..24661e21cd0 100644
--- a/spec/frontend/ide/stores/getters_spec.js
+++ b/spec/frontend/ide/stores/getters_spec.js
@@ -268,7 +268,7 @@ describe('IDE store getters', () => {
currentProject: undefined,
};
- expect(getters.isOnDefaultBranch({}, localGetters)).toBeFalsy();
+ expect(getters.isOnDefaultBranch({}, localGetters)).toBe(undefined);
});
it("returns true when project's default branch matches current branch", () => {
@@ -279,7 +279,7 @@ describe('IDE store getters', () => {
branchName: 'main',
};
- expect(getters.isOnDefaultBranch({}, localGetters)).toBeTruthy();
+ expect(getters.isOnDefaultBranch({}, localGetters)).toBe(true);
});
it("returns false when project's default branch doesn't match current branch", () => {
@@ -290,7 +290,7 @@ describe('IDE store getters', () => {
branchName: 'feature',
};
- expect(getters.isOnDefaultBranch({}, localGetters)).toBeFalsy();
+ expect(getters.isOnDefaultBranch({}, localGetters)).toBe(false);
});
});
diff --git a/spec/frontend/ide/stores/modules/commit/getters_spec.js b/spec/frontend/ide/stores/modules/commit/getters_spec.js
index 1e34087b290..38ebe36c2c5 100644
--- a/spec/frontend/ide/stores/modules/commit/getters_spec.js
+++ b/spec/frontend/ide/stores/modules/commit/getters_spec.js
@@ -14,21 +14,21 @@ describe('IDE commit module getters', () => {
describe('discardDraftButtonDisabled', () => {
it('returns true when commitMessage is empty', () => {
- expect(getters.discardDraftButtonDisabled(state)).toBeTruthy();
+ expect(getters.discardDraftButtonDisabled(state)).toBe(true);
});
it('returns false when commitMessage is not empty & loading is false', () => {
state.commitMessage = 'test';
state.submitCommitLoading = false;
- expect(getters.discardDraftButtonDisabled(state)).toBeFalsy();
+ expect(getters.discardDraftButtonDisabled(state)).toBe(false);
});
it('returns true when commitMessage is not empty & loading is true', () => {
state.commitMessage = 'test';
state.submitCommitLoading = true;
- expect(getters.discardDraftButtonDisabled(state)).toBeTruthy();
+ expect(getters.discardDraftButtonDisabled(state)).toBe(true);
});
});
@@ -152,13 +152,13 @@ describe('IDE commit module getters', () => {
it('returns false if NOT creating a new branch', () => {
state.commitAction = COMMIT_TO_CURRENT_BRANCH;
- expect(getters.isCreatingNewBranch(state)).toBeFalsy();
+ expect(getters.isCreatingNewBranch(state)).toBe(false);
});
it('returns true if creating a new branch', () => {
state.commitAction = COMMIT_TO_NEW_BRANCH;
- expect(getters.isCreatingNewBranch(state)).toBeTruthy();
+ expect(getters.isCreatingNewBranch(state)).toBe(true);
});
});
@@ -183,7 +183,7 @@ describe('IDE commit module getters', () => {
});
it('should never hide "New MR" option', () => {
- expect(getters.shouldHideNewMrOption(state, localGetters, null, rootGetters)).toBeFalsy();
+ expect(getters.shouldHideNewMrOption(state, localGetters, null, rootGetters)).toBeNull();
});
});
@@ -195,13 +195,13 @@ describe('IDE commit module getters', () => {
it('should NOT hide "New MR" option if user can NOT push to the current branch', () => {
rootGetters.canPushToBranch = false;
- expect(getters.shouldHideNewMrOption(state, localGetters, null, rootGetters)).toBeFalsy();
+ expect(getters.shouldHideNewMrOption(state, localGetters, null, rootGetters)).toBe(false);
});
it('should hide "New MR" option if user can push to the current branch', () => {
rootGetters.canPushToBranch = true;
- expect(getters.shouldHideNewMrOption(state, localGetters, null, rootGetters)).toBeTruthy();
+ expect(getters.shouldHideNewMrOption(state, localGetters, null, rootGetters)).toBe(true);
});
});
@@ -211,7 +211,7 @@ describe('IDE commit module getters', () => {
});
it('should never hide "New MR" option', () => {
- expect(getters.shouldHideNewMrOption(state, localGetters, null, rootGetters)).toBeFalsy();
+ expect(getters.shouldHideNewMrOption(state, localGetters, null, rootGetters)).toBeNull();
});
});
@@ -223,13 +223,13 @@ describe('IDE commit module getters', () => {
it('should NOT hide "New MR" option if there is NO existing MR for the current branch', () => {
rootGetters.hasMergeRequest = false;
- expect(getters.shouldHideNewMrOption(state, localGetters, null, rootGetters)).toBeFalsy();
+ expect(getters.shouldHideNewMrOption(state, localGetters, null, rootGetters)).toBeNull();
});
it('should hide "New MR" option if there is existing MR for the current branch', () => {
rootGetters.hasMergeRequest = true;
- expect(getters.shouldHideNewMrOption(state, localGetters, null, rootGetters)).toBeTruthy();
+ expect(getters.shouldHideNewMrOption(state, localGetters, null, rootGetters)).toBe(true);
});
});
@@ -247,17 +247,13 @@ describe('IDE commit module getters', () => {
it('should hide "New MR" when there is an existing MR', () => {
rootGetters.hasMergeRequest = true;
- expect(
- getters.shouldHideNewMrOption(state, localGetters, null, rootGetters),
- ).toBeTruthy();
+ expect(getters.shouldHideNewMrOption(state, localGetters, null, rootGetters)).toBe(true);
});
it('should hide "New MR" when there is no existing MR', () => {
rootGetters.hasMergeRequest = false;
- expect(
- getters.shouldHideNewMrOption(state, localGetters, null, rootGetters),
- ).toBeTruthy();
+ expect(getters.shouldHideNewMrOption(state, localGetters, null, rootGetters)).toBe(true);
});
});
@@ -270,17 +266,17 @@ describe('IDE commit module getters', () => {
rootGetters.hasMergeRequest = false;
rootGetters.canPushToBranch = true;
- expect(getters.shouldHideNewMrOption(state, localGetters, null, rootGetters)).toBeFalsy();
+ expect(getters.shouldHideNewMrOption(state, localGetters, null, rootGetters)).toBe(false);
rootGetters.hasMergeRequest = true;
rootGetters.canPushToBranch = true;
- expect(getters.shouldHideNewMrOption(state, localGetters, null, rootGetters)).toBeFalsy();
+ expect(getters.shouldHideNewMrOption(state, localGetters, null, rootGetters)).toBe(false);
rootGetters.hasMergeRequest = false;
rootGetters.canPushToBranch = false;
- expect(getters.shouldHideNewMrOption(state, localGetters, null, rootGetters)).toBeFalsy();
+ expect(getters.shouldHideNewMrOption(state, localGetters, null, rootGetters)).toBe(false);
});
});
});
@@ -292,7 +288,7 @@ describe('IDE commit module getters', () => {
rootGetters.hasMergeRequest = true;
rootGetters.canPushToBranch = true;
- expect(getters.shouldHideNewMrOption(state, localGetters, null, rootGetters)).toBeFalsy();
+ expect(getters.shouldHideNewMrOption(state, localGetters, null, rootGetters)).toBe(false);
});
});
diff --git a/spec/frontend/ide/stores/mutations/file_spec.js b/spec/frontend/ide/stores/mutations/file_spec.js
index 1453f26c1d9..69ec2e7a6f5 100644
--- a/spec/frontend/ide/stores/mutations/file_spec.js
+++ b/spec/frontend/ide/stores/mutations/file_spec.js
@@ -22,7 +22,7 @@ describe('IDE store file mutations', () => {
active: true,
});
- expect(localFile.active).toBeTruthy();
+ expect(localFile.active).toBe(true);
});
it('sets pending tab as not active', () => {
@@ -41,7 +41,7 @@ describe('IDE store file mutations', () => {
it('adds into opened files', () => {
mutations.TOGGLE_FILE_OPEN(localState, localFile.path);
- expect(localFile.opened).toBeTruthy();
+ expect(localFile.opened).toBe(true);
expect(localState.openFiles.length).toBe(1);
});
@@ -50,7 +50,7 @@ describe('IDE store file mutations', () => {
mutations.TOGGLE_FILE_OPEN(localState, localFile.path);
mutations.TOGGLE_FILE_OPEN(localState, localFile.path);
- expect(localFile.opened).toBeFalsy();
+ expect(localFile.opened).toBe(false);
expect(localState.openFiles.length).toBe(0);
});
});
@@ -162,7 +162,7 @@ describe('IDE store file mutations', () => {
callMutationForFile(localFile);
- expect(localFile.raw).toBeFalsy();
+ expect(localFile.raw).toEqual('');
expect(localState.stagedFiles[0].raw).toBe('testing');
});
@@ -172,7 +172,7 @@ describe('IDE store file mutations', () => {
callMutationForFile(localFile);
- expect(localFile.raw).toBeFalsy();
+ expect(localFile.raw).toEqual('');
expect(localFile.content).toBe('testing');
});
@@ -202,7 +202,7 @@ describe('IDE store file mutations', () => {
callMutationForFile(localFile);
- expect(localFile.raw).toBeFalsy();
+ expect(localFile.raw).toEqual('');
expect(localState.stagedFiles[0].raw).toBe('testing');
});
});
@@ -239,7 +239,7 @@ describe('IDE store file mutations', () => {
});
expect(localFile.content).toBe('testing');
- expect(localFile.changed).toBeTruthy();
+ expect(localFile.changed).toBe(true);
});
it('sets changed if file is a temp file', () => {
@@ -250,7 +250,7 @@ describe('IDE store file mutations', () => {
content: '',
});
- expect(localFile.changed).toBeTruthy();
+ expect(localFile.changed).toBe(true);
});
});
@@ -329,7 +329,7 @@ describe('IDE store file mutations', () => {
mutations.DISCARD_FILE_CHANGES(localState, localFile.path);
expect(localFile.content).toBe('');
- expect(localFile.changed).toBeFalsy();
+ expect(localFile.changed).toBe(false);
});
it('adds to root tree if deleted', () => {
@@ -527,7 +527,7 @@ describe('IDE store file mutations', () => {
changed: true,
});
- expect(localFile.changed).toBeTruthy();
+ expect(localFile.changed).toBe(true);
});
});
diff --git a/spec/frontend/ide/stores/mutations/merge_request_spec.js b/spec/frontend/ide/stores/mutations/merge_request_spec.js
index afbe6770c0d..2af06835181 100644
--- a/spec/frontend/ide/stores/mutations/merge_request_spec.js
+++ b/spec/frontend/ide/stores/mutations/merge_request_spec.js
@@ -30,7 +30,7 @@ describe('IDE store merge request mutations', () => {
const newMr = localState.projects.abcproject.mergeRequests[1];
expect(newMr.title).toBe('mr');
- expect(newMr.active).toBeTruthy();
+ expect(newMr.active).toBe(true);
});
it('keeps original data', () => {
diff --git a/spec/frontend/ide/utils_spec.js b/spec/frontend/ide/utils_spec.js
index 2f8447af518..fd9d481251d 100644
--- a/spec/frontend/ide/utils_spec.js
+++ b/spec/frontend/ide/utils_spec.js
@@ -46,7 +46,7 @@ describe('WebIDE utils', () => {
content: 'SELECT "éêė" from tablename',
mimeType: 'application/sql',
}),
- ).toBeFalsy();
+ ).toBe(false);
});
it('returns true for ASCII only content for unknown types', () => {
@@ -56,7 +56,7 @@ describe('WebIDE utils', () => {
content: 'plain text',
mimeType: 'application/x-new-type',
}),
- ).toBeTruthy();
+ ).toBe(true);
});
it('returns false for non-ASCII content for unknown types', () => {
@@ -66,7 +66,7 @@ describe('WebIDE utils', () => {
content: '{"éêė":"value"}',
mimeType: 'application/octet-stream',
}),
- ).toBeFalsy();
+ ).toBe(false);
});
it.each`
diff --git a/spec/frontend/integrations/edit/components/dynamic_field_spec.js b/spec/frontend/integrations/edit/components/dynamic_field_spec.js
index ee2f6541b03..5af0e272285 100644
--- a/spec/frontend/integrations/edit/components/dynamic_field_spec.js
+++ b/spec/frontend/integrations/edit/components/dynamic_field_spec.js
@@ -204,7 +204,7 @@ describe('DynamicField', () => {
});
expect(findGlFormGroup().find('small').html()).toContain(
- '[<code>1</code> <a>3</a> <a target="_blank" href="foo">4</a>]',
+ '[<code>1</code> <a>3</a> <a href="foo">4</a>]',
);
});
});
diff --git a/spec/frontend/integrations/overrides/components/integration_overrides_spec.js b/spec/frontend/integrations/overrides/components/integration_overrides_spec.js
index 6aa3e661677..fd60d7f817f 100644
--- a/spec/frontend/integrations/overrides/components/integration_overrides_spec.js
+++ b/spec/frontend/integrations/overrides/components/integration_overrides_spec.js
@@ -15,6 +15,7 @@ import UrlSync from '~/vue_shared/components/url_sync.vue';
const mockOverrides = Array(DEFAULT_PER_PAGE * 3)
.fill(1)
.map((_, index) => ({
+ id: index,
name: `test-proj-${index}`,
avatar_url: `avatar-${index}`,
full_path: `test-proj-${index}`,
@@ -59,6 +60,7 @@ describe('IntegrationOverrides', () => {
const avatar = link.findComponent(ProjectAvatar);
return {
+ id: avatar.props('projectId'),
href: link.attributes('href'),
avatarUrl: avatar.props('projectAvatarUrl'),
avatarName: avatar.props('projectName'),
@@ -90,7 +92,7 @@ describe('IntegrationOverrides', () => {
const table = findGlTable();
expect(table.exists()).toBe(true);
- expect(table.attributes('busy')).toBeFalsy();
+ expect(table.attributes('busy')).toBeUndefined();
});
it('renders IntegrationTabs with count', async () => {
@@ -109,6 +111,7 @@ describe('IntegrationOverrides', () => {
it('renders overrides as rows in table', () => {
expect(findRowsAsModel()).toEqual(
mockOverrides.map((x) => ({
+ id: x.id,
href: x.full_path,
avatarUrl: x.avatar_url,
avatarName: x.name,
diff --git a/spec/frontend/invite_members/components/invite_members_modal_spec.js b/spec/frontend/invite_members/components/invite_members_modal_spec.js
index 045a454e63a..2058784b033 100644
--- a/spec/frontend/invite_members/components/invite_members_modal_spec.js
+++ b/spec/frontend/invite_members/components/invite_members_modal_spec.js
@@ -1,4 +1,4 @@
-import { GlLink, GlModal, GlSprintf, GlFormGroup } from '@gitlab/ui';
+import { GlLink, GlModal, GlSprintf, GlFormGroup, GlCollapse, GlIcon } from '@gitlab/ui';
import MockAdapter from 'axios-mock-adapter';
import { nextTick } from 'vue';
import { stubComponent } from 'helpers/stub_component';
@@ -18,6 +18,7 @@ import {
MEMBERS_PLACEHOLDER_DISABLED,
MEMBERS_TO_PROJECT_CELEBRATE_INTRO_TEXT,
LEARN_GITLAB,
+ EXPANDED_ERRORS,
} from '~/invite_members/constants';
import eventHub from '~/invite_members/event_hub';
import ContentTransition from '~/vue_shared/components/content_transition.vue';
@@ -36,6 +37,7 @@ import {
user3,
user4,
user5,
+ user6,
GlEmoji,
} from '../mock_data/member_modal';
@@ -95,9 +97,12 @@ describe('InviteMembersModal', () => {
const findBase = () => wrapper.findComponent(InviteModalBase);
const findIntroText = () => wrapper.findByTestId('modal-base-intro-text').text();
const findMemberErrorAlert = () => wrapper.findByTestId('alert-member-error');
+ const findMoreInviteErrorsButton = () => wrapper.findByTestId('accordion-button');
+ const findAccordion = () => wrapper.findComponent(GlCollapse);
+ const findErrorsIcon = () => wrapper.findComponent(GlIcon);
const findMemberErrorMessage = (element) =>
- `${Object.keys(invitationsApiResponse.MULTIPLE_RESTRICTED.message)[element]}: ${
- Object.values(invitationsApiResponse.MULTIPLE_RESTRICTED.message)[element]
+ `${Object.keys(invitationsApiResponse.EXPANDED_RESTRICTED.message)[element]}: ${
+ Object.values(invitationsApiResponse.EXPANDED_RESTRICTED.message)[element]
}`;
const emitEventFromModal = (eventName) => () =>
findModal().vm.$emit(eventName, { preventDefault: jest.fn() });
@@ -666,8 +671,8 @@ describe('InviteMembersModal', () => {
it('displays errors for multiple and allows clearing', async () => {
createInviteMembersToGroupWrapper();
- await triggerMembersTokenSelect([user3, user4, user5]);
- mockInvitationsApi(httpStatus.CREATED, invitationsApiResponse.MULTIPLE_RESTRICTED);
+ await triggerMembersTokenSelect([user3, user4, user5, user6]);
+ mockInvitationsApi(httpStatus.CREATED, invitationsApiResponse.EXPANDED_RESTRICTED);
clickInviteButton();
@@ -675,19 +680,44 @@ describe('InviteMembersModal', () => {
expect(findMemberErrorAlert().exists()).toBe(true);
expect(findMemberErrorAlert().props('title')).toContain(
- "The following 3 members couldn't be invited",
+ "The following 4 members couldn't be invited",
);
expect(findMemberErrorAlert().text()).toContain(findMemberErrorMessage(0));
expect(findMemberErrorAlert().text()).toContain(findMemberErrorMessage(1));
expect(findMemberErrorAlert().text()).toContain(findMemberErrorMessage(2));
+ expect(findMemberErrorAlert().text()).toContain(findMemberErrorMessage(3));
+ expect(findAccordion().exists()).toBe(true);
+ expect(findMoreInviteErrorsButton().text()).toContain('Show more (2)');
+ expect(findErrorsIcon().attributes('class')).not.toContain('gl-rotate-180');
+ expect(findAccordion().attributes('visible')).toBeUndefined();
+
+ await findMoreInviteErrorsButton().vm.$emit('click');
+
+ expect(findMoreInviteErrorsButton().text()).toContain(EXPANDED_ERRORS);
+ expect(findErrorsIcon().attributes('class')).toContain('gl-rotate-180');
+ expect(findAccordion().attributes('visible')).toBeDefined();
+
+ await findMoreInviteErrorsButton().vm.$emit('click');
+
+ expect(findMoreInviteErrorsButton().text()).toContain('Show more (2)');
+ expect(findAccordion().attributes('visible')).toBeUndefined();
await removeMembersToken(user3);
+ expect(findMoreInviteErrorsButton().text()).toContain('Show more (1)');
expect(findMemberErrorAlert().props('title')).toContain(
- "The following 2 members couldn't be invited",
+ "The following 3 members couldn't be invited",
);
expect(findMemberErrorAlert().text()).not.toContain(findMemberErrorMessage(0));
+ await removeMembersToken(user6);
+
+ expect(findMoreInviteErrorsButton().exists()).toBe(false);
+ expect(findMemberErrorAlert().props('title')).toContain(
+ "The following 2 members couldn't be invited",
+ );
+ expect(findMemberErrorAlert().text()).not.toContain(findMemberErrorMessage(2));
+
await removeMembersToken(user4);
expect(findMemberErrorAlert().props('title')).toContain(
diff --git a/spec/frontend/invite_members/components/members_token_select_spec.js b/spec/frontend/invite_members/components/members_token_select_spec.js
index 6375d0f7e2e..0455460918c 100644
--- a/spec/frontend/invite_members/components/members_token_select_spec.js
+++ b/spec/frontend/invite_members/components/members_token_select_spec.js
@@ -5,6 +5,7 @@ import { stubComponent } from 'helpers/stub_component';
import waitForPromises from 'helpers/wait_for_promises';
import * as UserApi from '~/api/user_api';
import MembersTokenSelect from '~/invite_members/components/members_token_select.vue';
+import { VALID_TOKEN_BACKGROUND, INVALID_TOKEN_BACKGROUND } from '~/invite_members/constants';
const label = 'testgroup';
const placeholder = 'Search for a member';
@@ -49,6 +50,39 @@ describe('MembersTokenSelect', () => {
});
});
+ describe('when there are invalidMembers', () => {
+ it('adds in the correct class values for the tokens', async () => {
+ const badToken = { ...user1, class: INVALID_TOKEN_BACKGROUND };
+ const goodToken = { ...user2, class: VALID_TOKEN_BACKGROUND };
+
+ wrapper = createComponent();
+
+ findTokenSelector().vm.$emit('input', [user1, user2]);
+
+ await waitForPromises();
+
+ expect(findTokenSelector().props('selectedTokens')).toEqual([user1, user2]);
+
+ await wrapper.setProps({ invalidMembers: { one_1: 'bad stuff' } });
+
+ expect(findTokenSelector().props('selectedTokens')).toEqual([badToken, goodToken]);
+ });
+
+ it('does not change class when invalid members are cleared', async () => {
+ // arrange - invalidMembers is non-empty and then tokens are added
+ wrapper = createComponent();
+ await wrapper.setProps({ invalidMembers: { one_1: 'bad stuff' } });
+ findTokenSelector().vm.$emit('input', [user1, user2]);
+ await waitForPromises();
+
+ // act - invalidMembers clears out
+ await wrapper.setProps({ invalidMembers: {} });
+
+ // assert - we didn't try to update the tokens
+ expect(findTokenSelector().props('selectedTokens')).toEqual([user1, user2]);
+ });
+ });
+
describe('users', () => {
beforeEach(() => {
jest.spyOn(UserApi, 'getUsers').mockResolvedValue({ data: allUsers });
diff --git a/spec/frontend/invite_members/components/user_limit_notification_spec.js b/spec/frontend/invite_members/components/user_limit_notification_spec.js
index bbc17932a49..543fc28a342 100644
--- a/spec/frontend/invite_members/components/user_limit_notification_spec.js
+++ b/spec/frontend/invite_members/components/user_limit_notification_spec.js
@@ -9,6 +9,8 @@ import {
import { freeUsersLimit, membersCount } from '../mock_data/member_modal';
+const WARNING_ALERT_TITLE = 'You only have space for 2 more members in name';
+
describe('UserLimitNotification', () => {
let wrapper;
@@ -33,7 +35,7 @@ describe('UserLimitNotification', () => {
},
...props,
},
- provide: { name: 'my group' },
+ provide: { name: 'name' },
stubs: { GlSprintf },
});
};
@@ -50,7 +52,7 @@ describe('UserLimitNotification', () => {
});
});
- describe('when close to limit with a personal namepace', () => {
+ describe('when close to limit within a personal namepace', () => {
beforeEach(() => {
createComponent(true, false, { membersCount: 3, userNamespace: true });
});
@@ -58,27 +60,24 @@ describe('UserLimitNotification', () => {
it('renders the limit for a personal namespace', () => {
const alert = findAlert();
- expect(alert.attributes('title')).toEqual(
- 'You only have space for 2 more members in your personal projects',
- );
+ expect(alert.attributes('title')).toEqual(WARNING_ALERT_TITLE);
+
expect(alert.text()).toEqual(
'To make more space, you can remove members who no longer need access.',
);
});
});
- describe('when close to limit', () => {
+ describe('when close to limit within a group', () => {
it("renders user's limit notification", () => {
createComponent(true, false, { membersCount: 3 });
const alert = findAlert();
- expect(alert.attributes('title')).toEqual(
- 'You only have space for 2 more members in my group',
- );
+ expect(alert.attributes('title')).toEqual(WARNING_ALERT_TITLE);
expect(alert.text()).toEqual(
- 'To get more members an owner of this namespace can start a trial or upgrade to a paid tier.',
+ 'To get more members an owner of the group can start a trial or upgrade to a paid tier.',
);
});
});
@@ -89,7 +88,7 @@ describe('UserLimitNotification', () => {
const alert = findAlert();
- expect(alert.attributes('title')).toEqual("You've reached your 5 members limit for my group");
+ expect(alert.attributes('title')).toEqual("You've reached your 5 members limit for name");
expect(alert.text()).toEqual(REACHED_LIMIT_UPGRADE_SUGGESTION_MESSAGE);
});
diff --git a/spec/frontend/invite_members/mock_data/api_responses.js b/spec/frontend/invite_members/mock_data/api_responses.js
index 4ad3b6aeb66..6fe06decb6b 100644
--- a/spec/frontend/invite_members/mock_data/api_responses.js
+++ b/spec/frontend/invite_members/mock_data/api_responses.js
@@ -26,6 +26,20 @@ const MULTIPLE_RESTRICTED = {
status: 'error',
};
+const EXPANDED_RESTRICTED = {
+ message: {
+ 'email@example.com':
+ "The member's email address is not allowed for this project. Go to the Admin area > Sign-up restrictions, and check Allowed domains for sign-ups.",
+ 'email4@example.com':
+ "The member's email address is not allowed for this project. Go to the Admin area > Sign-up restrictions, and check the Domain denylist.",
+ 'email5@example.com':
+ "The member's email address is not allowed for this project. Go to the Admin area > Sign-up restrictions, and check the Domain denylist.",
+ root:
+ "The member's email address is not allowed for this project. Go to the Admin area > Sign-up restrictions, and check Allowed domains for sign-ups.",
+ },
+ status: 'error',
+};
+
const EMAIL_TAKEN = {
message: {
'email@example.org': "The member's email address has already been taken",
@@ -41,4 +55,5 @@ export const invitationsApiResponse = {
EMAIL_RESTRICTED,
MULTIPLE_RESTRICTED,
EMAIL_TAKEN,
+ EXPANDED_RESTRICTED,
};
diff --git a/spec/frontend/invite_members/mock_data/member_modal.js b/spec/frontend/invite_members/mock_data/member_modal.js
index 7d675b6206c..4f4e9345e46 100644
--- a/spec/frontend/invite_members/mock_data/member_modal.js
+++ b/spec/frontend/invite_members/mock_data/member_modal.js
@@ -39,5 +39,10 @@ export const user5 = {
name: 'root',
avatar_url: '',
};
+export const user6 = {
+ id: 'user-defined-token3',
+ name: 'email5@example.com',
+ avatar_url: '',
+};
export const GlEmoji = { template: '<img/>' };
diff --git a/spec/frontend/issuable/components/related_issuable_item_spec.js b/spec/frontend/issuable/components/related_issuable_item_spec.js
index 6b48f83041a..3f9f048605a 100644
--- a/spec/frontend/issuable/components/related_issuable_item_spec.js
+++ b/spec/frontend/issuable/components/related_issuable_item_spec.js
@@ -1,23 +1,25 @@
-import { mount } from '@vue/test-utils';
-import { nextTick } from 'vue';
+import { GlIcon, GlLink, GlButton } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
import { TEST_HOST } from 'helpers/test_constants';
import IssueDueDate from '~/boards/components/issue_due_date.vue';
import { formatDate } from '~/lib/utils/datetime_utility';
+import { updateHistory } from '~/lib/utils/url_utility';
+import { __ } from '~/locale';
import RelatedIssuableItem from '~/issuable/components/related_issuable_item.vue';
+import IssueMilestone from '~/issuable/components/issue_milestone.vue';
+import IssueAssignees from '~/issuable/components/issue_assignees.vue';
+import WorkItemDetailModal from '~/work_items/components/work_item_detail_modal.vue';
import { defaultAssignees, defaultMilestone } from './related_issuable_mock_data';
+jest.mock('~/lib/utils/url_utility', () => ({
+ ...jest.requireActual('~/lib/utils/url_utility'),
+ updateHistory: jest.fn(),
+}));
+
describe('RelatedIssuableItem', () => {
let wrapper;
- function mountComponent({ mountMethod = mount, stubs = {}, props = {}, slots = {} } = {}) {
- wrapper = mountMethod(RelatedIssuableItem, {
- propsData: props,
- slots,
- stubs,
- });
- }
-
- const props = {
+ const defaultProps = {
idKey: 1,
displayReference: 'gitlab-org/gitlab-test#1',
pathIdSeparator: '#',
@@ -31,84 +33,94 @@ describe('RelatedIssuableItem', () => {
assignees: defaultAssignees,
eventNamespace: 'relatedIssue',
};
- const slots = {
- dueDate: '<div class="js-due-date-slot"></div>',
- weight: '<div class="js-weight-slot"></div>',
- };
-
- const findRemoveButton = () => wrapper.find({ ref: 'removeButton' });
- const findLockIcon = () => wrapper.find({ ref: 'lockIcon' });
- beforeEach(() => {
- mountComponent({ props, slots });
- });
+ const findIcon = () => wrapper.findComponent(GlIcon);
+ const findIssueDueDate = () => wrapper.findComponent(IssueDueDate);
+ const findLockIcon = () => wrapper.find('[data-testid="lockIcon"]');
+ const findRemoveButton = () => wrapper.findComponent(GlButton);
+ const findTitleLink = () => wrapper.findComponent(GlLink);
+ const findWorkItemDetailModal = () => wrapper.findComponent(WorkItemDetailModal);
+
+ function mountComponent({ data = {}, props = {} } = {}) {
+ wrapper = shallowMount(RelatedIssuableItem, {
+ propsData: {
+ ...defaultProps,
+ ...props,
+ },
+ data() {
+ return data;
+ },
+ });
+ }
afterEach(() => {
wrapper.destroy();
});
it('contains issuable-info-container class when canReorder is false', () => {
- expect(wrapper.props('canReorder')).toBe(false);
- expect(wrapper.find('.issuable-info-container').exists()).toBe(true);
+ mountComponent({ props: { canReorder: false } });
+
+ expect(wrapper.classes('issuable-info-container')).toBe(true);
});
it('does not render token state', () => {
+ mountComponent();
+
expect(wrapper.find('.text-secondary svg').exists()).toBe(false);
});
it('does not render remove button', () => {
- expect(wrapper.find({ ref: 'removeButton' }).exists()).toBe(false);
+ mountComponent();
+
+ expect(findRemoveButton().exists()).toBe(false);
});
describe('token title', () => {
+ beforeEach(() => {
+ mountComponent();
+ });
+
it('links to computedPath', () => {
- expect(wrapper.find('.item-title a').attributes('href')).toEqual(wrapper.props('path'));
+ expect(findTitleLink().attributes('href')).toBe(defaultProps.path);
});
it('renders confidential icon', () => {
- expect(wrapper.find('.confidential-icon').exists()).toBe(true);
+ expect(findIcon().attributes('title')).toBe(__('Confidential'));
});
it('renders title', () => {
- expect(wrapper.find('.item-title a').text()).toEqual(props.title);
+ expect(findTitleLink().text()).toBe(defaultProps.title);
});
});
describe('token state', () => {
- const tokenState = () => wrapper.find({ ref: 'iconElementXL' });
-
- beforeEach(() => {
- wrapper.setProps({ state: 'opened' });
- });
-
- it('renders if hasState', () => {
- expect(tokenState().exists()).toBe(true);
- });
-
it('renders state title', () => {
- const stateTitle = tokenState().attributes('title');
- const formattedCreateDate = formatDate(props.createdAt);
+ mountComponent({ props: { state: 'opened' } });
+ const stateTitle = findIcon().attributes('title');
+ const formattedCreateDate = formatDate(defaultProps.createdAt);
expect(stateTitle).toContain('<span class="bold">Created</span>');
expect(stateTitle).toContain(`<span class="text-tertiary">${formattedCreateDate}</span>`);
});
it('renders aria label', () => {
- expect(tokenState().attributes('aria-label')).toEqual('opened');
+ mountComponent({ props: { state: 'opened' } });
+
+ expect(findIcon().attributes('arialabel')).toBe('opened');
});
it('renders open icon when open state', () => {
- expect(tokenState().classes('issue-token-state-icon-open')).toBe(true);
+ mountComponent({ props: { state: 'opened' } });
+
+ expect(findIcon().props('name')).toBe('issue-open-m');
+ expect(findIcon().classes('issue-token-state-icon-open')).toBe(true);
});
- it('renders close icon when close state', async () => {
- wrapper.setProps({
- state: 'closed',
- closedAt: '2018-12-01T00:00:00.00Z',
- });
- await nextTick();
+ it('renders close icon when close state', () => {
+ mountComponent({ props: { state: 'closed', closedAt: '2018-12-01T00:00:00.00Z' } });
- expect(tokenState().classes('issue-token-state-icon-closed')).toBe(true);
+ expect(findIcon().props('name')).toBe('issue-close');
+ expect(findIcon().classes('issue-token-state-icon-closed')).toBe(true);
});
});
@@ -116,75 +128,66 @@ describe('RelatedIssuableItem', () => {
const tokenMetadata = () => wrapper.find('.item-meta');
it('renders item path and ID', () => {
+ mountComponent();
const pathAndID = tokenMetadata().find('.item-path-id').text();
expect(pathAndID).toContain('gitlab-org/gitlab-test');
expect(pathAndID).toContain('#1');
});
- it('renders milestone icon and name', () => {
- const milestoneIcon = tokenMetadata().find('.item-milestone svg');
- const milestoneTitle = tokenMetadata().find('.item-milestone .milestone-title');
+ it('renders milestone', () => {
+ mountComponent();
- expect(milestoneIcon.attributes('data-testid')).toBe('clock-icon');
- expect(milestoneTitle.text()).toContain('Milestone title');
+ expect(wrapper.findComponent(IssueMilestone).props('milestone')).toEqual(
+ defaultProps.milestone,
+ );
});
it('renders due date component with correct due date', () => {
- expect(wrapper.find(IssueDueDate).props('date')).toBe(props.dueDate);
+ mountComponent();
+
+ expect(findIssueDueDate().props('date')).toBe(defaultProps.dueDate);
});
- it('does not render red icon for overdue issue that is closed', async () => {
- mountComponent({
- props: {
- ...props,
- closedAt: '2018-12-01T00:00:00.00Z',
- },
- });
- await nextTick();
+ it('does not render red icon for overdue issue that is closed', () => {
+ mountComponent({ props: { closedAt: '2018-12-01T00:00:00.00Z' } });
- expect(wrapper.find(IssueDueDate).props('closed')).toBe(true);
+ expect(findIssueDueDate().props('closed')).toBe(true);
});
});
describe('token assignees', () => {
it('renders assignees avatars', () => {
- // Expect 2 times 2 because assignees are rendered twice, due to layout issues
- expect(wrapper.findAll('.item-assignees .user-avatar-link').length).toBeDefined();
+ mountComponent();
- expect(wrapper.find('.item-assignees .avatar-counter').text()).toContain('+2');
+ expect(wrapper.findComponent(IssueAssignees).props('assignees')).toEqual(
+ defaultProps.assignees,
+ );
});
});
describe('remove button', () => {
beforeEach(() => {
- wrapper.setProps({ canRemove: true });
+ mountComponent({ props: { canRemove: true }, data: { removeDisabled: true } });
});
it('renders if canRemove', () => {
- expect(findRemoveButton().exists()).toBe(true);
+ expect(findRemoveButton().props('icon')).toBe('close');
+ expect(findRemoveButton().attributes('aria-label')).toBe(__('Remove'));
});
it('does not render the lock icon', () => {
expect(findLockIcon().exists()).toBe(false);
});
- it('renders disabled button when removeDisabled', async () => {
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({ removeDisabled: true });
- await nextTick();
-
- expect(findRemoveButton().attributes('disabled')).toEqual('disabled');
+ it('renders disabled button when removeDisabled', () => {
+ expect(findRemoveButton().attributes('disabled')).toBe('true');
});
- it('triggers onRemoveRequest when clicked', async () => {
- findRemoveButton().trigger('click');
- await nextTick();
- const { relatedIssueRemoveRequest } = wrapper.emitted();
+ it('triggers onRemoveRequest when clicked', () => {
+ findRemoveButton().vm.$emit('click');
- expect(relatedIssueRemoveRequest.length).toBe(1);
- expect(relatedIssueRemoveRequest[0]).toEqual([props.idKey]);
+ expect(wrapper.emitted('relatedIssueRemoveRequest')).toEqual([[defaultProps.idKey]]);
});
});
@@ -192,10 +195,7 @@ describe('RelatedIssuableItem', () => {
const lockedMessage = 'Issues created from a vulnerability cannot be removed';
beforeEach(() => {
- wrapper.setProps({
- isLocked: true,
- lockedMessage,
- });
+ mountComponent({ props: { isLocked: true, lockedMessage } });
});
it('does not render the remove button', () => {
@@ -206,4 +206,67 @@ describe('RelatedIssuableItem', () => {
expect(findLockIcon().attributes('title')).toBe(lockedMessage);
});
});
+
+ describe('work item modal', () => {
+ const workItem = 'gid://gitlab/WorkItem/1';
+
+ it('renders', () => {
+ mountComponent();
+
+ expect(findWorkItemDetailModal().props('workItemId')).toBe(workItem);
+ });
+
+ describe('when work item is issue and the related issue title is clicked', () => {
+ it('does not open', () => {
+ mountComponent({ props: { workItemType: 'ISSUE' } });
+ wrapper.vm.$refs.modal.show = jest.fn();
+
+ findTitleLink().vm.$emit('click', { preventDefault: () => {} });
+
+ expect(wrapper.vm.$refs.modal.show).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('when work item is task and the related issue title is clicked', () => {
+ beforeEach(() => {
+ mountComponent({ props: { workItemType: 'TASK' } });
+ wrapper.vm.$refs.modal.show = jest.fn();
+ findTitleLink().vm.$emit('click', { preventDefault: () => {} });
+ });
+
+ it('opens', () => {
+ expect(wrapper.vm.$refs.modal.show).toHaveBeenCalled();
+ });
+
+ it('updates the url params with the work item id', () => {
+ expect(updateHistory).toHaveBeenCalledWith({
+ url: `${TEST_HOST}/?work_item_id=1`,
+ replace: true,
+ });
+ });
+ });
+
+ describe('when it emits "workItemDeleted" event', () => {
+ it('emits "relatedIssueRemoveRequest" event', () => {
+ mountComponent();
+
+ findWorkItemDetailModal().vm.$emit('workItemDeleted', workItem);
+
+ expect(wrapper.emitted('relatedIssueRemoveRequest')).toEqual([[workItem]]);
+ });
+ });
+
+ describe('when it emits "close" event', () => {
+ it('removes the work item id from the url params', () => {
+ mountComponent();
+
+ findWorkItemDetailModal().vm.$emit('close');
+
+ expect(updateHistory).toHaveBeenCalledWith({
+ url: `${TEST_HOST}/`,
+ replace: true,
+ });
+ });
+ });
+ });
});
diff --git a/spec/frontend/issuable/popover/components/issue_popover_spec.js b/spec/frontend/issuable/popover/components/issue_popover_spec.js
index 3e77e750f3a..444165f61c7 100644
--- a/spec/frontend/issuable/popover/components/issue_popover_spec.js
+++ b/spec/frontend/issuable/popover/components/issue_popover_spec.js
@@ -1,33 +1,23 @@
-import { GlSkeletonLoader } from '@gitlab/ui';
+import { GlIcon, GlSkeletonLoader } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
+import issueQueryResponse from 'test_fixtures/graphql/issuable/popover/queries/issue.query.graphql.json';
+import issueQuery from 'ee_else_ce/issuable/popover/queries/issue.query.graphql';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
+import IssueDueDate from '~/boards/components/issue_due_date.vue';
+import IssueMilestone from '~/issuable/components/issue_milestone.vue';
import StatusBox from '~/issuable/components/status_box.vue';
import IssuePopover from '~/issuable/popover/components/issue_popover.vue';
-import issueQuery from '~/issuable/popover/queries/issue.query.graphql';
+import WorkItemTypeIcon from '~/work_items/components/work_item_type_icon.vue';
describe('Issue Popover', () => {
let wrapper;
Vue.use(VueApollo);
- const issueQueryResponse = {
- data: {
- project: {
- __typename: 'Project',
- id: '1',
- issue: {
- __typename: 'Issue',
- id: 'gid://gitlab/Issue/1',
- createdAt: '2020-07-01T04:08:01Z',
- state: 'opened',
- title: 'Issue title',
- },
- },
- },
- };
+ const findWorkItemIcon = () => wrapper.findComponent(WorkItemTypeIcon);
const mountComponent = ({
queryResponse = jest.fn().mockResolvedValue(issueQueryResponse),
@@ -53,6 +43,12 @@ describe('Issue Popover', () => {
expect(wrapper.findComponent(GlSkeletonLoader).exists()).toBe(true);
});
+ it('should not show any work item icon while apollo is loading', () => {
+ mountComponent();
+
+ expect(findWorkItemIcon().exists()).toBe(false);
+ });
+
describe('when loaded', () => {
beforeEach(() => {
mountComponent();
@@ -74,8 +70,40 @@ describe('Issue Popover', () => {
expect(wrapper.find('h5').text()).toBe(issueQueryResponse.data.project.issue.title);
});
+ it('shows the work type icon', () => {
+ expect(findWorkItemIcon().props('workItemType')).toBe(
+ issueQueryResponse.data.project.issue.type,
+ );
+ });
+
it('shows reference', () => {
expect(wrapper.text()).toContain('foo/bar#1');
});
+
+ it('shows confidential icon', () => {
+ const icon = wrapper.findComponent(GlIcon);
+
+ expect(icon.exists()).toBe(true);
+ expect(icon.props('name')).toBe('eye-slash');
+ });
+
+ it('shows due date', () => {
+ const component = wrapper.findComponent(IssueDueDate);
+
+ expect(component.exists()).toBe(true);
+ expect(component.props('date')).toBe('2020-07-05');
+ expect(component.props('closed')).toBe(false);
+ });
+
+ it('shows milestone', () => {
+ const component = wrapper.findComponent(IssueMilestone);
+
+ expect(component.exists()).toBe(true);
+ expect(component.props('milestone')).toMatchObject({
+ title: '15.2',
+ startDate: '2020-07-01',
+ dueDate: '2020-07-30',
+ });
+ });
});
});
diff --git a/spec/frontend/issuable/related_issues/components/add_issuable_form_spec.js b/spec/frontend/issuable/related_issues/components/add_issuable_form_spec.js
index ce98a16dbb7..16d4459f597 100644
--- a/spec/frontend/issuable/related_issues/components/add_issuable_form_spec.js
+++ b/spec/frontend/issuable/related_issues/components/add_issuable_form_spec.js
@@ -157,8 +157,8 @@ describe('AddIssuableForm', () => {
describe('categorized issuables', () => {
it.each`
issuableType | pathIdSeparator | contextHeader | contextFooter
- ${issuableTypesMap.ISSUE} | ${PathIdSeparator.Issue} | ${'The current issue'} | ${'the following issue(s)'}
- ${issuableTypesMap.EPIC} | ${PathIdSeparator.Epic} | ${'The current epic'} | ${'the following epic(s)'}
+ ${issuableTypesMap.ISSUE} | ${PathIdSeparator.Issue} | ${'The current issue'} | ${'the following issues'}
+ ${issuableTypesMap.EPIC} | ${PathIdSeparator.Epic} | ${'The current epic'} | ${'the following epics'}
`(
'show header text as "$contextHeader" and footer text as "$contextFooter" issuableType is set to $issuableType',
({ issuableType, contextHeader, contextFooter }) => {
diff --git a/spec/frontend/issuable/related_issues/components/related_issues_block_spec.js b/spec/frontend/issuable/related_issues/components/related_issues_block_spec.js
index 7a350df0ba6..772cc75a205 100644
--- a/spec/frontend/issuable/related_issues/components/related_issues_block_spec.js
+++ b/spec/frontend/issuable/related_issues/components/related_issues_block_spec.js
@@ -1,5 +1,6 @@
-import { GlButton, GlIcon } from '@gitlab/ui';
-import { shallowMount, mount } from '@vue/test-utils';
+import { nextTick } from 'vue';
+import { GlIcon } from '@gitlab/ui';
+import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
import {
issuable1,
issuable2,
@@ -17,7 +18,9 @@ import {
describe('RelatedIssuesBlock', () => {
let wrapper;
- const findIssueCountBadgeAddButton = () => wrapper.find(GlButton);
+ const findToggleButton = () => wrapper.findByTestId('toggle-links');
+ const findRelatedIssuesBody = () => wrapper.findByTestId('related-issues-body');
+ const findIssueCountBadgeAddButton = () => wrapper.findByTestId('related-issues-plus-button');
afterEach(() => {
if (wrapper) {
@@ -28,7 +31,7 @@ describe('RelatedIssuesBlock', () => {
describe('with defaults', () => {
beforeEach(() => {
- wrapper = mount(RelatedIssuesBlock, {
+ wrapper = mountExtended(RelatedIssuesBlock, {
propsData: {
pathIdSeparator: PathIdSeparator.Issue,
issuableType: issuableTypesMap.ISSUE,
@@ -37,13 +40,13 @@ describe('RelatedIssuesBlock', () => {
});
it.each`
- issuableType | pathIdSeparator | titleText | helpLinkText | addButtonText
- ${'issue'} | ${PathIdSeparator.Issue} | ${'Linked issues'} | ${'Read more about related issues'} | ${'Add a related issue'}
- ${'epic'} | ${PathIdSeparator.Epic} | ${'Linked epics'} | ${'Read more about related epics'} | ${'Add a related epic'}
+ issuableType | pathIdSeparator | titleText | helpLinkText | addButtonText
+ ${'issue'} | ${PathIdSeparator.Issue} | ${'Linked items'} | ${'Read more about related issues'} | ${'Add a related issue'}
+ ${'epic'} | ${PathIdSeparator.Epic} | ${'Linked epics'} | ${'Read more about related epics'} | ${'Add a related epic'}
`(
'displays "$titleText" in the header, "$helpLinkText" aria-label for help link, and "$addButtonText" aria-label for add button when issuableType is set to "$issuableType"',
({ issuableType, pathIdSeparator, titleText, helpLinkText, addButtonText }) => {
- wrapper = mount(RelatedIssuesBlock, {
+ wrapper = mountExtended(RelatedIssuesBlock, {
propsData: {
pathIdSeparator,
issuableType,
@@ -73,7 +76,7 @@ describe('RelatedIssuesBlock', () => {
it('displays header text slot data', () => {
const headerText = '<div>custom header text</div>';
- wrapper = shallowMount(RelatedIssuesBlock, {
+ wrapper = shallowMountExtended(RelatedIssuesBlock, {
propsData: {
pathIdSeparator: PathIdSeparator.Issue,
issuableType: 'issue',
@@ -89,7 +92,7 @@ describe('RelatedIssuesBlock', () => {
it('displays header actions slot data', () => {
const headerActions = '<button data-testid="custom-button">custom button</button>';
- wrapper = shallowMount(RelatedIssuesBlock, {
+ wrapper = shallowMountExtended(RelatedIssuesBlock, {
propsData: {
pathIdSeparator: PathIdSeparator.Issue,
issuableType: 'issue',
@@ -103,7 +106,7 @@ describe('RelatedIssuesBlock', () => {
describe('with isFetching=true', () => {
beforeEach(() => {
- wrapper = mount(RelatedIssuesBlock, {
+ wrapper = mountExtended(RelatedIssuesBlock, {
propsData: {
pathIdSeparator: PathIdSeparator.Issue,
isFetching: true,
@@ -119,7 +122,7 @@ describe('RelatedIssuesBlock', () => {
describe('with canAddRelatedIssues=true', () => {
beforeEach(() => {
- wrapper = mount(RelatedIssuesBlock, {
+ wrapper = mountExtended(RelatedIssuesBlock, {
propsData: {
pathIdSeparator: PathIdSeparator.Issue,
canAdmin: true,
@@ -135,7 +138,7 @@ describe('RelatedIssuesBlock', () => {
describe('with isFormVisible=true', () => {
beforeEach(() => {
- wrapper = mount(RelatedIssuesBlock, {
+ wrapper = mountExtended(RelatedIssuesBlock, {
propsData: {
pathIdSeparator: PathIdSeparator.Issue,
isFormVisible: true,
@@ -159,7 +162,7 @@ describe('RelatedIssuesBlock', () => {
const categorizedHeadings = () => wrapper.findAll('h4');
const headingTextAt = (index) => categorizedHeadings().at(index).text();
const mountComponent = (showCategorizedIssues) => {
- wrapper = mount(RelatedIssuesBlock, {
+ wrapper = mountExtended(RelatedIssuesBlock, {
propsData: {
pathIdSeparator: PathIdSeparator.Issue,
relatedIssues: [issuable1, issuable2, issuable3],
@@ -217,7 +220,7 @@ describe('RelatedIssuesBlock', () => {
},
].forEach(({ issuableType, icon }) => {
it(`issuableType=${issuableType} is passed`, () => {
- wrapper = shallowMount(RelatedIssuesBlock, {
+ wrapper = shallowMountExtended(RelatedIssuesBlock, {
propsData: {
pathIdSeparator: PathIdSeparator.Issue,
issuableType,
@@ -230,4 +233,42 @@ describe('RelatedIssuesBlock', () => {
});
});
});
+
+ describe('toggle', () => {
+ beforeEach(() => {
+ wrapper = shallowMountExtended(RelatedIssuesBlock, {
+ propsData: {
+ pathIdSeparator: PathIdSeparator.Issue,
+ relatedIssues: [issuable1, issuable2, issuable3],
+ issuableType: issuableTypesMap.ISSUE,
+ },
+ });
+ });
+
+ it('is expanded by default', () => {
+ expect(findToggleButton().props('icon')).toBe('chevron-lg-up');
+ expect(findToggleButton().props('disabled')).toBe(false);
+ expect(findRelatedIssuesBody().exists()).toBe(true);
+ });
+
+ it('expands on click toggle button', async () => {
+ findToggleButton().vm.$emit('click');
+ await nextTick();
+
+ expect(findToggleButton().props('icon')).toBe('chevron-lg-down');
+ expect(findRelatedIssuesBody().exists()).toBe(false);
+ });
+ });
+
+ it('toggle button is disabled when issue has no related items', () => {
+ wrapper = shallowMountExtended(RelatedIssuesBlock, {
+ propsData: {
+ pathIdSeparator: PathIdSeparator.Issue,
+ relatedIssues: [],
+ issuableType: 'issue',
+ },
+ });
+
+ expect(findToggleButton().props('disabled')).toBe(true);
+ });
});
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 1a03ea58b60..b518d2fbdec 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
@@ -1,4 +1,4 @@
-import { mount, shallowMount } from '@vue/test-utils';
+import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import { nextTick } from 'vue';
import waitForPromises from 'helpers/wait_for_promises';
@@ -9,8 +9,9 @@ import {
} from 'jest/issuable/components/related_issuable_mock_data';
import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
-import RelatedIssuesRoot from '~/related_issues/components/related_issues_root.vue';
import { linkedIssueTypesMap } from '~/related_issues/constants';
+import RelatedIssuesBlock from '~/related_issues/components/related_issues_block.vue';
+import RelatedIssuesRoot from '~/related_issues/components/related_issues_root.vue';
import relatedIssuesService from '~/related_issues/services/related_issues_service';
jest.mock('~/flash');
@@ -19,6 +20,8 @@ describe('RelatedIssuesRoot', () => {
let wrapper;
let mock;
+ const findRelatedIssuesBlock = () => wrapper.findComponent(RelatedIssuesBlock);
+
beforeEach(() => {
mock = new MockAdapter(axios);
mock.onGet(defaultProps.endpoint).reply(200, []);
@@ -26,100 +29,114 @@ describe('RelatedIssuesRoot', () => {
afterEach(() => {
mock.restore();
- if (wrapper) {
- wrapper.destroy();
- wrapper = null;
- }
+ wrapper.destroy();
});
- const createComponent = (mountFn = mount) => {
- wrapper = mountFn(RelatedIssuesRoot, {
- propsData: defaultProps,
+ const createComponent = ({ props = {}, data = {} } = {}) => {
+ wrapper = mount(RelatedIssuesRoot, {
+ propsData: {
+ ...defaultProps,
+ ...props,
+ },
+ data() {
+ return data;
+ },
});
// Wait for fetch request `fetchRelatedIssues` to complete before starting to test
return waitForPromises();
};
- describe('methods', () => {
- describe('onRelatedIssueRemoveRequest', () => {
- beforeEach(() => {
- jest
- .spyOn(relatedIssuesService.prototype, 'fetchRelatedIssues')
- .mockReturnValue(Promise.reject());
-
- return createComponent().then(() => {
+ describe('events', () => {
+ describe('when "relatedIssueRemoveRequest" event is emitted', () => {
+ describe('when emitted value is a numerical issue', () => {
+ beforeEach(async () => {
+ jest
+ .spyOn(relatedIssuesService.prototype, 'fetchRelatedIssues')
+ .mockReturnValue(Promise.reject());
+ await createComponent();
wrapper.vm.store.setRelatedIssues([issuable1]);
});
- });
- it('remove related issue and succeeds', () => {
- mock.onDelete(issuable1.referencePath).reply(200, { issues: [] });
+ it('removes related issue on API success', async () => {
+ mock.onDelete(issuable1.referencePath).reply(200, { issues: [] });
+
+ findRelatedIssuesBlock().vm.$emit('relatedIssueRemoveRequest', issuable1.id);
+ await axios.waitForAll();
+
+ expect(findRelatedIssuesBlock().props('relatedIssues')).toEqual([]);
+ });
+
+ it('does not remove related issue on API error', async () => {
+ mock.onDelete(issuable1.referencePath).reply(422, {});
- wrapper.vm.onRelatedIssueRemoveRequest(issuable1.id);
+ findRelatedIssuesBlock().vm.$emit('relatedIssueRemoveRequest', issuable1.id);
+ await axios.waitForAll();
- return axios.waitForAll().then(() => {
- expect(wrapper.vm.state.relatedIssues).toEqual([]);
+ expect(findRelatedIssuesBlock().props('relatedIssues')).toEqual([
+ expect.objectContaining({ id: issuable1.id }),
+ ]);
});
});
- it('remove related issue, fails, and restores to related issues', () => {
- mock.onDelete(issuable1.referencePath).reply(422, {});
+ describe('when emitted value is a work item id', () => {
+ it('removes related issue', async () => {
+ const workItem = `gid://gitlab/WorkItem/${issuable1.id}`;
+ createComponent({ data: { state: { relatedIssues: [issuable1] } } });
- wrapper.vm.onRelatedIssueRemoveRequest(issuable1.id);
+ findRelatedIssuesBlock().vm.$emit('relatedIssueRemoveRequest', workItem);
+ await nextTick();
- return axios.waitForAll().then(() => {
- expect(wrapper.vm.state.relatedIssues).toHaveLength(1);
- expect(wrapper.vm.state.relatedIssues[0].id).toEqual(issuable1.id);
+ expect(findRelatedIssuesBlock().props('relatedIssues')).toEqual([]);
});
});
});
- describe('onToggleAddRelatedIssuesForm', () => {
- beforeEach(() => createComponent(shallowMount));
+ describe('when "toggleAddRelatedIssuesForm" event is emitted', () => {
+ it('toggles related issues form to visible from hidden', async () => {
+ createComponent();
- it('toggle related issues form to visible', () => {
- wrapper.vm.onToggleAddRelatedIssuesForm();
+ findRelatedIssuesBlock().vm.$emit('toggleAddRelatedIssuesForm');
+ await nextTick();
- expect(wrapper.vm.isFormVisible).toEqual(true);
+ expect(findRelatedIssuesBlock().props('isFormVisible')).toBe(true);
});
- it('show add related issues form to hidden', () => {
- wrapper.vm.isFormVisible = true;
+ it('toggles related issues form to hidden from visible', async () => {
+ createComponent({ data: { isFormVisible: true } });
- wrapper.vm.onToggleAddRelatedIssuesForm();
+ findRelatedIssuesBlock().vm.$emit('toggleAddRelatedIssuesForm');
+ await nextTick();
- expect(wrapper.vm.isFormVisible).toEqual(false);
+ expect(findRelatedIssuesBlock().props('isFormVisible')).toBe(false);
});
});
- describe('onPendingIssueRemoveRequest', () => {
- beforeEach(() =>
- createComponent().then(() => {
- wrapper.vm.store.setPendingReferences([issuable1.reference]);
- }),
- );
+ describe('when "pendingIssuableRemoveRequest" event is emitted', () => {
+ beforeEach(() => {
+ createComponent();
+ wrapper.vm.store.setPendingReferences([issuable1.reference]);
+ });
- it('remove pending related issue', () => {
- expect(wrapper.vm.state.pendingReferences).toHaveLength(1);
+ it('removes pending related issue', async () => {
+ expect(findRelatedIssuesBlock().props('pendingReferences')).toHaveLength(1);
- wrapper.vm.onPendingIssueRemoveRequest(0);
+ findRelatedIssuesBlock().vm.$emit('pendingIssuableRemoveRequest', 0);
+ await nextTick();
- expect(wrapper.vm.state.pendingReferences).toHaveLength(0);
+ expect(findRelatedIssuesBlock().props('pendingReferences')).toHaveLength(0);
});
});
- describe('onPendingFormSubmit', () => {
- beforeEach(() => {
+ describe('when "addIssuableFormSubmit" event is emitted', () => {
+ beforeEach(async () => {
jest
.spyOn(relatedIssuesService.prototype, 'fetchRelatedIssues')
.mockReturnValue(Promise.reject());
-
- return createComponent().then(() => {
- jest.spyOn(wrapper.vm, 'processAllReferences');
- jest.spyOn(wrapper.vm.service, 'addRelatedIssues');
- createFlash.mockClear();
- });
+ await createComponent();
+ jest.spyOn(wrapper.vm, 'processAllReferences');
+ jest.spyOn(wrapper.vm.service, 'addRelatedIssues');
+ createFlash.mockClear();
});
it('processes references before submitting', () => {
@@ -130,23 +147,22 @@ describe('RelatedIssuesRoot', () => {
linkedIssueType,
};
- wrapper.vm.onPendingFormSubmit(emitObj);
+ findRelatedIssuesBlock().vm.$emit('addIssuableFormSubmit', emitObj);
expect(wrapper.vm.processAllReferences).toHaveBeenCalledWith(input);
expect(wrapper.vm.service.addRelatedIssues).toHaveBeenCalledWith([input], linkedIssueType);
});
- it('submit zero pending issue as related issue', () => {
+ it('submits zero pending issues as related issue', () => {
wrapper.vm.store.setPendingReferences([]);
- wrapper.vm.onPendingFormSubmit({});
- return waitForPromises().then(() => {
- expect(wrapper.vm.state.pendingReferences).toHaveLength(0);
- expect(wrapper.vm.state.relatedIssues).toHaveLength(0);
- });
+ findRelatedIssuesBlock().vm.$emit('addIssuableFormSubmit', {});
+
+ expect(findRelatedIssuesBlock().props('pendingReferences')).toHaveLength(0);
+ expect(findRelatedIssuesBlock().props('relatedIssues')).toHaveLength(0);
});
- it('submit pending issue as related issue', () => {
+ it('submits pending issue as related issue', async () => {
mock.onPost(defaultProps.endpoint).reply(200, {
issuables: [issuable1],
result: {
@@ -154,18 +170,18 @@ describe('RelatedIssuesRoot', () => {
status: 'success',
},
});
-
wrapper.vm.store.setPendingReferences([issuable1.reference]);
- wrapper.vm.onPendingFormSubmit({});
- return waitForPromises().then(() => {
- expect(wrapper.vm.state.pendingReferences).toHaveLength(0);
- expect(wrapper.vm.state.relatedIssues).toHaveLength(1);
- expect(wrapper.vm.state.relatedIssues[0].id).toEqual(issuable1.id);
- });
+ findRelatedIssuesBlock().vm.$emit('addIssuableFormSubmit', {});
+ await waitForPromises();
+
+ expect(findRelatedIssuesBlock().props('pendingReferences')).toHaveLength(0);
+ expect(findRelatedIssuesBlock().props('relatedIssues')).toEqual([
+ expect.objectContaining({ id: issuable1.id }),
+ ]);
});
- it('submit multiple pending issues as related issues', () => {
+ it('submits multiple pending issues as related issues', async () => {
mock.onPost(defaultProps.endpoint).reply(200, {
issuables: [issuable1, issuable2],
result: {
@@ -173,201 +189,148 @@ describe('RelatedIssuesRoot', () => {
status: 'success',
},
});
-
wrapper.vm.store.setPendingReferences([issuable1.reference, issuable2.reference]);
- wrapper.vm.onPendingFormSubmit({});
- return waitForPromises().then(() => {
- expect(wrapper.vm.state.pendingReferences).toHaveLength(0);
- expect(wrapper.vm.state.relatedIssues).toHaveLength(2);
- expect(wrapper.vm.state.relatedIssues[0].id).toEqual(issuable1.id);
- expect(wrapper.vm.state.relatedIssues[1].id).toEqual(issuable2.id);
- });
+ findRelatedIssuesBlock().vm.$emit('addIssuableFormSubmit', {});
+ await waitForPromises();
+
+ expect(findRelatedIssuesBlock().props('pendingReferences')).toHaveLength(0);
+ expect(findRelatedIssuesBlock().props('relatedIssues')).toEqual([
+ expect.objectContaining({ id: issuable1.id }),
+ expect.objectContaining({ id: issuable2.id }),
+ ]);
});
- it('displays a message from the backend upon error', () => {
+ it('displays a message from the backend upon error', async () => {
const input = '#123';
const message = 'error';
-
mock.onPost(defaultProps.endpoint).reply(409, { message });
wrapper.vm.store.setPendingReferences([issuable1.reference, issuable2.reference]);
expect(createFlash).not.toHaveBeenCalled();
- wrapper.vm.onPendingFormSubmit(input);
-
- return waitForPromises().then(() => {
- expect(createFlash).toHaveBeenCalledWith({
- message,
- });
- });
- });
- });
- describe('onPendingFormCancel', () => {
- beforeEach(() =>
- createComponent().then(() => {
- wrapper.vm.isFormVisible = true;
- wrapper.vm.inputValue = 'foo';
- }),
- );
-
- it('when canceling and hiding add issuable form', async () => {
- wrapper.vm.onPendingFormCancel();
+ findRelatedIssuesBlock().vm.$emit('addIssuableFormSubmit', input);
+ await waitForPromises();
- await nextTick();
- expect(wrapper.vm.isFormVisible).toEqual(false);
- expect(wrapper.vm.inputValue).toEqual('');
- expect(wrapper.vm.state.pendingReferences).toHaveLength(0);
+ expect(createFlash).toHaveBeenCalledWith({ message });
});
});
- describe('fetchRelatedIssues', () => {
- beforeEach(() => createComponent());
-
- it('sets isFetching while fetching', async () => {
- wrapper.vm.fetchRelatedIssues();
+ describe('when "addIssuableFormCancel" event is emitted', () => {
+ beforeEach(() => createComponent({ data: { isFormVisible: true, inputValue: 'foo' } }));
- expect(wrapper.vm.isFetching).toEqual(true);
+ it('hides form and resets input', async () => {
+ findRelatedIssuesBlock().vm.$emit('addIssuableFormCancel');
+ await nextTick();
- await waitForPromises();
- expect(wrapper.vm.isFetching).toEqual(false);
- });
-
- it('should fetch related issues', async () => {
- mock.onGet(defaultProps.endpoint).reply(200, [issuable1, issuable2]);
-
- wrapper.vm.fetchRelatedIssues();
-
- await waitForPromises();
- expect(wrapper.vm.state.relatedIssues).toHaveLength(2);
- expect(wrapper.vm.state.relatedIssues[0].id).toEqual(issuable1.id);
- expect(wrapper.vm.state.relatedIssues[1].id).toEqual(issuable2.id);
+ expect(findRelatedIssuesBlock().props('isFormVisible')).toBe(false);
+ expect(findRelatedIssuesBlock().props('inputValue')).toBe('');
+ expect(findRelatedIssuesBlock().props('pendingReferences')).toHaveLength(0);
});
});
- describe('onInput', () => {
- beforeEach(() => createComponent());
-
- it('fill in issue number reference and adds to pending related issues', () => {
+ describe('when "addIssuableFormInput" event is emitted', () => {
+ it('updates pending references with issue reference', async () => {
const input = '#123 ';
- wrapper.vm.onInput({
+ createComponent();
+
+ findRelatedIssuesBlock().vm.$emit('addIssuableFormInput', {
untouchedRawReferences: [input.trim()],
touchedReference: input,
});
+ await nextTick();
- expect(wrapper.vm.state.pendingReferences).toHaveLength(1);
- expect(wrapper.vm.state.pendingReferences[0]).toEqual('#123');
+ expect(findRelatedIssuesBlock().props('pendingReferences')).toEqual([input.trim()]);
});
- it('fill in with full reference', () => {
+ it('updates pending references with full reference', async () => {
const input = 'asdf/qwer#444 ';
- wrapper.vm.onInput({ untouchedRawReferences: [input.trim()], touchedReference: input });
+ createComponent();
+
+ findRelatedIssuesBlock().vm.$emit('addIssuableFormInput', {
+ untouchedRawReferences: [input.trim()],
+ touchedReference: input,
+ });
+ await nextTick();
- expect(wrapper.vm.state.pendingReferences).toHaveLength(1);
- expect(wrapper.vm.state.pendingReferences[0]).toEqual('asdf/qwer#444');
+ expect(findRelatedIssuesBlock().props('pendingReferences')).toEqual([input.trim()]);
});
- it('fill in with issue link', () => {
+ it('updates pending references with issue link', async () => {
const link = 'http://localhost:3000/foo/bar/issues/111';
const input = `${link} `;
- wrapper.vm.onInput({ untouchedRawReferences: [input.trim()], touchedReference: input });
+ createComponent();
- expect(wrapper.vm.state.pendingReferences).toHaveLength(1);
- expect(wrapper.vm.state.pendingReferences[0]).toEqual(link);
+ findRelatedIssuesBlock().vm.$emit('addIssuableFormInput', {
+ untouchedRawReferences: [input.trim()],
+ touchedReference: input,
+ });
+ await nextTick();
+
+ expect(findRelatedIssuesBlock().props('pendingReferences')).toEqual([link]);
});
- it('fill in with multiple references', () => {
+ it('updates pending references with multiple references', async () => {
const input = 'asdf/qwer#444 #12 ';
- wrapper.vm.onInput({
+ createComponent();
+
+ findRelatedIssuesBlock().vm.$emit('addIssuableFormInput', {
untouchedRawReferences: input.trim().split(/\s/),
touchedReference: '2',
});
+ await nextTick();
- expect(wrapper.vm.state.pendingReferences).toHaveLength(2);
- expect(wrapper.vm.state.pendingReferences[0]).toEqual('asdf/qwer#444');
- expect(wrapper.vm.state.pendingReferences[1]).toEqual('#12');
+ expect(findRelatedIssuesBlock().props('pendingReferences')).toEqual([
+ 'asdf/qwer#444',
+ '#12',
+ ]);
});
- it('fill in with some invalid things', () => {
+ it('updates pending references with invalid values', async () => {
const input = 'something random ';
- wrapper.vm.onInput({
+ createComponent();
+
+ findRelatedIssuesBlock().vm.$emit('addIssuableFormInput', {
untouchedRawReferences: input.trim().split(/\s/),
touchedReference: '2',
});
+ await nextTick();
- expect(wrapper.vm.state.pendingReferences).toHaveLength(2);
- expect(wrapper.vm.state.pendingReferences[0]).toEqual('something');
- expect(wrapper.vm.state.pendingReferences[1]).toEqual('random');
+ expect(findRelatedIssuesBlock().props('pendingReferences')).toEqual([
+ 'something',
+ 'random',
+ ]);
});
- it.each`
- pathIdSeparator
- ${'#'}
- ${'&'}
- `(
- 'prepends $pathIdSeparator when user enters a numeric value [0-9]',
- async ({ pathIdSeparator }) => {
+ it.each(['#', '&'])(
+ 'prepends %s when user enters a numeric value [0-9]',
+ async (pathIdSeparator) => {
const input = '23';
+ createComponent({ props: { pathIdSeparator } });
- await wrapper.setProps({
- pathIdSeparator,
- });
-
- wrapper.vm.onInput({
+ findRelatedIssuesBlock().vm.$emit('addIssuableFormInput', {
untouchedRawReferences: input.trim().split(/\s/),
touchedReference: input,
});
+ await nextTick();
- expect(wrapper.vm.inputValue).toBe(`${pathIdSeparator}${input}`);
+ expect(findRelatedIssuesBlock().props('inputValue')).toBe(`${pathIdSeparator}${input}`);
},
);
-
- it('prepends # when user enters a number', async () => {
- const input = 23;
-
- wrapper.vm.onInput({
- untouchedRawReferences: String(input).trim().split(/\s/),
- touchedReference: input,
- });
-
- expect(wrapper.vm.inputValue).toBe(`#${input}`);
- });
});
- describe('onBlur', () => {
- beforeEach(() =>
- createComponent().then(() => {
- jest.spyOn(wrapper.vm, 'processAllReferences').mockImplementation(() => {});
- }),
- );
-
- it('add any references to pending when blurring', () => {
- const input = '#123';
-
- wrapper.vm.onBlur(input);
-
- expect(wrapper.vm.processAllReferences).toHaveBeenCalledWith(input);
+ describe('when "addIssuableFormBlur" event is emitted', () => {
+ beforeEach(() => {
+ createComponent();
+ jest.spyOn(wrapper.vm, 'processAllReferences').mockImplementation(() => {});
});
- });
-
- describe('processAllReferences', () => {
- beforeEach(() => createComponent());
- it('add valid reference to pending', () => {
+ it('adds any references to pending when blurring', () => {
const input = '#123';
- wrapper.vm.processAllReferences(input);
- expect(wrapper.vm.state.pendingReferences).toHaveLength(1);
- expect(wrapper.vm.state.pendingReferences[0]).toEqual('#123');
- });
+ findRelatedIssuesBlock().vm.$emit('addIssuableFormBlur', input);
- it('add any valid references to pending', () => {
- const input = 'asdf #123';
- wrapper.vm.processAllReferences(input);
-
- expect(wrapper.vm.state.pendingReferences).toHaveLength(2);
- expect(wrapper.vm.state.pendingReferences[0]).toEqual('asdf');
- expect(wrapper.vm.state.pendingReferences[1]).toEqual('#123');
+ expect(wrapper.vm.processAllReferences).toHaveBeenCalledWith(input);
});
});
});
diff --git a/spec/frontend/issues/list/components/issues_list_app_spec.js b/spec/frontend/issues/list/components/issues_list_app_spec.js
index 3d3dbfa6853..a39853fd29c 100644
--- a/spec/frontend/issues/list/components/issues_list_app_spec.js
+++ b/spec/frontend/issues/list/components/issues_list_app_spec.js
@@ -52,6 +52,12 @@ import { getSortKey, getSortOptions } from '~/issues/list/utils';
import axios from '~/lib/utils/axios_utils';
import { scrollUp } from '~/lib/utils/scroll_utils';
import { joinPaths } from '~/lib/utils/url_utility';
+import {
+ WORK_ITEM_TYPE_ENUM_INCIDENT,
+ WORK_ITEM_TYPE_ENUM_ISSUE,
+ WORK_ITEM_TYPE_ENUM_TASK,
+ WORK_ITEM_TYPE_ENUM_TEST_CASE,
+} from '~/work_items/constants';
jest.mock('@sentry/browser');
jest.mock('~/flash');
@@ -123,6 +129,7 @@ describe('CE IssuesListApp component', () => {
const mountComponent = ({
provide = {},
data = {},
+ workItems = false,
issuesQueryResponse = mockIssuesQueryResponse,
issuesCountsQueryResponse = mockIssuesCountsQueryResponse,
sortPreferenceMutationResponse = jest.fn().mockResolvedValue(setSortPreferenceMutationResponse),
@@ -141,6 +148,9 @@ describe('CE IssuesListApp component', () => {
apolloProvider: createMockApollo(requestHandlers),
router,
provide: {
+ glFeatures: {
+ workItems,
+ },
...defaultProvide,
...provide,
},
@@ -168,22 +178,6 @@ describe('CE IssuesListApp component', () => {
return waitForPromises();
});
- it('queries list with types `ISSUE` and `INCIDENT', () => {
- const expectedTypes = ['ISSUE', 'INCIDENT', 'TEST_CASE'];
-
- expect(mockIssuesQueryResponse).toHaveBeenCalledWith(
- expect.objectContaining({
- types: expectedTypes,
- }),
- );
-
- expect(mockIssuesCountsQueryResponse).toHaveBeenCalledWith(
- expect.objectContaining({
- types: expectedTypes,
- }),
- );
- });
-
it('renders', () => {
expect(findIssuableList().props()).toMatchObject({
namespace: defaultProvide.fullPath,
@@ -1024,6 +1018,21 @@ describe('CE IssuesListApp component', () => {
});
});
});
+
+ describe('when "page-size-change" event is emitted by IssuableList', () => {
+ it('updates url params with new page size', async () => {
+ wrapper = mountComponent();
+ router.push = jest.fn();
+
+ findIssuableList().vm.$emit('page-size-change', 50);
+ await nextTick();
+
+ expect(router.push).toHaveBeenCalledTimes(1);
+ expect(router.push).toHaveBeenCalledWith({
+ query: expect.objectContaining({ first_page_size: 50 }),
+ });
+ });
+ });
});
describe('public visibility', () => {
@@ -1045,17 +1054,45 @@ describe('CE IssuesListApp component', () => {
});
});
- describe('when "page-size-change" event is emitted by IssuableList', () => {
- it('updates url params with new page size', async () => {
- wrapper = mountComponent();
- router.push = jest.fn();
+ describe('fetching issues', () => {
+ describe('when work_items feature flag is disabled', () => {
+ beforeEach(() => {
+ wrapper = mountComponent({ workItems: false });
+ jest.runOnlyPendingTimers();
+ });
- findIssuableList().vm.$emit('page-size-change', 50);
- await nextTick();
+ it('fetches issue, incident, and test case types', () => {
+ const types = [
+ WORK_ITEM_TYPE_ENUM_ISSUE,
+ WORK_ITEM_TYPE_ENUM_INCIDENT,
+ WORK_ITEM_TYPE_ENUM_TEST_CASE,
+ ];
- expect(router.push).toHaveBeenCalledTimes(1);
- expect(router.push).toHaveBeenCalledWith({
- query: expect.objectContaining({ first_page_size: 50 }),
+ expect(mockIssuesQueryResponse).toHaveBeenCalledWith(expect.objectContaining({ types }));
+ expect(mockIssuesCountsQueryResponse).toHaveBeenCalledWith(
+ expect.objectContaining({ types }),
+ );
+ });
+ });
+
+ describe('when work_items feature flag is enabled', () => {
+ beforeEach(() => {
+ wrapper = mountComponent({ workItems: true });
+ jest.runOnlyPendingTimers();
+ });
+
+ it('fetches issue, incident, test case, and task types', () => {
+ const types = [
+ WORK_ITEM_TYPE_ENUM_ISSUE,
+ WORK_ITEM_TYPE_ENUM_INCIDENT,
+ WORK_ITEM_TYPE_ENUM_TEST_CASE,
+ WORK_ITEM_TYPE_ENUM_TASK,
+ ];
+
+ expect(mockIssuesQueryResponse).toHaveBeenCalledWith(expect.objectContaining({ types }));
+ expect(mockIssuesCountsQueryResponse).toHaveBeenCalledWith(
+ expect.objectContaining({ types }),
+ );
});
});
});
diff --git a/spec/frontend/issues/list/mock_data.js b/spec/frontend/issues/list/mock_data.js
index 4347c580a4d..42e9d348b16 100644
--- a/spec/frontend/issues/list/mock_data.js
+++ b/spec/frontend/issues/list/mock_data.js
@@ -37,6 +37,7 @@ export const getIssuesQueryResponse = {
userDiscussionsCount: 4,
webPath: 'project/-/issues/789',
webUrl: 'project/-/issues/789',
+ type: 'issue',
assignees: {
nodes: [
{
diff --git a/spec/frontend/issues/show/components/app_spec.js b/spec/frontend/issues/show/components/app_spec.js
index 27604b8ccf3..12f9707da04 100644
--- a/spec/frontend/issues/show/components/app_spec.js
+++ b/spec/frontend/issues/show/components/app_spec.js
@@ -119,7 +119,7 @@ describe('Issuable output', () => {
expect(findEdited().exists()).toBe(true);
expect(findEdited().props('updatedByPath')).toMatch(/\/some_user$/);
- expect(findEdited().props('updatedAt')).toBeTruthy();
+ expect(findEdited().props('updatedAt')).toBe(initialRequest.updated_at);
expect(wrapper.vm.state.lock_version).toBe(initialRequest.lock_version);
})
.then(() => {
@@ -133,7 +133,7 @@ describe('Issuable output', () => {
expect(findEdited().exists()).toBe(true);
expect(findEdited().props('updatedByName')).toBe('Other User');
expect(findEdited().props('updatedByPath')).toMatch(/\/other_user$/);
- expect(findEdited().props('updatedAt')).toBeTruthy();
+ expect(findEdited().props('updatedAt')).toBe(secondRequest.updated_at);
});
});
diff --git a/spec/frontend/issues/show/components/description_spec.js b/spec/frontend/issues/show/components/description_spec.js
index 8ee57f97754..bdb1448148e 100644
--- a/spec/frontend/issues/show/components/description_spec.js
+++ b/spec/frontend/issues/show/components/description_spec.js
@@ -249,7 +249,7 @@ describe('Description component', () => {
await nextTick();
expect(document.querySelector('.issuable-meta #task_status_short').textContent.trim()).toBe(
- '1/1 task',
+ '1/1 checklist item',
);
});
@@ -266,7 +266,7 @@ describe('Description component', () => {
});
});
- describe('with work items feature flag is enabled', () => {
+ describe('with work_items_create_from_markdown feature flag enabled', () => {
describe('empty description', () => {
beforeEach(() => {
createComponent({
@@ -275,7 +275,7 @@ describe('Description component', () => {
},
provide: {
glFeatures: {
- workItems: true,
+ workItemsCreateFromMarkdown: true,
},
},
});
@@ -295,7 +295,7 @@ describe('Description component', () => {
},
provide: {
glFeatures: {
- workItems: true,
+ workItemsCreateFromMarkdown: true,
},
},
});
@@ -344,7 +344,7 @@ describe('Description component', () => {
descriptionHtml: descriptionHtmlWithTask,
},
provide: {
- glFeatures: { workItems: true },
+ glFeatures: { workItemsCreateFromMarkdown: true },
},
});
return nextTick();
@@ -406,7 +406,7 @@ describe('Description component', () => {
createComponent({
props: { descriptionHtml: descriptionHtmlWithTask },
- provide: { glFeatures: { workItems: true } },
+ provide: { glFeatures: { workItemsCreateFromMarkdown: true } },
});
expect(showDetailsModal).toHaveBeenCalledTimes(modalOpened);
@@ -422,7 +422,7 @@ describe('Description component', () => {
descriptionHtml: descriptionHtmlWithTask,
},
provide: {
- glFeatures: { workItems: true },
+ glFeatures: { workItemsCreateFromMarkdown: true },
},
});
return nextTick();
diff --git a/spec/frontend/issues/show/components/edit_actions_spec.js b/spec/frontend/issues/show/components/edit_actions_spec.js
index 79368023d76..d58bf1be812 100644
--- a/spec/frontend/issues/show/components/edit_actions_spec.js
+++ b/spec/frontend/issues/show/components/edit_actions_spec.js
@@ -75,7 +75,7 @@ describe('Edit Actions component', () => {
it('renders all buttons as enabled', () => {
const buttons = findEditButtons().wrappers;
buttons.forEach((button) => {
- expect(button.attributes('disabled')).toBeFalsy();
+ expect(button.attributes('disabled')).toBeUndefined();
});
});
diff --git a/spec/frontend/issues/show/components/incidents/create_timeline_events_form_spec.js b/spec/frontend/issues/show/components/incidents/create_timeline_events_form_spec.js
new file mode 100644
index 00000000000..3ab2bb3460b
--- /dev/null
+++ b/spec/frontend/issues/show/components/incidents/create_timeline_events_form_spec.js
@@ -0,0 +1,189 @@
+import VueApollo from 'vue-apollo';
+import Vue from 'vue';
+import { GlDatepicker } from '@gitlab/ui';
+import { __, s__ } from '~/locale';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import CreateTimelineEvent from '~/issues/show/components/incidents/create_timeline_event.vue';
+import TimelineEventsForm from '~/issues/show/components/incidents/timeline_events_form.vue';
+import createTimelineEventMutation from '~/issues/show/components/incidents/graphql/queries/create_timeline_event.mutation.graphql';
+import getTimelineEvents from '~/issues/show/components/incidents/graphql/queries/get_timeline_events.query.graphql';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import { createAlert } from '~/flash';
+import { useFakeDate } from 'helpers/fake_date';
+import {
+ timelineEventsCreateEventResponse,
+ timelineEventsCreateEventError,
+ mockGetTimelineData,
+} from './mock_data';
+
+Vue.use(VueApollo);
+
+jest.mock('~/flash');
+
+const fakeDate = '2020-07-08T00:00:00.000Z';
+
+const mockInputData = {
+ incidentId: 'gid://gitlab/Issue/1',
+ note: 'test',
+ occurredAt: '2020-07-08T00:00:00.000Z',
+};
+
+describe('Create Timeline events', () => {
+ useFakeDate(fakeDate);
+ let wrapper;
+ let responseSpy;
+ let apolloProvider;
+
+ const findSubmitButton = () => wrapper.findByText(__('Save'));
+ const findSubmitAndAddButton = () =>
+ wrapper.findByText(s__('Incident|Save and add another event'));
+ const findCancelButton = () => wrapper.findByText(__('Cancel'));
+ const findDatePicker = () => wrapper.findComponent(GlDatepicker);
+ const findNoteInput = () => wrapper.findByTestId('input-note');
+ const setNoteInput = () => {
+ const textarea = findNoteInput().element;
+ textarea.value = mockInputData.note;
+ textarea.dispatchEvent(new Event('input'));
+ };
+ const findHourInput = () => wrapper.findByTestId('input-hours');
+ const findMinuteInput = () => wrapper.findByTestId('input-minutes');
+ const setDatetime = () => {
+ const inputDate = new Date(mockInputData.occurredAt);
+ findDatePicker().vm.$emit('input', inputDate);
+ findHourInput().vm.$emit('input', inputDate.getHours());
+ findMinuteInput().vm.$emit('input', inputDate.getMinutes());
+ };
+ const fillForm = () => {
+ setDatetime();
+ setNoteInput();
+ };
+
+ function createMockApolloProvider() {
+ const requestHandlers = [[createTimelineEventMutation, responseSpy]];
+ const mockApollo = createMockApollo(requestHandlers);
+
+ mockApollo.clients.defaultClient.cache.writeQuery({
+ query: getTimelineEvents,
+ data: mockGetTimelineData,
+ variables: {
+ fullPath: 'group/project',
+ incidentId: 'gid://gitlab/Issue/1',
+ },
+ });
+
+ return mockApollo;
+ }
+
+ const mountComponent = () => {
+ wrapper = mountExtended(CreateTimelineEvent, {
+ propsData: {
+ hasTimelineEvents: true,
+ },
+ provide: {
+ fullPath: 'group/project',
+ issuableId: '1',
+ },
+ apolloProvider,
+ });
+ };
+
+ beforeEach(() => {
+ responseSpy = jest.fn().mockResolvedValue(timelineEventsCreateEventResponse);
+ apolloProvider = createMockApolloProvider();
+ });
+
+ afterEach(() => {
+ createAlert.mockReset();
+ wrapper.destroy();
+ });
+
+ describe('createIncidentTimelineEvent', () => {
+ const closeFormEvent = { 'hide-new-timeline-events-form': [[]] };
+
+ const expectedData = {
+ input: mockInputData,
+ };
+
+ beforeEach(() => {
+ mountComponent();
+ fillForm();
+ });
+
+ describe('on submit', () => {
+ beforeEach(async () => {
+ findSubmitButton().trigger('click');
+ await waitForPromises();
+ });
+
+ it('should call the mutation with the right variables', () => {
+ expect(responseSpy).toHaveBeenCalledWith(expectedData);
+ });
+
+ it('should close the form on successful addition', () => {
+ expect(wrapper.emitted()).toEqual(closeFormEvent);
+ });
+ });
+
+ describe('on submit and add', () => {
+ beforeEach(async () => {
+ findSubmitAndAddButton().trigger('click');
+ await waitForPromises();
+ });
+
+ it('should keep the form open for save and add another', () => {
+ expect(wrapper.emitted()).toEqual({});
+ });
+ });
+
+ describe('on cancel', () => {
+ beforeEach(async () => {
+ findCancelButton().trigger('click');
+ await waitForPromises();
+ });
+
+ it('should close the form', () => {
+ expect(wrapper.emitted()).toEqual(closeFormEvent);
+ });
+ });
+ });
+
+ describe('error handling', () => {
+ it('should show an error when submission returns an error', async () => {
+ const expectedAlertArgs = {
+ message: `Error creating incident timeline event: ${timelineEventsCreateEventError.data.timelineEventCreate.errors[0]}`,
+ };
+ responseSpy.mockResolvedValueOnce(timelineEventsCreateEventError);
+ mountComponent();
+
+ findSubmitButton().trigger('click');
+ await waitForPromises();
+
+ expect(createAlert).toHaveBeenCalledWith(expectedAlertArgs);
+ });
+
+ it('should show an error when submission fails', async () => {
+ const expectedAlertArgs = {
+ captureError: true,
+ error: new Error(),
+ message: 'Something went wrong while creating the incident timeline event.',
+ };
+ responseSpy.mockRejectedValueOnce();
+ mountComponent();
+
+ findSubmitButton().trigger('click');
+ await waitForPromises();
+
+ expect(createAlert).toHaveBeenCalledWith(expectedAlertArgs);
+ });
+
+ it('should keep the form open on failed addition', async () => {
+ responseSpy.mockResolvedValueOnce(timelineEventsCreateEventError);
+ mountComponent();
+
+ await wrapper.findComponent(TimelineEventsForm).vm.$emit('save-event', mockInputData);
+ await waitForPromises;
+ expect(wrapper.emitted()).toEqual({});
+ });
+ });
+});
diff --git a/spec/frontend/issues/show/components/incidents/mock_data.js b/spec/frontend/issues/show/components/incidents/mock_data.js
index afc6099caf4..75c0a7350ae 100644
--- a/spec/frontend/issues/show/components/incidents/mock_data.js
+++ b/spec/frontend/issues/show/components/incidents/mock_data.js
@@ -72,10 +72,14 @@ export const timelineEventsQueryEmptyResponse = {
};
export const timelineEventsCreateEventResponse = {
- timelineEvent: {
- ...mockEvents[0],
+ data: {
+ timelineEventCreate: {
+ timelineEvent: {
+ ...mockEvents[0],
+ },
+ errors: [],
+ },
},
- errors: [],
};
export const timelineEventsCreateEventError = {
@@ -103,3 +107,21 @@ const timelineEventDeleteData = (errors = []) => {
export const timelineEventsDeleteEventResponse = timelineEventDeleteData();
export const timelineEventsDeleteEventError = timelineEventDeleteData(['Item does not exist']);
+
+export const mockGetTimelineData = {
+ project: {
+ id: 'gid://gitlab/Project/19',
+ incidentManagementTimelineEvents: {
+ nodes: [
+ {
+ id: 'gid://gitlab/IncidentManagement::TimelineEvent/8',
+ note: 'another one2',
+ noteHtml: '<p>another one2</p>',
+ action: 'comment',
+ occurredAt: '2022-07-01T12:47:00Z',
+ createdAt: '2022-07-20T12:47:40Z',
+ },
+ ],
+ },
+ },
+};
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 620cdfc53b0..cd2cbb63246 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
@@ -3,49 +3,33 @@ import Vue, { nextTick } from 'vue';
import { GlDatepicker } from '@gitlab/ui';
import { shallowMountExtended, mountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import IncidentTimelineEventForm from '~/issues/show/components/incidents/timeline_events_form.vue';
-import createTimelineEventMutation from '~/issues/show/components/incidents/graphql/queries/create_timeline_event.mutation.graphql';
-import createMockApollo from 'helpers/mock_apollo_helper';
+import TimelineEventsForm from '~/issues/show/components/incidents/timeline_events_form.vue';
import { createAlert } from '~/flash';
import { useFakeDate } from 'helpers/fake_date';
-import { timelineEventsCreateEventResponse, timelineEventsCreateEventError } from './mock_data';
Vue.use(VueApollo);
jest.mock('~/flash');
-const addEventResponse = jest.fn().mockResolvedValue(timelineEventsCreateEventResponse);
-
-function createMockApolloProvider(response = addEventResponse) {
- const requestHandlers = [[createTimelineEventMutation, response]];
- return createMockApollo(requestHandlers);
-}
+const fakeDate = '2020-07-08T00:00:00.000Z';
describe('Timeline events form', () => {
// July 8 2020
- useFakeDate(2020, 6, 8);
+ useFakeDate(fakeDate);
let wrapper;
- const mountComponent = ({ mockApollo, mountMethod = shallowMountExtended, stubs }) => {
- wrapper = mountMethod(IncidentTimelineEventForm, {
+ const mountComponent = ({ mountMethod = shallowMountExtended }) => {
+ wrapper = mountMethod(TimelineEventsForm, {
propsData: {
hasTimelineEvents: true,
+ isEventProcessed: false,
},
- provide: {
- fullPath: 'group/project',
- issuableId: '1',
- },
- apolloProvider: mockApollo,
- stubs,
});
};
afterEach(() => {
- addEventResponse.mockReset();
createAlert.mockReset();
- if (wrapper) {
- wrapper.destroy();
- }
+ wrapper.destroy();
});
const findSubmitButton = () => wrapper.findByText('Save');
@@ -75,24 +59,28 @@ describe('Timeline events form', () => {
};
describe('form button behaviour', () => {
- const closeFormEvent = { 'hide-incident-timeline-event-form': [[]] };
beforeEach(() => {
- mountComponent({ mockApollo: createMockApolloProvider(), mountMethod: mountExtended });
+ mountComponent({ mountMethod: mountExtended });
});
- it('should close the form on submit', async () => {
+ it('should save event on submit', async () => {
await submitForm();
- expect(wrapper.emitted()).toEqual(closeFormEvent);
+
+ expect(wrapper.emitted()).toEqual({
+ 'save-event': [[{ note: '', occurredAt: fakeDate }, false]],
+ });
});
- it('should not close the form on "submit and add another"', async () => {
+ it('should save event on "submit and add another"', async () => {
await submitFormAndAddAnother();
- expect(wrapper.emitted()).toEqual({});
+ expect(wrapper.emitted()).toEqual({
+ 'save-event': [[{ note: '', occurredAt: fakeDate }, true]],
+ });
});
- it('should close the form on cancel', async () => {
+ it('should emit cancel on cancel', async () => {
await cancelForm();
- expect(wrapper.emitted()).toEqual(closeFormEvent);
+ expect(wrapper.emitted()).toEqual({ cancel: [[]] });
});
it('should clear the form', async () => {
@@ -111,71 +99,4 @@ describe('Timeline events form', () => {
expect(findMinuteInput().element.value).toBe('0');
});
});
-
- describe('addTimelineEventQuery', () => {
- const expectedData = {
- input: {
- incidentId: 'gid://gitlab/Issue/1',
- note: '',
- occurredAt: '2020-07-08T00:00:00.000Z',
- },
- };
-
- let mockApollo;
-
- beforeEach(() => {
- mockApollo = createMockApolloProvider();
- mountComponent({ mockApollo, mountMethod: mountExtended });
- });
-
- it('should call the mutation with the right variables', async () => {
- await submitForm();
-
- expect(addEventResponse).toHaveBeenCalledWith(expectedData);
- });
-
- it('should call the mutation with user selected variables', async () => {
- const expectedUserSelectedData = {
- input: {
- ...expectedData.input,
- occurredAt: '2021-08-12T05:45:00.000Z',
- },
- };
-
- setDatetime();
-
- await nextTick();
- await submitForm();
-
- expect(addEventResponse).toHaveBeenCalledWith(expectedUserSelectedData);
- });
- });
-
- describe('error handling', () => {
- it('should show an error when submission returns an error', async () => {
- const expectedAlertArgs = {
- message: 'Error creating incident timeline event: Create error',
- };
- addEventResponse.mockResolvedValueOnce(timelineEventsCreateEventError);
- mountComponent({ mockApollo: createMockApolloProvider(), mountMethod: mountExtended });
-
- await submitForm();
-
- expect(createAlert).toHaveBeenCalledWith(expectedAlertArgs);
- });
-
- it('should show an error when submission fails', async () => {
- const expectedAlertArgs = {
- captureError: true,
- error: new Error(),
- message: 'Something went wrong while creating the incident timeline event.',
- };
- addEventResponse.mockRejectedValueOnce();
- mountComponent({ mockApollo: createMockApolloProvider(), mountMethod: mountExtended });
-
- await submitForm();
-
- expect(createAlert).toHaveBeenCalledWith(expectedAlertArgs);
- });
- });
});
diff --git a/spec/frontend/issues/show/components/incidents/timeline_events_list_item_spec.js b/spec/frontend/issues/show/components/incidents/timeline_events_item_spec.js
index e686f2eb4ec..90e55003ab3 100644
--- a/spec/frontend/issues/show/components/incidents/timeline_events_list_item_spec.js
+++ b/spec/frontend/issues/show/components/incidents/timeline_events_item_spec.js
@@ -2,7 +2,7 @@ import timezoneMock from 'timezone-mock';
import { GlIcon, GlDropdown } from '@gitlab/ui';
import { nextTick } from 'vue';
import { mountExtended } from 'helpers/vue_test_utils_helper';
-import IncidentTimelineEventListItem from '~/issues/show/components/incidents/timeline_events_list_item.vue';
+import IncidentTimelineEventItem from '~/issues/show/components/incidents/timeline_events_item.vue';
import { mockEvents } from './mock_data';
describe('IncidentTimelineEventList', () => {
@@ -10,7 +10,7 @@ describe('IncidentTimelineEventList', () => {
const mountComponent = ({ propsData, provide } = {}) => {
const { action, noteHtml, occurredAt } = mockEvents[0];
- wrapper = mountExtended(IncidentTimelineEventListItem, {
+ wrapper = mountExtended(IncidentTimelineEventItem, {
propsData: {
action,
noteHtml,
diff --git a/spec/frontend/issues/show/components/incidents/timeline_events_list_spec.js b/spec/frontend/issues/show/components/incidents/timeline_events_list_spec.js
index ae07237cf7d..4d2d53c990e 100644
--- a/spec/frontend/issues/show/components/incidents/timeline_events_list_spec.js
+++ b/spec/frontend/issues/show/components/incidents/timeline_events_list_spec.js
@@ -3,7 +3,7 @@ import VueApollo from 'vue-apollo';
import Vue from 'vue';
import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
import IncidentTimelineEventList from '~/issues/show/components/incidents/timeline_events_list.vue';
-import IncidentTimelineEventListItem from '~/issues/show/components/incidents/timeline_events_list_item.vue';
+import IncidentTimelineEventListItem from '~/issues/show/components/incidents/timeline_events_item.vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import deleteTimelineEventMutation from '~/issues/show/components/incidents/graphql/queries/delete_timeline_event.mutation.graphql';
diff --git a/spec/frontend/issues/show/components/incidents/timeline_events_tab_spec.js b/spec/frontend/issues/show/components/incidents/timeline_events_tab_spec.js
index 2d87851a761..2cdb971395d 100644
--- a/spec/frontend/issues/show/components/incidents/timeline_events_tab_spec.js
+++ b/spec/frontend/issues/show/components/incidents/timeline_events_tab_spec.js
@@ -5,7 +5,7 @@ import { shallowMountExtended, mountExtended } from 'helpers/vue_test_utils_help
import waitForPromises from 'helpers/wait_for_promises';
import TimelineEventsTab from '~/issues/show/components/incidents/timeline_events_tab.vue';
import IncidentTimelineEventsList from '~/issues/show/components/incidents/timeline_events_list.vue';
-import IncidentTimelineEventForm from '~/issues/show/components/incidents/timeline_events_form.vue';
+import CreateTimelineEvent from '~/issues/show/components/incidents/create_timeline_event.vue';
import timelineEventsQuery from '~/issues/show/components/incidents/graphql/queries/get_timeline_events.query.graphql';
import createMockApollo from 'helpers/mock_apollo_helper';
import { createAlert } from '~/flash';
@@ -53,7 +53,7 @@ describe('TimelineEventsTab', () => {
const findLoadingSpinner = () => wrapper.findComponent(GlLoadingIcon);
const findEmptyState = () => wrapper.findComponent(GlEmptyState);
const findTimelineEventsList = () => wrapper.findComponent(IncidentTimelineEventsList);
- const findTimelineEventForm = () => wrapper.findComponent(IncidentTimelineEventForm);
+ const findCreateTimelineEvent = () => wrapper.findComponent(CreateTimelineEvent);
const findAddEventButton = () => wrapper.findByText(timelineTabI18n.addEventButton);
describe('Timeline events tab', () => {
@@ -143,18 +143,18 @@ describe('TimelineEventsTab', () => {
});
it('should not show a form by default', () => {
- expect(findTimelineEventForm().isVisible()).toBe(false);
+ expect(findCreateTimelineEvent().isVisible()).toBe(false);
});
it('should show a form when button is clicked', async () => {
await findAddEventButton().trigger('click');
- expect(findTimelineEventForm().isVisible()).toBe(true);
+ expect(findCreateTimelineEvent().isVisible()).toBe(true);
});
it('should clear the form when button is clicked', async () => {
const mockClear = jest.fn();
- wrapper.vm.$refs.eventForm.clear = mockClear;
+ wrapper.vm.$refs.createEventForm.clearForm = mockClear;
await findAddEventButton().trigger('click');
@@ -165,9 +165,9 @@ describe('TimelineEventsTab', () => {
// open the form
await findAddEventButton().trigger('click');
- await findTimelineEventForm().vm.$emit('hide-incident-timeline-event-form');
+ await findCreateTimelineEvent().vm.$emit('hide-new-timeline-events-form');
- expect(findTimelineEventForm().isVisible()).toBe(false);
+ expect(findCreateTimelineEvent().isVisible()).toBe(false);
});
});
});
diff --git a/spec/frontend/issues/show/components/incidents/utils_spec.js b/spec/frontend/issues/show/components/incidents/utils_spec.js
index 0da0114c654..d3a86680f14 100644
--- a/spec/frontend/issues/show/components/incidents/utils_spec.js
+++ b/spec/frontend/issues/show/components/incidents/utils_spec.js
@@ -24,7 +24,7 @@ describe('incident utils', () => {
describe('get event icon', () => {
it('should display a matching event icon name', () => {
- ['comment', 'issues', 'status'].forEach((name) => {
+ ['comment', 'issues', 'label', 'status'].forEach((name) => {
expect(getEventIcon(name)).toBe(name);
});
});
diff --git a/spec/frontend/jira_connect/branches/components/project_dropdown_spec.js b/spec/frontend/jira_connect/branches/components/project_dropdown_spec.js
index 136a5967ee4..b0218a9df12 100644
--- a/spec/frontend/jira_connect/branches/components/project_dropdown_spec.js
+++ b/spec/frontend/jira_connect/branches/components/project_dropdown_spec.js
@@ -148,7 +148,7 @@ describe('ProjectDropdown', () => {
});
it('emits `error` event', () => {
- expect(wrapper.emitted('error')).toBeTruthy();
+ expect(wrapper.emitted('error')).toHaveLength(1);
});
});
diff --git a/spec/frontend/jira_connect/subscriptions/components/add_namespace_button_spec.js b/spec/frontend/jira_connect/subscriptions/components/add_namespace_button_spec.js
index 5ec1b7b7932..9f92ad2adc1 100644
--- a/spec/frontend/jira_connect/subscriptions/components/add_namespace_button_spec.js
+++ b/spec/frontend/jira_connect/subscriptions/components/add_namespace_button_spec.js
@@ -38,7 +38,6 @@ describe('AddNamespaceButton', () => {
it('button is bound to the modal', () => {
const { value } = getBinding(findButton().element, 'gl-modal');
- expect(value).toBeTruthy();
expect(value).toBe(ADD_NAMESPACE_MODAL_ID);
});
});
diff --git a/spec/frontend/jira_connect/subscriptions/components/sign_in_oauth_button_spec.js b/spec/frontend/jira_connect/subscriptions/components/sign_in_oauth_button_spec.js
index 8f79c74368f..ed0abaaf576 100644
--- a/spec/frontend/jira_connect/subscriptions/components/sign_in_oauth_button_spec.js
+++ b/spec/frontend/jira_connect/subscriptions/components/sign_in_oauth_button_spec.js
@@ -128,7 +128,7 @@ describe('SignInOauthButton', () => {
});
it('does not emit `sign-in` event', () => {
- expect(wrapper.emitted('sign-in')).toBeFalsy();
+ expect(wrapper.emitted('sign-in')).toBeUndefined();
});
it('sets `loading` prop of button to `false`', () => {
@@ -179,7 +179,7 @@ describe('SignInOauthButton', () => {
});
it('emits `sign-in` event with user data', () => {
- expect(wrapper.emitted('sign-in')[0]).toBeTruthy();
+ expect(wrapper.emitted('sign-in')).toHaveLength(1);
});
});
@@ -200,7 +200,7 @@ describe('SignInOauthButton', () => {
});
it('does not emit `sign-in` event', () => {
- expect(wrapper.emitted('sign-in')).toBeFalsy();
+ expect(wrapper.emitted('sign-in')).toBeUndefined();
});
it('sets `loading` prop of button to `false`', () => {
diff --git a/spec/frontend/jira_connect/subscriptions/pages/sign_in/sign_in_page_spec.js b/spec/frontend/jira_connect/subscriptions/pages/sign_in/sign_in_page_spec.js
index 65b08fba592..c12a45b2f41 100644
--- a/spec/frontend/jira_connect/subscriptions/pages/sign_in/sign_in_page_spec.js
+++ b/spec/frontend/jira_connect/subscriptions/pages/sign_in/sign_in_page_spec.js
@@ -68,7 +68,7 @@ describe('SignInPage', () => {
describe('when error event is emitted', () => {
it('emits another error event', () => {
findSignInGitlabCom().vm.$emit('error');
- expect(wrapper.emitted('error')[0]).toBeTruthy();
+ expect(wrapper.emitted('error')).toHaveLength(1);
});
});
diff --git a/spec/frontend/jobs/components/job_log_controllers_spec.js b/spec/frontend/jobs/components/job_log_controllers_spec.js
index cc97d111c06..aa85253a177 100644
--- a/spec/frontend/jobs/components/job_log_controllers_spec.js
+++ b/spec/frontend/jobs/components/job_log_controllers_spec.js
@@ -1,8 +1,9 @@
import { GlSearchBoxByClick } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
-import { nextTick } from 'vue';
import JobLogControllers from '~/jobs/components/job_log_controllers.vue';
import HelpPopover from '~/vue_shared/components/help_popover.vue';
+import { backoffMockImplementation } from 'helpers/backoff_helper';
+import * as commonUtils from '~/lib/utils/common_utils';
import { mockJobLog } from '../mock_data';
const mockToastShow = jest.fn();
@@ -10,10 +11,15 @@ const mockToastShow = jest.fn();
describe('Job log controllers', () => {
let wrapper;
+ beforeEach(() => {
+ jest.spyOn(commonUtils, 'backOff').mockImplementation(backoffMockImplementation);
+ });
+
afterEach(() => {
if (wrapper?.destroy) {
wrapper.destroy();
}
+ commonUtils.backOff.mockReset();
});
const defaultProps = {
@@ -24,10 +30,11 @@ describe('Job log controllers', () => {
isScrollBottomDisabled: false,
isScrollingDown: true,
isJobLogSizeVisible: true,
+ isComplete: true,
jobLog: mockJobLog,
};
- const createWrapper = (props, jobLogSearch = false) => {
+ const createWrapper = (props, { jobLogJumpToFailures = false } = {}) => {
wrapper = mount(JobLogControllers, {
propsData: {
...defaultProps,
@@ -35,7 +42,7 @@ describe('Job log controllers', () => {
},
provide: {
glFeatures: {
- jobLogSearch,
+ jobLogJumpToFailures,
},
},
data() {
@@ -58,6 +65,7 @@ describe('Job log controllers', () => {
const findScrollBottom = () => wrapper.find('[data-testid="job-controller-scroll-bottom"]');
const findJobLogSearch = () => wrapper.findComponent(GlSearchBoxByClick);
const findSearchHelp = () => wrapper.findComponent(HelpPopover);
+ const findScrollFailure = () => wrapper.find('[data-testid="job-controller-scroll-to-failure"]');
describe('Truncate information', () => {
describe('with isJobLogSizeVisible', () => {
@@ -109,9 +117,7 @@ describe('Job log controllers', () => {
});
it('emits scrollJobLogTop event on click', async () => {
- findScrollTop().trigger('click');
-
- await nextTick();
+ await findScrollTop().trigger('click');
expect(wrapper.emitted().scrollJobLogTop).toHaveLength(1);
});
@@ -131,9 +137,7 @@ describe('Job log controllers', () => {
});
it('does not emit scrollJobLogTop event on click', async () => {
- findScrollTop().trigger('click');
-
- await nextTick();
+ await findScrollTop().trigger('click');
expect(wrapper.emitted().scrollJobLogTop).toBeUndefined();
});
@@ -147,9 +151,7 @@ describe('Job log controllers', () => {
});
it('emits scrollJobLogBottom event on click', async () => {
- findScrollBottom().trigger('click');
-
- await nextTick();
+ await findScrollBottom().trigger('click');
expect(wrapper.emitted().scrollJobLogBottom).toHaveLength(1);
});
@@ -169,9 +171,7 @@ describe('Job log controllers', () => {
});
it('does not emit scrollJobLogBottom event on click', async () => {
- findScrollBottom().trigger('click');
-
- await nextTick();
+ await findScrollBottom().trigger('click');
expect(wrapper.emitted().scrollJobLogBottom).toBeUndefined();
});
@@ -201,41 +201,115 @@ describe('Job log controllers', () => {
});
});
});
- });
- describe('Job log search', () => {
- describe('with feature flag off', () => {
- it('does not display job log search', () => {
- createWrapper();
+ describe('scroll to failure button', () => {
+ describe('with feature flag disabled', () => {
+ it('does not display button', () => {
+ createWrapper();
- expect(findJobLogSearch().exists()).toBe(false);
- expect(findSearchHelp().exists()).toBe(false);
+ expect(findScrollFailure().exists()).toBe(false);
+ });
});
- });
- describe('with feature flag on', () => {
- beforeEach(() => {
- createWrapper({}, { jobLogSearch: true });
- });
+ describe('with red text failures on the page', () => {
+ let firstFailure;
+ let secondFailure;
+
+ beforeEach(() => {
+ jest.spyOn(document, 'querySelectorAll').mockReturnValueOnce(['mock-element']);
+
+ createWrapper({}, { jobLogJumpToFailures: true });
+
+ firstFailure = document.createElement('div');
+ firstFailure.className = 'term-fg-l-red';
+ document.body.appendChild(firstFailure);
+
+ secondFailure = document.createElement('div');
+ secondFailure.className = 'term-fg-l-red';
+ document.body.appendChild(secondFailure);
+ });
+
+ afterEach(() => {
+ if (firstFailure) {
+ firstFailure.remove();
+ firstFailure = null;
+ }
+
+ if (secondFailure) {
+ secondFailure.remove();
+ secondFailure = null;
+ }
+ });
+
+ it('is enabled', () => {
+ expect(findScrollFailure().props('disabled')).toBe(false);
+ });
+
+ it('scrolls to each failure', async () => {
+ jest.spyOn(firstFailure, 'scrollIntoView');
- it('displays job log search', () => {
- expect(findJobLogSearch().exists()).toBe(true);
- expect(findSearchHelp().exists()).toBe(true);
+ await findScrollFailure().trigger('click');
+
+ expect(firstFailure.scrollIntoView).toHaveBeenCalled();
+
+ await findScrollFailure().trigger('click');
+
+ expect(secondFailure.scrollIntoView).toHaveBeenCalled();
+
+ await findScrollFailure().trigger('click');
+
+ expect(firstFailure.scrollIntoView).toHaveBeenCalled();
+ });
});
- it('emits search results', () => {
- const expectedSearchResults = [[[mockJobLog[6].lines[1], mockJobLog[6].lines[2]]]];
+ describe('with no red text failures on the page', () => {
+ beforeEach(() => {
+ jest.spyOn(document, 'querySelectorAll').mockReturnValueOnce([]);
- findJobLogSearch().vm.$emit('submit');
+ createWrapper({}, { jobLogJumpToFailures: true });
+ });
- expect(wrapper.emitted('searchResults')).toEqual(expectedSearchResults);
+ it('is disabled', () => {
+ expect(findScrollFailure().props('disabled')).toBe(true);
+ });
});
- it('clears search results', () => {
- findJobLogSearch().vm.$emit('clear');
+ describe('when the job log is not complete', () => {
+ beforeEach(() => {
+ jest.spyOn(document, 'querySelectorAll').mockReturnValueOnce(['mock-element']);
+
+ createWrapper({ isComplete: false }, { jobLogJumpToFailures: true });
+ });
- expect(wrapper.emitted('searchResults')).toEqual([[[]]]);
+ it('is enabled', () => {
+ expect(findScrollFailure().props('disabled')).toBe(false);
+ });
});
});
});
+
+ describe('Job log search', () => {
+ beforeEach(() => {
+ createWrapper();
+ });
+
+ it('displays job log search', () => {
+ expect(findJobLogSearch().exists()).toBe(true);
+ expect(findSearchHelp().exists()).toBe(true);
+ });
+
+ it('emits search results', () => {
+ const expectedSearchResults = [[[mockJobLog[6].lines[1], mockJobLog[6].lines[2]]]];
+
+ findJobLogSearch().vm.$emit('submit');
+
+ expect(wrapper.emitted('searchResults')).toEqual(expectedSearchResults);
+ });
+
+ it('clears search results', () => {
+ findJobLogSearch().vm.$emit('clear');
+
+ expect(wrapper.emitted('searchResults')).toEqual([[[]]]);
+ });
+ });
});
diff --git a/spec/frontend/jobs/components/sidebar_detail_row_spec.js b/spec/frontend/jobs/components/sidebar_detail_row_spec.js
index 43f2e022dd8..8d2680608ab 100644
--- a/spec/frontend/jobs/components/sidebar_detail_row_spec.js
+++ b/spec/frontend/jobs/components/sidebar_detail_row_spec.js
@@ -7,7 +7,7 @@ describe('Sidebar detail row', () => {
const title = 'this is the title';
const value = 'this is the value';
- const helpUrl = '/help/ci/runners/index.html';
+ const helpUrl = 'https://docs.gitlab.com/runner/register/index.html';
const findHelpLink = () => wrapper.findComponent(GlLink);
diff --git a/spec/frontend/labels/labels_select_spec.js b/spec/frontend/labels/labels_select_spec.js
index f6e280564cc..63f7c725bc7 100644
--- a/spec/frontend/labels/labels_select_spec.js
+++ b/spec/frontend/labels/labels_select_spec.js
@@ -101,6 +101,12 @@ describe('LabelsSelect', () => {
expect($labelEl.find('a').attr('data-html')).toBe('true');
});
+ it('generated label item template has correct title for tooltip', () => {
+ expect($labelEl.find('a').attr('title')).toBe(
+ "<span class='font-weight-bold scoped-label-tooltip-title'>Scoped label</span><br>Foobar",
+ );
+ });
+
it('generated label item template has correct label styles and classes', () => {
expect($labelEl.find('span.gl-label-text').attr('style')).toBe(
`background-color: ${label.color};`,
diff --git a/spec/frontend/lib/dompurify_spec.js b/spec/frontend/lib/dompurify_spec.js
index b585c69e911..29b927ef628 100644
--- a/spec/frontend/lib/dompurify_spec.js
+++ b/spec/frontend/lib/dompurify_spec.js
@@ -173,4 +173,50 @@ describe('~/lib/dompurify', () => {
expect(sanitize(html)).toBe(`<a>internal link</a>`);
});
});
+
+ describe('links with target attribute', () => {
+ const getSanitizedNode = (html) => {
+ return document.createRange().createContextualFragment(sanitize(html)).firstElementChild;
+ };
+
+ it('adds secure context', () => {
+ const html = `<a href="https://example.com" target="_blank">link</a>`;
+ const el = getSanitizedNode(html);
+
+ expect(el.getAttribute('target')).toBe('_blank');
+ expect(el.getAttribute('rel')).toBe('noopener noreferrer');
+ });
+
+ it('adds secure context and merge existing `rel` values', () => {
+ const html = `<a href="https://example.com" target="_blank" rel="help External">link</a>`;
+ const el = getSanitizedNode(html);
+
+ expect(el.getAttribute('target')).toBe('_blank');
+ expect(el.getAttribute('rel')).toBe('help external noopener noreferrer');
+ });
+
+ it('does not duplicate noopener/noreferrer `rel` values', () => {
+ const html = `<a href="https://example.com" target="_blank" rel="noreferrer noopener">link</a>`;
+ const el = getSanitizedNode(html);
+
+ expect(el.getAttribute('target')).toBe('_blank');
+ expect(el.getAttribute('rel')).toBe('noreferrer noopener');
+ });
+
+ it('does not update `rel` values when target is not `_blank` ', () => {
+ const html = `<a href="https://example.com" target="_self" rel="help">internal</a>`;
+ const el = getSanitizedNode(html);
+
+ expect(el.getAttribute('target')).toBe('_self');
+ expect(el.getAttribute('rel')).toBe('help');
+ });
+
+ it('does not update `rel` values when target attribute is not present', () => {
+ const html = `<a href="https://example.com">link</a>`;
+ const el = getSanitizedNode(html);
+
+ expect(el.hasAttribute('target')).toBe(false);
+ expect(el.hasAttribute('rel')).toBe(false);
+ });
+ });
});
diff --git a/spec/frontend/lib/gfm/index_spec.js b/spec/frontend/lib/gfm/index_spec.js
index b722315d63a..f53f809b799 100644
--- a/spec/frontend/lib/gfm/index_spec.js
+++ b/spec/frontend/lib/gfm/index_spec.js
@@ -96,26 +96,164 @@ describe('gfm', () => {
);
});
});
- });
- describe('when skipping the rendering of code blocks', () => {
- it('transforms code nodes into codeblock html tags', async () => {
- const result = await markdownToAST(
- `
+ describe('when skipping the rendering of code blocks', () => {
+ it('transforms code nodes into codeblock html tags', async () => {
+ const result = await markdownToAST(
+ `
\`\`\`javascript
console.log('Hola');
\`\`\`\
`,
- ['code'],
- );
+ ['code'],
+ );
+
+ expectInRoot(
+ result,
+ expect.objectContaining({
+ tagName: 'codeblock',
+ properties: {
+ language: 'javascript',
+ },
+ }),
+ );
+ });
+ });
+
+ describe('when skipping the rendering of reference definitions', () => {
+ it('transforms code nodes into codeblock html tags', async () => {
+ const result = await markdownToAST(
+ `
+[gitlab][gitlab]
+
+[gitlab]: https://gitlab.com "GitLab"
+ `,
+ ['definition'],
+ );
+
+ expectInRoot(
+ result,
+ expect.objectContaining({
+ type: 'element',
+ tagName: 'referencedefinition',
+ properties: {
+ identifier: 'gitlab',
+ title: 'GitLab',
+ url: 'https://gitlab.com',
+ },
+ children: [
+ {
+ type: 'text',
+ value: '[gitlab]: https://gitlab.com "GitLab"',
+ },
+ ],
+ }),
+ );
+ });
+ });
+
+ describe('when skipping the rendering of link and image references', () => {
+ it('transforms linkReference and imageReference nodes into html tags', async () => {
+ const result = await markdownToAST(
+ `
+[gitlab][gitlab] and ![GitLab Logo][gitlab-logo]
+
+[gitlab]: https://gitlab.com "GitLab"
+[gitlab-logo]: https://gitlab.com/gitlab-logo.png "GitLab Logo"
+ `,
+ ['linkReference', 'imageReference'],
+ );
+
+ expectInRoot(
+ result,
+ expect.objectContaining({
+ tagName: 'p',
+ children: expect.arrayContaining([
+ expect.objectContaining({
+ type: 'element',
+ tagName: 'a',
+ properties: expect.objectContaining({
+ href: 'https://gitlab.com',
+ isReference: 'true',
+ identifier: 'gitlab',
+ title: 'GitLab',
+ }),
+ }),
+ expect.objectContaining({
+ type: 'element',
+ tagName: 'img',
+ properties: expect.objectContaining({
+ src: 'https://gitlab.com/gitlab-logo.png',
+ isReference: 'true',
+ identifier: 'gitlab-logo',
+ title: 'GitLab Logo',
+ alt: 'GitLab Logo',
+ }),
+ }),
+ ]),
+ }),
+ );
+ });
+
+ it('normalizes the urls extracted from the reference definitions', async () => {
+ const result = await markdownToAST(
+ `
+[gitlab][gitlab] and ![GitLab Logo][gitlab]
+
+[gitlab]: /url\\bar*baz
+ `,
+ ['linkReference', 'imageReference'],
+ );
+
+ expectInRoot(
+ result,
+ expect.objectContaining({
+ tagName: 'p',
+ children: expect.arrayContaining([
+ expect.objectContaining({
+ type: 'element',
+ tagName: 'a',
+ properties: expect.objectContaining({
+ href: '/url%5Cbar*baz',
+ }),
+ }),
+ expect.objectContaining({
+ type: 'element',
+ tagName: 'img',
+ properties: expect.objectContaining({
+ src: '/url%5Cbar*baz',
+ }),
+ }),
+ ]),
+ }),
+ );
+ });
+ });
+ });
+
+ describe('when skipping the rendering of frontmatter types', () => {
+ it.each`
+ type | input
+ ${'yaml'} | ${'---\ntitle: page\n---'}
+ ${'toml'} | ${'+++\ntitle: page\n+++'}
+ ${'json'} | ${';;;\ntitle: page\n;;;'}
+ `('transforms $type nodes into frontmatter html tags', async ({ input, type }) => {
+ const result = await markdownToAST(input, [type]);
expectInRoot(
result,
expect.objectContaining({
- tagName: 'codeblock',
+ type: 'element',
+ tagName: 'frontmatter',
properties: {
- language: 'javascript',
+ language: type,
},
+ children: [
+ {
+ type: 'text',
+ value: 'title: page',
+ },
+ ],
}),
);
});
diff --git a/spec/frontend/lib/utils/common_utils_spec.js b/spec/frontend/lib/utils/common_utils_spec.js
index 7cf101a5e59..a2ace8857ed 100644
--- a/spec/frontend/lib/utils/common_utils_spec.js
+++ b/spec/frontend/lib/utils/common_utils_spec.js
@@ -292,16 +292,11 @@ describe('common_utils', () => {
const spy = jest.fn();
const debouncedSpy = commonUtils.debounceByAnimationFrame(spy);
- return new Promise((resolve) => {
- window.requestAnimationFrame(() => {
- debouncedSpy();
- debouncedSpy();
- window.requestAnimationFrame(() => {
- expect(spy).toHaveBeenCalledTimes(1);
- resolve();
- });
- });
- });
+ debouncedSpy();
+ debouncedSpy();
+ jest.runOnlyPendingTimers();
+
+ expect(spy).toHaveBeenCalledTimes(1);
});
});
@@ -633,7 +628,7 @@ describe('common_utils', () => {
it('returns an empty object if `conversionFunction` parameter is not a function', () => {
const result = commonUtils.convertObjectProps(null, mockObjects.convertObjectProps.obj);
- expect(isEmptyObject(result)).toBeTruthy();
+ expect(isEmptyObject(result)).toBe(true);
});
});
@@ -650,9 +645,9 @@ describe('common_utils', () => {
: commonUtils[functionName];
it('returns an empty object if `obj` parameter is null, undefined or an empty object', () => {
- expect(isEmptyObject(testFunction(null))).toBeTruthy();
- expect(isEmptyObject(testFunction())).toBeTruthy();
- expect(isEmptyObject(testFunction({}))).toBeTruthy();
+ expect(isEmptyObject(testFunction(null))).toBe(true);
+ expect(isEmptyObject(testFunction())).toBe(true);
+ expect(isEmptyObject(testFunction({}))).toBe(true);
});
it('converts object properties', () => {
diff --git a/spec/frontend/lib/utils/confirm_via_gl_modal/confirm_modal_spec.js b/spec/frontend/lib/utils/confirm_via_gl_modal/confirm_modal_spec.js
index d6131b1a1d7..313e028d861 100644
--- a/spec/frontend/lib/utils/confirm_via_gl_modal/confirm_modal_spec.js
+++ b/spec/frontend/lib/utils/confirm_via_gl_modal/confirm_modal_spec.js
@@ -42,12 +42,12 @@ describe('Confirm Modal', () => {
it('should emit `confirmed` event on `primary` modal event', () => {
findGlModal().vm.$emit('primary');
- expect(wrapper.emitted('confirmed')).toBeTruthy();
+ expect(wrapper.emitted('confirmed')).toHaveLength(1);
});
it('should emit closed` event on `hidden` modal event', () => {
modal.vm.$emit('hidden');
- expect(wrapper.emitted('closed')).toBeTruthy();
+ expect(wrapper.emitted('closed')).toHaveLength(1);
});
});
diff --git a/spec/frontend/lib/utils/rails_ujs_spec.js b/spec/frontend/lib/utils/rails_ujs_spec.js
index c10301523c9..da9cc5c6f3c 100644
--- a/spec/frontend/lib/utils/rails_ujs_spec.js
+++ b/spec/frontend/lib/utils/rails_ujs_spec.js
@@ -18,14 +18,12 @@ function mockXHRResponse({ responseText, responseContentType } = {}) {
.mockReturnValue(responseContentType);
jest.spyOn(global.XMLHttpRequest.prototype, 'send').mockImplementation(function send() {
- requestAnimationFrame(() => {
- Object.defineProperties(this, {
- readyState: { value: XMLHttpRequest.DONE },
- status: { value: 200 },
- response: { value: responseText },
- });
- this.onreadystatechange();
+ Object.defineProperties(this, {
+ readyState: { value: XMLHttpRequest.DONE },
+ status: { value: 200 },
+ response: { value: responseText },
});
+ this.onreadystatechange();
});
}
diff --git a/spec/frontend/lib/utils/recurrence_spec.js b/spec/frontend/lib/utils/recurrence_spec.js
index fc22529dffc..8bf3ea4e25a 100644
--- a/spec/frontend/lib/utils/recurrence_spec.js
+++ b/spec/frontend/lib/utils/recurrence_spec.js
@@ -211,9 +211,10 @@ describe('recurrence', () => {
describe('eject', () => {
it('removes the handler assigned to the particular count slot', () => {
- recurInstance.handle(1, jest.fn());
+ const func = jest.fn();
+ recurInstance.handle(1, func);
- expect(recurInstance.handlers[1]).toBeTruthy();
+ expect(recurInstance.handlers[1]).toStrictEqual(func);
recurInstance.eject(1);
diff --git a/spec/frontend/lib/utils/sticky_spec.js b/spec/frontend/lib/utils/sticky_spec.js
index 01e8fe777af..ec9e746c838 100644
--- a/spec/frontend/lib/utils/sticky_spec.js
+++ b/spec/frontend/lib/utils/sticky_spec.js
@@ -34,13 +34,13 @@ describe('sticky', () => {
isSticky(el, 0, el.offsetTop);
isSticky(el, 0, el.offsetTop);
- expect(el.classList.contains('is-stuck')).toBeTruthy();
+ expect(el.classList.contains('is-stuck')).toBe(true);
});
it('adds is-stuck class', () => {
isSticky(el, 0, el.offsetTop);
- expect(el.classList.contains('is-stuck')).toBeTruthy();
+ expect(el.classList.contains('is-stuck')).toBe(true);
});
it('inserts placeholder element', () => {
@@ -64,7 +64,7 @@ describe('sticky', () => {
it('does not add is-stuck class', () => {
isSticky(el, 0, 0);
- expect(el.classList.contains('is-stuck')).toBeFalsy();
+ expect(el.classList.contains('is-stuck')).toBe(false);
});
it('removes placeholder', () => {
diff --git a/spec/frontend/lib/utils/text_markdown_spec.js b/spec/frontend/lib/utils/text_markdown_spec.js
index d1bca3c73b6..733d89fe08c 100644
--- a/spec/frontend/lib/utils/text_markdown_spec.js
+++ b/spec/frontend/lib/utils/text_markdown_spec.js
@@ -193,6 +193,7 @@ describe('init markdown', () => {
${'- [ ] item'} | ${'- [ ] item\n- [ ] '}
${'- [x] item'} | ${'- [x] item\n- [ ] '}
${'- [X] item'} | ${'- [X] item\n- [ ] '}
+ ${'- [~] item'} | ${'- [~] item\n- [ ] '}
${'- [ ] nbsp (U+00A0)'} | ${'- [ ] nbsp (U+00A0)\n- [ ] '}
${'- item\n - second'} | ${'- item\n - second\n - '}
${'- - -'} | ${'- - -'}
@@ -205,6 +206,7 @@ describe('init markdown', () => {
${'1. [ ] item'} | ${'1. [ ] item\n2. [ ] '}
${'1. [x] item'} | ${'1. [x] item\n2. [ ] '}
${'1. [X] item'} | ${'1. [X] item\n2. [ ] '}
+ ${'1. [~] item'} | ${'1. [~] item\n2. [ ] '}
${'108. item'} | ${'108. item\n109. '}
${'108. item\n - second'} | ${'108. item\n - second\n - '}
${'108. item\n 1. second'} | ${'108. item\n 1. second\n 2. '}
@@ -228,11 +230,13 @@ describe('init markdown', () => {
${'- [ ] item\n- [ ] '} | ${'- [ ] item\n'}
${'- [x] item\n- [x] '} | ${'- [x] item\n'}
${'- [X] item\n- [X] '} | ${'- [X] item\n'}
+ ${'- [~] item\n- [~] '} | ${'- [~] item\n'}
${'- item\n - second\n - '} | ${'- item\n - second\n'}
${'1. item\n2. '} | ${'1. item\n'}
${'1. [ ] item\n2. [ ] '} | ${'1. [ ] item\n'}
${'1. [x] item\n2. [x] '} | ${'1. [x] item\n'}
${'1. [X] item\n2. [X] '} | ${'1. [X] item\n'}
+ ${'1. [~] item\n2. [~] '} | ${'1. [~] item\n'}
${'108. item\n109. '} | ${'108. item\n'}
${'108. item\n - second\n - '} | ${'108. item\n - second\n'}
${'108. item\n 1. second\n 1. '} | ${'108. item\n 1. second\n'}
@@ -301,6 +305,129 @@ describe('init markdown', () => {
});
});
+ describe('shifting selected lines left or right', () => {
+ const indentEvent = new KeyboardEvent('keydown', { key: ']', metaKey: true });
+ const outdentEvent = new KeyboardEvent('keydown', { key: '[', metaKey: true });
+
+ beforeEach(() => {
+ textArea.addEventListener('keydown', keypressNoteText);
+ textArea.addEventListener('compositionstart', compositionStartNoteText);
+ textArea.addEventListener('compositionend', compositionEndNoteText);
+ });
+
+ it.each`
+ selectionStart | selectionEnd | expected | expectedSelectionStart | expectedSelectionEnd
+ ${0} | ${0} | ${' 012\n456\n89'} | ${2} | ${2}
+ ${5} | ${5} | ${'012\n 456\n89'} | ${7} | ${7}
+ ${10} | ${10} | ${'012\n456\n 89'} | ${12} | ${12}
+ ${0} | ${2} | ${' 012\n456\n89'} | ${0} | ${4}
+ ${1} | ${2} | ${' 012\n456\n89'} | ${3} | ${4}
+ ${5} | ${7} | ${'012\n 456\n89'} | ${7} | ${9}
+ ${0} | ${7} | ${' 012\n 456\n89'} | ${0} | ${11}
+ ${2} | ${9} | ${' 012\n 456\n 89'} | ${4} | ${15}
+ `(
+ 'indents the selected lines two spaces to the right',
+ ({
+ selectionStart,
+ selectionEnd,
+ expected,
+ expectedSelectionStart,
+ expectedSelectionEnd,
+ }) => {
+ const text = '012\n456\n89';
+ textArea.value = text;
+ textArea.setSelectionRange(selectionStart, selectionEnd);
+
+ textArea.dispatchEvent(indentEvent);
+
+ expect(textArea.value).toEqual(expected);
+ expect(textArea.selectionStart).toEqual(expectedSelectionStart);
+ expect(textArea.selectionEnd).toEqual(expectedSelectionEnd);
+ },
+ );
+
+ it('indents a blank line two spaces to the right', () => {
+ textArea.value = '012\n\n89';
+ textArea.setSelectionRange(4, 4);
+
+ textArea.dispatchEvent(indentEvent);
+
+ expect(textArea.value).toEqual('012\n \n89');
+ expect(textArea.selectionStart).toEqual(6);
+ expect(textArea.selectionEnd).toEqual(6);
+ });
+
+ it.each`
+ selectionStart | selectionEnd | expected | expectedSelectionStart | expectedSelectionEnd
+ ${0} | ${0} | ${'234\n 789\n 34'} | ${0} | ${0}
+ ${3} | ${3} | ${'234\n 789\n 34'} | ${1} | ${1}
+ ${7} | ${7} | ${' 234\n789\n 34'} | ${6} | ${6}
+ ${0} | ${3} | ${'234\n 789\n 34'} | ${0} | ${1}
+ ${8} | ${10} | ${' 234\n789\n 34'} | ${7} | ${9}
+ ${14} | ${15} | ${' 234\n 789\n34'} | ${12} | ${13}
+ ${0} | ${15} | ${'234\n789\n34'} | ${0} | ${10}
+ ${3} | ${13} | ${'234\n789\n34'} | ${1} | ${8}
+ ${6} | ${6} | ${' 234\n789\n 34'} | ${6} | ${6}
+ `(
+ 'outdents the selected lines two spaces to the left',
+ ({
+ selectionStart,
+ selectionEnd,
+ expected,
+ expectedSelectionStart,
+ expectedSelectionEnd,
+ }) => {
+ const text = ' 234\n 789\n 34';
+ textArea.value = text;
+ textArea.setSelectionRange(selectionStart, selectionEnd);
+
+ textArea.dispatchEvent(outdentEvent);
+
+ expect(textArea.value).toEqual(expected);
+ expect(textArea.selectionStart).toEqual(expectedSelectionStart);
+ expect(textArea.selectionEnd).toEqual(expectedSelectionEnd);
+ },
+ );
+
+ it('outdent a blank line has no effect', () => {
+ textArea.value = '012\n\n89';
+ textArea.setSelectionRange(4, 4);
+
+ textArea.dispatchEvent(outdentEvent);
+
+ expect(textArea.value).toEqual('012\n\n89');
+ expect(textArea.selectionStart).toEqual(4);
+ expect(textArea.selectionEnd).toEqual(4);
+ });
+
+ it('does not indent if meta is not set', () => {
+ const indentNoMetaEvent = new KeyboardEvent('keydown', { key: ']' });
+ const text = '012\n456\n89';
+ textArea.value = text;
+ textArea.setSelectionRange(0, 0);
+
+ textArea.dispatchEvent(indentNoMetaEvent);
+
+ expect(textArea.value).toEqual(text);
+ });
+
+ it.each`
+ keyEvent
+ ${new KeyboardEvent('keydown', { key: ']', metaKey: false })}
+ ${new KeyboardEvent('keydown', { key: ']', metaKey: true, shiftKey: true })}
+ ${new KeyboardEvent('keydown', { key: ']', metaKey: true, altKey: true })}
+ ${new KeyboardEvent('keydown', { key: ']', metaKey: true, ctrlKey: true })}
+ `('does not indent if meta is not set', ({ keyEvent }) => {
+ const text = '012\n456\n89';
+ textArea.value = text;
+ textArea.setSelectionRange(0, 0);
+
+ textArea.dispatchEvent(keyEvent);
+
+ expect(textArea.value).toEqual(text);
+ });
+ });
+
describe('with selection', () => {
let text = 'initial selected value';
let selected = 'selected';
@@ -377,6 +504,15 @@ describe('init markdown', () => {
expect(textArea.value).toEqual(text);
});
+
+ it('does nothing if meta is set', () => {
+ const event = new KeyboardEvent('keydown', { key: '[', metaKey: true });
+
+ textArea.addEventListener('keydown', keypressNoteText);
+ textArea.dispatchEvent(event);
+
+ expect(textArea.value).toEqual(text);
+ });
});
describe('and text to be selected', () => {
diff --git a/spec/frontend/lib/utils/url_utility_spec.js b/spec/frontend/lib/utils/url_utility_spec.js
index 81cf4bd293b..2c6b603197d 100644
--- a/spec/frontend/lib/utils/url_utility_spec.js
+++ b/spec/frontend/lib/utils/url_utility_spec.js
@@ -348,15 +348,13 @@ describe('URL utility', () => {
describe('urlContainsSha', () => {
it('returns true when there is a valid 40-character SHA1 hash in the URL', () => {
shas.valid.forEach((sha) => {
- expect(
- urlUtils.urlContainsSha({ url: `http://urlstuff/${sha}/moreurlstuff` }),
- ).toBeTruthy();
+ expect(urlUtils.urlContainsSha({ url: `http://urlstuff/${sha}/moreurlstuff` })).toBe(true);
});
});
it('returns false when there is not a valid 40-character SHA1 hash in the URL', () => {
shas.invalid.forEach((str) => {
- expect(urlUtils.urlContainsSha({ url: `http://urlstuff/${str}/moreurlstuff` })).toBeFalsy();
+ expect(urlUtils.urlContainsSha({ url: `http://urlstuff/${str}/moreurlstuff` })).toBe(false);
});
});
});
@@ -555,18 +553,22 @@ describe('URL utility', () => {
describe('relativePathToAbsolute', () => {
it.each`
- path | base | result
- ${'./foo'} | ${'bar/'} | ${'/bar/foo'}
- ${'../john.md'} | ${'bar/baz/foo.php'} | ${'/bar/john.md'}
- ${'../images/img.png'} | ${'bar/baz/foo.php'} | ${'/bar/images/img.png'}
- ${'../images/Image 1.png'} | ${'bar/baz/foo.php'} | ${'/bar/images/Image 1.png'}
- ${'/images/img.png'} | ${'bar/baz/foo.php'} | ${'/images/img.png'}
- ${'/images/img.png'} | ${'/bar/baz/foo.php'} | ${'/images/img.png'}
- ${'../john.md'} | ${'/bar/baz/foo.php'} | ${'/bar/john.md'}
- ${'../john.md'} | ${'///bar/baz/foo.php'} | ${'/bar/john.md'}
- ${'/images/img.png'} | ${'https://gitlab.com/user/project/'} | ${'https://gitlab.com/images/img.png'}
- ${'../images/img.png'} | ${'https://gitlab.com/user/project/'} | ${'https://gitlab.com/user/images/img.png'}
- ${'../images/Image 1.png'} | ${'https://gitlab.com/user/project/'} | ${'https://gitlab.com/user/images/Image%201.png'}
+ path | base | result
+ ${'./foo'} | ${'bar/'} | ${'/bar/foo'}
+ ${'../john.md'} | ${'bar/baz/foo.php'} | ${'/bar/john.md'}
+ ${'../images/img.png'} | ${'bar/baz/foo.php'} | ${'/bar/images/img.png'}
+ ${'../images/Image 1.png'} | ${'bar/baz/foo.php'} | ${'/bar/images/Image 1.png'}
+ ${'/images/img.png'} | ${'bar/baz/foo.php'} | ${'/images/img.png'}
+ ${'/images/img.png'} | ${'bar/baz//foo.php'} | ${'/images/img.png'}
+ ${'/images//img.png'} | ${'bar/baz/foo.php'} | ${'/images/img.png'}
+ ${'/images/img.png'} | ${'/bar/baz/foo.php'} | ${'/images/img.png'}
+ ${'../john.md'} | ${'/bar/baz/foo.php'} | ${'/bar/john.md'}
+ ${'../john.md'} | ${'///bar/baz/foo.php'} | ${'/bar/john.md'}
+ ${'/images/img.png'} | ${'https://gitlab.com/user/project/'} | ${'https://gitlab.com/images/img.png'}
+ ${'/images/img.png'} | ${'https://gitlab.com////user/project/'} | ${'https://gitlab.com/images/img.png'}
+ ${'/images////img.png'} | ${'https://gitlab.com/user/project/'} | ${'https://gitlab.com/images/img.png'}
+ ${'../images/img.png'} | ${'https://gitlab.com/user/project/'} | ${'https://gitlab.com/user/images/img.png'}
+ ${'../images/Image 1.png'} | ${'https://gitlab.com/user/project/'} | ${'https://gitlab.com/user/images/Image%201.png'}
`(
'converts relative path "$path" with base "$base" to absolute path => "expected"',
({ path, base, result }) => {
@@ -809,13 +811,13 @@ describe('URL utility', () => {
});
it('should compare against the window location if no compare value is provided', () => {
- expect(urlUtils.urlIsDifferent('different')).toBeTruthy();
- expect(urlUtils.urlIsDifferent(current)).toBeFalsy();
+ expect(urlUtils.urlIsDifferent('different')).toBe(true);
+ expect(urlUtils.urlIsDifferent(current)).toBe(false);
});
it('should use the provided compare value', () => {
- expect(urlUtils.urlIsDifferent('different', current)).toBeTruthy();
- expect(urlUtils.urlIsDifferent(current, current)).toBeFalsy();
+ expect(urlUtils.urlIsDifferent('different', current)).toBe(true);
+ expect(urlUtils.urlIsDifferent(current, current)).toBe(false);
});
});
@@ -1058,4 +1060,28 @@ describe('URL utility', () => {
expect(urlUtils.PROMO_URL).toBe(url);
});
});
+
+ describe('removeUrlProtocol', () => {
+ it.each`
+ input | output
+ ${'http://gitlab.com'} | ${'gitlab.com'}
+ ${'https://gitlab.com'} | ${'gitlab.com'}
+ ${'foo:bar.com'} | ${'bar.com'}
+ ${'gitlab.com'} | ${'gitlab.com'}
+ `('transforms $input to $output', ({ input, output }) => {
+ expect(urlUtils.removeUrlProtocol(input)).toBe(output);
+ });
+ });
+
+ describe('removeLastSlashInUrlPath', () => {
+ it.each`
+ input | output
+ ${'https://www.gitlab.com/path/'} | ${'https://www.gitlab.com/path'}
+ ${'https://www.gitlab.com/?query=search'} | ${'https://www.gitlab.com?query=search'}
+ ${'https://www.gitlab.com/#fragment'} | ${'https://www.gitlab.com#fragment'}
+ ${'https://www.gitlab.com/hello'} | ${'https://www.gitlab.com/hello'}
+ `('transforms $input to $output', ({ input, output }) => {
+ expect(urlUtils.removeLastSlashInUrlPath(input)).toBe(output);
+ });
+ });
});
diff --git a/spec/frontend/members/components/action_buttons/access_request_action_buttons_spec.js b/spec/frontend/members/components/action_buttons/access_request_action_buttons_spec.js
index f1471f625f8..3dac47974e7 100644
--- a/spec/frontend/members/components/action_buttons/access_request_action_buttons_spec.js
+++ b/spec/frontend/members/components/action_buttons/access_request_action_buttons_spec.js
@@ -17,8 +17,8 @@ describe('AccessRequestActionButtons', () => {
});
};
- const findRemoveMemberButton = () => wrapper.find(RemoveMemberButton);
- const findApproveButton = () => wrapper.find(ApproveAccessRequestButton);
+ const findRemoveMemberButton = () => wrapper.findComponent(RemoveMemberButton);
+ const findApproveButton = () => wrapper.findComponent(ApproveAccessRequestButton);
afterEach(() => {
wrapper.destroy();
diff --git a/spec/frontend/members/components/action_buttons/approve_access_request_button_spec.js b/spec/frontend/members/components/action_buttons/approve_access_request_button_spec.js
index 08d7cf3c932..15bb03480e1 100644
--- a/spec/frontend/members/components/action_buttons/approve_access_request_button_spec.js
+++ b/spec/frontend/members/components/action_buttons/approve_access_request_button_spec.js
@@ -43,8 +43,8 @@ describe('ApproveAccessRequestButton', () => {
});
};
- const findForm = () => wrapper.find(GlForm);
- const findButton = () => findForm().find(GlButton);
+ const findForm = () => wrapper.findComponent(GlForm);
+ const findButton = () => findForm().findComponent(GlButton);
beforeEach(() => {
createComponent();
diff --git a/spec/frontend/members/components/action_buttons/invite_action_buttons_spec.js b/spec/frontend/members/components/action_buttons/invite_action_buttons_spec.js
index 79252456f67..ea819b4fb83 100644
--- a/spec/frontend/members/components/action_buttons/invite_action_buttons_spec.js
+++ b/spec/frontend/members/components/action_buttons/invite_action_buttons_spec.js
@@ -16,8 +16,8 @@ describe('InviteActionButtons', () => {
});
};
- const findRemoveMemberButton = () => wrapper.find(RemoveMemberButton);
- const findResendInviteButton = () => wrapper.find(ResendInviteButton);
+ const findRemoveMemberButton = () => wrapper.findComponent(RemoveMemberButton);
+ const findResendInviteButton = () => wrapper.findComponent(ResendInviteButton);
afterEach(() => {
wrapper.destroy();
diff --git a/spec/frontend/members/components/action_buttons/leave_button_spec.js b/spec/frontend/members/components/action_buttons/leave_button_spec.js
index 4859d033464..ecfbf4460a6 100644
--- a/spec/frontend/members/components/action_buttons/leave_button_spec.js
+++ b/spec/frontend/members/components/action_buttons/leave_button_spec.js
@@ -22,7 +22,7 @@ describe('LeaveButton', () => {
});
};
- const findButton = () => wrapper.find(GlButton);
+ const findButton = () => wrapper.findComponent(GlButton);
beforeEach(() => {
createComponent();
@@ -44,7 +44,7 @@ describe('LeaveButton', () => {
});
it('renders leave modal', () => {
- const leaveModal = wrapper.find(LeaveModal);
+ const leaveModal = wrapper.findComponent(LeaveModal);
expect(leaveModal.exists()).toBe(true);
expect(leaveModal.props('member')).toEqual(member);
diff --git a/spec/frontend/members/components/action_buttons/remove_group_link_button_spec.js b/spec/frontend/members/components/action_buttons/remove_group_link_button_spec.js
index ca655e36c42..b511cebdf28 100644
--- a/spec/frontend/members/components/action_buttons/remove_group_link_button_spec.js
+++ b/spec/frontend/members/components/action_buttons/remove_group_link_button_spec.js
@@ -42,7 +42,7 @@ describe('RemoveGroupLinkButton', () => {
});
};
- const findButton = () => wrapper.find(GlButton);
+ const findButton = () => wrapper.findComponent(GlButton);
beforeEach(() => {
createComponent();
diff --git a/spec/frontend/members/components/action_buttons/resend_invite_button_spec.js b/spec/frontend/members/components/action_buttons/resend_invite_button_spec.js
index 8e933d16463..51cfd47ddf4 100644
--- a/spec/frontend/members/components/action_buttons/resend_invite_button_spec.js
+++ b/spec/frontend/members/components/action_buttons/resend_invite_button_spec.js
@@ -44,7 +44,7 @@ describe('ResendInviteButton', () => {
};
const findForm = () => wrapper.find('form');
- const findButton = () => findForm().find(GlButton);
+ const findButton = () => findForm().findComponent(GlButton);
beforeEach(() => {
createComponent();
diff --git a/spec/frontend/members/components/action_buttons/user_action_buttons_spec.js b/spec/frontend/members/components/action_buttons/user_action_buttons_spec.js
index 3e4ffb6e61b..6ac46619bc9 100644
--- a/spec/frontend/members/components/action_buttons/user_action_buttons_spec.js
+++ b/spec/frontend/members/components/action_buttons/user_action_buttons_spec.js
@@ -19,7 +19,7 @@ describe('UserActionButtons', () => {
});
};
- const findRemoveMemberButton = () => wrapper.find(RemoveMemberButton);
+ const findRemoveMemberButton = () => wrapper.findComponent(RemoveMemberButton);
afterEach(() => {
wrapper.destroy();
@@ -80,7 +80,7 @@ describe('UserActionButtons', () => {
},
});
- expect(wrapper.find(LeaveButton).exists()).toBe(true);
+ expect(wrapper.findComponent(LeaveButton).exists()).toBe(true);
});
});
});
diff --git a/spec/frontend/members/components/app_spec.js b/spec/frontend/members/components/app_spec.js
index 4124a1870a6..d105a4d9fde 100644
--- a/spec/frontend/members/components/app_spec.js
+++ b/spec/frontend/members/components/app_spec.js
@@ -41,8 +41,8 @@ describe('MembersApp', () => {
});
};
- const findAlert = () => wrapper.find(GlAlert);
- const findFilterSortContainer = () => wrapper.find(FilterSortContainer);
+ const findAlert = () => wrapper.findComponent(GlAlert);
+ const findFilterSortContainer = () => wrapper.findComponent(FilterSortContainer);
beforeEach(() => {
commonUtils.scrollToElement = jest.fn();
diff --git a/spec/frontend/members/components/avatars/group_avatar_spec.js b/spec/frontend/members/components/avatars/group_avatar_spec.js
index 9c1574a84ee..13c50de9835 100644
--- a/spec/frontend/members/components/avatars/group_avatar_spec.js
+++ b/spec/frontend/members/components/avatars/group_avatar_spec.js
@@ -30,7 +30,7 @@ describe('MemberList', () => {
});
it('renders link to group', () => {
- const link = wrapper.find(GlAvatarLink);
+ const link = wrapper.findComponent(GlAvatarLink);
expect(link.exists()).toBe(true);
expect(link.attributes('href')).toBe(group.webUrl);
diff --git a/spec/frontend/members/components/avatars/user_avatar_spec.js b/spec/frontend/members/components/avatars/user_avatar_spec.js
index 7bcf4a11413..9b908e5b6f0 100644
--- a/spec/frontend/members/components/avatars/user_avatar_spec.js
+++ b/spec/frontend/members/components/avatars/user_avatar_spec.js
@@ -33,7 +33,7 @@ describe('UserAvatar', () => {
it("renders link to user's profile", () => {
createComponent();
- const link = wrapper.find(GlAvatarLink);
+ const link = wrapper.findComponent(GlAvatarLink);
expect(link.exists()).toBe(true);
expect(link.attributes()).toMatchObject({
@@ -77,7 +77,7 @@ describe('UserAvatar', () => {
`('renders the "$badgeText" badge', ({ member, badgeText }) => {
createComponent({ member });
- expect(wrapper.find(GlBadge).text()).toBe(badgeText);
+ expect(wrapper.findComponent(GlBadge).text()).toBe(badgeText);
});
it('renders the "It\'s you" badge when member is current user', () => {
diff --git a/spec/frontend/members/components/filter_sort/filter_sort_container_spec.js b/spec/frontend/members/components/filter_sort/filter_sort_container_spec.js
index 4ca8a3bdc36..de2f6e6dd47 100644
--- a/spec/frontend/members/components/filter_sort/filter_sort_container_spec.js
+++ b/spec/frontend/members/components/filter_sort/filter_sort_container_spec.js
@@ -60,7 +60,7 @@ describe('FilterSortContainer', () => {
},
});
- expect(wrapper.find(MembersFilteredSearchBar).exists()).toBe(true);
+ expect(wrapper.findComponent(MembersFilteredSearchBar).exists()).toBe(true);
});
});
@@ -70,7 +70,7 @@ describe('FilterSortContainer', () => {
tableSortableFields: ['account'],
});
- expect(wrapper.find(SortDropdown).exists()).toBe(true);
+ expect(wrapper.findComponent(SortDropdown).exists()).toBe(true);
});
});
});
diff --git a/spec/frontend/members/components/filter_sort/members_filtered_search_bar_spec.js b/spec/frontend/members/components/filter_sort/members_filtered_search_bar_spec.js
index b692eea4aa5..4580fdb06f2 100644
--- a/spec/frontend/members/components/filter_sort/members_filtered_search_bar_spec.js
+++ b/spec/frontend/members/components/filter_sort/members_filtered_search_bar_spec.js
@@ -56,7 +56,7 @@ describe('MembersFilteredSearchBar', () => {
});
};
- const findFilteredSearchBar = () => wrapper.find(FilteredSearchBar);
+ const findFilteredSearchBar = () => wrapper.findComponent(FilteredSearchBar);
it('passes correct props to `FilteredSearchBar` component', () => {
createComponent();
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 709ad907a38..5581fd52458 100644
--- a/spec/frontend/members/components/filter_sort/sort_dropdown_spec.js
+++ b/spec/frontend/members/components/filter_sort/sort_dropdown_spec.js
@@ -43,13 +43,13 @@ describe('SortDropdown', () => {
});
};
- const findSortingComponent = () => wrapper.find(GlSorting);
+ const findSortingComponent = () => wrapper.findComponent(GlSorting);
const findSortDirectionToggle = () =>
findSortingComponent().find('button[title="Sort direction"]');
const findDropdownToggle = () => wrapper.find('button[aria-haspopup="true"]');
const findDropdownItemByText = (text) =>
wrapper
- .findAll(GlSortingItem)
+ .findAllComponents(GlSortingItem)
.wrappers.find((dropdownItemWrapper) => dropdownItemWrapper.text() === text);
beforeEach(() => {
diff --git a/spec/frontend/members/components/modals/remove_group_link_modal_spec.js b/spec/frontend/members/components/modals/remove_group_link_modal_spec.js
index 447496910b8..af96396f09f 100644
--- a/spec/frontend/members/components/modals/remove_group_link_modal_spec.js
+++ b/spec/frontend/members/components/modals/remove_group_link_modal_spec.js
@@ -47,8 +47,8 @@ describe('RemoveGroupLinkModal', () => {
});
};
- const findModal = () => wrapper.find(GlModal);
- const findForm = () => findModal().find(GlForm);
+ const findModal = () => wrapper.findComponent(GlModal);
+ const findForm = () => findModal().findComponent(GlForm);
const getByText = (text, options) =>
createWrapper(within(findModal().element).getByText(text, options));
diff --git a/spec/frontend/members/components/modals/remove_member_modal_spec.js b/spec/frontend/members/components/modals/remove_member_modal_spec.js
index 1d39c4b3175..59b112492b8 100644
--- a/spec/frontend/members/components/modals/remove_member_modal_spec.js
+++ b/spec/frontend/members/components/modals/remove_member_modal_spec.js
@@ -46,7 +46,7 @@ describe('RemoveMemberModal', () => {
});
};
- const findForm = () => wrapper.find({ ref: 'form' });
+ const findForm = () => wrapper.findComponent({ ref: 'form' });
const findGlModal = () => wrapper.findComponent(GlModal);
const findUserDeletionObstaclesList = () => wrapper.findComponent(UserDeletionObstaclesList);
diff --git a/spec/frontend/members/components/table/created_at_spec.js b/spec/frontend/members/components/table/created_at_spec.js
index 74b71e22893..793c122587d 100644
--- a/spec/frontend/members/components/table/created_at_spec.js
+++ b/spec/frontend/members/components/table/created_at_spec.js
@@ -39,7 +39,7 @@ describe('CreatedAt', () => {
});
it('uses `TimeAgoTooltip` component to display tooltip', () => {
- expect(wrapper.find(TimeAgoTooltip).exists()).toBe(true);
+ expect(wrapper.findComponent(TimeAgoTooltip).exists()).toBe(true);
});
});
diff --git a/spec/frontend/members/components/table/expiration_datepicker_spec.js b/spec/frontend/members/components/table/expiration_datepicker_spec.js
index 4fb43fbd888..9b8f053348b 100644
--- a/spec/frontend/members/components/table/expiration_datepicker_spec.js
+++ b/spec/frontend/members/components/table/expiration_datepicker_spec.js
@@ -56,7 +56,7 @@ describe('ExpirationDatepicker', () => {
};
const findInput = () => wrapper.find('input');
- const findDatepicker = () => wrapper.find(GlDatepicker);
+ const findDatepicker = () => wrapper.findComponent(GlDatepicker);
afterEach(() => {
wrapper.destroy();
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 1379b2d26ce..f3f50bf620a 100644
--- a/spec/frontend/members/components/table/member_action_buttons_spec.js
+++ b/spec/frontend/members/components/table/member_action_buttons_spec.js
@@ -38,7 +38,7 @@ describe('MemberActionButtons', () => {
({ memberType, member, expectedComponent }) => {
createComponent({ memberType, member });
- expect(wrapper.find(expectedComponent).exists()).toBe(true);
+ expect(wrapper.findComponent(expectedComponent).exists()).toBe(true);
},
);
});
diff --git a/spec/frontend/members/components/table/member_avatar_spec.js b/spec/frontend/members/components/table/member_avatar_spec.js
index 3cce64effbc..35f82c28fc5 100644
--- a/spec/frontend/members/components/table/member_avatar_spec.js
+++ b/spec/frontend/members/components/table/member_avatar_spec.js
@@ -33,7 +33,7 @@ describe('MemberList', () => {
({ memberType, member, expectedComponent }) => {
createComponent({ memberType, member });
- expect(wrapper.find(expectedComponent).exists()).toBe(true);
+ expect(wrapper.findComponent(expectedComponent).exists()).toBe(true);
},
);
});
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 6575a7c7126..fd56699602e 100644
--- a/spec/frontend/members/components/table/members_table_cell_spec.js
+++ b/spec/frontend/members/components/table/members_table_cell_spec.js
@@ -69,7 +69,7 @@ describe('MembersTableCell', () => {
});
};
- const findWrappedComponent = () => wrapper.find(WrappedComponent);
+ const findWrappedComponent = () => wrapper.findComponent(WrappedComponent);
const memberCurrentUser = {
...memberMock,
diff --git a/spec/frontend/members/components/table/members_table_spec.js b/spec/frontend/members/components/table/members_table_spec.js
index 08baa663bf0..0ed01396fcb 100644
--- a/spec/frontend/members/components/table/members_table_spec.js
+++ b/spec/frontend/members/components/table/members_table_spec.js
@@ -81,13 +81,13 @@ describe('MembersTable', () => {
const url = 'https://localhost/foo-bar/-/project_members?tab=invited';
- const findTable = () => wrapper.find(GlTable);
+ const findTable = () => wrapper.findComponent(GlTable);
const findTableCellByMemberId = (tableCellLabel, memberId) =>
wrapper
.findByTestId(`members-table-row-${memberId}`)
.find(`[data-label="${tableCellLabel}"][role="cell"]`);
- const findPagination = () => extendedWrapper(wrapper.find(GlPagination));
+ const findPagination = () => extendedWrapper(wrapper.findComponent(GlPagination));
const expectCorrectLinkToPage2 = () => {
expect(findPagination().findByText('2', { selector: 'a' }).attributes('href')).toBe(
@@ -126,7 +126,10 @@ describe('MembersTable', () => {
if (expectedComponent) {
expect(
- wrapper.find(`[data-label="${label}"][role="cell"]`).find(expectedComponent).exists(),
+ wrapper
+ .find(`[data-label="${label}"][role="cell"]`)
+ .findComponent(expectedComponent)
+ .exists(),
).toBe(true);
}
});
@@ -179,7 +182,10 @@ describe('MembersTable', () => {
expect(actionField.exists()).toBe(true);
expect(actionField.classes('gl-sr-only')).toBe(true);
expect(
- wrapper.find(`[data-label="Actions"][role="cell"]`).find(MemberActionButtons).exists(),
+ wrapper
+ .find(`[data-label="Actions"][role="cell"]`)
+ .findComponent(MemberActionButtons)
+ .exists(),
).toBe(true);
});
@@ -250,9 +256,9 @@ describe('MembersTable', () => {
it('renders badge in "Max role" field', () => {
createComponent({ members: [memberMock], tableFields: ['maxRole'] });
- expect(wrapper.find(`[data-label="Max role"][role="cell"]`).find(GlBadge).text()).toBe(
- memberMock.accessLevel.stringValue,
- );
+ expect(
+ wrapper.find(`[data-label="Max role"][role="cell"]`).findComponent(GlBadge).text(),
+ ).toBe(memberMock.accessLevel.stringValue);
});
});
diff --git a/spec/frontend/members/components/table/role_dropdown_spec.js b/spec/frontend/members/components/table/role_dropdown_spec.js
index 2f1626a7044..b254cce4d72 100644
--- a/spec/frontend/members/components/table/role_dropdown_spec.js
+++ b/spec/frontend/members/components/table/role_dropdown_spec.js
@@ -57,11 +57,11 @@ describe('RoleDropdown', () => {
);
const getCheckedDropdownItem = () =>
wrapper
- .findAll(GlDropdownItem)
+ .findAllComponents(GlDropdownItem)
.wrappers.find((dropdownItemWrapper) => dropdownItemWrapper.props('isChecked'));
const findDropdownToggle = () => wrapper.find('button[aria-haspopup="true"]');
- const findDropdown = () => wrapper.find(GlDropdown);
+ const findDropdown = () => wrapper.findComponent(GlDropdown);
afterEach(() => {
wrapper.destroy();
diff --git a/spec/frontend/members/index_spec.js b/spec/frontend/members/index_spec.js
index 251a8b0b774..5c813eb2a67 100644
--- a/spec/frontend/members/index_spec.js
+++ b/spec/frontend/members/index_spec.js
@@ -39,7 +39,7 @@ describe('initMembersApp', () => {
it('renders `MembersTabs`', () => {
setup();
- expect(wrapper.find(MembersTabs).exists()).toBe(true);
+ expect(wrapper.findComponent(MembersTabs).exists()).toBe(true);
});
it('parses and sets `members` in Vuex store', () => {
diff --git a/spec/frontend/monitoring/components/charts/anomaly_spec.js b/spec/frontend/monitoring/components/charts/anomaly_spec.js
index 8dc6132709e..3674a49f42c 100644
--- a/spec/frontend/monitoring/components/charts/anomaly_spec.js
+++ b/spec/frontend/monitoring/components/charts/anomaly_spec.js
@@ -20,7 +20,7 @@ describe('Anomaly chart component', () => {
propsData: { ...props },
});
};
- const findTimeSeries = () => wrapper.find(MonitorTimeSeriesChart);
+ const findTimeSeries = () => wrapper.findComponent(MonitorTimeSeriesChart);
const getTimeSeriesProps = () => findTimeSeries().props();
describe('wrapped monitor-time-series-chart component', () => {
diff --git a/spec/frontend/monitoring/components/charts/bar_spec.js b/spec/frontend/monitoring/components/charts/bar_spec.js
index 6368c53943a..5339a7a525b 100644
--- a/spec/frontend/monitoring/components/charts/bar_spec.js
+++ b/spec/frontend/monitoring/components/charts/bar_spec.js
@@ -33,7 +33,7 @@ describe('Bar component', () => {
let chartData;
beforeEach(() => {
- glbarChart = barChart.find(GlBarChart);
+ glbarChart = barChart.findComponent(GlBarChart);
chartData = barChart.vm.chartData[graphData.metrics[0].label];
});
diff --git a/spec/frontend/monitoring/components/charts/column_spec.js b/spec/frontend/monitoring/components/charts/column_spec.js
index e10cb3a456a..0158966997f 100644
--- a/spec/frontend/monitoring/components/charts/column_spec.js
+++ b/spec/frontend/monitoring/components/charts/column_spec.js
@@ -44,7 +44,7 @@ describe('Column component', () => {
},
});
};
- const findChart = () => wrapper.find(GlColumnChart);
+ const findChart = () => wrapper.findComponent(GlColumnChart);
const chartProps = (prop) => findChart().props(prop);
beforeEach(() => {
diff --git a/spec/frontend/monitoring/components/charts/gauge_spec.js b/spec/frontend/monitoring/components/charts/gauge_spec.js
index c8f67d5d8c7..484199698ea 100644
--- a/spec/frontend/monitoring/components/charts/gauge_spec.js
+++ b/spec/frontend/monitoring/components/charts/gauge_spec.js
@@ -8,7 +8,7 @@ describe('Gauge Chart component', () => {
let wrapper;
- const findGaugeChart = () => wrapper.find(GlGaugeChart);
+ const findGaugeChart = () => wrapper.findComponent(GlGaugeChart);
const createWrapper = ({ ...graphProps } = {}) => {
wrapper = shallowMount(GaugeChart, {
diff --git a/spec/frontend/monitoring/components/charts/heatmap_spec.js b/spec/frontend/monitoring/components/charts/heatmap_spec.js
index 841b7e0648a..e163d4e73a0 100644
--- a/spec/frontend/monitoring/components/charts/heatmap_spec.js
+++ b/spec/frontend/monitoring/components/charts/heatmap_spec.js
@@ -8,7 +8,7 @@ describe('Heatmap component', () => {
let wrapper;
let store;
- const findChart = () => wrapper.find(GlHeatmap);
+ const findChart = () => wrapper.findComponent(GlHeatmap);
const graphData = heatmapGraphData();
diff --git a/spec/frontend/monitoring/components/charts/single_stat_spec.js b/spec/frontend/monitoring/components/charts/single_stat_spec.js
index 8633b49565f..62a0b7e6ad3 100644
--- a/spec/frontend/monitoring/components/charts/single_stat_spec.js
+++ b/spec/frontend/monitoring/components/charts/single_stat_spec.js
@@ -15,7 +15,7 @@ describe('Single Stat Chart component', () => {
});
};
- const findChart = () => wrapper.find(GlSingleStat);
+ const findChart = () => wrapper.findComponent(GlSingleStat);
beforeEach(() => {
createComponent();
diff --git a/spec/frontend/monitoring/components/charts/stacked_column_spec.js b/spec/frontend/monitoring/components/charts/stacked_column_spec.js
index 9cab3650f28..91fe36bc6e4 100644
--- a/spec/frontend/monitoring/components/charts/stacked_column_spec.js
+++ b/spec/frontend/monitoring/components/charts/stacked_column_spec.js
@@ -15,8 +15,8 @@ describe('Stacked column chart component', () => {
let wrapper;
- const findChart = () => wrapper.find(GlStackedColumnChart);
- const findLegend = () => wrapper.find(GlChartLegend);
+ const findChart = () => wrapper.findComponent(GlStackedColumnChart);
+ const findLegend = () => wrapper.findComponent(GlChartLegend);
const createWrapper = (props = {}, mountingMethod = shallowMount) =>
mountingMethod(StackedColumnChart, {
diff --git a/spec/frontend/monitoring/components/charts/time_series_spec.js b/spec/frontend/monitoring/components/charts/time_series_spec.js
index f4bca26f659..503dee7b937 100644
--- a/spec/frontend/monitoring/components/charts/time_series_spec.js
+++ b/spec/frontend/monitoring/components/charts/time_series_spec.js
@@ -9,7 +9,6 @@ import { mount, shallowMount } from '@vue/test-utils';
import timezoneMock from 'timezone-mock';
import { nextTick } from 'vue';
import { TEST_HOST } from 'helpers/test_constants';
-import { setTestTimeout } from 'helpers/timeout';
import { shallowWrapperContainsSlotText } from 'helpers/vue_test_utils_helper';
import TimeSeries from '~/monitoring/components/charts/time_series.vue';
import { panelTypes, chartHeight } from '~/monitoring/constants';
@@ -59,17 +58,13 @@ describe('Time series component', () => {
});
};
- beforeEach(() => {
- setTestTimeout(1000);
- });
-
afterEach(() => {
wrapper.destroy();
});
describe('With a single time series', () => {
describe('general functions', () => {
- const findChart = () => wrapper.find({ ref: 'chart' });
+ const findChart = () => wrapper.findComponent({ ref: 'chart' });
beforeEach(async () => {
createWrapper({}, mount);
@@ -215,7 +210,7 @@ describe('Time series component', () => {
const name = 'Metric 1';
const value = '5.556';
const dataIndex = 0;
- const seriesLabel = wrapper.find(GlChartSeriesLabel);
+ const seriesLabel = wrapper.findComponent(GlChartSeriesLabel);
expect(seriesLabel.vm.color).toBe('');
@@ -225,7 +220,11 @@ describe('Time series component', () => {
]);
expect(
- shallowWrapperContainsSlotText(wrapper.find(GlLineChart), 'tooltip-content', value),
+ shallowWrapperContainsSlotText(
+ wrapper.findComponent(GlLineChart),
+ 'tooltip-content',
+ value,
+ ),
).toBe(true);
});
@@ -598,7 +597,7 @@ describe('Time series component', () => {
glChartComponents.forEach((dynamicComponent) => {
describe(`GitLab UI: ${dynamicComponent.chartType}`, () => {
- const findChartComponent = () => wrapper.find(dynamicComponent.component);
+ const findChartComponent = () => wrapper.findComponent(dynamicComponent.component);
beforeEach(async () => {
createWrapper(
@@ -656,7 +655,7 @@ describe('Time series component', () => {
wrapper.vm.tooltip.commitUrl = commitUrl;
await nextTick();
- const commitLink = wrapper.find(GlLink);
+ const commitLink = wrapper.findComponent(GlLink);
expect(shallowWrapperContainsSlotText(commitLink, 'default', mockSha)).toBe(true);
expect(commitLink.attributes('href')).toEqual(commitUrl);
@@ -680,7 +679,9 @@ describe('Time series component', () => {
let lineColors;
beforeEach(() => {
- lineColors = wrapper.find(GlAreaChart).vm.series.map((item) => item.lineStyle.color);
+ lineColors = wrapper
+ .findComponent(GlAreaChart)
+ .vm.series.map((item) => item.lineStyle.color);
});
it('should contain different colors for contiguous time series', () => {
@@ -690,7 +691,7 @@ describe('Time series component', () => {
});
it('should match series color with tooltip label color', () => {
- const labels = wrapper.findAll(GlChartSeriesLabel);
+ const labels = wrapper.findAllComponents(GlChartSeriesLabel);
lineColors.forEach((color, index) => {
const labelColor = labels.at(index).props('color');
@@ -700,7 +701,7 @@ describe('Time series component', () => {
it('should match series color with legend color', () => {
const legendColors = wrapper
- .find(GlChartLegend)
+ .findComponent(GlChartLegend)
.props('seriesInfo')
.map((item) => item.color);
@@ -713,7 +714,7 @@ describe('Time series component', () => {
});
describe('legend layout', () => {
- const findLegend = () => wrapper.find(GlChartLegend);
+ const findLegend = () => wrapper.findComponent(GlChartLegend);
beforeEach(async () => {
createWrapper({}, mount);
diff --git a/spec/frontend/monitoring/components/dashboard_actions_menu_spec.js b/spec/frontend/monitoring/components/dashboard_actions_menu_spec.js
index d74f959ac0f..bb57420d406 100644
--- a/spec/frontend/monitoring/components/dashboard_actions_menu_spec.js
+++ b/spec/frontend/monitoring/components/dashboard_actions_menu_spec.js
@@ -92,7 +92,7 @@ describe('Actions menu', () => {
});
it('renders custom metrics form fields', () => {
- expect(wrapper.find(CustomMetricsFormFields).exists()).toBe(true);
+ expect(wrapper.findComponent(CustomMetricsFormFields).exists()).toBe(true);
});
});
@@ -316,7 +316,7 @@ describe('Actions menu', () => {
});
it('is not disabled', () => {
- expect(findStarDashboardItem().attributes('disabled')).toBeFalsy();
+ expect(findStarDashboardItem().attributes('disabled')).toBeUndefined();
});
it('is disabled when starring is taking place', async () => {
diff --git a/spec/frontend/monitoring/components/dashboard_header_spec.js b/spec/frontend/monitoring/components/dashboard_header_spec.js
index e28c2913949..18ccda2c41c 100644
--- a/spec/frontend/monitoring/components/dashboard_header_spec.js
+++ b/spec/frontend/monitoring/components/dashboard_header_spec.js
@@ -29,18 +29,19 @@ describe('Dashboard header', () => {
let store;
let wrapper;
- const findDashboardDropdown = () => wrapper.find(DashboardsDropdown);
+ const findDashboardDropdown = () => wrapper.findComponent(DashboardsDropdown);
- const findEnvsDropdown = () => wrapper.find({ ref: 'monitorEnvironmentsDropdown' });
- const findEnvsDropdownItems = () => findEnvsDropdown().findAll(GlDropdownItem);
- const findEnvsDropdownSearch = () => findEnvsDropdown().find(GlSearchBoxByType);
- const findEnvsDropdownSearchMsg = () => wrapper.find({ ref: 'monitorEnvironmentsDropdownMsg' });
- const findEnvsDropdownLoadingIcon = () => findEnvsDropdown().find(GlLoadingIcon);
+ const findEnvsDropdown = () => wrapper.findComponent({ ref: 'monitorEnvironmentsDropdown' });
+ const findEnvsDropdownItems = () => findEnvsDropdown().findAllComponents(GlDropdownItem);
+ const findEnvsDropdownSearch = () => findEnvsDropdown().findComponent(GlSearchBoxByType);
+ const findEnvsDropdownSearchMsg = () =>
+ wrapper.findComponent({ ref: 'monitorEnvironmentsDropdownMsg' });
+ const findEnvsDropdownLoadingIcon = () => findEnvsDropdown().findComponent(GlLoadingIcon);
- const findDateTimePicker = () => wrapper.find(DateTimePicker);
- const findRefreshButton = () => wrapper.find(RefreshButton);
+ const findDateTimePicker = () => wrapper.findComponent(DateTimePicker);
+ const findRefreshButton = () => wrapper.findComponent(RefreshButton);
- const findActionsMenu = () => wrapper.find(ActionsMenu);
+ const findActionsMenu = () => wrapper.findComponent(ActionsMenu);
const setSearchTerm = (searchTerm) => {
store.commit(`monitoringDashboard/${types.SET_ENVIRONMENTS_FILTER}`, searchTerm);
diff --git a/spec/frontend/monitoring/components/dashboard_panel_builder_spec.js b/spec/frontend/monitoring/components/dashboard_panel_builder_spec.js
index f19ef6c6fb7..d71f6374967 100644
--- a/spec/frontend/monitoring/components/dashboard_panel_builder_spec.js
+++ b/spec/frontend/monitoring/components/dashboard_panel_builder_spec.js
@@ -32,14 +32,14 @@ describe('dashboard invalid url parameters', () => {
});
};
- const findForm = () => wrapper.find(GlForm);
- const findTxtArea = () => findForm().find(GlFormTextarea);
+ const findForm = () => wrapper.findComponent(GlForm);
+ const findTxtArea = () => findForm().findComponent(GlFormTextarea);
const findSubmitBtn = () => findForm().find('[type="submit"]');
- const findClipboardCopyBtn = () => wrapper.find({ ref: 'clipboardCopyBtn' });
- const findViewDocumentationBtn = () => wrapper.find({ ref: 'viewDocumentationBtn' });
- const findOpenRepositoryBtn = () => wrapper.find({ ref: 'openRepositoryBtn' });
- const findPanel = () => wrapper.find(DashboardPanel);
- const findTimeRangePicker = () => wrapper.find(DateTimePicker);
+ const findClipboardCopyBtn = () => wrapper.findComponent({ ref: 'clipboardCopyBtn' });
+ const findViewDocumentationBtn = () => wrapper.findComponent({ ref: 'viewDocumentationBtn' });
+ const findOpenRepositoryBtn = () => wrapper.findComponent({ ref: 'openRepositoryBtn' });
+ const findPanel = () => wrapper.findComponent(DashboardPanel);
+ const findTimeRangePicker = () => wrapper.findComponent(DateTimePicker);
const findRefreshButton = () => wrapper.find('[data-testid="previewRefreshButton"]');
beforeEach(() => {
@@ -192,8 +192,8 @@ describe('dashboard invalid url parameters', () => {
});
it('displays an alert', () => {
- expect(wrapper.find(GlAlert).exists()).toBe(true);
- expect(wrapper.find(GlAlert).text()).toBe(mockError);
+ expect(wrapper.findComponent(GlAlert).exists()).toBe(true);
+ expect(wrapper.findComponent(GlAlert).text()).toBe(mockError);
});
it('displays an empty dashboard panel', () => {
@@ -215,11 +215,11 @@ describe('dashboard invalid url parameters', () => {
});
it('displays no alert', () => {
- expect(wrapper.find(GlAlert).exists()).toBe(false);
+ expect(wrapper.findComponent(GlAlert).exists()).toBe(false);
});
it('displays panel with data', () => {
- const { title, type } = wrapper.find(DashboardPanel).props('graphData');
+ const { title, type } = wrapper.findComponent(DashboardPanel).props('graphData');
expect(title).toBe(mockPanel.title);
expect(type).toBe(mockPanel.type);
diff --git a/spec/frontend/monitoring/components/dashboard_panel_spec.js b/spec/frontend/monitoring/components/dashboard_panel_spec.js
index 7c54a4742ac..d797d9e2ad0 100644
--- a/spec/frontend/monitoring/components/dashboard_panel_spec.js
+++ b/spec/frontend/monitoring/components/dashboard_panel_spec.js
@@ -3,7 +3,6 @@ import { shallowMount } from '@vue/test-utils';
import AxiosMockAdapter from 'axios-mock-adapter';
import Vuex from 'vuex';
import { nextTick } from 'vue';
-import { setTestTimeout } from 'helpers/timeout';
import axios from '~/lib/utils/axios_utils';
import MonitorAnomalyChart from '~/monitoring/components/charts/anomaly.vue';
@@ -42,11 +41,11 @@ describe('Dashboard Panel', () => {
const exampleText = 'example_text';
- const findCopyLink = () => wrapper.find({ ref: 'copyChartLink' });
- const findTimeChart = () => wrapper.find({ ref: 'timeSeriesChart' });
- const findTitle = () => wrapper.find({ ref: 'graphTitle' });
- const findCtxMenu = () => wrapper.find({ ref: 'contextualMenu' });
- const findMenuItems = () => wrapper.findAll(GlDropdownItem);
+ const findCopyLink = () => wrapper.findComponent({ ref: 'copyChartLink' });
+ const findTimeChart = () => wrapper.findComponent({ ref: 'timeSeriesChart' });
+ const findTitle = () => wrapper.findComponent({ ref: 'graphTitle' });
+ const findCtxMenu = () => wrapper.findComponent({ ref: 'contextualMenu' });
+ const findMenuItems = () => wrapper.findAllComponents(GlDropdownItem);
const findMenuItemByText = (text) => findMenuItems().filter((i) => i.text() === text);
const createWrapper = (props, { mountFn = shallowMount, ...options } = {}) => {
@@ -72,8 +71,6 @@ describe('Dashboard Panel', () => {
};
beforeEach(() => {
- setTestTimeout(1000);
-
store = createStore();
state = store.state.monitoringDashboard;
@@ -118,7 +115,7 @@ describe('Dashboard Panel', () => {
});
it('renders no download csv link', () => {
- expect(wrapper.find({ ref: 'downloadCsvLink' }).exists()).toBe(false);
+ expect(wrapper.findComponent({ ref: 'downloadCsvLink' }).exists()).toBe(false);
});
it('does not contain graph widgets', () => {
@@ -126,7 +123,7 @@ describe('Dashboard Panel', () => {
});
it('The Empty Chart component is rendered and is a Vue instance', () => {
- expect(wrapper.find(MonitorEmptyChart).exists()).toBe(true);
+ expect(wrapper.findComponent(MonitorEmptyChart).exists()).toBe(true);
});
});
@@ -146,7 +143,7 @@ describe('Dashboard Panel', () => {
});
it('renders no download csv link', () => {
- expect(wrapper.find({ ref: 'downloadCsvLink' }).exists()).toBe(false);
+ expect(wrapper.findComponent({ ref: 'downloadCsvLink' }).exists()).toBe(false);
});
it('does not contain graph widgets', () => {
@@ -154,7 +151,7 @@ describe('Dashboard Panel', () => {
});
it('The Empty Chart component is rendered and is a Vue instance', () => {
- expect(wrapper.find(MonitorEmptyChart).exists()).toBe(true);
+ expect(wrapper.findComponent(MonitorEmptyChart).exists()).toBe(true);
});
});
@@ -173,7 +170,7 @@ describe('Dashboard Panel', () => {
it('contains graph widgets', () => {
expect(findCtxMenu().exists()).toBe(true);
- expect(wrapper.find({ ref: 'downloadCsvLink' }).exists()).toBe(true);
+ expect(wrapper.findComponent({ ref: 'downloadCsvLink' }).exists()).toBe(true);
});
it('sets no clipboard copy link on dropdown by default', () => {
@@ -208,12 +205,12 @@ describe('Dashboard Panel', () => {
it('empty chart is rendered for empty results', () => {
createWrapper({ graphData: graphDataEmpty });
- expect(wrapper.find(MonitorEmptyChart).exists()).toBe(true);
+ expect(wrapper.findComponent(MonitorEmptyChart).exists()).toBe(true);
});
it('area chart is rendered by default', () => {
createWrapper();
- expect(wrapper.find(MonitorTimeSeriesChart).exists()).toBe(true);
+ expect(wrapper.findComponent(MonitorTimeSeriesChart).exists()).toBe(true);
});
describe.each`
@@ -234,8 +231,8 @@ describe('Dashboard Panel', () => {
});
it(`renders the chart component and binds attributes`, () => {
- expect(wrapper.find(component).exists()).toBe(true);
- expect(wrapper.find(component).attributes()).toMatchObject(attrs);
+ expect(wrapper.findComponent(component).exists()).toBe(true);
+ expect(wrapper.findComponent(component).attributes()).toMatchObject(attrs);
});
it(`contextual menu is ${hasCtxMenu ? '' : 'not '}shown`, () => {
@@ -273,7 +270,7 @@ describe('Dashboard Panel', () => {
});
describe('Edit custom metric dropdown item', () => {
- const findEditCustomMetricLink = () => wrapper.find({ ref: 'editMetricLink' });
+ const findEditCustomMetricLink = () => wrapper.findComponent({ ref: 'editMetricLink' });
const mockEditPath = '/root/kubernetes-gke-project/prometheus/metrics/23/edit';
beforeEach(async () => {
@@ -434,7 +431,7 @@ describe('Dashboard Panel', () => {
});
it('it renders a time series chart with no errors', () => {
- expect(wrapper.find(MonitorTimeSeriesChart).exists()).toBe(true);
+ expect(wrapper.findComponent(MonitorTimeSeriesChart).exists()).toBe(true);
});
});
@@ -446,7 +443,7 @@ describe('Dashboard Panel', () => {
it('displays a heatmap in local timezone', () => {
createWrapper({ graphData: heatmapGraphData() });
- expect(wrapper.find(MonitorHeatmapChart).props('timezone')).toBe('LOCAL');
+ expect(wrapper.findComponent(MonitorHeatmapChart).props('timezone')).toBe('LOCAL');
});
describe('when timezone is set to UTC', () => {
@@ -461,13 +458,13 @@ describe('Dashboard Panel', () => {
it('displays a heatmap with UTC', () => {
createWrapper({ graphData: heatmapGraphData() });
- expect(wrapper.find(MonitorHeatmapChart).props('timezone')).toBe('UTC');
+ expect(wrapper.findComponent(MonitorHeatmapChart).props('timezone')).toBe('UTC');
});
});
});
describe('Expand to full screen', () => {
- const findExpandBtn = () => wrapper.find({ ref: 'expandBtn' });
+ const findExpandBtn = () => wrapper.findComponent({ ref: 'expandBtn' });
describe('when there is no @expand listener', () => {
it('does not show `View full screen` option', () => {
@@ -495,7 +492,7 @@ describe('Dashboard Panel', () => {
});
describe('When graphData contains links', () => {
- const findManageLinksItem = () => wrapper.find({ ref: 'manageLinksItem' });
+ const findManageLinksItem = () => wrapper.findComponent({ ref: 'manageLinksItem' });
const mockLinks = [
{
url: 'https://example.com',
diff --git a/spec/frontend/monitoring/components/dashboard_spec.js b/spec/frontend/monitoring/components/dashboard_spec.js
index 90171cfc65e..608404e5c5b 100644
--- a/spec/frontend/monitoring/components/dashboard_spec.js
+++ b/spec/frontend/monitoring/components/dashboard_spec.js
@@ -97,8 +97,10 @@ describe('Dashboard', () => {
createShallowWrapper({ hasMetrics: true });
await nextTick();
- expect(wrapper.find(EmptyState).exists()).toBe(true);
- expect(wrapper.find(EmptyState).props('selectedState')).toBe(dashboardEmptyStates.LOADING);
+ expect(wrapper.findComponent(EmptyState).exists()).toBe(true);
+ expect(wrapper.findComponent(EmptyState).props('selectedState')).toBe(
+ dashboardEmptyStates.LOADING,
+ );
});
it('hides the group panels when showPanels is false', async () => {
@@ -126,7 +128,7 @@ describe('Dashboard', () => {
describe('panel containers layout', () => {
const findPanelLayoutWrapperAt = (index) => {
return wrapper
- .find(GraphGroup)
+ .findComponent(GraphGroup)
.findAll('[data-testid="dashboard-panel-layout-wrapper"]')
.at(index);
};
@@ -366,7 +368,7 @@ describe('Dashboard', () => {
});
describe('when all panels in the first group are loading', () => {
- const findGroupAt = (i) => wrapper.findAll(GraphGroup).at(i);
+ const findGroupAt = (i) => wrapper.findAllComponents(GraphGroup).at(i);
beforeEach(async () => {
setupStoreWithDashboard(store);
@@ -409,7 +411,7 @@ describe('Dashboard', () => {
setupStoreWithData(store);
await nextTick();
- wrapper.findAll(GraphGroup).wrappers.forEach((groupWrapper) => {
+ wrapper.findAllComponents(GraphGroup).wrappers.forEach((groupWrapper) => {
expect(groupWrapper.props('isLoading')).toBe(false);
});
});
@@ -443,7 +445,7 @@ describe('Dashboard', () => {
});
describe('single panel expands to "full screen" mode', () => {
- const findExpandedPanel = () => wrapper.find({ ref: 'expandedPanel' });
+ const findExpandedPanel = () => wrapper.findComponent({ ref: 'expandedPanel' });
describe('when the panel is not expanded', () => {
beforeEach(async () => {
@@ -457,7 +459,7 @@ describe('Dashboard', () => {
});
it('can set a panel as expanded', () => {
- const panel = wrapper.findAll(DashboardPanel).at(1);
+ const panel = wrapper.findAllComponents(DashboardPanel).at(1);
jest.spyOn(store, 'dispatch');
@@ -503,7 +505,7 @@ describe('Dashboard', () => {
});
it('displays a single panel and others are hidden', () => {
- const panels = wrapper.findAll(MockPanel);
+ const panels = wrapper.findAllComponents(MockPanel);
const visiblePanels = panels.filter((w) => w.isVisible());
expect(findExpandedPanel().isVisible()).toBe(true);
@@ -523,7 +525,7 @@ describe('Dashboard', () => {
});
it('restores full dashboard by clicking `back`', () => {
- wrapper.find({ ref: 'goBackBtn' }).vm.$emit('click');
+ wrapper.findComponent({ ref: 'goBackBtn' }).vm.$emit('click');
expect(store.dispatch).toHaveBeenCalledWith(
'monitoringDashboard/clearExpandedPanel',
@@ -551,21 +553,21 @@ describe('Dashboard', () => {
});
it('shows a group empty area', () => {
- const emptyGroup = wrapper.findAll({ ref: 'empty-group' });
+ const emptyGroup = wrapper.findAllComponents({ ref: 'empty-group' });
expect(emptyGroup).toHaveLength(1);
expect(emptyGroup.is(GroupEmptyState)).toBe(true);
});
it('group empty area displays a NO_DATA state', () => {
- expect(wrapper.findAll({ ref: 'empty-group' }).at(0).props('selectedState')).toEqual(
- metricStates.NO_DATA,
- );
+ expect(
+ wrapper.findAllComponents({ ref: 'empty-group' }).at(0).props('selectedState'),
+ ).toEqual(metricStates.NO_DATA);
});
});
describe('drag and drop function', () => {
- const findDraggables = () => wrapper.findAll(VueDraggable);
+ const findDraggables = () => wrapper.findAllComponents(VueDraggable);
const findEnabledDraggables = () => findDraggables().filter((f) => !f.attributes('disabled'));
const findDraggablePanels = () => wrapper.findAll('.js-draggable-panel');
const findRearrangeButton = () => wrapper.find('.js-rearrange-button');
@@ -677,7 +679,7 @@ describe('Dashboard', () => {
});
it('hides dashboard header by default', () => {
- expect(wrapper.find({ ref: 'prometheusGraphsHeader' }).exists()).toEqual(false);
+ expect(wrapper.findComponent({ ref: 'prometheusGraphsHeader' }).exists()).toEqual(false);
});
it('renders correctly', () => {
@@ -742,7 +744,7 @@ describe('Dashboard', () => {
const panelIndex = 1; // skip expanded panel
const getClipboardTextFirstPanel = () =>
- wrapper.findAll(DashboardPanel).at(panelIndex).props('clipboardText');
+ wrapper.findAllComponents(DashboardPanel).at(panelIndex).props('clipboardText');
beforeEach(async () => {
setupStoreWithData(store);
@@ -770,7 +772,7 @@ describe('Dashboard', () => {
// While the recommendation in the documentation is to test
// with a data-testid attribute, I want to make sure that
// the dashboard panels have a ref attribute set.
- const getDashboardPanel = () => wrapper.find({ ref: panelRef });
+ const getDashboardPanel = () => wrapper.findComponent({ ref: panelRef });
beforeEach(async () => {
setupStoreWithData(store);
diff --git a/spec/frontend/monitoring/components/dashboard_url_time_spec.js b/spec/frontend/monitoring/components/dashboard_url_time_spec.js
index 64c48100b31..a327e234581 100644
--- a/spec/frontend/monitoring/components/dashboard_url_time_spec.js
+++ b/spec/frontend/monitoring/components/dashboard_url_time_spec.js
@@ -35,7 +35,8 @@ describe('dashboard invalid url parameters', () => {
});
};
- const findDateTimePicker = () => wrapper.find(DashboardHeader).find({ ref: 'dateTimePicker' });
+ const findDateTimePicker = () =>
+ wrapper.findComponent(DashboardHeader).findComponent({ ref: 'dateTimePicker' });
beforeEach(() => {
store = createStore();
diff --git a/spec/frontend/monitoring/components/dashboards_dropdown_spec.js b/spec/frontend/monitoring/components/dashboards_dropdown_spec.js
index f6d30384847..721992e710a 100644
--- a/spec/frontend/monitoring/components/dashboards_dropdown_spec.js
+++ b/spec/frontend/monitoring/components/dashboards_dropdown_spec.js
@@ -33,11 +33,11 @@ describe('DashboardsDropdown', () => {
});
}
- const findItems = () => wrapper.findAll(GlDropdownItem);
- const findItemAt = (i) => wrapper.findAll(GlDropdownItem).at(i);
- const findSearchInput = () => wrapper.find({ ref: 'monitorDashboardsDropdownSearch' });
- const findNoItemsMsg = () => wrapper.find({ ref: 'monitorDashboardsDropdownMsg' });
- const findStarredListDivider = () => wrapper.find({ ref: 'starredListDivider' });
+ const findItems = () => wrapper.findAllComponents(GlDropdownItem);
+ const findItemAt = (i) => wrapper.findAllComponents(GlDropdownItem).at(i);
+ const findSearchInput = () => wrapper.findComponent({ ref: 'monitorDashboardsDropdownSearch' });
+ const findNoItemsMsg = () => wrapper.findComponent({ ref: 'monitorDashboardsDropdownMsg' });
+ const findStarredListDivider = () => wrapper.findComponent({ ref: 'starredListDivider' });
// setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
// eslint-disable-next-line no-restricted-syntax
const setSearchTerm = (searchTerm) => wrapper.setData({ searchTerm });
@@ -127,7 +127,7 @@ describe('DashboardsDropdown', () => {
});
it('displays a star icon', () => {
- const star = findItemAt(0).find(GlIcon);
+ const star = findItemAt(0).findComponent(GlIcon);
expect(star.exists()).toBe(true);
expect(star.attributes('name')).toBe('star');
});
@@ -148,7 +148,7 @@ describe('DashboardsDropdown', () => {
});
it('displays no star icon', () => {
- const star = findItemAt(0).find(GlIcon);
+ const star = findItemAt(0).findComponent(GlIcon);
expect(star.exists()).toBe(false);
});
diff --git a/spec/frontend/monitoring/components/duplicate_dashboard_form_spec.js b/spec/frontend/monitoring/components/duplicate_dashboard_form_spec.js
index 0dd3afd7c83..755204dc721 100644
--- a/spec/frontend/monitoring/components/duplicate_dashboard_form_spec.js
+++ b/spec/frontend/monitoring/components/duplicate_dashboard_form_spec.js
@@ -18,7 +18,7 @@ const createMountedWrapper = (props = {}) => {
describe('DuplicateDashboardForm', () => {
const defaultBranch = 'main';
- const findByRef = (ref) => wrapper.find({ ref });
+ const findByRef = (ref) => wrapper.findComponent({ ref });
const setValue = (ref, val) => {
findByRef(ref).setValue(val);
};
diff --git a/spec/frontend/monitoring/components/duplicate_dashboard_modal_spec.js b/spec/frontend/monitoring/components/duplicate_dashboard_modal_spec.js
index 7e7a7a66d77..3032c236741 100644
--- a/spec/frontend/monitoring/components/duplicate_dashboard_modal_spec.js
+++ b/spec/frontend/monitoring/components/duplicate_dashboard_modal_spec.js
@@ -44,9 +44,9 @@ describe('duplicate dashboard modal', () => {
});
}
- const findAlert = () => wrapper.find(GlAlert);
- const findModal = () => wrapper.find(GlModal);
- const findDuplicateDashboardForm = () => wrapper.find(DuplicateDashboardForm);
+ const findAlert = () => wrapper.findComponent(GlAlert);
+ const findModal = () => wrapper.findComponent(GlModal);
+ const findDuplicateDashboardForm = () => wrapper.findComponent(DuplicateDashboardForm);
beforeEach(() => {
mockDashboards = dashboardGitResponse;
@@ -74,7 +74,7 @@ describe('duplicate dashboard modal', () => {
expect(okEvent.preventDefault).toHaveBeenCalled();
expect(wrapper.emitted().dashboardDuplicated).toBeTruthy();
expect(wrapper.emitted().dashboardDuplicated[0]).toEqual([dashboardGitResponse[0]]);
- expect(wrapper.find(GlLoadingIcon).exists()).toBe(false);
+ expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(false);
expect(wrapper.vm.$refs.duplicateDashboardModal.hide).toHaveBeenCalled();
expect(findAlert().exists()).toBe(false);
});
@@ -92,7 +92,7 @@ describe('duplicate dashboard modal', () => {
expect(findAlert().exists()).toBe(true);
expect(findAlert().text()).toBe(errMsg);
- expect(wrapper.find(GlLoadingIcon).exists()).toBe(false);
+ expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(false);
expect(wrapper.vm.$refs.duplicateDashboardModal.hide).not.toHaveBeenCalled();
});
@@ -102,7 +102,7 @@ describe('duplicate dashboard modal', () => {
commitMessage: 'A commit message',
};
- findModal().find(DuplicateDashboardForm).vm.$emit('change', formVals);
+ findModal().findComponent(DuplicateDashboardForm).vm.$emit('change', formVals);
// Binding's second argument contains the modal id
expect(wrapper.vm.form).toEqual(formVals);
diff --git a/spec/frontend/monitoring/components/embeds/embed_group_spec.js b/spec/frontend/monitoring/components/embeds/embed_group_spec.js
index 47366b345a8..6695353bdb5 100644
--- a/spec/frontend/monitoring/components/embeds/embed_group_spec.js
+++ b/spec/frontend/monitoring/components/embeds/embed_group_spec.js
@@ -58,14 +58,14 @@ describe('Embed Group', () => {
metricsWithDataGetter.mockReturnValue([]);
mountComponent();
- expect(wrapper.find(GlCard).isVisible()).toBe(false);
+ expect(wrapper.findComponent(GlCard).isVisible()).toBe(false);
});
it('shows the component when chart data is loaded', () => {
metricsWithDataGetter.mockReturnValue([1]);
mountComponent();
- expect(wrapper.find(GlCard).isVisible()).toBe(true);
+ expect(wrapper.findComponent(GlCard).isVisible()).toBe(true);
});
it('is expanded by default', () => {
@@ -79,7 +79,7 @@ describe('Embed Group', () => {
metricsWithDataGetter.mockReturnValue([1]);
mountComponent({ shallow: false, stubs: { MetricEmbed: true } });
- wrapper.find(GlButton).trigger('click');
+ wrapper.findComponent(GlButton).trigger('click');
await nextTick();
expect(wrapper.find('.gl-card-body').classes()).toContain('d-none');
@@ -93,11 +93,11 @@ describe('Embed Group', () => {
});
it('renders an Embed component', () => {
- expect(wrapper.find(MetricEmbed).exists()).toBe(true);
+ expect(wrapper.findComponent(MetricEmbed).exists()).toBe(true);
});
it('passes the correct props to the Embed component', () => {
- expect(wrapper.find(MetricEmbed).props()).toEqual(singleEmbedProps());
+ expect(wrapper.findComponent(MetricEmbed).props()).toEqual(singleEmbedProps());
});
it('adds the monitoring dashboard module', () => {
@@ -112,7 +112,7 @@ describe('Embed Group', () => {
});
it('passes the correct props to the dashboard Embed component', () => {
- expect(wrapper.find(MetricEmbed).props()).toEqual(dashboardEmbedProps());
+ expect(wrapper.findComponent(MetricEmbed).props()).toEqual(dashboardEmbedProps());
});
it('adds the monitoring dashboard module', () => {
@@ -127,11 +127,11 @@ describe('Embed Group', () => {
});
it('creates Embed components', () => {
- expect(wrapper.findAll(MetricEmbed)).toHaveLength(2);
+ expect(wrapper.findAllComponents(MetricEmbed)).toHaveLength(2);
});
it('passes the correct props to the Embed components', () => {
- expect(wrapper.findAll(MetricEmbed).wrappers.map((item) => item.props())).toEqual(
+ expect(wrapper.findAllComponents(MetricEmbed).wrappers.map((item) => item.props())).toEqual(
multipleEmbedProps(),
);
});
@@ -147,14 +147,14 @@ describe('Embed Group', () => {
metricsWithDataGetter.mockReturnValue([1]);
mountComponent({ shallow: false, stubs: { MetricEmbed: true } });
- expect(wrapper.find(GlButton).text()).toBe('Hide chart');
+ expect(wrapper.findComponent(GlButton).text()).toBe('Hide chart');
});
it('has a plural label when there are multiple embeds', () => {
metricsWithDataGetter.mockReturnValue([2]);
mountComponent({ shallow: false, stubs: { MetricEmbed: true } });
- expect(wrapper.find(GlButton).text()).toBe('Hide charts');
+ expect(wrapper.findComponent(GlButton).text()).toBe('Hide charts');
});
});
});
diff --git a/spec/frontend/monitoring/components/embeds/metric_embed_spec.js b/spec/frontend/monitoring/components/embeds/metric_embed_spec.js
index f9f1be4f277..beff3da2baf 100644
--- a/spec/frontend/monitoring/components/embeds/metric_embed_spec.js
+++ b/spec/frontend/monitoring/components/embeds/metric_embed_spec.js
@@ -64,7 +64,7 @@ describe('MetricEmbed', () => {
it('shows an empty state when no metrics are present', () => {
expect(wrapper.find('.metrics-embed').exists()).toBe(true);
- expect(wrapper.find(DashboardPanel).exists()).toBe(false);
+ expect(wrapper.findComponent(DashboardPanel).exists()).toBe(false);
});
});
@@ -92,12 +92,12 @@ describe('MetricEmbed', () => {
it('shows a chart when metrics are present', () => {
expect(wrapper.find('.metrics-embed').exists()).toBe(true);
- expect(wrapper.find(DashboardPanel).exists()).toBe(true);
- expect(wrapper.findAll(DashboardPanel).length).toBe(2);
+ expect(wrapper.findComponent(DashboardPanel).exists()).toBe(true);
+ expect(wrapper.findAllComponents(DashboardPanel).length).toBe(2);
});
it('includes groupId with dashboardUrl', () => {
- expect(wrapper.find(DashboardPanel).props('groupId')).toBe(TEST_HOST);
+ expect(wrapper.findComponent(DashboardPanel).props('groupId')).toBe(TEST_HOST);
});
});
});
diff --git a/spec/frontend/monitoring/components/empty_state_spec.js b/spec/frontend/monitoring/components/empty_state_spec.js
index 1ecb101574b..ddefa8c5cd0 100644
--- a/spec/frontend/monitoring/components/empty_state_spec.js
+++ b/spec/frontend/monitoring/components/empty_state_spec.js
@@ -25,8 +25,8 @@ describe('EmptyState', () => {
selectedState: dashboardEmptyStates.LOADING,
});
- expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
- expect(wrapper.find(GlEmptyState).exists()).toBe(false);
+ expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
+ expect(wrapper.findComponent(GlEmptyState).exists()).toBe(false);
});
it('shows gettingStarted state', () => {
diff --git a/spec/frontend/monitoring/components/graph_group_spec.js b/spec/frontend/monitoring/components/graph_group_spec.js
index 31f52f6627b..104263e73e0 100644
--- a/spec/frontend/monitoring/components/graph_group_spec.js
+++ b/spec/frontend/monitoring/components/graph_group_spec.js
@@ -6,10 +6,10 @@ import GraphGroup from '~/monitoring/components/graph_group.vue';
describe('Graph group component', () => {
let wrapper;
- const findGroup = () => wrapper.find({ ref: 'graph-group' });
- const findContent = () => wrapper.find({ ref: 'graph-group-content' });
- const findLoadingIcon = () => wrapper.find(GlLoadingIcon);
- const findCaretIcon = () => wrapper.find(GlIcon);
+ const findGroup = () => wrapper.findComponent({ ref: 'graph-group' });
+ const findContent = () => wrapper.findComponent({ ref: 'graph-group-content' });
+ const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
+ const findCaretIcon = () => wrapper.findComponent(GlIcon);
const findToggleButton = () => wrapper.find('[data-testid="group-toggle-button"]');
const createComponent = (propsData) => {
diff --git a/spec/frontend/monitoring/components/group_empty_state_spec.js b/spec/frontend/monitoring/components/group_empty_state_spec.js
index 1dd2ed4e141..e3cd26b0e48 100644
--- a/spec/frontend/monitoring/components/group_empty_state_spec.js
+++ b/spec/frontend/monitoring/components/group_empty_state_spec.js
@@ -45,7 +45,7 @@ describe('GroupEmptyState', () => {
});
it('passes the expected props to GlEmptyState', () => {
- expect(wrapper.find(GlEmptyState).props()).toMatchSnapshot();
+ expect(wrapper.findComponent(GlEmptyState).props()).toMatchSnapshot();
});
});
});
diff --git a/spec/frontend/monitoring/components/links_section_spec.js b/spec/frontend/monitoring/components/links_section_spec.js
index c9b5aeeecb8..94938e7f459 100644
--- a/spec/frontend/monitoring/components/links_section_spec.js
+++ b/spec/frontend/monitoring/components/links_section_spec.js
@@ -21,7 +21,7 @@ describe('Links Section component', () => {
links,
};
};
- const findLinks = () => wrapper.findAll(GlLink);
+ const findLinks = () => wrapper.findAllComponents(GlLink);
beforeEach(() => {
store = createStore();
diff --git a/spec/frontend/monitoring/components/refresh_button_spec.js b/spec/frontend/monitoring/components/refresh_button_spec.js
index 0e45cc021c5..e00736954a9 100644
--- a/spec/frontend/monitoring/components/refresh_button_spec.js
+++ b/spec/frontend/monitoring/components/refresh_button_spec.js
@@ -15,9 +15,9 @@ describe('RefreshButton', () => {
wrapper = shallowMount(RefreshButton, { store, ...options });
};
- const findRefreshBtn = () => wrapper.find(GlButton);
- const findDropdown = () => wrapper.find(GlDropdown);
- const findOptions = () => findDropdown().findAll(GlDropdownItem);
+ const findRefreshBtn = () => wrapper.findComponent(GlButton);
+ const findDropdown = () => wrapper.findComponent(GlDropdown);
+ const findOptions = () => findDropdown().findAllComponents(GlDropdownItem);
const findOptionAt = (index) => findOptions().at(index);
const expectFetchDataToHaveBeenCalledTimes = (times) => {
diff --git a/spec/frontend/monitoring/components/variables/dropdown_field_spec.js b/spec/frontend/monitoring/components/variables/dropdown_field_spec.js
index 643bbb39f04..012e2e9c3e2 100644
--- a/spec/frontend/monitoring/components/variables/dropdown_field_spec.js
+++ b/spec/frontend/monitoring/components/variables/dropdown_field_spec.js
@@ -27,8 +27,8 @@ describe('Custom variable component', () => {
});
};
- const findDropdown = () => wrapper.find(GlDropdown);
- const findDropdownItems = () => wrapper.findAll(GlDropdownItem);
+ const findDropdown = () => wrapper.findComponent(GlDropdown);
+ const findDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
it('renders dropdown element when all necessary props are passed', () => {
createShallowWrapper();
diff --git a/spec/frontend/monitoring/components/variables_section_spec.js b/spec/frontend/monitoring/components/variables_section_spec.js
index 64b93bd3027..d6f8aac99aa 100644
--- a/spec/frontend/monitoring/components/variables_section_spec.js
+++ b/spec/frontend/monitoring/components/variables_section_spec.js
@@ -24,8 +24,8 @@ describe('Metrics dashboard/variables section component', () => {
});
};
- const findTextInputs = () => wrapper.findAll(TextField);
- const findCustomInputs = () => wrapper.findAll(DropdownField);
+ const findTextInputs = () => wrapper.findAllComponents(TextField);
+ const findCustomInputs = () => wrapper.findAllComponents(DropdownField);
beforeEach(() => {
store = createStore();
diff --git a/spec/frontend/monitoring/pages/panel_new_page_spec.js b/spec/frontend/monitoring/pages/panel_new_page_spec.js
index c89cbc52bcb..fa112fca2db 100644
--- a/spec/frontend/monitoring/pages/panel_new_page_spec.js
+++ b/spec/frontend/monitoring/pages/panel_new_page_spec.js
@@ -41,8 +41,8 @@ describe('monitoring/pages/panel_new_page', () => {
});
};
- const findBackButton = () => wrapper.find(GlButtonStub);
- const findPanelBuilder = () => wrapper.find(DashboardPanelBuilder);
+ const findBackButton = () => wrapper.findComponent(GlButtonStub);
+ const findPanelBuilder = () => wrapper.findComponent(DashboardPanelBuilder);
beforeEach(() => {
store = createStore();
diff --git a/spec/frontend/monitoring/router_spec.js b/spec/frontend/monitoring/router_spec.js
index 7758dd351b7..368bd955fb3 100644
--- a/spec/frontend/monitoring/router_spec.js
+++ b/spec/frontend/monitoring/router_spec.js
@@ -61,8 +61,8 @@ describe('Monitoring router', () => {
currentDashboard,
});
- expect(wrapper.find(DashboardPage).exists()).toBe(true);
- expect(wrapper.find(DashboardPage).find(Dashboard).exists()).toBe(true);
+ expect(wrapper.findComponent(DashboardPage).exists()).toBe(true);
+ expect(wrapper.findComponent(DashboardPage).findComponent(Dashboard).exists()).toBe(true);
});
});
@@ -84,8 +84,8 @@ describe('Monitoring router', () => {
currentDashboard,
});
- expect(wrapper.find(DashboardPage).exists()).toBe(true);
- expect(wrapper.find(DashboardPage).find(Dashboard).exists()).toBe(true);
+ expect(wrapper.findComponent(DashboardPage).exists()).toBe(true);
+ expect(wrapper.findComponent(DashboardPage).findComponent(Dashboard).exists()).toBe(true);
});
});
@@ -100,7 +100,7 @@ describe('Monitoring router', () => {
const wrapper = createWrapper(BASE_PATH, path);
expect(wrapper.vm.$route.params.dashboard).toBe(currentDashboard);
- expect(wrapper.find(PanelNewPage).exists()).toBe(true);
+ expect(wrapper.findComponent(PanelNewPage).exists()).toBe(true);
});
});
});
diff --git a/spec/frontend/monitoring/store/utils_spec.js b/spec/frontend/monitoring/store/utils_spec.js
index c25de8caa95..54f9c59308e 100644
--- a/spec/frontend/monitoring/store/utils_spec.js
+++ b/spec/frontend/monitoring/store/utils_spec.js
@@ -511,10 +511,10 @@ describe('mapToDashboardViewModel', () => {
describe('uniqMetricsId', () => {
[
{ input: { id: 1 }, expected: `${NOT_IN_DB_PREFIX}_1` },
- { input: { metric_id: 2 }, expected: '2_undefined' },
- { input: { metric_id: 2, id: 21 }, expected: '2_21' },
- { input: { metric_id: 22, id: 1 }, expected: '22_1' },
- { input: { metric_id: 'aaa', id: '_a' }, expected: 'aaa__a' },
+ { input: { metricId: 2 }, expected: '2_undefined' },
+ { input: { metricId: 2, id: 21 }, expected: '2_21' },
+ { input: { metricId: 22, id: 1 }, expected: '22_1' },
+ { input: { metricId: 'aaa', id: '_a' }, expected: 'aaa__a' },
].forEach(({ input, expected }) => {
it(`creates unique metric ID with ${JSON.stringify(input)}`, () => {
expect(uniqMetricsId(input)).toEqual(expected);
diff --git a/spec/frontend/nav/components/top_nav_dropdown_menu_spec.js b/spec/frontend/nav/components/top_nav_dropdown_menu_spec.js
index 70df05a2781..6cfbdb16111 100644
--- a/spec/frontend/nav/components/top_nav_dropdown_menu_spec.js
+++ b/spec/frontend/nav/components/top_nav_dropdown_menu_spec.js
@@ -124,7 +124,7 @@ describe('~/nav/components/top_nav_dropdown_menu.vue', () => {
});
it('clicked on link with view', () => {
- expect(primaryLink.props('menuItem').view).toBeTruthy();
+ expect(primaryLink.props('menuItem').view).toBe(TEST_NAV_DATA.views.projects.namespace);
});
it('changes active view', () => {
diff --git a/spec/frontend/notes/components/comment_field_layout_spec.js b/spec/frontend/notes/components/comment_field_layout_spec.js
index d69c2c4adfa..6662492fd81 100644
--- a/spec/frontend/notes/components/comment_field_layout_spec.js
+++ b/spec/frontend/notes/components/comment_field_layout_spec.js
@@ -22,8 +22,8 @@ describe('Comment Field Layout Component', () => {
confidential_issues_docs_path: CONFIDENTIAL_ISSUES_DOCS_PATH,
};
- const findIssuableNoteWarning = () => wrapper.find(NoteableWarning);
- const findEmailParticipantsWarning = () => wrapper.find(EmailParticipantsWarning);
+ const findIssuableNoteWarning = () => wrapper.findComponent(NoteableWarning);
+ const findEmailParticipantsWarning = () => wrapper.findComponent(EmailParticipantsWarning);
const findErrorAlert = () => wrapper.findByTestId('comment-field-alert-container');
const createWrapper = (props = {}, slots = {}) => {
diff --git a/spec/frontend/notes/components/diff_discussion_header_spec.js b/spec/frontend/notes/components/diff_discussion_header_spec.js
index 7878737fd31..5800f68b114 100644
--- a/spec/frontend/notes/components/diff_discussion_header_spec.js
+++ b/spec/frontend/notes/components/diff_discussion_header_spec.js
@@ -1,6 +1,7 @@
-import { mount } from '@vue/test-utils';
+import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
+import { GlAvatar, GlAvatarLink } from '@gitlab/ui';
import diffDiscussionHeader from '~/notes/components/diff_discussion_header.vue';
import createStore from '~/notes/stores';
@@ -15,7 +16,7 @@ describe('diff_discussion_header component', () => {
window.mrTabs = {};
store = createStore();
- wrapper = mount(diffDiscussionHeader, {
+ wrapper = shallowMount(diffDiscussionHeader, {
store,
propsData: { discussion: discussionMock },
});
@@ -25,15 +26,25 @@ describe('diff_discussion_header component', () => {
wrapper.destroy();
});
- it('should render user avatar', async () => {
- const discussion = { ...discussionMock };
- discussion.diff_file = mockDiffFile;
- discussion.diff_discussion = true;
+ describe('Avatar', () => {
+ const firstNoteAuthor = discussionMock.notes[0].author;
+ const findAvatarLink = () => wrapper.findComponent(GlAvatarLink);
+ const findAvatar = () => wrapper.findComponent(GlAvatar);
- wrapper.setProps({ discussion });
+ it('should render user avatar and user avatar link', () => {
+ expect(findAvatar().exists()).toBe(true);
+ expect(findAvatarLink().exists()).toBe(true);
+ });
+
+ it('renders avatar of the first note author', () => {
+ const props = findAvatar().props();
- await nextTick();
- expect(wrapper.find('.user-avatar-link').exists()).toBe(true);
+ expect(props).toMatchObject({
+ src: firstNoteAuthor.avatar_url,
+ alt: firstNoteAuthor.name,
+ size: { default: 24, md: 32 },
+ });
+ });
});
describe('action text', () => {
diff --git a/spec/frontend/notes/components/discussion_actions_spec.js b/spec/frontend/notes/components/discussion_actions_spec.js
index 925dbcc09ec..d16c13d6fd3 100644
--- a/spec/frontend/notes/components/discussion_actions_spec.js
+++ b/spec/frontend/notes/components/discussion_actions_spec.js
@@ -47,9 +47,9 @@ describe('DiscussionActions', () => {
it('renders reply placeholder, resolve discussion button, resolve with issue button and jump to next discussion button', () => {
createComponent();
- expect(wrapper.find(ReplyPlaceholder).exists()).toBe(true);
- expect(wrapper.find(ResolveDiscussionButton).exists()).toBe(true);
- expect(wrapper.find(ResolveWithIssueButton).exists()).toBe(true);
+ expect(wrapper.findComponent(ReplyPlaceholder).exists()).toBe(true);
+ expect(wrapper.findComponent(ResolveDiscussionButton).exists()).toBe(true);
+ expect(wrapper.findComponent(ResolveWithIssueButton).exists()).toBe(true);
});
it('only renders reply placholder if disccusion is not resolvable', () => {
@@ -57,15 +57,15 @@ describe('DiscussionActions', () => {
discussion.resolvable = false;
createComponent({ discussion });
- expect(wrapper.find(ReplyPlaceholder).exists()).toBe(true);
- expect(wrapper.find(ResolveDiscussionButton).exists()).toBe(false);
- expect(wrapper.find(ResolveWithIssueButton).exists()).toBe(false);
+ expect(wrapper.findComponent(ReplyPlaceholder).exists()).toBe(true);
+ expect(wrapper.findComponent(ResolveDiscussionButton).exists()).toBe(false);
+ expect(wrapper.findComponent(ResolveWithIssueButton).exists()).toBe(false);
});
it('does not render resolve with issue button if resolveWithIssuePath is falsy', () => {
createComponent({ resolveWithIssuePath: '' });
- expect(wrapper.find(ResolveWithIssueButton).exists()).toBe(false);
+ expect(wrapper.findComponent(ResolveWithIssueButton).exists()).toBe(false);
});
describe.each`
@@ -82,8 +82,8 @@ describe('DiscussionActions', () => {
});
it(shouldRender ? 'renders resolve buttons' : 'does not render resolve buttons', () => {
- expect(wrapper.find(ResolveDiscussionButton).exists()).toBe(shouldRender);
- expect(wrapper.find(ResolveWithIssueButton).exists()).toBe(shouldRender);
+ expect(wrapper.findComponent(ResolveDiscussionButton).exists()).toBe(shouldRender);
+ expect(wrapper.findComponent(ResolveWithIssueButton).exists()).toBe(shouldRender);
});
});
});
@@ -95,7 +95,7 @@ describe('DiscussionActions', () => {
createComponent({}, { attachTo: document.body });
jest.spyOn(wrapper.vm, '$emit');
- wrapper.find(ReplyPlaceholder).find('textarea').trigger('focus');
+ wrapper.findComponent(ReplyPlaceholder).find('textarea').trigger('focus');
expect(wrapper.vm.$emit).toHaveBeenCalledWith('showReplyForm');
});
@@ -103,7 +103,7 @@ describe('DiscussionActions', () => {
createComponent();
jest.spyOn(wrapper.vm, '$emit');
- wrapper.find(ResolveDiscussionButton).find('button').trigger('click');
+ wrapper.findComponent(ResolveDiscussionButton).find('button').trigger('click');
expect(wrapper.vm.$emit).toHaveBeenCalledWith('resolve');
});
});
diff --git a/spec/frontend/notes/components/discussion_counter_spec.js b/spec/frontend/notes/components/discussion_counter_spec.js
index f016cef18e6..a7e2f1efa09 100644
--- a/spec/frontend/notes/components/discussion_counter_spec.js
+++ b/spec/frontend/notes/components/discussion_counter_spec.js
@@ -47,7 +47,7 @@ describe('DiscussionCounter component', () => {
it('does not render', () => {
wrapper = shallowMount(DiscussionCounter, { store, propsData: { blocksMerge: true } });
- expect(wrapper.find({ ref: 'discussionCounter' }).exists()).toBe(false);
+ expect(wrapper.findComponent({ ref: 'discussionCounter' }).exists()).toBe(false);
});
});
@@ -57,7 +57,7 @@ describe('DiscussionCounter component', () => {
store.dispatch('updateResolvableDiscussionsCounts');
wrapper = shallowMount(DiscussionCounter, { store, propsData: { blocksMerge: true } });
- expect(wrapper.find({ ref: 'discussionCounter' }).exists()).toBe(false);
+ expect(wrapper.findComponent({ ref: 'discussionCounter' }).exists()).toBe(false);
});
});
@@ -77,7 +77,7 @@ describe('DiscussionCounter component', () => {
updateStore();
wrapper = shallowMount(DiscussionCounter, { store, propsData: { blocksMerge: true } });
- expect(wrapper.find({ ref: 'discussionCounter' }).exists()).toBe(true);
+ expect(wrapper.findComponent({ ref: 'discussionCounter' }).exists()).toBe(true);
});
it.each`
@@ -103,7 +103,7 @@ describe('DiscussionCounter component', () => {
updateStore({ resolvable: true, resolved });
wrapper = shallowMount(DiscussionCounter, { store, propsData: { blocksMerge: true } });
- expect(wrapper.findAll(GlButton)).toHaveLength(groupLength);
+ expect(wrapper.findAllComponents(GlButton)).toHaveLength(groupLength);
});
});
diff --git a/spec/frontend/notes/components/discussion_filter_note_spec.js b/spec/frontend/notes/components/discussion_filter_note_spec.js
index ad9a2e898eb..48f5030aa1a 100644
--- a/spec/frontend/notes/components/discussion_filter_note_spec.js
+++ b/spec/frontend/notes/components/discussion_filter_note_spec.js
@@ -31,14 +31,14 @@ describe('DiscussionFilterNote component', () => {
it('emits `dropdownSelect` event with 0 parameter on clicking Show all activity button', () => {
jest.spyOn(eventHub, '$emit').mockImplementation(() => {});
- wrapper.findAll(GlButton).at(0).vm.$emit('click');
+ wrapper.findAllComponents(GlButton).at(0).vm.$emit('click');
expect(eventHub.$emit).toHaveBeenCalledWith('dropdownSelect', 0);
});
it('emits `dropdownSelect` event with 1 parameter on clicking Show comments only button', () => {
jest.spyOn(eventHub, '$emit').mockImplementation(() => {});
- wrapper.findAll(GlButton).at(1).vm.$emit('click');
+ wrapper.findAllComponents(GlButton).at(1).vm.$emit('click');
expect(eventHub.$emit).toHaveBeenCalledWith('dropdownSelect', 1);
});
diff --git a/spec/frontend/notes/components/discussion_notes_spec.js b/spec/frontend/notes/components/discussion_notes_spec.js
index 3506b6ac9f3..1b8b6bec490 100644
--- a/spec/frontend/notes/components/discussion_notes_spec.js
+++ b/spec/frontend/notes/components/discussion_notes_spec.js
@@ -61,13 +61,13 @@ describe('DiscussionNotes', () => {
it('renders an element for each note in the discussion', () => {
createComponent();
const notesCount = discussionMock.notes.length;
- const els = wrapper.findAll(NoteableNote);
+ const els = wrapper.findAllComponents(NoteableNote);
expect(els.length).toBe(notesCount);
});
it('renders one element if replies groupping is enabled', () => {
createComponent({ shouldGroupReplies: true });
- const els = wrapper.findAll(NoteableNote);
+ const els = wrapper.findAllComponents(NoteableNote);
expect(els.length).toBe(1);
});
diff --git a/spec/frontend/notes/components/discussion_reply_placeholder_spec.js b/spec/frontend/notes/components/discussion_reply_placeholder_spec.js
index 3932f818c4e..971e3987929 100644
--- a/spec/frontend/notes/components/discussion_reply_placeholder_spec.js
+++ b/spec/frontend/notes/components/discussion_reply_placeholder_spec.js
@@ -15,7 +15,7 @@ describe('ReplyPlaceholder', () => {
});
};
- const findTextarea = () => wrapper.find({ ref: 'textarea' });
+ const findTextarea = () => wrapper.findComponent({ ref: 'textarea' });
afterEach(() => {
wrapper.destroy();
diff --git a/spec/frontend/notes/components/discussion_resolve_button_spec.js b/spec/frontend/notes/components/discussion_resolve_button_spec.js
index ca0c0ca6de8..17c3523cf48 100644
--- a/spec/frontend/notes/components/discussion_resolve_button_spec.js
+++ b/spec/frontend/notes/components/discussion_resolve_button_spec.js
@@ -28,7 +28,7 @@ describe('resolveDiscussionButton', () => {
});
it('should emit a onClick event on button click', async () => {
- const button = wrapper.find(GlButton);
+ const button = wrapper.findComponent(GlButton);
button.vm.$emit('click');
@@ -39,7 +39,7 @@ describe('resolveDiscussionButton', () => {
});
it('should contain the provided button title', () => {
- const button = wrapper.find(GlButton);
+ const button = wrapper.findComponent(GlButton);
expect(button.text()).toContain(buttonTitle);
});
@@ -52,7 +52,7 @@ describe('resolveDiscussionButton', () => {
},
});
- const button = wrapper.find(GlButton);
+ const button = wrapper.findComponent(GlButton);
expect(button.props('loading')).toEqual(true);
});
@@ -65,7 +65,7 @@ describe('resolveDiscussionButton', () => {
},
});
- const button = wrapper.find(GlButton);
+ const button = wrapper.findComponent(GlButton);
await nextTick();
expect(button.props('loading')).toEqual(false);
diff --git a/spec/frontend/notes/components/discussion_resolve_with_issue_button_spec.js b/spec/frontend/notes/components/discussion_resolve_with_issue_button_spec.js
index 5bc6282db03..71406eeb7b4 100644
--- a/spec/frontend/notes/components/discussion_resolve_with_issue_button_spec.js
+++ b/spec/frontend/notes/components/discussion_resolve_with_issue_button_spec.js
@@ -20,7 +20,7 @@ describe('ResolveWithIssueButton', () => {
});
it('it should have a link with the provided link property as href', () => {
- const button = wrapper.find(GlButton);
+ const button = wrapper.findComponent(GlButton);
expect(button.attributes().href).toBe(url);
});
diff --git a/spec/frontend/notes/components/note_actions/reply_button_spec.js b/spec/frontend/notes/components/note_actions/reply_button_spec.js
index 4993ded365d..20b32b8c178 100644
--- a/spec/frontend/notes/components/note_actions/reply_button_spec.js
+++ b/spec/frontend/notes/components/note_actions/reply_button_spec.js
@@ -15,7 +15,7 @@ describe('ReplyButton', () => {
});
it('emits startReplying on click', () => {
- wrapper.find(GlButton).vm.$emit('click');
+ wrapper.findComponent(GlButton).vm.$emit('click');
expect(wrapper.emitted('startReplying')).toEqual([[]]);
});
diff --git a/spec/frontend/notes/components/note_actions_spec.js b/spec/frontend/notes/components/note_actions_spec.js
index bf5a6b4966a..cbe11c20798 100644
--- a/spec/frontend/notes/components/note_actions_spec.js
+++ b/spec/frontend/notes/components/note_actions_spec.js
@@ -16,7 +16,7 @@ describe('noteActions', () => {
let actions;
let axiosMock;
- const findUserAccessRoleBadge = (idx) => wrapper.findAll(UserAccessRoleBadge).at(idx);
+ const findUserAccessRoleBadge = (idx) => wrapper.findAllComponents(UserAccessRoleBadge).at(idx);
const findUserAccessRoleBadgeText = (idx) => findUserAccessRoleBadge(idx).text().trim();
const mountNoteActions = (propsData, computed) => {
@@ -159,7 +159,7 @@ describe('noteActions', () => {
});
});
- describe('when a user has access to edit an issue', () => {
+ describe('when a user can set metadata of an issue', () => {
const testButtonClickTriggersAction = () => {
axiosMock.onPut(`${TEST_HOST}/api/v4/projects/group/project/issues/1`).reply(() => {
expect(actions.updateAssignees).toHaveBeenCalled();
@@ -176,7 +176,7 @@ describe('noteActions', () => {
});
store.state.noteableData = {
current_user: {
- can_update: true,
+ can_set_issue_metadata: true,
},
};
store.state.userData = userDataMock;
@@ -191,6 +191,31 @@ describe('noteActions', () => {
it('should be possible to unassign the comment author', testButtonClickTriggersAction);
});
+ describe('when a user can update but not set metadata of an issue', () => {
+ beforeEach(() => {
+ wrapper = mountNoteActions(props, {
+ targetType: () => 'issue',
+ });
+ store.state.noteableData = {
+ current_user: {
+ can_update: true,
+ can_set_issue_metadata: false,
+ },
+ };
+ store.state.userData = userDataMock;
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ axiosMock.restore();
+ });
+
+ it('should not be possible to assign or unassign the comment author', () => {
+ const assignUserButton = wrapper.find('[data-testid="assign-user"]');
+ expect(assignUserButton.exists()).toBe(false);
+ });
+ });
+
describe('when a user does not have access to edit an issue', () => {
const testButtonDoesNotRender = () => {
const assignUserButton = wrapper.find('[data-testid="assign-user"]');
@@ -241,7 +266,7 @@ describe('noteActions', () => {
});
it('shows a reply button', () => {
- const replyButton = wrapper.find({ ref: 'replyButton' });
+ const replyButton = wrapper.findComponent({ ref: 'replyButton' });
expect(replyButton.exists()).toBe(true);
});
@@ -256,7 +281,7 @@ describe('noteActions', () => {
});
it('does not show a reply button', () => {
- const replyButton = wrapper.find({ ref: 'replyButton' });
+ const replyButton = wrapper.findComponent({ ref: 'replyButton' });
expect(replyButton.exists()).toBe(false);
});
@@ -270,7 +295,7 @@ describe('noteActions', () => {
});
it('should render the right resolve button title', () => {
- const resolveButton = wrapper.find({ ref: 'resolveButton' });
+ const resolveButton = wrapper.findComponent({ ref: 'resolveButton' });
expect(resolveButton.exists()).toBe(true);
expect(resolveButton.attributes('title')).toBe('Thread stays unresolved');
diff --git a/spec/frontend/notes/components/note_attachment_spec.js b/spec/frontend/notes/components/note_attachment_spec.js
index d47c2beaaf8..24632f8e427 100644
--- a/spec/frontend/notes/components/note_attachment_spec.js
+++ b/spec/frontend/notes/components/note_attachment_spec.js
@@ -4,8 +4,8 @@ import NoteAttachment from '~/notes/components/note_attachment.vue';
describe('Issue note attachment', () => {
let wrapper;
- const findImage = () => wrapper.find({ ref: 'attachmentImage' });
- const findUrl = () => wrapper.find({ ref: 'attachmentUrl' });
+ const findImage = () => wrapper.findComponent({ ref: 'attachmentImage' });
+ const findUrl = () => wrapper.findComponent({ ref: 'attachmentUrl' });
const createComponent = (attachment) => {
wrapper = shallowMount(NoteAttachment, {
diff --git a/spec/frontend/notes/components/note_body_spec.js b/spec/frontend/notes/components/note_body_spec.js
index 0f765a8da87..c2e56d3e7a7 100644
--- a/spec/frontend/notes/components/note_body_spec.js
+++ b/spec/frontend/notes/components/note_body_spec.js
@@ -7,7 +7,6 @@ import NoteAwardsList from '~/notes/components/note_awards_list.vue';
import NoteForm from '~/notes/components/note_form.vue';
import createStore from '~/notes/stores';
import notes from '~/notes/stores/modules/index';
-import { INTERNAL_NOTE_CLASSES } from '~/notes/constants';
import Suggestions from '~/vue_shared/components/markdown/suggestions.vue';
@@ -59,22 +58,10 @@ describe('issue_note_body component', () => {
expect(wrapper.findComponent(NoteAwardsList).exists()).toBe(true);
});
- it('should not have internal note classes', () => {
- expect(wrapper.findByTestId('note-internal-container').classes()).not.toEqual(
- INTERNAL_NOTE_CLASSES,
- );
- });
-
describe('isInternalNote', () => {
beforeEach(() => {
wrapper = createComponent({ props: { isInternalNote: true } });
});
-
- it('should have internal note classes', () => {
- expect(wrapper.findByTestId('note-internal-container').classes()).toEqual(
- INTERNAL_NOTE_CLASSES,
- );
- });
});
describe('isEditing', () => {
@@ -110,12 +97,6 @@ describe('issue_note_body component', () => {
beforeEach(() => {
wrapper.setProps({ isInternalNote: true });
});
-
- it('should not have internal note classes', () => {
- expect(wrapper.findByTestId('note-internal-container').classes()).not.toEqual(
- INTERNAL_NOTE_CLASSES,
- );
- });
});
});
@@ -162,7 +143,7 @@ describe('issue_note_body component', () => {
});
it('passes the correct default placeholder commit message for a suggestion to the suggestions component', () => {
- const commitMessage = wrapper.find(Suggestions).attributes('defaultcommitmessage');
+ const commitMessage = wrapper.findComponent(Suggestions).attributes('defaultcommitmessage');
expect(commitMessage).toBe('branch/pathnameuseruser usertonabc11');
});
diff --git a/spec/frontend/notes/components/note_form_spec.js b/spec/frontend/notes/components/note_form_spec.js
index 252c24d1117..fad04e9063d 100644
--- a/spec/frontend/notes/components/note_form_spec.js
+++ b/spec/frontend/notes/components/note_form_spec.js
@@ -6,6 +6,7 @@ import { getDraft, updateDraft } from '~/lib/utils/autosave';
import NoteForm from '~/notes/components/note_form.vue';
import createStore from '~/notes/stores';
import MarkdownField from '~/vue_shared/components/markdown/field.vue';
+import { AT_WHO_ACTIVE_CLASS } from '~/gfm_auto_complete';
import { noteableDataMock, notesDataMock, discussionMock, note } from '../mock_data';
jest.mock('~/lib/utils/autosave');
@@ -91,7 +92,7 @@ describe('issue_note_form component', () => {
expect(conflictWarning.exists()).toBe(true);
expect(conflictWarning.text().replace(/\s+/g, ' ').trim()).toBe(message);
- expect(conflictWarning.find(GlLink).attributes('href')).toBe('#note_545');
+ expect(conflictWarning.findComponent(GlLink).attributes('href')).toBe('#note_545');
});
});
@@ -133,7 +134,7 @@ describe('issue_note_form component', () => {
it('should link to markdown docs', () => {
const { markdownDocsPath } = notesDataMock;
- const markdownField = wrapper.find(MarkdownField);
+ const markdownField = wrapper.findComponent(MarkdownField);
const markdownFieldProps = markdownField.props();
expect(markdownFieldProps.markdownDocsPath).toBe(markdownDocsPath);
@@ -201,6 +202,21 @@ describe('issue_note_form component', () => {
expect(wrapper.emitted().cancelForm).toHaveLength(1);
});
+ it('will not cancel form if there is an active at-who-active class', async () => {
+ wrapper.setProps({
+ ...props,
+ });
+ await nextTick();
+
+ const textareaEl = wrapper.vm.$refs.textarea;
+ const cancelButton = findCancelButton();
+ textareaEl.classList.add(AT_WHO_ACTIVE_CLASS);
+ cancelButton.vm.$emit('click');
+ await nextTick();
+
+ expect(wrapper.emitted().cancelForm).toBeUndefined();
+ });
+
it('should be possible to update the note', async () => {
wrapper.setProps({
...props,
diff --git a/spec/frontend/notes/components/note_header_spec.js b/spec/frontend/notes/components/note_header_spec.js
index ad2cf1c5a35..43fbc5e26dc 100644
--- a/spec/frontend/notes/components/note_header_spec.js
+++ b/spec/frontend/notes/components/note_header_spec.js
@@ -15,15 +15,15 @@ const actions = {
describe('NoteHeader component', () => {
let wrapper;
- const findActionsWrapper = () => wrapper.find({ ref: 'discussionActions' });
+ const findActionsWrapper = () => wrapper.findComponent({ ref: 'discussionActions' });
const findToggleThreadButton = () => wrapper.findByTestId('thread-toggle');
- const findChevronIcon = () => wrapper.find({ ref: 'chevronIcon' });
- const findActionText = () => wrapper.find({ ref: 'actionText' });
- const findTimestampLink = () => wrapper.find({ ref: 'noteTimestampLink' });
- const findTimestamp = () => wrapper.find({ ref: 'noteTimestamp' });
+ const findChevronIcon = () => wrapper.findComponent({ ref: 'chevronIcon' });
+ const findActionText = () => wrapper.findComponent({ ref: 'actionText' });
+ const findTimestampLink = () => wrapper.findComponent({ ref: 'noteTimestampLink' });
+ const findTimestamp = () => wrapper.findComponent({ ref: 'noteTimestamp' });
const findInternalNoteIndicator = () => wrapper.findByTestId('internalNoteIndicator');
- const findSpinner = () => wrapper.find({ ref: 'spinner' });
- const findAuthorStatus = () => wrapper.find({ ref: 'authorStatus' });
+ 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>"';
@@ -228,7 +228,7 @@ describe('NoteHeader component', () => {
const dispatchEvent = jest.spyOn(wrapper.vm.$refs.authorNameLink, 'dispatchEvent');
- wrapper.find({ ref: 'authorUsernameLink' }).trigger('mouseenter');
+ wrapper.findComponent({ ref: 'authorUsernameLink' }).trigger('mouseenter');
expect(dispatchEvent).toHaveBeenCalledWith(new Event('mouseenter'));
});
@@ -238,7 +238,7 @@ describe('NoteHeader component', () => {
const dispatchEvent = jest.spyOn(wrapper.vm.$refs.authorNameLink, 'dispatchEvent');
- wrapper.find({ ref: 'authorUsernameLink' }).trigger('mouseleave');
+ wrapper.findComponent({ ref: 'authorUsernameLink' }).trigger('mouseleave');
expect(dispatchEvent).toHaveBeenCalledWith(new Event('mouseleave'));
});
@@ -266,8 +266,8 @@ describe('NoteHeader component', () => {
it('toggles hover specific CSS classes on author name link', async () => {
createComponent({ author });
- const authorUsernameLink = wrapper.find({ ref: 'authorUsernameLink' });
- const authorNameLink = wrapper.find({ ref: 'authorNameLink' });
+ const authorUsernameLink = wrapper.findComponent({ ref: 'authorUsernameLink' });
+ const authorNameLink = wrapper.findComponent({ ref: 'authorNameLink' });
authorUsernameLink.trigger('mouseenter');
diff --git a/spec/frontend/notes/components/noteable_discussion_spec.js b/spec/frontend/notes/components/noteable_discussion_spec.js
index 603db56a098..b34305688d9 100644
--- a/spec/frontend/notes/components/noteable_discussion_spec.js
+++ b/spec/frontend/notes/components/noteable_discussion_spec.js
@@ -73,13 +73,13 @@ describe('noteable_discussion component', () => {
expect(wrapper.vm.isReplying).toEqual(false);
- const replyPlaceholder = wrapper.find(ReplyPlaceholder);
+ const replyPlaceholder = wrapper.findComponent(ReplyPlaceholder);
replyPlaceholder.vm.$emit('focus');
await nextTick();
expect(wrapper.vm.isReplying).toEqual(true);
- const noteForm = wrapper.find(NoteForm);
+ const noteForm = wrapper.findComponent(NoteForm);
expect(noteForm.exists()).toBe(true);
@@ -100,11 +100,11 @@ describe('noteable_discussion component', () => {
wrapper.setProps({ discussion: { ...discussionMock, confidential: isNoteInternal } });
await nextTick();
- const replyPlaceholder = wrapper.find(ReplyPlaceholder);
+ const replyPlaceholder = wrapper.findComponent(ReplyPlaceholder);
replyPlaceholder.vm.$emit('focus');
await nextTick();
- expect(wrapper.find(NoteForm).props('saveButtonTitle')).toBe(saveButtonTitle);
+ expect(wrapper.findComponent(NoteForm).props('saveButtonTitle')).toBe(saveButtonTitle);
},
);
@@ -116,7 +116,7 @@ describe('noteable_discussion component', () => {
await nextTick();
- wrapper.find(DiscussionNotes).vm.$emit('startReplying');
+ wrapper.findComponent(DiscussionNotes).vm.$emit('startReplying');
await nextTick();
@@ -139,7 +139,7 @@ describe('noteable_discussion component', () => {
});
it('does not display a button to resolve with issue', () => {
- const button = wrapper.find(ResolveWithIssueButton);
+ const button = wrapper.findComponent(ResolveWithIssueButton);
expect(button.exists()).toBe(false);
});
@@ -159,7 +159,7 @@ describe('noteable_discussion component', () => {
});
it('displays a button to resolve with issue', () => {
- const button = wrapper.find(ResolveWithIssueButton);
+ const button = wrapper.findComponent(ResolveWithIssueButton);
expect(button.exists()).toBe(true);
});
diff --git a/spec/frontend/notes/components/noteable_note_spec.js b/spec/frontend/notes/components/noteable_note_spec.js
index 3350609bb90..e049c5bc0c8 100644
--- a/spec/frontend/notes/components/noteable_note_spec.js
+++ b/spec/frontend/notes/components/noteable_note_spec.js
@@ -285,11 +285,25 @@ describe('issue_note', () => {
await waitForPromises();
expect(alertSpy).not.toHaveBeenCalled();
expect(wrapper.vm.note.note_html).toBe(
- '<p><img src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"></p>\n',
+ '<img src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7">',
);
});
});
+ describe('internal note', () => {
+ it('has internal note class for internal notes', () => {
+ createWrapper({ note: { ...note, confidential: true } });
+
+ expect(wrapper.classes()).toContain('internal-note');
+ });
+
+ it('does not have internal note class for external notes', () => {
+ createWrapper({ note });
+
+ expect(wrapper.classes()).not.toContain('internal-note');
+ });
+ });
+
describe('cancel edit', () => {
beforeEach(() => {
createWrapper();
@@ -357,7 +371,7 @@ describe('issue_note', () => {
createWrapper();
updateActions();
wrapper.findComponent(NoteBody).vm.$emit('handleFormUpdate', params);
- expect(wrapper.emitted('handleUpdateNote')).toBeTruthy();
+ expect(wrapper.emitted('handleUpdateNote')).toHaveLength(1);
});
it('does not stringify empty position', () => {
diff --git a/spec/frontend/notes/components/notes_app_spec.js b/spec/frontend/notes/components/notes_app_spec.js
index 36a68118fa7..d4cb07d97dc 100644
--- a/spec/frontend/notes/components/notes_app_spec.js
+++ b/spec/frontend/notes/components/notes_app_spec.js
@@ -4,7 +4,6 @@ import $ from 'jquery';
import { nextTick } from 'vue';
import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import setWindowLocation from 'helpers/set_window_location_helper';
-import { setTestTimeout } from 'helpers/timeout';
import waitForPromises from 'helpers/wait_for_promises';
import DraftNote from '~/batch_comments/components/draft_note.vue';
import batchComments from '~/batch_comments/stores/modules/batch_comments';
@@ -19,8 +18,6 @@ import '~/behaviors/markdown/render_gfm';
import OrderedLayout from '~/vue_shared/components/ordered_layout.vue';
import * as mockData from '../mock_data';
-setTestTimeout(1000);
-
const TYPE_COMMENT_FORM = 'comment-form';
const TYPE_NOTES_LIST = 'notes-list';
@@ -359,7 +356,7 @@ describe('note_app', () => {
});
it('should listen hashchange event', () => {
- const notesApp = wrapper.find(NotesApp);
+ const notesApp = wrapper.findComponent(NotesApp);
const hash = 'some dummy hash';
jest.spyOn(urlUtility, 'getLocationHash').mockReturnValueOnce(hash);
const setTargetNoteHash = jest.spyOn(notesApp.vm, 'setTargetNoteHash');
@@ -439,7 +436,7 @@ describe('note_app', () => {
});
it('correctly finds only draft comments', () => {
- const drafts = wrapper.findAll(DraftNote).wrappers;
+ const drafts = wrapper.findAllComponents(DraftNote).wrappers;
expect(drafts.map((x) => x.props('draft'))).toEqual(
mockData.draftComments.map(({ note }) => expect.objectContaining({ note })),
diff --git a/spec/frontend/notes/components/sort_discussion_spec.js b/spec/frontend/notes/components/sort_discussion_spec.js
index bde27b7e5fc..8b6e05da3c0 100644
--- a/spec/frontend/notes/components/sort_discussion_spec.js
+++ b/spec/frontend/notes/components/sort_discussion_spec.js
@@ -21,7 +21,7 @@ describe('Sort Discussion component', () => {
});
};
- const findLocalStorageSync = () => wrapper.find(LocalStorageSync);
+ const findLocalStorageSync = () => wrapper.findComponent(LocalStorageSync);
beforeEach(() => {
store = createStore();
diff --git a/spec/frontend/notes/components/timeline_toggle_spec.js b/spec/frontend/notes/components/timeline_toggle_spec.js
index 84fa3008835..cf79416d300 100644
--- a/spec/frontend/notes/components/timeline_toggle_spec.js
+++ b/spec/frontend/notes/components/timeline_toggle_spec.js
@@ -27,7 +27,7 @@ describe('Timeline toggle', () => {
});
};
- const findGlButton = () => wrapper.find(GlButton);
+ const findGlButton = () => wrapper.findComponent(GlButton);
beforeEach(() => {
store = createStore();
diff --git a/spec/frontend/notes/deprecated_notes_spec.js b/spec/frontend/notes/deprecated_notes_spec.js
index 40b124b9029..d5e2a189afe 100644
--- a/spec/frontend/notes/deprecated_notes_spec.js
+++ b/spec/frontend/notes/deprecated_notes_spec.js
@@ -7,7 +7,6 @@ import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import { createSpyObj } from 'helpers/jest_helpers';
import { TEST_HOST } from 'helpers/test_constants';
import waitForPromises from 'helpers/wait_for_promises';
-import { setTestTimeoutOnce } from 'helpers/timeout';
import axios from '~/lib/utils/axios_utils';
import * as urlUtility from '~/lib/utils/url_utility';
@@ -48,7 +47,6 @@ describe.skip('Old Notes (~/deprecated_notes.js)', () => {
// random failures.
// It seems that running tests in parallel increases failure rate.
jest.setTimeout(4000);
- setTestTimeoutOnce(4000);
});
afterEach(async () => {
@@ -510,7 +508,7 @@ describe.skip('Old Notes (~/deprecated_notes.js)', () => {
notes.putEditFormInPlace($el);
- expect(notes.glForm.enableGFM).toBeTruthy();
+ expect(notes.glForm.enableGFM).toBe('');
});
});
@@ -783,21 +781,21 @@ describe.skip('Old Notes (~/deprecated_notes.js)', () => {
const sampleComment = '/wip\n/milestone %1.0\n/merge\n/unassign Merging this';
const hasQuickActions = notes.hasQuickActions(sampleComment);
- expect(hasQuickActions).toBeTruthy();
+ expect(hasQuickActions).toBe(true);
});
it('should return false when comment does NOT begin with a quick action', () => {
const sampleComment = 'Hey, /unassign Merging this';
const hasQuickActions = notes.hasQuickActions(sampleComment);
- expect(hasQuickActions).toBeFalsy();
+ expect(hasQuickActions).toBe(false);
});
it('should return false when comment does NOT have any quick actions', () => {
const sampleComment = 'Looking good, Awesome!';
const hasQuickActions = notes.hasQuickActions(sampleComment);
- expect(hasQuickActions).toBeFalsy();
+ expect(hasQuickActions).toBe(false);
});
});
@@ -887,14 +885,14 @@ describe.skip('Old Notes (~/deprecated_notes.js)', () => {
expect($tempNote.prop('nodeName')).toEqual('LI');
expect($tempNote.attr('id')).toEqual(uniqueId);
- expect($tempNote.hasClass('being-posted')).toBeTruthy();
- expect($tempNote.hasClass('fade-in-half')).toBeTruthy();
+ expect($tempNote.hasClass('being-posted')).toBe(true);
+ expect($tempNote.hasClass('fade-in-half')).toBe(true);
$tempNote.find('.timeline-icon > a, .note-header-info > a').each((i, el) => {
expect(el.getAttribute('href')).toEqual(`/${currentUsername}`);
});
expect($tempNote.find('.timeline-icon .avatar').attr('src')).toEqual(currentUserAvatar);
- expect($tempNote.find('.timeline-content').hasClass('discussion')).toBeFalsy();
+ expect($tempNote.find('.timeline-content').hasClass('discussion')).toBe(false);
expect($tempNoteHeader.find('.d-none.d-sm-inline-block').text().trim()).toEqual(
currentUserFullname,
);
@@ -916,7 +914,7 @@ describe.skip('Old Notes (~/deprecated_notes.js)', () => {
});
expect($tempNote.prop('nodeName')).toEqual('LI');
- expect($tempNote.find('.timeline-content').hasClass('discussion')).toBeTruthy();
+ expect($tempNote.find('.timeline-content').hasClass('discussion')).toBe(true);
});
it('should return a escaped user name', () => {
@@ -954,8 +952,8 @@ describe.skip('Old Notes (~/deprecated_notes.js)', () => {
expect($tempNote.prop('nodeName')).toEqual('LI');
expect($tempNote.attr('id')).toEqual(uniqueId);
- expect($tempNote.hasClass('being-posted')).toBeTruthy();
- expect($tempNote.hasClass('fade-in-half')).toBeTruthy();
+ expect($tempNote.hasClass('being-posted')).toBe(true);
+ expect($tempNote.hasClass('fade-in-half')).toBe(true);
expect($tempNote.find('.timeline-content i').text().trim()).toEqual(sampleCommandDescription);
});
});
diff --git a/spec/frontend/operation_settings/components/metrics_settings_spec.js b/spec/frontend/operation_settings/components/metrics_settings_spec.js
index c1fa1d24a82..21145466016 100644
--- a/spec/frontend/operation_settings/components/metrics_settings_spec.js
+++ b/spec/frontend/operation_settings/components/metrics_settings_spec.js
@@ -105,7 +105,10 @@ describe('operation settings external dashboard component', () => {
it('uses description text', () => {
const description = formGroup.find('small');
- expect(description.text()).not.toBeFalsy();
+ const expectedDescription =
+ "Choose whether to display dashboard metrics in UTC or the user's local timezone.";
+
+ expect(description.text()).toBe(expectedDescription);
});
});
@@ -138,7 +141,10 @@ describe('operation settings external dashboard component', () => {
it('uses description text', () => {
const description = formGroup.find('small');
- expect(description.text()).not.toBeFalsy();
+ const expectedDescription =
+ 'Add a button to the metrics dashboard linking directly to your existing external dashboard.';
+
+ expect(description.text()).toBe(expectedDescription);
});
});
@@ -151,7 +157,6 @@ describe('operation settings external dashboard component', () => {
});
it('defaults to externalDashboardUrl', () => {
- expect(input.attributes().value).toBeTruthy();
expect(input.attributes().value).toBe(externalDashboardUrl);
});
diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/tags_list_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/tags_list_spec.js
index ef6c4a1fa32..b163557618e 100644
--- a/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/tags_list_spec.js
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/tags_list_spec.js
@@ -4,7 +4,6 @@ import { GlEmptyState } from '@gitlab/ui';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import { stripTypenames } from 'helpers/graphql_helpers';
import component from '~/packages_and_registries/container_registry/explorer/components/details_page/tags_list.vue';
import TagsListRow from '~/packages_and_registries/container_registry/explorer/components/details_page/tags_list_row.vue';
@@ -96,8 +95,8 @@ describe('Tags List', () => {
it('binds the correct props', () => {
expect(findRegistryList().props()).toMatchObject({
title: '2 tags',
- pagination: stripTypenames(tagsPageInfo),
- items: stripTypenames(tags),
+ pagination: tagsPageInfo,
+ items: tags,
idProperty: 'name',
});
});
diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/cleanup_status_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/cleanup_status_spec.js
index a5b2b1d7cf8..61503d0f3bf 100644
--- a/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/cleanup_status_spec.js
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/cleanup_status_spec.js
@@ -90,18 +90,26 @@ describe('cleanup_status', () => {
`(
'when the status is $status is $visible that the extra icon is visible',
({ status, visible }) => {
- mountComponent({ status });
+ mountComponent({ status, expirationPolicy: { next_run_at: '2063-04-08T01:44:03Z' } });
expect(findExtraInfoIcon().exists()).toBe(visible);
},
);
+ it(`when the status is ${UNFINISHED_STATUS} & expirationPolicy does not exist the extra icon is not visible`, () => {
+ mountComponent({
+ status: UNFINISHED_STATUS,
+ });
+
+ expect(findExtraInfoIcon().exists()).toBe(false);
+ });
+
it(`has a popover with a learn more link and a time frame for the next run`, () => {
jest.spyOn(Date, 'now').mockImplementation(() => new Date('2063-04-04T00:42:00Z').getTime());
mountComponent({
status: UNFINISHED_STATUS,
- expirationPolicy: { next_run: '2063-04-08T01:44:03Z' },
+ expirationPolicy: { next_run_at: '2063-04-08T01:44:03Z' },
});
expect(findPopover().exists()).toBe(true);
@@ -113,7 +121,7 @@ describe('cleanup_status', () => {
it('id matches popover target attribute', () => {
mountComponent({
status: UNFINISHED_STATUS,
- next_run_at: '2063-04-08T01:44:03Z',
+ expirationPolicy: { next_run_at: '2063-04-08T01:44:03Z' },
});
const id = findExtraInfoIcon().attributes('id');
diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/mock_data.js b/spec/frontend/packages_and_registries/container_registry/explorer/mock_data.js
index f9739509ef9..b11048cd7a2 100644
--- a/spec/frontend/packages_and_registries/container_registry/explorer/mock_data.js
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/mock_data.js
@@ -13,7 +13,7 @@ export const imagesListResponse = [
expirationPolicyCleanupStatus: 'UNSCHEDULED',
project: {
id: 'gid://gitlab/Project/22',
- path: 'gitlab-test',
+ path: 'GITLAB-TEST',
},
},
{
diff --git a/spec/frontend/packages_and_registries/dependency_proxy/app_spec.js b/spec/frontend/packages_and_registries/dependency_proxy/app_spec.js
index f2901148e17..fb50d623543 100644
--- a/spec/frontend/packages_and_registries/dependency_proxy/app_spec.js
+++ b/spec/frontend/packages_and_registries/dependency_proxy/app_spec.js
@@ -50,6 +50,7 @@ describe('DependencyProxyApp', () => {
groupPath: 'gitlab-org',
groupId: dummyGrouptId,
noManifestsIllustration: 'noManifestsIllustration',
+ canClearCache: true,
};
function createComponent({ provide = provideDefaults } = {}) {
@@ -268,6 +269,23 @@ describe('DependencyProxyApp', () => {
'All items in the cache are scheduled for removal.',
);
});
+
+ describe('when user has no permission to clear cache', () => {
+ beforeEach(() => {
+ createComponent({
+ provide: {
+ groupPath: 'gitlab-org',
+ groupId: dummyGrouptId,
+ noManifestsIllustration: 'noManifestsIllustration',
+ canClearCache: false,
+ },
+ });
+ });
+
+ it('does not show the clear cache dropdown list', () => {
+ expect(findClearCacheDropdownList().exists()).toBe(false);
+ });
+ });
});
});
});
diff --git a/spec/frontend/packages_and_registries/infrastructure_registry/components/shared/package_list_row_spec.js b/spec/frontend/packages_and_registries/infrastructure_registry/components/shared/package_list_row_spec.js
index 79c1b18c9f9..721bdd34a4f 100644
--- a/spec/frontend/packages_and_registries/infrastructure_registry/components/shared/package_list_row_spec.js
+++ b/spec/frontend/packages_and_registries/infrastructure_registry/components/shared/package_list_row_spec.js
@@ -128,7 +128,7 @@ describe('packages_list_row', () => {
findDeleteButton().vm.$emit('click');
await nextTick();
- expect(wrapper.emitted('packageToDelete')).toBeTruthy();
+ expect(wrapper.emitted('packageToDelete')).toHaveLength(1);
expect(wrapper.emitted('packageToDelete')[0]).toEqual([packageWithoutTags]);
});
});
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 fdddc131412..61923233d2e 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/package_title_spec.js.snap
+++ b/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/package_title_spec.js.snap
@@ -29,19 +29,25 @@ exports[`PackageTitle renders with tags 1`] = `
<div
class="gl-display-flex gl-align-items-center gl-text-gray-500 gl-mt-3"
>
- <span
+ <div
+ class="gl-display-flex gl-gap-3"
data-testid="sub-header"
>
v
1.0.0
published
<time-ago-tooltip-stub
- class="gl-ml-2"
cssclass=""
time="2020-08-17T14:23:32Z"
tooltipplacement="top"
/>
- </span>
+
+ <package-tags-stub
+ hidelabel="true"
+ tagdisplaylimit="2"
+ tags="[object Object],[object Object],[object Object]"
+ />
+ </div>
</div>
</div>
</div>
@@ -73,15 +79,6 @@ exports[`PackageTitle renders with tags 1`] = `
texttooltip=""
/>
</div>
- <div
- class="gl-display-flex gl-align-items-center gl-mr-5"
- >
- <package-tags-stub
- hidelabel="true"
- tagdisplaylimit="2"
- tags="[object Object],[object Object],[object Object]"
- />
- </div>
</div>
</div>
@@ -121,19 +118,21 @@ exports[`PackageTitle renders without tags 1`] = `
<div
class="gl-display-flex gl-align-items-center gl-text-gray-500 gl-mt-3"
>
- <span
+ <div
+ class="gl-display-flex gl-gap-3"
data-testid="sub-header"
>
v
1.0.0
published
<time-ago-tooltip-stub
- class="gl-ml-2"
cssclass=""
time="2020-08-17T14:23:32Z"
tooltipplacement="top"
/>
- </span>
+
+ <!---->
+ </div>
</div>
</div>
</div>
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/pypi_installation_spec.js.snap b/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/pypi_installation_spec.js.snap
index 06ae8645101..92c2cd90568 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/pypi_installation_spec.js.snap
+++ b/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/pypi_installation_spec.js.snap
@@ -2,19 +2,161 @@
exports[`PypiInstallation renders all the messages 1`] = `
<div>
- <installation-title-stub
- options="[object Object]"
- packagetype="pypi"
- />
+ <div
+ class="gl-display-flex gl-justify-content-space-between gl-align-items-center"
+ >
+ <h3
+ class="gl-font-lg"
+ >
+ Installation
+ </h3>
+
+ <div>
+ <div
+ class="dropdown b-dropdown gl-new-dropdown btn-group"
+ id="__BVID__27"
+ lazy=""
+ >
+ <!---->
+ <button
+ aria-expanded="false"
+ aria-haspopup="true"
+ class="btn dropdown-toggle btn-default btn-md gl-button gl-dropdown-toggle"
+ id="__BVID__27__BV_toggle_"
+ type="button"
+ >
+ <!---->
+
+ <!---->
+
+ <span
+ class="gl-new-dropdown-button-text"
+ >
+ Show PyPi commands
+ </span>
+
+ <svg
+ aria-hidden="true"
+ class="gl-button-icon dropdown-chevron gl-icon s16"
+ data-testid="chevron-down-icon"
+ role="img"
+ >
+ <use
+ href="#chevron-down"
+ />
+ </svg>
+ </button>
+ <ul
+ aria-labelledby="__BVID__27__BV_toggle_"
+ class="dropdown-menu"
+ role="menu"
+ tabindex="-1"
+ >
+ <!---->
+ </ul>
+ </div>
+ </div>
+ </div>
- <code-instruction-stub
- copytext="Copy Pip command"
- data-testid="pip-command"
- instruction="pip install @gitlab-org/package-15 --extra-index-url http://__token__:<your_personal_token>@gdk.test:3000/api/v4/projects/1/packages/pypi/simple"
- label="Pip Command"
- trackingaction="copy_pip_install_command"
- trackinglabel="code_instruction"
- />
+ <fieldset
+ aria-describedby="installation-pip-command-group__BV_description_"
+ class="form-group gl-form-group"
+ id="installation-pip-command-group"
+ >
+ <legend
+ class="bv-no-focus-ring col-form-label pt-0 col-form-label"
+ id="installation-pip-command-group__BV_label_"
+ tabindex="-1"
+ >
+
+
+
+ <!---->
+
+ <!---->
+ </legend>
+ <div
+ aria-labelledby="installation-pip-command-group__BV_label_"
+ class="bv-no-focus-ring"
+ role="group"
+ tabindex="-1"
+ >
+ <div
+ data-testid="pip-command"
+ id="installation-pip-command"
+ >
+ <label
+ for="instruction-input_5"
+ >
+ Pip Command
+ </label>
+
+ <div
+ class="gl-mb-3"
+ >
+ <div
+ class="input-group gl-mb-3"
+ >
+ <input
+ class="form-control gl-font-monospace"
+ data-testid="instruction-input"
+ id="instruction-input_5"
+ readonly="readonly"
+ type="text"
+ />
+
+ <span
+ class="input-group-append"
+ data-testid="instruction-button"
+ >
+ <button
+ aria-label="Copy Pip command"
+ aria-live="polite"
+ class="btn input-group-text btn-default btn-md gl-button btn-default-secondary btn-icon"
+ data-clipboard-handle-tooltip="false"
+ data-clipboard-text="pip install @gitlab-org/package-15 --extra-index-url http://__token__:<your_personal_token>@gdk.test:3000/api/v4/projects/1/packages/pypi/simple"
+ id="clipboard-button-6"
+ title="Copy Pip command"
+ type="button"
+ >
+ <!---->
+
+ <svg
+ aria-hidden="true"
+ class="gl-button-icon gl-icon s16"
+ data-testid="copy-to-clipboard-icon"
+ role="img"
+ >
+ <use
+ href="#copy-to-clipboard"
+ />
+ </svg>
+
+ <!---->
+ </button>
+ </span>
+ </div>
+ </div>
+ </div>
+ <!---->
+ <!---->
+ <small
+ class="form-text text-muted"
+ id="installation-pip-command-group__BV_description_"
+ tabindex="-1"
+ >
+ You will need a
+ <a
+ class="gl-link"
+ data-testid="access-token-link"
+ href="/help/user/profile/personal_access_tokens"
+ >
+ personal access token
+ </a>
+ .
+ </small>
+ </div>
+ </fieldset>
<h3
class="gl-font-lg"
@@ -30,25 +172,33 @@ exports[`PypiInstallation renders all the messages 1`] = `
file.
</p>
- <code-instruction-stub
- copytext="Copy .pypirc content"
+ <div
data-testid="pypi-setup-content"
- instruction="[gitlab]
+ >
+ <!---->
+
+ <div>
+ <pre
+ class="gl-font-monospace"
+ data-testid="multiline-instruction"
+ >
+ [gitlab]
repository = http://gdk.test:3000/api/v4/projects/1/packages/pypi
username = __token__
-password = <your personal access token>"
- label=""
- multiline="true"
- trackingaction="copy_pypi_setup_command"
- trackinglabel="code_instruction"
- />
+password = &lt;your personal access token&gt;
+ </pre>
+ </div>
+ </div>
For more information on the PyPi registry,
- <gl-link-stub
+ <a
+ class="gl-link"
+ data-testid="pypi-docs-link"
href="/help/user/packages/pypi_repository/index"
+ rel="noopener"
target="_blank"
>
see the documentation
- </gl-link-stub>
+ </a>
.
</div>
`;
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/package_files_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/package_files_spec.js
index 0447ead0830..529a6a22ddf 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/details/package_files_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/details/package_files_spec.js
@@ -1,6 +1,6 @@
-import { GlDropdown, GlButton } from '@gitlab/ui';
+import { GlDropdown, GlButton, GlFormCheckbox } from '@gitlab/ui';
import { nextTick } from 'vue';
-import stubChildren from 'helpers/stub_children';
+import { stubComponent } from 'helpers/stub_component';
import { mountExtended, extendedWrapper } from 'helpers/vue_test_utils_helper';
import { packageFiles as packageFilesMock } from 'jest/packages_and_registries/package_registry/mock_data';
import PackageFiles from '~/packages_and_registries/package_registry/components/details/package_files.vue';
@@ -11,6 +11,7 @@ describe('Package Files', () => {
let wrapper;
const findAllRows = () => wrapper.findAllByTestId('file-row');
+ const findDeleteSelectedButton = () => wrapper.findByTestId('delete-selected');
const findFirstRow = () => extendedWrapper(findAllRows().at(0));
const findSecondRow = () => extendedWrapper(findAllRows().at(1));
const findFirstRowDownloadLink = () => findFirstRow().findByTestId('download-link');
@@ -22,19 +23,27 @@ describe('Package Files', () => {
const findActionMenuDelete = () => findFirstActionMenu().findByTestId('delete-file');
const findFirstToggleDetailsButton = () => findFirstRow().findComponent(GlButton);
const findFirstRowShaComponent = (id) => wrapper.findByTestId(id);
+ const findCheckAllCheckbox = () => wrapper.findByTestId('package-files-checkbox-all');
+ const findAllRowCheckboxes = () => wrapper.findAllByTestId('package-files-checkbox');
const files = packageFilesMock();
const [file] = files;
- const createComponent = ({ packageFiles = [file], canDelete = true } = {}) => {
+ const createComponent = ({
+ packageFiles = [file],
+ isLoading = false,
+ canDelete = true,
+ stubs,
+ } = {}) => {
wrapper = mountExtended(PackageFiles, {
propsData: {
canDelete,
+ isLoading,
packageFiles,
},
stubs: {
- ...stubChildren(PackageFiles),
- GlTableLite: false,
+ GlTable: false,
+ ...stubs,
},
});
};
@@ -157,43 +166,170 @@ describe('Package Files', () => {
expect(findSecondRowCommitLink().exists()).toBe(false);
});
});
+ });
- describe('action menu', () => {
- describe('when the user can delete', () => {
- it('exists', () => {
- createComponent();
+ describe('action menu', () => {
+ describe('when the user can delete', () => {
+ it('exists', () => {
+ createComponent();
- expect(findFirstActionMenu().exists()).toBe(true);
- });
+ expect(findFirstActionMenu().exists()).toBe(true);
+ expect(findFirstActionMenu().props('icon')).toBe('ellipsis_v');
+ expect(findFirstActionMenu().props('textSrOnly')).toBe(true);
+ expect(findFirstActionMenu().props('text')).toMatchInterpolatedText('More actions');
+ });
- describe('menu items', () => {
- describe('delete file', () => {
- it('exists', () => {
- createComponent();
+ describe('menu items', () => {
+ describe('delete file', () => {
+ it('exists', () => {
+ createComponent();
- expect(findActionMenuDelete().exists()).toBe(true);
- });
+ expect(findActionMenuDelete().exists()).toBe(true);
+ });
- it('emits a delete event when clicked', () => {
- createComponent();
+ it('emits a delete event when clicked', async () => {
+ createComponent();
- findActionMenuDelete().vm.$emit('click');
+ await findActionMenuDelete().trigger('click');
- const [[{ id }]] = wrapper.emitted('delete-file');
- expect(id).toBe(file.id);
- });
+ const [[items]] = wrapper.emitted('delete-files');
+ const [{ id }] = items;
+ expect(id).toBe(file.id);
});
});
});
+ });
+
+ describe('when the user can not delete', () => {
+ const canDelete = false;
+
+ it('does not exist', () => {
+ createComponent({ canDelete });
+
+ expect(findFirstActionMenu().exists()).toBe(false);
+ });
+ });
+ });
+
+ describe('multi select', () => {
+ describe('when user can delete', () => {
+ it('delete selected button exists & is disabled', () => {
+ createComponent();
+
+ expect(findDeleteSelectedButton().exists()).toBe(true);
+ expect(findDeleteSelectedButton().text()).toMatchInterpolatedText('Delete selected');
+ expect(findDeleteSelectedButton().props('disabled')).toBe(true);
+ });
+
+ it('delete selected button exists & is disabled when isLoading prop is true', () => {
+ createComponent({ isLoading: true });
+
+ expect(findDeleteSelectedButton().props('disabled')).toBe(true);
+ });
+
+ it('checkboxes to select file are visible', () => {
+ createComponent({ packageFiles: files });
+
+ expect(findCheckAllCheckbox().exists()).toBe(true);
+ expect(findAllRowCheckboxes()).toHaveLength(2);
+ });
+
+ it('selecting a checkbox enables delete selected button', async () => {
+ createComponent();
+
+ const first = findAllRowCheckboxes().at(0);
+
+ await first.setChecked(true);
+
+ expect(findDeleteSelectedButton().props('disabled')).toBe(false);
+ });
+
+ describe('select all checkbox', () => {
+ it('will toggle between selecting all and deselecting all files', async () => {
+ const getChecked = () => findAllRowCheckboxes().filter((x) => x.element.checked === true);
+
+ createComponent({ packageFiles: files });
+
+ expect(getChecked()).toHaveLength(0);
+
+ await findCheckAllCheckbox().setChecked(true);
- describe('when the user can not delete', () => {
- const canDelete = false;
+ expect(getChecked()).toHaveLength(files.length);
- it('does not exist', () => {
- createComponent({ canDelete });
+ await findCheckAllCheckbox().setChecked(false);
- expect(findFirstActionMenu().exists()).toBe(false);
+ expect(getChecked()).toHaveLength(0);
});
+
+ it('will toggle the indeterminate state when some but not all files are selected', async () => {
+ const expectIndeterminateState = (state) =>
+ expect(findCheckAllCheckbox().props('indeterminate')).toBe(state);
+
+ createComponent({
+ packageFiles: files,
+ stubs: { GlFormCheckbox: stubComponent(GlFormCheckbox, { props: ['indeterminate'] }) },
+ });
+
+ expectIndeterminateState(false);
+
+ await findSecondRow().trigger('click');
+
+ expectIndeterminateState(true);
+
+ await findSecondRow().trigger('click');
+
+ expectIndeterminateState(false);
+
+ findCheckAllCheckbox().trigger('click');
+
+ expectIndeterminateState(false);
+
+ await findSecondRow().trigger('click');
+
+ expectIndeterminateState(true);
+ });
+ });
+
+ it('emits a delete event when selected', async () => {
+ createComponent();
+
+ const first = findAllRowCheckboxes().at(0);
+
+ await first.setChecked(true);
+
+ await findDeleteSelectedButton().trigger('click');
+
+ const [[items]] = wrapper.emitted('delete-files');
+ const [{ id }] = items;
+ expect(id).toBe(file.id);
+ });
+
+ it('emits delete event with both items when all are selected', async () => {
+ createComponent({ packageFiles: files });
+
+ await findCheckAllCheckbox().setChecked(true);
+
+ await findDeleteSelectedButton().trigger('click');
+
+ const [[items]] = wrapper.emitted('delete-files');
+ expect(items).toHaveLength(2);
+ });
+ });
+
+ describe('when user cannot delete', () => {
+ const canDelete = false;
+
+ it('delete selected button does not exist', () => {
+ createComponent({ canDelete });
+
+ expect(findDeleteSelectedButton().exists()).toBe(false);
+ });
+
+ it('checkboxes to select file are not visible', () => {
+ createComponent({ packageFiles: files, canDelete });
+
+ expect(findCheckAllCheckbox().exists()).toBe(false);
+ expect(findAllRowCheckboxes()).toHaveLength(0);
});
});
});
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/package_history_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/package_history_spec.js
index f4e6d43812d..ec2e833552a 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/details/package_history_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/details/package_history_spec.js
@@ -17,6 +17,12 @@ import HistoryItem from '~/vue_shared/components/registry/history_item.vue';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import waitForPromises from 'helpers/wait_for_promises';
import getPackagePipelines from '~/packages_and_registries/package_registry/graphql/queries/get_package_pipelines.query.graphql';
+import Tracking from '~/tracking';
+import {
+ TRACKING_ACTION_CLICK_PIPELINE_LINK,
+ TRACKING_ACTION_CLICK_COMMIT_LINK,
+ TRACKING_LABEL_PACKAGE_HISTORY,
+} from '~/packages_and_registries/package_registry/constants';
Vue.use(VueApollo);
@@ -181,7 +187,6 @@ describe('Package History', () => {
it('link', () => {
const linkElement = findElementLink(element);
const exist = Boolean(link);
-
expect(linkElement.exists()).toBe(exist);
if (exist) {
expect(linkElement.attributes('href')).toBe(link);
@@ -189,4 +194,29 @@ describe('Package History', () => {
});
},
);
+ describe('tracking', () => {
+ let eventSpy;
+ const category = 'UI::Packages';
+
+ beforeEach(() => {
+ mountComponent();
+ eventSpy = jest.spyOn(Tracking, 'event');
+ });
+
+ it('clicking pipeline link tracks the right action', () => {
+ wrapper.vm.trackPipelineClick();
+ expect(eventSpy).toHaveBeenCalledWith(category, TRACKING_ACTION_CLICK_PIPELINE_LINK, {
+ category,
+ label: TRACKING_LABEL_PACKAGE_HISTORY,
+ });
+ });
+
+ it('clicking commit link tracks the right action', () => {
+ wrapper.vm.trackCommitClick();
+ expect(eventSpy).toHaveBeenCalledWith(category, TRACKING_ACTION_CLICK_COMMIT_LINK, {
+ category,
+ label: TRACKING_LABEL_PACKAGE_HISTORY,
+ });
+ });
+ });
});
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 d306f7834f0..37416dcd4e7 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
@@ -22,16 +22,21 @@ const packageWithTags = {
packageFiles: { nodes: packageFiles() },
};
+const defaultProvide = {
+ isGroupPage: false,
+};
+
describe('PackageTitle', () => {
let wrapper;
- async function createComponent(packageEntity = packageWithTags) {
+ async function createComponent(packageEntity = packageWithTags, provide = defaultProvide) {
wrapper = shallowMountExtended(PackageTitle, {
propsData: { packageEntity },
stubs: {
TitleArea,
GlSprintf,
},
+ provide,
directives: {
GlResizeObserver: createMockDirective(),
},
@@ -199,11 +204,22 @@ describe('PackageTitle', () => {
expect(findPipelineProject().exists()).toBe(false);
});
- it('correctly shows the pipeline project if there is one', async () => {
+ it('does not display the pipeline project on project page even if it exists', async () => {
await createComponent({
...packageData(),
pipelines: { nodes: packagePipelines() },
});
+ expect(findPipelineProject().exists()).toBe(false);
+ });
+
+ it('correctly shows the pipeline project on group page if there is one', async () => {
+ await createComponent(
+ {
+ ...packageData(),
+ pipelines: { nodes: packagePipelines() },
+ },
+ { isGroupPage: true },
+ );
expect(findPipelineProject().props()).toMatchObject({
text: packagePipelines()[0].project.name,
icon: 'review-list',
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/pypi_installation_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/pypi_installation_spec.js
index f2fef6436a6..20acb0872e5 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/details/pypi_installation_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/details/pypi_installation_spec.js
@@ -1,9 +1,10 @@
-import { GlLink, GlSprintf } from '@gitlab/ui';
-import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { GlSprintf } from '@gitlab/ui';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
import { packageData } from 'jest/packages_and_registries/package_registry/mock_data';
import InstallationTitle from '~/packages_and_registries/package_registry/components/details/installation_title.vue';
import PypiInstallation from '~/packages_and_registries/package_registry/components/details/pypi_installation.vue';
import {
+ PERSONAL_ACCESS_TOKEN_HELP_URL,
PACKAGE_TYPE_PYPI,
TRACKING_ACTION_COPY_PIP_INSTALL_COMMAND,
TRACKING_ACTION_COPY_PYPI_SETUP_COMMAND,
@@ -24,11 +25,12 @@ password = <your personal access token>`;
const pipCommand = () => wrapper.findByTestId('pip-command');
const setupInstruction = () => wrapper.findByTestId('pypi-setup-content');
+ const findAccessTokenLink = () => wrapper.findByTestId('access-token-link');
const findInstallationTitle = () => wrapper.findComponent(InstallationTitle);
- const findSetupDocsLink = () => wrapper.findComponent(GlLink);
+ const findSetupDocsLink = () => wrapper.findByTestId('pypi-docs-link');
function createComponent() {
- wrapper = shallowMountExtended(PypiInstallation, {
+ wrapper = mountExtended(PypiInstallation, {
propsData: {
packageEntity,
},
@@ -78,6 +80,12 @@ password = <your personal access token>`;
});
});
+ it('has a link to personal access token docs', () => {
+ expect(findAccessTokenLink().attributes()).toMatchObject({
+ href: PERSONAL_ACCESS_TOKEN_HELP_URL,
+ });
+ });
+
it('has a link to the docs', () => {
expect(findSetupDocsLink().attributes()).toMatchObject({
href: PYPI_HELP_PATH,
diff --git a/spec/frontend/packages_and_registries/package_registry/components/list/package_list_row_spec.js b/spec/frontend/packages_and_registries/package_registry/components/list/package_list_row_spec.js
index c16c09b5326..eb1e76377ff 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/list/package_list_row_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/list/package_list_row_spec.js
@@ -123,7 +123,7 @@ describe('packages_list_row', () => {
findDeleteDropdown().vm.$emit('click');
await nextTick();
- expect(wrapper.emitted('packageToDelete')).toBeTruthy();
+ expect(wrapper.emitted('packageToDelete')).toHaveLength(1);
expect(wrapper.emitted('packageToDelete')[0]).toEqual([packageWithoutTags]);
});
});
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 d40feee582f..22236424e6a 100644
--- a/spec/frontend/packages_and_registries/package_registry/mock_data.js
+++ b/spec/frontend/packages_and_registries/package_registry/mock_data.js
@@ -141,6 +141,7 @@ export const packageData = (extend) => ({
});
export const conanMetadata = () => ({
+ __typename: 'ConanMetadata',
id: 'conan-1',
packageChannel: 'stable',
packageUsername: 'gitlab-org+gitlab-test',
@@ -148,9 +149,8 @@ export const conanMetadata = () => ({
recipePath: 'package-8/1.0.0/gitlab-org+gitlab-test/stable',
});
-const conanMetadataQuery = () => ({ ...conanMetadata(), __typename: 'ConanMetadata' });
-
export const composerMetadata = () => ({
+ __typename: 'ComposerMetadata',
targetSha: 'b83d6e391c22777fca1ed3012fce84f633d7fed0',
composerJson: {
license: 'MIT',
@@ -158,19 +158,14 @@ export const composerMetadata = () => ({
},
});
-const composerMetadataQuery = () => ({
- ...composerMetadata(),
- __typename: 'ComposerMetadata',
-});
-
export const pypiMetadata = () => ({
+ __typename: 'PypiMetadata',
id: 'pypi-1',
requiredPython: '1.0.0',
});
-const pypiMetadataQuery = () => ({ ...pypiMetadata(), __typename: 'PypiMetadata' });
-
export const mavenMetadata = () => ({
+ __typename: 'MavenMetadata',
id: 'maven-1',
appName: 'appName',
appGroup: 'appGroup',
@@ -178,23 +173,20 @@ export const mavenMetadata = () => ({
path: 'path',
});
-const mavenMetadataQuery = () => ({ ...mavenMetadata(), __typename: 'MavenMetadata' });
-
export const nugetMetadata = () => ({
+ __typename: 'NugetMetadata',
id: 'nuget-1',
iconUrl: 'iconUrl',
licenseUrl: 'licenseUrl',
projectUrl: 'projectUrl',
});
-const nugetMetadataQuery = () => ({ ...nugetMetadata(), __typename: 'NugetMetadata' });
-
const packageTypeMetadataQueryMapping = {
- CONAN: conanMetadataQuery,
- COMPOSER: composerMetadataQuery,
- PYPI: pypiMetadataQuery,
- MAVEN: mavenMetadataQuery,
- NUGET: nugetMetadataQuery,
+ CONAN: conanMetadata,
+ COMPOSER: composerMetadata,
+ PYPI: pypiMetadata,
+ MAVEN: mavenMetadata,
+ NUGET: nugetMetadata,
};
export const pagination = (extend) => ({
@@ -221,6 +213,7 @@ export const packageDetailsQuery = (extendPackage) => ({
id: '1',
path: 'projectPath',
name: 'gitlab-test',
+ fullPath: 'gitlab-test',
},
tags: {
nodes: packageTags(),
@@ -231,6 +224,9 @@ export const packageDetailsQuery = (extendPackage) => ({
__typename: 'PipelineConnection',
},
packageFiles: {
+ pageInfo: {
+ hasNextPage: true,
+ },
nodes: packageFiles(),
__typename: 'PackageFileConnection',
},
@@ -310,16 +306,16 @@ export const packageDestroyMutationError = () => ({
],
});
-export const packageDestroyFileMutation = () => ({
+export const packageDestroyFilesMutation = () => ({
data: {
- destroyPackageFile: {
+ destroyPackageFiles: {
errors: [],
},
},
});
-export const packageDestroyFileMutationError = () => ({
+export const packageDestroyFilesMutationError = () => ({
data: {
- destroyPackageFile: null,
+ destroyPackageFiles: null,
},
errors: [
{
@@ -331,7 +327,7 @@ export const packageDestroyFileMutationError = () => ({
column: 3,
},
],
- path: ['destroyPackageFile'],
+ path: ['destroyPackageFiles'],
},
],
});
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 3cadb001c58..de78e6bb87b 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
@@ -22,6 +22,8 @@ import {
PACKAGE_TYPE_COMPOSER,
DELETE_PACKAGE_FILE_SUCCESS_MESSAGE,
DELETE_PACKAGE_FILE_ERROR_MESSAGE,
+ DELETE_PACKAGE_FILES_SUCCESS_MESSAGE,
+ DELETE_PACKAGE_FILES_ERROR_MESSAGE,
PACKAGE_TYPE_NUGET,
PACKAGE_TYPE_MAVEN,
PACKAGE_TYPE_CONAN,
@@ -29,7 +31,7 @@ import {
PACKAGE_TYPE_NPM,
} from '~/packages_and_registries/package_registry/constants';
-import destroyPackageFileMutation from '~/packages_and_registries/package_registry/graphql/mutations/destroy_package_file.mutation.graphql';
+import destroyPackageFilesMutation from '~/packages_and_registries/package_registry/graphql/mutations/destroy_package_files.mutation.graphql';
import getPackageDetails from '~/packages_and_registries/package_registry/graphql/queries/get_package_details.query.graphql';
import {
packageDetailsQuery,
@@ -38,8 +40,8 @@ import {
dependencyLinks,
emptyPackageDetailsQuery,
packageFiles,
- packageDestroyFileMutation,
- packageDestroyFileMutationError,
+ packageDestroyFilesMutation,
+ packageDestroyFilesMutationError,
} from '../mock_data';
jest.mock('~/flash');
@@ -58,6 +60,7 @@ describe('PackagesApp', () => {
emptyListIllustration: 'svgPath',
projectListUrl: 'projectListUrl',
groupListUrl: 'groupListUrl',
+ isGroupPage: false,
breadCrumbState,
};
@@ -65,14 +68,14 @@ describe('PackagesApp', () => {
function createComponent({
resolver = jest.fn().mockResolvedValue(packageDetailsQuery()),
- fileDeleteMutationResolver = jest.fn().mockResolvedValue(packageDestroyFileMutation()),
+ filesDeleteMutationResolver = jest.fn().mockResolvedValue(packageDestroyFilesMutation()),
routeId = '1',
} = {}) {
Vue.use(VueApollo);
const requestHandlers = [
[getPackageDetails, resolver],
- [destroyPackageFileMutation, fileDeleteMutationResolver],
+ [destroyPackageFilesMutation, filesDeleteMutationResolver],
];
apolloProvider = createMockApollo(requestHandlers);
@@ -110,6 +113,7 @@ describe('PackagesApp', () => {
const findDeleteButton = () => wrapper.findByTestId('delete-package');
const findPackageFiles = () => wrapper.findComponent(PackageFiles);
const findDeleteFileModal = () => wrapper.findByTestId('delete-file-modal');
+ const findDeleteFilesModal = () => wrapper.findByTestId('delete-files-modal');
const findVersionRows = () => wrapper.findAllComponents(VersionRow);
const noVersionsMessage = () => wrapper.findByTestId('no-versions-message');
const findDependenciesCountBadge = () => wrapper.findComponent(GlBadge);
@@ -288,6 +292,7 @@ describe('PackagesApp', () => {
expect(findPackageFiles().props('packageFiles')[0]).toMatchObject(expectedFile);
expect(findPackageFiles().props('canDelete')).toBe(packageData().canDestroy);
+ expect(findPackageFiles().props('isLoading')).toEqual(false);
});
it('does not render the package files table when the package is composer', async () => {
@@ -305,24 +310,69 @@ describe('PackagesApp', () => {
describe('deleting a file', () => {
const [fileToDelete] = packageFiles();
- const doDeleteFile = () => {
- findPackageFiles().vm.$emit('delete-file', fileToDelete);
+ const doDeleteFile = async () => {
+ findPackageFiles().vm.$emit('delete-files', [fileToDelete]);
findDeleteFileModal().vm.$emit('primary');
return waitForPromises();
};
- it('opens a confirmation modal', async () => {
+ it('opens delete file confirmation modal', async () => {
createComponent();
await waitForPromises();
- findPackageFiles().vm.$emit('delete-file', fileToDelete);
+ const showDeleteFileSpy = jest.spyOn(wrapper.vm.$refs.deleteFileModal, 'show');
+ const showDeletePackageSpy = jest.spyOn(wrapper.vm.$refs.deleteModal, 'show');
+
+ findPackageFiles().vm.$emit('delete-files', [fileToDelete]);
+
+ expect(showDeletePackageSpy).not.toBeCalled();
+ expect(showDeleteFileSpy).toBeCalled();
+ });
+
+ it('when its the only file opens delete package confirmation modal', async () => {
+ const [packageFile] = packageFiles();
+ const resolver = jest.fn().mockResolvedValue(
+ packageDetailsQuery({
+ packageFiles: {
+ pageInfo: {
+ hasNextPage: false,
+ },
+ nodes: [packageFile],
+ __typename: 'PackageFileConnection',
+ },
+ }),
+ );
+
+ createComponent({
+ resolver,
+ });
+
+ await waitForPromises();
+
+ const showDeleteFileSpy = jest.spyOn(wrapper.vm.$refs.deleteFileModal, 'show');
+ const showDeletePackageSpy = jest.spyOn(wrapper.vm.$refs.deleteModal, 'show');
+
+ findPackageFiles().vm.$emit('delete-files', [fileToDelete]);
+
+ expect(showDeletePackageSpy).toBeCalled();
+ expect(showDeleteFileSpy).not.toBeCalled();
+ });
+
+ it('confirming on the modal sets the loading state', async () => {
+ createComponent();
+
+ await waitForPromises();
+
+ findPackageFiles().vm.$emit('delete-files', [fileToDelete]);
+
+ findDeleteFileModal().vm.$emit('primary');
await nextTick();
- expect(findDeleteFileModal().exists()).toBe(true);
+ expect(findPackageFiles().props('isLoading')).toEqual(true);
});
it('confirming on the modal deletes the file and shows a success message', async () => {
@@ -344,7 +394,7 @@ describe('PackagesApp', () => {
describe('errors', () => {
it('shows an error when the mutation request fails', async () => {
- createComponent({ fileDeleteMutationResolver: jest.fn().mockRejectedValue() });
+ createComponent({ filesDeleteMutationResolver: jest.fn().mockRejectedValue() });
await waitForPromises();
await doDeleteFile();
@@ -358,9 +408,9 @@ describe('PackagesApp', () => {
it('shows an error when the mutation request returns an error payload', async () => {
createComponent({
- fileDeleteMutationResolver: jest
+ filesDeleteMutationResolver: jest
.fn()
- .mockResolvedValue(packageDestroyFileMutationError()),
+ .mockResolvedValue(packageDestroyFilesMutationError()),
});
await waitForPromises();
@@ -374,6 +424,117 @@ describe('PackagesApp', () => {
});
});
});
+
+ describe('deleting multiple files', () => {
+ const doDeleteFiles = async () => {
+ findPackageFiles().vm.$emit('delete-files', packageFiles());
+
+ findDeleteFilesModal().vm.$emit('primary');
+
+ return waitForPromises();
+ };
+
+ it('opens delete files confirmation modal', async () => {
+ createComponent();
+
+ await waitForPromises();
+
+ const showDeleteFilesSpy = jest.spyOn(wrapper.vm.$refs.deleteFilesModal, 'show');
+
+ findPackageFiles().vm.$emit('delete-files', packageFiles());
+
+ expect(showDeleteFilesSpy).toBeCalled();
+ });
+
+ it('confirming on the modal sets the loading state', async () => {
+ createComponent();
+
+ await waitForPromises();
+
+ findPackageFiles().vm.$emit('delete-files', packageFiles());
+
+ findDeleteFilesModal().vm.$emit('primary');
+
+ await nextTick();
+
+ expect(findPackageFiles().props('isLoading')).toEqual(true);
+ });
+
+ it('confirming on the modal deletes the file and shows a success message', async () => {
+ const resolver = jest.fn().mockResolvedValue(packageDetailsQuery());
+ createComponent({ resolver });
+
+ await waitForPromises();
+
+ await doDeleteFiles();
+
+ expect(createFlash).toHaveBeenCalledWith(
+ expect.objectContaining({
+ message: DELETE_PACKAGE_FILES_SUCCESS_MESSAGE,
+ }),
+ );
+ // we are re-fetching the package details, so we expect the resolver to have been called twice
+ expect(resolver).toHaveBeenCalledTimes(2);
+ });
+
+ describe('errors', () => {
+ it('shows an error when the mutation request fails', async () => {
+ createComponent({ filesDeleteMutationResolver: jest.fn().mockRejectedValue() });
+ await waitForPromises();
+
+ await doDeleteFiles();
+
+ expect(createFlash).toHaveBeenCalledWith(
+ expect.objectContaining({
+ message: DELETE_PACKAGE_FILES_ERROR_MESSAGE,
+ }),
+ );
+ });
+
+ it('shows an error when the mutation request returns an error payload', async () => {
+ createComponent({
+ filesDeleteMutationResolver: jest
+ .fn()
+ .mockResolvedValue(packageDestroyFilesMutationError()),
+ });
+ await waitForPromises();
+
+ await doDeleteFiles();
+
+ expect(createFlash).toHaveBeenCalledWith(
+ expect.objectContaining({
+ message: DELETE_PACKAGE_FILES_ERROR_MESSAGE,
+ }),
+ );
+ });
+ });
+ });
+
+ describe('deleting all files', () => {
+ it('opens the delete package confirmation modal', async () => {
+ const resolver = jest.fn().mockResolvedValue(
+ packageDetailsQuery({
+ packageFiles: {
+ pageInfo: {
+ hasNextPage: false,
+ },
+ nodes: packageFiles(),
+ },
+ }),
+ );
+ createComponent({
+ resolver,
+ });
+
+ await waitForPromises();
+
+ const showDeletePackageSpy = jest.spyOn(wrapper.vm.$refs.deleteModal, 'show');
+
+ findPackageFiles().vm.$emit('delete-files', packageFiles());
+
+ expect(showDeletePackageSpy).toBeCalled();
+ });
+ });
});
describe('versions', () => {
diff --git a/spec/frontend/packages_and_registries/settings/project/settings/components/__snapshots__/container_expiration_policy_form_spec.js.snap b/spec/frontend/packages_and_registries/settings/project/settings/components/__snapshots__/container_expiration_policy_form_spec.js.snap
index 108d9478788..5d08574234c 100644
--- a/spec/frontend/packages_and_registries/settings/project/settings/components/__snapshots__/container_expiration_policy_form_spec.js.snap
+++ b/spec/frontend/packages_and_registries/settings/project/settings/components/__snapshots__/container_expiration_policy_form_spec.js.snap
@@ -5,6 +5,7 @@ exports[`Container Expiration Policy Settings Form Cadence matches snapshot 1`]
class="gl-mr-7 gl-mb-0!"
data-testid="cadence-dropdown"
description=""
+ dropdownclass=""
formoptions="[object Object],[object Object],[object Object],[object Object],[object Object]"
label="Run cleanup:"
name="cadence"
@@ -24,6 +25,7 @@ exports[`Container Expiration Policy Settings Form Keep N matches snapshot 1`] =
<expiration-dropdown-stub
data-testid="keep-n-dropdown"
description=""
+ dropdownclass=""
formoptions="[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object]"
label="Keep the most recent:"
name="keep-n"
@@ -47,6 +49,7 @@ exports[`Container Expiration Policy Settings Form OlderThan matches snapshot 1`
<expiration-dropdown-stub
data-testid="older-than-dropdown"
description=""
+ dropdownclass=""
formoptions="[object Object],[object Object],[object Object],[object Object],[object Object],[object Object]"
label="Remove tags older than:"
name="older-than"
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 d4b6c66ddeb..0696144215c 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
@@ -1,4 +1,5 @@
export const containerExpirationPolicyData = () => ({
+ __typename: 'ContainerExpirationPolicy',
cadence: 'EVERY_DAY',
enabled: true,
keepN: 'TEN_TAGS',
@@ -13,7 +14,6 @@ export const expirationPolicyPayload = (override) => ({
project: {
id: '1',
containerExpirationPolicy: {
- __typename: 'ContainerExpirationPolicy',
...containerExpirationPolicyData(),
...override,
},
@@ -42,6 +42,7 @@ export const expirationPolicyMutationPayload = ({ override, errors = [] } = {})
});
export const packagesCleanupPolicyData = {
+ __typename: 'PackagesCleanupPolicy',
keepNDuplicatedPackageFiles: 'ALL_PACKAGE_FILES',
nextRunAt: '2020-11-19T07:37:03.941Z',
};
@@ -51,7 +52,6 @@ export const packagesCleanupPolicyPayload = (override) => ({
project: {
id: '1',
packagesCleanupPolicy: {
- __typename: 'PackagesCleanupPolicy',
...packagesCleanupPolicyData,
...override,
},
diff --git a/spec/frontend/pages/admin/application_settings/account_and_limits_spec.js b/spec/frontend/pages/admin/application_settings/account_and_limits_spec.js
index 542eb2f3ab8..85ed94b748d 100644
--- a/spec/frontend/pages/admin/application_settings/account_and_limits_spec.js
+++ b/spec/frontend/pages/admin/application_settings/account_and_limits_spec.js
@@ -23,17 +23,17 @@ describe('AccountAndLimits', () => {
describe('Changing of userInternalRegex when userDefaultExternal', () => {
it('is unchecked', () => {
- expect($userDefaultExternal.prop('checked')).toBeFalsy();
+ expect($userDefaultExternal.prop('checked')).toBe(false);
expect($userInternalRegex.placeholder).toEqual(PLACEHOLDER_USER_EXTERNAL_DEFAULT_FALSE);
- expect($userInternalRegex.readOnly).toBeTruthy();
+ expect($userInternalRegex.readOnly).toBe(true);
});
it('is checked', () => {
if (!$userDefaultExternal.prop('checked')) $userDefaultExternal.click();
- expect($userDefaultExternal.prop('checked')).toBeTruthy();
+ expect($userDefaultExternal.prop('checked')).toBe(true);
expect($userInternalRegex.placeholder).toEqual(PLACEHOLDER_USER_EXTERNAL_DEFAULT_TRUE);
- expect($userInternalRegex.readOnly).toBeFalsy();
+ expect($userInternalRegex.readOnly).toBe(false);
});
});
});
diff --git a/spec/frontend/pages/groups/new/components/app_spec.js b/spec/frontend/pages/groups/new/components/app_spec.js
new file mode 100644
index 00000000000..ab483316086
--- /dev/null
+++ b/spec/frontend/pages/groups/new/components/app_spec.js
@@ -0,0 +1,39 @@
+import { shallowMount } from '@vue/test-utils';
+import App from '~/pages/groups/new/components/app.vue';
+import NewNamespacePage from '~/vue_shared/new_namespace/new_namespace_page.vue';
+
+describe('App component', () => {
+ let wrapper;
+
+ const createComponent = (propsData = {}) => {
+ wrapper = shallowMount(App, { propsData });
+ };
+
+ const findNewNamespacePage = () => wrapper.findComponent(NewNamespacePage);
+
+ const findCreateGroupPanel = () =>
+ findNewNamespacePage()
+ .props('panels')
+ .find((panel) => panel.name === 'create-group-pane');
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('creates correct component for group creation', () => {
+ createComponent();
+
+ expect(findNewNamespacePage().props('initialBreadcrumb')).toBe('New group');
+ expect(findCreateGroupPanel().title).toBe('Create group');
+ });
+
+ it('creates correct component for subgroup creation', () => {
+ const props = { parentGroupName: 'parent', importExistingGroupPath: '/path' };
+
+ createComponent(props);
+
+ expect(findNewNamespacePage().props('initialBreadcrumb')).toBe('parent');
+ expect(findCreateGroupPanel().title).toBe('Create subgroup');
+ expect(findCreateGroupPanel().detailProps).toEqual(props);
+ });
+});
diff --git a/spec/frontend/pages/groups/new/components/create_group_description_details_spec.js b/spec/frontend/pages/groups/new/components/create_group_description_details_spec.js
new file mode 100644
index 00000000000..56a1fd03f71
--- /dev/null
+++ b/spec/frontend/pages/groups/new/components/create_group_description_details_spec.js
@@ -0,0 +1,57 @@
+import { shallowMount } from '@vue/test-utils';
+import { GlSprintf, GlLink } from '@gitlab/ui';
+import CreateGroupDescriptionDetails from '~/pages/groups/new/components/create_group_description_details.vue';
+import { helpPagePath } from '~/helpers/help_page_helper';
+
+describe('CreateGroupDescriptionDetails component', () => {
+ let wrapper;
+
+ const createComponent = (propsData = {}) => {
+ wrapper = shallowMount(CreateGroupDescriptionDetails, {
+ propsData,
+ stubs: { GlSprintf, GlLink },
+ });
+ };
+
+ const findLinkHref = (at) => wrapper.findAllComponents(GlLink).at(at);
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('creates correct component for group creation', () => {
+ createComponent();
+
+ const groupsLink = findLinkHref(0);
+ expect(groupsLink.attributes('href')).toBe(helpPagePath('user/group/index'));
+ expect(groupsLink.text()).toBe('Groups');
+
+ const subgroupsLink = findLinkHref(1);
+ expect(subgroupsLink.text()).toBe('subgroups');
+ expect(subgroupsLink.attributes('href')).toBe(helpPagePath('user/group/subgroups/index'));
+
+ expect(wrapper.text()).toBe(
+ 'Groups allow you to manage and collaborate across multiple projects. Members of a group have access to all of its projects. Groups can also be nested by creating subgroups.',
+ );
+ });
+
+ it('creates correct component for subgroup creation', () => {
+ createComponent({ parentGroupName: 'parent', importExistingGroupPath: '/path' });
+
+ const groupsLink = findLinkHref(0);
+ expect(groupsLink.attributes('href')).toBe(helpPagePath('user/group/index'));
+ expect(groupsLink.text()).toBe('Groups');
+
+ const subgroupsLink = findLinkHref(1);
+ expect(subgroupsLink.text()).toBe('subgroups');
+ expect(subgroupsLink.attributes('href')).toBe(helpPagePath('user/group/subgroups/index'));
+
+ const importGroupLink = findLinkHref(2);
+ expect(importGroupLink.text()).toBe('import an existing group');
+ expect(importGroupLink.attributes('href')).toBe('/path');
+
+ expect(wrapper.text()).toBe(
+ 'Groups and subgroups allow you to manage and collaborate across multiple projects. Members of a group have access to all of its projects. You can also import an existing group.',
+ );
+ });
+});
diff --git a/spec/frontend/pages/projects/graphs/__snapshots__/code_coverage_spec.js.snap b/spec/frontend/pages/projects/graphs/__snapshots__/code_coverage_spec.js.snap
index 43361bb6f24..21a38f066d9 100644
--- a/spec/frontend/pages/projects/graphs/__snapshots__/code_coverage_spec.js.snap
+++ b/spec/frontend/pages/projects/graphs/__snapshots__/code_coverage_spec.js.snap
@@ -3,6 +3,33 @@
exports[`Code Coverage when fetching data is successful matches the snapshot 1`] = `
<div>
<div
+ class="gl-display-flex gl-justify-content-space-between gl-align-items-center gl-border-t gl-pt-4 gl-mb-3"
+ >
+ <h4
+ class="gl-m-0"
+ sub-header=""
+ >
+ <gl-sprintf-stub
+ message="Code coverage statistics for %{ref} %{start_date} - %{end_date}"
+ />
+ </h4>
+
+ <gl-button-stub
+ buttontextclasses=""
+ category="primary"
+ data-testid="download-button"
+ href="url/"
+ icon=""
+ size="small"
+ variant="default"
+ >
+
+ Download raw data (.csv)
+
+ </gl-button-stub>
+ </div>
+
+ <div
class="gl-mt-3 gl-mb-3"
>
<!---->
@@ -79,6 +106,7 @@ exports[`Code Coverage when fetching data is successful matches the snapshot 1`]
legendmaxtext="Max"
legendmintext="Min"
option="[object Object]"
+ responsive=""
thresholds=""
/>
</div>
diff --git a/spec/frontend/pages/projects/graphs/code_coverage_spec.js b/spec/frontend/pages/projects/graphs/code_coverage_spec.js
index 0f763e3220a..f272891919d 100644
--- a/spec/frontend/pages/projects/graphs/code_coverage_spec.js
+++ b/spec/frontend/pages/projects/graphs/code_coverage_spec.js
@@ -15,17 +15,26 @@ describe('Code Coverage', () => {
let mockAxios;
const graphEndpoint = '/graph';
+ const graphStartDate = '13 February';
+ const graphEndDate = '12 May';
+ const graphRef = 'master';
+ const graphCsvPath = 'url/';
const findAlert = () => wrapper.find(GlAlert);
const findAreaChart = () => wrapper.find(GlAreaChart);
const findAllDropdownItems = () => wrapper.findAll(GlDropdownItem);
const findFirstDropdownItem = () => findAllDropdownItems().at(0);
const findSecondDropdownItem = () => findAllDropdownItems().at(1);
+ const findDownloadButton = () => wrapper.find('[data-testid="download-button"]');
const createComponent = () => {
wrapper = shallowMount(CodeCoverage, {
propsData: {
graphEndpoint,
+ graphStartDate,
+ graphEndDate,
+ graphRef,
+ graphCsvPath,
},
});
};
@@ -64,6 +73,10 @@ describe('Code Coverage', () => {
it('shows no error messages', () => {
expect(findAlert().exists()).toBe(false);
});
+
+ it('does not render download button', () => {
+ expect(findDownloadButton().exists()).toBe(true);
+ });
});
describe('when fetching data fails', () => {
@@ -112,6 +125,10 @@ describe('Code Coverage', () => {
it('still renders an empty graph', () => {
expect(findAreaChart().exists()).toBe(true);
});
+
+ it('does not render download button', () => {
+ expect(findDownloadButton().exists()).toBe(false);
+ });
});
describe('dropdown options', () => {
@@ -146,8 +163,8 @@ describe('Code Coverage', () => {
await nextTick();
- expect(findFirstDropdownItem().attributes('ischecked')).toBeFalsy();
- expect(findSecondDropdownItem().attributes('ischecked')).toBeTruthy();
+ expect(findFirstDropdownItem().attributes('ischecked')).toBe(undefined);
+ expect(findSecondDropdownItem().attributes('ischecked')).toBe('true');
});
it('updates the graph data when selecting a different option in dropdown', async () => {
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 42eeff89bf4..5b9c48f0d9b 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
@@ -226,7 +226,6 @@ describe('Timezone Dropdown', () => {
it('returns the correct object if the identifier exists', () => {
const res = findTimezoneByIdentifier(tzList, identifier);
- expect(res).toBeTruthy();
expect(res).toBe(tzList[2]);
});
diff --git a/spec/frontend/pages/projects/shared/permissions/components/settings_panel_spec.js b/spec/frontend/pages/projects/shared/permissions/components/settings_panel_spec.js
index 85660d09baa..f908508c4b5 100644
--- a/spec/frontend/pages/projects/shared/permissions/components/settings_panel_spec.js
+++ b/spec/frontend/pages/projects/shared/permissions/components/settings_panel_spec.js
@@ -127,6 +127,7 @@ describe('Settings Panel', () => {
const findOperationsVisibilityInput = () =>
findOperationsSettings().findComponent(ProjectFeatureSetting);
const findConfirmDangerButton = () => wrapper.findComponent(ConfirmDanger);
+ const findEnvironmentsSettings = () => wrapper.findComponent({ ref: 'environments-settings' });
afterEach(() => {
wrapper.destroy();
@@ -786,4 +787,23 @@ describe('Settings Panel', () => {
expect(findOperationsSettings().exists()).toBe(true);
});
});
+
+ describe('Environments', () => {
+ describe('with feature flag', () => {
+ it('should show the environments toggle', () => {
+ wrapper = mountComponent({
+ glFeatures: { splitOperationsVisibilityPermissions: true },
+ });
+
+ expect(findEnvironmentsSettings().exists()).toBe(true);
+ });
+ });
+ describe('without feature flag', () => {
+ it('should not show the environments toggle', () => {
+ wrapper = mountComponent({});
+
+ expect(findEnvironmentsSettings().exists()).toBe(false);
+ });
+ });
+ });
});
diff --git a/spec/frontend/pages/shared/wikis/components/wiki_form_spec.js b/spec/frontend/pages/shared/wikis/components/wiki_form_spec.js
index a5db10d106d..204c48f8de1 100644
--- a/spec/frontend/pages/shared/wikis/components/wiki_form_spec.js
+++ b/spec/frontend/pages/shared/wikis/components/wiki_form_spec.js
@@ -1,5 +1,5 @@
import { nextTick } from 'vue';
-import { GlAlert, GlButton, GlFormInput, GlFormGroup } from '@gitlab/ui';
+import { GlAlert, GlButton, GlFormInput, GlFormGroup, GlSegmentedControl } from '@gitlab/ui';
import { mount, shallowMount } from '@vue/test-utils';
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
@@ -106,6 +106,7 @@ describe('WikiForm', () => {
MarkdownField,
GlAlert,
GlButton,
+ GlSegmentedControl,
LocalStorageSync: stubComponent(LocalStorageSync),
GlFormInput,
GlFormGroup,
@@ -317,20 +318,20 @@ describe('WikiForm', () => {
});
describe('when content editor is not active', () => {
- it('displays "Edit rich text" label in the toggle editing mode button', () => {
- expect(findToggleEditingModeButton().text()).toBe('Edit rich text');
+ it('displays "Source" label in the toggle editing mode button', () => {
+ expect(findToggleEditingModeButton().props().checked).toBe('source');
});
describe('when clicking the toggle editing mode button', () => {
beforeEach(async () => {
- await findToggleEditingModeButton().trigger('click');
+ await findToggleEditingModeButton().vm.$emit('input', 'richText');
});
it('hides the classic editor', () => {
expect(findClassicEditor().exists()).toBe(false);
});
- it('hides the content editor', () => {
+ it('shows the content editor', () => {
expect(findContentEditor().exists()).toBe(true);
});
});
@@ -342,7 +343,7 @@ describe('WikiForm', () => {
expect(findContentEditor().exists()).toBe(false);
// enable content editor
- await findLocalStorageSync().vm.$emit('input', true);
+ await findLocalStorageSync().vm.$emit('input', 'richText');
expect(findContentEditor().exists()).toBe(true);
expect(findClassicEditor().exists()).toBe(false);
@@ -352,17 +353,18 @@ describe('WikiForm', () => {
describe('when content editor is active', () => {
let mockContentEditor;
- beforeEach(async () => {
+ beforeEach(() => {
+ createWrapper();
mockContentEditor = {
getSerializedContent: jest.fn(),
setSerializedContent: jest.fn(),
};
- await findToggleEditingModeButton().trigger('click');
+ findToggleEditingModeButton().vm.$emit('input', 'richText');
});
- it('displays "Edit source" label in the toggle editing mode button', () => {
- expect(findToggleEditingModeButton().text()).toBe('Edit source');
+ 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', () => {
@@ -374,7 +376,8 @@ describe('WikiForm', () => {
);
findContentEditor().vm.$emit('initialized', mockContentEditor);
- await findToggleEditingModeButton().trigger('click');
+ await findToggleEditingModeButton().vm.$emit('input', 'source');
+ await nextTick();
});
it('hides the content editor', () => {
@@ -389,6 +392,38 @@ describe('WikiForm', () => {
expect(findContent().element.value).toBe(contentEditorFakeSerializedContent);
});
});
+
+ 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();
+ });
+ });
+ });
});
});
@@ -398,7 +433,7 @@ describe('WikiForm', () => {
createWrapper({ mountFn: mount });
mock.onPost(/preview-markdown/).reply(400);
- await findToggleEditingModeButton().trigger('click');
+ await findToggleEditingModeButton().vm.$emit('input', 'richText');
// try waiting for content editor to load (but it will never actually load)
await waitForPromises();
@@ -410,7 +445,7 @@ describe('WikiForm', () => {
describe('toggling editing modes to the classic editor', () => {
beforeEach(() => {
- return findToggleEditingModeButton().trigger('click');
+ return findToggleEditingModeButton().vm.$emit('input', 'source');
});
it('switches to classic editor', () => {
@@ -426,7 +461,7 @@ describe('WikiForm', () => {
mock.onPost(/preview-markdown/).reply(200, { body: '<p>hello <strong>world</strong></p>' });
- await findToggleEditingModeButton().trigger('click');
+ await findToggleEditingModeButton().vm.$emit('input', 'richText');
await waitForPromises();
});
@@ -463,7 +498,6 @@ describe('WikiForm', () => {
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,
});
diff --git a/spec/frontend/pipeline_editor/components/drawer/cards/first_pipeline_card_spec.js b/spec/frontend/pipeline_editor/components/drawer/cards/first_pipeline_card_spec.js
index bf5d15516c2..7e1e5004d91 100644
--- a/spec/frontend/pipeline_editor/components/drawer/cards/first_pipeline_card_spec.js
+++ b/spec/frontend/pipeline_editor/components/drawer/cards/first_pipeline_card_spec.js
@@ -8,16 +8,8 @@ describe('First pipeline card', () => {
let wrapper;
let trackingSpy;
- const defaultProvide = {
- runnerHelpPagePath: '/help/runners',
- };
-
const createComponent = () => {
- wrapper = mount(FirstPipelineCard, {
- provide: {
- ...defaultProvide,
- },
- });
+ wrapper = mount(FirstPipelineCard);
};
const getLinkByName = (name) => getByRole(wrapper.element, 'link', { name });
@@ -43,7 +35,7 @@ describe('First pipeline card', () => {
});
it('renders the link', () => {
- expect(findRunnersLink().href).toContain(defaultProvide.runnerHelpPagePath);
+ expect(findRunnersLink().href).toBe(wrapper.vm.$options.RUNNER_HELP_URL);
});
describe('tracking', () => {
diff --git a/spec/frontend/pipeline_editor/components/lint/ci_lint_spec.js b/spec/frontend/pipeline_editor/components/lint/ci_lint_spec.js
deleted file mode 100644
index 238942a34ff..00000000000
--- a/spec/frontend/pipeline_editor/components/lint/ci_lint_spec.js
+++ /dev/null
@@ -1,72 +0,0 @@
-import { GlAlert, GlLink } from '@gitlab/ui';
-import { mount, shallowMount } from '@vue/test-utils';
-import CiLint from '~/pipeline_editor/components/lint/ci_lint.vue';
-import { mergeUnwrappedCiConfig, mockLintHelpPagePath } from '../../mock_data';
-
-describe('~/pipeline_editor/components/lint/ci_lint.vue', () => {
- let wrapper;
-
- const createComponent = ({ props, mountFn = shallowMount } = {}) => {
- wrapper = mountFn(CiLint, {
- provide: {
- lintHelpPagePath: mockLintHelpPagePath,
- },
- propsData: {
- ciConfig: mergeUnwrappedCiConfig(),
- ...props,
- },
- });
- };
-
- const findAllByTestId = (selector) => wrapper.findAll(`[data-testid="${selector}"]`);
- const findAlert = () => wrapper.find(GlAlert);
- const findLintParameters = () => findAllByTestId('ci-lint-parameter');
- const findLintParameterAt = (i) => findLintParameters().at(i);
- const findLintValueAt = (i) => findAllByTestId('ci-lint-value').at(i);
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- describe('Valid Results', () => {
- beforeEach(() => {
- createComponent({ props: { isValid: true }, mountFn: mount });
- });
-
- it('displays valid results', () => {
- expect(findAlert().text()).toMatch('Status: Syntax is correct.');
- });
-
- it('displays link to the right help page', () => {
- expect(findAlert().find(GlLink).attributes('href')).toBe(mockLintHelpPagePath);
- });
-
- it('displays jobs', () => {
- expect(findLintParameters()).toHaveLength(3);
-
- expect(findLintParameterAt(0).text()).toBe('Test Job - job_test_1');
- expect(findLintParameterAt(1).text()).toBe('Test Job - job_test_2');
- expect(findLintParameterAt(2).text()).toBe('Build Job - job_build');
- });
-
- it('displays jobs details', () => {
- expect(findLintParameters()).toHaveLength(3);
-
- expect(findLintValueAt(0).text()).toMatchInterpolatedText(
- 'echo "test 1" Only policy: branches, tags When: on_success',
- );
- expect(findLintValueAt(1).text()).toMatchInterpolatedText(
- 'echo "test 2" Only policy: branches, tags When: on_success',
- );
- expect(findLintValueAt(2).text()).toMatchInterpolatedText(
- 'echo "build" Only policy: branches, tags When: on_success',
- );
- });
-
- it('displays invalid results', () => {
- createComponent({ props: { isValid: false }, mountFn: mount });
-
- expect(findAlert().text()).toMatch('Status: Syntax is incorrect.');
- });
- });
-});
diff --git a/spec/frontend/pipeline_editor/components/pipeline_editor_tabs_spec.js b/spec/frontend/pipeline_editor/components/pipeline_editor_tabs_spec.js
index 87a7f07f7d4..2f3e1b49b37 100644
--- a/spec/frontend/pipeline_editor/components/pipeline_editor_tabs_spec.js
+++ b/spec/frontend/pipeline_editor/components/pipeline_editor_tabs_spec.js
@@ -1,11 +1,12 @@
+// TODO
+
import { GlAlert, GlBadge, GlLoadingIcon, GlTabs } from '@gitlab/ui';
-import { createLocalVue, mount, shallowMount } from '@vue/test-utils';
+import { mount, shallowMount } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
import Vue, { nextTick } from 'vue';
import createMockApollo from 'helpers/mock_apollo_helper';
import setWindowLocation from 'helpers/set_window_location_helper';
import CiConfigMergedPreview from '~/pipeline_editor/components/editor/ci_config_merged_preview.vue';
-import CiLint from '~/pipeline_editor/components/lint/ci_lint.vue';
import CiValidate from '~/pipeline_editor/components/validate/ci_validate.vue';
import WalkthroughPopover from '~/pipeline_editor/components/popovers/walkthrough_popover.vue';
import PipelineEditorTabs from '~/pipeline_editor/components/pipeline_editor_tabs.vue';
@@ -30,8 +31,7 @@ import {
mockLintResponseWithoutMerged,
} from '../mock_data';
-const localVue = createLocalVue();
-localVue.use(VueApollo);
+Vue.use(VueApollo);
Vue.config.ignoredElements = ['gl-emoji'];
@@ -64,7 +64,12 @@ describe('Pipeline editor tabs component', () => {
};
},
provide: {
+ ciConfigPath: '/path/to/ci-config',
ciLintPath: mockCiLintPath,
+ currentBranch: 'main',
+ projectFullPath: '/path/to/project',
+ simulatePipelineHelpPagePath: 'path/to/help/page',
+ validateTabIllustrationPath: 'path/to/svg',
...provide,
},
stubs: {
@@ -88,21 +93,18 @@ describe('Pipeline editor tabs component', () => {
provide,
mountFn,
options: {
- localVue,
apolloProvider: mockApollo,
},
});
};
const findEditorTab = () => wrapper.find('[data-testid="editor-tab"]');
- const findLintTab = () => wrapper.find('[data-testid="lint-tab"]');
const findMergedTab = () => wrapper.find('[data-testid="merged-tab"]');
const findValidateTab = () => wrapper.find('[data-testid="validate-tab"]');
const findVisualizationTab = () => wrapper.find('[data-testid="visualization-tab"]');
const findAlert = () => wrapper.findComponent(GlAlert);
const findBadge = () => wrapper.findComponent(GlBadge);
- const findCiLint = () => wrapper.findComponent(CiLint);
const findCiValidate = () => wrapper.findComponent(CiValidate);
const findGlTabs = () => wrapper.findComponent(GlTabs);
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
@@ -121,7 +123,8 @@ describe('Pipeline editor tabs component', () => {
describe('editor tab', () => {
it('displays editor only after the tab is mounted', async () => {
- createComponent({ mountFn: mount });
+ mockBlobContentData.mockResolvedValue(mockBlobContentQueryResponse);
+ createComponentWithApollo({ mountFn: mount });
expect(findTextEditor().exists()).toBe(false);
@@ -156,138 +159,57 @@ describe('Pipeline editor tabs component', () => {
});
describe('validate tab', () => {
- describe('with simulatePipeline feature flag ON', () => {
- describe('after loading', () => {
- beforeEach(() => {
- createComponent({
- provide: { glFeatures: { simulatePipeline: true } },
- });
- });
-
- it('displays the tab and the validate component', () => {
- expect(findValidateTab().exists()).toBe(true);
- expect(findCiValidate().exists()).toBe(true);
- });
+ describe('after loading', () => {
+ beforeEach(() => {
+ createComponent();
});
- describe('NEW badge', () => {
- describe('default', () => {
- beforeEach(() => {
- mockBlobContentData.mockResolvedValue(mockBlobContentQueryResponse);
- createComponentWithApollo({
- mountFn: mount,
- props: {
- currentTab: VALIDATE_TAB,
- },
- provide: {
- glFeatures: { simulatePipeline: true },
- ciConfigPath: '/path/to/ci-config',
- currentBranch: 'main',
- projectFullPath: '/path/to/project',
- simulatePipelineHelpPagePath: 'path/to/help/page',
- validateTabIllustrationPath: 'path/to/svg',
- },
- });
- });
-
- it('renders badge by default', () => {
- expect(findBadge().exists()).toBe(true);
- expect(findBadge().text()).toBe(wrapper.vm.$options.i18n.new);
- });
-
- it('hides badge when moving away from the validate tab', async () => {
- expect(findBadge().exists()).toBe(true);
-
- await findEditorTab().vm.$emit('click');
-
- expect(findBadge().exists()).toBe(false);
- });
- });
-
- describe('if badge has been dismissed before', () => {
- beforeEach(() => {
- localStorage.setItem(VALIDATE_TAB_BADGE_DISMISSED_KEY, 'true');
- mockBlobContentData.mockResolvedValue(mockBlobContentQueryResponse);
- createComponentWithApollo({
- mountFn: mount,
- provide: {
- glFeatures: { simulatePipeline: true },
- ciConfigPath: '/path/to/ci-config',
- currentBranch: 'main',
- projectFullPath: '/path/to/project',
- simulatePipelineHelpPagePath: 'path/to/help/page',
- validateTabIllustrationPath: 'path/to/svg',
- },
- });
- });
-
- it('does not render badge if it has been dismissed before', () => {
- expect(findBadge().exists()).toBe(false);
- });
- });
+ it('displays the tab and the validate component', () => {
+ expect(findValidateTab().exists()).toBe(true);
+ expect(findCiValidate().exists()).toBe(true);
});
});
- describe('with simulatePipeline feature flag OFF', () => {
- beforeEach(() => {
- createComponent({
- provide: {
- glFeatures: {
- simulatePipeline: false,
+ describe('NEW badge', () => {
+ describe('default', () => {
+ beforeEach(() => {
+ mockBlobContentData.mockResolvedValue(mockBlobContentQueryResponse);
+ createComponentWithApollo({
+ mountFn: mount,
+ props: {
+ currentTab: VALIDATE_TAB,
},
- },
+ });
});
- });
- it('does not render the tab and the validate component', () => {
- expect(findValidateTab().exists()).toBe(false);
- expect(findCiValidate().exists()).toBe(false);
- });
- });
- });
+ it('renders badge by default', () => {
+ expect(findBadge().exists()).toBe(true);
+ expect(findBadge().text()).toBe(wrapper.vm.$options.i18n.new);
+ });
- describe('lint tab', () => {
- describe('while loading', () => {
- beforeEach(() => {
- createComponent({ appStatus: EDITOR_APP_STATUS_LOADING });
- });
+ it('hides badge when moving away from the validate tab', async () => {
+ expect(findBadge().exists()).toBe(true);
- it('displays a loading icon if the lint query is loading', () => {
- expect(findLoadingIcon().exists()).toBe(true);
- });
+ await findEditorTab().vm.$emit('click');
- it('does not display the lint component', () => {
- expect(findCiLint().exists()).toBe(false);
- });
- });
- describe('after loading', () => {
- beforeEach(() => {
- createComponent();
- });
-
- it('display the tab and the lint component', () => {
- expect(findLintTab().exists()).toBe(true);
- expect(findCiLint().exists()).toBe(true);
+ expect(findBadge().exists()).toBe(false);
+ });
});
- });
- describe('with simulatePipeline feature flag ON', () => {
- beforeEach(() => {
- createComponent({
- provide: {
- glFeatures: {
- simulatePipeline: true,
- },
- },
+ describe('if badge has been dismissed before', () => {
+ beforeEach(() => {
+ localStorage.setItem(VALIDATE_TAB_BADGE_DISMISSED_KEY, 'true');
+ mockBlobContentData.mockResolvedValue(mockBlobContentQueryResponse);
+ createComponentWithApollo({ mountFn: mount });
});
- });
- it('does not render the tab and the lint component', () => {
- expect(findLintTab().exists()).toBe(false);
- expect(findCiLint().exists()).toBe(false);
+ it('does not render badge if it has been dismissed before', () => {
+ expect(findBadge().exists()).toBe(false);
+ });
});
});
});
+
describe('merged tab', () => {
describe('while loading', () => {
beforeEach(() => {
@@ -328,19 +250,19 @@ describe('Pipeline editor tabs component', () => {
describe('show tab content based on status', () => {
it.each`
- appStatus | editor | viz | lint | merged
+ appStatus | editor | viz | validate | merged
${undefined} | ${true} | ${true} | ${true} | ${true}
- ${EDITOR_APP_STATUS_EMPTY} | ${true} | ${false} | ${false} | ${false}
+ ${EDITOR_APP_STATUS_EMPTY} | ${true} | ${false} | ${true} | ${false}
${EDITOR_APP_STATUS_INVALID} | ${true} | ${false} | ${true} | ${false}
${EDITOR_APP_STATUS_VALID} | ${true} | ${true} | ${true} | ${true}
`(
- 'when status is $appStatus, we show - editor:$editor | viz:$viz | lint:$lint | merged:$merged ',
- ({ appStatus, editor, viz, lint, merged }) => {
+ 'when status is $appStatus, we show - editor:$editor | viz:$viz | validate:$validate | merged:$merged ',
+ ({ appStatus, editor, viz, validate, merged }) => {
createComponent({ appStatus });
expect(findTextEditor().exists()).toBe(editor);
expect(findPipelineGraph().exists()).toBe(viz);
- expect(findCiLint().exists()).toBe(lint);
+ expect(findValidateTab().exists()).toBe(validate);
expect(findMergedPreview().exists()).toBe(merged);
},
);
@@ -386,11 +308,8 @@ describe('Pipeline editor tabs component', () => {
describe('pipeline editor walkthrough', () => {
describe('when isNewCiConfigFile prop is true (default)', () => {
- beforeEach(async () => {
- createComponent({
- mountFn: mount,
- });
- await nextTick();
+ beforeEach(() => {
+ createComponent();
});
it('shows walkthrough popover', async () => {
@@ -400,8 +319,7 @@ describe('Pipeline editor tabs component', () => {
describe('when isNewCiConfigFile prop is false', () => {
it('does not show walkthrough popover', async () => {
- createComponent({ props: { isNewCiConfigFile: false }, mountFn: mount });
- await nextTick();
+ createComponent({ props: { isNewCiConfigFile: false } });
expect(findWalkthroughPopover().exists()).toBe(false);
});
});
@@ -411,7 +329,6 @@ describe('Pipeline editor tabs component', () => {
const handler = jest.fn();
createComponent({
- mountFn: mount,
listeners: {
event: handler,
},
diff --git a/spec/frontend/pipeline_editor/components/validate/ci_validate_spec.js b/spec/frontend/pipeline_editor/components/validate/ci_validate_spec.js
index f5f01b675b2..09d4f9736ad 100644
--- a/spec/frontend/pipeline_editor/components/validate/ci_validate_spec.js
+++ b/spec/frontend/pipeline_editor/components/validate/ci_validate_spec.js
@@ -2,6 +2,7 @@ import { GlAlert, GlDropdown, GlIcon, GlLoadingIcon, GlPopover } from '@gitlab/u
import { nextTick } from 'vue';
import { createLocalVue } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
+import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import CiLintResults from '~/pipeline_editor/components/lint/ci_lint_results.vue';
@@ -9,6 +10,7 @@ import CiValidate, { i18n } from '~/pipeline_editor/components/validate/ci_valid
import ValidatePipelinePopover from '~/pipeline_editor/components/popovers/validate_pipeline_popover.vue';
import getBlobContent from '~/pipeline_editor/graphql/queries/blob_content.query.graphql';
import lintCIMutation from '~/pipeline_editor/graphql/mutations/client/lint_ci.mutation.graphql';
+import { pipelineEditorTrackingOptions } from '~/pipeline_editor/constants';
import {
mockBlobContentQueryResponse,
mockCiLintPath,
@@ -24,6 +26,7 @@ describe('Pipeline Editor Validate Tab', () => {
let wrapper;
let mockApollo;
let mockBlobContentData;
+ let trackingSpy;
const createComponent = ({
props,
@@ -140,9 +143,24 @@ describe('Pipeline Editor Validate Tab', () => {
mockBlobContentData.mockResolvedValue(mockBlobContentQueryResponse);
await createComponentWithApollo();
+ trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue(mockLintDataValid);
});
+ afterEach(() => {
+ unmockTracking();
+ });
+
+ it('tracks the simulation event', () => {
+ const {
+ label,
+ actions: { simulatePipeline },
+ } = pipelineEditorTrackingOptions;
+ findCta().vm.$emit('click');
+
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, simulatePipeline, { label });
+ });
+
it('renders loading state while simulation is ongoing', async () => {
findCta().vm.$emit('click');
await nextTick();
@@ -159,7 +177,7 @@ describe('Pipeline Editor Validate Tab', () => {
expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({
mutation: lintCIMutation,
variables: {
- dry_run: true,
+ dry: true,
content: mockCiYml,
endpoint: mockCiLintPath,
},
@@ -224,10 +242,27 @@ describe('Pipeline Editor Validate Tab', () => {
mockBlobContentData.mockResolvedValue(mockBlobContentQueryResponse);
await createComponentWithApollo();
+ trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue(mockLintDataValid);
await findCta().vm.$emit('click');
});
+ afterEach(() => {
+ unmockTracking();
+ });
+
+ it('tracks the second simulation event', async () => {
+ const {
+ label,
+ actions: { resimulatePipeline },
+ } = pipelineEditorTrackingOptions;
+
+ await wrapper.setProps({ ciFileContent: 'new yaml content' });
+ findResultsCta().vm.$emit('click');
+
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, resimulatePipeline, { label });
+ });
+
it('renders content change status', async () => {
await wrapper.setProps({ ciFileContent: 'new yaml content' });
@@ -243,7 +278,7 @@ describe('Pipeline Editor Validate Tab', () => {
expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({
mutation: lintCIMutation,
variables: {
- dry_run: true,
+ dry: true,
content: 'new yaml content',
endpoint: mockCiLintPath,
},
diff --git a/spec/frontend/pipeline_editor/pipeline_editor_home_spec.js b/spec/frontend/pipeline_editor/pipeline_editor_home_spec.js
index c6964f190b4..0cb7155c8c0 100644
--- a/spec/frontend/pipeline_editor/pipeline_editor_home_spec.js
+++ b/spec/frontend/pipeline_editor/pipeline_editor_home_spec.js
@@ -14,7 +14,7 @@ import PipelineEditorTabs from '~/pipeline_editor/components/pipeline_editor_tab
import {
CREATE_TAB,
FILE_TREE_DISPLAY_KEY,
- LINT_TAB,
+ VALIDATE_TAB,
MERGED_TAB,
TABS_INDEX,
VISUALIZE_TAB,
@@ -138,7 +138,7 @@ describe('Pipeline editor home wrapper', () => {
tab | shouldShow
${MERGED_TAB} | ${false}
${VISUALIZE_TAB} | ${false}
- ${LINT_TAB} | ${false}
+ ${VALIDATE_TAB} | ${false}
${CREATE_TAB} | ${true}
`(
'when the active tab is $tab the commit form is shown: $shouldShow',
@@ -170,7 +170,7 @@ describe('Pipeline editor home wrapper', () => {
tab | shouldShow
${MERGED_TAB} | ${false}
${VISUALIZE_TAB} | ${false}
- ${LINT_TAB} | ${false}
+ ${VALIDATE_TAB} | ${false}
${CREATE_TAB} | ${true}
`(
'when the tab query param is $tab the commit form is shown: $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 eec55091efa..18dbd1ce9d6 100644
--- a/spec/frontend/pipeline_new/components/pipeline_new_form_spec.js
+++ b/spec/frontend/pipeline_new/components/pipeline_new_form_spec.js
@@ -39,6 +39,7 @@ describe('Pipeline New Form', () => {
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"]');
@@ -102,6 +103,8 @@ describe('Pipeline New Form', () => {
});
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(findRefsDropdown().props('value')).toEqual({ shortName: 'tag-1' });
expect(findVariableRows()).toHaveLength(3);
});
@@ -114,6 +117,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');
});
it('does not display remove icon for last row', () => {
diff --git a/spec/frontend/pipeline_schedules/components/take_ownership_modal_spec.js b/spec/frontend/pipeline_schedules/components/take_ownership_modal_spec.js
new file mode 100644
index 00000000000..d787611fe8f
--- /dev/null
+++ b/spec/frontend/pipeline_schedules/components/take_ownership_modal_spec.js
@@ -0,0 +1,54 @@
+import { GlModal } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import TakeOwnershipModal from '~/pipeline_schedules/components/take_ownership_modal.vue';
+
+describe('Take ownership modal', () => {
+ let wrapper;
+ const url = `/root/job-log-tester/-/pipeline_schedules/3/take_ownership`;
+
+ const createComponent = (props = {}) => {
+ wrapper = shallowMountExtended(TakeOwnershipModal, {
+ propsData: {
+ ownershipUrl: url,
+ ...props,
+ },
+ });
+ };
+
+ const findModal = () => wrapper.findComponent(GlModal);
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('has a primary action set to a url and a post data-method', () => {
+ const actionPrimary = findModal().props('actionPrimary');
+
+ expect(actionPrimary.attributes).toEqual(
+ expect.objectContaining([
+ {
+ category: 'primary',
+ variant: 'confirm',
+ href: url,
+ 'data-method': 'post',
+ },
+ ]),
+ );
+ });
+
+ it('shows a take ownership message', () => {
+ expect(findModal().text()).toBe(
+ 'Only the owner of a pipeline schedule can make changes to it. Do you want to take ownership of this schedule?',
+ );
+ });
+
+ it('emits the cancel event when clicking on cancel', async () => {
+ findModal().vm.$emit('cancel');
+
+ expect(findModal().emitted('cancel')).toHaveLength(1);
+ });
+});
diff --git a/spec/frontend/pipeline_wizard/components/editor_spec.js b/spec/frontend/pipeline_wizard/components/editor_spec.js
index 446412a4f02..540a08d2c7f 100644
--- a/spec/frontend/pipeline_wizard/components/editor_spec.js
+++ b/spec/frontend/pipeline_wizard/components/editor_spec.js
@@ -42,7 +42,7 @@ describe('Pages Yaml Editor wrapper', () => {
it('does not cause the touch event to be emitted', () => {
wrapper.setProps({ doc });
- expect(wrapper.emitted('touch')).not.toBeTruthy();
+ expect(wrapper.emitted('touch')).toBeUndefined();
});
});
@@ -63,7 +63,7 @@ describe('Pages Yaml Editor wrapper', () => {
it('emits touch if content is changed in editor', async () => {
await wrapper.vm.editor.setValue('foo: boo');
- expect(wrapper.emitted('touch')).toBeTruthy();
+ expect(wrapper.emitted('touch')).toEqual([expect.any(Array)]);
});
});
});
diff --git a/spec/frontend/pipeline_wizard/components/step_spec.js b/spec/frontend/pipeline_wizard/components/step_spec.js
index aa87b1d0b04..00b57f95ccc 100644
--- a/spec/frontend/pipeline_wizard/components/step_spec.js
+++ b/spec/frontend/pipeline_wizard/components/step_spec.js
@@ -139,7 +139,7 @@ describe('Pipeline Wizard - Step Page', () => {
await mockPrevClick();
await nextTick();
- expect(wrapper.emitted().back).toBeTruthy();
+ expect(wrapper.emitted().back).toEqual(expect.arrayContaining([]));
});
it('lets "next" event bubble upwards', async () => {
@@ -148,7 +148,7 @@ describe('Pipeline Wizard - Step Page', () => {
await mockNextClick();
await nextTick();
- expect(wrapper.emitted().next).toBeTruthy();
+ expect(wrapper.emitted().next).toEqual(expect.arrayContaining([]));
});
});
diff --git a/spec/frontend/pipeline_wizard/components/widgets/checklist_spec.js b/spec/frontend/pipeline_wizard/components/widgets/checklist_spec.js
index 43719595c5c..b8e194015b0 100644
--- a/spec/frontend/pipeline_wizard/components/widgets/checklist_spec.js
+++ b/spec/frontend/pipeline_wizard/components/widgets/checklist_spec.js
@@ -1,4 +1,4 @@
-import { GlFormCheckbox, GlFormCheckboxGroup } from '@gitlab/ui';
+import { GlFormCheckbox, GlFormGroup, GlFormCheckboxGroup } from '@gitlab/ui';
import { nextTick } from 'vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import ChecklistWidget from '~/pipeline_wizard/components/widgets/checklist.vue';
@@ -21,6 +21,7 @@ describe('Pipeline Wizard - Checklist Widget', () => {
return eventArray[eventArray.length - 1];
};
const findItem = (atIndex = 0) => wrapper.findAllComponents(GlFormCheckbox).at(atIndex);
+ const getGlFormGroup = () => wrapper.getComponent(GlFormGroup);
const getGlFormCheckboxGroup = () => wrapper.getComponent(GlFormCheckboxGroup);
// The item.ids *can* be passed inside props.items, but are usually
@@ -57,6 +58,16 @@ describe('Pipeline Wizard - Checklist Widget', () => {
expect(findItem().text()).toBe(props.items[0]);
});
+ it('assigns the same non-null value to label-for and form id', () => {
+ createComponent();
+ const formGroupLabelFor = getGlFormGroup().attributes('label-for');
+ const formCheckboxGroupId = getGlFormCheckboxGroup().attributes('id');
+
+ expect(formGroupLabelFor).not.toBeNull();
+ expect(formCheckboxGroupId).not.toBeNull();
+ expect(formGroupLabelFor).toBe(formCheckboxGroupId);
+ });
+
it('displays an item with a help text', () => {
createComponent();
const { text, help } = props.items[1];
diff --git a/spec/frontend/pipelines/components/pipeline_tabs_spec.js b/spec/frontend/pipelines/components/pipeline_tabs_spec.js
index e0210307823..3680d9d62c7 100644
--- a/spec/frontend/pipelines/components/pipeline_tabs_spec.js
+++ b/spec/frontend/pipelines/components/pipeline_tabs_spec.js
@@ -24,12 +24,14 @@ describe('The Pipeline Tabs', () => {
const findFailedJobsBadge = () => wrapper.findByTestId('failed-builds-counter');
const findJobsBadge = () => wrapper.findByTestId('builds-counter');
+ const findTestsBadge = () => wrapper.findByTestId('tests-counter');
const defaultProvide = {
defaultTabValue: '',
failedJobsCount: 1,
failedJobsSummary: [],
totalJobCount: 10,
+ testsCount: 123,
};
const createComponent = (provide = {}) => {
@@ -41,7 +43,6 @@ describe('The Pipeline Tabs', () => {
},
stubs: {
GlTab,
- TestReports: { template: '<div id="tests" />' },
},
}),
);
@@ -82,6 +83,7 @@ describe('The Pipeline Tabs', () => {
tabName | badgeComponent | badgeText
${'Jobs'} | ${findJobsBadge} | ${String(defaultProvide.totalJobCount)}
${'Failed Jobs'} | ${findFailedJobsBadge} | ${String(defaultProvide.failedJobsCount)}
+ ${'Tests'} | ${findTestsBadge} | ${String(defaultProvide.testsCount)}
`('shows badge for $tabName with the correct text', ({ badgeComponent, badgeText }) => {
createComponent();
diff --git a/spec/frontend/pipelines/components/pipelines_filtered_search_spec.js b/spec/frontend/pipelines/components/pipelines_filtered_search_spec.js
index 6c743f92116..f958f12acd4 100644
--- a/spec/frontend/pipelines/components/pipelines_filtered_search_spec.js
+++ b/spec/frontend/pipelines/components/pipelines_filtered_search_spec.js
@@ -102,7 +102,7 @@ describe('Pipelines filtered search', () => {
it('emits filterPipelines on submit with correct filter', () => {
findFilteredSearch().vm.$emit('submit', mockSearch);
- expect(wrapper.emitted('filterPipelines')).toBeTruthy();
+ expect(wrapper.emitted('filterPipelines')).toHaveLength(1);
expect(wrapper.emitted('filterPipelines')[0]).toEqual([mockSearch]);
});
diff --git a/spec/frontend/pipelines/components/pipelines_list/pipeline_stage_spec.js b/spec/frontend/pipelines/components/pipelines_list/pipeline_stage_spec.js
index 1ff32b03344..e712cdeaea2 100644
--- a/spec/frontend/pipelines/components/pipelines_list/pipeline_stage_spec.js
+++ b/spec/frontend/pipelines/components/pipelines_list/pipeline_stage_spec.js
@@ -1,4 +1,5 @@
import { GlDropdown } from '@gitlab/ui';
+import { nextTick } from 'vue';
import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
@@ -61,11 +62,10 @@ describe('Pipelines stage component', () => {
const findMergeTrainWarning = () => wrapper.find('[data-testid="warning-message-merge-trains"]');
const findLoadingState = () => wrapper.find('[data-testid="pipeline-stage-loading-state"]');
- const openStageDropdown = () => {
- findDropdownToggle().trigger('click');
- return new Promise((resolve) => {
- wrapper.vm.$root.$on('bv::dropdown::show', resolve);
- });
+ const openStageDropdown = async () => {
+ await findDropdownToggle().trigger('click');
+ await waitForPromises();
+ await nextTick();
};
describe('loading state', () => {
@@ -77,7 +77,10 @@ describe('Pipelines stage component', () => {
await openStageDropdown();
});
- it('displays loading state while jobs are being fetched', () => {
+ it('displays loading state while jobs are being fetched', async () => {
+ jest.runOnlyPendingTimers();
+ await nextTick();
+
expect(findLoadingState().exists()).toBe(true);
expect(findLoadingState().text()).toBe(PipelineStage.i18n.loadingText);
});
@@ -98,46 +101,41 @@ describe('Pipelines stage component', () => {
expect(glTooltipDirectiveMock.mock.calls[0][1].modifiers.ds0).toBe(true);
});
- it('should render a dropdown with the status icon', () => {
+ it('renders a dropdown with the status icon', () => {
expect(findDropdown().exists()).toBe(true);
expect(findDropdownToggle().exists()).toBe(true);
expect(findCiIcon().exists()).toBe(true);
});
- it('should render a borderless ci-icon', () => {
+ it('renders a borderless ci-icon', () => {
expect(findCiIcon().exists()).toBe(true);
expect(findCiIcon().props('isBorderless')).toBe(true);
expect(findCiIcon().classes('borderless')).toBe(true);
});
- it('should render a ci-icon with a custom border class', () => {
+ it('renders a ci-icon with a custom border class', () => {
expect(findCiIcon().exists()).toBe(true);
expect(findCiIcon().classes('gl-border')).toBe(true);
});
});
- describe('when update dropdown is changed', () => {
- beforeEach(() => {
- createComponent();
- });
- });
-
describe('when user opens dropdown and stage request is successful', () => {
beforeEach(async () => {
mock.onGet(dropdownPath).reply(200, stageReply);
createComponent();
await openStageDropdown();
+ await jest.runAllTimers();
await axios.waitForAll();
});
- it('should render the received data and emit `clickedDropdown` event', async () => {
+ it('renders the received data and emit `clickedDropdown` event', async () => {
expect(findDropdownMenu().text()).toContain(stageReply.latest_statuses[0].name);
expect(findDropdownMenuTitle().text()).toContain(stageReply.name);
expect(eventHub.$emit).toHaveBeenCalledWith('clickedDropdown');
});
- it('should refresh when updateDropdown is set to true', async () => {
+ it('refreshes when updateDropdown is set to true', async () => {
expect(mock.history.get).toHaveLength(1);
wrapper.setProps({ updateDropdown: true });
@@ -148,15 +146,14 @@ describe('Pipelines stage component', () => {
});
describe('when user opens dropdown and stage request fails', () => {
- beforeEach(async () => {
+ it('should close the dropdown', async () => {
mock.onGet(dropdownPath).reply(500);
createComponent();
await openStageDropdown();
await axios.waitForAll();
- });
+ await waitForPromises();
- it('should close the dropdown', () => {
expect(findDropdown().classes('show')).toBe(false);
});
});
@@ -181,26 +178,29 @@ describe('Pipelines stage component', () => {
it('should update the stage to request the new endpoint provided', async () => {
await openStageDropdown();
- await axios.waitForAll();
+ jest.runOnlyPendingTimers();
+ await waitForPromises();
expect(findDropdownMenu().text()).toContain('this is the updated content');
});
});
describe('pipelineActionRequestComplete', () => {
- beforeEach(() => {
+ beforeEach(async () => {
mock.onGet(dropdownPath).reply(200, stageReply);
mock.onPost(`${stageReply.latest_statuses[0].status.action.path}.json`).reply(200);
createComponent();
+ await waitForPromises();
+ await nextTick();
});
const clickCiAction = async () => {
await openStageDropdown();
- await axios.waitForAll();
+ jest.runOnlyPendingTimers();
+ await waitForPromises();
- findCiActionBtn().trigger('click');
- await axios.waitForAll();
+ await findCiActionBtn().trigger('click');
};
it('closes dropdown when job item action is clicked', async () => {
@@ -211,29 +211,30 @@ describe('Pipelines stage component', () => {
expect(hidden).toHaveBeenCalledTimes(0);
await clickCiAction();
+ await waitForPromises();
expect(hidden).toHaveBeenCalledTimes(1);
});
it('emits `pipelineActionRequestComplete` when job item action is clicked', async () => {
await clickCiAction();
+ await waitForPromises();
expect(wrapper.emitted('pipelineActionRequestComplete')).toHaveLength(1);
});
});
describe('With merge trains enabled', () => {
- beforeEach(async () => {
+ it('shows a warning on the dropdown', async () => {
mock.onGet(dropdownPath).reply(200, stageReply);
createComponent({
isMergeTrain: true,
});
await openStageDropdown();
- await axios.waitForAll();
- });
+ jest.runOnlyPendingTimers();
+ await waitForPromises();
- it('shows a warning on the dropdown', () => {
const warning = findMergeTrainWarning();
expect(warning.text()).toBe('Merge train pipeline jobs can not be retried');
diff --git a/spec/frontend/pipelines/graph/linked_pipeline_spec.js b/spec/frontend/pipelines/graph/linked_pipeline_spec.js
index cdeaa0db61d..7d1e4774a24 100644
--- a/spec/frontend/pipelines/graph/linked_pipeline_spec.js
+++ b/spec/frontend/pipelines/graph/linked_pipeline_spec.js
@@ -426,7 +426,7 @@ describe('Linked pipeline', () => {
jest.spyOn(wrapper.vm, '$emit');
findButton().trigger('click');
- expect(wrapper.emitted().pipelineClicked).toBeTruthy();
+ expect(wrapper.emitted().pipelineClicked).toHaveLength(1);
});
it(`should emit ${BV_HIDE_TOOLTIP} to close the tooltip`, () => {
diff --git a/spec/frontend/pipelines/performance_insights_modal_spec.js b/spec/frontend/pipelines/performance_insights_modal_spec.js
index b745eb1d78e..8c802be7718 100644
--- a/spec/frontend/pipelines/performance_insights_modal_spec.js
+++ b/spec/frontend/pipelines/performance_insights_modal_spec.js
@@ -20,6 +20,7 @@ describe('Performance insights modal', () => {
const findModal = () => wrapper.findComponent(GlModal);
const findAlert = () => wrapper.findComponent(GlAlert);
const findLink = () => wrapper.findComponent(GlLink);
+ const findLimitText = () => wrapper.findByTestId('limit-alert-text');
const findQueuedCardData = () => wrapper.findByTestId('insights-queued-card-data');
const findQueuedCardLink = () => wrapper.findByTestId('insights-queued-card-link');
const findExecutedCardData = () => wrapper.findByTestId('insights-executed-card-data');
@@ -62,8 +63,19 @@ describe('Performance insights modal', () => {
expect(findModal().exists()).toBe(true);
});
- it('does not dispaly alert', () => {
- expect(findAlert().exists()).toBe(false);
+ it('displays alert', () => {
+ expect(findAlert().exists()).toBe(true);
+ });
+
+ it('displays feedback issue link', () => {
+ expect(findLink().text()).toBe('Feedback issue');
+ expect(findLink().attributes('href')).toBe(
+ 'https://gitlab.com/gitlab-org/gitlab/-/issues/365902',
+ );
+ });
+
+ it('does not display limit text', () => {
+ expect(findLimitText().exists()).toBe(false);
});
describe('queued duration card', () => {
@@ -107,16 +119,13 @@ describe('Performance insights modal', () => {
});
});
- describe('limit alert', () => {
- it('displays limit alert when there is a next page', async () => {
+ describe('with next page', () => {
+ it('displays limit text when there is a next page', async () => {
createComponent([[getPerformanceInsights, getPerformanceInsightsNextPageHandler]]);
await waitForPromises();
- expect(findAlert().exists()).toBe(true);
- expect(findLink().attributes('href')).toBe(
- 'https://gitlab.com/gitlab-org/gitlab/-/issues/365902',
- );
+ expect(findLimitText().exists()).toBe(true);
});
});
});
diff --git a/spec/frontend/pipelines/pipeline_multi_actions_spec.js b/spec/frontend/pipelines/pipeline_multi_actions_spec.js
index e24d2e51f08..f554166da33 100644
--- a/spec/frontend/pipelines/pipeline_multi_actions_spec.js
+++ b/spec/frontend/pipelines/pipeline_multi_actions_spec.js
@@ -84,13 +84,22 @@ describe('Pipeline Multi Actions Dropdown', () => {
expect(wrapper.vm.artifacts).toEqual(artifacts);
});
- it('should render all the provided artifacts', () => {
- createComponent({ mockData: { artifacts } });
+ it('should render all the provided artifacts when search query is empty', () => {
+ const searchQuery = '';
+ createComponent({ mockData: { searchQuery, artifacts } });
expect(findAllArtifactItems()).toHaveLength(artifacts.length);
expect(findEmptyMessage().exists()).toBe(false);
});
+ it('should render filtered artifacts when search query is not empty', () => {
+ const searchQuery = 'job-2';
+ createComponent({ mockData: { searchQuery, artifacts } });
+
+ expect(findAllArtifactItems()).toHaveLength(1);
+ expect(findEmptyMessage().exists()).toBe(false);
+ });
+
it('should render the correct artifact name and path', () => {
createComponent({ mockData: { artifacts } });
diff --git a/spec/frontend/pipelines/pipeline_url_spec.js b/spec/frontend/pipelines/pipeline_url_spec.js
index c6104a13216..25a97ecf49d 100644
--- a/spec/frontend/pipelines/pipeline_url_spec.js
+++ b/spec/frontend/pipelines/pipeline_url_spec.js
@@ -61,14 +61,14 @@ describe('Pipeline Url Component', () => {
describe('commit user avatar', () => {
it('renders when commit author exists', () => {
const pipelineBranch = mockPipelineBranch();
- const { avatar_url, name, path } = pipelineBranch.pipeline.commit.author;
+ const { avatar_url: imgSrc, name, path } = pipelineBranch.pipeline.commit.author;
createComponent(pipelineBranch);
const component = wrapper.findComponent(UserAvatarLink);
expect(component.exists()).toBe(true);
expect(component.props()).toMatchObject({
imgSize: 16,
- imgSrc: avatar_url,
+ imgSrc,
imgAlt: name,
linkHref: path,
tooltipText: name,
diff --git a/spec/frontend/pipelines/pipelines_spec.js b/spec/frontend/pipelines/pipelines_spec.js
index ad6d650670a..0bed24e588e 100644
--- a/spec/frontend/pipelines/pipelines_spec.js
+++ b/spec/frontend/pipelines/pipelines_spec.js
@@ -45,6 +45,7 @@ describe('Pipelines', () => {
ciLintPath: '/ci/lint',
resetCachePath: `${mockProjectPath}/settings/ci_cd/reset_cache`,
newPipelinePath: `${mockProjectPath}/pipelines/new`,
+
ciRunnerSettingsPath: `${mockProjectPath}/-/settings/ci_cd#js-runners-settings`,
};
@@ -654,7 +655,12 @@ describe('Pipelines', () => {
// Mock init a polling cycle
wrapper.vm.poll.options.notificationCallback(true);
- findStagesDropdownToggle().trigger('click');
+ await findStagesDropdownToggle().trigger('click');
+ jest.runOnlyPendingTimers();
+
+ // cancelMock is getting overwritten in pipelines_service.js#L29
+ // so we have to spy on it again here
+ cancelMock = jest.spyOn(wrapper.vm.service.cancelationSource, 'cancel');
await waitForPromises();
@@ -664,7 +670,8 @@ describe('Pipelines', () => {
});
it('stops polling & restarts polling', async () => {
- findStagesDropdownToggle().trigger('click');
+ await findStagesDropdownToggle().trigger('click');
+ jest.runOnlyPendingTimers();
await waitForPromises();
expect(cancelMock).not.toHaveBeenCalled();
diff --git a/spec/frontend/pipelines/test_reports/test_reports_spec.js b/spec/frontend/pipelines/test_reports/test_reports_spec.js
index 3c3143b1865..9b9ee4172f9 100644
--- a/spec/frontend/pipelines/test_reports/test_reports_spec.js
+++ b/spec/frontend/pipelines/test_reports/test_reports_spec.js
@@ -94,8 +94,8 @@ describe('Test reports app', () => {
beforeEach(() => createComponent());
it('sets testReports and shows tests', () => {
- expect(wrapper.vm.testReports).toBeTruthy();
- expect(wrapper.vm.showTests).toBeTruthy();
+ expect(wrapper.vm.testReports).toEqual(expect.any(Object));
+ expect(wrapper.vm.showTests).toBe(true);
});
it('shows tests details', () => {
diff --git a/spec/frontend/projects/commits/components/author_select_spec.js b/spec/frontend/projects/commits/components/author_select_spec.js
index d11090cba8a..57e5ef0ed1d 100644
--- a/spec/frontend/projects/commits/components/author_select_spec.js
+++ b/spec/frontend/projects/commits/components/author_select_spec.js
@@ -71,7 +71,7 @@ describe('Author Select', () => {
wrapper.setData({ hasSearchParam: true });
await nextTick();
- expect(findDropdownContainer().attributes('disabled')).toBeFalsy();
+ expect(findDropdownContainer().attributes('disabled')).toBeUndefined();
});
it('has correct tooltip message', async () => {
@@ -91,13 +91,13 @@ describe('Author Select', () => {
wrapper.setData({ hasSearchParam: false });
await nextTick();
- expect(findDropdown().attributes('disabled')).toBeFalsy();
+ expect(findDropdown().attributes('disabled')).toBeUndefined();
});
it('hasSearchParam if user types a truthy string', () => {
wrapper.vm.setSearchParam('false');
- expect(wrapper.vm.hasSearchParam).toBeTruthy();
+ expect(wrapper.vm.hasSearchParam).toBe(true);
});
});
@@ -153,9 +153,9 @@ describe('Author Select', () => {
});
it('has the correct props', async () => {
- const [{ avatar_url, username }] = authors;
+ const [{ avatar_url: avatarUrl, username }] = authors;
const result = {
- avatarUrl: avatar_url,
+ avatarUrl,
secondaryText: username,
isChecked: true,
};
diff --git a/spec/frontend/projects/compare/components/app_spec.js b/spec/frontend/projects/compare/components/app_spec.js
index 18e7f2e0f6e..c9ffdf20c32 100644
--- a/spec/frontend/projects/compare/components/app_spec.js
+++ b/spec/frontend/projects/compare/components/app_spec.js
@@ -34,7 +34,8 @@ describe('CompareApp component', () => {
expect(wrapper.props()).toEqual(
expect.objectContaining({
projectCompareIndexPath: defaultProps.projectCompareIndexPath,
- refsProjectPath: defaultProps.refsProjectPath,
+ sourceProjectRefsPath: defaultProps.sourceProjectRefsPath,
+ targetProjectRefsPath: defaultProps.targetProjectRefsPath,
paramsFrom: defaultProps.paramsFrom,
paramsTo: defaultProps.paramsTo,
}),
diff --git a/spec/frontend/projects/compare/components/mock_data.js b/spec/frontend/projects/compare/components/mock_data.js
index 61309928c26..81d64469a2a 100644
--- a/spec/frontend/projects/compare/components/mock_data.js
+++ b/spec/frontend/projects/compare/components/mock_data.js
@@ -1,7 +1,12 @@
-const refsProjectPath = 'some/refs/path';
+const sourceProjectRefsPath = 'some/refs/path';
+const targetProjectRefsPath = 'some/refs/path';
const paramsName = 'to';
const paramsBranch = 'main';
-const defaultProject = {
+const sourceProject = {
+ name: 'some-to-name',
+ id: '2',
+};
+const targetProject = {
name: 'some-to-name',
id: '1',
};
@@ -9,29 +14,31 @@ const defaultProject = {
export const appDefaultProps = {
projectCompareIndexPath: 'some/path',
projectMergeRequestPath: '',
- projects: [defaultProject],
+ projects: [sourceProject],
paramsFrom: 'main',
paramsTo: 'target/branch',
createMrPath: '',
- refsProjectPath,
- defaultProject,
+ sourceProjectRefsPath,
+ targetProjectRefsPath,
+ sourceProject,
+ targetProject,
};
export const revisionCardDefaultProps = {
- selectedProject: defaultProject,
+ selectedProject: targetProject,
paramsBranch,
revisionText: 'Source',
- refsProjectPath,
+ refsProjectPath: sourceProjectRefsPath,
paramsName,
};
export const repoDropdownDefaultProps = {
- selectedProject: defaultProject,
+ selectedProject: targetProject,
paramsName,
};
export const revisionDropdownDefaultProps = {
- refsProjectPath,
+ refsProjectPath: sourceProjectRefsPath,
paramsBranch,
paramsName,
};
diff --git a/spec/frontend/projects/project_new_spec.js b/spec/frontend/projects/project_new_spec.js
index 3034037fb1d..4fcecc3a307 100644
--- a/spec/frontend/projects/project_new_spec.js
+++ b/spec/frontend/projects/project_new_spec.js
@@ -1,6 +1,7 @@
import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import { TEST_HOST } from 'helpers/test_constants';
import projectNew from '~/projects/project_new';
+import { mockTracking, triggerEvent, unmockTracking } from 'helpers/tracking_helper';
describe('New Project', () => {
let $projectImportUrl;
@@ -12,21 +13,27 @@ describe('New Project', () => {
beforeEach(() => {
setHTMLFixture(`
- <div class='toggle-import-form'>
- <div class='import-url-data'>
- <div class="form-group">
- <input id="project_import_url" />
- </div>
- <div id="import-url-auth-method">
- <div class="form-group">
- <input id="project-import-url-user" />
+ <div class="tab-pane active">
+ <div class='toggle-import-form'>
+ <form id="new_project">
+ <div class='import-url-data'>
+ <div class="form-group">
+ <input id="project_import_url" />
+ </div>
+ <div id="import-url-auth-method">
+ <div class="form-group">
+ <input id="project-import-url-user" />
+ </div>
+ <div class="form-group">
+ <input id="project_import_url_password" />
+ </div>
+ </div>
+ <input id="project_name" />
+ <input id="project_path" />
</div>
- <div class="form-group">
- <input id="project_import_url_password" />
- </div>
- </div>
- <input id="project_name" />
- <input id="project_path" />
+ <div class="js-user-readme-repo"></div>
+ <button class="js-create-project-button"/>
+ </form>
</div>
</div>
`);
@@ -45,6 +52,38 @@ describe('New Project', () => {
el.value = value;
};
+ describe('tracks manual path input', () => {
+ let trackingSpy;
+
+ beforeEach(() => {
+ trackingSpy = mockTracking('_category_', undefined, jest.spyOn);
+ projectNew.bindEvents();
+ $projectPath.oldInputValue = '_old_value_';
+ });
+
+ afterEach(() => {
+ unmockTracking();
+ });
+
+ it('tracks the event', () => {
+ $projectPath.value = '_new_value_';
+
+ triggerEvent($projectPath, 'blur');
+
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, 'user_input_path_slug', {
+ label: 'new_project_form',
+ });
+ });
+
+ it('does not track the event when there has been no change', () => {
+ $projectPath.value = '_old_value_';
+
+ triggerEvent($projectPath, 'blur');
+
+ expect(trackingSpy).not.toHaveBeenCalled();
+ });
+ });
+
describe('deriveProjectPathFromUrl', () => {
const dummyImportUrl = `${TEST_HOST}/dummy/import/url.git`;
diff --git a/spec/frontend/projects/settings/branch_rules/branch_dropdown_spec.js b/spec/frontend/projects/settings/branch_rules/branch_dropdown_spec.js
index 5997c2a083c..79bce5a4b3f 100644
--- a/spec/frontend/projects/settings/branch_rules/branch_dropdown_spec.js
+++ b/spec/frontend/projects/settings/branch_rules/branch_dropdown_spec.js
@@ -1,6 +1,6 @@
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
-import { GlDropdown, GlSearchBoxByType, GlDropdownItem } from '@gitlab/ui';
+import { GlDropdown, GlSearchBoxByType, GlDropdownItem, GlSprintf } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import BranchDropdown, {
i18n,
@@ -36,15 +36,20 @@ describe('Branch dropdown', () => {
await waitForPromises();
};
- const findGlDropdown = () => wrapper.find(GlDropdown);
- const findAllBranches = () => wrapper.findAll(GlDropdownItem);
+ const findGlDropdown = () => wrapper.findComponent(GlDropdown);
+ const findAllBranches = () => wrapper.findAllComponents(GlDropdownItem);
const findNoDataMsg = () => wrapper.findByTestId('no-data');
- const findGlSearchBoxByType = () => wrapper.find(GlSearchBoxByType);
+ const findGlSearchBoxByType = () => wrapper.findComponent(GlSearchBoxByType);
const findWildcardButton = () => wrapper.findByTestId('create-wildcard-button');
+ const findHelpText = () => wrapper.findComponent(GlSprintf);
const setSearchTerm = (searchTerm) => findGlSearchBoxByType().vm.$emit('input', searchTerm);
beforeEach(() => createComponent());
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
it('renders a GlDropdown component with the correct props', () => {
expect(findGlDropdown().props()).toMatchObject({ text: value });
});
@@ -85,6 +90,10 @@ describe('Branch dropdown', () => {
findWildcardButton().vm.$emit('click');
expect(wrapper.emitted('createWildcard')).toEqual([[searchTerm]]);
});
+
+ it('renders help text', () => {
+ expect(findHelpText().attributes('message')).toBe(i18n.branchHelpText);
+ });
});
it('displays an error message if fetch failed', async () => {
diff --git a/spec/frontend/projects/settings/branch_rules/components/protections/index_spec.js b/spec/frontend/projects/settings/branch_rules/components/protections/index_spec.js
new file mode 100644
index 00000000000..3592fa50622
--- /dev/null
+++ b/spec/frontend/projects/settings/branch_rules/components/protections/index_spec.js
@@ -0,0 +1,57 @@
+import { nextTick } from 'vue';
+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';
+
+describe('Branch Protections', () => {
+ let wrapper;
+
+ const createComponent = async () => {
+ wrapper = mountExtended(Protections, {
+ propsData: { protections },
+ });
+ await nextTick();
+ };
+
+ const findHeading = () => wrapper.find('h4');
+ const findHelpText = () => wrapper.findByTestId('protections-help-text');
+ const findHelpLink = () => wrapper.findComponent(GlLink);
+ const findPushProtections = () => wrapper.findComponent(PushProtections);
+ const findMergeProtections = () => wrapper.findComponent(MergeProtections);
+
+ beforeEach(() => createComponent());
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('renders a heading', () => {
+ expect(findHeading().text()).toBe(i18n.protections);
+ });
+
+ it('renders help text', () => {
+ expect(findHelpText().text()).toMatchInterpolatedText(i18n.protectionsHelpText);
+ expect(findHelpLink().attributes('href')).toBe('/help/user/project/protected_branches');
+ });
+
+ it('renders a PushProtections component with correct props', () => {
+ expect(findPushProtections().props('membersAllowedToPush')).toStrictEqual(
+ protections.membersAllowedToPush,
+ );
+ expect(findPushProtections().props('allowForcePush')).toBe(protections.allowForcePush);
+ });
+
+ it('renders a MergeProtections component with correct props', () => {
+ expect(findMergeProtections().props('membersAllowedToMerge')).toStrictEqual(
+ protections.membersAllowedToMerge,
+ );
+ expect(findMergeProtections().props('requireCodeOwnersApproval')).toBe(
+ protections.requireCodeOwnersApproval,
+ );
+ });
+});
diff --git a/spec/frontend/projects/settings/branch_rules/components/protections/merge_protections_spec.js b/spec/frontend/projects/settings/branch_rules/components/protections/merge_protections_spec.js
new file mode 100644
index 00000000000..0e168a2ad78
--- /dev/null
+++ b/spec/frontend/projects/settings/branch_rules/components/protections/merge_protections_spec.js
@@ -0,0 +1,53 @@
+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';
+
+describe('Merge Protections', () => {
+ let wrapper;
+
+ const propsData = {
+ membersAllowedToMerge,
+ requireCodeOwnersApproval,
+ };
+
+ const createComponent = () => {
+ wrapper = mountExtended(MergeProtections, {
+ propsData,
+ });
+ };
+
+ const findFormGroup = () => wrapper.findComponent(GlFormGroup);
+ const findCodeOwnersApprovalCheckbox = () => wrapper.findComponent(GlFormCheckbox);
+
+ beforeEach(() => createComponent());
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('renders a form group with the correct label', () => {
+ expect(findFormGroup().text()).toContain(i18n.allowedToMerge);
+ });
+
+ describe('Require code owners approval checkbox', () => {
+ it('renders a checkbox with the correct props', () => {
+ expect(findCodeOwnersApprovalCheckbox().vm.$attrs.checked).toBe(
+ propsData.requireCodeOwnersApproval,
+ );
+ });
+
+ it('renders help text', () => {
+ expect(findCodeOwnersApprovalCheckbox().text()).toContain(i18n.requireApprovalTitle);
+ expect(findCodeOwnersApprovalCheckbox().text()).toContain(i18n.requireApprovalHelpText);
+ });
+
+ it('emits a change-allow-force-push event when changed', () => {
+ findCodeOwnersApprovalCheckbox().vm.$emit('change', false);
+
+ expect(wrapper.emitted('change-require-code-owners-approval')[0]).toEqual([false]);
+ });
+ });
+});
diff --git a/spec/frontend/projects/settings/branch_rules/components/protections/push_protections_spec.js b/spec/frontend/projects/settings/branch_rules/components/protections/push_protections_spec.js
new file mode 100644
index 00000000000..d54dad08338
--- /dev/null
+++ b/spec/frontend/projects/settings/branch_rules/components/protections/push_protections_spec.js
@@ -0,0 +1,50 @@
+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';
+
+describe('Push Protections', () => {
+ let wrapper;
+ const propsData = {
+ membersAllowedToPush,
+ allowForcePush,
+ };
+
+ const createComponent = () => {
+ wrapper = shallowMountExtended(PushProtections, {
+ propsData,
+ });
+ };
+
+ const findFormGroup = () => wrapper.findComponent(GlFormGroup);
+ const findAllowForcePushCheckbox = () => wrapper.findComponent(GlFormCheckbox);
+ const findHelpText = () => wrapper.findComponent(GlSprintf);
+
+ beforeEach(() => createComponent());
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('renders a form group with the correct label', () => {
+ expect(findFormGroup().attributes('label')).toBe(i18n.allowedToPush);
+ });
+
+ describe('Allow force push checkbox', () => {
+ it('renders a checkbox with the correct props', () => {
+ expect(findAllowForcePushCheckbox().vm.$attrs.checked).toBe(propsData.allowForcePush);
+ });
+
+ it('renders help text', () => {
+ expect(findHelpText().attributes('message')).toBe(i18n.forcePushTitle);
+ });
+
+ it('emits a change-allow-force-push event when changed', () => {
+ findAllowForcePushCheckbox().vm.$emit('change', false);
+
+ expect(wrapper.emitted('change-allow-force-push')[0]).toEqual([false]);
+ });
+ });
+});
diff --git a/spec/frontend/projects/settings/branch_rules/mock_data.js b/spec/frontend/projects/settings/branch_rules/mock_data.js
new file mode 100644
index 00000000000..32cca027d19
--- /dev/null
+++ b/spec/frontend/projects/settings/branch_rules/mock_data.js
@@ -0,0 +1,10 @@
+export const membersAllowedToPush = ['Maintainers', 'Developers'];
+export const allowForcePush = false;
+export const membersAllowedToMerge = ['Maintainers'];
+export const requireCodeOwnersApproval = false;
+export const protections = {
+ membersAllowedToPush,
+ allowForcePush,
+ membersAllowedToMerge,
+ requireCodeOwnersApproval,
+};
diff --git a/spec/frontend/projects/settings/branch_rules/rule_edit_spec.js b/spec/frontend/projects/settings/branch_rules/rule_edit_spec.js
index 66ae6ddc02d..b0b2b9191d4 100644
--- a/spec/frontend/projects/settings/branch_rules/rule_edit_spec.js
+++ b/spec/frontend/projects/settings/branch_rules/rule_edit_spec.js
@@ -3,9 +3,12 @@ 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';
jest.mock('~/lib/utils/url_utility', () => ({
getParameterByName: jest.fn().mockImplementation(() => 'main'),
+ joinPaths: jest.fn(),
+ setUrlFragment: jest.fn(),
}));
describe('Edit branch rule', () => {
@@ -16,10 +19,15 @@ describe('Edit branch rule', () => {
wrapper = shallowMountExtended(RuleEdit, { propsData: { projectPath } });
};
- const findBranchDropdown = () => wrapper.find(BranchDropdown);
+ const findBranchDropdown = () => wrapper.findComponent(BranchDropdown);
+ const findProtections = () => wrapper.findComponent(Protections);
beforeEach(() => createComponent());
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
it('gets the branch param from url', () => {
expect(getParameterByName).toHaveBeenCalledWith('branch');
});
@@ -46,4 +54,55 @@ describe('Edit branch rule', () => {
expect(findBranchDropdown().props('value')).toBe(wildcard);
});
});
+
+ describe('Protections', () => {
+ it('renders a Protections component with the correct props', () => {
+ expect(findProtections().props('protections')).toMatchObject({
+ membersAllowedToPush: [],
+ allowForcePush: false,
+ membersAllowedToMerge: [],
+ requireCodeOwnersApproval: false,
+ });
+ });
+
+ it('updates protections when change-allowed-to-push-members is emitted', async () => {
+ const membersAllowedToPush = ['test'];
+ findProtections().vm.$emit('change-allowed-to-push-members', membersAllowedToPush);
+ await nextTick();
+
+ expect(findProtections().props('protections')).toEqual(
+ expect.objectContaining({ membersAllowedToPush }),
+ );
+ });
+
+ it('updates protections when change-allow-force-push is emitted', async () => {
+ const allowForcePush = true;
+ findProtections().vm.$emit('change-allow-force-push', allowForcePush);
+ await nextTick();
+
+ expect(findProtections().props('protections')).toEqual(
+ expect.objectContaining({ allowForcePush }),
+ );
+ });
+
+ it('updates protections when change-allowed-to-merge-members is emitted', async () => {
+ const membersAllowedToMerge = ['test'];
+ findProtections().vm.$emit('change-allowed-to-merge-members', membersAllowedToMerge);
+ await nextTick();
+
+ expect(findProtections().props('protections')).toEqual(
+ expect.objectContaining({ membersAllowedToMerge }),
+ );
+ });
+
+ it('updates protections when change-require-code-owners-approval is emitted', async () => {
+ const requireCodeOwnersApproval = true;
+ findProtections().vm.$emit('change-require-code-owners-approval', requireCodeOwnersApproval);
+ await nextTick();
+
+ expect(findProtections().props('protections')).toEqual(
+ expect.objectContaining({ requireCodeOwnersApproval }),
+ );
+ });
+ });
});
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 85b09ced024..bde7148078d 100644
--- a/spec/frontend/projects/settings/components/transfer_project_form_spec.js
+++ b/spec/frontend/projects/settings/components/transfer_project_form_spec.js
@@ -1,11 +1,19 @@
+import Vue, { nextTick } from 'vue';
+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 { 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 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 waitForPromises from 'helpers/wait_for_promises';
describe('Transfer project form', () => {
let wrapper;
@@ -13,36 +21,50 @@ describe('Transfer project form', () => {
const confirmButtonText = 'Confirm';
const confirmationPhrase = 'You must construct additional pylons!';
- const createComponent = () =>
- shallowMountExtended(TransferProjectForm, {
+ const runDebounce = () => jest.runAllTimers();
+
+ Vue.use(VueApollo);
+
+ const defaultQueryHandler = jest
+ .fn()
+ .mockResolvedValue(searchNamespacesWhereUserCanTransferProjectsQueryResponsePage1);
+
+ const createComponent = ({
+ requestHandlers = [[searchNamespacesWhereUserCanTransferProjectsQuery, defaultQueryHandler]],
+ } = {}) => {
+ wrapper = shallowMountExtended(TransferProjectForm, {
propsData: {
userNamespaces,
groupNamespaces,
confirmButtonText,
confirmationPhrase,
},
+ apolloProvider: createMockApollo(requestHandlers),
});
+ };
const findNamespaceSelect = () => wrapper.findComponent(NamespaceSelect);
const findConfirmDanger = () => wrapper.findComponent(ConfirmDanger);
- beforeEach(() => {
- wrapper = createComponent();
- });
-
afterEach(() => {
wrapper.destroy();
});
it('renders the namespace selector', () => {
+ createComponent();
+
expect(findNamespaceSelect().exists()).toBe(true);
});
it('renders the confirm button', () => {
+ createComponent();
+
expect(findConfirmDanger().exists()).toBe(true);
});
it('disables the confirm button by default', () => {
+ createComponent();
+
expect(findConfirmDanger().attributes('disabled')).toBe('true');
});
@@ -50,6 +72,8 @@ describe('Transfer project form', () => {
const [selectedItem] = groupNamespaces;
beforeEach(() => {
+ createComponent();
+
findNamespaceSelect().vm.$emit('select', selectedItem);
});
@@ -69,4 +93,132 @@ describe('Transfer project form', () => {
expect(wrapper.emitted('confirm')).toBeDefined();
});
});
+
+ it('passes correct props to `NamespaceSelect` component', async () => {
+ createComponent();
+
+ runDebounce();
+ await waitForPromises();
+
+ 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,
+ });
+ });
+
+ describe('when `search` event is fired', () => {
+ const arrange = async () => {
+ createComponent();
+
+ findNamespaceSelect().vm.$emit('search', 'foo');
+
+ await nextTick();
+ };
+
+ it('sets `isSearchLoading` prop to `true`', async () => {
+ await arrange();
+
+ expect(findNamespaceSelect().props('isSearchLoading')).toBe(true);
+ });
+
+ it('passes `search` variable to query', async () => {
+ await arrange();
+
+ runDebounce();
+ await waitForPromises();
+
+ expect(defaultQueryHandler).toHaveBeenCalledWith(expect.objectContaining({ search: 'foo' }));
+ });
+ });
+
+ describe('when `load-more-groups` event is fired', () => {
+ let queryHandler;
+
+ const arrange = async () => {
+ queryHandler = jest.fn();
+ queryHandler.mockResolvedValueOnce(
+ searchNamespacesWhereUserCanTransferProjectsQueryResponsePage1,
+ );
+ queryHandler.mockResolvedValueOnce(
+ searchNamespacesWhereUserCanTransferProjectsQueryResponsePage2,
+ );
+
+ createComponent({
+ requestHandlers: [[searchNamespacesWhereUserCanTransferProjectsQuery, queryHandler]],
+ });
+
+ runDebounce();
+ await waitForPromises();
+
+ findNamespaceSelect().vm.$emit('load-more-groups');
+ await nextTick();
+ };
+
+ it('sets `isLoadingMoreGroups` prop to `true`', async () => {
+ await arrange();
+
+ expect(findNamespaceSelect().props('isLoadingMoreGroups')).toBe(true);
+ });
+
+ it('passes `after` and `first` variables to query', async () => {
+ await arrange();
+
+ runDebounce();
+ await waitForPromises();
+
+ expect(queryHandler).toHaveBeenCalledWith(
+ expect.objectContaining({
+ first: 25,
+ after:
+ searchNamespacesWhereUserCanTransferProjectsQueryResponsePage1.data.currentUser.groups
+ .pageInfo.endCursor,
+ }),
+ );
+ });
+
+ 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,
+ })),
+ );
+ });
+
+ it('updates `hasNextPageOfGroups` prop', async () => {
+ await arrange();
+
+ runDebounce();
+ await waitForPromises();
+
+ expect(findNamespaceSelect().props('hasNextPageOfGroups')).toBe(false);
+ });
+ });
});
diff --git a/spec/frontend/prometheus_metrics/custom_metrics_spec.js b/spec/frontend/prometheus_metrics/custom_metrics_spec.js
index fc906194059..a079b0b97fd 100644
--- a/spec/frontend/prometheus_metrics/custom_metrics_spec.js
+++ b/spec/frontend/prometheus_metrics/custom_metrics_spec.js
@@ -50,39 +50,33 @@ describe('PrometheusMetrics', () => {
customMetrics.showMonitoringCustomMetricsPanelState(PANEL_STATE.LOADING);
expect(customMetrics.$monitoredCustomMetricsLoading.hasClass('hidden')).toEqual(false);
- expect(customMetrics.$monitoredCustomMetricsEmpty.hasClass('hidden')).toBeTruthy();
- expect(customMetrics.$monitoredCustomMetricsList.hasClass('hidden')).toBeTruthy();
- expect(
- customMetrics.$monitoredCustomMetricsNoIntegrationText.hasClass('hidden'),
- ).toBeTruthy();
-
- expect(customMetrics.$newCustomMetricButton.hasClass('hidden')).toBeTruthy();
- expect(customMetrics.$newCustomMetricText.hasClass('hidden')).toBeTruthy();
+ expect(customMetrics.$monitoredCustomMetricsEmpty.hasClass('hidden')).toBe(true);
+ expect(customMetrics.$monitoredCustomMetricsList.hasClass('hidden')).toBe(true);
+ expect(customMetrics.$monitoredCustomMetricsNoIntegrationText.hasClass('hidden')).toBe(true);
+
+ expect(customMetrics.$newCustomMetricButton.hasClass('hidden')).toBe(true);
+ expect(customMetrics.$newCustomMetricText.hasClass('hidden')).toBe(true);
});
it('should show metrics list when called with `list`', () => {
customMetrics.showMonitoringCustomMetricsPanelState(PANEL_STATE.LIST);
- expect(customMetrics.$monitoredCustomMetricsLoading.hasClass('hidden')).toBeTruthy();
- expect(customMetrics.$monitoredCustomMetricsEmpty.hasClass('hidden')).toBeTruthy();
+ expect(customMetrics.$monitoredCustomMetricsLoading.hasClass('hidden')).toBe(true);
+ expect(customMetrics.$monitoredCustomMetricsEmpty.hasClass('hidden')).toBe(true);
expect(customMetrics.$monitoredCustomMetricsList.hasClass('hidden')).toEqual(false);
- expect(
- customMetrics.$monitoredCustomMetricsNoIntegrationText.hasClass('hidden'),
- ).toBeTruthy();
+ expect(customMetrics.$monitoredCustomMetricsNoIntegrationText.hasClass('hidden')).toBe(true);
expect(customMetrics.$newCustomMetricButton.hasClass('hidden')).toEqual(false);
- expect(customMetrics.$newCustomMetricText.hasClass('hidden')).toBeTruthy();
+ expect(customMetrics.$newCustomMetricText.hasClass('hidden')).toBe(true);
});
it('should show empty state when called with `empty`', () => {
customMetrics.showMonitoringCustomMetricsPanelState(PANEL_STATE.EMPTY);
- expect(customMetrics.$monitoredCustomMetricsLoading.hasClass('hidden')).toBeTruthy();
+ expect(customMetrics.$monitoredCustomMetricsLoading.hasClass('hidden')).toBe(true);
expect(customMetrics.$monitoredCustomMetricsEmpty.hasClass('hidden')).toEqual(false);
- expect(customMetrics.$monitoredCustomMetricsList.hasClass('hidden')).toBeTruthy();
- expect(
- customMetrics.$monitoredCustomMetricsNoIntegrationText.hasClass('hidden'),
- ).toBeTruthy();
+ expect(customMetrics.$monitoredCustomMetricsList.hasClass('hidden')).toBe(true);
+ expect(customMetrics.$monitoredCustomMetricsNoIntegrationText.hasClass('hidden')).toBe(true);
expect(customMetrics.$newCustomMetricButton.hasClass('hidden')).toEqual(false);
expect(customMetrics.$newCustomMetricText.hasClass('hidden')).toEqual(false);
@@ -94,14 +88,12 @@ describe('PrometheusMetrics', () => {
const $metricsListLi = customMetrics.$monitoredCustomMetricsList.find('li');
- expect(customMetrics.$monitoredCustomMetricsLoading.hasClass('hidden')).toBeTruthy();
+ expect(customMetrics.$monitoredCustomMetricsLoading.hasClass('hidden')).toBe(true);
expect(customMetrics.$monitoredCustomMetricsList.hasClass('hidden')).toEqual(false);
- expect(
- customMetrics.$monitoredCustomMetricsNoIntegrationText.hasClass('hidden'),
- ).toBeTruthy();
+ expect(customMetrics.$monitoredCustomMetricsNoIntegrationText.hasClass('hidden')).toBe(true);
expect(customMetrics.$newCustomMetricButton.hasClass('hidden')).toEqual(false);
- expect(customMetrics.$newCustomMetricText.hasClass('hidden')).toBeTruthy();
+ expect(customMetrics.$newCustomMetricText.hasClass('hidden')).toBe(true);
expect($metricsListLi.length).toEqual(metrics.length);
});
@@ -114,10 +106,10 @@ describe('PrometheusMetrics', () => {
false,
);
- expect(customMetrics.$monitoredCustomMetricsLoading.hasClass('hidden')).toBeTruthy();
- expect(customMetrics.$monitoredCustomMetricsList.hasClass('hidden')).toBeTruthy();
- expect(customMetrics.$newCustomMetricButton.hasClass('hidden')).toBeTruthy();
- expect(customMetrics.$newCustomMetricText.hasClass('hidden')).toBeTruthy();
+ expect(customMetrics.$monitoredCustomMetricsLoading.hasClass('hidden')).toBe(true);
+ expect(customMetrics.$monitoredCustomMetricsList.hasClass('hidden')).toBe(true);
+ expect(customMetrics.$newCustomMetricButton.hasClass('hidden')).toBe(true);
+ expect(customMetrics.$newCustomMetricText.hasClass('hidden')).toBe(true);
});
});
});
diff --git a/spec/frontend/prometheus_metrics/prometheus_metrics_spec.js b/spec/frontend/prometheus_metrics/prometheus_metrics_spec.js
index 0df2aad5882..a65cbe1a47a 100644
--- a/spec/frontend/prometheus_metrics/prometheus_metrics_spec.js
+++ b/spec/frontend/prometheus_metrics/prometheus_metrics_spec.js
@@ -54,25 +54,25 @@ describe('PrometheusMetrics', () => {
it('should show loading state when called with `loading`', () => {
prometheusMetrics.showMonitoringMetricsPanelState(PANEL_STATE.LOADING);
- expect(prometheusMetrics.$monitoredMetricsLoading.hasClass('hidden')).toBeFalsy();
- expect(prometheusMetrics.$monitoredMetricsEmpty.hasClass('hidden')).toBeTruthy();
- expect(prometheusMetrics.$monitoredMetricsList.hasClass('hidden')).toBeTruthy();
+ expect(prometheusMetrics.$monitoredMetricsLoading.hasClass('hidden')).toBe(false);
+ expect(prometheusMetrics.$monitoredMetricsEmpty.hasClass('hidden')).toBe(true);
+ expect(prometheusMetrics.$monitoredMetricsList.hasClass('hidden')).toBe(true);
});
it('should show metrics list when called with `list`', () => {
prometheusMetrics.showMonitoringMetricsPanelState(PANEL_STATE.LIST);
- expect(prometheusMetrics.$monitoredMetricsLoading.hasClass('hidden')).toBeTruthy();
- expect(prometheusMetrics.$monitoredMetricsEmpty.hasClass('hidden')).toBeTruthy();
- expect(prometheusMetrics.$monitoredMetricsList.hasClass('hidden')).toBeFalsy();
+ expect(prometheusMetrics.$monitoredMetricsLoading.hasClass('hidden')).toBe(true);
+ expect(prometheusMetrics.$monitoredMetricsEmpty.hasClass('hidden')).toBe(true);
+ expect(prometheusMetrics.$monitoredMetricsList.hasClass('hidden')).toBe(false);
});
it('should show empty state when called with `empty`', () => {
prometheusMetrics.showMonitoringMetricsPanelState(PANEL_STATE.EMPTY);
- expect(prometheusMetrics.$monitoredMetricsLoading.hasClass('hidden')).toBeTruthy();
- expect(prometheusMetrics.$monitoredMetricsEmpty.hasClass('hidden')).toBeFalsy();
- expect(prometheusMetrics.$monitoredMetricsList.hasClass('hidden')).toBeTruthy();
+ expect(prometheusMetrics.$monitoredMetricsLoading.hasClass('hidden')).toBe(true);
+ expect(prometheusMetrics.$monitoredMetricsEmpty.hasClass('hidden')).toBe(false);
+ expect(prometheusMetrics.$monitoredMetricsList.hasClass('hidden')).toBe(true);
});
});
@@ -88,8 +88,8 @@ describe('PrometheusMetrics', () => {
const $metricsListLi = prometheusMetrics.$monitoredMetricsList.find('li');
- expect(prometheusMetrics.$monitoredMetricsLoading.hasClass('hidden')).toBeTruthy();
- expect(prometheusMetrics.$monitoredMetricsList.hasClass('hidden')).toBeFalsy();
+ expect(prometheusMetrics.$monitoredMetricsLoading.hasClass('hidden')).toBe(true);
+ expect(prometheusMetrics.$monitoredMetricsList.hasClass('hidden')).toBe(false);
expect(prometheusMetrics.$monitoredMetricsCount.text()).toEqual(
'3 exporters with 12 metrics were found',
@@ -102,8 +102,8 @@ describe('PrometheusMetrics', () => {
it('should show missing environment variables list', () => {
prometheusMetrics.populateActiveMetrics(missingVarMetrics);
- expect(prometheusMetrics.$monitoredMetricsLoading.hasClass('hidden')).toBeTruthy();
- expect(prometheusMetrics.$missingEnvVarPanel.hasClass('hidden')).toBeFalsy();
+ expect(prometheusMetrics.$monitoredMetricsLoading.hasClass('hidden')).toBe(true);
+ expect(prometheusMetrics.$missingEnvVarPanel.hasClass('hidden')).toBe(false);
expect(prometheusMetrics.$missingEnvVarMetricCount.text()).toEqual('2');
expect(prometheusMetrics.$missingEnvVarPanel.find('li').length).toEqual(2);
@@ -143,12 +143,12 @@ describe('PrometheusMetrics', () => {
prometheusMetrics.loadActiveMetrics();
- expect(prometheusMetrics.$monitoredMetricsLoading.hasClass('hidden')).toBeFalsy();
+ expect(prometheusMetrics.$monitoredMetricsLoading.hasClass('hidden')).toBe(false);
expect(axios.get).toHaveBeenCalledWith(prometheusMetrics.activeMetricsEndpoint);
await waitForPromises();
- expect(prometheusMetrics.$monitoredMetricsLoading.hasClass('hidden')).toBeTruthy();
+ expect(prometheusMetrics.$monitoredMetricsLoading.hasClass('hidden')).toBe(true);
});
it('should show empty state if response failed to load', async () => {
@@ -158,8 +158,8 @@ describe('PrometheusMetrics', () => {
await waitForPromises();
- expect(prometheusMetrics.$monitoredMetricsLoading.hasClass('hidden')).toBeTruthy();
- expect(prometheusMetrics.$monitoredMetricsEmpty.hasClass('hidden')).toBeFalsy();
+ expect(prometheusMetrics.$monitoredMetricsLoading.hasClass('hidden')).toBe(true);
+ expect(prometheusMetrics.$monitoredMetricsEmpty.hasClass('hidden')).toBe(false);
});
it('should populate metrics list once response is loaded', async () => {
diff --git a/spec/frontend/releases/__snapshots__/util_spec.js.snap b/spec/frontend/releases/__snapshots__/util_spec.js.snap
index 90a33152877..55e3dda60a0 100644
--- a/spec/frontend/releases/__snapshots__/util_spec.js.snap
+++ b/spec/frontend/releases/__snapshots__/util_spec.js.snap
@@ -55,6 +55,7 @@ Object {
"commitPath": "http://localhost/releases-namespace/releases-project/-/commit/b83d6e391c22777fca1ed3012fce84f633d7fed0",
"descriptionHtml": "<p data-sourcepos=\\"1:1-1:23\\" dir=\\"auto\\">An okay release <gl-emoji title=\\"shrug\\" data-name=\\"shrug\\" data-unicode-version=\\"9.0\\">🤷</gl-emoji></p>",
"evidences": Array [],
+ "historicalRelease": false,
"milestones": Array [],
"name": "The second release",
"releasedAt": 2019-01-10T00:00:00.000Z,
@@ -159,6 +160,7 @@ Object {
"sha": "760d6cdfb0879c3ffedec13af470e0f71cf52c6cde4d",
},
],
+ "historicalRelease": false,
"milestones": Array [
Object {
"__typename": "Milestone",
@@ -208,6 +210,7 @@ exports[`releases/util.js convertOneReleaseForEditingGraphQLResponse matches sna
Object {
"data": Object {
"_links": Object {
+ "__typename": "ReleaseLinks",
"self": "http://localhost/releases-namespace/releases-project/-/releases/v1.1",
"selfUrl": "http://localhost/releases-namespace/releases-project/-/releases/v1.1",
},
@@ -215,6 +218,7 @@ Object {
"count": undefined,
"links": Array [
Object {
+ "__typename": "ReleaseAssetLink",
"directAssetPath": "/binaries/awesome-app-3",
"id": "gid://gitlab/Releases::Link/13",
"linkType": "image",
@@ -222,6 +226,7 @@ Object {
"url": "https://example.com/image",
},
Object {
+ "__typename": "ReleaseAssetLink",
"directAssetPath": "/binaries/awesome-app-2",
"id": "gid://gitlab/Releases::Link/12",
"linkType": "package",
@@ -229,6 +234,7 @@ Object {
"url": "https://example.com/package",
},
Object {
+ "__typename": "ReleaseAssetLink",
"directAssetPath": "/binaries/awesome-app-1",
"id": "gid://gitlab/Releases::Link/11",
"linkType": "runbook",
@@ -236,6 +242,7 @@ Object {
"url": "http://localhost/releases-namespace/releases-project/runbook",
},
Object {
+ "__typename": "ReleaseAssetLink",
"directAssetPath": "/binaries/linux-amd64",
"id": "gid://gitlab/Releases::Link/10",
"linkType": "other",
@@ -250,6 +257,7 @@ Object {
"evidences": Array [],
"milestones": Array [
Object {
+ "__typename": "Milestone",
"id": "gid://gitlab/Milestone/123",
"issueStats": Object {},
"stats": undefined,
@@ -258,6 +266,7 @@ Object {
"webUrl": undefined,
},
Object {
+ "__typename": "Milestone",
"id": "gid://gitlab/Milestone/124",
"issueStats": Object {},
"stats": undefined,
@@ -373,6 +382,7 @@ Object {
"sha": "760d6cdfb0879c3ffedec13af470e0f71cf52c6cde4d",
},
],
+ "historicalRelease": false,
"milestones": Array [
Object {
"__typename": "Milestone",
diff --git a/spec/frontend/releases/components/release_block_header_spec.js b/spec/frontend/releases/components/release_block_header_spec.js
index 167ae4f32a2..c9921185bad 100644
--- a/spec/frontend/releases/components/release_block_header_spec.js
+++ b/spec/frontend/releases/components/release_block_header_spec.js
@@ -1,8 +1,9 @@
-import { GlLink } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
+import { GlLink, GlBadge } from '@gitlab/ui';
import { merge } from 'lodash';
import originalRelease from 'test_fixtures/api/releases/release.json';
import setWindowLocation from 'helpers/set_window_location_helper';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { __ } from '~/locale';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import ReleaseBlockHeader from '~/releases/components/release_block_header.vue';
import { BACK_URL_PARAM } from '~/releases/constants';
@@ -12,10 +13,11 @@ describe('Release block header', () => {
let release;
const factory = (releaseUpdates = {}) => {
- wrapper = shallowMount(ReleaseBlockHeader, {
+ wrapper = shallowMountExtended(ReleaseBlockHeader, {
propsData: {
release: merge({}, release, releaseUpdates),
},
+ stubs: { GlBadge },
});
};
@@ -30,6 +32,7 @@ describe('Release block header', () => {
const findHeader = () => wrapper.find('h2');
const findHeaderLink = () => findHeader().find(GlLink);
const findEditButton = () => wrapper.find('.js-edit-button');
+ const findBadge = () => wrapper.findComponent(GlBadge);
describe('when _links.self is provided', () => {
beforeEach(() => {
@@ -84,4 +87,34 @@ describe('Release block header', () => {
expect(findEditButton().exists()).toBe(false);
});
});
+
+ describe('upcoming release', () => {
+ beforeEach(() => {
+ factory({ upcomingRelease: true, historicalRelease: false });
+ });
+
+ it('shows a badge that the release is upcoming', () => {
+ const badge = findBadge();
+
+ expect(badge.text()).toBe(__('Upcoming Release'));
+ expect(badge.props('variant')).toBe('warning');
+ });
+ });
+
+ describe('historical release', () => {
+ beforeEach(() => {
+ factory({ upcomingRelease: false, historicalRelease: true });
+ });
+
+ it('shows a badge that the release is historical', () => {
+ const badge = findBadge();
+
+ expect(badge.text()).toBe(__('Historical release'));
+ expect(badge.attributes('title')).toBe(
+ __(
+ 'This release was created with a date in the past. Evidence collection at the moment of the release is unavailable.',
+ ),
+ );
+ });
+ });
});
diff --git a/spec/frontend/reports/components/report_section_spec.js b/spec/frontend/reports/components/report_section_spec.js
index 888b49f3e0c..bdfba8d6878 100644
--- a/spec/frontend/reports/components/report_section_spec.js
+++ b/spec/frontend/reports/components/report_section_spec.js
@@ -1,16 +1,15 @@
-import { mount } from '@vue/test-utils';
-import Vue, { nextTick } from 'vue';
-import mountComponent, { mountComponentWithSlots } from 'helpers/vue_mount_component_helper';
-import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import { GlButton } from '@gitlab/ui';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
import HelpPopover from '~/vue_shared/components/help_popover.vue';
-import reportSection from '~/reports/components/report_section.vue';
+import ReportItem from '~/reports/components/report_item.vue';
+import ReportSection from '~/reports/components/report_section.vue';
-describe('Report section', () => {
- let vm;
+describe('ReportSection component', () => {
let wrapper;
- const ReportSection = Vue.extend(reportSection);
- const findCollapseButton = () => wrapper.findByTestId('report-section-expand-button');
+
+ const findButton = () => wrapper.findComponent(GlButton);
const findPopover = () => wrapper.findComponent(HelpPopover);
+ const findReportSection = () => wrapper.find('.js-report-section-container');
const resolvedIssues = [
{
@@ -33,34 +32,24 @@ describe('Report section', () => {
alwaysOpen: false,
};
- const createComponent = (props) => {
- wrapper = extendedWrapper(
- mount(reportSection, {
- propsData: {
- ...defaultProps,
- ...props,
- },
- }),
- );
- return wrapper;
+ const createComponent = ({ props = {}, data = {}, slots = {} } = {}) => {
+ wrapper = mountExtended(ReportSection, {
+ propsData: {
+ ...defaultProps,
+ ...props,
+ },
+ data() {
+ return data;
+ },
+ slots,
+ });
};
afterEach(() => {
- if (vm) {
- vm.$destroy();
- vm = null;
- }
- if (wrapper) {
- wrapper.destroy();
- wrapper = null;
- }
+ wrapper.destroy();
});
describe('computed', () => {
- beforeEach(() => {
- vm = mountComponent(ReportSection, defaultProps);
- });
-
describe('isCollapsible', () => {
const testMatrix = [
{ hasIssues: false, alwaysOpen: false, isCollapsible: false },
@@ -73,12 +62,10 @@ describe('Report section', () => {
const issues = hasIssues ? 'has issues' : 'has no issues';
const open = alwaysOpen ? 'is always open' : 'is not always open';
- it(`is ${isCollapsible}, if the report ${issues} and ${open}`, async () => {
- vm.hasIssues = hasIssues;
- vm.alwaysOpen = alwaysOpen;
+ it(`is ${isCollapsible}, if the report ${issues} and ${open}`, () => {
+ createComponent({ props: { hasIssues, alwaysOpen } });
- await nextTick();
- expect(vm.isCollapsible).toBe(isCollapsible);
+ expect(wrapper.vm.isCollapsible).toBe(isCollapsible);
});
});
});
@@ -95,12 +82,10 @@ describe('Report section', () => {
const issues = isCollapsed ? 'is collapsed' : 'is not collapsed';
const open = alwaysOpen ? 'is always open' : 'is not always open';
- it(`is ${isExpanded}, if the report ${issues} and ${open}`, async () => {
- vm.isCollapsed = isCollapsed;
- vm.alwaysOpen = alwaysOpen;
+ it(`is ${isExpanded}, if the report ${issues} and ${open}`, () => {
+ createComponent({ props: { alwaysOpen }, data: { isCollapsed } });
- await nextTick();
- expect(vm.isExpanded).toBe(isExpanded);
+ expect(wrapper.vm.isExpanded).toBe(isExpanded);
});
});
});
@@ -108,110 +93,105 @@ describe('Report section', () => {
describe('when it is loading', () => {
it('should render loading indicator', () => {
- vm = mountComponent(ReportSection, {
- component: '',
- status: 'LOADING',
- loadingText: 'Loading Code Quality report',
- errorText: 'foo',
- successText: 'Code quality improved on 1 point and degraded on 1 point',
- hasIssues: false,
+ createComponent({
+ props: {
+ component: '',
+ status: 'LOADING',
+ loadingText: 'Loading Code Quality report',
+ errorText: 'foo',
+ successText: 'Code quality improved on 1 point and degraded on 1 point',
+ hasIssues: false,
+ },
});
- expect(vm.$el.textContent.trim()).toEqual('Loading Code Quality report');
+ expect(wrapper.text()).toBe('Loading Code Quality report');
});
});
describe('with success status', () => {
- beforeEach(() => {
- vm = mountComponent(ReportSection, {
- ...defaultProps,
- hasIssues: true,
- });
- });
-
it('should render provided data', () => {
- expect(vm.$el.querySelector('.js-code-text').textContent.trim()).toEqual(
- 'Code quality improved on 1 point and degraded on 1 point',
- );
+ createComponent({ props: { hasIssues: true } });
- expect(vm.$el.querySelectorAll('.report-block-container li').length).toEqual(
- resolvedIssues.length,
+ expect(wrapper.find('.js-code-text').text()).toBe(
+ 'Code quality improved on 1 point and degraded on 1 point',
);
+ expect(wrapper.findAllComponents(ReportItem)).toHaveLength(resolvedIssues.length);
});
describe('toggleCollapsed', () => {
- const hiddenCss = { display: 'none' };
-
it('toggles issues', async () => {
- vm.$el.querySelector('button').click();
+ createComponent({ props: { hasIssues: true } });
+
+ await findButton().trigger('click');
- await nextTick();
- expect(vm.$el.querySelector('.js-report-section-container')).not.toHaveCss(hiddenCss);
- expect(vm.$el.querySelector('button').textContent.trim()).toEqual('Collapse');
+ expect(findReportSection().isVisible()).toBe(true);
+ expect(findButton().text()).toBe('Collapse');
- vm.$el.querySelector('button').click();
+ await findButton().trigger('click');
- await nextTick();
- expect(vm.$el.querySelector('.js-report-section-container')).toHaveCss(hiddenCss);
- expect(vm.$el.querySelector('button').textContent.trim()).toEqual('Expand');
+ expect(findReportSection().isVisible()).toBe(false);
+ expect(findButton().text()).toBe('Expand');
});
- it('is always expanded, if always-open is set to true', async () => {
- vm.alwaysOpen = true;
- await nextTick();
- expect(vm.$el.querySelector('.js-report-section-container')).not.toHaveCss(hiddenCss);
- expect(vm.$el.querySelector('button')).toBeNull();
+ 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);
});
});
});
describe('snowplow events', () => {
- it('does emit an event on issue toggle if the shouldEmitToggleEvent prop does exist', async () => {
- createComponent({ hasIssues: true, shouldEmitToggleEvent: true });
+ it('does emit an event on issue toggle if the shouldEmitToggleEvent prop does exist', () => {
+ createComponent({ props: { hasIssues: true, shouldEmitToggleEvent: true } });
- expect(wrapper.emitted().toggleEvent).toBeUndefined();
+ expect(wrapper.emitted('toggleEvent')).toBeUndefined();
- findCollapseButton().trigger('click');
- await nextTick();
- expect(wrapper.emitted().toggleEvent).toHaveLength(1);
+ findButton().trigger('click');
+
+ expect(wrapper.emitted('toggleEvent')).toEqual([[]]);
});
- it('does not emit an event on issue toggle if the shouldEmitToggleEvent prop does not exist', async () => {
- createComponent({ hasIssues: true });
+ it('does not emit an event on issue toggle if the shouldEmitToggleEvent prop does not exist', () => {
+ createComponent({ props: { hasIssues: true } });
+
+ expect(wrapper.emitted('toggleEvent')).toBeUndefined();
- expect(wrapper.emitted().toggleEvent).toBeUndefined();
+ findButton().trigger('click');
- findCollapseButton().trigger('click');
- await nextTick();
- expect(wrapper.emitted().toggleEvent).toBeUndefined();
+ expect(wrapper.emitted('toggleEvent')).toBeUndefined();
});
- it('does not emit an event if always-open is set to true', async () => {
- createComponent({ alwaysOpen: true, hasIssues: true, shouldEmitToggleEvent: true });
+ it('does not emit an event if always-open is set to true', () => {
+ createComponent({
+ props: { alwaysOpen: true, hasIssues: true, shouldEmitToggleEvent: true },
+ });
- await nextTick();
- expect(wrapper.emitted().toggleEvent).toBeUndefined();
+ expect(wrapper.emitted('toggleEvent')).toBeUndefined();
});
});
describe('with failed request', () => {
it('should render error indicator', () => {
- vm = mountComponent(ReportSection, {
- component: '',
- status: 'ERROR',
- loadingText: 'Loading Code Quality report',
- errorText: 'Failed to load Code Quality report',
- successText: 'Code quality improved on 1 point and degraded on 1 point',
- hasIssues: false,
+ createComponent({
+ props: {
+ component: '',
+ status: 'ERROR',
+ loadingText: 'Loading Code Quality report',
+ errorText: 'Failed to load Code Quality report',
+ successText: 'Code quality improved on 1 point and degraded on 1 point',
+ hasIssues: false,
+ },
});
- expect(vm.$el.textContent.trim()).toEqual('Failed to load Code Quality report');
+ expect(wrapper.text()).toBe('Failed to load Code Quality report');
});
});
describe('with action buttons passed to the slot', () => {
beforeEach(() => {
- vm = mountComponentWithSlots(ReportSection, {
+ createComponent({
props: {
status: 'SUCCESS',
successText: 'success',
@@ -224,17 +204,17 @@ describe('Report section', () => {
});
it('should render the passed button', () => {
- expect(vm.$el.textContent.trim()).toContain('Action!');
+ expect(wrapper.text()).toContain('Action!');
});
it('should still render the expand/collapse button', () => {
- expect(vm.$el.querySelector('.js-collapse-btn').textContent.trim()).toEqual('Expand');
+ expect(findButton().text()).toBe('Expand');
});
});
describe('Success and Error slots', () => {
const createComponentWithSlots = (status) => {
- vm = mountComponentWithSlots(ReportSection, {
+ createComponent({
props: {
status,
hasIssues: true,
@@ -250,25 +230,25 @@ describe('Report section', () => {
it('only renders success slot when status is "SUCCESS"', () => {
createComponentWithSlots('SUCCESS');
- expect(vm.$el.textContent.trim()).toContain('This is a success');
- expect(vm.$el.textContent.trim()).not.toContain('This is an error');
- expect(vm.$el.textContent.trim()).not.toContain('This is loading');
+ expect(wrapper.text()).toContain('This is a success');
+ expect(wrapper.text()).not.toContain('This is an error');
+ expect(wrapper.text()).not.toContain('This is loading');
});
it('only renders error slot when status is "ERROR"', () => {
createComponentWithSlots('ERROR');
- expect(vm.$el.textContent.trim()).toContain('This is an error');
- expect(vm.$el.textContent.trim()).not.toContain('This is a success');
- expect(vm.$el.textContent.trim()).not.toContain('This is loading');
+ expect(wrapper.text()).toContain('This is an error');
+ expect(wrapper.text()).not.toContain('This is a success');
+ expect(wrapper.text()).not.toContain('This is loading');
});
it('only renders loading slot when status is "LOADING"', () => {
createComponentWithSlots('LOADING');
- expect(vm.$el.textContent.trim()).toContain('This is loading');
- expect(vm.$el.textContent.trim()).not.toContain('This is an error');
- expect(vm.$el.textContent.trim()).not.toContain('This is a success');
+ expect(wrapper.text()).toContain('This is loading');
+ expect(wrapper.text()).not.toContain('This is an error');
+ expect(wrapper.text()).not.toContain('This is a success');
});
});
@@ -280,9 +260,7 @@ describe('Report section', () => {
};
beforeEach(() => {
- createComponent({
- popoverOptions: options,
- });
+ createComponent({ props: { popoverOptions: options } });
});
it('popover is shown with options', () => {
@@ -292,7 +270,7 @@ describe('Report section', () => {
describe('when popover options are not defined', () => {
beforeEach(() => {
- createComponent({ popoverOptions: {} });
+ createComponent({ props: { popoverOptions: {} } });
});
it('popover is not shown', () => {
diff --git a/spec/frontend/repository/components/blob_content_viewer_spec.js b/spec/frontend/repository/components/blob_content_viewer_spec.js
index 2b70cb84c67..0f7cf4e61b2 100644
--- a/spec/frontend/repository/components/blob_content_viewer_spec.js
+++ b/spec/frontend/repository/components/blob_content_viewer_spec.js
@@ -21,12 +21,13 @@ import blobInfoQuery from '~/repository/queries/blob_info.query.graphql';
import userInfoQuery from '~/repository/queries/user_info.query.graphql';
import applicationInfoQuery from '~/repository/queries/application_info.query.graphql';
import CodeIntelligence from '~/code_navigation/components/app.vue';
-import { redirectTo } from '~/lib/utils/url_utility';
+import * as urlUtility from '~/lib/utils/url_utility';
import { isLoggedIn, handleLocationHash } from '~/lib/utils/common_utils';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import httpStatusCodes from '~/lib/utils/http_status';
import LineHighlighter from '~/blob/line_highlighter';
import { LEGACY_FILE_TYPES } from '~/repository/constants';
+import { SIMPLE_BLOB_VIEWER, RICH_BLOB_VIEWER } from '~/blob/components/constants';
import {
simpleViewerMock,
richViewerMock,
@@ -53,7 +54,12 @@ const mockAxios = new MockAdapter(axios);
const createMockStore = () =>
new Vuex.Store({ actions: { fetchData: jest.fn, setInitialData: jest.fn() } });
-const createComponent = async (mockData = {}, mountFn = shallowMount) => {
+const mockRouterPush = jest.fn();
+const mockRouter = {
+ push: mockRouterPush,
+};
+
+const createComponent = async (mockData = {}, mountFn = shallowMount, mockRoute = {}) => {
Vue.use(VueApollo);
const {
@@ -106,6 +112,10 @@ const createComponent = async (mockData = {}, mountFn = shallowMount) => {
apolloProvider: fakeApollo,
propsData: propsMock,
mixins: [{ data: () => ({ ref: refMock }) }],
+ mocks: {
+ $route: mockRoute,
+ $router: mockRouter,
+ },
provide: {
targetBranch: 'test',
originalBranch: 'default-ref',
@@ -158,10 +168,11 @@ describe('Blob content viewer component', () => {
it('renders a BlobHeader component', async () => {
await createComponent();
- expect(findBlobHeader().props('activeViewerType')).toEqual('simple');
+ expect(findBlobHeader().props('activeViewerType')).toEqual(SIMPLE_BLOB_VIEWER);
expect(findBlobHeader().props('hasRenderError')).toEqual(false);
expect(findBlobHeader().props('hideViewerSwitcher')).toEqual(true);
expect(findBlobHeader().props('blob')).toEqual(simpleViewerMock);
+ expect(mockRouterPush).not.toHaveBeenCalled();
});
it('copies blob text to clipboard', async () => {
@@ -179,7 +190,7 @@ describe('Blob content viewer component', () => {
expect(findBlobContent().props('activeViewer')).toEqual({
fileType: 'text',
tooLarge: false,
- type: 'simple',
+ type: SIMPLE_BLOB_VIEWER,
renderError: null,
});
});
@@ -229,6 +240,12 @@ describe('Blob content viewer component', () => {
expect(LineHighlighter).toHaveBeenCalled();
});
+ it('does not load the LineHighlighter for RichViewers', async () => {
+ mockAxios.onGet(legacyViewerUrl).replyOnce(httpStatusCodes.OK, 'test');
+ await createComponent({ blob: { ...richViewerMock, fileType, highlightJs } });
+ expect(LineHighlighter).not.toHaveBeenCalled();
+ });
+
it('scrolls to the hash', async () => {
mockAxios.onGet(legacyViewerUrl).replyOnce(httpStatusCodes.OK, 'test');
await createComponent({ blob: { ...simpleViewerMock, fileType, highlightJs } });
@@ -241,10 +258,11 @@ describe('Blob content viewer component', () => {
it('renders a BlobHeader component', async () => {
await createComponent({ blob: richViewerMock });
- expect(findBlobHeader().props('activeViewerType')).toEqual('rich');
+ expect(findBlobHeader().props('activeViewerType')).toEqual(RICH_BLOB_VIEWER);
expect(findBlobHeader().props('hasRenderError')).toEqual(false);
expect(findBlobHeader().props('hideViewerSwitcher')).toEqual(false);
expect(findBlobHeader().props('blob')).toEqual(richViewerMock);
+ expect(mockRouterPush).not.toHaveBeenCalled();
});
it('renders a BlobContent component', async () => {
@@ -254,30 +272,49 @@ describe('Blob content viewer component', () => {
expect(findBlobContent().props('activeViewer')).toEqual({
fileType: 'markup',
tooLarge: false,
- type: 'rich',
+ type: RICH_BLOB_VIEWER,
renderError: null,
});
});
- it('updates viewer type when viewer changed is clicked', async () => {
+ it('changes to simple viewer when URL has code line hash', async () => {
+ jest.spyOn(urlUtility, 'getLocationHash').mockReturnValueOnce('L5');
+
await createComponent({ blob: richViewerMock });
expect(findBlobContent().props('activeViewer')).toEqual(
expect.objectContaining({
- type: 'rich',
+ type: SIMPLE_BLOB_VIEWER,
+ }),
+ );
+ expect(findBlobHeader().props('activeViewerType')).toEqual(SIMPLE_BLOB_VIEWER);
+ });
+
+ it('updates viewer type when viewer changed is clicked', async () => {
+ await createComponent({ blob: richViewerMock }, shallowMount, { path: '/mock_path' });
+
+ expect(findBlobContent().props('activeViewer')).toEqual(
+ expect.objectContaining({
+ type: RICH_BLOB_VIEWER,
}),
);
- expect(findBlobHeader().props('activeViewerType')).toEqual('rich');
+ expect(findBlobHeader().props('activeViewerType')).toEqual(RICH_BLOB_VIEWER);
- findBlobHeader().vm.$emit('viewer-changed', 'simple');
+ findBlobHeader().vm.$emit('viewer-changed', SIMPLE_BLOB_VIEWER);
await nextTick();
- expect(findBlobHeader().props('activeViewerType')).toEqual('simple');
+ expect(findBlobHeader().props('activeViewerType')).toEqual(SIMPLE_BLOB_VIEWER);
expect(findBlobContent().props('activeViewer')).toEqual(
expect.objectContaining({
- type: 'simple',
+ type: SIMPLE_BLOB_VIEWER,
}),
);
+ expect(mockRouterPush).toHaveBeenCalledWith({
+ path: '/mock_path',
+ query: {
+ plain: '1',
+ },
+ });
});
});
@@ -497,12 +534,12 @@ describe('Blob content viewer component', () => {
it('simple edit redirects to the simple editor', () => {
findWebIdeLink().vm.$emit('edit', 'simple');
- expect(redirectTo).toHaveBeenCalledWith(simpleViewerMock.editBlobPath);
+ expect(urlUtility.redirectTo).toHaveBeenCalledWith(simpleViewerMock.editBlobPath);
});
it('IDE edit redirects to the IDE editor', () => {
findWebIdeLink().vm.$emit('edit', 'ide');
- expect(redirectTo).toHaveBeenCalledWith(simpleViewerMock.ideEditPath);
+ expect(urlUtility.redirectTo).toHaveBeenCalledWith(simpleViewerMock.ideEditPath);
});
it.each`
@@ -537,4 +574,32 @@ describe('Blob content viewer component', () => {
},
);
});
+
+ describe('active viewer based on plain attribute', () => {
+ it.each`
+ hasRichViewer | plain | activeViewerType
+ ${true} | ${'0'} | ${RICH_BLOB_VIEWER}
+ ${true} | ${'1'} | ${SIMPLE_BLOB_VIEWER}
+ ${false} | ${'0'} | ${SIMPLE_BLOB_VIEWER}
+ ${false} | ${'1'} | ${SIMPLE_BLOB_VIEWER}
+ `(
+ 'activeViewerType is `$activeViewerType` when hasRichViewer is $hasRichViewer and plain is set to $plain',
+ async ({ hasRichViewer, plain, activeViewerType }) => {
+ await createComponent(
+ { blob: hasRichViewer ? richViewerMock : simpleViewerMock },
+ shallowMount,
+ { query: { plain } },
+ );
+
+ await nextTick();
+
+ expect(findBlobContent().props('activeViewer')).toEqual(
+ expect.objectContaining({
+ type: activeViewerType,
+ }),
+ );
+ expect(findBlobHeader().props('activeViewerType')).toEqual(activeViewerType);
+ },
+ );
+ });
});
diff --git a/spec/frontend/repository/mock_data.js b/spec/frontend/repository/mock_data.js
index 0a5766a25f9..4db295fe0b7 100644
--- a/spec/frontend/repository/mock_data.js
+++ b/spec/frontend/repository/mock_data.js
@@ -8,6 +8,7 @@ export const simpleViewerMock = {
language: 'javascript',
path: 'some_file.js',
webPath: 'some_file.js',
+ blamePath: 'blame/file.js',
editBlobPath: 'some_file.js/edit',
gitpodBlobUrl: 'https://gitpod.io#path/to/blob.js',
ideEditPath: 'some_file.js/ide/edit',
diff --git a/spec/frontend/right_sidebar_spec.js b/spec/frontend/right_sidebar_spec.js
index 5847842f5a6..3b220ba8351 100644
--- a/spec/frontend/right_sidebar_spec.js
+++ b/spec/frontend/right_sidebar_spec.js
@@ -70,7 +70,7 @@ describe('RightSidebar', () => {
it('should not hide collapsed icons', () => {
[].forEach.call(document.querySelectorAll('.sidebar-collapsed-icon'), (el) => {
- expect(el.querySelector('.fa, svg').classList.contains('hidden')).toBeFalsy();
+ expect(el.querySelector('.fa, svg').classList.contains('hidden')).toBe(false);
});
});
});
diff --git a/spec/frontend/runner/admin_runner_edit/admin_runner_edit_app_spec.js b/spec/frontend/runner/admin_runner_edit/admin_runner_edit_app_spec.js
index 8a34cb14d8b..ffe3599ac64 100644
--- a/spec/frontend/runner/admin_runner_edit/admin_runner_edit_app_spec.js
+++ b/spec/frontend/runner/admin_runner_edit/admin_runner_edit_app_spec.js
@@ -87,10 +87,10 @@ describe('AdminRunnerEditApp', () => {
await createComponentWithApollo();
expect(findRunnerUpdateForm().props()).toMatchObject({
- runner: mockRunner,
loading: false,
runnerPath: mockRunnerPath,
});
+ expect(findRunnerUpdateForm().props('runner')).toEqual(mockRunner);
});
describe('When there is an error', () => {
diff --git a/spec/frontend/runner/admin_runner_show/admin_runner_show_app_spec.js b/spec/frontend/runner/admin_runner_show/admin_runner_show_app_spec.js
index 433be5d5027..509681c5a77 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
@@ -14,6 +14,7 @@ import RunnerPauseButton from '~/runner/components/runner_pause_button.vue';
import RunnerDeleteButton from '~/runner/components/runner_delete_button.vue';
import RunnerEditButton from '~/runner/components/runner_edit_button.vue';
import RunnersJobs from '~/runner/components/runner_jobs.vue';
+
import runnerQuery from '~/runner/graphql/show/runner.query.graphql';
import AdminRunnerShowApp from '~/runner/admin_runner_show/admin_runner_show_app.vue';
import { captureException } from '~/runner/sentry_utils';
@@ -94,10 +95,10 @@ describe('AdminRunnerShowApp', () => {
});
it('shows basic runner details', async () => {
- const expected = `Description Instance runner
+ const expected = `Description My Runner
Last contact Never contacted
Version 1.0.0
- IP Address 127.0.0.1
+ IP Address None
Executor None
Architecture None
Platform darwin
@@ -182,17 +183,19 @@ describe('AdminRunnerShowApp', () => {
});
describe('When loading', () => {
- beforeEach(() => {
+ it('does not show runner details', () => {
mockRunnerQueryResult();
createComponent();
- });
- it('does not show runner details', () => {
expect(findRunnerDetails().exists()).toBe(false);
});
it('does not show runner jobs', () => {
+ mockRunnerQueryResult();
+
+ createComponent();
+
expect(findRunnersJobs().exists()).toBe(false);
});
});
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 aa1aa723491..97341be7d5d 100644
--- a/spec/frontend/runner/admin_runners/admin_runners_app_spec.js
+++ b/spec/frontend/runner/admin_runners/admin_runners_app_spec.js
@@ -1,4 +1,4 @@
-import Vue from 'vue';
+import Vue, { nextTick } from 'vue';
import { GlToast, GlLink } from '@gitlab/ui';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
@@ -19,10 +19,11 @@ import { createLocalState } from '~/runner/graphql/list/local_state';
import AdminRunnersApp from '~/runner/admin_runners/admin_runners_app.vue';
import RunnerTypeTabs from '~/runner/components/runner_type_tabs.vue';
import RunnerFilteredSearchBar from '~/runner/components/runner_filtered_search_bar.vue';
+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';
-import RunnerCount from '~/runner/components/stat/runner_count.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';
@@ -37,12 +38,10 @@ import {
PARAM_KEY_STATUS,
PARAM_KEY_TAG,
STATUS_ONLINE,
- STATUS_OFFLINE,
- STATUS_STALE,
RUNNER_PAGE_SIZE,
} from '~/runner/constants';
import allRunnersQuery from 'ee_else_ce/runner/graphql/list/all_runners.query.graphql';
-import allRunnersCountQuery from '~/runner/graphql/list/all_runners_count.query.graphql';
+import allRunnersCountQuery from 'ee_else_ce/runner/graphql/list/all_runners_count.query.graphql';
import { captureException } from '~/runner/sentry_utils';
import {
@@ -51,6 +50,7 @@ import {
allRunnersDataPaginated,
onlineContactTimeoutSecs,
staleTimeoutSecs,
+ emptyPageInfo,
emptyStateSvgPath,
emptyStateFilteredSvgPath,
} from '../mock_data';
@@ -72,19 +72,24 @@ jest.mock('~/lib/utils/url_utility', () => ({
Vue.use(VueApollo);
Vue.use(GlToast);
+const COUNT_QUERIES = 7; // 4 tabs + 3 status queries
+
describe('AdminRunnersApp', () => {
let wrapper;
let cacheConfig;
let localMutations;
+ let showToast;
const findRunnerStats = () => wrapper.findComponent(RunnerStats);
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));
- const findRunnerPaginationNext = () => findRunnerPagination().findByLabelText('Go to next page');
+ const findRunnerPaginationNext = () => findRunnerPagination().findByText(s__('Pagination|Next'));
const findRunnerFilteredSearchBar = () => wrapper.findComponent(RunnerFilteredSearchBar);
const createComponent = ({
@@ -117,6 +122,8 @@ describe('AdminRunnersApp', () => {
...options,
});
+ showToast = jest.spyOn(wrapper.vm.$root.$toast, 'show');
+
return waitForPromises();
};
@@ -128,17 +135,10 @@ describe('AdminRunnersApp', () => {
afterEach(() => {
mockRunnersHandler.mockReset();
mockRunnersCountHandler.mockReset();
+ showToast.mockReset();
wrapper.destroy();
});
- it('shows the runner tabs with a runner count for each type', async () => {
- await createComponent({ mountFn: mountExtended });
-
- expect(findRunnerTypeTabs().text()).toMatchInterpolatedText(
- `All ${mockRunnersCount} Instance ${mockRunnersCount} Group ${mockRunnersCount} Project ${mockRunnersCount}`,
- );
- });
-
it('shows the runner setup instructions', () => {
createComponent();
@@ -146,27 +146,38 @@ describe('AdminRunnersApp', () => {
expect(findRegistrationDropdown().props('type')).toBe(INSTANCE_TYPE);
});
- it('shows total runner counts', async () => {
- await createComponent({ mountFn: mountExtended });
+ describe('shows total runner counts', () => {
+ beforeEach(async () => {
+ await createComponent({ mountFn: mountExtended });
+ });
+
+ it('fetches counts', () => {
+ expect(mockRunnersCountHandler).toHaveBeenCalledTimes(COUNT_QUERIES);
+ });
+
+ it('shows the runner tabs', () => {
+ expect(findRunnerTypeTabs().text()).toMatchInterpolatedText(
+ `All ${mockRunnersCount} Instance ${mockRunnersCount} Group ${mockRunnersCount} Project ${mockRunnersCount}`,
+ );
+ });
- expect(mockRunnersCountHandler).toHaveBeenCalledWith({ status: STATUS_ONLINE });
- expect(mockRunnersCountHandler).toHaveBeenCalledWith({ status: STATUS_OFFLINE });
- expect(mockRunnersCountHandler).toHaveBeenCalledWith({ status: STATUS_STALE });
-
- expect(findRunnerStats().text()).toContain(
- `${s__('Runners|Online runners')} ${mockRunnersCount}`,
- );
- expect(findRunnerStats().text()).toContain(
- `${s__('Runners|Offline runners')} ${mockRunnersCount}`,
- );
- expect(findRunnerStats().text()).toContain(
- `${s__('Runners|Stale runners')} ${mockRunnersCount}`,
- );
+ it('shows the total', () => {
+ expect(findRunnerStats().text()).toContain(
+ `${s__('Runners|Online runners')} ${mockRunnersCount}`,
+ );
+ expect(findRunnerStats().text()).toContain(
+ `${s__('Runners|Offline runners')} ${mockRunnersCount}`,
+ );
+ expect(findRunnerStats().text()).toContain(
+ `${s__('Runners|Stale runners')} ${mockRunnersCount}`,
+ );
+ });
});
it('shows the runners list', async () => {
await createComponent();
+ expect(mockRunnersHandler).toHaveBeenCalledTimes(1);
expect(findRunnerList().props('runners')).toEqual(mockRunners);
});
@@ -226,18 +237,13 @@ describe('AdminRunnersApp', () => {
});
describe('Single runner row', () => {
- let showToast;
-
const { id: graphqlId, shortSha } = mockRunners[0];
const id = getIdFromGraphQLId(graphqlId);
- const COUNT_QUERIES = 7; // Smart queries that display a filtered count of runners
- const FILTERED_COUNT_QUERIES = 4; // Smart queries that display a count of runners in tabs
beforeEach(async () => {
mockRunnersCountHandler.mockClear();
await createComponent({ mountFn: mountExtended });
- showToast = jest.spyOn(wrapper.vm.$root.$toast, 'show');
});
it('Links to the runner page', async () => {
@@ -252,7 +258,7 @@ describe('AdminRunnersApp', () => {
findRunnerActionsCell().vm.$emit('toggledPaused');
- expect(mockRunnersCountHandler).toHaveBeenCalledTimes(COUNT_QUERIES + FILTERED_COUNT_QUERIES);
+ expect(mockRunnersCountHandler).toHaveBeenCalledTimes(COUNT_QUERIES * 2);
expect(showToast).toHaveBeenCalledTimes(0);
});
@@ -266,25 +272,20 @@ describe('AdminRunnersApp', () => {
describe('when a filter is preselected', () => {
beforeEach(async () => {
- setWindowLocation(`?status[]=${STATUS_ONLINE}&runner_type[]=${INSTANCE_TYPE}&tag[]=tag1`);
+ setWindowLocation(`?status[]=${STATUS_ONLINE}&runner_type[]=${INSTANCE_TYPE}&paused[]=true`);
- await createComponent({
- stubs: {
- RunnerStats,
- RunnerCount,
- },
- });
+ await createComponent({ mountFn: mountExtended });
});
it('sets the filters in the search bar', () => {
expect(findRunnerFilteredSearchBar().props('value')).toEqual({
runnerType: INSTANCE_TYPE,
filters: [
- { type: 'status', value: { data: STATUS_ONLINE, operator: '=' } },
- { type: 'tag', value: { data: 'tag1', operator: '=' } },
+ { type: PARAM_KEY_STATUS, value: { data: STATUS_ONLINE, operator: '=' } },
+ { type: PARAM_KEY_PAUSED, value: { data: 'true', operator: '=' } },
],
sort: 'CREATED_DESC',
- pagination: { page: 1 },
+ pagination: {},
});
});
@@ -292,7 +293,7 @@ describe('AdminRunnersApp', () => {
expect(mockRunnersHandler).toHaveBeenLastCalledWith({
status: STATUS_ONLINE,
type: INSTANCE_TYPE,
- tagList: ['tag1'],
+ paused: true,
sort: DEFAULT_SORT,
first: RUNNER_PAGE_SIZE,
});
@@ -302,41 +303,34 @@ describe('AdminRunnersApp', () => {
expect(mockRunnersCountHandler).toHaveBeenCalledWith({
type: INSTANCE_TYPE,
status: STATUS_ONLINE,
- tagList: ['tag1'],
+ paused: true,
});
});
});
describe('when a filter is selected by the user', () => {
- beforeEach(() => {
- createComponent({
- stubs: {
- RunnerStats,
- RunnerCount,
- },
- });
+ beforeEach(async () => {
+ await createComponent({ mountFn: mountExtended });
findRunnerFilteredSearchBar().vm.$emit('input', {
runnerType: null,
- filters: [
- { type: PARAM_KEY_STATUS, value: { data: STATUS_ONLINE, operator: '=' } },
- { type: PARAM_KEY_TAG, value: { data: 'tag1', operator: '=' } },
- ],
+ filters: [{ type: PARAM_KEY_STATUS, value: { data: STATUS_ONLINE, operator: '=' } }],
sort: CREATED_ASC,
});
+
+ await nextTick();
});
it('updates the browser url', () => {
expect(updateHistory).toHaveBeenLastCalledWith({
title: expect.any(String),
- url: expect.stringContaining('?status[]=ONLINE&tag[]=tag1&sort=CREATED_ASC'),
+ url: expect.stringContaining('?status[]=ONLINE&sort=CREATED_ASC'),
});
});
it('requests the runners with filters', () => {
expect(mockRunnersHandler).toHaveBeenLastCalledWith({
status: STATUS_ONLINE,
- tagList: ['tag1'],
sort: CREATED_ASC,
first: RUNNER_PAGE_SIZE,
});
@@ -344,7 +338,6 @@ describe('AdminRunnersApp', () => {
it('fetches count results for requested status', () => {
expect(mockRunnersCountHandler).toHaveBeenCalledWith({
- tagList: ['tag1'],
status: STATUS_ONLINE,
});
});
@@ -353,39 +346,79 @@ describe('AdminRunnersApp', () => {
it('when runners have not loaded, shows a loading state', () => {
createComponent();
expect(findRunnerList().props('loading')).toBe(true);
+ expect(findRunnerPagination().attributes('disabled')).toBe('true');
});
describe('when bulk delete is enabled', () => {
- beforeEach(() => {
- createComponent({
- provide: {
- glFeatures: { adminRunnersBulkDelete: true },
- },
+ describe('Before runners are deleted', () => {
+ beforeEach(async () => {
+ await createComponent({
+ mountFn: mountExtended,
+ provide: {
+ glFeatures: { adminRunnersBulkDelete: true },
+ },
+ });
});
- });
- it('runner list is checkable', () => {
- expect(findRunnerList().props('checkable')).toBe(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);
+ });
+
+ 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,
+ });
+ });
});
- it('responds to checked items by updating the local cache', () => {
- const setRunnerCheckedMock = jest
- .spyOn(localMutations, 'setRunnerChecked')
- .mockImplementation(() => {});
+ describe('When runners are deleted', () => {
+ beforeEach(async () => {
+ await createComponent({
+ mountFn: mountExtended,
+ provide: {
+ glFeatures: { adminRunnersBulkDelete: true },
+ },
+ });
+ });
- const runner = mockRunners[0];
+ it('count data is refetched', async () => {
+ expect(mockRunnersCountHandler).toHaveBeenCalledTimes(COUNT_QUERIES);
- expect(setRunnerCheckedMock).toHaveBeenCalledTimes(0);
+ findRunnerBulkDelete().vm.$emit('deleted', { message: 'Runners deleted' });
- findRunnerList().vm.$emit('checked', {
- runner,
- isChecked: true,
+ expect(mockRunnersCountHandler).toHaveBeenCalledTimes(COUNT_QUERIES * 2);
});
- expect(setRunnerCheckedMock).toHaveBeenCalledTimes(1);
- expect(setRunnerCheckedMock).toHaveBeenCalledWith({
- runner,
- isChecked: true,
+ it('toast is shown', async () => {
+ expect(showToast).toHaveBeenCalledTimes(0);
+
+ findRunnerBulkDelete().vm.$emit('deleted', { message: 'Runners deleted' });
+
+ expect(showToast).toHaveBeenCalledTimes(1);
+ expect(showToast).toHaveBeenCalledWith('Runners deleted');
});
});
});
@@ -394,13 +427,20 @@ describe('AdminRunnersApp', () => {
beforeEach(async () => {
mockRunnersHandler.mockResolvedValue({
data: {
- runners: { nodes: [] },
+ runners: {
+ nodes: [],
+ pageInfo: emptyPageInfo,
+ },
},
});
await createComponent();
});
+ it('shows no errors', () => {
+ expect(createAlert).not.toHaveBeenCalled();
+ });
+
it('shows an empty state', () => {
expect(findRunnerListEmptyState().props('isSearchFiltered')).toBe(false);
});
@@ -440,19 +480,25 @@ describe('AdminRunnersApp', () => {
});
describe('Pagination', () => {
+ const { pageInfo } = allRunnersDataPaginated.data.runners;
+
beforeEach(async () => {
mockRunnersHandler.mockResolvedValue(allRunnersDataPaginated);
await createComponent({ mountFn: mountExtended });
});
+ it('passes the page info', () => {
+ expect(findRunnerPagination().props('pageInfo')).toEqual(pageInfo);
+ });
+
it('navigates to the next page', async () => {
await findRunnerPaginationNext().trigger('click');
expect(mockRunnersHandler).toHaveBeenLastCalledWith({
sort: CREATED_DESC,
first: RUNNER_PAGE_SIZE,
- after: allRunnersDataPaginated.data.runners.pageInfo.endCursor,
+ after: pageInfo.endCursor,
});
});
});
diff --git a/spec/frontend/runner/components/cells/runner_summary_cell_spec.js b/spec/frontend/runner/components/cells/runner_summary_cell_spec.js
index b2e8c5a3ad9..b06ab652212 100644
--- a/spec/frontend/runner/components/cells/runner_summary_cell_spec.js
+++ b/spec/frontend/runner/components/cells/runner_summary_cell_spec.js
@@ -1,3 +1,4 @@
+import { __ } from '~/locale';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import RunnerSummaryCell from '~/runner/components/cells/runner_summary_cell.vue';
import { INSTANCE_TYPE, PROJECT_TYPE } from '~/runner/constants';
@@ -61,8 +62,16 @@ describe('RunnerTypeCell', () => {
expect(wrapper.text()).toContain(mockDescription);
});
- it('Displays the runner ip address', () => {
- expect(wrapper.text()).toContain(mockIpAddress);
+ it('Displays ip address', () => {
+ expect(wrapper.text()).toContain(`${__('IP Address')} ${mockIpAddress}`);
+ });
+
+ it('Displays no ip address', () => {
+ createComponent({
+ ipAddress: null,
+ });
+
+ expect(wrapper.text()).not.toContain(__('IP Address'));
});
it('Displays a custom slot', () => {
diff --git a/spec/frontend/runner/components/registration/registration_token_spec.js b/spec/frontend/runner/components/registration/registration_token_spec.js
index ed1a698d36f..19344a68f79 100644
--- a/spec/frontend/runner/components/registration/registration_token_spec.js
+++ b/spec/frontend/runner/components/registration/registration_token_spec.js
@@ -1,5 +1,5 @@
import { GlToast } from '@gitlab/ui';
-import { createLocalVue } from '@vue/test-utils';
+import Vue from 'vue';
import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
import RegistrationToken from '~/runner/components/registration/registration_token.vue';
import InputCopyToggleVisibility from '~/vue_shared/components/form/input_copy_toggle_visibility.vue';
@@ -11,28 +11,17 @@ describe('RegistrationToken', () => {
let wrapper;
let showToast;
- const findInputCopyToggleVisibility = () => wrapper.findComponent(InputCopyToggleVisibility);
-
- const vueWithGlToast = () => {
- const localVue = createLocalVue();
- localVue.use(GlToast);
- return localVue;
- };
+ Vue.use(GlToast);
- const createComponent = ({
- props = {},
- withGlToast = true,
- mountFn = shallowMountExtended,
- } = {}) => {
- const localVue = withGlToast ? vueWithGlToast() : undefined;
+ const findInputCopyToggleVisibility = () => wrapper.findComponent(InputCopyToggleVisibility);
+ const createComponent = ({ props = {}, mountFn = shallowMountExtended } = {}) => {
wrapper = mountFn(RegistrationToken, {
propsData: {
value: mockToken,
inputId: 'token-value',
...props,
},
- localVue,
});
showToast = wrapper.vm.$toast ? jest.spyOn(wrapper.vm.$toast, 'show') : null;
@@ -69,13 +58,5 @@ describe('RegistrationToken', () => {
expect(showToast).toHaveBeenCalledTimes(1);
expect(showToast).toHaveBeenCalledWith('Registration token copied!');
});
-
- it('does not fail when toast is not defined', () => {
- createComponent({ withGlToast: false });
- findInputCopyToggleVisibility().vm.$emit('copy');
-
- // This block also tests for unhandled errors
- expect(showToast).toBeNull();
- });
});
});
diff --git a/spec/frontend/runner/components/runner_assigned_item_spec.js b/spec/frontend/runner/components/runner_assigned_item_spec.js
index 1ff6983fbe7..cc09046c000 100644
--- a/spec/frontend/runner/components/runner_assigned_item_spec.js
+++ b/spec/frontend/runner/components/runner_assigned_item_spec.js
@@ -1,10 +1,12 @@
-import { GlAvatar } from '@gitlab/ui';
+import { GlAvatar, GlBadge } from '@gitlab/ui';
+import { s__ } from '~/locale';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import RunnerAssignedItem from '~/runner/components/runner_assigned_item.vue';
import { AVATAR_SHAPE_OPTION_RECT } from '~/vue_shared/constants';
const mockHref = '/group/project';
const mockName = 'Project';
+const mockDescription = 'Project description';
const mockFullName = 'Group / Project';
const mockAvatarUrl = '/avatar.png';
@@ -12,6 +14,7 @@ describe('RunnerAssignedItem', () => {
let wrapper;
const findAvatar = () => wrapper.findByTestId('item-avatar');
+ const findBadge = () => wrapper.findComponent(GlBadge);
const createComponent = ({ props = {} } = {}) => {
wrapper = shallowMountExtended(RunnerAssignedItem, {
@@ -20,6 +23,7 @@ describe('RunnerAssignedItem', () => {
name: mockName,
fullName: mockFullName,
avatarUrl: mockAvatarUrl,
+ description: mockDescription,
...props,
},
});
@@ -51,4 +55,14 @@ describe('RunnerAssignedItem', () => {
expect(groupFullName.attributes('href')).toBe(mockHref);
});
+
+ it('Shows description', () => {
+ expect(wrapper.text()).toContain(mockDescription);
+ });
+
+ it('Shows owner badge', () => {
+ createComponent({ props: { isOwner: true } });
+
+ expect(findBadge().text()).toBe(s__('Runner|Owner'));
+ });
});
diff --git a/spec/frontend/runner/components/runner_bulk_delete_checkbox_spec.js b/spec/frontend/runner/components/runner_bulk_delete_checkbox_spec.js
new file mode 100644
index 00000000000..0ac89e82314
--- /dev/null
+++ b/spec/frontend/runner/components/runner_bulk_delete_checkbox_spec.js
@@ -0,0 +1,101 @@
+import Vue from 'vue';
+import { GlFormCheckbox } from '@gitlab/ui';
+import VueApollo from 'vue-apollo';
+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');
+
+describe('RunnerBulkDeleteCheckbox', () => {
+ let wrapper;
+ let mockState;
+ let mockCheckedRunnerIds;
+
+ 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 createComponent = ({ props = {} } = {}) => {
+ const { cacheConfig, localMutations } = mockState;
+ const apolloProvider = createMockApollo(undefined, undefined, cacheConfig);
+
+ wrapper = shallowMountExtended(RunnerBulkDeleteCheckbox, {
+ apolloProvider,
+ provide: {
+ localMutations,
+ },
+ propsData: {
+ runners: mockRunners,
+ ...props,
+ },
+ });
+ };
+
+ beforeEach(() => {
+ mockState = createLocalState();
+
+ jest
+ .spyOn(mockState.cacheConfig.typePolicies.Query.fields, 'checkedRunnerIds')
+ .mockImplementation(() => mockCheckedRunnerIds);
+
+ 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 () => {
+ mockCheckedRunnerIds = checkedRunnerIds;
+
+ createComponent();
+ });
+
+ it(`is ${is}`, () => {
+ expect(findCheckbox().attributes('disabled')).toBe(disabled);
+ expect(findCheckbox().attributes('checked')).toBe(checked);
+ expect(findCheckbox().attributes('indeterminate')).toBe(indeterminate);
+ });
+ });
+
+ describe('When user selects', () => {
+ beforeEach(() => {
+ mockCheckedRunnerIds = mockIds;
+ createComponent();
+ });
+
+ it.each([[true], [false]])('sets checked to %s', (checked) => {
+ findCheckbox().vm.$emit('change', checked);
+
+ expect(mockState.localMutations.setRunnersChecked).toHaveBeenCalledTimes(1);
+ expect(mockState.localMutations.setRunnersChecked).toHaveBeenCalledWith({
+ isChecked: checked,
+ runners: mockRunners,
+ });
+ });
+ });
+
+ describe('When runners are loading', () => {
+ beforeEach(() => {
+ createComponent({ props: { runners: [] } });
+ });
+
+ it(`is disabled`, () => {
+ expect(findCheckbox().attributes('disabled')).toBe('true');
+ expect(findCheckbox().attributes('checked')).toBe(undefined);
+ expect(findCheckbox().attributes('indeterminate')).toBe(undefined);
+ });
+ });
+});
diff --git a/spec/frontend/runner/components/runner_bulk_delete_spec.js b/spec/frontend/runner/components/runner_bulk_delete_spec.js
index f5b56396cf1..6df918c684f 100644
--- a/spec/frontend/runner/components/runner_bulk_delete_spec.js
+++ b/spec/frontend/runner/components/runner_bulk_delete_spec.js
@@ -1,37 +1,65 @@
import Vue from 'vue';
-import { GlSprintf } from '@gitlab/ui';
+import { GlModal, GlSprintf } from '@gitlab/ui';
import VueApollo from 'vue-apollo';
+import { createAlert } from '~/flash';
+import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
+import { s__ } from '~/locale';
import RunnerBulkDelete from '~/runner/components/runner_bulk_delete.vue';
import createMockApollo from 'helpers/mock_apollo_helper';
+import BulkRunnerDeleteMutation from '~/runner/graphql/list/bulk_runner_delete.mutation.graphql';
import { createLocalState } from '~/runner/graphql/list/local_state';
import waitForPromises from 'helpers/wait_for_promises';
+import { allRunnersData } from '../mock_data';
Vue.use(VueApollo);
-jest.mock('~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal');
+jest.mock('~/flash');
describe('RunnerBulkDelete', () => {
let wrapper;
+ let apolloCache;
let mockState;
let mockCheckedRunnerIds;
- const findClearBtn = () => wrapper.findByTestId('clear-btn');
- const findDeleteBtn = () => wrapper.findByTestId('delete-btn');
+ const findClearBtn = () => wrapper.findByText(s__('Runners|Clear selection'));
+ const findDeleteBtn = () => wrapper.findByText(s__('Runners|Delete selected'));
+ const findModal = () => wrapper.findComponent(GlModal);
+
+ const mockRunners = allRunnersData.data.runners.nodes;
+ const mockId1 = allRunnersData.data.runners.nodes[0].id;
+ const mockId2 = allRunnersData.data.runners.nodes[1].id;
+
+ const bulkRunnerDeleteHandler = jest.fn();
const createComponent = () => {
const { cacheConfig, localMutations } = mockState;
+ const apolloProvider = createMockApollo(
+ [[BulkRunnerDeleteMutation, bulkRunnerDeleteHandler]],
+ undefined,
+ cacheConfig,
+ );
wrapper = shallowMountExtended(RunnerBulkDelete, {
- apolloProvider: createMockApollo(undefined, undefined, cacheConfig),
+ apolloProvider,
provide: {
localMutations,
},
+ propsData: {
+ runners: mockRunners,
+ },
+ directives: {
+ GlTooltip: createMockDirective(),
+ },
stubs: {
GlSprintf,
+ GlModal,
},
});
+
+ apolloCache = apolloProvider.defaultClient.cache;
+ jest.spyOn(apolloCache, 'evict');
+ jest.spyOn(apolloCache, 'gc');
};
beforeEach(() => {
@@ -43,6 +71,7 @@ describe('RunnerBulkDelete', () => {
});
afterEach(() => {
+ bulkRunnerDeleteHandler.mockReset();
wrapper.destroy();
});
@@ -61,10 +90,10 @@ describe('RunnerBulkDelete', () => {
});
describe.each`
- count | ids | text
- ${1} | ${['gid:Runner/1']} | ${'1 runner'}
- ${2} | ${['gid:Runner/1', 'gid:Runner/2']} | ${'2 runners'}
- `('When $count runner(s) are checked', ({ count, ids, text }) => {
+ count | ids | text
+ ${1} | ${[mockId1]} | ${'1 runner'}
+ ${2} | ${[mockId1, mockId2]} | ${'2 runners'}
+ `('When $count runner(s) are checked', ({ ids, text }) => {
beforeEach(() => {
mockCheckedRunnerIds = ids;
@@ -86,18 +115,129 @@ describe('RunnerBulkDelete', () => {
});
it('shows confirmation modal', () => {
- expect(confirmAction).toHaveBeenCalledTimes(0);
+ const modalId = getBinding(findDeleteBtn().element, 'gl-modal');
+
+ expect(findModal().props('modal-id')).toBe(modalId);
+ expect(findModal().text()).toContain(text);
+ });
+ });
+
+ describe('when runners are deleted', () => {
+ let evt;
+ let mockHideModal;
+
+ beforeEach(() => {
+ mockCheckedRunnerIds = [mockId1, mockId2];
+
+ createComponent();
+
+ jest.spyOn(mockState.localMutations, 'clearChecked').mockImplementation(() => {});
+ mockHideModal = jest.spyOn(findModal().vm, 'hide');
+ });
+
+ describe('when deletion is successful', () => {
+ beforeEach(() => {
+ bulkRunnerDeleteHandler.mockResolvedValue({
+ data: {
+ bulkRunnerDelete: { deletedIds: mockCheckedRunnerIds, errors: [] },
+ },
+ });
+
+ evt = {
+ preventDefault: jest.fn(),
+ };
+ findModal().vm.$emit('primary', evt);
+ });
+
+ it('has loading state', async () => {
+ expect(findModal().props('actionPrimary').attributes.loading).toBe(true);
+ expect(findModal().props('actionCancel').attributes.loading).toBe(true);
+
+ await waitForPromises();
+
+ expect(findModal().props('actionPrimary').attributes.loading).toBe(false);
+ expect(findModal().props('actionCancel').attributes.loading).toBe(false);
+ });
+
+ it('modal is not prevented from closing', () => {
+ expect(evt.preventDefault).toHaveBeenCalledTimes(1);
+ });
+
+ it('mutation is called', async () => {
+ expect(bulkRunnerDeleteHandler).toHaveBeenCalledWith({
+ input: { ids: mockCheckedRunnerIds },
+ });
+ });
+
+ it('user interface is updated', async () => {
+ const { evict, gc } = apolloCache;
+
+ expect(evict).toHaveBeenCalledTimes(mockCheckedRunnerIds.length);
+ expect(evict).toHaveBeenCalledWith({
+ id: expect.stringContaining(mockCheckedRunnerIds[0]),
+ });
+ expect(evict).toHaveBeenCalledWith({
+ id: expect.stringContaining(mockCheckedRunnerIds[1]),
+ });
+
+ expect(gc).toHaveBeenCalledTimes(1);
+ });
+
+ it('modal is hidden', () => {
+ expect(mockHideModal).toHaveBeenCalledTimes(1);
+ });
+ });
+
+ describe('when deletion fails', () => {
+ beforeEach(() => {
+ bulkRunnerDeleteHandler.mockRejectedValue(new Error('error!'));
+
+ evt = {
+ preventDefault: jest.fn(),
+ };
+ findModal().vm.$emit('primary', evt);
+ });
+
+ it('has loading state', async () => {
+ expect(findModal().props('actionPrimary').attributes.loading).toBe(true);
+ expect(findModal().props('actionCancel').attributes.loading).toBe(true);
+
+ await waitForPromises();
+
+ expect(findModal().props('actionPrimary').attributes.loading).toBe(false);
+ expect(findModal().props('actionCancel').attributes.loading).toBe(false);
+ });
+
+ it('modal is not prevented from closing', () => {
+ expect(evt.preventDefault).toHaveBeenCalledTimes(1);
+ });
+
+ it('mutation is called', () => {
+ expect(bulkRunnerDeleteHandler).toHaveBeenCalledWith({
+ input: { ids: mockCheckedRunnerIds },
+ });
+ });
+
+ it('user interface is not updated', async () => {
+ await waitForPromises();
- findDeleteBtn().vm.$emit('click');
+ const { evict, gc } = apolloCache;
- expect(confirmAction).toHaveBeenCalledTimes(1);
+ expect(evict).not.toHaveBeenCalled();
+ expect(gc).not.toHaveBeenCalled();
+ expect(mockState.localMutations.clearChecked).not.toHaveBeenCalled();
+ });
- const [, confirmOptions] = confirmAction.mock.calls[0];
- const { title, modalHtmlMessage, primaryBtnText } = confirmOptions;
+ it('alert is called', async () => {
+ await waitForPromises();
- expect(title).toMatch(text);
- expect(primaryBtnText).toMatch(text);
- expect(modalHtmlMessage).toMatch(`<strong>${count}</strong>`);
+ expect(createAlert).toHaveBeenCalled();
+ expect(createAlert).toHaveBeenCalledWith({
+ message: expect.any(String),
+ captureError: true,
+ error: expect.any(Error),
+ });
+ });
});
});
});
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 83fb1764c6d..e35bec3aa38 100644
--- a/spec/frontend/runner/components/runner_filtered_search_bar_spec.js
+++ b/spec/frontend/runner/components/runner_filtered_search_bar_spec.js
@@ -143,7 +143,7 @@ describe('RunnerList', () => {
runnerType: INSTANCE_TYPE,
filters: mockFilters,
sort: mockOtherSort,
- pagination: { page: 1 },
+ pagination: {},
});
});
});
@@ -156,7 +156,7 @@ describe('RunnerList', () => {
runnerType: null,
filters: mockFilters,
sort: mockDefaultSort,
- pagination: { page: 1 },
+ pagination: {},
});
});
@@ -167,7 +167,7 @@ describe('RunnerList', () => {
runnerType: null,
filters: [],
sort: mockOtherSort,
- pagination: { page: 1 },
+ pagination: {},
});
});
});
diff --git a/spec/frontend/runner/components/runner_jobs_spec.js b/spec/frontend/runner/components/runner_jobs_spec.js
index 20582aaaf40..4d38afb25ee 100644
--- a/spec/frontend/runner/components/runner_jobs_spec.js
+++ b/spec/frontend/runner/components/runner_jobs_spec.js
@@ -73,8 +73,7 @@ describe('RunnerJobs', () => {
it('Shows jobs', () => {
const jobs = findRunnerJobsTable().props('jobs');
- expect(jobs).toHaveLength(mockJobs.length);
- expect(jobs[0]).toMatchObject(mockJobs[0]);
+ expect(jobs).toEqual(mockJobs);
});
describe('When "Next" page is clicked', () => {
diff --git a/spec/frontend/runner/components/runner_list_spec.js b/spec/frontend/runner/components/runner_list_spec.js
index eca4bbc3490..7b58a81bb0d 100644
--- a/spec/frontend/runner/components/runner_list_spec.js
+++ b/spec/frontend/runner/components/runner_list_spec.js
@@ -88,9 +88,7 @@ describe('RunnerList', () => {
createComponent({}, mountExtended);
// Badges
- expect(findCell({ fieldKey: 'status' }).text()).toMatchInterpolatedText(
- 'never contacted paused',
- );
+ expect(findCell({ fieldKey: 'status' }).text()).toMatchInterpolatedText('never contacted');
// Runner summary
expect(findCell({ fieldKey: 'summary' }).text()).toContain(
@@ -124,10 +122,10 @@ describe('RunnerList', () => {
expect(findCell({ fieldKey: 'checkbox' }).find('input').exists()).toBe(true);
});
- it('Emits a checked event', () => {
+ it('Emits a checked event', async () => {
const checkbox = findCell({ fieldKey: 'checkbox' }).find('input');
- checkbox.setChecked();
+ await checkbox.setChecked();
expect(wrapper.emitted('checked')).toHaveLength(1);
expect(wrapper.emitted('checked')[0][0]).toEqual({
diff --git a/spec/frontend/runner/components/runner_pagination_spec.js b/spec/frontend/runner/components/runner_pagination_spec.js
index e144b52ceb3..499cc59250d 100644
--- a/spec/frontend/runner/components/runner_pagination_spec.js
+++ b/spec/frontend/runner/components/runner_pagination_spec.js
@@ -1,5 +1,5 @@
-import { GlPagination } from '@gitlab/ui';
-import { mount } from '@vue/test-utils';
+import { GlKeysetPagination } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
import RunnerPagination from '~/runner/components/runner_pagination.vue';
const mockStartCursor = 'START_CURSOR';
@@ -8,21 +8,11 @@ const mockEndCursor = 'END_CURSOR';
describe('RunnerPagination', () => {
let wrapper;
- const findPagination = () => wrapper.findComponent(GlPagination);
+ const findPagination = () => wrapper.findComponent(GlKeysetPagination);
- const createComponent = ({ page = 1, hasPreviousPage = false, hasNextPage = true } = {}) => {
- wrapper = mount(RunnerPagination, {
- propsData: {
- value: {
- page,
- },
- pageInfo: {
- hasPreviousPage,
- hasNextPage,
- startCursor: mockStartCursor,
- endCursor: mockEndCursor,
- },
- },
+ const createComponent = (propsData = {}) => {
+ wrapper = shallowMount(RunnerPagination, {
+ propsData,
});
};
@@ -30,114 +20,96 @@ describe('RunnerPagination', () => {
wrapper.destroy();
});
- describe('When on the first page', () => {
- beforeEach(() => {
- createComponent({
- page: 1,
- hasPreviousPage: false,
- hasNextPage: true,
- });
- });
-
- it('Contains the current page information', () => {
- expect(findPagination().props('value')).toBe(1);
- expect(findPagination().props('prevPage')).toBe(null);
- expect(findPagination().props('nextPage')).toBe(2);
- });
-
- it('Goes to the second page', () => {
- findPagination().vm.$emit('input', 2);
-
- expect(wrapper.emitted('input')[0]).toEqual([
- {
- after: mockEndCursor,
- page: 2,
- },
- ]);
- });
- });
-
describe('When in between pages', () => {
+ const mockPageInfo = {
+ startCursor: mockStartCursor,
+ endCursor: mockEndCursor,
+ hasPreviousPage: true,
+ hasNextPage: true,
+ };
+
beforeEach(() => {
createComponent({
- page: 2,
- hasPreviousPage: true,
- hasNextPage: true,
+ pageInfo: mockPageInfo,
});
});
it('Contains the current page information', () => {
- expect(findPagination().props('value')).toBe(2);
- expect(findPagination().props('prevPage')).toBe(1);
- expect(findPagination().props('nextPage')).toBe(3);
+ expect(findPagination().props()).toMatchObject(mockPageInfo);
});
- it('Shows the next and previous pages', () => {
- const links = findPagination().findAll('a');
-
- expect(links).toHaveLength(2);
- expect(links.at(0).text()).toBe('Previous');
- expect(links.at(1).text()).toBe('Next');
- });
-
- it('Goes to the last page', () => {
- findPagination().vm.$emit('input', 3);
+ it('Goes to the prev page', () => {
+ findPagination().vm.$emit('prev');
expect(wrapper.emitted('input')[0]).toEqual([
{
- after: mockEndCursor,
- page: 3,
+ before: mockStartCursor,
},
]);
});
- it('Goes to the first page', () => {
- findPagination().vm.$emit('input', 1);
+ it('Goes to the next page', () => {
+ findPagination().vm.$emit('next');
expect(wrapper.emitted('input')[0]).toEqual([
{
- page: 1,
+ after: mockEndCursor,
},
]);
});
});
- describe('When in the last page', () => {
+ describe.each`
+ page | hasPreviousPage | hasNextPage
+ ${'first'} | ${false} | ${true}
+ ${'last'} | ${true} | ${false}
+ `('When on the $page page', ({ page, hasPreviousPage, hasNextPage }) => {
+ const mockPageInfo = {
+ startCursor: mockStartCursor,
+ endCursor: mockEndCursor,
+ hasPreviousPage,
+ hasNextPage,
+ };
+
beforeEach(() => {
createComponent({
- page: 3,
- hasPreviousPage: true,
- hasNextPage: false,
+ pageInfo: mockPageInfo,
});
});
- it('Contains the current page', () => {
- expect(findPagination().props('value')).toBe(3);
- expect(findPagination().props('prevPage')).toBe(2);
- expect(findPagination().props('nextPage')).toBe(null);
+ it(`Contains the ${page} page information`, () => {
+ expect(findPagination().props()).toMatchObject(mockPageInfo);
});
});
- describe('When only one page', () => {
+ describe('When no other pages', () => {
beforeEach(() => {
createComponent({
- page: 1,
- hasPreviousPage: false,
- hasNextPage: false,
+ pageInfo: {
+ hasPreviousPage: false,
+ hasNextPage: false,
+ },
});
});
- it('does not display pagination', () => {
- expect(wrapper.html()).toBe('');
+ it('is not shown', () => {
+ expect(findPagination().exists()).toBe(false);
});
+ });
- it('Contains the current page', () => {
- expect(findPagination().props('value')).toBe(1);
+ describe('When adding more attributes', () => {
+ beforeEach(() => {
+ createComponent({
+ pageInfo: {
+ hasPreviousPage: true,
+ hasNextPage: false,
+ },
+ disabled: true,
+ });
});
- it('Shows no more page buttons', () => {
- expect(findPagination().props('prevPage')).toBe(null);
- expect(findPagination().props('nextPage')).toBe(null);
+ it('attributes are passed', () => {
+ expect(findPagination().props('disabled')).toBe(true);
});
});
});
diff --git a/spec/frontend/runner/components/runner_projects_spec.js b/spec/frontend/runner/components/runner_projects_spec.js
index 6932b3b5197..c988fb8477d 100644
--- a/spec/frontend/runner/components/runner_projects_spec.js
+++ b/spec/frontend/runner/components/runner_projects_spec.js
@@ -95,6 +95,7 @@ describe('RunnerProjects', () => {
name,
fullName: nameWithNamespace,
avatarUrl,
+ isOwner: true, // first project is always owner
});
});
diff --git a/spec/frontend/runner/components/stat/runner_count_spec.js b/spec/frontend/runner/components/stat/runner_count_spec.js
index 89b51b1b4a7..2a6a745099f 100644
--- a/spec/frontend/runner/components/stat/runner_count_spec.js
+++ b/spec/frontend/runner/components/stat/runner_count_spec.js
@@ -7,8 +7,8 @@ import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { captureException } from '~/runner/sentry_utils';
-import allRunnersCountQuery from '~/runner/graphql/list/all_runners_count.query.graphql';
-import groupRunnersCountQuery from '~/runner/graphql/list/group_runners_count.query.graphql';
+import allRunnersCountQuery from 'ee_else_ce/runner/graphql/list/all_runners_count.query.graphql';
+import groupRunnersCountQuery from 'ee_else_ce/runner/graphql/list/group_runners_count.query.graphql';
import { runnersCountData, groupRunnersCountData } from '../../mock_data';
diff --git a/spec/frontend/runner/components/stat/runner_single_stat_spec.js b/spec/frontend/runner/components/stat/runner_single_stat_spec.js
new file mode 100644
index 00000000000..964a6a6ff71
--- /dev/null
+++ b/spec/frontend/runner/components/stat/runner_single_stat_spec.js
@@ -0,0 +1,61 @@
+import { GlSingleStat } from '@gitlab/ui/dist/charts';
+import { shallowMount } from '@vue/test-utils';
+import RunnerSingleStat from '~/runner/components/stat/runner_single_stat.vue';
+import RunnerCount from '~/runner/components/stat/runner_count.vue';
+import { INSTANCE_TYPE, GROUP_TYPE } from '~/runner/constants';
+
+describe('RunnerStats', () => {
+ let wrapper;
+
+ const findRunnerCount = () => wrapper.findComponent(RunnerCount);
+ const findGlSingleStat = () => wrapper.findComponent(GlSingleStat);
+
+ const createComponent = ({ props = {}, count, mountFn = shallowMount, ...options } = {}) => {
+ wrapper = mountFn(RunnerSingleStat, {
+ propsData: {
+ scope: INSTANCE_TYPE,
+ title: 'My title',
+ variables: {},
+ ...props,
+ },
+ stubs: {
+ RunnerCount: {
+ props: ['scope', 'variables', 'skip'],
+ render() {
+ return this.$scopedSlots.default({
+ count,
+ });
+ },
+ },
+ },
+ ...options,
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it.each`
+ case | count | value
+ ${'number'} | ${99} | ${'99'}
+ ${'long number'} | ${1000} | ${'1,000'}
+ ${'empty number'} | ${null} | ${'-'}
+ `('formats $case', ({ count, value }) => {
+ createComponent({ count });
+
+ expect(findGlSingleStat().props('value')).toBe(value);
+ });
+
+ it('Passes runner count props', () => {
+ const props = {
+ scope: GROUP_TYPE,
+ variables: { paused: true },
+ skip: true,
+ };
+
+ createComponent({ props });
+
+ expect(findRunnerCount().props()).toEqual(props);
+ });
+});
diff --git a/spec/frontend/runner/components/stat/runner_stats_spec.js b/spec/frontend/runner/components/stat/runner_stats_spec.js
index f1ba6403dfb..7f1f22be94f 100644
--- a/spec/frontend/runner/components/stat/runner_stats_spec.js
+++ b/spec/frontend/runner/components/stat/runner_stats_spec.js
@@ -1,15 +1,13 @@
import { shallowMount, mount } from '@vue/test-utils';
import { s__ } from '~/locale';
import RunnerStats from '~/runner/components/stat/runner_stats.vue';
-import RunnerCount from '~/runner/components/stat/runner_count.vue';
-import RunnerStatusStat from '~/runner/components/stat/runner_status_stat.vue';
+import RunnerSingleStat from '~/runner/components/stat/runner_single_stat.vue';
import { INSTANCE_TYPE, STATUS_ONLINE, STATUS_OFFLINE, STATUS_STALE } from '~/runner/constants';
describe('RunnerStats', () => {
let wrapper;
- const findRunnerCountAt = (i) => wrapper.findAllComponents(RunnerCount).at(i);
- const findRunnerStatusStatAt = (i) => wrapper.findAllComponents(RunnerStatusStat).at(i);
+ const findSingleStats = () => wrapper.findAllComponents(RunnerSingleStat).wrappers;
const createComponent = ({ props = {}, mountFn = shallowMount, ...options } = {}) => {
wrapper = mountFn(RunnerStats, {
@@ -53,31 +51,12 @@ describe('RunnerStats', () => {
expect(text).toMatch(`${s__('Runners|Stale runners')} 1`);
});
- it('Displays counts for filtered searches', () => {
- createComponent({ props: { variables: { paused: true } } });
+ it('Displays all counts for filtered searches', () => {
+ const mockVariables = { paused: true };
+ createComponent({ props: { variables: mockVariables } });
- expect(findRunnerCountAt(0).props('variables').paused).toBe(true);
- expect(findRunnerCountAt(1).props('variables').paused).toBe(true);
- expect(findRunnerCountAt(2).props('variables').paused).toBe(true);
- });
-
- it('Skips overlapping statuses', () => {
- createComponent({ props: { variables: { status: STATUS_ONLINE } } });
-
- expect(findRunnerCountAt(0).props('skip')).toBe(false);
- expect(findRunnerCountAt(1).props('skip')).toBe(true);
- expect(findRunnerCountAt(2).props('skip')).toBe(true);
- });
-
- it.each`
- i | status
- ${0} | ${STATUS_ONLINE}
- ${1} | ${STATUS_OFFLINE}
- ${2} | ${STATUS_STALE}
- `('Displays status $status at index $i', ({ i, status }) => {
- createComponent({ mountFn: mount });
-
- expect(findRunnerCountAt(i).props('variables').status).toBe(status);
- expect(findRunnerStatusStatAt(i).props('status')).toBe(status);
+ findSingleStats().forEach((stat) => {
+ expect(stat.props('variables')).toMatchObject(mockVariables);
+ });
});
});
diff --git a/spec/frontend/runner/components/stat/runner_status_stat_spec.js b/spec/frontend/runner/components/stat/runner_status_stat_spec.js
deleted file mode 100644
index 3218272eac7..00000000000
--- a/spec/frontend/runner/components/stat/runner_status_stat_spec.js
+++ /dev/null
@@ -1,67 +0,0 @@
-import { GlSingleStat } from '@gitlab/ui/dist/charts';
-import { shallowMount, mount } from '@vue/test-utils';
-import RunnerStatusStat from '~/runner/components/stat/runner_status_stat.vue';
-import { STATUS_ONLINE, STATUS_OFFLINE, STATUS_STALE } from '~/runner/constants';
-
-describe('RunnerStatusStat', () => {
- let wrapper;
-
- const findSingleStat = () => wrapper.findComponent(GlSingleStat);
-
- const createComponent = ({ props = {} } = {}, mountFn = shallowMount) => {
- wrapper = mountFn(RunnerStatusStat, {
- propsData: {
- status: STATUS_ONLINE,
- value: 99,
- ...props,
- },
- });
- };
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- describe.each`
- status | variant | title | badge
- ${STATUS_ONLINE} | ${'success'} | ${'Online runners'} | ${'online'}
- ${STATUS_OFFLINE} | ${'muted'} | ${'Offline runners'} | ${'offline'}
- ${STATUS_STALE} | ${'warning'} | ${'Stale runners'} | ${'stale'}
- `('Renders a stat for status "$status"', ({ status, variant, title, badge }) => {
- beforeEach(() => {
- createComponent({ props: { status } }, mount);
- });
-
- it('Renders text', () => {
- expect(wrapper.text()).toMatch(new RegExp(`${title} 99\\s+${badge}`));
- });
-
- it(`Uses variant ${variant}`, () => {
- expect(findSingleStat().props('variant')).toBe(variant);
- });
- });
-
- it('Formats stat number', () => {
- createComponent({ props: { value: 1000 } }, mount);
-
- expect(wrapper.text()).toMatch('Online runners 1,000');
- });
-
- it('Shows a null result', () => {
- createComponent({ props: { value: null } }, mount);
-
- expect(wrapper.text()).toMatch('Online runners -');
- });
-
- it('Shows an undefined result', () => {
- createComponent({ props: { value: undefined } }, mount);
-
- expect(wrapper.text()).toMatch('Online runners -');
- });
-
- it('Shows result for an unknown status', () => {
- createComponent({ props: { status: 'UNKNOWN' } }, mount);
-
- expect(wrapper.text()).toMatch('Runners 99');
- });
-});
diff --git a/spec/frontend/runner/graphql/local_state_spec.js b/spec/frontend/runner/graphql/local_state_spec.js
index 5c4302e4aa2..ae874fef00d 100644
--- a/spec/frontend/runner/graphql/local_state_spec.js
+++ b/spec/frontend/runner/graphql/local_state_spec.js
@@ -1,6 +1,8 @@
+import { gql } from '@apollo/client/core';
import createApolloClient from '~/lib/graphql';
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';
describe('~/runner/graphql/list/local_state', () => {
let localState;
@@ -18,6 +20,21 @@ describe('~/runner/graphql/list/local_state', () => {
apolloClient = createApolloClient({}, { cacheConfig, typeDefs });
};
+ const addMockRunnerToCache = (id) => {
+ // mock some runners in the cache to prevent dangling references
+ apolloClient.writeFragment({
+ id: `${RUNNER_TYPENAME}:${id}`,
+ fragment: gql`
+ fragment DummyRunner on CiRunner {
+ __typename
+ }
+ `,
+ data: {
+ __typename: RUNNER_TYPENAME,
+ },
+ });
+ };
+
const queryCheckedRunnerIds = () => {
const { checkedRunnerIds } = apolloClient.readQuery({
query: getCheckedRunnerIdsQuery,
@@ -34,10 +51,25 @@ describe('~/runner/graphql/list/local_state', () => {
apolloClient = null;
});
- describe('default', () => {
- it('has empty checked list', () => {
+ describe('queryCheckedRunnerIds', () => {
+ it('has empty checked list by default', () => {
expect(queryCheckedRunnerIds()).toEqual([]);
});
+
+ it('returns checked runners that have a reference in the cache', () => {
+ addMockRunnerToCache('a');
+ localState.localMutations.setRunnerChecked({ runner: { id: 'a' }, 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 });
+
+ expect(queryCheckedRunnerIds()).toEqual(['a']);
+ });
});
describe.each`
@@ -48,6 +80,7 @@ describe('~/runner/graphql/list/local_state', () => {
`('setRunnerChecked', ({ inputs, expected }) => {
beforeEach(() => {
inputs.forEach(([id, isChecked]) => {
+ addMockRunnerToCache(id);
localState.localMutations.setRunnerChecked({ runner: { id }, isChecked });
});
});
@@ -56,9 +89,34 @@ describe('~/runner/graphql/list/local_state', () => {
});
});
+ describe.each`
+ inputs | expected
+ ${[[['a', 'b'], true]]} | ${['a', 'b']}
+ ${[[['a', 'b'], false]]} | ${[]}
+ ${[[['a', 'b'], true], [['c', 'd'], true]]} | ${['a', 'b', 'c', 'd']}
+ ${[[['a', 'b'], true], [['a', 'b'], false]]} | ${[]}
+ ${[[['a', 'b'], true], [['b'], false]]} | ${['a']}
+ `('setRunnersChecked', ({ inputs, expected }) => {
+ beforeEach(() => {
+ inputs.forEach(([ids, isChecked]) => {
+ ids.forEach(addMockRunnerToCache);
+
+ localState.localMutations.setRunnersChecked({
+ runners: ids.map((id) => ({ id })),
+ isChecked,
+ });
+ });
+ });
+
+ it(`for inputs="${inputs}" has a ids="[${expected}]"`, () => {
+ expect(queryCheckedRunnerIds()).toEqual(expected);
+ });
+ });
+
describe('clearChecked', () => {
it('clears all checked items', () => {
['a', 'b', 'c'].forEach((id) => {
+ addMockRunnerToCache(id);
localState.localMutations.setRunnerChecked({ runner: { id }, isChecked: true });
});
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 2065874c288..cee1d436942 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
@@ -92,10 +92,10 @@ describe('GroupRunnerShowApp', () => {
});
it('shows basic runner details', () => {
- const expected = `Description Instance runner
+ const expected = `Description My Runner
Last contact Never contacted
Version 1.0.0
- IP Address 127.0.0.1
+ IP Address None
Executor None
Architecture None
Platform darwin
@@ -178,13 +178,10 @@ describe('GroupRunnerShowApp', () => {
});
describe('When loading', () => {
- beforeEach(() => {
+ it('does not show runner details', () => {
mockRunnerQueryResult();
createComponent();
- });
-
- it('does not show runner details', () => {
expect(findRunnerDetails().exists()).toBe(false);
});
});
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 9c42b0d6865..57d64202219 100644
--- a/spec/frontend/runner/group_runners/group_runners_app_spec.js
+++ b/spec/frontend/runner/group_runners/group_runners_app_spec.js
@@ -13,13 +13,13 @@ import { createAlert } from '~/flash';
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 RunnerTypeTabs from '~/runner/components/runner_type_tabs.vue';
import RunnerFilteredSearchBar from '~/runner/components/runner_filtered_search_bar.vue';
import RunnerList from '~/runner/components/runner_list.vue';
import RunnerListEmptyState from '~/runner/components/runner_list_empty_state.vue';
import RunnerStats from '~/runner/components/stat/runner_stats.vue';
-import RunnerCount from '~/runner/components/stat/runner_count.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';
@@ -32,15 +32,14 @@ import {
GROUP_TYPE,
PARAM_KEY_PAUSED,
PARAM_KEY_STATUS,
- PARAM_KEY_TAG,
STATUS_ONLINE,
STATUS_OFFLINE,
STATUS_STALE,
RUNNER_PAGE_SIZE,
I18N_EDIT,
} from '~/runner/constants';
-import groupRunnersQuery from '~/runner/graphql/list/group_runners.query.graphql';
-import groupRunnersCountQuery from '~/runner/graphql/list/group_runners_count.query.graphql';
+import groupRunnersQuery from 'ee_else_ce/runner/graphql/list/group_runners.query.graphql';
+import groupRunnersCountQuery from 'ee_else_ce/runner/graphql/list/group_runners_count.query.graphql';
import GroupRunnersApp from '~/runner/group_runners/group_runners_app.vue';
import { captureException } from '~/runner/sentry_utils';
import {
@@ -49,6 +48,7 @@ import {
groupRunnersCountData,
onlineContactTimeoutSecs,
staleTimeoutSecs,
+ emptyPageInfo,
emptyStateSvgPath,
emptyStateFilteredSvgPath,
} from '../mock_data';
@@ -82,7 +82,7 @@ describe('GroupRunnersApp', () => {
const findRunnerListEmptyState = () => wrapper.findComponent(RunnerListEmptyState);
const findRunnerRow = (id) => extendedWrapper(wrapper.findByTestId(`runner-row-${id}`));
const findRunnerPagination = () => extendedWrapper(wrapper.findComponent(RunnerPagination));
- const findRunnerPaginationNext = () => findRunnerPagination().findByLabelText('Go to next page');
+ const findRunnerPaginationNext = () => findRunnerPagination().findByText(s__('Pagination|Next'));
const findRunnerFilteredSearchBar = () => wrapper.findComponent(RunnerFilteredSearchBar);
const createComponent = ({ props = {}, mountFn = shallowMountExtended, ...options } = {}) => {
@@ -111,7 +111,7 @@ describe('GroupRunnersApp', () => {
return waitForPromises();
};
- beforeEach(async () => {
+ beforeEach(() => {
mockGroupRunnersHandler.mockResolvedValue(groupRunnersData);
mockGroupRunnersCountHandler.mockResolvedValue(groupRunnersCountData);
});
@@ -197,6 +197,7 @@ describe('GroupRunnersApp', () => {
type: PARAM_KEY_STATUS,
options: expect.any(Array),
}),
+ upgradeStatusTokenConfig,
]);
});
@@ -254,12 +255,7 @@ describe('GroupRunnersApp', () => {
beforeEach(async () => {
setWindowLocation(`?status[]=${STATUS_ONLINE}&runner_type[]=${INSTANCE_TYPE}`);
- await createComponent({
- stubs: {
- RunnerStats,
- RunnerCount,
- },
- });
+ await createComponent({ mountFn: mountExtended });
});
it('sets the filters in the search bar', () => {
@@ -267,7 +263,7 @@ describe('GroupRunnersApp', () => {
runnerType: INSTANCE_TYPE,
filters: [{ type: 'status', value: { data: STATUS_ONLINE, operator: '=' } }],
sort: 'CREATED_DESC',
- pagination: { page: 1 },
+ pagination: {},
});
});
@@ -292,19 +288,11 @@ describe('GroupRunnersApp', () => {
describe('when a filter is selected by the user', () => {
beforeEach(async () => {
- createComponent({
- stubs: {
- RunnerStats,
- RunnerCount,
- },
- });
+ await createComponent({ mountFn: mountExtended });
findRunnerFilteredSearchBar().vm.$emit('input', {
runnerType: null,
- filters: [
- { type: PARAM_KEY_STATUS, value: { data: STATUS_ONLINE, operator: '=' } },
- { type: PARAM_KEY_TAG, value: { data: 'tag1', operator: '=' } },
- ],
+ filters: [{ type: PARAM_KEY_STATUS, value: { data: STATUS_ONLINE, operator: '=' } }],
sort: CREATED_ASC,
});
@@ -314,7 +302,7 @@ describe('GroupRunnersApp', () => {
it('updates the browser url', () => {
expect(updateHistory).toHaveBeenLastCalledWith({
title: expect.any(String),
- url: expect.stringContaining('?status[]=ONLINE&tag[]=tag1&sort=CREATED_ASC'),
+ url: expect.stringContaining('?status[]=ONLINE&sort=CREATED_ASC'),
});
});
@@ -322,7 +310,6 @@ describe('GroupRunnersApp', () => {
expect(mockGroupRunnersHandler).toHaveBeenLastCalledWith({
groupFullPath: mockGroupFullPath,
status: STATUS_ONLINE,
- tagList: ['tag1'],
sort: CREATED_ASC,
first: RUNNER_PAGE_SIZE,
});
@@ -331,7 +318,6 @@ describe('GroupRunnersApp', () => {
it('fetches count results for requested status', () => {
expect(mockGroupRunnersCountHandler).toHaveBeenCalledWith({
groupFullPath: mockGroupFullPath,
- tagList: ['tag1'],
status: STATUS_ONLINE,
});
});
@@ -340,6 +326,7 @@ describe('GroupRunnersApp', () => {
it('when runners have not loaded, shows a loading state', () => {
createComponent();
expect(findRunnerList().props('loading')).toBe(true);
+ expect(findRunnerPagination().attributes('disabled')).toBe('true');
});
describe('when no runners are found', () => {
@@ -348,13 +335,20 @@ describe('GroupRunnersApp', () => {
data: {
group: {
id: '1',
- runners: { nodes: [] },
+ runners: {
+ edges: [],
+ pageInfo: emptyPageInfo,
+ },
},
},
});
await createComponent();
});
+ it('shows no errors', () => {
+ expect(createAlert).not.toHaveBeenCalled();
+ });
+
it('shows an empty state', async () => {
expect(findRunnerListEmptyState().exists()).toBe(true);
});
@@ -379,12 +373,18 @@ describe('GroupRunnersApp', () => {
});
describe('Pagination', () => {
+ const { pageInfo } = groupRunnersDataPaginated.data.group.runners;
+
beforeEach(async () => {
mockGroupRunnersHandler.mockResolvedValue(groupRunnersDataPaginated);
await createComponent({ mountFn: mountExtended });
});
+ it('passes the page info', () => {
+ expect(findRunnerPagination().props('pageInfo')).toEqual(pageInfo);
+ });
+
it('navigates to the next page', async () => {
await findRunnerPaginationNext().trigger('click');
@@ -392,7 +392,7 @@ describe('GroupRunnersApp', () => {
groupFullPath: mockGroupFullPath,
sort: CREATED_DESC,
first: RUNNER_PAGE_SIZE,
- after: groupRunnersDataPaginated.data.group.runners.pageInfo.endCursor,
+ after: pageInfo.endCursor,
});
});
});
diff --git a/spec/frontend/runner/mock_data.js b/spec/frontend/runner/mock_data.js
index e5472ace817..555ec40184f 100644
--- a/spec/frontend/runner/mock_data.js
+++ b/spec/frontend/runner/mock_data.js
@@ -19,6 +19,14 @@ import groupRunnersCountData from 'test_fixtures/graphql/runner/list/group_runne
import { RUNNER_PAGE_SIZE } from '~/runner/constants';
+const emptyPageInfo = {
+ __typename: 'PageInfo',
+ hasNextPage: false,
+ hasPreviousPage: false,
+ startCursor: '',
+ endCursor: '',
+};
+
// Other mock data
// Mock searches and their corresponding urls
@@ -26,7 +34,7 @@ export const mockSearchExamples = [
{
name: 'a default query',
urlQuery: '',
- search: { runnerType: null, filters: [], pagination: { page: 1 }, sort: 'CREATED_DESC' },
+ search: { runnerType: null, filters: [], pagination: {}, sort: 'CREATED_DESC' },
graphqlVariables: { sort: 'CREATED_DESC', first: RUNNER_PAGE_SIZE },
isDefault: true,
},
@@ -36,7 +44,7 @@ export const mockSearchExamples = [
search: {
runnerType: null,
filters: [{ type: 'status', value: { data: 'ACTIVE', operator: '=' } }],
- pagination: { page: 1 },
+ pagination: {},
sort: 'CREATED_DESC',
},
graphqlVariables: { status: 'ACTIVE', sort: 'CREATED_DESC', first: RUNNER_PAGE_SIZE },
@@ -52,7 +60,7 @@ export const mockSearchExamples = [
value: { data: 'something' },
},
],
- pagination: { page: 1 },
+ pagination: {},
sort: 'CREATED_DESC',
},
graphqlVariables: { search: 'something', sort: 'CREATED_DESC', first: RUNNER_PAGE_SIZE },
@@ -72,7 +80,7 @@ export const mockSearchExamples = [
value: { data: 'else' },
},
],
- pagination: { page: 1 },
+ pagination: {},
sort: 'CREATED_DESC',
},
graphqlVariables: { search: 'something else', sort: 'CREATED_DESC', first: RUNNER_PAGE_SIZE },
@@ -83,7 +91,7 @@ export const mockSearchExamples = [
search: {
runnerType: 'INSTANCE_TYPE',
filters: [],
- pagination: { page: 1 },
+ pagination: {},
sort: 'CREATED_DESC',
},
graphqlVariables: { type: 'INSTANCE_TYPE', sort: 'CREATED_DESC', first: RUNNER_PAGE_SIZE },
@@ -97,7 +105,7 @@ export const mockSearchExamples = [
{ type: 'status', value: { data: 'ACTIVE', operator: '=' } },
{ type: 'status', value: { data: 'PAUSED', operator: '=' } },
],
- pagination: { page: 1 },
+ pagination: {},
sort: 'CREATED_DESC',
},
graphqlVariables: { status: 'ACTIVE', sort: 'CREATED_DESC', first: RUNNER_PAGE_SIZE },
@@ -108,7 +116,7 @@ export const mockSearchExamples = [
search: {
runnerType: 'INSTANCE_TYPE',
filters: [{ type: 'status', value: { data: 'ACTIVE', operator: '=' } }],
- pagination: { page: 1 },
+ pagination: {},
sort: 'CREATED_ASC',
},
graphqlVariables: {
@@ -124,7 +132,7 @@ export const mockSearchExamples = [
search: {
runnerType: null,
filters: [{ type: 'tag', value: { data: 'tag-1', operator: '=' } }],
- pagination: { page: 1 },
+ pagination: {},
sort: 'CREATED_DESC',
},
graphqlVariables: {
@@ -142,7 +150,7 @@ export const mockSearchExamples = [
{ type: 'tag', value: { data: 'tag-1', operator: '=' } },
{ type: 'tag', value: { data: 'tag-2', operator: '=' } },
],
- pagination: { page: 1 },
+ pagination: {},
sort: 'CREATED_DESC',
},
graphqlVariables: {
@@ -153,22 +161,22 @@ export const mockSearchExamples = [
},
{
name: 'the next page',
- urlQuery: '?page=2&after=AFTER_CURSOR',
+ urlQuery: '?after=AFTER_CURSOR',
search: {
runnerType: null,
filters: [],
- pagination: { page: 2, after: 'AFTER_CURSOR' },
+ pagination: { after: 'AFTER_CURSOR' },
sort: 'CREATED_DESC',
},
graphqlVariables: { sort: 'CREATED_DESC', after: 'AFTER_CURSOR', first: RUNNER_PAGE_SIZE },
},
{
name: 'the previous page',
- urlQuery: '?page=2&before=BEFORE_CURSOR',
+ urlQuery: '?before=BEFORE_CURSOR',
search: {
runnerType: null,
filters: [],
- pagination: { page: 2, before: 'BEFORE_CURSOR' },
+ pagination: { before: 'BEFORE_CURSOR' },
sort: 'CREATED_DESC',
},
graphqlVariables: { sort: 'CREATED_DESC', before: 'BEFORE_CURSOR', last: RUNNER_PAGE_SIZE },
@@ -176,7 +184,7 @@ export const mockSearchExamples = [
{
name: 'the next page filtered by a status, an instance type, tags and a non default sort',
urlQuery:
- '?status[]=ACTIVE&runner_type[]=INSTANCE_TYPE&tag[]=tag-1&tag[]=tag-2&sort=CREATED_ASC&page=2&after=AFTER_CURSOR',
+ '?status[]=ACTIVE&runner_type[]=INSTANCE_TYPE&tag[]=tag-1&tag[]=tag-2&sort=CREATED_ASC&after=AFTER_CURSOR',
search: {
runnerType: 'INSTANCE_TYPE',
filters: [
@@ -184,7 +192,7 @@ export const mockSearchExamples = [
{ type: 'tag', value: { data: 'tag-1', operator: '=' } },
{ type: 'tag', value: { data: 'tag-2', operator: '=' } },
],
- pagination: { page: 2, after: 'AFTER_CURSOR' },
+ pagination: { after: 'AFTER_CURSOR' },
sort: 'CREATED_ASC',
},
graphqlVariables: {
@@ -202,7 +210,7 @@ export const mockSearchExamples = [
search: {
runnerType: null,
filters: [{ type: 'paused', value: { data: 'true', operator: '=' } }],
- pagination: { page: 1 },
+ pagination: {},
sort: 'CREATED_DESC',
},
graphqlVariables: { paused: true, sort: 'CREATED_DESC', first: RUNNER_PAGE_SIZE },
@@ -213,7 +221,7 @@ export const mockSearchExamples = [
search: {
runnerType: null,
filters: [{ type: 'paused', value: { data: 'false', operator: '=' } }],
- pagination: { page: 1 },
+ pagination: {},
sort: 'CREATED_DESC',
},
graphqlVariables: { paused: false, sort: 'CREATED_DESC', first: RUNNER_PAGE_SIZE },
@@ -233,6 +241,7 @@ export {
groupRunnersData,
groupRunnersDataPaginated,
groupRunnersCountData,
+ emptyPageInfo,
runnerData,
runnerWithGroupData,
runnerProjectsData,
diff --git a/spec/frontend/runner/runner_search_utils_spec.js b/spec/frontend/runner/runner_search_utils_spec.js
index 6f954143ab1..e1f90482b34 100644
--- a/spec/frontend/runner/runner_search_utils_spec.js
+++ b/spec/frontend/runner/runner_search_utils_spec.js
@@ -24,11 +24,14 @@ describe('search_params.js', () => {
});
it.each`
- query | updatedQuery
- ${'status[]=ACTIVE'} | ${'paused[]=false'}
- ${'status[]=ACTIVE&a=b'} | ${'a=b&paused[]=false'}
- ${'status[]=ACTIVE'} | ${'paused[]=false'}
- ${'status[]=PAUSED'} | ${'paused[]=true'}
+ query | updatedQuery
+ ${'status[]=ACTIVE'} | ${'paused[]=false'}
+ ${'status[]=ACTIVE&a=b'} | ${'a=b&paused[]=false'}
+ ${'status[]=ACTIVE'} | ${'paused[]=false'}
+ ${'status[]=PAUSED'} | ${'paused[]=true'}
+ ${'page=2&after=AFTER'} | ${'after=AFTER'}
+ ${'page=2&before=BEFORE'} | ${'before=BEFORE'}
+ ${'status[]=PAUSED&page=2&after=AFTER'} | ${'after=AFTER&paused[]=true'}
`('updates "$query" to "$updatedQuery"', ({ query, updatedQuery }) => {
const mockUrl = 'http://test.host/admin/runners?';
@@ -49,24 +52,6 @@ describe('search_params.js', () => {
{ type: 'filtered-search-term', value: { data: 'text' } },
]);
});
-
- it('When a page cannot be parsed as a number, it defaults to `1`', () => {
- expect(fromUrlQueryToSearch('?page=NONSENSE&after=AFTER_CURSOR').pagination).toEqual({
- page: 1,
- });
- });
-
- it('When a page is less than 1, it defaults to `1`', () => {
- expect(fromUrlQueryToSearch('?page=0&after=AFTER_CURSOR').pagination).toEqual({
- page: 1,
- });
- });
-
- it('When a page with no cursor is given, it defaults to `1`', () => {
- expect(fromUrlQueryToSearch('?page=2').pagination).toEqual({
- page: 1,
- });
- });
});
describe('fromSearchToUrl', () => {
@@ -143,8 +128,11 @@ describe('search_params.js', () => {
});
});
- it('given a missing pagination, evaluates as not filtered', () => {
- expect(isSearchFiltered({ pagination: null })).toBe(false);
- });
+ it.each([null, undefined, {}])(
+ 'given a missing pagination, evaluates as not filtered',
+ (mockPagination) => {
+ expect(isSearchFiltered({ pagination: mockPagination })).toBe(false);
+ },
+ );
});
});
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 184c16fda6e..b6451af57d7 100644
--- a/spec/frontend/security_configuration/components/training_provider_list_spec.js
+++ b/spec/frontend/security_configuration/components/training_provider_list_spec.js
@@ -402,7 +402,7 @@ describe('TrainingProviderList component', () => {
it('has disabled state for radio', () => {
findPrimaryProviderRadios().wrappers.forEach((radio) => {
- expect(radio.attributes('disabled')).toBeTruthy();
+ expect(radio.attributes('disabled')).toBe('true');
});
});
diff --git a/spec/frontend/sidebar/assignees_spec.js b/spec/frontend/sidebar/assignees_spec.js
index a4474ead956..c2aff456abb 100644
--- a/spec/frontend/sidebar/assignees_spec.js
+++ b/spec/frontend/sidebar/assignees_spec.js
@@ -70,7 +70,7 @@ describe('Assignee component', () => {
wrapper.find('[data-testid="assign-yourself"]').trigger('click');
await nextTick();
- expect(wrapper.emitted('assign-self')).toBeTruthy();
+ expect(wrapper.emitted('assign-self')).toHaveLength(1);
});
});
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 5fd364afbe4..88015ed42a3 100644
--- a/spec/frontend/sidebar/components/assignees/sidebar_assignees_widget_spec.js
+++ b/spec/frontend/sidebar/components/assignees/sidebar_assignees_widget_spec.js
@@ -148,6 +148,7 @@ describe('Sidebar assignees widget', () => {
expect(findAssignees().props('users')).toEqual([
{
+ __typename: 'UserCore',
id: 'gid://gitlab/User/2',
avatarUrl:
'https://www.gravatar.com/avatar/a95e5b71488f4b9d69ce5ff58bfd28d6?s=80\u0026d=identicon',
diff --git a/spec/frontend/sidebar/components/attention_requested_toggle_spec.js b/spec/frontend/sidebar/components/attention_requested_toggle_spec.js
deleted file mode 100644
index 58fa878a189..00000000000
--- a/spec/frontend/sidebar/components/attention_requested_toggle_spec.js
+++ /dev/null
@@ -1,121 +0,0 @@
-import { GlButton } from '@gitlab/ui';
-import { mount } from '@vue/test-utils';
-import AttentionRequestedToggle from '~/sidebar/components/attention_requested_toggle.vue';
-
-let wrapper;
-
-function factory(propsData = {}) {
- wrapper = mount(AttentionRequestedToggle, { propsData });
-}
-
-const findToggle = () => wrapper.findComponent(GlButton);
-
-describe('Attention require toggle', () => {
- afterEach(() => {
- wrapper.destroy();
- });
-
- it('renders button', () => {
- factory({
- type: 'reviewer',
- user: { attention_requested: false, can_update_merge_request: true },
- });
-
- expect(findToggle().exists()).toBe(true);
- });
-
- it.each`
- attentionRequested | icon
- ${true} | ${'attention-solid'}
- ${false} | ${'attention'}
- `(
- 'renders $icon icon when attention_requested is $attentionRequested',
- ({ attentionRequested, icon }) => {
- factory({
- type: 'reviewer',
- user: { attention_requested: attentionRequested, can_update_merge_request: true },
- });
-
- expect(findToggle().props('icon')).toBe(icon);
- },
- );
-
- it.each`
- attentionRequested | selected
- ${true} | ${true}
- ${false} | ${false}
- `(
- 'renders button with as selected when $selected when attention_requested is $attentionRequested',
- ({ attentionRequested, selected }) => {
- factory({
- type: 'reviewer',
- user: { attention_requested: attentionRequested, can_update_merge_request: true },
- });
-
- expect(findToggle().props('selected')).toBe(selected);
- },
- );
-
- it('emits toggle-attention-requested on click', async () => {
- factory({
- type: 'reviewer',
- user: { attention_requested: true, can_update_merge_request: true },
- });
-
- await findToggle().trigger('click');
-
- expect(wrapper.emitted('toggle-attention-requested')[0]).toEqual([
- {
- user: { attention_requested: true, can_update_merge_request: true },
- callback: expect.anything(),
- direction: 'remove',
- },
- ]);
- });
-
- it('does not emit toggle-attention-requested on click if can_update_merge_request is false', async () => {
- factory({
- type: 'reviewer',
- user: { attention_requested: true, can_update_merge_request: false },
- });
-
- await findToggle().trigger('click');
-
- expect(wrapper.emitted('toggle-attention-requested')).toBe(undefined);
- });
-
- it('sets loading on click', async () => {
- factory({
- type: 'reviewer',
- user: { attention_requested: true, can_update_merge_request: true },
- });
-
- await findToggle().trigger('click');
-
- expect(findToggle().props('loading')).toBe(true);
- });
-
- it.each`
- type | attentionRequested | tooltip | canUpdateMergeRequest
- ${'reviewer'} | ${true} | ${AttentionRequestedToggle.i18n.removeAttentionRequest} | ${true}
- ${'reviewer'} | ${false} | ${AttentionRequestedToggle.i18n.addAttentionRequest} | ${true}
- ${'assignee'} | ${false} | ${AttentionRequestedToggle.i18n.addAttentionRequest} | ${true}
- ${'reviewer'} | ${true} | ${AttentionRequestedToggle.i18n.attentionRequestedNoPermission} | ${false}
- ${'reviewer'} | ${false} | ${AttentionRequestedToggle.i18n.noAttentionRequestedNoPermission} | ${false}
- ${'assignee'} | ${true} | ${AttentionRequestedToggle.i18n.attentionRequestedNoPermission} | ${false}
- ${'assignee'} | ${false} | ${AttentionRequestedToggle.i18n.noAttentionRequestedNoPermission} | ${false}
- `(
- 'sets tooltip as $tooltip when attention_requested is $attentionRequested, type is $type and, can_update_merge_request is $canUpdateMergeRequest',
- ({ type, attentionRequested, tooltip, canUpdateMergeRequest }) => {
- factory({
- type,
- user: {
- attention_requested: attentionRequested,
- can_update_merge_request: canUpdateMergeRequest,
- },
- });
-
- expect(findToggle().attributes('aria-label')).toBe(tooltip);
- },
- );
-});
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..7775ed6aa37 100644
--- a/spec/frontend/sidebar/components/confidential/sidebar_confidentiality_form_spec.js
+++ b/spec/frontend/sidebar/components/confidential/sidebar_confidentiality_form_spec.js
@@ -71,7 +71,12 @@ describe('Sidebar Confidentiality Form', () => {
it('creates a flash if mutation contains errors', async () => {
createComponent({
mutate: jest.fn().mockResolvedValue({
- data: { issuableSetConfidential: { errors: ['Houston, we have a problem!'] } },
+ data: {
+ issuableSetConfidential: {
+ issuable: { confidential: false },
+ errors: ['Houston, we have a problem!'],
+ },
+ },
}),
});
findConfidentialToggle().vm.$emit('click', new MouseEvent('click'));
@@ -82,6 +87,24 @@ describe('Sidebar Confidentiality Form', () => {
});
});
+ it('emits `closeForm` event with confidentiality value when mutation is successful', async () => {
+ createComponent({
+ mutate: jest.fn().mockResolvedValue({
+ data: {
+ issuableSetConfidential: {
+ issuable: { confidential: true },
+ errors: [],
+ },
+ },
+ }),
+ });
+
+ findConfidentialToggle().vm.$emit('click', new MouseEvent('click'));
+ await waitForPromises();
+
+ expect(wrapper.emitted('closeForm')).toEqual([[{ confidential: true }]]);
+ });
+
describe('when issue is not confidential', () => {
beforeEach(() => {
createComponent();
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 1de71e52264..18ee423d12e 100644
--- a/spec/frontend/sidebar/components/confidential/sidebar_confidentiality_widget_spec.js
+++ b/spec/frontend/sidebar/components/confidential/sidebar_confidentiality_widget_spec.js
@@ -132,6 +132,7 @@ describe('Sidebar Confidentiality Widget', () => {
it('closes the form and dispatches an event when `closeForm` is emitted', async () => {
createComponent();
const el = wrapper.vm.$el;
+ const closeFormPayload = { confidential: true };
jest.spyOn(el, 'dispatchEvent');
await waitForPromises();
@@ -140,12 +141,12 @@ describe('Sidebar Confidentiality Widget', () => {
expect(findConfidentialityForm().isVisible()).toBe(true);
- findConfidentialityForm().vm.$emit('closeForm');
+ findConfidentialityForm().vm.$emit('closeForm', closeFormPayload);
await nextTick();
expect(findConfidentialityForm().isVisible()).toBe(false);
expect(el.dispatchEvent).toHaveBeenCalled();
- expect(wrapper.emitted('closeForm')).toHaveLength(1);
+ expect(wrapper.emitted('closeForm')).toEqual([[closeFormPayload]]);
});
it('emits `expandSidebar` event when it is emitted from child component', async () => {
diff --git a/spec/frontend/sidebar/components/incidents/escalation_status_spec.js b/spec/frontend/sidebar/components/incidents/escalation_status_spec.js
index 8d8c10d10f1..83764cb6739 100644
--- a/spec/frontend/sidebar/components/incidents/escalation_status_spec.js
+++ b/spec/frontend/sidebar/components/incidents/escalation_status_spec.js
@@ -1,4 +1,5 @@
import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
+import { nextTick } from 'vue';
import waitForPromises from 'helpers/wait_for_promises';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import EscalationStatus from '~/sidebar/components/incidents/escalation_status.vue';
@@ -61,6 +62,8 @@ describe('EscalationStatus', () => {
createComponent();
// Open dropdown
await toggleDropdown();
+ jest.runOnlyPendingTimers();
+ await nextTick();
expect(findDropdownMenu().classes('show')).toBe(true);
@@ -74,6 +77,8 @@ describe('EscalationStatus', () => {
createComponent({ preventDropdownClose: true });
// Open dropdown
await toggleDropdown();
+ jest.runOnlyPendingTimers();
+ await nextTick();
expect(findDropdownMenu().classes('show')).toBe(true);
diff --git a/spec/frontend/sidebar/components/participants/sidebar_participants_widget_spec.js b/spec/frontend/sidebar/components/participants/sidebar_participants_widget_spec.js
index 338ecf944f3..859e63b3df6 100644
--- a/spec/frontend/sidebar/components/participants/sidebar_participants_widget_spec.js
+++ b/spec/frontend/sidebar/components/participants/sidebar_participants_widget_spec.js
@@ -1,7 +1,6 @@
import { shallowMount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
-import { stripTypenames } from 'helpers/graphql_helpers';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import Participants from '~/sidebar/components/participants/participants.vue';
@@ -67,11 +66,9 @@ describe('Sidebar Participants Widget', () => {
});
it('passes participants to child component', () => {
- const participantsWithoutTypename = stripTypenames(
+ expect(findParticipants().props('participants')).toEqual(
epicParticipantsResponse().data.workspace.issuable.participants.nodes,
);
-
- expect(findParticipants().props('participants')).toEqual(participantsWithoutTypename);
});
});
diff --git a/spec/frontend/sidebar/components/reviewers/uncollapsed_reviewer_list_spec.js b/spec/frontend/sidebar/components/reviewers/uncollapsed_reviewer_list_spec.js
index 8999f120a0f..2c24df2436a 100644
--- a/spec/frontend/sidebar/components/reviewers/uncollapsed_reviewer_list_spec.js
+++ b/spec/frontend/sidebar/components/reviewers/uncollapsed_reviewer_list_spec.js
@@ -1,9 +1,22 @@
import { shallowMount } from '@vue/test-utils';
import { TEST_HOST } from 'helpers/test_constants';
-import AttentionRequestedToggle from '~/sidebar/components/attention_requested_toggle.vue';
import ReviewerAvatarLink from '~/sidebar/components/reviewers/reviewer_avatar_link.vue';
import UncollapsedReviewerList from '~/sidebar/components/reviewers/uncollapsed_reviewer_list.vue';
-import userDataMock from '../../user_data_mock';
+
+const userDataMock = () => ({
+ id: 1,
+ name: 'Root',
+ state: 'active',
+ username: 'root',
+ webUrl: `${TEST_HOST}/root`,
+ avatarUrl: `${TEST_HOST}/avatar/root.png`,
+ mergeRequestInteraction: {
+ canMerge: true,
+ canUpdate: true,
+ reviewed: true,
+ approved: false,
+ },
+});
describe('UncollapsedReviewerList component', () => {
let wrapper;
@@ -70,7 +83,10 @@ describe('UncollapsedReviewerList component', () => {
id: 2,
name: 'nonrooty-nonrootersen',
username: 'hello-world',
- approved: true,
+ mergeRequestInteraction: {
+ ...user.mergeRequestInteraction,
+ approved: true,
+ },
};
beforeEach(() => {
@@ -119,18 +135,4 @@ describe('UncollapsedReviewerList component', () => {
expect(wrapper.find('[data-testid="re-request-success"]').exists()).toBe(true);
});
});
-
- it('hides re-request review button when attentionRequired feature flag is enabled', () => {
- createComponent({ users: [userDataMock()] }, { mrAttentionRequests: true });
-
- expect(wrapper.findAll('[data-testid="re-request-button"]').length).toBe(0);
- });
-
- it('emits toggle-attention-requested', () => {
- createComponent({ users: [userDataMock()] }, { mrAttentionRequests: true });
-
- wrapper.find(AttentionRequestedToggle).vm.$emit('toggle-attention-requested', 'data');
-
- expect(wrapper.emitted('toggle-attention-requested')[0]).toEqual(['data']);
- });
});
diff --git a/spec/frontend/sidebar/mock_data.js b/spec/frontend/sidebar/mock_data.js
index 229757ff40c..9c6e23e928c 100644
--- a/spec/frontend/sidebar/mock_data.js
+++ b/spec/frontend/sidebar/mock_data.js
@@ -343,6 +343,14 @@ export const issuableQueryResponse = {
__typename: 'Issue',
id: 'gid://gitlab/Issue/1',
iid: '1',
+ author: {
+ id: '1',
+ avatarUrl: '/avatar',
+ name: 'root',
+ username: 'root',
+ webUrl: 'root',
+ status: null,
+ },
assignees: {
nodes: [
{
@@ -450,7 +458,7 @@ export const subscriptionResponse = {
},
};
-const mockUser1 = {
+export const mockUser1 = {
__typename: 'UserCore',
id: 'gid://gitlab/User/1',
avatarUrl:
@@ -459,6 +467,7 @@ const mockUser1 = {
username: 'root',
webUrl: '/root',
status: null,
+ canMerge: false,
};
export const mockUser2 = {
@@ -469,6 +478,7 @@ export const mockUser2 = {
username: 'rookie',
webUrl: 'rookie',
status: null,
+ canMerge: false,
};
export const searchResponse = {
diff --git a/spec/frontend/sidebar/reviewer_title_spec.js b/spec/frontend/sidebar/reviewer_title_spec.js
index 3c250be5d5e..6b4eed5ad0f 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()).toBeFalsy();
+ expect(wrapper.find(GlLoadingIcon).exists()).toBe(false);
});
it('renders spinner when loading', () => {
@@ -57,7 +57,7 @@ describe('ReviewerTitle component', () => {
editable: false,
});
- expect(wrapper.find(GlLoadingIcon).exists()).toBeTruthy();
+ expect(wrapper.find(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 351dfc9a6ed..88bacc9b7f7 100644
--- a/spec/frontend/sidebar/reviewers_spec.js
+++ b/spec/frontend/sidebar/reviewers_spec.js
@@ -1,9 +1,23 @@
import { GlIcon } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import { trimText } from 'helpers/text_helper';
-import UsersMockHelper from 'helpers/user_mock_data_helper';
+import { TEST_HOST } from 'helpers/test_constants';
import Reviewer from '~/sidebar/components/reviewers/reviewers.vue';
-import UsersMock from './mock_data';
+
+const usersMock = (id = 1) => ({
+ id,
+ name: 'Root',
+ state: 'active',
+ username: 'root',
+ webUrl: `${TEST_HOST}/root`,
+ avatarUrl: `${TEST_HOST}/avatar/root.png`,
+ mergeRequestInteraction: {
+ canMerge: true,
+ canUpdate: true,
+ reviewed: true,
+ approved: false,
+ },
+});
describe('Reviewer component', () => {
const getDefaultProps = () => ({
@@ -42,23 +56,23 @@ describe('Reviewer component', () => {
it('displays one reviewer icon when collapsed', () => {
createWrapper({
...getDefaultProps(),
- users: [UsersMock.user],
+ users: [usersMock()],
});
const collapsedChildren = findCollapsedChildren();
const reviewer = collapsedChildren.at(0);
expect(collapsedChildren.length).toBe(1);
- expect(reviewer.find('.avatar').attributes('src')).toBe(UsersMock.user.avatar);
- expect(reviewer.find('.avatar').attributes('alt')).toBe(`${UsersMock.user.name}'s avatar`);
+ expect(reviewer.find('.avatar').attributes('src')).toContain('avatar/root.png');
+ expect(reviewer.find('.avatar').attributes('alt')).toBe(`Root's avatar`);
- expect(trimText(reviewer.find('.author').text())).toBe(UsersMock.user.name);
+ expect(trimText(reviewer.find('.author').text())).toBe('Root');
});
});
describe('Two or more reviewers/users', () => {
it('displays two reviewer icons when collapsed', () => {
- const users = UsersMockHelper.createNumberRandomUsers(2);
+ const users = [usersMock(), usersMock(2)];
createWrapper({
...getDefaultProps(),
users,
@@ -70,21 +84,21 @@ describe('Reviewer component', () => {
const first = collapsedChildren.at(0);
- expect(first.find('.avatar').attributes('src')).toBe(users[0].avatar_url);
+ expect(first.find('.avatar').attributes('src')).toBe(users[0].avatarUrl);
expect(first.find('.avatar').attributes('alt')).toBe(`${users[0].name}'s avatar`);
expect(trimText(first.find('.author').text())).toBe(users[0].name);
const second = collapsedChildren.at(1);
- expect(second.find('.avatar').attributes('src')).toBe(users[1].avatar_url);
+ expect(second.find('.avatar').attributes('src')).toBe(users[1].avatarUrl);
expect(second.find('.avatar').attributes('alt')).toBe(`${users[1].name}'s avatar`);
expect(trimText(second.find('.author').text())).toBe(users[1].name);
});
it('displays one reviewer icon and counter when collapsed', () => {
- const users = UsersMockHelper.createNumberRandomUsers(3);
+ const users = [usersMock(), usersMock(2), usersMock(3)];
createWrapper({
...getDefaultProps(),
users,
@@ -96,7 +110,7 @@ describe('Reviewer component', () => {
const first = collapsedChildren.at(0);
- expect(first.find('.avatar').attributes('src')).toBe(users[0].avatar_url);
+ expect(first.find('.avatar').attributes('src')).toBe(users[0].avatarUrl);
expect(first.find('.avatar').attributes('alt')).toBe(`${users[0].name}'s avatar`);
expect(trimText(first.find('.author').text())).toBe(users[0].name);
@@ -107,7 +121,7 @@ describe('Reviewer component', () => {
});
it('Shows two reviewers', () => {
- const users = UsersMockHelper.createNumberRandomUsers(2);
+ const users = [usersMock(), usersMock(2)];
createWrapper({
...getDefaultProps(),
users,
@@ -118,10 +132,10 @@ describe('Reviewer component', () => {
});
it('shows sorted reviewer where "can merge" users are sorted first', () => {
- const users = UsersMockHelper.createNumberRandomUsers(3);
- users[0].can_merge = false;
- users[1].can_merge = false;
- users[2].can_merge = true;
+ const users = [usersMock(), usersMock(2), usersMock(3)];
+ users[0].mergeRequestInteraction.canMerge = false;
+ users[1].mergeRequestInteraction.canMerge = false;
+ users[2].mergeRequestInteraction.canMerge = true;
createWrapper({
...getDefaultProps(),
@@ -129,14 +143,14 @@ describe('Reviewer component', () => {
editable: true,
});
- expect(wrapper.vm.sortedReviewers[0].can_merge).toBe(true);
+ expect(wrapper.vm.sortedReviewers[0].mergeRequestInteraction.canMerge).toBe(true);
});
it('passes the sorted reviewers to the uncollapsed-reviewer-list', () => {
- const users = UsersMockHelper.createNumberRandomUsers(3);
- users[0].can_merge = false;
- users[1].can_merge = false;
- users[2].can_merge = true;
+ const users = [usersMock(), usersMock(2), usersMock(3)];
+ users[0].mergeRequestInteraction.canMerge = false;
+ users[1].mergeRequestInteraction.canMerge = false;
+ users[2].mergeRequestInteraction.canMerge = true;
createWrapper({
...getDefaultProps(),
@@ -149,10 +163,10 @@ describe('Reviewer component', () => {
});
it('passes the sorted reviewers to the collapsed-reviewer-list', () => {
- const users = UsersMockHelper.createNumberRandomUsers(3);
- users[0].can_merge = false;
- users[1].can_merge = false;
- users[2].can_merge = true;
+ const users = [usersMock(), usersMock(2), usersMock(3)];
+ users[0].mergeRequestInteraction.canMerge = false;
+ users[1].mergeRequestInteraction.canMerge = false;
+ users[2].mergeRequestInteraction.canMerge = true;
createWrapper({
...getDefaultProps(),
diff --git a/spec/frontend/sidebar/sidebar_mediator_spec.js b/spec/frontend/sidebar/sidebar_mediator_spec.js
index 82fb10ab1d2..e32694abcce 100644
--- a/spec/frontend/sidebar/sidebar_mediator_spec.js
+++ b/spec/frontend/sidebar/sidebar_mediator_spec.js
@@ -1,12 +1,9 @@
import MockAdapter from 'axios-mock-adapter';
-import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import * as urlUtility from '~/lib/utils/url_utility';
import SidebarService, { gqClient } from '~/sidebar/services/sidebar_service';
import SidebarMediator from '~/sidebar/sidebar_mediator';
import SidebarStore from '~/sidebar/stores/sidebar_store';
-import toast from '~/vue_shared/plugins/global_toast';
-import { refreshUserMergeRequestCounts } from '~/commons/nav/user_merge_requests';
import Mock from './mock_data';
jest.mock('~/flash');
@@ -122,93 +119,4 @@ describe('Sidebar mediator', () => {
urlSpy.mockRestore();
});
});
-
- describe('toggleAttentionRequested', () => {
- let requestAttentionMock;
- let removeAttentionRequestMock;
-
- beforeEach(() => {
- requestAttentionMock = jest.spyOn(mediator.service, 'requestAttention').mockResolvedValue();
- removeAttentionRequestMock = jest
- .spyOn(mediator.service, 'removeAttentionRequest')
- .mockResolvedValue();
- });
-
- it.each`
- attentionIsCurrentlyRequested | serviceMethod
- ${true} | ${'remove'}
- ${false} | ${'add'}
- `(
- "calls the $serviceMethod service method when the user's attention request is set to $attentionIsCurrentlyRequested",
- async ({ serviceMethod }) => {
- const methods = {
- add: requestAttentionMock,
- remove: removeAttentionRequestMock,
- };
- mediator.store.reviewers = [{ id: 1, attention_requested: false, username: 'root' }];
-
- await mediator.toggleAttentionRequested('reviewer', {
- user: { id: 1, username: 'root' },
- callback: jest.fn(),
- direction: serviceMethod,
- });
-
- expect(methods[serviceMethod]).toHaveBeenCalledWith(1);
- expect(refreshUserMergeRequestCounts).toHaveBeenCalled();
- },
- );
-
- it.each`
- type | method
- ${'reviewer'} | ${'findReviewer'}
- `('finds $type', ({ type, method }) => {
- const methodSpy = jest.spyOn(mediator.store, method);
-
- mediator.toggleAttentionRequested(type, { user: { id: 1 }, callback: jest.fn() });
-
- expect(methodSpy).toHaveBeenCalledWith({ id: 1 });
- });
-
- it.each`
- attentionRequested | toastMessage
- ${true} | ${'Removed attention request from @root'}
- ${false} | ${'Requested attention from @root'}
- `(
- 'it creates toast $toastMessage when attention_requested is $attentionRequested',
- async ({ attentionRequested, toastMessage }) => {
- mediator.store.reviewers = [
- { id: 1, attention_requested: attentionRequested, username: 'root' },
- ];
-
- await mediator.toggleAttentionRequested('reviewer', {
- user: { id: 1, username: 'root' },
- callback: jest.fn(),
- });
-
- expect(toast).toHaveBeenCalledWith(toastMessage);
- },
- );
-
- describe('errors', () => {
- beforeEach(() => {
- jest
- .spyOn(mediator.service, 'removeAttentionRequest')
- .mockRejectedValueOnce(new Error('Something went wrong'));
- });
-
- it('shows an error message', async () => {
- await mediator.toggleAttentionRequested('reviewer', {
- user: { id: 1, username: 'root' },
- callback: jest.fn(),
- direction: 'remove',
- });
-
- expect(createFlash).toHaveBeenCalledWith(
- expect.objectContaining({
- message: 'Updating the attention request for root failed.',
- }),
- );
- });
- });
- });
});
diff --git a/spec/frontend/snippets/components/__snapshots__/snippet_blob_edit_spec.js.snap b/spec/frontend/snippets/components/__snapshots__/snippet_blob_edit_spec.js.snap
index 6fc358a6a15..76e84fa183c 100644
--- a/spec/frontend/snippets/components/__snapshots__/snippet_blob_edit_spec.js.snap
+++ b/spec/frontend/snippets/components/__snapshots__/snippet_blob_edit_spec.js.snap
@@ -16,6 +16,7 @@ exports[`Snippet Blob Edit component with loaded blob matches snapshot 1`] = `
<source-editor-stub
debouncevalue="250"
editoroptions="[object Object]"
+ extensions="[object Object]"
fileglobalid="blob_local_7"
filename="foo/bar/test.md"
value="Lorem ipsum dolar sit amet,
diff --git a/spec/frontend/surveys/merge_request_performance/app_spec.js b/spec/frontend/surveys/merge_request_performance/app_spec.js
index 6e8cc660b1d..cd549155914 100644
--- a/spec/frontend/surveys/merge_request_performance/app_spec.js
+++ b/spec/frontend/surveys/merge_request_performance/app_spec.js
@@ -25,6 +25,9 @@ describe('MergeRequestExperienceSurveyApp', () => {
shouldShowCallout,
});
wrapper = shallowMountExtended(MergeRequestExperienceSurveyApp, {
+ propsData: {
+ accountAge: 0,
+ },
stubs: {
UserCalloutDismisser: dismisserComponent,
GlSprintf,
@@ -82,11 +85,17 @@ describe('MergeRequestExperienceSurveyApp', () => {
expect(trackingSpy).toHaveBeenCalledWith(undefined, 'survey:mr_experience', {
value: 5,
label: 'overall',
+ extra: {
+ accountAge: 0,
+ },
});
rate.vm.$emit('rate', 4);
expect(trackingSpy).toHaveBeenCalledWith(undefined, 'survey:mr_experience', {
value: 4,
label: 'performance',
+ extra: {
+ accountAge: 0,
+ },
});
});
diff --git a/spec/frontend/test_setup.js b/spec/frontend/test_setup.js
index b4626625f31..3fb226e5ed3 100644
--- a/spec/frontend/test_setup.js
+++ b/spec/frontend/test_setup.js
@@ -1,8 +1,7 @@
/* Setup for unit test environment */
+// eslint-disable-next-line no-restricted-syntax
+import { setImmediate } from 'timers';
import 'helpers/shared_test_setup';
-import { initializeTestTimeout } from 'helpers/timeout';
-
-initializeTestTimeout(process.env.CI ? 6000 : 500);
afterEach(() =>
// give Promises a bit more time so they fail the right test
diff --git a/spec/frontend/user_popovers_spec.js b/spec/frontend/user_popovers_spec.js
index b171c8fc9ed..0530569c9df 100644
--- a/spec/frontend/user_popovers_spec.js
+++ b/spec/frontend/user_popovers_spec.js
@@ -10,6 +10,8 @@ jest.mock('~/api/user_api', () => ({
}));
describe('User Popovers', () => {
+ let origGon;
+
const fixtureTemplate = 'merge_requests/merge_request_with_mentions.html';
const selector = '.js-user-link[data-user], .js-user-link[data-user-id]';
@@ -39,7 +41,7 @@ describe('User Popovers', () => {
el.dispatchEvent(event);
};
- beforeEach(() => {
+ const setupTestSubject = () => {
loadHTMLFixture(fixtureTemplate);
const usersCacheSpy = () => Promise.resolve(dummyUser);
@@ -56,147 +58,179 @@ describe('User Popovers', () => {
document.body.appendChild(mountingRoot);
popoverInstance.$mount(mountingRoot);
});
+ };
+
+ beforeEach(() => {
+ origGon = window.gon;
+ window.gon = {};
});
afterEach(() => {
- resetHTMLFixture();
+ window.gon = origGon;
});
- describe('shows a placeholder popover on hover', () => {
- let linksWithUsers;
+ describe('when signed out', () => {
beforeEach(() => {
- linksWithUsers = findFixtureLinks();
+ setupTestSubject();
+ });
+
+ it('does not show a placeholder popover on hover', () => {
+ const linksWithUsers = findFixtureLinks();
linksWithUsers.forEach((el) => {
triggerEvent('mouseover', el);
});
+
+ expect(findPopovers().length).toBe(0);
});
+ });
- it('for initial links', () => {
- expect(findPopovers().length).toBe(linksWithUsers.length);
+ describe('when signed in', () => {
+ beforeEach(() => {
+ window.gon.current_user_id = 7;
+
+ setupTestSubject();
});
- it('for elements added after initial load', async () => {
- const addedLinks = [createUserLink(), createUserLink()];
- addedLinks.forEach((link) => {
- document.body.appendChild(link);
- });
+ afterEach(() => {
+ resetHTMLFixture();
+ });
- jest.runOnlyPendingTimers();
+ describe('shows a placeholder popover on hover', () => {
+ let linksWithUsers;
+ beforeEach(() => {
+ linksWithUsers = findFixtureLinks();
+ linksWithUsers.forEach((el) => {
+ triggerEvent('mouseover', el);
+ });
+ });
- addedLinks.forEach((link) => {
- triggerEvent('mouseover', link);
+ it('for initial links', () => {
+ expect(findPopovers().length).toBe(linksWithUsers.length);
});
- expect(findPopovers().length).toBe(linksWithUsers.length + addedLinks.length);
+ it('for elements added after initial load', async () => {
+ const addedLinks = [createUserLink(), createUserLink()];
+ addedLinks.forEach((link) => {
+ document.body.appendChild(link);
+ });
+
+ jest.runOnlyPendingTimers();
+
+ addedLinks.forEach((link) => {
+ triggerEvent('mouseover', link);
+ });
+
+ expect(findPopovers().length).toBe(linksWithUsers.length + addedLinks.length);
+ });
});
- });
- it('does not initialize the popovers for group references', async () => {
- const [groupLink] = Array.from(document.querySelectorAll('.js-user-link[data-group]'));
+ it('does not initialize the popovers for group references', async () => {
+ const [groupLink] = Array.from(document.querySelectorAll('.js-user-link[data-group]'));
- triggerEvent('mouseover', groupLink);
- jest.runOnlyPendingTimers();
+ triggerEvent('mouseover', groupLink);
+ jest.runOnlyPendingTimers();
- expect(findPopovers().length).toBe(0);
- });
+ expect(findPopovers().length).toBe(0);
+ });
- it('does not initialize the popovers for @all references', async () => {
- const [projectLink] = Array.from(document.querySelectorAll('.js-user-link[data-project]'));
+ it('does not initialize the popovers for @all references', async () => {
+ const [projectLink] = Array.from(document.querySelectorAll('.js-user-link[data-project]'));
- triggerEvent('mouseover', projectLink);
- jest.runOnlyPendingTimers();
+ triggerEvent('mouseover', projectLink);
+ jest.runOnlyPendingTimers();
- expect(findPopovers().length).toBe(0);
- });
+ expect(findPopovers().length).toBe(0);
+ });
- it('does not initialize the user popovers twice for the same element', async () => {
- const [firstUserLink] = findFixtureLinks();
- triggerEvent('mouseover', firstUserLink);
- jest.runOnlyPendingTimers();
- triggerEvent('mouseleave', firstUserLink);
- jest.runOnlyPendingTimers();
- triggerEvent('mouseover', firstUserLink);
- jest.runOnlyPendingTimers();
+ it('does not initialize the user popovers twice for the same element', async () => {
+ const [firstUserLink] = findFixtureLinks();
+ triggerEvent('mouseover', firstUserLink);
+ jest.runOnlyPendingTimers();
+ triggerEvent('mouseleave', firstUserLink);
+ jest.runOnlyPendingTimers();
+ triggerEvent('mouseover', firstUserLink);
+ jest.runOnlyPendingTimers();
- expect(findPopovers().length).toBe(1);
- });
+ expect(findPopovers().length).toBe(1);
+ });
- describe('when user link emits mouseenter event with empty user cache', () => {
- let userLink;
+ describe('when user link emits mouseenter event with empty user cache', () => {
+ let userLink;
- beforeEach(() => {
- UsersCache.retrieveById.mockReset();
+ beforeEach(() => {
+ UsersCache.retrieveById.mockReset();
- [userLink] = findFixtureLinks();
+ [userLink] = findFixtureLinks();
- triggerEvent('mouseover', userLink);
- });
+ triggerEvent('mouseover', userLink);
+ });
- it('populates popover with preloaded user data', () => {
- const { name, userId, username } = userLink.dataset;
+ it('populates popover with preloaded user data', () => {
+ const { name, userId, username } = userLink.dataset;
- expect(userLink.user).toEqual(
- expect.objectContaining({
- name,
- userId,
- username,
- }),
- );
+ expect(userLink.user).toEqual(
+ expect.objectContaining({
+ name,
+ userId,
+ username,
+ }),
+ );
+ });
});
- });
- describe('when user link emits mouseenter event', () => {
- let userLink;
+ describe('when user link emits mouseenter event', () => {
+ let userLink;
- beforeEach(() => {
- [userLink] = findFixtureLinks();
+ beforeEach(() => {
+ [userLink] = findFixtureLinks();
- triggerEvent('mouseover', userLink);
- });
+ triggerEvent('mouseover', userLink);
+ });
- it('removes title attribute from user links', () => {
- expect(userLink.getAttribute('title')).toBeFalsy();
- expect(userLink.dataset.originalTitle).toBeFalsy();
- });
+ it('removes title attribute from user links', () => {
+ expect(userLink.getAttribute('title')).toBeFalsy();
+ expect(userLink.dataset.originalTitle).toBeFalsy();
+ });
- it('fetches user info and status from the user cache', () => {
- const { userId } = userLink.dataset;
+ it('fetches user info and status from the user cache', () => {
+ const { userId } = userLink.dataset;
- expect(UsersCache.retrieveById).toHaveBeenCalledWith(userId);
- expect(UsersCache.retrieveStatusById).toHaveBeenCalledWith(userId);
- });
+ expect(UsersCache.retrieveById).toHaveBeenCalledWith(userId);
+ expect(UsersCache.retrieveStatusById).toHaveBeenCalledWith(userId);
+ });
- it('removes aria-describedby attribute from the user link on mouseleave', () => {
- userLink.setAttribute('aria-describedby', 'popover');
- triggerEvent('mouseleave', userLink);
+ it('removes aria-describedby attribute from the user link on mouseleave', () => {
+ userLink.setAttribute('aria-describedby', 'popover');
+ triggerEvent('mouseleave', userLink);
- expect(userLink.getAttribute('aria-describedby')).toBe(null);
- });
+ expect(userLink.getAttribute('aria-describedby')).toBe(null);
+ });
- it('updates toggle follow button and `UsersCache` when toggle follow button is clicked', async () => {
- const [firstPopover] = findPopovers();
- const withinFirstPopover = within(firstPopover);
- const findFollowButton = () => withinFirstPopover.queryByRole('button', { name: 'Follow' });
- const findUnfollowButton = () =>
- withinFirstPopover.queryByRole('button', { name: 'Unfollow' });
+ it('updates toggle follow button and `UsersCache` when toggle follow button is clicked', async () => {
+ const [firstPopover] = findPopovers();
+ const withinFirstPopover = within(firstPopover);
+ const findFollowButton = () => withinFirstPopover.queryByRole('button', { name: 'Follow' });
+ const findUnfollowButton = () =>
+ withinFirstPopover.queryByRole('button', { name: 'Unfollow' });
- jest.runOnlyPendingTimers();
+ jest.runOnlyPendingTimers();
- const { userId } = document.querySelector(selector).dataset;
+ const { userId } = document.querySelector(selector).dataset;
- triggerEvent('click', findFollowButton());
+ triggerEvent('click', findFollowButton());
- await waitForPromises();
+ await waitForPromises();
- expect(findUnfollowButton()).not.toBe(null);
- expect(UsersCache.updateById).toHaveBeenCalledWith(userId, { is_followed: true });
+ expect(findUnfollowButton()).not.toBe(null);
+ expect(UsersCache.updateById).toHaveBeenCalledWith(userId, { is_followed: true });
- triggerEvent('click', findUnfollowButton());
+ triggerEvent('click', findUnfollowButton());
- await waitForPromises();
+ await waitForPromises();
- expect(findFollowButton()).not.toBe(null);
- expect(UsersCache.updateById).toHaveBeenCalledWith(userId, { is_followed: false });
+ expect(findFollowButton()).not.toBe(null);
+ expect(UsersCache.updateById).toHaveBeenCalledWith(userId, { is_followed: false });
+ });
});
});
});
diff --git a/spec/frontend/vue_mr_widget/components/extensions/actions_spec.js b/spec/frontend/vue_merge_request_widget/components/action_buttons.js
index a13db2f4d72..6d714aeaf18 100644
--- a/spec/frontend/vue_mr_widget/components/extensions/actions_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/action_buttons.js
@@ -1,6 +1,6 @@
import { GlButton, GlDropdownItem } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import Actions from '~/vue_merge_request_widget/components/extensions/actions.vue';
+import Actions from '~/vue_merge_request_widget/components/action_buttons.vue';
let wrapper;
diff --git a/spec/frontend/vue_mr_widget/components/added_commit_message_spec.js b/spec/frontend/vue_merge_request_widget/components/added_commit_message_spec.js
index 150680caa7e..cb53dc1fb61 100644
--- a/spec/frontend/vue_mr_widget/components/added_commit_message_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/added_commit_message_spec.js
@@ -10,11 +10,6 @@ function factory(propsData) {
targetBranch: 'main',
...propsData,
},
- provide: {
- glFeatures: {
- restructuredMrWidget: true.valueOf,
- },
- },
});
}
diff --git a/spec/frontend/vue_mr_widget/components/approvals/approvals_spec.js b/spec/frontend/vue_merge_request_widget/components/approvals/approvals_spec.js
index 05cd1bb5b3d..05cd1bb5b3d 100644
--- a/spec/frontend/vue_mr_widget/components/approvals/approvals_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/approvals/approvals_spec.js
diff --git a/spec/frontend/vue_mr_widget/components/approvals/approvals_summary_optional_spec.js b/spec/frontend/vue_merge_request_widget/components/approvals/approvals_summary_optional_spec.js
index 65cafc647e0..65cafc647e0 100644
--- a/spec/frontend/vue_mr_widget/components/approvals/approvals_summary_optional_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/approvals/approvals_summary_optional_spec.js
diff --git a/spec/frontend/vue_mr_widget/components/approvals/approvals_summary_spec.js b/spec/frontend/vue_merge_request_widget/components/approvals/approvals_summary_spec.js
index c2606346292..c2606346292 100644
--- a/spec/frontend/vue_mr_widget/components/approvals/approvals_summary_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/approvals/approvals_summary_spec.js
diff --git a/spec/frontend/vue_mr_widget/components/approvals/humanized_text_spec.js b/spec/frontend/vue_merge_request_widget/components/approvals/humanized_text_spec.js
index d6776c00b29..d6776c00b29 100644
--- a/spec/frontend/vue_mr_widget/components/approvals/humanized_text_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/approvals/humanized_text_spec.js
diff --git a/spec/frontend/vue_mr_widget/components/artifacts_list_app_spec.js b/spec/frontend/vue_merge_request_widget/components/artifacts_list_app_spec.js
index e2386bc7f2b..e2386bc7f2b 100644
--- a/spec/frontend/vue_mr_widget/components/artifacts_list_app_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/artifacts_list_app_spec.js
diff --git a/spec/frontend/vue_mr_widget/components/artifacts_list_spec.js b/spec/frontend/vue_merge_request_widget/components/artifacts_list_spec.js
index 712abfe228a..712abfe228a 100644
--- a/spec/frontend/vue_mr_widget/components/artifacts_list_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/artifacts_list_spec.js
diff --git a/spec/frontend/vue_mr_widget/components/extensions/child_content_spec.js b/spec/frontend/vue_merge_request_widget/components/extensions/child_content_spec.js
index 198a4c2823a..198a4c2823a 100644
--- a/spec/frontend/vue_mr_widget/components/extensions/child_content_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/extensions/child_content_spec.js
diff --git a/spec/frontend/vue_mr_widget/components/extensions/index_spec.js b/spec/frontend/vue_merge_request_widget/components/extensions/index_spec.js
index dc25596655a..dc25596655a 100644
--- a/spec/frontend/vue_mr_widget/components/extensions/index_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/extensions/index_spec.js
diff --git a/spec/frontend/vue_mr_widget/components/extensions/status_icon_spec.js b/spec/frontend/vue_merge_request_widget/components/extensions/status_icon_spec.js
index f3aa5bb774f..f3aa5bb774f 100644
--- a/spec/frontend/vue_mr_widget/components/extensions/status_icon_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/extensions/status_icon_spec.js
diff --git a/spec/frontend/vue_mr_widget/components/extensions/utils_spec.js b/spec/frontend/vue_merge_request_widget/components/extensions/utils_spec.js
index 5799799ad5e..5799799ad5e 100644
--- a/spec/frontend/vue_mr_widget/components/extensions/utils_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/extensions/utils_spec.js
diff --git a/spec/frontend/vue_mr_widget/components/mr_collapsible_extension_spec.js b/spec/frontend/vue_merge_request_widget/components/mr_collapsible_extension_spec.js
index 01fbcb2154f..01fbcb2154f 100644
--- a/spec/frontend/vue_mr_widget/components/mr_collapsible_extension_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/mr_collapsible_extension_spec.js
diff --git a/spec/frontend/vue_mr_widget/components/mr_widget_alert_message_spec.js b/spec/frontend/vue_merge_request_widget/components/mr_widget_alert_message_spec.js
index 5d923d0383f..5d923d0383f 100644
--- a/spec/frontend/vue_mr_widget/components/mr_widget_alert_message_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/mr_widget_alert_message_spec.js
diff --git a/spec/frontend/vue_mr_widget/components/mr_widget_author_spec.js b/spec/frontend/vue_merge_request_widget/components/mr_widget_author_spec.js
index 8a42e2e2ce7..8a42e2e2ce7 100644
--- a/spec/frontend/vue_mr_widget/components/mr_widget_author_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/mr_widget_author_spec.js
diff --git a/spec/frontend/vue_mr_widget/components/mr_widget_author_time_spec.js b/spec/frontend/vue_merge_request_widget/components/mr_widget_author_time_spec.js
index 8fd93809e01..8fd93809e01 100644
--- a/spec/frontend/vue_mr_widget/components/mr_widget_author_time_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/mr_widget_author_time_spec.js
diff --git a/spec/frontend/vue_mr_widget/components/mr_widget_container_spec.js b/spec/frontend/vue_merge_request_widget/components/mr_widget_container_spec.js
index 4e3e918f7fb..4e3e918f7fb 100644
--- a/spec/frontend/vue_mr_widget/components/mr_widget_container_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/mr_widget_container_spec.js
diff --git a/spec/frontend/vue_mr_widget/components/mr_widget_expandable_section_spec.js b/spec/frontend/vue_merge_request_widget/components/mr_widget_expandable_section_spec.js
index 631aef412a6..631aef412a6 100644
--- a/spec/frontend/vue_mr_widget/components/mr_widget_expandable_section_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/mr_widget_expandable_section_spec.js
diff --git a/spec/frontend/vue_mr_widget/components/mr_widget_icon_spec.js b/spec/frontend/vue_merge_request_widget/components/mr_widget_icon_spec.js
index ebd10f31fa7..ebd10f31fa7 100644
--- a/spec/frontend/vue_mr_widget/components/mr_widget_icon_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/mr_widget_icon_spec.js
diff --git a/spec/frontend/vue_mr_widget/components/mr_widget_memory_usage_spec.js b/spec/frontend/vue_merge_request_widget/components/mr_widget_memory_usage_spec.js
index f0106914674..193a16bae8d 100644
--- a/spec/frontend/vue_mr_widget/components/mr_widget_memory_usage_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/mr_widget_memory_usage_spec.js
@@ -80,20 +80,20 @@ describe('MemoryUsage', () => {
it('should have default data', () => {
const data = MemoryUsage.data();
- expect(Array.isArray(data.memoryMetrics)).toBeTruthy();
+ expect(Array.isArray(data.memoryMetrics)).toBe(true);
expect(data.memoryMetrics.length).toBe(0);
expect(typeof data.deploymentTime).toBe('number');
expect(data.deploymentTime).toBe(0);
expect(typeof data.hasMetrics).toBe('boolean');
- expect(data.hasMetrics).toBeFalsy();
+ expect(data.hasMetrics).toBe(false);
expect(typeof data.loadFailed).toBe('boolean');
- expect(data.loadFailed).toBeFalsy();
+ expect(data.loadFailed).toBe(false);
expect(typeof data.loadingMetrics).toBe('boolean');
- expect(data.loadingMetrics).toBeTruthy();
+ expect(data.loadingMetrics).toBe(true);
expect(typeof data.backOffRequestCounter).toBe('number');
expect(data.backOffRequestCounter).toBe(0);
@@ -144,7 +144,7 @@ describe('MemoryUsage', () => {
vm.computeGraphData(metrics, deployment_time);
const { hasMetrics, memoryMetrics, deploymentTime, memoryFrom, memoryTo } = vm;
- expect(hasMetrics).toBeTruthy();
+ expect(hasMetrics).toBe(true);
expect(memoryMetrics.length).toBeGreaterThan(0);
expect(deploymentTime).toEqual(deployment_time);
expect(memoryFrom).toEqual('9.13');
@@ -171,7 +171,7 @@ describe('MemoryUsage', () => {
describe('template', () => {
it('should render template elements correctly', () => {
- expect(el.classList.contains('mr-memory-usage')).toBeTruthy();
+ expect(el.classList.contains('mr-memory-usage')).toBe(true);
expect(el.querySelector('.js-usage-info')).toBeDefined();
});
diff --git a/spec/frontend/vue_mr_widget/components/mr_widget_pipeline_container_spec.js b/spec/frontend/vue_merge_request_widget/components/mr_widget_pipeline_container_spec.js
index efe2bf75c3f..efe2bf75c3f 100644
--- a/spec/frontend/vue_mr_widget/components/mr_widget_pipeline_container_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/mr_widget_pipeline_container_spec.js
diff --git a/spec/frontend/vue_mr_widget/components/mr_widget_pipeline_spec.js b/spec/frontend/vue_merge_request_widget/components/mr_widget_pipeline_spec.js
index 6347e3c3be3..6347e3c3be3 100644
--- a/spec/frontend/vue_mr_widget/components/mr_widget_pipeline_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/mr_widget_pipeline_spec.js
diff --git a/spec/frontend/vue_mr_widget/components/mr_widget_rebase_spec.js b/spec/frontend/vue_merge_request_widget/components/mr_widget_rebase_spec.js
index 6db82cedd80..534c0baf35d 100644
--- a/spec/frontend/vue_mr_widget/components/mr_widget_rebase_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/mr_widget_rebase_spec.js
@@ -1,4 +1,4 @@
-import { shallowMount } from '@vue/test-utils';
+import { mount } from '@vue/test-utils';
import { nextTick } from 'vue';
import WidgetRebase from '~/vue_merge_request_widget/components/states/mr_widget_rebase.vue';
import eventHub from '~/vue_merge_request_widget/event_hub';
@@ -8,8 +8,8 @@ jest.mock('~/vue_shared/plugins/global_toast');
let wrapper;
-function createWrapper(propsData, mergeRequestWidgetGraphql, rebaseWithoutCiUi) {
- wrapper = shallowMount(WidgetRebase, {
+function createWrapper(propsData, mergeRequestWidgetGraphql) {
+ wrapper = mount(WidgetRebase, {
propsData,
data() {
return {
@@ -22,7 +22,7 @@ function createWrapper(propsData, mergeRequestWidgetGraphql, rebaseWithoutCiUi)
},
};
},
- provide: { glFeatures: { mergeRequestWidgetGraphql, rebaseWithoutCiUi } },
+ provide: { glFeatures: { mergeRequestWidgetGraphql } },
mocks: {
$apollo: {
queries: {
@@ -110,7 +110,7 @@ describe('Merge request widget rebase component', () => {
expect(findRebaseMessageText()).toContain('Something went wrong!');
});
- describe('Rebase buttons with flag rebaseWithoutCiUi', () => {
+ describe('Rebase buttons with', () => {
beforeEach(() => {
createWrapper(
{
@@ -124,7 +124,6 @@ describe('Merge request widget rebase component', () => {
},
},
mergeRequestWidgetGraphql,
- { rebaseWithoutCiUi: true },
);
});
@@ -149,35 +148,6 @@ describe('Merge request widget rebase component', () => {
expect(rebaseMock).toHaveBeenCalledWith({ skipCi: true });
});
});
-
- describe('Rebase button with rebaseWithoutCiUI flag disabled', () => {
- beforeEach(() => {
- createWrapper(
- {
- mr: {
- rebaseInProgress: false,
- canPushToSourceBranch: true,
- },
- service: {
- rebase: rebaseMock,
- poll: pollMock,
- },
- },
- mergeRequestWidgetGraphql,
- );
- });
-
- it('standard rebase button is rendered', () => {
- expect(findStandardRebaseButton().exists()).toBe(true);
- expect(findRebaseWithoutCiButton().exists()).toBe(false);
- });
-
- it('calls rebase method with skip_ci false', () => {
- findStandardRebaseButton().vm.$emit('click');
-
- expect(rebaseMock).toHaveBeenCalledWith({ skipCi: false });
- });
- });
});
describe('without permissions', () => {
@@ -216,24 +186,7 @@ describe('Merge request widget rebase component', () => {
});
});
- it('does not render the "Rebase without pipeline" button with rebaseWithoutCiUI flag enabled', () => {
- createWrapper(
- {
- mr: {
- rebaseInProgress: false,
- canPushToSourceBranch: false,
- targetBranch: exampleTargetBranch,
- },
- service: {},
- },
- mergeRequestWidgetGraphql,
- { rebaseWithoutCiUi: true },
- );
-
- expect(findRebaseWithoutCiButton().exists()).toBe(false);
- });
-
- it('does not render the standard rebase button with rebaseWithoutCiUI flag disabled', () => {
+ it('does render the "Rebase without pipeline" button', () => {
createWrapper(
{
mr: {
@@ -246,7 +199,7 @@ describe('Merge request widget rebase component', () => {
mergeRequestWidgetGraphql,
);
- expect(findStandardRebaseButton().exists()).toBe(false);
+ expect(findRebaseWithoutCiButton().exists()).toBe(true);
});
});
diff --git a/spec/frontend/vue_mr_widget/components/mr_widget_related_links_spec.js b/spec/frontend/vue_merge_request_widget/components/mr_widget_related_links_spec.js
index 15522f7ac1d..15522f7ac1d 100644
--- a/spec/frontend/vue_mr_widget/components/mr_widget_related_links_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/mr_widget_related_links_spec.js
diff --git a/spec/frontend/vue_mr_widget/components/mr_widget_status_icon_spec.js b/spec/frontend/vue_merge_request_widget/components/mr_widget_status_icon_spec.js
index c25e10c5249..11373be578a 100644
--- a/spec/frontend/vue_mr_widget/components/mr_widget_status_icon_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/mr_widget_status_icon_spec.js
@@ -6,7 +6,6 @@ describe('MR widget status icon component', () => {
let wrapper;
const findLoadingIcon = () => wrapper.find(GlLoadingIcon);
- const findDisabledMergeButton = () => wrapper.find('[data-testid="disabled-merge-button"]');
const createWrapper = (props, mountFn = shallowMount) => {
wrapper = mountFn(mrStatusIcon, {
@@ -41,20 +40,4 @@ describe('MR widget status icon component', () => {
expect(wrapper.find('[data-testid="status_failed-icon"]').exists()).toBe(true);
});
});
-
- describe('with disabled button', () => {
- it('renders a disabled button', () => {
- createWrapper({ status: 'failed', showDisabledButton: true });
-
- expect(findDisabledMergeButton().exists()).toBe(true);
- });
- });
-
- describe('without disabled button', () => {
- it('does not render a disabled button', () => {
- createWrapper({ status: 'failed' });
-
- expect(findDisabledMergeButton().exists()).toBe(false);
- });
- });
});
diff --git a/spec/frontend/vue_mr_widget/components/mr_widget_suggest_pipeline_spec.js b/spec/frontend/vue_merge_request_widget/components/mr_widget_suggest_pipeline_spec.js
index 352bc1a08ea..352bc1a08ea 100644
--- a/spec/frontend/vue_mr_widget/components/mr_widget_suggest_pipeline_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/mr_widget_suggest_pipeline_spec.js
diff --git a/spec/frontend/vue_mr_widget/components/pipeline_tour_mock_data.js b/spec/frontend/vue_merge_request_widget/components/pipeline_tour_mock_data.js
index eef087d62b8..eef087d62b8 100644
--- a/spec/frontend/vue_mr_widget/components/pipeline_tour_mock_data.js
+++ b/spec/frontend/vue_merge_request_widget/components/pipeline_tour_mock_data.js
diff --git a/spec/frontend/vue_mr_widget/components/review_app_link_spec.js b/spec/frontend/vue_merge_request_widget/components/review_app_link_spec.js
index e393b56034d..e393b56034d 100644
--- a/spec/frontend/vue_mr_widget/components/review_app_link_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/review_app_link_spec.js
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
new file mode 100644
index 00000000000..de25e2a0450
--- /dev/null
+++ b/spec/frontend/vue_merge_request_widget/components/states/__snapshots__/mr_widget_auto_merge_enabled_spec.js.snap
@@ -0,0 +1,241 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`MRWidgetAutoMergeEnabled when graphql is disabled template should have correct elements 1`] = `
+<div
+ class="mr-widget-body media"
+>
+ <svg
+ aria-hidden="true"
+ class="gl-text-blue-500 gl-mr-3 gl-mt-1 gl-icon s24"
+ data-testid="status_scheduled-icon"
+ role="img"
+ >
+ <use
+ href="#status_scheduled"
+ />
+ </svg>
+
+ <div
+ class="media-body gl-display-flex"
+ >
+
+ <h4
+ class="gl-mr-3"
+ data-testid="statusText"
+ >
+ Set by
+ <a
+ class="author-link inline"
+ >
+ <img
+ class="avatar avatar-inline s16"
+ src="no_avatar.png"
+ />
+
+ <span
+ class="author"
+ >
+
+ </span>
+ </a>
+ to be merged automatically when the pipeline succeeds
+ </h4>
+
+ <div
+ class="gl-display-flex gl-md-display-block gl-font-size-0 gl-ml-auto gl-mt-1"
+ >
+ <div>
+ <div
+ class="dropdown b-dropdown gl-new-dropdown gl-display-block gl-md-display-none! btn-group"
+ lazy=""
+ no-caret=""
+ >
+ <!---->
+ <button
+ aria-expanded="false"
+ aria-haspopup="true"
+ class="btn dropdown-toggle btn-default btn-sm gl-p-2! gl-button gl-dropdown-toggle btn-default-tertiary dropdown-icon-only dropdown-toggle-no-caret"
+ type="button"
+ >
+ <!---->
+
+ <svg
+ aria-hidden="true"
+ class="dropdown-icon gl-icon s16"
+ data-testid="ellipsis_v-icon"
+ role="img"
+ >
+ <use
+ href="#ellipsis_v"
+ />
+ </svg>
+
+ <span
+ class="gl-new-dropdown-button-text gl-sr-only"
+ >
+
+ </span>
+
+ <svg
+ aria-hidden="true"
+ class="gl-button-icon dropdown-chevron gl-icon s16"
+ data-testid="chevron-down-icon"
+ role="img"
+ >
+ <use
+ href="#chevron-down"
+ />
+ </svg>
+ </button>
+ <ul
+ class="dropdown-menu dropdown-menu-right"
+ role="menu"
+ tabindex="-1"
+ >
+ <!---->
+ </ul>
+ </div>
+
+ <button
+ class="btn gl-display-none gl-md-display-block gl-float-left btn-confirm btn-sm gl-button btn-confirm-tertiary js-cancel-auto-merge"
+ data-qa-selector="cancel_auto_merge_button"
+ data-testid="cancelAutomaticMergeButton"
+ type="button"
+ >
+ <!---->
+
+ <!---->
+
+ <span
+ class="gl-button-text"
+ >
+
+ Cancel auto-merge
+
+ </span>
+ </button>
+ </div>
+ </div>
+ </div>
+</div>
+`;
+
+exports[`MRWidgetAutoMergeEnabled when graphql is enabled template should have correct elements 1`] = `
+<div
+ class="mr-widget-body media"
+>
+ <svg
+ aria-hidden="true"
+ class="gl-text-blue-500 gl-mr-3 gl-mt-1 gl-icon s24"
+ data-testid="status_scheduled-icon"
+ role="img"
+ >
+ <use
+ href="#status_scheduled"
+ />
+ </svg>
+
+ <div
+ class="media-body gl-display-flex"
+ >
+
+ <h4
+ class="gl-mr-3"
+ data-testid="statusText"
+ >
+ Set by
+ <a
+ class="author-link inline"
+ >
+ <img
+ class="avatar avatar-inline s16"
+ src="no_avatar.png"
+ />
+
+ <span
+ class="author"
+ >
+
+ </span>
+ </a>
+ to be merged automatically when the pipeline succeeds
+ </h4>
+
+ <div
+ class="gl-display-flex gl-md-display-block gl-font-size-0 gl-ml-auto gl-mt-1"
+ >
+ <div>
+ <div
+ class="dropdown b-dropdown gl-new-dropdown gl-display-block gl-md-display-none! btn-group"
+ lazy=""
+ no-caret=""
+ >
+ <!---->
+ <button
+ aria-expanded="false"
+ aria-haspopup="true"
+ class="btn dropdown-toggle btn-default btn-sm gl-p-2! gl-button gl-dropdown-toggle btn-default-tertiary dropdown-icon-only dropdown-toggle-no-caret"
+ type="button"
+ >
+ <!---->
+
+ <svg
+ aria-hidden="true"
+ class="dropdown-icon gl-icon s16"
+ data-testid="ellipsis_v-icon"
+ role="img"
+ >
+ <use
+ href="#ellipsis_v"
+ />
+ </svg>
+
+ <span
+ class="gl-new-dropdown-button-text gl-sr-only"
+ >
+
+ </span>
+
+ <svg
+ aria-hidden="true"
+ class="gl-button-icon dropdown-chevron gl-icon s16"
+ data-testid="chevron-down-icon"
+ role="img"
+ >
+ <use
+ href="#chevron-down"
+ />
+ </svg>
+ </button>
+ <ul
+ class="dropdown-menu dropdown-menu-right"
+ role="menu"
+ tabindex="-1"
+ >
+ <!---->
+ </ul>
+ </div>
+
+ <button
+ class="btn gl-display-none gl-md-display-block gl-float-left btn-confirm btn-sm gl-button btn-confirm-tertiary js-cancel-auto-merge"
+ data-qa-selector="cancel_auto_merge_button"
+ data-testid="cancelAutomaticMergeButton"
+ type="button"
+ >
+ <!---->
+
+ <!---->
+
+ <span
+ class="gl-button-text"
+ >
+
+ Cancel auto-merge
+
+ </span>
+ </button>
+ </div>
+ </div>
+ </div>
+</div>
+`;
diff --git a/spec/frontend/vue_mr_widget/components/states/__snapshots__/mr_widget_pipeline_failed_spec.js.snap b/spec/frontend/vue_merge_request_widget/components/states/__snapshots__/mr_widget_pipeline_failed_spec.js.snap
index 98297630792..7e741bf4660 100644
--- a/spec/frontend/vue_mr_widget/components/states/__snapshots__/mr_widget_pipeline_failed_spec.js.snap
+++ b/spec/frontend/vue_merge_request_widget/components/states/__snapshots__/mr_widget_pipeline_failed_spec.js.snap
@@ -5,7 +5,7 @@ exports[`PipelineFailed should render error message with a disabled merge button
class="mr-widget-body media"
>
<status-icon-stub
- showdisabledbutton="true"
+ show-disabled-button="true"
status="warning"
/>
@@ -13,7 +13,7 @@ exports[`PipelineFailed should render error message with a disabled merge button
class="media-body space-children"
>
<span
- class="bold"
+ class="gl-ml-0! gl-text-body! bold"
>
<gl-sprintf-stub
message="Merge blocked: pipeline must succeed. Push a commit that fixes the failure, or %{linkStart}learn about other solutions.%{linkEnd}"
diff --git a/spec/frontend/vue_mr_widget/components/states/__snapshots__/new_ready_to_merge_spec.js.snap b/spec/frontend/vue_merge_request_widget/components/states/__snapshots__/new_ready_to_merge_spec.js.snap
index f9936f22ea3..f9936f22ea3 100644
--- a/spec/frontend/vue_mr_widget/components/states/__snapshots__/new_ready_to_merge_spec.js.snap
+++ b/spec/frontend/vue_merge_request_widget/components/states/__snapshots__/new_ready_to_merge_spec.js.snap
diff --git a/spec/frontend/vue_mr_widget/components/states/commit_edit_spec.js b/spec/frontend/vue_merge_request_widget/components/states/commit_edit_spec.js
index c0add94e6ed..c0add94e6ed 100644
--- a/spec/frontend/vue_mr_widget/components/states/commit_edit_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/states/commit_edit_spec.js
diff --git a/spec/frontend/vue_mr_widget/components/states/merge_checks_failed_spec.js b/spec/frontend/vue_merge_request_widget/components/states/merge_checks_failed_spec.js
index 1900b53ac11..1900b53ac11 100644
--- a/spec/frontend/vue_mr_widget/components/states/merge_checks_failed_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/states/merge_checks_failed_spec.js
diff --git a/spec/frontend/vue_mr_widget/components/states/merge_failed_pipeline_confirmation_dialog_spec.js b/spec/frontend/vue_merge_request_widget/components/states/merge_failed_pipeline_confirmation_dialog_spec.js
index 0e1c38437f0..c9aca01083d 100644
--- a/spec/frontend/vue_mr_widget/components/states/merge_failed_pipeline_confirmation_dialog_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/states/merge_failed_pipeline_confirmation_dialog_spec.js
@@ -50,7 +50,7 @@ describe('MergeFailedPipelineConfirmationDialog', () => {
it('should emit the mergeWithFailedPipeline event', () => {
findMergeBtn().vm.$emit('click');
- expect(wrapper.emitted('mergeWithFailedPipeline')).toBeTruthy();
+ expect(wrapper.emitted('mergeWithFailedPipeline')).toHaveLength(1);
});
it('when the cancel button is clicked should emit cancel and call hide', () => {
@@ -58,14 +58,14 @@ describe('MergeFailedPipelineConfirmationDialog', () => {
findCancelBtn().vm.$emit('click');
- expect(wrapper.emitted('cancel')).toBeTruthy();
+ expect(wrapper.emitted('cancel')).toHaveLength(1);
expect(findModal().vm.hide).toHaveBeenCalled();
});
it('should emit cancel when the hide event is emitted', () => {
findModal().vm.$emit('hide');
- expect(wrapper.emitted('cancel')).toBeTruthy();
+ expect(wrapper.emitted('cancel')).toHaveLength(1);
});
it('when modal is shown it will focus the cancel button', () => {
diff --git a/spec/frontend/vue_mr_widget/components/states/mr_widget_archived_spec.js b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_archived_spec.js
index f3061d792d0..9332b7e334a 100644
--- a/spec/frontend/vue_mr_widget/components/states/mr_widget_archived_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_archived_spec.js
@@ -18,11 +18,6 @@ describe('MRWidgetArchived', () => {
expect(vm.$el.querySelector('.ci-status-icon')).not.toBeNull();
});
- it('renders a disabled button', () => {
- expect(vm.$el.querySelector('button').getAttribute('disabled')).toEqual('disabled');
- expect(vm.$el.querySelector('button').textContent.trim()).toEqual('Merge');
- });
-
it('renders information', () => {
expect(vm.$el.querySelector('.bold').textContent.trim()).toEqual(
'Merge unavailable: merge requests are read-only on archived projects.',
diff --git a/spec/frontend/vue_mr_widget/components/states/mr_widget_auto_merge_enabled_spec.js b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled_spec.js
index 7387ed2d5e9..28182793683 100644
--- a/spec/frontend/vue_mr_widget/components/states/mr_widget_auto_merge_enabled_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled_spec.js
@@ -1,4 +1,4 @@
-import { shallowMount } from '@vue/test-utils';
+import { mount } from '@vue/test-utils';
import { nextTick } from 'vue';
import { trimText } from 'helpers/text_helper';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
@@ -37,7 +37,7 @@ function factory(propsData, stateOverride = {}) {
}
wrapper = extendedWrapper(
- shallowMount(autoMergeEnabledComponent, {
+ mount(autoMergeEnabledComponent, {
propsData: {
mr: propsData,
service: new MRWidgetService({}),
@@ -73,7 +73,7 @@ const defaultMrProps = () => ({
autoMergeStrategy: MWPS_MERGE_STRATEGY,
});
-const getStatusText = () => wrapper.findByTestId('statusText').attributes('message');
+const getStatusText = () => wrapper.findByTestId('statusText').text();
describe('MRWidgetAutoMergeEnabled', () => {
let oldWindowGl;
@@ -102,74 +102,6 @@ describe('MRWidgetAutoMergeEnabled', () => {
});
describe('computed', () => {
- describe('canRemoveSourceBranch', () => {
- it('should return true when user is able to remove source branch', () => {
- factory({
- ...defaultMrProps(),
- });
-
- expect(wrapper.findByTestId('removeSourceBranchButton').exists()).toBe(true);
- });
-
- it.each`
- mergeUserId | currentUserId
- ${2} | ${1}
- ${1} | ${2}
- `(
- 'should return false when user id is not the same with who set the MWPS',
- ({ mergeUserId, currentUserId }) => {
- factory({
- ...defaultMrProps(),
- mergeUserId,
- currentUserId,
- });
-
- expect(wrapper.findByTestId('removeSourceBranchButton').exists()).toBe(false);
- },
- );
-
- it('should not find "Delete" button when shouldRemoveSourceBranch set to true', () => {
- factory({
- ...defaultMrProps(),
- shouldRemoveSourceBranch: true,
- });
-
- expect(wrapper.findByTestId('removeSourceBranchButton').exists()).toBe(false);
- });
-
- it('should find "Delete" button when shouldRemoveSourceBranch overrides state.forceRemoveSourceBranch', () => {
- factory(
- {
- ...defaultMrProps(),
- shouldRemoveSourceBranch: false,
- },
- {
- forceRemoveSourceBranch: true,
- },
- );
-
- expect(wrapper.findByTestId('removeSourceBranchButton').exists()).toBe(true);
- });
-
- it('should find "Delete" button when shouldRemoveSourceBranch set to false', () => {
- factory({
- ...defaultMrProps(),
- shouldRemoveSourceBranch: false,
- });
-
- expect(wrapper.findByTestId('removeSourceBranchButton').exists()).toBe(true);
- });
-
- it('should return false if user is not able to remove the source branch', () => {
- factory({
- ...defaultMrProps(),
- canRemoveSourceBranch: false,
- });
-
- expect(wrapper.findByTestId('removeSourceBranchButton').exists()).toBe(false);
- });
- });
-
describe('cancelButtonText', () => {
it('should return "Cancel" if MWPS is selected', () => {
factory({
@@ -205,7 +137,7 @@ describe('MRWidgetAutoMergeEnabled', () => {
await waitForPromises();
- expect(wrapper.vm.isCancellingAutoMerge).toBeTruthy();
+ expect(wrapper.vm.isCancellingAutoMerge).toBe(true);
if (mergeRequestWidgetGraphql) {
expect(eventHub.$emit).toHaveBeenCalledWith('MRWidgetUpdateRequested');
} else {
@@ -265,50 +197,14 @@ describe('MRWidgetAutoMergeEnabled', () => {
expect(wrapper.find('.js-cancel-auto-merge').props('loading')).toBe(true);
});
- it('should show source branch will be deleted text when it source branch set to remove', () => {
- factory({
- ...defaultMrProps(),
- shouldRemoveSourceBranch: true,
- });
-
- const normalizedText = wrapper.text().replace(/\s+/g, ' ');
-
- expect(normalizedText).toContain('Deletes the source branch');
- expect(normalizedText).not.toContain('Does not delete the source branch');
- });
-
- it('should not show delete source branch button when user not able to delete source branch', () => {
- factory({
- ...defaultMrProps(),
- currentUserId: 4,
- });
-
- expect(wrapper.find('.js-remove-source-branch').exists()).toBe(false);
- });
-
- it('should disable delete source branch button when the action is in progress', async () => {
- factory({
- ...defaultMrProps(),
- });
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({
- isRemovingSourceBranch: true,
- });
-
- await nextTick();
-
- expect(wrapper.find('.js-remove-source-branch').props('loading')).toBe(true);
- });
-
it('should render the status text as "...to merged automatically" if MWPS is selected', () => {
factory({
...defaultMrProps(),
autoMergeStrategy: MWPS_MERGE_STRATEGY,
});
- expect(getStatusText()).toBe(
- 'Set by %{merge_author} to be merged automatically when the pipeline succeeds',
+ expect(getStatusText()).toContain(
+ 'to be merged automatically when the pipeline succeeds',
);
});
diff --git a/spec/frontend/vue_mr_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 24198096564..9320e733636 100644
--- a/spec/frontend/vue_mr_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
@@ -1,5 +1,5 @@
import { GlLoadingIcon, GlButton } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
+import { mount } from '@vue/test-utils';
import { nextTick } from 'vue';
import AutoMergeFailedComponent from '~/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.vue';
import eventHub from '~/vue_merge_request_widget/event_hub';
@@ -10,7 +10,7 @@ describe('MRWidgetAutoMergeFailed', () => {
const findButton = () => wrapper.find(GlButton);
const createComponent = (props = {}, mergeRequestWidgetGraphql = false) => {
- wrapper = shallowMount(AutoMergeFailedComponent, {
+ wrapper = mount(AutoMergeFailedComponent, {
propsData: { ...props },
data() {
if (mergeRequestWidgetGraphql) {
@@ -60,7 +60,7 @@ describe('MRWidgetAutoMergeFailed', () => {
await nextTick();
- expect(findButton().attributes('disabled')).toBe('true');
+ expect(findButton().attributes('disabled')).toBe('disabled');
expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
});
});
diff --git a/spec/frontend/vue_mr_widget/components/states/mr_widget_checking_spec.js b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_checking_spec.js
index afe6bd0e767..02de426204b 100644
--- a/spec/frontend/vue_mr_widget/components/states/mr_widget_checking_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_checking_spec.js
@@ -15,10 +15,6 @@ describe('MRWidgetChecking', () => {
vm.$destroy();
});
- it('renders disabled button', () => {
- expect(vm.$el.querySelector('button').getAttribute('disabled')).toEqual('disabled');
- });
-
it('renders loading icon', () => {
expect(vm.$el.querySelector('.mr-widget-icon span').classList).toContain('gl-spinner');
});
diff --git a/spec/frontend/vue_mr_widget/components/states/mr_widget_closed_spec.js b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_closed_spec.js
index 6ae218ce6f8..f7d046eb8f9 100644
--- a/spec/frontend/vue_mr_widget/components/states/mr_widget_closed_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_closed_spec.js
@@ -36,28 +36,4 @@ describe('MRWidgetClosed', () => {
it('renders warning icon', () => {
expect(vm.$el.querySelector('.js-ci-status-icon-warning')).not.toBeNull();
});
-
- it('renders closed by information with author and time', () => {
- expect(
- vm.$el.querySelector('.js-mr-widget-author').textContent.trim().replace(/\s\s+/g, ' '),
- ).toContain('Closed by Administrator less than a minute ago');
- });
-
- it('links to the user that closed the MR', () => {
- expect(vm.$el.querySelector('.author-link').getAttribute('href')).toEqual(
- 'http://localhost:3000/root',
- );
- });
-
- it('renders information about the changes not being merged', () => {
- expect(
- vm.$el.querySelector('.mr-info-list').textContent.trim().replace(/\s\s+/g, ' '),
- ).toContain('The changes were not merged into so_long_jquery');
- });
-
- it('renders link for target branch', () => {
- expect(vm.$el.querySelector('.label-branch').getAttribute('href')).toEqual(
- '/twitter/flight/commits/so_long_jquery',
- );
- });
});
diff --git a/spec/frontend/vue_mr_widget/components/states/mr_widget_commit_message_dropdown_spec.js b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_commit_message_dropdown_spec.js
index 663fabb761c..663fabb761c 100644
--- a/spec/frontend/vue_mr_widget/components/states/mr_widget_commit_message_dropdown_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_commit_message_dropdown_spec.js
diff --git a/spec/frontend/vue_mr_widget/components/states/mr_widget_commits_header_spec.js b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_commits_header_spec.js
index 2796403b7d0..774e2bafed3 100644
--- a/spec/frontend/vue_mr_widget/components/states/mr_widget_commits_header_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_commits_header_spec.js
@@ -27,7 +27,6 @@ describe('Commits header component', () => {
const findHeaderWrapper = () => wrapper.find('.js-mr-widget-commits-count');
const findCommitToggle = () => wrapper.find('.commit-edit-toggle');
- const findCommitsCountMessage = () => wrapper.find('.commits-count-message');
const findTargetBranchMessage = () => wrapper.find('.label-branch');
const findModifyButton = () => wrapper.find('.modify-message-button');
@@ -40,7 +39,7 @@ describe('Commits header component', () => {
});
it('has commits count message showing 1 commit', () => {
- expect(findCommitsCountMessage().text()).toBe('1 commit');
+ expect(wrapper.text()).toContain('1 commit');
});
it('has button with modify commit message', () => {
@@ -75,7 +74,7 @@ describe('Commits header component', () => {
});
it('has commits count message showing correct amount of commits', () => {
- expect(findCommitsCountMessage().text()).toBe('5 commits');
+ expect(wrapper.text()).toContain('5 commits');
});
it('has button with modify merge commit message', () => {
@@ -89,7 +88,7 @@ describe('Commits header component', () => {
});
it('has commits count message showing one commit when squash is enabled', () => {
- expect(findCommitsCountMessage().text()).toBe('1 commit');
+ expect(wrapper.text()).toContain('1 commit');
});
it('has button with modify commit messages text', () => {
diff --git a/spec/frontend/vue_mr_widget/components/states/mr_widget_conflicts_spec.js b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_conflicts_spec.js
index 7a92484695c..7a9fd5b002d 100644
--- a/spec/frontend/vue_mr_widget/components/states/mr_widget_conflicts_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_conflicts_spec.js
@@ -1,4 +1,4 @@
-import { shallowMount } from '@vue/test-utils';
+import { mount } from '@vue/test-utils';
import { nextTick } from 'vue';
import { TEST_HOST } from 'helpers/test_constants';
import { removeBreakLine } from 'helpers/text_helper';
@@ -23,7 +23,7 @@ describe('MRWidgetConflicts', () => {
async function createComponent(propsData = {}) {
wrapper = extendedWrapper(
- shallowMount(ConflictsComponent, {
+ mount(ConflictsComponent, {
propsData,
provide: {
glFeatures: {
diff --git a/spec/frontend/vue_mr_widget/components/states/mr_widget_failed_to_merge_spec.js b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_failed_to_merge_spec.js
index 6d8e7056366..989aa76f09b 100644
--- a/spec/frontend/vue_mr_widget/components/states/mr_widget_failed_to_merge_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_failed_to_merge_spec.js
@@ -1,6 +1,5 @@
import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
-import StatusIcon from '~/vue_merge_request_widget/components/mr_widget_status_icon.vue';
import MrWidgetFailedToMerge from '~/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.vue';
import eventHub from '~/vue_merge_request_widget/event_hub';
@@ -54,7 +53,31 @@ describe('MRWidgetFailedToMerge', () => {
await nextTick();
- expect(wrapper.vm.mergeError).toBe('contains line breaks.');
+ expect(wrapper.find('[data-testid="merge-error"]').text()).toBe('contains line breaks.');
+ });
+
+ it('does not append an extra period', async () => {
+ createComponent({ mr: { mergeError: 'contains a period.' } });
+
+ await nextTick();
+
+ expect(wrapper.find('[data-testid="merge-error"]').text()).toBe('contains a period.');
+ });
+
+ it('does not insert an extra space between the final character and the period', async () => {
+ createComponent({ mr: { mergeError: 'contains a <a href="http://example.com">link</a>.' } });
+
+ await nextTick();
+
+ expect(wrapper.find('[data-testid="merge-error"]').text()).toBe('contains a link.');
+ });
+
+ it('removes extra spaces', async () => {
+ createComponent({ mr: { mergeError: 'contains a lot of spaces .' } });
+
+ await nextTick();
+
+ expect(wrapper.find('[data-testid="merge-error"]').text()).toBe('contains a lot of spaces.');
});
});
@@ -116,7 +139,6 @@ describe('MRWidgetFailedToMerge', () => {
it('renders warning icon and disabled merge button', () => {
expect(wrapper.find('.js-ci-status-icon-warning')).not.toBeNull();
- expect(wrapper.find(StatusIcon).props('showDisabledButton')).toBe(true);
});
it('renders given error', () => {
diff --git a/spec/frontend/vue_mr_widget/components/states/mr_widget_merged_spec.js b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_merged_spec.js
index 29ee7e0010f..2606933450e 100644
--- a/spec/frontend/vue_mr_widget/components/states/mr_widget_merged_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_merged_spec.js
@@ -1,5 +1,5 @@
import { getByRole } from '@testing-library/dom';
-import Vue, { nextTick } from 'vue';
+import Vue from 'vue';
import mountComponent from 'helpers/vue_mount_component_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { OPEN_REVERT_MODAL, OPEN_CHERRY_PICK_MODAL } from '~/projects/commit/constants';
@@ -10,14 +10,6 @@ import eventHub from '~/vue_merge_request_widget/event_hub';
describe('MRWidgetMerged', () => {
let vm;
const targetBranch = 'foo';
- const selectors = {
- get copyMergeShaButton() {
- return vm.$el.querySelector('button.js-mr-merged-copy-sha');
- },
- get mergeCommitShaLink() {
- return vm.$el.querySelector('a.js-mr-merged-commit-sha');
- },
- };
beforeEach(() => {
jest.spyOn(document, 'dispatchEvent');
@@ -177,58 +169,11 @@ describe('MRWidgetMerged', () => {
expect(vm.$el.textContent).toContain('Administrator');
});
- it('renders branch information', () => {
- expect(vm.$el.textContent).toContain('The changes were merged into');
- expect(vm.$el.textContent).toContain(targetBranch);
- });
-
- it('renders information about branch being deleted', () => {
- expect(vm.$el.textContent).toContain('The source branch has been deleted');
- });
-
it('shows revert and cherry-pick buttons', () => {
expect(vm.$el.textContent).toContain('Revert');
expect(vm.$el.textContent).toContain('Cherry-pick');
});
- it('shows button to copy commit SHA to clipboard', () => {
- expect(selectors.copyMergeShaButton).not.toBe(null);
- expect(selectors.copyMergeShaButton.dataset.clipboardText).toBe(vm.mr.mergeCommitSha);
- });
-
- it('hides button to copy commit SHA if SHA does not exist', async () => {
- vm.mr.mergeCommitSha = null;
-
- await nextTick();
-
- expect(selectors.copyMergeShaButton).toBe(null);
- expect(vm.$el.querySelector('.mr-info-list').innerText).not.toContain('with');
- });
-
- it('shows merge commit SHA link', () => {
- expect(selectors.mergeCommitShaLink).not.toBe(null);
- expect(selectors.mergeCommitShaLink.text).toContain(vm.mr.shortMergeCommitSha);
- expect(selectors.mergeCommitShaLink.href).toBe(vm.mr.mergeCommitPath);
- });
-
- it('should not show source branch deleted text', async () => {
- vm.mr.sourceBranchRemoved = false;
-
- await nextTick();
-
- expect(vm.$el.innerText).not.toContain('The source branch has been deleted');
- });
-
- it('should show source branch deleting text', async () => {
- vm.mr.isRemovingSourceBranch = true;
- vm.mr.sourceBranchRemoved = false;
-
- await nextTick();
-
- expect(vm.$el.innerText).toContain('The source branch is being deleted');
- expect(vm.$el.innerText).not.toContain('The source branch has been deleted');
- });
-
it('should use mergedEvent mergedAt as tooltip title', () => {
expect(vm.$el.querySelector('time').getAttribute('title')).toBe('Jan 24, 2018 1:02pm UTC');
});
diff --git a/spec/frontend/vue_mr_widget/components/states/mr_widget_merging_spec.js b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_merging_spec.js
index e16c897a49b..49bd3739fdb 100644
--- a/spec/frontend/vue_mr_widget/components/states/mr_widget_merging_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_merging_spec.js
@@ -43,19 +43,6 @@ describe('MRWidgetMerging', () => {
).toContain('Merging!');
});
- it('renders branch information', () => {
- expect(
- wrapper
- .find('.mr-info-list')
- .text()
- .trim()
- .replace(/\s\s+/g, ' ')
- .replace(/[\r\n]+/g, ' '),
- ).toEqual('Merges changes into branch');
-
- expect(wrapper.find('a').attributes('href')).toBe('/branch-path');
- });
-
describe('initiateMergePolling', () => {
it('should call simplePoll', () => {
wrapper.vm.initiateMergePolling();
diff --git a/spec/frontend/vue_mr_widget/components/states/mr_widget_missing_branch_spec.js b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_missing_branch_spec.js
index ddce07954ab..ddce07954ab 100644
--- a/spec/frontend/vue_mr_widget/components/states/mr_widget_missing_branch_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_missing_branch_spec.js
diff --git a/spec/frontend/vue_mr_widget/components/states/mr_widget_not_allowed_spec.js b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_not_allowed_spec.js
index 63e93074857..63e93074857 100644
--- a/spec/frontend/vue_mr_widget/components/states/mr_widget_not_allowed_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_not_allowed_spec.js
diff --git a/spec/frontend/vue_mr_widget/components/states/mr_widget_nothing_to_merge_spec.js b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_nothing_to_merge_spec.js
index c7c0b69425d..6de0c06c33d 100644
--- a/spec/frontend/vue_mr_widget/components/states/mr_widget_nothing_to_merge_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_nothing_to_merge_spec.js
@@ -13,7 +13,7 @@ describe('NothingToMerge', () => {
});
it('should have correct elements', () => {
- expect(vm.$el.classList.contains('mr-widget-body')).toBeTruthy();
+ expect(vm.$el.classList.contains('mr-widget-body')).toBe(true);
expect(vm.$el.querySelector('[data-testid="createFileButton"]').href).toContain(newBlobPath);
expect(vm.$el.innerText).toContain('Use merge requests to propose changes to your project');
});
diff --git a/spec/frontend/vue_mr_widget/components/states/mr_widget_pipeline_blocked_spec.js b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked_spec.js
index 9b10b078e89..9b10b078e89 100644
--- a/spec/frontend/vue_mr_widget/components/states/mr_widget_pipeline_blocked_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked_spec.js
diff --git a/spec/frontend/vue_mr_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 3e0840fef4e..4e44ac539f2 100644
--- a/spec/frontend/vue_mr_widget/components/states/mr_widget_pipeline_failed_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_pipeline_failed_spec.js
@@ -1,5 +1,4 @@
import { shallowMount } from '@vue/test-utils';
-import statusIcon from '~/vue_merge_request_widget/components/mr_widget_status_icon.vue';
import PipelineFailed from '~/vue_merge_request_widget/components/states/pipeline_failed.vue';
describe('PipelineFailed', () => {
@@ -9,8 +8,6 @@ describe('PipelineFailed', () => {
wrapper = shallowMount(PipelineFailed);
};
- const findStatusIcon = () => wrapper.find(statusIcon);
-
beforeEach(() => {
createComponent();
});
@@ -23,8 +20,4 @@ describe('PipelineFailed', () => {
it('should render error message with a disabled merge button', () => {
expect(wrapper.element).toMatchSnapshot();
});
-
- it('merge button should be disabled', () => {
- expect(findStatusIcon().props('showDisabledButton')).toBe(true);
- });
});
diff --git a/spec/frontend/vue_mr_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 46d90ddc83c..6e89cd41559 100644
--- a/spec/frontend/vue_mr_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
@@ -1,5 +1,5 @@
-import { createLocalVue, shallowMount } from '@vue/test-utils';
-import { nextTick } from 'vue';
+import { shallowMount } from '@vue/test-utils';
+import Vue, { nextTick } from 'vue';
import { GlSprintf } from '@gitlab/ui';
import VueApollo from 'vue-apollo';
import produce from 'immer';
@@ -10,7 +10,6 @@ import readyToMergeQuery from 'ee_else_ce/vue_merge_request_widget/queries/state
import simplePoll from '~/lib/utils/simple_poll';
import CommitEdit from '~/vue_merge_request_widget/components/states/commit_edit.vue';
import CommitMessageDropdown from '~/vue_merge_request_widget/components/states/commit_message_dropdown.vue';
-import CommitsHeader from '~/vue_merge_request_widget/components/states/commits_header.vue';
import ReadyToMerge from '~/vue_merge_request_widget/components/states/ready_to_merge.vue';
import SquashBeforeMerge from '~/vue_merge_request_widget/components/states/squash_before_merge.vue';
import MergeFailedPipelineConfirmationDialog from '~/vue_merge_request_widget/components/states/merge_failed_pipeline_confirmation_dialog.vue';
@@ -60,6 +59,7 @@ const createTestMr = (customConfig) => {
transitionStateMachine: (transition) => eventHub.$emit('StateMachineValueChanged', transition),
translateStateToMachine: () => this.transitionStateMachine(),
state: 'open',
+ canMerge: true,
};
Object.assign(mr, customConfig.mr);
@@ -71,8 +71,8 @@ const createTestService = () => ({
merge: jest.fn(),
poll: jest.fn().mockResolvedValue(),
});
-const localVue = createLocalVue();
-localVue.use(VueApollo);
+
+Vue.use(VueApollo);
let wrapper;
let readyToMergeResponseSpy;
@@ -90,10 +90,9 @@ const createReadyToMergeResponse = (customMr) => {
const createComponent = (
customConfig = {},
mergeRequestWidgetGraphql = false,
- restructuredMrWidget = false,
+ restructuredMrWidget = true,
) => {
wrapper = shallowMount(ReadyToMerge, {
- localVue,
propsData: {
mr: createTestMr(customConfig),
service: createTestService(),
@@ -112,7 +111,6 @@ const createComponent = (
};
const findCheckboxElement = () => wrapper.find(SquashBeforeMerge);
-const findCommitsHeaderElement = () => wrapper.find(CommitsHeader);
const findCommitEditElements = () => wrapper.findAll(CommitEdit);
const findCommitDropdownElement = () => wrapper.find(CommitMessageDropdown);
const findFirstCommitEditLabel = () => findCommitEditElements().at(0).props('label');
@@ -371,7 +369,7 @@ describe('ReadyToMerge', () => {
const params = wrapper.vm.service.merge.mock.calls[0][0];
- expect(params.should_remove_source_branch).toBeTruthy();
+ expect(params.should_remove_source_branch).toBe(true);
expect(params.auto_merge_strategy).toBeUndefined();
});
@@ -395,7 +393,7 @@ describe('ReadyToMerge', () => {
const params = wrapper.vm.service.merge.mock.calls[0][0];
- expect(params.should_remove_source_branch).toBeTruthy();
+ expect(params.should_remove_source_branch).toBe(true);
expect(params.auto_merge_strategy).toBeUndefined();
});
@@ -471,8 +469,8 @@ describe('ReadyToMerge', () => {
expect(eventHub.$emit).toHaveBeenCalledWith('SetBranchRemoveFlag', [false]);
- expect(cpc).toBeFalsy();
- expect(spc).toBeTruthy();
+ expect(cpc).toBe(false);
+ expect(spc).toBe(true);
});
it('should continue polling until MR is merged', async () => {
@@ -494,8 +492,8 @@ describe('ReadyToMerge', () => {
await waitForPromises();
- expect(cpc).toBeTruthy();
- expect(spc).toBeFalsy();
+ expect(cpc).toBe(true);
+ expect(spc).toBe(false);
});
});
});
@@ -529,13 +527,13 @@ describe('ReadyToMerge', () => {
mr: { commitsCount: 2, enableSquashBeforeMerge: true },
});
- expect(findCheckboxElement().exists()).toBeTruthy();
+ expect(findCheckboxElement().exists()).toBe(true);
});
it('should not be rendered when squash before merge is disabled', () => {
createComponent({ mr: { commitsCount: 2, enableSquashBeforeMerge: false } });
- expect(findCheckboxElement().exists()).toBeFalsy();
+ expect(findCheckboxElement().exists()).toBe(false);
});
it('should be rendered when there is only 1 commit', () => {
@@ -576,71 +574,9 @@ describe('ReadyToMerge', () => {
});
});
- describe('commits count collapsible header', () => {
- it('should be rendered when fast-forward is disabled', () => {
- createComponent();
-
- expect(findCommitsHeaderElement().exists()).toBeTruthy();
- });
-
- describe('when fast-forward is enabled', () => {
- it('should be rendered if squash and squash before are enabled and there is more than 1 commit', () => {
- createComponent({
- mr: {
- ffOnlyEnabled: true,
- enableSquashBeforeMerge: true,
- squashIsSelected: true,
- commitsCount: 2,
- },
- });
-
- expect(findCommitsHeaderElement().exists()).toBeTruthy();
- });
-
- it('should not be rendered if squash before merge is disabled', () => {
- createComponent({
- mr: {
- ffOnlyEnabled: true,
- enableSquashBeforeMerge: false,
- squash: true,
- commitsCount: 2,
- },
- });
-
- expect(findCommitsHeaderElement().exists()).toBeFalsy();
- });
-
- it('should not be rendered if squash is disabled', () => {
- createComponent({
- mr: {
- ffOnlyEnabled: true,
- squash: false,
- enableSquashBeforeMerge: true,
- commitsCount: 2,
- },
- });
-
- expect(findCommitsHeaderElement().exists()).toBeFalsy();
- });
-
- it('should not be rendered if commits count is 1', () => {
- createComponent({
- mr: {
- ffOnlyEnabled: true,
- squash: true,
- enableSquashBeforeMerge: true,
- commitsCount: 1,
- },
- });
-
- expect(findCommitsHeaderElement().exists()).toBeFalsy();
- });
- });
- });
-
describe('commits edit components', () => {
describe('when fast-forward merge is enabled', () => {
- it('should not be rendered if squash is disabled', () => {
+ it('should not be rendered if squash is disabled', async () => {
createComponent({
mr: {
ffOnlyEnabled: true,
@@ -679,7 +615,7 @@ describe('ReadyToMerge', () => {
expect(findCommitEditElements().length).toBe(0);
});
- it('should have one edit component if squash is enabled and there is more than 1 commit', () => {
+ it('should have one edit component if squash is enabled and there is more than 1 commit', async () => {
createComponent({
mr: {
ffOnlyEnabled: true,
@@ -689,18 +625,14 @@ describe('ReadyToMerge', () => {
},
});
+ await wrapper.find('[data-testid="widget_edit_commit_message"]').vm.$emit('input', true);
+
expect(findCommitEditElements().length).toBe(1);
expect(findFirstCommitEditLabel()).toBe('Squash commit message');
});
});
- it('should have one edit component when squash is disabled', () => {
- createComponent();
-
- expect(findCommitEditElements().length).toBe(1);
- });
-
- it('should have two edit components when squash is enabled and there is more than 1 commit', () => {
+ it('should have two edit components when squash is enabled and there is more than 1 commit', async () => {
createComponent({
mr: {
commitsCount: 2,
@@ -709,6 +641,8 @@ describe('ReadyToMerge', () => {
},
});
+ await wrapper.find('[data-testid="widget_edit_commit_message"]').vm.$emit('input', true);
+
expect(findCommitEditElements().length).toBe(2);
});
@@ -738,11 +672,12 @@ describe('ReadyToMerge', () => {
},
});
await nextTick();
+ await wrapper.find('[data-testid="widget_edit_commit_message"]').vm.$emit('input', true);
expect(findCommitEditElements().length).toBe(2);
});
- it('should have one edit components when squash is enabled and there is 1 commit only', () => {
+ it('should have one edit components when squash is enabled and there is 1 commit only', async () => {
createComponent({
mr: {
commitsCount: 1,
@@ -751,16 +686,12 @@ describe('ReadyToMerge', () => {
},
});
- expect(findCommitEditElements().length).toBe(1);
- });
-
- it('should have correct edit merge commit label', () => {
- createComponent();
+ await wrapper.find('[data-testid="widget_edit_commit_message"]').vm.$emit('input', true);
- expect(findFirstCommitEditLabel()).toBe('Merge commit message');
+ expect(findCommitEditElements().length).toBe(1);
});
- it('should have correct edit squash commit label', () => {
+ it('should have correct edit squash commit label', async () => {
createComponent({
mr: {
commitsCount: 2,
@@ -769,6 +700,8 @@ describe('ReadyToMerge', () => {
},
});
+ await wrapper.find('[data-testid="widget_edit_commit_message"]').vm.$emit('input', true);
+
expect(findFirstCommitEditLabel()).toBe('Squash commit message');
});
});
@@ -777,48 +710,26 @@ describe('ReadyToMerge', () => {
it('should not be rendered if squash is disabled', () => {
createComponent();
- expect(findCommitDropdownElement().exists()).toBeFalsy();
+ expect(findCommitDropdownElement().exists()).toBe(false);
});
- it('should be rendered if squash is enabled and there is more than 1 commit', () => {
+ it('should be rendered if squash is enabled and there is more than 1 commit', async () => {
createComponent({
mr: { enableSquashBeforeMerge: true, squashIsSelected: true, commitsCount: 2 },
});
- expect(findCommitDropdownElement().exists()).toBeTruthy();
+ await wrapper.find('[data-testid="widget_edit_commit_message"]').vm.$emit('input', true);
+
+ expect(findCommitDropdownElement().exists()).toBe(true);
});
});
- it('renders a tip including a link to docs on templates', () => {
+ it('renders a tip including a link to docs on templates', async () => {
createComponent();
- expect(findTipLink().exists()).toBe(true);
- });
- });
-
- describe('Merge request project settings', () => {
- describe('when the merge commit merge method is enabled', () => {
- beforeEach(() => {
- createComponent({
- mr: { ffOnlyEnabled: false },
- });
- });
+ await wrapper.find('[data-testid="widget_edit_commit_message"]').vm.$emit('input', true);
- it('should not show fast forward message', () => {
- expect(wrapper.find('.mr-fast-forward-message').exists()).toBe(false);
- });
- });
-
- describe('when the fast-forward merge method is enabled', () => {
- beforeEach(() => {
- createComponent({
- mr: { ffOnlyEnabled: true },
- });
- });
-
- it('should show fast forward message', () => {
- expect(wrapper.find('.mr-fast-forward-message').exists()).toBe(true);
- });
+ expect(findTipLink().exists()).toBe(true);
});
});
@@ -873,6 +784,7 @@ describe('ReadyToMerge', () => {
createDefaultGqlComponent();
await waitForPromises();
+ await wrapper.find('[data-testid="widget_edit_commit_message"]').vm.$emit('input', true);
expect(finderFn()).toBe(initialValue);
});
@@ -880,6 +792,7 @@ describe('ReadyToMerge', () => {
it('should have updated value after graphql refetch', async () => {
createDefaultGqlComponent();
await waitForPromises();
+ await wrapper.find('[data-testid="widget_edit_commit_message"]').vm.$emit('input', true);
triggerApprovalUpdated();
await waitForPromises();
@@ -890,6 +803,7 @@ describe('ReadyToMerge', () => {
it('should not update if user has touched', async () => {
createDefaultGqlComponent();
await waitForPromises();
+ await wrapper.find('[data-testid="widget_edit_commit_message"]').vm.$emit('input', true);
const input = wrapper.find(inputId);
input.element.value = USER_COMMIT_MESSAGE;
diff --git a/spec/frontend/vue_mr_widget/components/states/mr_widget_sha_mismatch_spec.js b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_sha_mismatch_spec.js
index 2a343997cf5..2a343997cf5 100644
--- a/spec/frontend/vue_mr_widget/components/states/mr_widget_sha_mismatch_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_sha_mismatch_spec.js
diff --git a/spec/frontend/vue_mr_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..6ea2e8675d3 100644
--- a/spec/frontend/vue_mr_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
diff --git a/spec/frontend/vue_mr_widget/components/states/mr_widget_unresolved_discussions_spec.js b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_unresolved_discussions_spec.js
index e2d79c61b9b..e2d79c61b9b 100644
--- a/spec/frontend/vue_mr_widget/components/states/mr_widget_unresolved_discussions_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_unresolved_discussions_spec.js
diff --git a/spec/frontend/vue_mr_widget/components/states/mr_widget_wip_spec.js b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_wip_spec.js
index 4998147c6b6..af52901f508 100644
--- a/spec/frontend/vue_mr_widget/components/states/mr_widget_wip_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_wip_spec.js
@@ -26,11 +26,11 @@ describe('Wip', () => {
it('should have props', () => {
const { mr, service } = WorkInProgress.props;
- expect(mr.type instanceof Object).toBeTruthy();
- expect(mr.required).toBeTruthy();
+ expect(mr.type instanceof Object).toBe(true);
+ expect(mr.required).toBe(true);
- expect(service.type instanceof Object).toBeTruthy();
- expect(service.required).toBeTruthy();
+ expect(service.type instanceof Object).toBe(true);
+ expect(service.required).toBe(true);
});
});
@@ -64,7 +64,7 @@ describe('Wip', () => {
await waitForPromises();
- expect(vm.isMakingRequest).toBeTruthy();
+ expect(vm.isMakingRequest).toBe(true);
expect(eventHub.$emit).toHaveBeenCalledWith('UpdateWidgetData', mrObj);
expect(toast).toHaveBeenCalledWith('Marked as ready. Merging is now allowed.');
});
@@ -81,12 +81,10 @@ describe('Wip', () => {
});
it('should have correct elements', () => {
- expect(el.classList.contains('mr-widget-body')).toBeTruthy();
+ expect(el.classList.contains('mr-widget-body')).toBe(true);
expect(el.innerText).toContain(
"Merge blocked: merge request must be marked as ready. It's still marked as draft.",
);
- expect(el.querySelector('button').getAttribute('disabled')).toBeTruthy();
- expect(el.querySelector('button').innerText).toContain('Merge');
expect(el.querySelector('.js-remove-draft').innerText.replace(/\s\s+/g, ' ')).toContain(
'Mark as ready',
);
@@ -97,7 +95,7 @@ describe('Wip', () => {
await nextTick();
- expect(el.querySelector('.js-remove-draft')).toEqual(null);
+ expect(el.querySelector('.js-remove-draft')).toBeNull();
});
});
});
diff --git a/spec/frontend/vue_mr_widget/components/states/new_ready_to_merge_spec.js b/spec/frontend/vue_merge_request_widget/components/states/new_ready_to_merge_spec.js
index 5ec9654a4af..5ec9654a4af 100644
--- a/spec/frontend/vue_mr_widget/components/states/new_ready_to_merge_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/states/new_ready_to_merge_spec.js
diff --git a/spec/frontend/vue_mr_widget/components/terraform/mock_data.js b/spec/frontend/vue_merge_request_widget/components/terraform/mock_data.js
index 8e46af5dfd6..8e46af5dfd6 100644
--- a/spec/frontend/vue_mr_widget/components/terraform/mock_data.js
+++ b/spec/frontend/vue_merge_request_widget/components/terraform/mock_data.js
diff --git a/spec/frontend/vue_mr_widget/components/terraform/mr_widget_terraform_container_spec.js b/spec/frontend/vue_merge_request_widget/components/terraform/mr_widget_terraform_container_spec.js
index 8f20d6a8fc9..8f20d6a8fc9 100644
--- a/spec/frontend/vue_mr_widget/components/terraform/mr_widget_terraform_container_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/terraform/mr_widget_terraform_container_spec.js
diff --git a/spec/frontend/vue_mr_widget/components/terraform/terraform_plan_spec.js b/spec/frontend/vue_merge_request_widget/components/terraform/terraform_plan_spec.js
index 3c9f6c2e165..3c9f6c2e165 100644
--- a/spec/frontend/vue_mr_widget/components/terraform/terraform_plan_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/terraform/terraform_plan_spec.js
diff --git a/spec/frontend/vue_merge_request_widget/components/widget/app_spec.js b/spec/frontend/vue_merge_request_widget/components/widget/app_spec.js
new file mode 100644
index 00000000000..6bb718082a4
--- /dev/null
+++ b/spec/frontend/vue_merge_request_widget/components/widget/app_spec.js
@@ -0,0 +1,19 @@
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import App from '~/vue_merge_request_widget/components/widget/app.vue';
+
+describe('MR Widget App', () => {
+ let wrapper;
+
+ const createComponent = () => {
+ wrapper = shallowMountExtended(App, {
+ propsData: {
+ mr: {},
+ },
+ });
+ };
+
+ it('mounts the component', () => {
+ createComponent();
+ expect(wrapper.findByTestId('mr-widget-app').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
new file mode 100644
index 00000000000..3c08ffdef18
--- /dev/null
+++ b/spec/frontend/vue_merge_request_widget/components/widget/widget_spec.js
@@ -0,0 +1,167 @@
+import { nextTick } from 'vue';
+import * as Sentry from '@sentry/browser';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import StatusIcon from '~/vue_merge_request_widget/components/extensions/status_icon.vue';
+import Widget from '~/vue_merge_request_widget/components/widget/widget.vue';
+
+describe('MR Widget', () => {
+ let wrapper;
+
+ const findStatusIcon = () => wrapper.findComponent(StatusIcon);
+
+ const createComponent = ({ propsData, slots } = {}) => {
+ wrapper = shallowMountExtended(Widget, {
+ propsData: {
+ loadingText: 'Loading widget',
+ widgetName: 'MyWidget',
+ value: {
+ collapsed: null,
+ expanded: null,
+ },
+ ...propsData,
+ },
+ slots,
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('on mount', () => {
+ it('fetches collapsed', async () => {
+ const fetchCollapsedData = jest
+ .fn()
+ .mockReturnValue(Promise.resolve({ headers: {}, status: 200, data: {} }));
+
+ createComponent({ propsData: { fetchCollapsedData } });
+ await waitForPromises();
+ expect(fetchCollapsedData).toHaveBeenCalled();
+ expect(wrapper.vm.error).toBe(null);
+ });
+
+ it('sets the error text when fetch method fails', async () => {
+ const fetchCollapsedData = jest.fn().mockReturnValue(() => Promise.reject());
+ createComponent({ propsData: { fetchCollapsedData } });
+ await waitForPromises();
+ expect(wrapper.vm.error).toBe('Failed to load');
+ });
+
+ it('displays loading icon until request is made and then displays status icon when the request is complete', async () => {
+ const fetchCollapsedData = jest
+ .fn()
+ .mockReturnValue(Promise.resolve({ headers: {}, status: 200, data: {} }));
+
+ createComponent({ propsData: { fetchCollapsedData, statusIconName: 'warning' } });
+
+ // Let on mount be called
+ await nextTick();
+
+ expect(findStatusIcon().props('isLoading')).toBe(true);
+
+ // Wait until `fetchCollapsedData` is resolved
+ await waitForPromises();
+
+ expect(findStatusIcon().props('isLoading')).toBe(false);
+ expect(findStatusIcon().props('iconName')).toBe('warning');
+ });
+
+ it('displays the loading text', async () => {
+ const fetchCollapsedData = jest.fn().mockReturnValue(() => Promise.reject());
+ createComponent({ propsData: { fetchCollapsedData, statusIconName: 'warning' } });
+ expect(wrapper.text()).not.toContain('Loading');
+ await nextTick();
+ expect(wrapper.text()).toContain('Loading');
+ });
+ });
+
+ describe('fetch', () => {
+ it('sets the data.collapsed property after a successfull call - multiPolling: false', async () => {
+ const mockData = { headers: {}, status: 200, data: { vulnerabilities: [] } };
+ createComponent({ propsData: { fetchCollapsedData: async () => mockData } });
+ await waitForPromises();
+ expect(wrapper.emitted('input')[0][0]).toEqual({ collapsed: mockData.data, expanded: null });
+ });
+
+ it('sets the data.collapsed property after a successfull call - multiPolling: true', async () => {
+ const mockData1 = { headers: {}, status: 200, data: { vulnerabilities: [{ vuln: 1 }] } };
+ const mockData2 = { headers: {}, status: 200, data: { vulnerabilities: [{ vuln: 2 }] } };
+
+ createComponent({
+ propsData: {
+ multiPolling: true,
+ fetchCollapsedData: () => [
+ () => Promise.resolve(mockData1),
+ () => Promise.resolve(mockData2),
+ ],
+ },
+ });
+
+ await waitForPromises();
+
+ expect(wrapper.emitted('input')[0][0]).toEqual({
+ collapsed: [mockData1.data, mockData2.data],
+ expanded: null,
+ });
+ });
+
+ it('calls sentry when failed', async () => {
+ const error = new Error('Something went wrong');
+ jest.spyOn(Sentry, 'captureException').mockImplementation();
+ createComponent({
+ propsData: {
+ fetchCollapsedData: async () => Promise.reject(error),
+ },
+ });
+ await waitForPromises();
+ expect(wrapper.emitted('input')).toBeUndefined();
+ expect(Sentry.captureException).toHaveBeenCalledWith(error);
+ });
+ });
+
+ describe('content', () => {
+ it('displays summary property when summary slot is not provided', () => {
+ createComponent({
+ propsData: {
+ summary: 'Hello world',
+ fetchCollapsedData: async () => Promise.resolve(),
+ },
+ });
+
+ expect(wrapper.findByTestId('widget-extension-top-level-summary').text()).toBe('Hello world');
+ });
+
+ it.todo('displays content property when content slot is not provided');
+
+ it('displays the summary slot when provided', () => {
+ createComponent({
+ propsData: {
+ fetchCollapsedData: async () => Promise.resolve(),
+ },
+ slots: {
+ summary: '<b>More complex summary</b>',
+ },
+ });
+
+ expect(wrapper.findByTestId('widget-extension-top-level-summary').text()).toBe(
+ 'More complex summary',
+ );
+ });
+
+ it('displays the content slot when provided', () => {
+ createComponent({
+ propsData: {
+ fetchCollapsedData: async () => Promise.resolve(),
+ },
+ slots: {
+ content: '<b>More complex content</b>',
+ },
+ });
+
+ expect(wrapper.findByTestId('widget-extension-collapsed-section').text()).toBe(
+ 'More complex content',
+ );
+ });
+ });
+});
diff --git a/spec/frontend/vue_mr_widget/deployment/deployment_action_button_spec.js b/spec/frontend/vue_merge_request_widget/deployment/deployment_action_button_spec.js
index 7e7438bcc0f..7e7438bcc0f 100644
--- a/spec/frontend/vue_mr_widget/deployment/deployment_action_button_spec.js
+++ b/spec/frontend/vue_merge_request_widget/deployment/deployment_action_button_spec.js
diff --git a/spec/frontend/vue_mr_widget/deployment/deployment_actions_spec.js b/spec/frontend/vue_merge_request_widget/deployment/deployment_actions_spec.js
index a285d26f404..a285d26f404 100644
--- a/spec/frontend/vue_mr_widget/deployment/deployment_actions_spec.js
+++ b/spec/frontend/vue_merge_request_widget/deployment/deployment_actions_spec.js
diff --git a/spec/frontend/vue_mr_widget/deployment/deployment_list_spec.js b/spec/frontend/vue_merge_request_widget/deployment/deployment_list_spec.js
index 948d7ebab5e..948d7ebab5e 100644
--- a/spec/frontend/vue_mr_widget/deployment/deployment_list_spec.js
+++ b/spec/frontend/vue_merge_request_widget/deployment/deployment_list_spec.js
diff --git a/spec/frontend/vue_mr_widget/deployment/deployment_mock_data.js b/spec/frontend/vue_merge_request_widget/deployment/deployment_mock_data.js
index e98b1160ae4..e98b1160ae4 100644
--- a/spec/frontend/vue_mr_widget/deployment/deployment_mock_data.js
+++ b/spec/frontend/vue_merge_request_widget/deployment/deployment_mock_data.js
diff --git a/spec/frontend/vue_mr_widget/deployment/deployment_spec.js b/spec/frontend/vue_merge_request_widget/deployment/deployment_spec.js
index c27cbd8b781..c27cbd8b781 100644
--- a/spec/frontend/vue_mr_widget/deployment/deployment_spec.js
+++ b/spec/frontend/vue_merge_request_widget/deployment/deployment_spec.js
diff --git a/spec/frontend/vue_mr_widget/deployment/deployment_view_button_spec.js b/spec/frontend/vue_merge_request_widget/deployment/deployment_view_button_spec.js
index eb6e3711e2e..eb6e3711e2e 100644
--- a/spec/frontend/vue_mr_widget/deployment/deployment_view_button_spec.js
+++ b/spec/frontend/vue_merge_request_widget/deployment/deployment_view_button_spec.js
diff --git a/spec/frontend/vue_mr_widget/extensions/test_report/index_spec.js b/spec/frontend/vue_merge_request_widget/extensions/test_report/index_spec.js
index 5c1d3c8e8e8..5c1d3c8e8e8 100644
--- a/spec/frontend/vue_mr_widget/extensions/test_report/index_spec.js
+++ b/spec/frontend/vue_merge_request_widget/extensions/test_report/index_spec.js
diff --git a/spec/frontend/vue_mr_widget/extensions/test_report/utils_spec.js b/spec/frontend/vue_merge_request_widget/extensions/test_report/utils_spec.js
index 69ea70549fe..69ea70549fe 100644
--- a/spec/frontend/vue_mr_widget/extensions/test_report/utils_spec.js
+++ b/spec/frontend/vue_merge_request_widget/extensions/test_report/utils_spec.js
diff --git a/spec/frontend/vue_mr_widget/extentions/accessibility/index_spec.js b/spec/frontend/vue_merge_request_widget/extentions/accessibility/index_spec.js
index a06ad930abe..a06ad930abe 100644
--- a/spec/frontend/vue_mr_widget/extentions/accessibility/index_spec.js
+++ b/spec/frontend/vue_merge_request_widget/extentions/accessibility/index_spec.js
diff --git a/spec/frontend/vue_mr_widget/extentions/accessibility/mock_data.js b/spec/frontend/vue_merge_request_widget/extentions/accessibility/mock_data.js
index 06dc93d101f..06dc93d101f 100644
--- a/spec/frontend/vue_mr_widget/extentions/accessibility/mock_data.js
+++ b/spec/frontend/vue_merge_request_widget/extentions/accessibility/mock_data.js
diff --git a/spec/frontend/vue_mr_widget/extentions/code_quality/index_spec.js b/spec/frontend/vue_merge_request_widget/extentions/code_quality/index_spec.js
index 9a72e4a086b..9a72e4a086b 100644
--- a/spec/frontend/vue_mr_widget/extentions/code_quality/index_spec.js
+++ b/spec/frontend/vue_merge_request_widget/extentions/code_quality/index_spec.js
diff --git a/spec/frontend/vue_mr_widget/extentions/code_quality/mock_data.js b/spec/frontend/vue_merge_request_widget/extentions/code_quality/mock_data.js
index f5ad0ce7377..f5ad0ce7377 100644
--- a/spec/frontend/vue_mr_widget/extentions/code_quality/mock_data.js
+++ b/spec/frontend/vue_merge_request_widget/extentions/code_quality/mock_data.js
diff --git a/spec/frontend/vue_mr_widget/extentions/terraform/index_spec.js b/spec/frontend/vue_merge_request_widget/extentions/terraform/index_spec.js
index d9faa7b2d25..d9faa7b2d25 100644
--- a/spec/frontend/vue_mr_widget/extentions/terraform/index_spec.js
+++ b/spec/frontend/vue_merge_request_widget/extentions/terraform/index_spec.js
diff --git a/spec/frontend/vue_mr_widget/mock_data.js b/spec/frontend/vue_merge_request_widget/mock_data.js
index 20d00a116bb..20d00a116bb 100644
--- a/spec/frontend/vue_mr_widget/mock_data.js
+++ b/spec/frontend/vue_merge_request_widget/mock_data.js
diff --git a/spec/frontend/vue_mr_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..295b9df30b9 100644
--- a/spec/frontend/vue_mr_widget/mr_widget_how_to_merge_modal_spec.js
+++ b/spec/frontend/vue_merge_request_widget/mr_widget_how_to_merge_modal_spec.js
diff --git a/spec/frontend/vue_mr_widget/mr_widget_options_spec.js b/spec/frontend/vue_merge_request_widget/mr_widget_options_spec.js
index b3af5eba364..819841317f9 100644
--- a/spec/frontend/vue_mr_widget/mr_widget_options_spec.js
+++ b/spec/frontend/vue_merge_request_widget/mr_widget_options_spec.js
@@ -20,7 +20,6 @@ import {
import { SUCCESS } from '~/vue_merge_request_widget/components/deployment/constants';
import eventHub from '~/vue_merge_request_widget/event_hub';
import MrWidgetOptions from '~/vue_merge_request_widget/mr_widget_options.vue';
-import { stateKey } from '~/vue_merge_request_widget/stores/state_maps';
import StatusIcon from '~/vue_merge_request_widget/components/extensions/status_icon.vue';
import securityReportMergeRequestDownloadPathsQuery from '~/vue_shared/security_reports/graphql/queries/security_report_merge_request_download_paths.query.graphql';
import { faviconDataUrl, overlayDataUrl } from '../lib/utils/mock_data';
@@ -125,25 +124,13 @@ describe('MrWidgetOptions', () => {
it('should return true when hasCI is true', () => {
wrapper.vm.mr.hasCI = true;
- expect(wrapper.vm.shouldRenderPipelines).toBeTruthy();
+ expect(wrapper.vm.shouldRenderPipelines).toBe(true);
});
it('should return false when hasCI is false', () => {
wrapper.vm.mr.hasCI = false;
- expect(wrapper.vm.shouldRenderPipelines).toBeFalsy();
- });
- });
-
- describe('shouldRenderRelatedLinks', () => {
- it('should return false for the initial data', () => {
- expect(wrapper.vm.shouldRenderRelatedLinks).toBeFalsy();
- });
-
- it('should return true if there is relatedLinks in MR', () => {
- Vue.set(wrapper.vm.mr, 'relatedLinks', {});
-
- expect(wrapper.vm.shouldRenderRelatedLinks).toBeTruthy();
+ expect(wrapper.vm.shouldRenderPipelines).toBe(false);
});
});
@@ -316,7 +303,7 @@ describe('MrWidgetOptions', () => {
expect(wrapper.vm.service.checkStatus).toHaveBeenCalled();
expect(wrapper.vm.mr.setData).toHaveBeenCalled();
expect(wrapper.vm.handleNotification).toHaveBeenCalledWith(mockData);
- expect(isCbExecuted).toBeTruthy();
+ expect(isCbExecuted).toBe(true);
});
});
});
@@ -519,61 +506,6 @@ describe('MrWidgetOptions', () => {
});
});
- describe('rendering relatedLinks', () => {
- beforeEach(() => {
- return createComponent({
- ...mockData,
- issues_links: {
- closing: `
- <a class="close-related-link" href="#">
- Close
- </a>
- `,
- },
- });
- });
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- it('renders if there are relatedLinks', () => {
- expect(wrapper.find('.close-related-link').exists()).toBe(true);
- });
-
- it('does not render if state is nothingToMerge', async () => {
- wrapper.vm.mr.state = stateKey.nothingToMerge;
- await nextTick();
- expect(wrapper.find('.close-related-link').exists()).toBe(false);
- });
- });
-
- describe('rendering source branch removal status', () => {
- it('renders when user cannot remove branch and branch should be removed', async () => {
- wrapper.vm.mr.canRemoveSourceBranch = false;
- wrapper.vm.mr.shouldRemoveSourceBranch = true;
- wrapper.vm.mr.state = 'readyToMerge';
-
- await nextTick();
- const tooltip = wrapper.find('[data-testid="question-o-icon"]');
-
- expect(wrapper.text()).toContain('Deletes the source branch');
- expect(tooltip.attributes('title')).toBe(
- 'A user with write access to the source branch selected this option',
- );
- });
-
- it('does not render in merged state', async () => {
- wrapper.vm.mr.canRemoveSourceBranch = false;
- wrapper.vm.mr.shouldRemoveSourceBranch = true;
- wrapper.vm.mr.state = 'merged';
-
- await nextTick();
- expect(wrapper.text()).toContain('The source branch has been deleted');
- expect(wrapper.text()).not.toContain('Deletes the source branch');
- });
- });
-
describe('rendering deployments', () => {
const changes = [
{
@@ -1062,7 +994,7 @@ describe('MrWidgetOptions', () => {
await createComponent();
- expect(pollRequest).toHaveBeenCalledTimes(6);
+ expect(pollRequest).toHaveBeenCalledTimes(4);
});
});
@@ -1100,14 +1032,14 @@ describe('MrWidgetOptions', () => {
registerExtension(pollingErrorExtension);
await createComponent();
- expect(pollRequest).toHaveBeenCalledTimes(6);
+ expect(pollRequest).toHaveBeenCalledTimes(4);
});
it('captures sentry error and displays error when poll has failed', async () => {
registerExtension(pollingErrorExtension);
await createComponent();
- expect(Sentry.captureException).toHaveBeenCalledTimes(5);
+ expect(Sentry.captureException).toHaveBeenCalled();
expect(Sentry.captureException).toHaveBeenCalledWith(new Error('Fetch error'));
expect(wrapper.findComponent(StatusIcon).props('iconName')).toBe('failed');
});
@@ -1126,7 +1058,7 @@ describe('MrWidgetOptions', () => {
expect(
wrapper.find('[data-testid="widget-extension"] [data-testid="toggle-button"]').exists(),
).toBe(false);
- expect(Sentry.captureException).toHaveBeenCalledTimes(5);
+ expect(Sentry.captureException).toHaveBeenCalled();
expect(Sentry.captureException).toHaveBeenCalledWith(new Error('Fetch error'));
expect(wrapper.findComponent(StatusIcon).props('iconName')).toBe('failed');
});
diff --git a/spec/frontend/vue_mr_widget/stores/artifacts_list/actions_spec.js b/spec/frontend/vue_merge_request_widget/stores/artifacts_list/actions_spec.js
index 22562bb4ddb..22562bb4ddb 100644
--- a/spec/frontend/vue_mr_widget/stores/artifacts_list/actions_spec.js
+++ b/spec/frontend/vue_merge_request_widget/stores/artifacts_list/actions_spec.js
diff --git a/spec/frontend/vue_mr_widget/stores/artifacts_list/getters_spec.js b/spec/frontend/vue_merge_request_widget/stores/artifacts_list/getters_spec.js
index dc90fef63c6..dc90fef63c6 100644
--- a/spec/frontend/vue_mr_widget/stores/artifacts_list/getters_spec.js
+++ b/spec/frontend/vue_merge_request_widget/stores/artifacts_list/getters_spec.js
diff --git a/spec/frontend/vue_mr_widget/stores/artifacts_list/mutations_spec.js b/spec/frontend/vue_merge_request_widget/stores/artifacts_list/mutations_spec.js
index a4e6788c7f6..a4e6788c7f6 100644
--- a/spec/frontend/vue_mr_widget/stores/artifacts_list/mutations_spec.js
+++ b/spec/frontend/vue_merge_request_widget/stores/artifacts_list/mutations_spec.js
diff --git a/spec/frontend/vue_mr_widget/stores/get_state_key_spec.js b/spec/frontend/vue_merge_request_widget/stores/get_state_key_spec.js
index fc760f5c5be..0246a8d4b0f 100644
--- a/spec/frontend/vue_mr_widget/stores/get_state_key_spec.js
+++ b/spec/frontend/vue_merge_request_widget/stores/get_state_key_spec.js
@@ -25,10 +25,6 @@ describe('getStateKey', () => {
expect(bound()).toEqual('readyToMerge');
- context.canMerge = false;
-
- expect(bound()).toEqual('notAllowedToMerge');
-
context.autoMergeEnabled = true;
context.hasMergeableDiscussionsState = true;
@@ -105,22 +101,4 @@ describe('getStateKey', () => {
expect(bound()).toEqual('rebase');
});
-
- it.each`
- canMerge | isSHAMismatch | stateKey
- ${true} | ${true} | ${'shaMismatch'}
- ${false} | ${true} | ${'notAllowedToMerge'}
- ${false} | ${false} | ${'notAllowedToMerge'}
- `(
- 'returns $stateKey when canMerge is $canMerge and isSHAMismatch is $isSHAMismatch',
- ({ canMerge, isSHAMismatch, stateKey }) => {
- const bound = getStateKey.bind({
- canMerge,
- isSHAMismatch,
- commitsCount: 2,
- });
-
- expect(bound()).toEqual(stateKey);
- },
- );
});
diff --git a/spec/frontend/vue_mr_widget/stores/mr_widget_store_spec.js b/spec/frontend/vue_merge_request_widget/stores/mr_widget_store_spec.js
index 3cdb4265ef0..3cdb4265ef0 100644
--- a/spec/frontend/vue_mr_widget/stores/mr_widget_store_spec.js
+++ b/spec/frontend/vue_merge_request_widget/stores/mr_widget_store_spec.js
diff --git a/spec/frontend/vue_mr_widget/test_extensions.js b/spec/frontend/vue_merge_request_widget/test_extensions.js
index 1977f550577..1977f550577 100644
--- a/spec/frontend/vue_mr_widget/test_extensions.js
+++ b/spec/frontend/vue_merge_request_widget/test_extensions.js
diff --git a/spec/frontend/vue_mr_widget/components/states/__snapshots__/mr_widget_auto_merge_enabled_spec.js.snap b/spec/frontend/vue_mr_widget/components/states/__snapshots__/mr_widget_auto_merge_enabled_spec.js.snap
deleted file mode 100644
index 56a0218b374..00000000000
--- a/spec/frontend/vue_mr_widget/components/states/__snapshots__/mr_widget_auto_merge_enabled_spec.js.snap
+++ /dev/null
@@ -1,145 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`MRWidgetAutoMergeEnabled when graphql is disabled template should have correct elements 1`] = `
-<div
- class="mr-widget-body media"
->
- <gl-icon-stub
- class="gl-text-blue-500 gl-mr-3 gl-mt-1"
- name="status_scheduled"
- size="24"
- />
-
- <div
- class="media-body"
- >
- <h4
- class="gl-display-flex"
- >
- <span
- class="gl-mr-3"
- >
- <gl-sprintf-stub
- data-testid="statusText"
- message="Set by %{merge_author} to be merged automatically when the pipeline succeeds"
- />
- </span>
-
- <gl-button-stub
- buttontextclasses=""
- category="primary"
- class="js-cancel-auto-merge"
- data-qa-selector="cancel_auto_merge_button"
- data-testid="cancelAutomaticMergeButton"
- icon=""
- size="small"
- variant="default"
- >
-
- Cancel auto-merge
-
- </gl-button-stub>
- </h4>
-
- <section
- class="mr-info-list"
- >
- <p
- class="gl-display-flex"
- >
- <span
- class="gl-mr-3"
- >
- Does not delete the source branch
- </span>
-
- <gl-button-stub
- buttontextclasses=""
- category="primary"
- class="js-remove-source-branch"
- data-testid="removeSourceBranchButton"
- icon=""
- size="small"
- variant="default"
- >
-
- Delete source branch
-
- </gl-button-stub>
- </p>
- </section>
- </div>
-</div>
-`;
-
-exports[`MRWidgetAutoMergeEnabled when graphql is enabled template should have correct elements 1`] = `
-<div
- class="mr-widget-body media"
->
- <gl-icon-stub
- class="gl-text-blue-500 gl-mr-3 gl-mt-1"
- name="status_scheduled"
- size="24"
- />
-
- <div
- class="media-body"
- >
- <h4
- class="gl-display-flex"
- >
- <span
- class="gl-mr-3"
- >
- <gl-sprintf-stub
- data-testid="statusText"
- message="Set by %{merge_author} to be merged automatically when the pipeline succeeds"
- />
- </span>
-
- <gl-button-stub
- buttontextclasses=""
- category="primary"
- class="js-cancel-auto-merge"
- data-qa-selector="cancel_auto_merge_button"
- data-testid="cancelAutomaticMergeButton"
- icon=""
- size="small"
- variant="default"
- >
-
- Cancel auto-merge
-
- </gl-button-stub>
- </h4>
-
- <section
- class="mr-info-list"
- >
- <p
- class="gl-display-flex"
- >
- <span
- class="gl-mr-3"
- >
- Does not delete the source branch
- </span>
-
- <gl-button-stub
- buttontextclasses=""
- category="primary"
- class="js-remove-source-branch"
- data-testid="removeSourceBranchButton"
- icon=""
- size="small"
- variant="default"
- >
-
- Delete source branch
-
- </gl-button-stub>
- </p>
- </section>
- </div>
-</div>
-`;
diff --git a/spec/frontend/vue_shared/alert_details/alert_details_spec.js b/spec/frontend/vue_shared/alert_details/alert_details_spec.js
index ce51af31a70..59e21b2ff40 100644
--- a/spec/frontend/vue_shared/alert_details/alert_details_spec.js
+++ b/spec/frontend/vue_shared/alert_details/alert_details_spec.js
@@ -285,14 +285,14 @@ describe('AlertDetails', () => {
});
it('displays a loading state when loading', () => {
- expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
+ expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
});
});
describe('error state', () => {
it('displays a error state correctly', () => {
mountComponent({ data: { errored: true } });
- expect(wrapper.find(GlAlert).exists()).toBe(true);
+ expect(wrapper.findComponent(GlAlert).exists()).toBe(true);
});
it('renders html-errors correctly', () => {
@@ -304,7 +304,7 @@ describe('AlertDetails', () => {
it('does not display an error when dismissed', () => {
mountComponent({ data: { errored: true, isErrorDismissed: true } });
- expect(wrapper.find(GlAlert).exists()).toBe(false);
+ expect(wrapper.findComponent(GlAlert).exists()).toBe(false);
});
});
diff --git a/spec/frontend/vue_shared/alert_details/alert_metrics_spec.js b/spec/frontend/vue_shared/alert_details/alert_metrics_spec.js
index 1216681038f..cf04c1eb24a 100644
--- a/spec/frontend/vue_shared/alert_details/alert_metrics_spec.js
+++ b/spec/frontend/vue_shared/alert_details/alert_metrics_spec.js
@@ -28,8 +28,8 @@ describe('Alert Metrics', () => {
});
}
- const findChart = () => wrapper.find(MetricEmbed);
- const findEmptyState = () => wrapper.find({ ref: 'emptyState' });
+ const findChart = () => wrapper.findComponent(MetricEmbed);
+ const findEmptyState = () => wrapper.findComponent({ ref: 'emptyState' });
afterEach(() => {
if (wrapper) {
diff --git a/spec/frontend/vue_shared/alert_details/alert_status_spec.js b/spec/frontend/vue_shared/alert_details/alert_status_spec.js
index ba3b0335a8e..2a37ff2b784 100644
--- a/spec/frontend/vue_shared/alert_details/alert_status_spec.js
+++ b/spec/frontend/vue_shared/alert_details/alert_status_spec.js
@@ -13,7 +13,7 @@ describe('AlertManagementStatus', () => {
let wrapper;
const findStatusDropdown = () => wrapper.findComponent(GlDropdown);
const findFirstStatusOption = () => findStatusDropdown().findComponent(GlDropdownItem);
- const findAllStatusOptions = () => findStatusDropdown().findAll(GlDropdownItem);
+ const findAllStatusOptions = () => findStatusDropdown().findAllComponents(GlDropdownItem);
const findStatusDropdownHeader = () => wrapper.findByTestId('dropdown-header');
const selectFirstStatusOption = () => {
diff --git a/spec/frontend/vue_shared/alert_details/sidebar/alert_sidebar_assignees_spec.js b/spec/frontend/vue_shared/alert_details/sidebar/alert_sidebar_assignees_spec.js
index 29569734621..5a0ee5a59ba 100644
--- a/spec/frontend/vue_shared/alert_details/sidebar/alert_sidebar_assignees_spec.js
+++ b/spec/frontend/vue_shared/alert_details/sidebar/alert_sidebar_assignees_spec.js
@@ -128,7 +128,7 @@ describe('Alert Details Sidebar Assignees', () => {
wrapper.setData({ isDropdownSearching: false });
await nextTick();
- wrapper.find(SidebarAssignee).vm.$emit('update-alert-assignees', 'root');
+ wrapper.findComponent(SidebarAssignee).vm.$emit('update-alert-assignees', 'root');
expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({
mutation: AlertSetAssignees,
@@ -156,7 +156,7 @@ describe('Alert Details Sidebar Assignees', () => {
jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue(errorMutationResult);
await nextTick();
- const SideBarAssigneeItem = wrapper.findAll(SidebarAssignee).at(0);
+ const SideBarAssigneeItem = wrapper.findAllComponents(SidebarAssignee).at(0);
await SideBarAssigneeItem.vm.$emit('update-alert-assignees');
expect(wrapper.emitted('alert-error')).toBeDefined();
});
diff --git a/spec/frontend/vue_shared/alert_details/sidebar/alert_sidebar_spec.js b/spec/frontend/vue_shared/alert_details/sidebar/alert_sidebar_spec.js
index ef75e038bff..3b38349622f 100644
--- a/spec/frontend/vue_shared/alert_details/sidebar/alert_sidebar_spec.js
+++ b/spec/frontend/vue_shared/alert_details/sidebar/alert_sidebar_spec.js
@@ -65,7 +65,7 @@ describe('Alert Details Sidebar', () => {
mountMethod: mount,
alert: mockAlert,
});
- expect(wrapper.find(SidebarAssignees).exists()).toBe(true);
+ expect(wrapper.findComponent(SidebarAssignees).exists()).toBe(true);
});
it('should render side bar status dropdown', () => {
@@ -73,7 +73,7 @@ describe('Alert Details Sidebar', () => {
mountMethod: mount,
alert: mockAlert,
});
- expect(wrapper.find(SidebarStatus).exists()).toBe(true);
+ expect(wrapper.findComponent(SidebarStatus).exists()).toBe(true);
});
});
});
diff --git a/spec/frontend/vue_shared/alert_details/system_notes/alert_management_system_note_spec.js b/spec/frontend/vue_shared/alert_details/system_notes/alert_management_system_note_spec.js
index a5a9fb55737..6a750bb99c0 100644
--- a/spec/frontend/vue_shared/alert_details/system_notes/alert_management_system_note_spec.js
+++ b/spec/frontend/vue_shared/alert_details/system_notes/alert_management_system_note_spec.js
@@ -31,7 +31,7 @@ describe('Alert Details System Note', () => {
it('renders the correct system note', () => {
const noteId = wrapper.find('.note-wrapper').attributes('id');
- const iconName = wrapper.find(GlIcon).attributes('name');
+ const iconName = wrapper.findComponent(GlIcon).attributes('name');
expect(noteId).toBe('note_1628');
expect(iconName).toBe(mockAlert.notes.nodes[0].systemNoteIconName);
diff --git a/spec/frontend/vue_shared/components/actions_button_spec.js b/spec/frontend/vue_shared/components/actions_button_spec.js
index e5b7b693cb5..07c53c04723 100644
--- a/spec/frontend/vue_shared/components/actions_button_spec.js
+++ b/spec/frontend/vue_shared/components/actions_button_spec.js
@@ -45,9 +45,9 @@ describe('Actions button component', () => {
return directiveBinding.value;
};
- const findButton = () => wrapper.find(GlButton);
+ const findButton = () => wrapper.findComponent(GlButton);
const findButtonTooltip = () => getTooltip(findButton());
- const findDropdown = () => wrapper.find(GlDropdown);
+ const findDropdown = () => wrapper.findComponent(GlDropdown);
const findDropdownTooltip = () => getTooltip(findDropdown());
const parseDropdownItems = () =>
findDropdown()
diff --git a/spec/frontend/vue_shared/components/alert_details_table_spec.js b/spec/frontend/vue_shared/components/alert_details_table_spec.js
index b9a8a5bee97..8a9ee4699bd 100644
--- a/spec/frontend/vue_shared/components/alert_details_table_spec.js
+++ b/spec/frontend/vue_shared/components/alert_details_table_spec.js
@@ -74,7 +74,7 @@ describe('AlertDetails', () => {
});
it('displays a loading state when loading', () => {
- expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
+ expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
});
});
@@ -130,7 +130,7 @@ describe('AlertDetails', () => {
environmentData = { name: null, path: null };
mountComponent();
- expect(findTableFieldValueByKey('Environment').text()).toBeFalsy();
+ expect(findTableFieldValueByKey('Environment').text()).toBe('');
});
});
diff --git a/spec/frontend/vue_shared/components/blob_viewers/rich_viewer_spec.js b/spec/frontend/vue_shared/components/blob_viewers/rich_viewer_spec.js
index d14f3e5559f..ce7fd40937f 100644
--- a/spec/frontend/vue_shared/components/blob_viewers/rich_viewer_spec.js
+++ b/spec/frontend/vue_shared/components/blob_viewers/rich_viewer_spec.js
@@ -43,6 +43,6 @@ describe('Blob Rich Viewer component', () => {
});
it('is using Markdown View Field', () => {
- expect(wrapper.find(MarkdownFieldView).exists()).toBe(true);
+ expect(wrapper.findComponent(MarkdownFieldView).exists()).toBe(true);
});
});
diff --git a/spec/frontend/vue_shared/components/changed_file_icon_spec.js b/spec/frontend/vue_shared/components/changed_file_icon_spec.js
index 6b9658a6d18..ea708b6f3fe 100644
--- a/spec/frontend/vue_shared/components/changed_file_icon_spec.js
+++ b/spec/frontend/vue_shared/components/changed_file_icon_spec.js
@@ -25,7 +25,7 @@ describe('Changed file icon', () => {
wrapper.destroy();
});
- const findIcon = () => wrapper.find(GlIcon);
+ const findIcon = () => wrapper.findComponent(GlIcon);
const findIconName = () => findIcon().props('name');
const findIconClasses = () => findIcon().classes();
const findTooltipText = () => wrapper.attributes('title');
@@ -51,7 +51,7 @@ describe('Changed file icon', () => {
showTooltip: false,
});
- expect(findTooltipText()).toBeFalsy();
+ expect(findTooltipText()).toBeUndefined();
});
describe.each`
@@ -87,7 +87,7 @@ describe('Changed file icon', () => {
});
it('does not have tooltip text', () => {
- expect(findTooltipText()).toBeFalsy();
+ expect(findTooltipText()).toBeUndefined();
});
});
diff --git a/spec/frontend/vue_shared/components/ci_icon_spec.js b/spec/frontend/vue_shared/components/ci_icon_spec.js
index 1b502f9587c..2064bee9673 100644
--- a/spec/frontend/vue_shared/components/ci_icon_spec.js
+++ b/spec/frontend/vue_shared/components/ci_icon_spec.js
@@ -22,7 +22,7 @@ describe('CI Icon component', () => {
});
expect(wrapper.find('span').exists()).toBe(true);
- expect(wrapper.find(GlIcon).exists()).toBe(true);
+ expect(wrapper.findComponent(GlIcon).exists()).toBe(true);
});
describe('active icons', () => {
diff --git a/spec/frontend/vue_shared/components/clipboard_button_spec.js b/spec/frontend/vue_shared/components/clipboard_button_spec.js
index fca5e664a96..b18b00e70bb 100644
--- a/spec/frontend/vue_shared/components/clipboard_button_spec.js
+++ b/spec/frontend/vue_shared/components/clipboard_button_spec.js
@@ -21,7 +21,7 @@ describe('clipboard button', () => {
});
};
- const findButton = () => wrapper.find(GlButton);
+ const findButton = () => wrapper.findComponent(GlButton);
const expectConfirmationTooltip = async ({ event, message }) => {
const title = 'Copy this value';
diff --git a/spec/frontend/vue_shared/components/clone_dropdown_spec.js b/spec/frontend/vue_shared/components/clone_dropdown_spec.js
index eefd1838988..31c08260dd0 100644
--- a/spec/frontend/vue_shared/components/clone_dropdown_spec.js
+++ b/spec/frontend/vue_shared/components/clone_dropdown_spec.js
@@ -38,9 +38,9 @@ describe('Clone Dropdown Button', () => {
${'HTTP'} | ${1} | ${httpLink}
`('renders correct link and a copy-button for $name', ({ index, value }) => {
createComponent();
- const group = wrapper.findAll(GlFormInputGroup).at(index);
+ const group = wrapper.findAllComponents(GlFormInputGroup).at(index);
expect(group.props('value')).toBe(value);
- expect(group.find(GlFormInputGroup).exists()).toBe(true);
+ expect(group.findComponent(GlFormInputGroup).exists()).toBe(true);
});
it.each`
@@ -50,8 +50,8 @@ describe('Clone Dropdown Button', () => {
`('does not fail if only $name is set', ({ name, value }) => {
createComponent({ [name]: value });
- expect(wrapper.find(GlFormInputGroup).props('value')).toBe(value);
- expect(wrapper.findAll(GlDropdownSectionHeader).length).toBe(1);
+ expect(wrapper.findComponent(GlFormInputGroup).props('value')).toBe(value);
+ expect(wrapper.findAllComponents(GlDropdownSectionHeader).length).toBe(1);
});
});
@@ -63,12 +63,12 @@ describe('Clone Dropdown Button', () => {
`('allows null values for the props', ({ name, value }) => {
createComponent({ ...defaultPropsData, [name]: value });
- expect(wrapper.findAll(GlDropdownSectionHeader).length).toBe(1);
+ expect(wrapper.findAllComponents(GlDropdownSectionHeader).length).toBe(1);
});
it('correctly calculates httpLabel for HTTPS protocol', () => {
createComponent({ httpLink: httpsLink });
- expect(wrapper.find(GlDropdownSectionHeader).text()).toContain('HTTPS');
+ expect(wrapper.findComponent(GlDropdownSectionHeader).text()).toContain('HTTPS');
});
});
});
diff --git a/spec/frontend/vue_shared/components/color_picker/color_picker_spec.js b/spec/frontend/vue_shared/components/color_picker/color_picker_spec.js
index 8cbe0630426..060048c4bbd 100644
--- a/spec/frontend/vue_shared/components/color_picker/color_picker_spec.js
+++ b/spec/frontend/vue_shared/components/color_picker/color_picker_spec.js
@@ -16,14 +16,14 @@ describe('ColorPicker', () => {
const setColor = '#000000';
const invalidText = 'Please enter a valid hex (#RRGGBB or #RGB) color value';
- const findGlFormGroup = () => wrapper.find(GlFormGroup);
+ const findGlFormGroup = () => wrapper.findComponent(GlFormGroup);
const colorPreview = () => wrapper.find('[data-testid="color-preview"]');
- const colorPicker = () => wrapper.find(GlFormInput);
+ const colorPicker = () => wrapper.findComponent(GlFormInput);
const colorInput = () => wrapper.find('input[type="color"]');
- const colorTextInput = () => wrapper.find(GlFormInputGroup).find('input[type="text"]');
+ const colorTextInput = () => wrapper.findComponent(GlFormInputGroup).find('input[type="text"]');
const invalidFeedback = () => wrapper.find('.invalid-feedback');
- const description = () => wrapper.find(GlFormGroup).attributes('description');
- const presetColors = () => wrapper.findAll(GlLink);
+ const description = () => wrapper.findComponent(GlFormGroup).attributes('description');
+ const presetColors = () => wrapper.findAllComponents(GlLink);
beforeEach(() => {
gon.suggested_label_colors = {
diff --git a/spec/frontend/vue_shared/components/commit_spec.js b/spec/frontend/vue_shared/components/commit_spec.js
index d91853e7b79..1893e127f6f 100644
--- a/spec/frontend/vue_shared/components/commit_spec.js
+++ b/spec/frontend/vue_shared/components/commit_spec.js
@@ -9,11 +9,11 @@ describe('Commit component', () => {
let wrapper;
const findIcon = (name) => {
- const icons = wrapper.findAll(GlIcon).filter((c) => c.attributes('name') === name);
+ const icons = wrapper.findAllComponents(GlIcon).filter((c) => c.attributes('name') === name);
return icons.length ? icons.at(0) : icons;
};
- const findUserAvatar = () => wrapper.find(UserAvatarLink);
+ const findUserAvatar = () => wrapper.findComponent(UserAvatarLink);
const findRefName = () => wrapper.findByTestId('ref-name');
const createComponent = (propsData) => {
@@ -47,7 +47,7 @@ describe('Commit component', () => {
},
});
- expect(wrapper.find('.icon-container').find(GlIcon).exists()).toBe(true);
+ expect(wrapper.find('.icon-container').findComponent(GlIcon).exists()).toBe(true);
});
describe('Given all the props', () => {
diff --git a/spec/frontend/vue_shared/components/confirm_modal_spec.js b/spec/frontend/vue_shared/components/confirm_modal_spec.js
index 3ca1c943398..c1e682a1aae 100644
--- a/spec/frontend/vue_shared/components/confirm_modal_spec.js
+++ b/spec/frontend/vue_shared/components/confirm_modal_spec.js
@@ -51,13 +51,13 @@ describe('vue_shared/components/confirm_modal', () => {
wrapper.destroy();
});
- const findModal = () => wrapper.find(GlModalStub);
+ const findModal = () => wrapper.findComponent(GlModalStub);
const findForm = () => wrapper.find('form');
const findFormData = () =>
findForm()
.findAll('input')
.wrappers.map((x) => ({ name: x.attributes('name'), value: x.attributes('value') }));
- const findDomElementListener = () => wrapper.find(DomElementListener);
+ const findDomElementListener = () => wrapper.findComponent(DomElementListener);
const triggerOpenWithEventHub = (modalData) => {
eventHub.$emit(EVENT_OPEN_CONFIRM_MODAL, modalData);
};
@@ -104,7 +104,7 @@ describe('vue_shared/components/confirm_modal', () => {
});
it('renders GlModal with data', () => {
- expect(findModal().exists()).toBeTruthy();
+ expect(findModal().exists()).toBe(true);
expect(findModal().attributes()).toEqual(
expect.objectContaining({
oktitle: MOCK_MODAL_DATA.modalAttributes.okTitle,
diff --git a/spec/frontend/vue_shared/components/dismissible_alert_spec.js b/spec/frontend/vue_shared/components/dismissible_alert_spec.js
index 879d4aba441..8b1189f25d5 100644
--- a/spec/frontend/vue_shared/components/dismissible_alert_spec.js
+++ b/spec/frontend/vue_shared/components/dismissible_alert_spec.js
@@ -20,7 +20,7 @@ describe('vue_shared/components/dismissible_alert', () => {
wrapper.destroy();
});
- const findAlert = () => wrapper.find(GlAlert);
+ const findAlert = () => wrapper.findComponent(GlAlert);
describe('default', () => {
beforeEach(() => {
@@ -45,7 +45,7 @@ describe('vue_shared/components/dismissible_alert', () => {
});
it('emmits alertDismissed', () => {
- expect(wrapper.emitted('alertDismissed')).toBeTruthy();
+ expect(wrapper.emitted()).toHaveProperty('alertDismissed');
});
});
});
diff --git a/spec/frontend/vue_shared/components/dismissible_container_spec.js b/spec/frontend/vue_shared/components/dismissible_container_spec.js
index b8aeea38e77..f7030f38709 100644
--- a/spec/frontend/vue_shared/components/dismissible_container_spec.js
+++ b/spec/frontend/vue_shared/components/dismissible_container_spec.js
@@ -33,7 +33,7 @@ describe('DismissibleContainer', () => {
button.trigger('click');
- expect(wrapper.emitted().dismiss).toBeTruthy();
+ expect(wrapper.emitted().dismiss).toEqual(expect.any(Array));
});
});
diff --git a/spec/frontend/vue_shared/components/dropdown/dropdown_button_spec.js b/spec/frontend/vue_shared/components/dropdown/dropdown_button_spec.js
index 08e5d828b8f..e34ed31b4bf 100644
--- a/spec/frontend/vue_shared/components/dropdown/dropdown_button_spec.js
+++ b/spec/frontend/vue_shared/components/dropdown/dropdown_button_spec.js
@@ -1,80 +1,71 @@
-import Vue from 'vue';
+import { mount } from '@vue/test-utils';
+import DropdownButton from '~/vue_shared/components/dropdown/dropdown_button.vue';
-import { mountComponentWithSlots } from 'helpers/vue_mount_component_helper';
-import dropdownButtonComponent from '~/vue_shared/components/dropdown/dropdown_button.vue';
+describe('DropdownButton component', () => {
+ let wrapper;
-const defaultLabel = 'Select';
-const customLabel = 'Select project';
+ const defaultLabel = 'Select';
+ const customLabel = 'Select project';
-const createComponent = (props, slots = {}) => {
- const Component = Vue.extend(dropdownButtonComponent);
-
- return mountComponentWithSlots(Component, { props, slots });
-};
-
-describe('DropdownButtonComponent', () => {
- let vm;
-
- beforeEach(() => {
- vm = createComponent();
- });
+ const createComponent = (props, slots = {}) => {
+ wrapper = mount(DropdownButton, { propsData: props, slots });
+ };
afterEach(() => {
- vm.$destroy();
+ wrapper.destroy();
});
describe('computed', () => {
describe('dropdownToggleText', () => {
it('returns default toggle text', () => {
- expect(vm.toggleText).toBe(defaultLabel);
+ createComponent();
+
+ expect(wrapper.vm.toggleText).toBe(defaultLabel);
});
it('returns custom toggle text when provided via props', () => {
- const vmEmptyLabels = createComponent({ toggleText: customLabel });
+ createComponent({ toggleText: customLabel });
- expect(vmEmptyLabels.toggleText).toBe(customLabel);
- vmEmptyLabels.$destroy();
+ expect(wrapper.vm.toggleText).toBe(customLabel);
});
});
});
describe('template', () => {
it('renders component container element of type `button`', () => {
- expect(vm.$el.nodeName).toBe('BUTTON');
+ createComponent();
+
+ expect(wrapper.element.nodeName).toBe('BUTTON');
});
it('renders component container element with required data attributes', () => {
- expect(vm.$el.dataset.abilityName).toBe(vm.abilityName);
- expect(vm.$el.dataset.fieldName).toBe(vm.fieldName);
- expect(vm.$el.dataset.issueUpdate).toBe(vm.updatePath);
- expect(vm.$el.dataset.labels).toBe(vm.labelsPath);
- expect(vm.$el.dataset.namespacePath).toBe(vm.namespace);
- expect(vm.$el.dataset.showAny).not.toBeDefined();
+ createComponent();
+
+ expect(wrapper.element.dataset.abilityName).toBe(wrapper.vm.abilityName);
+ expect(wrapper.element.dataset.fieldName).toBe(wrapper.vm.fieldName);
+ expect(wrapper.element.dataset.issueUpdate).toBe(wrapper.vm.updatePath);
+ expect(wrapper.element.dataset.labels).toBe(wrapper.vm.labelsPath);
+ expect(wrapper.element.dataset.namespacePath).toBe(wrapper.vm.namespace);
+ expect(wrapper.element.dataset.showAny).toBeUndefined();
});
it('renders dropdown toggle text element', () => {
- const dropdownToggleTextEl = vm.$el.querySelector('.dropdown-toggle-text');
+ createComponent();
- expect(dropdownToggleTextEl).not.toBeNull();
- expect(dropdownToggleTextEl.innerText.trim()).toBe(defaultLabel);
+ expect(wrapper.find('.dropdown-toggle-text').text()).toBe(defaultLabel);
});
it('renders dropdown button icon', () => {
- const dropdownIconEl = vm.$el.querySelector('[data-testid="chevron-down-icon"]');
+ createComponent();
- expect(dropdownIconEl).not.toBeNull();
+ expect(wrapper.find('[data-testid="chevron-down-icon"]').exists()).toBe(true);
});
it('renders slot, if default slot exists', () => {
- vm = createComponent(
- {},
- {
- default: ['Lorem Ipsum Dolar'],
- },
- );
-
- expect(vm.$el.querySelector('.dropdown-toggle-text')).toBeNull();
- expect(vm.$el).toHaveText('Lorem Ipsum Dolar');
+ createComponent({}, { default: ['Lorem Ipsum Dolar'] });
+
+ expect(wrapper.find('.dropdown-toggle-text').exists()).toBe(false);
+ expect(wrapper.text()).toBe('Lorem Ipsum Dolar');
});
});
});
diff --git a/spec/frontend/vue_shared/components/dropdown/dropdown_widget_spec.js b/spec/frontend/vue_shared/components/dropdown/dropdown_widget_spec.js
index 084d0559665..dd3e55c82bb 100644
--- a/spec/frontend/vue_shared/components/dropdown/dropdown_widget_spec.js
+++ b/spec/frontend/vue_shared/components/dropdown/dropdown_widget_spec.js
@@ -8,7 +8,7 @@ describe('DropdownWidget component', () => {
let wrapper;
const findDropdown = () => wrapper.findComponent(GlDropdown);
- const findDropdownItems = () => wrapper.findAll(GlDropdownItem);
+ const findDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
const findSearch = () => wrapper.findComponent(GlSearchBoxByType);
const createComponent = ({ props = {} } = {}) => {
diff --git a/spec/frontend/vue_shared/components/expand_button_spec.js b/spec/frontend/vue_shared/components/expand_button_spec.js
index 87d6ed6b21f..170c947e520 100644
--- a/spec/frontend/vue_shared/components/expand_button_spec.js
+++ b/spec/frontend/vue_shared/components/expand_button_spec.js
@@ -37,11 +37,11 @@ describe('Expand button', () => {
});
it('renders no text when short text is not provided', () => {
- expect(wrapper.find(ExpandButton).text()).toBe('');
+ expect(wrapper.findComponent(ExpandButton).text()).toBe('');
});
it('does not render expanded text', () => {
- expect(wrapper.find(ExpandButton).text().trim()).not.toBe(text.short);
+ expect(wrapper.findComponent(ExpandButton).text().trim()).not.toBe(text.short);
});
describe('when short text is provided', () => {
@@ -55,13 +55,13 @@ describe('Expand button', () => {
});
it('renders short text', () => {
- expect(wrapper.find(ExpandButton).text().trim()).toBe(text.short);
+ expect(wrapper.findComponent(ExpandButton).text().trim()).toBe(text.short);
});
it('renders button before text', () => {
expect(expanderPrependEl().isVisible()).toBe(true);
expect(expanderAppendEl().isVisible()).toBe(false);
- expect(wrapper.find(ExpandButton).element).toMatchSnapshot();
+ expect(wrapper.findComponent(ExpandButton).element).toMatchSnapshot();
});
});
@@ -81,7 +81,7 @@ describe('Expand button', () => {
});
it('renders the expanded text', () => {
- expect(wrapper.find(ExpandButton).text()).toContain(text.expanded);
+ expect(wrapper.findComponent(ExpandButton).text()).toContain(text.expanded);
});
describe('when short text is provided', () => {
@@ -98,13 +98,13 @@ describe('Expand button', () => {
});
it('only renders expanded text', () => {
- expect(wrapper.find(ExpandButton).text().trim()).toBe(text.expanded);
+ expect(wrapper.findComponent(ExpandButton).text().trim()).toBe(text.expanded);
});
it('renders button after text', () => {
expect(expanderPrependEl().isVisible()).toBe(false);
expect(expanderAppendEl().isVisible()).toBe(true);
- expect(wrapper.find(ExpandButton).element).toMatchSnapshot();
+ expect(wrapper.findComponent(ExpandButton).element).toMatchSnapshot();
});
});
});
@@ -124,11 +124,11 @@ describe('Expand button', () => {
});
it('clicking hides expanded text', async () => {
- expect(wrapper.find(ExpandButton).text().trim()).toBe(text.expanded);
+ expect(wrapper.findComponent(ExpandButton).text().trim()).toBe(text.expanded);
expanderAppendEl().trigger('click');
await nextTick();
- expect(wrapper.find(ExpandButton).text().trim()).not.toBe(text.expanded);
+ expect(wrapper.findComponent(ExpandButton).text().trim()).not.toBe(text.expanded);
});
describe('when short text is provided', () => {
@@ -145,11 +145,11 @@ describe('Expand button', () => {
});
it('clicking reveals short text', async () => {
- expect(wrapper.find(ExpandButton).text().trim()).toBe(text.expanded);
+ expect(wrapper.findComponent(ExpandButton).text().trim()).toBe(text.expanded);
expanderAppendEl().trigger('click');
await nextTick();
- expect(wrapper.find(ExpandButton).text().trim()).toBe(text.short);
+ expect(wrapper.findComponent(ExpandButton).text().trim()).toBe(text.short);
});
});
});
diff --git a/spec/frontend/vue_shared/components/file_icon_spec.js b/spec/frontend/vue_shared/components/file_icon_spec.js
index b0e623520a8..3f4bfc86b67 100644
--- a/spec/frontend/vue_shared/components/file_icon_spec.js
+++ b/spec/frontend/vue_shared/components/file_icon_spec.js
@@ -6,7 +6,7 @@ import { FILE_SYMLINK_MODE } from '~/vue_shared/constants';
describe('File Icon component', () => {
let wrapper;
const findSvgIcon = () => wrapper.find('svg');
- const findGlIcon = () => wrapper.find(GlIcon);
+ const findGlIcon = () => wrapper.findComponent(GlIcon);
const getIconName = () =>
findSvgIcon()
.find('use')
@@ -61,7 +61,7 @@ describe('File Icon component', () => {
loading: true,
});
- expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
+ expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
});
it('should add a special class and a size class', () => {
diff --git a/spec/frontend/vue_shared/components/file_row_spec.js b/spec/frontend/vue_shared/components/file_row_spec.js
index 62fb29c455c..f5a545891d5 100644
--- a/spec/frontend/vue_shared/components/file_row_spec.js
+++ b/spec/frontend/vue_shared/components/file_row_spec.js
@@ -119,7 +119,7 @@ describe('File row component', () => {
level: 0,
});
- expect(wrapper.find(FileHeader).exists()).toBe(true);
+ expect(wrapper.findComponent(FileHeader).exists()).toBe(true);
});
it('matches the current route against encoded file URL', () => {
@@ -164,6 +164,6 @@ describe('File row component', () => {
level: 0,
});
- expect(wrapper.find(FileIcon).props('submodule')).toBe(submodule);
+ expect(wrapper.findComponent(FileIcon).props('submodule')).toBe(submodule);
});
});
diff --git a/spec/frontend/vue_shared/components/file_tree_spec.js b/spec/frontend/vue_shared/components/file_tree_spec.js
index 39a7c7a2b3a..e8818e09dc0 100644
--- a/spec/frontend/vue_shared/components/file_tree_spec.js
+++ b/spec/frontend/vue_shared/components/file_tree_spec.js
@@ -25,8 +25,8 @@ describe('File Tree component', () => {
});
};
- const findFileRow = () => wrapper.find(MockFileRow);
- const findChildrenTrees = () => wrapper.findAll(FileTree).wrappers.slice(1);
+ const findFileRow = () => wrapper.findComponent(MockFileRow);
+ const findChildrenTrees = () => wrapper.findAllComponents(FileTree).wrappers.slice(1);
const findChildrenTreeProps = () =>
findChildrenTrees().map((x) => ({
...x.props(),
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_bar_root_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_bar_root_spec.js
index e44bc8771f5..1b9ca8e6092 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_bar_root_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_bar_root_spec.js
@@ -88,10 +88,10 @@ describe('FilteredSearchBarRoot', () => {
expect(wrapper.vm.filterValue).toEqual([]);
expect(wrapper.vm.selectedSortOption).toBe(mockSortOptions[0]);
expect(wrapper.vm.selectedSortDirection).toBe(SortDirection.descending);
- expect(wrapper.find(GlButtonGroup).exists()).toBe(true);
- expect(wrapper.find(GlButton).exists()).toBe(true);
- expect(wrapper.find(GlDropdown).exists()).toBe(true);
- expect(wrapper.find(GlDropdownItem).exists()).toBe(true);
+ expect(wrapper.findComponent(GlButtonGroup).exists()).toBe(true);
+ expect(wrapper.findComponent(GlButton).exists()).toBe(true);
+ expect(wrapper.findComponent(GlDropdown).exists()).toBe(true);
+ expect(wrapper.findComponent(GlDropdownItem).exists()).toBe(true);
});
it('does not initialize `selectedSortOption` and `selectedSortDirection` when `sortOptions` is not applied and hides the sort dropdown', () => {
@@ -99,10 +99,10 @@ describe('FilteredSearchBarRoot', () => {
expect(wrapperNoSort.vm.filterValue).toEqual([]);
expect(wrapperNoSort.vm.selectedSortOption).toBe(undefined);
- expect(wrapperNoSort.find(GlButtonGroup).exists()).toBe(false);
- expect(wrapperNoSort.find(GlButton).exists()).toBe(false);
- expect(wrapperNoSort.find(GlDropdown).exists()).toBe(false);
- expect(wrapperNoSort.find(GlDropdownItem).exists()).toBe(false);
+ expect(wrapperNoSort.findComponent(GlButtonGroup).exists()).toBe(false);
+ expect(wrapperNoSort.findComponent(GlButton).exists()).toBe(false);
+ expect(wrapperNoSort.findComponent(GlDropdown).exists()).toBe(false);
+ expect(wrapperNoSort.findComponent(GlDropdownItem).exists()).toBe(false);
});
});
@@ -217,7 +217,7 @@ describe('FilteredSearchBarRoot', () => {
it('emits component event `onFilter` with empty array and true when initially selected filter value was cleared', async () => {
wrapper = createComponent({ initialFilterValue: [tokenValueLabel] });
- wrapper.find(GlFilteredSearch).vm.$emit('clear');
+ wrapper.findComponent(GlFilteredSearch).vm.$emit('clear');
await nextTick();
expect(wrapper.emitted('onFilter')[0]).toEqual([[], true]);
@@ -362,7 +362,7 @@ describe('FilteredSearchBarRoot', () => {
it('calls `blurSearchInput` method to remove focus from filter input field', () => {
jest.spyOn(wrapper.vm, 'blurSearchInput');
- wrapper.find(GlFilteredSearch).vm.$emit('submit', mockFilters);
+ wrapper.findComponent(GlFilteredSearch).vm.$emit('submit', mockFilters);
expect(wrapper.vm.blurSearchInput).toHaveBeenCalled();
});
@@ -392,7 +392,7 @@ describe('FilteredSearchBarRoot', () => {
});
it('renders gl-filtered-search component', () => {
- const glFilteredSearchEl = wrapper.find(GlFilteredSearch);
+ const glFilteredSearchEl = wrapper.findComponent(GlFilteredSearch);
expect(glFilteredSearchEl.props('placeholder')).toBe('Filter requirements');
expect(glFilteredSearchEl.props('availableTokens')).toEqual(mockAvailableTokens);
@@ -404,8 +404,10 @@ describe('FilteredSearchBarRoot', () => {
showCheckbox: true,
});
- expect(wrapperWithCheckbox.find(GlFormCheckbox).exists()).toBe(true);
- expect(wrapperWithCheckbox.find(GlFormCheckbox).attributes('checked')).not.toBeDefined();
+ expect(wrapperWithCheckbox.findComponent(GlFormCheckbox).exists()).toBe(true);
+ expect(
+ wrapperWithCheckbox.findComponent(GlFormCheckbox).attributes('checked'),
+ ).not.toBeDefined();
wrapperWithCheckbox.destroy();
@@ -414,7 +416,7 @@ describe('FilteredSearchBarRoot', () => {
checkboxChecked: true,
});
- expect(wrapperWithCheckbox.find(GlFormCheckbox).attributes('checked')).toBe('true');
+ expect(wrapperWithCheckbox.findComponent(GlFormCheckbox).attributes('checked')).toBe('true');
wrapperWithCheckbox.destroy();
});
@@ -448,7 +450,7 @@ describe('FilteredSearchBarRoot', () => {
await nextTick();
- expect(wrapperFullMount.find(GlDropdownItem).text()).toBe('Membership := Direct');
+ expect(wrapperFullMount.findComponent(GlDropdownItem).text()).toBe('Membership := Direct');
wrapperFullMount.destroy();
});
@@ -466,20 +468,20 @@ describe('FilteredSearchBarRoot', () => {
await nextTick();
- expect(wrapperFullMount.find(GlDropdownItem).text()).toBe('Membership := exclude');
+ expect(wrapperFullMount.findComponent(GlDropdownItem).text()).toBe('Membership := exclude');
wrapperFullMount.destroy();
});
});
it('renders sort dropdown component', () => {
- expect(wrapper.find(GlButtonGroup).exists()).toBe(true);
- expect(wrapper.find(GlDropdown).exists()).toBe(true);
- expect(wrapper.find(GlDropdown).props('text')).toBe(mockSortOptions[0].title);
+ expect(wrapper.findComponent(GlButtonGroup).exists()).toBe(true);
+ expect(wrapper.findComponent(GlDropdown).exists()).toBe(true);
+ expect(wrapper.findComponent(GlDropdown).props('text')).toBe(mockSortOptions[0].title);
});
it('renders sort dropdown items', () => {
- const dropdownItemsEl = wrapper.findAll(GlDropdownItem);
+ const dropdownItemsEl = wrapper.findAllComponents(GlDropdownItem);
expect(dropdownItemsEl).toHaveLength(mockSortOptions.length);
expect(dropdownItemsEl.at(0).text()).toBe(mockSortOptions[0].title);
@@ -488,7 +490,7 @@ describe('FilteredSearchBarRoot', () => {
});
it('renders sort direction button', () => {
- const sortButtonEl = wrapper.find(GlButton);
+ const sortButtonEl = wrapper.findComponent(GlButton);
expect(sortButtonEl.attributes('title')).toBe('Sort direction: Descending');
expect(sortButtonEl.props('icon')).toBe('sort-highest');
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/mock_data.js b/spec/frontend/vue_shared/components/filtered_search_bar/mock_data.js
index 86d1f21fd04..a6713b7e7e4 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/mock_data.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/mock_data.js
@@ -66,12 +66,14 @@ export const mockMilestones = [
export const mockCrmContacts = [
{
+ __typename: 'CustomerRelationsContact',
id: 'gid://gitlab/CustomerRelations::Contact/1',
firstName: 'John',
lastName: 'Smith',
email: 'john@smith.com',
},
{
+ __typename: 'CustomerRelationsContact',
id: 'gid://gitlab/CustomerRelations::Contact/2',
firstName: 'Andy',
lastName: 'Green',
@@ -81,10 +83,12 @@ export const mockCrmContacts = [
export const mockCrmOrganizations = [
{
+ __typename: 'CustomerRelationsOrganization',
id: 'gid://gitlab/CustomerRelations::Organization/1',
name: 'First Org Ltd.',
},
{
+ __typename: 'CustomerRelationsOrganization',
id: 'gid://gitlab/CustomerRelations::Organization/2',
name: 'Organizer S.p.a.',
},
@@ -102,11 +106,9 @@ export const mockProjectCrmContactsQueryResponse = {
__typename: 'CustomerRelationsContactConnection',
nodes: [
{
- __typename: 'CustomerRelationsContact',
...mockCrmContacts[0],
},
{
- __typename: 'CustomerRelationsContact',
...mockCrmContacts[1],
},
],
@@ -128,11 +130,9 @@ export const mockProjectCrmOrganizationsQueryResponse = {
__typename: 'CustomerRelationsOrganizationConnection',
nodes: [
{
- __typename: 'CustomerRelationsOrganization',
...mockCrmOrganizations[0],
},
{
- __typename: 'CustomerRelationsOrganization',
...mockCrmOrganizations[1],
},
],
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 3f24d5df858..302dfabffb2 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
@@ -195,7 +195,7 @@ describe('AuthorToken', () => {
});
await nextTick();
- const tokenSegments = wrapper.findAll(GlFilteredSearchTokenSegment);
+ const tokenSegments = wrapper.findAllComponents(GlFilteredSearchTokenSegment);
expect(tokenSegments).toHaveLength(3); // Author, =, "Administrator"
@@ -207,7 +207,7 @@ describe('AuthorToken', () => {
it('renders token value with correct avatarUrl from author object', async () => {
const getAvatarEl = () =>
- wrapper.findAll(GlFilteredSearchTokenSegment).at(2).findComponent(GlAvatar);
+ wrapper.findAllComponents(GlFilteredSearchTokenSegment).at(2).findComponent(GlAvatar);
wrapper = createComponent({
value: { data: mockAuthors[0].username },
@@ -252,7 +252,7 @@ describe('AuthorToken', () => {
await activateSuggestionsList();
- const suggestions = wrapper.findAll(GlFilteredSearchSuggestion);
+ const suggestions = wrapper.findAllComponents(GlFilteredSearchSuggestion);
expect(suggestions).toHaveLength(defaultAuthors.length + currentUserLength);
defaultAuthors.forEach((label, index) => {
@@ -266,12 +266,12 @@ describe('AuthorToken', () => {
config: { ...mockAuthorToken, defaultAuthors: [] },
stubs: { Portal: true },
});
- const tokenSegments = wrapper.findAll(GlFilteredSearchTokenSegment);
+ const tokenSegments = wrapper.findAllComponents(GlFilteredSearchTokenSegment);
const suggestionsSegment = tokenSegments.at(2);
suggestionsSegment.vm.$emit('activate');
await nextTick();
- expect(wrapper.find(GlDropdownDivider).exists()).toBe(false);
+ expect(wrapper.findComponent(GlDropdownDivider).exists()).toBe(false);
});
it('renders `DEFAULT_NONE_ANY` as default suggestions', async () => {
@@ -283,7 +283,7 @@ describe('AuthorToken', () => {
await activateSuggestionsList();
- const suggestions = wrapper.findAll(GlFilteredSearchSuggestion);
+ const suggestions = wrapper.findAllComponents(GlFilteredSearchSuggestion);
expect(suggestions).toHaveLength(2 + currentUserLength);
expect(suggestions.at(0).text()).toBe(DEFAULT_NONE_ANY[0].text);
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 7b495ec9bee..1de35daa3a5 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
@@ -114,7 +114,7 @@ describe('BranchToken', () => {
describe('template', () => {
const defaultBranches = DEFAULT_NONE_ANY;
async function showSuggestions() {
- const tokenSegments = wrapper.findAll(GlFilteredSearchTokenSegment);
+ const tokenSegments = wrapper.findAllComponents(GlFilteredSearchTokenSegment);
const suggestionsSegment = tokenSegments.at(2);
suggestionsSegment.vm.$emit('activate');
await nextTick();
@@ -133,11 +133,11 @@ describe('BranchToken', () => {
});
it('renders gl-filtered-search-token component', () => {
- expect(wrapper.find(GlFilteredSearchToken).exists()).toBe(true);
+ expect(wrapper.findComponent(GlFilteredSearchToken).exists()).toBe(true);
});
it('renders token item when value is selected', () => {
- const tokenSegments = wrapper.findAll(GlFilteredSearchTokenSegment);
+ const tokenSegments = wrapper.findAllComponents(GlFilteredSearchTokenSegment);
expect(tokenSegments).toHaveLength(3);
expect(tokenSegments.at(2).text()).toBe(mockBranches[0].name);
@@ -150,7 +150,7 @@ describe('BranchToken', () => {
stubs: { Portal: true },
});
await showSuggestions();
- const suggestions = wrapper.findAll(GlFilteredSearchSuggestion);
+ const suggestions = wrapper.findAllComponents(GlFilteredSearchSuggestion);
expect(suggestions).toHaveLength(defaultBranches.length);
defaultBranches.forEach((branch, index) => {
@@ -166,8 +166,8 @@ describe('BranchToken', () => {
});
await showSuggestions();
- expect(wrapper.find(GlFilteredSearchSuggestion).exists()).toBe(false);
- expect(wrapper.find(GlDropdownDivider).exists()).toBe(false);
+ expect(wrapper.findComponent(GlFilteredSearchSuggestion).exists()).toBe(false);
+ expect(wrapper.findComponent(GlDropdownDivider).exists()).toBe(false);
});
it('renders no suggestions as default', async () => {
@@ -177,7 +177,7 @@ describe('BranchToken', () => {
stubs: { Portal: true },
});
await showSuggestions();
- const suggestions = wrapper.findAll(GlFilteredSearchSuggestion);
+ const suggestions = wrapper.findAllComponents(GlFilteredSearchSuggestion);
expect(suggestions).toHaveLength(0);
});
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 157e021fc60..c9879987931 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
@@ -195,7 +195,7 @@ describe('CrmContactToken', () => {
value: { data: '1' },
});
- const baseTokenEl = wrapper.find(BaseToken);
+ const baseTokenEl = wrapper.findComponent(BaseToken);
expect(baseTokenEl.exists()).toBe(true);
expect(baseTokenEl.props()).toMatchObject({
@@ -210,7 +210,7 @@ describe('CrmContactToken', () => {
value: { data: `${getIdFromGraphQLId(contact.id)}` },
});
- const tokenSegments = wrapper.findAll(GlFilteredSearchTokenSegment);
+ const tokenSegments = wrapper.findAllComponents(GlFilteredSearchTokenSegment);
expect(tokenSegments).toHaveLength(3); // Contact, =, Contact name
expect(tokenSegments.at(2).text()).toBe(`${contact.firstName} ${contact.lastName}`); // Contact name
@@ -222,12 +222,12 @@ describe('CrmContactToken', () => {
config: { ...mockCrmContactToken, defaultContacts },
stubs: { Portal: true },
});
- const tokenSegments = wrapper.findAll(GlFilteredSearchTokenSegment);
+ const tokenSegments = wrapper.findAllComponents(GlFilteredSearchTokenSegment);
const suggestionsSegment = tokenSegments.at(2);
suggestionsSegment.vm.$emit('activate');
await nextTick();
- const suggestions = wrapper.findAll(GlFilteredSearchSuggestion);
+ const suggestions = wrapper.findAllComponents(GlFilteredSearchSuggestion);
expect(suggestions).toHaveLength(defaultContacts.length);
defaultContacts.forEach((contact, index) => {
@@ -241,13 +241,13 @@ describe('CrmContactToken', () => {
config: { ...mockCrmContactToken, defaultContacts: [] },
stubs: { Portal: true },
});
- const tokenSegments = wrapper.findAll(GlFilteredSearchTokenSegment);
+ const tokenSegments = wrapper.findAllComponents(GlFilteredSearchTokenSegment);
const suggestionsSegment = tokenSegments.at(2);
suggestionsSegment.vm.$emit('activate');
await nextTick();
- expect(wrapper.find(GlFilteredSearchSuggestion).exists()).toBe(false);
- expect(wrapper.find(GlDropdownDivider).exists()).toBe(false);
+ expect(wrapper.findComponent(GlFilteredSearchSuggestion).exists()).toBe(false);
+ expect(wrapper.findComponent(GlDropdownDivider).exists()).toBe(false);
});
it('renders `DEFAULT_NONE_ANY` as default suggestions', () => {
@@ -256,11 +256,11 @@ describe('CrmContactToken', () => {
config: { ...mockCrmContactToken },
stubs: { Portal: true },
});
- const tokenSegments = wrapper.findAll(GlFilteredSearchTokenSegment);
+ const tokenSegments = wrapper.findAllComponents(GlFilteredSearchTokenSegment);
const suggestionsSegment = tokenSegments.at(2);
suggestionsSegment.vm.$emit('activate');
- const suggestions = wrapper.findAll(GlFilteredSearchSuggestion);
+ const suggestions = wrapper.findAllComponents(GlFilteredSearchSuggestion);
expect(suggestions).toHaveLength(DEFAULT_NONE_ANY.length);
DEFAULT_NONE_ANY.forEach((contact, index) => {
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 977f8bbef61..16333b052e6 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
@@ -194,7 +194,7 @@ describe('CrmOrganizationToken', () => {
value: { data: '1' },
});
- const baseTokenEl = wrapper.find(BaseToken);
+ const baseTokenEl = wrapper.findComponent(BaseToken);
expect(baseTokenEl.exists()).toBe(true);
expect(baseTokenEl.props()).toMatchObject({
@@ -209,7 +209,7 @@ describe('CrmOrganizationToken', () => {
value: { data: `${getIdFromGraphQLId(organization.id)}` },
});
- const tokenSegments = wrapper.findAll(GlFilteredSearchTokenSegment);
+ const tokenSegments = wrapper.findAllComponents(GlFilteredSearchTokenSegment);
expect(tokenSegments).toHaveLength(3); // Organization, =, Organization name
expect(tokenSegments.at(2).text()).toBe(organization.name); // Organization name
@@ -221,12 +221,12 @@ describe('CrmOrganizationToken', () => {
config: { ...mockCrmOrganizationToken, defaultOrganizations },
stubs: { Portal: true },
});
- const tokenSegments = wrapper.findAll(GlFilteredSearchTokenSegment);
+ const tokenSegments = wrapper.findAllComponents(GlFilteredSearchTokenSegment);
const suggestionsSegment = tokenSegments.at(2);
suggestionsSegment.vm.$emit('activate');
await nextTick();
- const suggestions = wrapper.findAll(GlFilteredSearchSuggestion);
+ const suggestions = wrapper.findAllComponents(GlFilteredSearchSuggestion);
expect(suggestions).toHaveLength(defaultOrganizations.length);
defaultOrganizations.forEach((organization, index) => {
@@ -240,13 +240,13 @@ describe('CrmOrganizationToken', () => {
config: { ...mockCrmOrganizationToken, defaultOrganizations: [] },
stubs: { Portal: true },
});
- const tokenSegments = wrapper.findAll(GlFilteredSearchTokenSegment);
+ const tokenSegments = wrapper.findAllComponents(GlFilteredSearchTokenSegment);
const suggestionsSegment = tokenSegments.at(2);
suggestionsSegment.vm.$emit('activate');
await nextTick();
- expect(wrapper.find(GlFilteredSearchSuggestion).exists()).toBe(false);
- expect(wrapper.find(GlDropdownDivider).exists()).toBe(false);
+ expect(wrapper.findComponent(GlFilteredSearchSuggestion).exists()).toBe(false);
+ expect(wrapper.findComponent(GlDropdownDivider).exists()).toBe(false);
});
it('renders `DEFAULT_NONE_ANY` as default suggestions', () => {
@@ -255,11 +255,11 @@ describe('CrmOrganizationToken', () => {
config: { ...mockCrmOrganizationToken },
stubs: { Portal: true },
});
- const tokenSegments = wrapper.findAll(GlFilteredSearchTokenSegment);
+ const tokenSegments = wrapper.findAllComponents(GlFilteredSearchTokenSegment);
const suggestionsSegment = tokenSegments.at(2);
suggestionsSegment.vm.$emit('activate');
- const suggestions = wrapper.findAll(GlFilteredSearchSuggestion);
+ const suggestions = wrapper.findAllComponents(GlFilteredSearchSuggestion);
expect(suggestions).toHaveLength(DEFAULT_NONE_ANY.length);
DEFAULT_NONE_ANY.forEach((organization, index) => {
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 dcb0d095b1b..bf4a6eb7635 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
@@ -135,14 +135,16 @@ describe('EmojiToken', () => {
});
it('renders gl-filtered-search-token component', () => {
- expect(wrapper.find(GlFilteredSearchToken).exists()).toBe(true);
+ expect(wrapper.findComponent(GlFilteredSearchToken).exists()).toBe(true);
});
it('renders token item when value is selected', () => {
- const tokenSegments = wrapper.findAll(GlFilteredSearchTokenSegment);
+ const tokenSegments = wrapper.findAllComponents(GlFilteredSearchTokenSegment);
expect(tokenSegments).toHaveLength(3); // My Reaction, =, "thumbsup"
- expect(tokenSegments.at(2).find(GlEmoji).attributes('data-name')).toEqual('thumbsup');
+ expect(tokenSegments.at(2).findComponent(GlEmoji).attributes('data-name')).toEqual(
+ 'thumbsup',
+ );
});
it('renders provided defaultEmojis as suggestions', async () => {
@@ -151,12 +153,12 @@ describe('EmojiToken', () => {
config: { ...mockReactionEmojiToken, defaultEmojis },
stubs: { Portal: true, GlEmoji },
});
- const tokenSegments = wrapper.findAll(GlFilteredSearchTokenSegment);
+ const tokenSegments = wrapper.findAllComponents(GlFilteredSearchTokenSegment);
const suggestionsSegment = tokenSegments.at(2);
suggestionsSegment.vm.$emit('activate');
await nextTick();
- const suggestions = wrapper.findAll(GlFilteredSearchSuggestion);
+ const suggestions = wrapper.findAllComponents(GlFilteredSearchSuggestion);
expect(suggestions).toHaveLength(defaultEmojis.length);
defaultEmojis.forEach((emoji, index) => {
@@ -170,13 +172,13 @@ describe('EmojiToken', () => {
config: { ...mockReactionEmojiToken, defaultEmojis: [] },
stubs: { Portal: true, GlEmoji },
});
- const tokenSegments = wrapper.findAll(GlFilteredSearchTokenSegment);
+ const tokenSegments = wrapper.findAllComponents(GlFilteredSearchTokenSegment);
const suggestionsSegment = tokenSegments.at(2);
suggestionsSegment.vm.$emit('activate');
await nextTick();
- expect(wrapper.find(GlFilteredSearchSuggestion).exists()).toBe(false);
- expect(wrapper.find(GlDropdownDivider).exists()).toBe(false);
+ expect(wrapper.findComponent(GlFilteredSearchSuggestion).exists()).toBe(false);
+ expect(wrapper.findComponent(GlDropdownDivider).exists()).toBe(false);
});
it('renders `DEFAULT_LABEL_NONE` and `DEFAULT_LABEL_ANY` as default suggestions', async () => {
@@ -185,12 +187,12 @@ describe('EmojiToken', () => {
config: { ...mockReactionEmojiToken },
stubs: { Portal: true, GlEmoji },
});
- const tokenSegments = wrapper.findAll(GlFilteredSearchTokenSegment);
+ const tokenSegments = wrapper.findAllComponents(GlFilteredSearchTokenSegment);
const suggestionsSegment = tokenSegments.at(2);
suggestionsSegment.vm.$emit('activate');
await nextTick();
- const suggestions = wrapper.findAll(GlFilteredSearchSuggestion);
+ const suggestions = wrapper.findAllComponents(GlFilteredSearchSuggestion);
expect(suggestions).toHaveLength(2);
expect(suggestions.at(0).text()).toBe(DEFAULT_LABEL_NONE.text);
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 51161a1a0ef..01e281884ed 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
@@ -156,7 +156,7 @@ describe('LabelToken', () => {
});
it('renders base-token component', () => {
- const baseTokenEl = wrapper.find(BaseToken);
+ const baseTokenEl = wrapper.findComponent(BaseToken);
expect(baseTokenEl.exists()).toBe(true);
expect(baseTokenEl.props()).toMatchObject({
@@ -166,7 +166,7 @@ describe('LabelToken', () => {
});
it('renders token item when value is selected', () => {
- const tokenSegments = wrapper.findAll(GlFilteredSearchTokenSegment);
+ const tokenSegments = wrapper.findAllComponents(GlFilteredSearchTokenSegment);
expect(tokenSegments).toHaveLength(3); // Label, =, "Foo Label"
expect(tokenSegments.at(2).text()).toBe(`~${mockRegularLabel.title}`); // "Foo Label"
@@ -181,12 +181,12 @@ describe('LabelToken', () => {
config: { ...mockLabelToken, defaultLabels },
stubs: { Portal: true },
});
- const tokenSegments = wrapper.findAll(GlFilteredSearchTokenSegment);
+ const tokenSegments = wrapper.findAllComponents(GlFilteredSearchTokenSegment);
const suggestionsSegment = tokenSegments.at(2);
suggestionsSegment.vm.$emit('activate');
await nextTick();
- const suggestions = wrapper.findAll(GlFilteredSearchSuggestion);
+ const suggestions = wrapper.findAllComponents(GlFilteredSearchSuggestion);
expect(suggestions).toHaveLength(defaultLabels.length);
defaultLabels.forEach((label, index) => {
@@ -200,13 +200,13 @@ describe('LabelToken', () => {
config: { ...mockLabelToken, defaultLabels: [] },
stubs: { Portal: true },
});
- const tokenSegments = wrapper.findAll(GlFilteredSearchTokenSegment);
+ const tokenSegments = wrapper.findAllComponents(GlFilteredSearchTokenSegment);
const suggestionsSegment = tokenSegments.at(2);
suggestionsSegment.vm.$emit('activate');
await nextTick();
- expect(wrapper.find(GlFilteredSearchSuggestion).exists()).toBe(false);
- expect(wrapper.find(GlDropdownDivider).exists()).toBe(false);
+ expect(wrapper.findComponent(GlFilteredSearchSuggestion).exists()).toBe(false);
+ expect(wrapper.findComponent(GlDropdownDivider).exists()).toBe(false);
});
it('renders `DEFAULT_NONE_ANY` as default suggestions', () => {
@@ -215,11 +215,11 @@ describe('LabelToken', () => {
config: { ...mockLabelToken },
stubs: { Portal: true },
});
- const tokenSegments = wrapper.findAll(GlFilteredSearchTokenSegment);
+ const tokenSegments = wrapper.findAllComponents(GlFilteredSearchTokenSegment);
const suggestionsSegment = tokenSegments.at(2);
suggestionsSegment.vm.$emit('activate');
- const suggestions = wrapper.findAll(GlFilteredSearchSuggestion);
+ const suggestions = wrapper.findAllComponents(GlFilteredSearchSuggestion);
expect(suggestions).toHaveLength(DEFAULT_NONE_ANY.length);
DEFAULT_NONE_ANY.forEach((label, index) => {
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 7c545f76c0b..f71ba51fc5b 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
@@ -155,11 +155,11 @@ describe('MilestoneToken', () => {
});
it('renders gl-filtered-search-token component', () => {
- expect(wrapper.find(GlFilteredSearchToken).exists()).toBe(true);
+ expect(wrapper.findComponent(GlFilteredSearchToken).exists()).toBe(true);
});
it('renders token item when value is selected', () => {
- const tokenSegments = wrapper.findAll(GlFilteredSearchTokenSegment);
+ const tokenSegments = wrapper.findAllComponents(GlFilteredSearchTokenSegment);
expect(tokenSegments).toHaveLength(3); // Milestone, =, '%"4.0"'
expect(tokenSegments.at(2).text()).toBe(`%${mockRegularMilestone.title}`); // "4.0 RC1"
@@ -171,12 +171,12 @@ describe('MilestoneToken', () => {
config: { ...mockMilestoneToken, defaultMilestones },
stubs: { Portal: true },
});
- const tokenSegments = wrapper.findAll(GlFilteredSearchTokenSegment);
+ const tokenSegments = wrapper.findAllComponents(GlFilteredSearchTokenSegment);
const suggestionsSegment = tokenSegments.at(2);
suggestionsSegment.vm.$emit('activate');
await nextTick();
- const suggestions = wrapper.findAll(GlFilteredSearchSuggestion);
+ const suggestions = wrapper.findAllComponents(GlFilteredSearchSuggestion);
expect(suggestions).toHaveLength(defaultMilestones.length);
defaultMilestones.forEach((milestone, index) => {
@@ -190,13 +190,13 @@ describe('MilestoneToken', () => {
config: { ...mockMilestoneToken, defaultMilestones: [] },
stubs: { Portal: true },
});
- const tokenSegments = wrapper.findAll(GlFilteredSearchTokenSegment);
+ const tokenSegments = wrapper.findAllComponents(GlFilteredSearchTokenSegment);
const suggestionsSegment = tokenSegments.at(2);
suggestionsSegment.vm.$emit('activate');
await nextTick();
- expect(wrapper.find(GlFilteredSearchSuggestion).exists()).toBe(false);
- expect(wrapper.find(GlDropdownDivider).exists()).toBe(false);
+ expect(wrapper.findComponent(GlFilteredSearchSuggestion).exists()).toBe(false);
+ expect(wrapper.findComponent(GlDropdownDivider).exists()).toBe(false);
});
it('renders `DEFAULT_MILESTONES` as default suggestions', async () => {
@@ -205,12 +205,12 @@ describe('MilestoneToken', () => {
config: { ...mockMilestoneToken },
stubs: { Portal: true },
});
- const tokenSegments = wrapper.findAll(GlFilteredSearchTokenSegment);
+ const tokenSegments = wrapper.findAllComponents(GlFilteredSearchTokenSegment);
const suggestionsSegment = tokenSegments.at(2);
suggestionsSegment.vm.$emit('activate');
await nextTick();
- const suggestions = wrapper.findAll(GlFilteredSearchSuggestion);
+ const suggestions = wrapper.findAllComponents(GlFilteredSearchSuggestion);
expect(suggestions).toHaveLength(DEFAULT_MILESTONES.length);
DEFAULT_MILESTONES.forEach((milestone, index) => {
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 b180e8c12dd..6699ae5fb69 100644
--- a/spec/frontend/vue_shared/components/gitlab_version_check_spec.js
+++ b/spec/frontend/vue_shared/components/gitlab_version_check_spec.js
@@ -26,13 +26,44 @@ describe('GitlabVersionCheck', () => {
wrapper = shallowMount(GitlabVersionCheck);
};
+ const dummyGon = {
+ relative_url_root: '/',
+ };
+
+ let originalGon;
+
afterEach(() => {
wrapper.destroy();
mock.restore();
+ window.gon = originalGon;
});
const findGlBadge = () => wrapper.findComponent(GlBadge);
+ describe.each`
+ root | description
+ ${'/'} | ${'not used (uses its own (sub)domain)'}
+ ${'/gitlab'} | ${'custom path'}
+ ${'/service/gitlab'} | ${'custom path with 2 depth'}
+ `('path for version_check.json', ({ root, description }) => {
+ describe(`when relative url is ${description}: ${root}`, () => {
+ beforeEach(async () => {
+ originalGon = window.gon;
+ window.gon = { ...dummyGon };
+ window.gon.relative_url_root = root;
+ createComponent(defaultResponse);
+ await waitForPromises(); // Ensure we wrap up the axios call
+ });
+
+ it('reflects the relative url setting', () => {
+ expect(mock.history.get.length).toBe(1);
+
+ const pathRegex = new RegExp(`^${root}`);
+ expect(mock.history.get[0].url).toMatch(pathRegex);
+ });
+ });
+ });
+
describe('template', () => {
describe.each`
description | mockResponse | renders
diff --git a/spec/frontend/vue_shared/components/gl_modal_vuex_spec.js b/spec/frontend/vue_shared/components/gl_modal_vuex_spec.js
index c0a6588833e..2dcd91f737f 100644
--- a/spec/frontend/vue_shared/components/gl_modal_vuex_spec.js
+++ b/spec/frontend/vue_shared/components/gl_modal_vuex_spec.js
@@ -59,7 +59,7 @@ describe('GlModalVuex', () => {
default: `<div>${TEST_SLOT}</div>`,
},
});
- const glModal = wrapper.find(GlModal);
+ const glModal = wrapper.findComponent(GlModal);
expect(glModal.props('modalId')).toBe(TEST_MODAL_ID);
expect(glModal.text()).toContain(TEST_SLOT);
@@ -76,7 +76,7 @@ describe('GlModalVuex', () => {
okVariant,
},
});
- const glModal = wrapper.find(GlModal);
+ const glModal = wrapper.findComponent(GlModal);
expect(glModal.attributes('title')).toEqual(title);
expect(glModal.attributes('oktitle')).toEqual(title);
@@ -90,7 +90,7 @@ describe('GlModalVuex', () => {
listeners: { ok },
});
- const glModal = wrapper.find(GlModal);
+ const glModal = wrapper.findComponent(GlModal);
glModal.vm.$emit('ok');
expect(ok).toHaveBeenCalledTimes(1);
@@ -101,7 +101,7 @@ describe('GlModalVuex', () => {
factory();
- const glModal = wrapper.find(GlModal);
+ const glModal = wrapper.findComponent(GlModal);
glModal.vm.$emit('shown');
expect(actions.show).toHaveBeenCalledTimes(1);
@@ -112,7 +112,7 @@ describe('GlModalVuex', () => {
factory();
- const glModal = wrapper.find(GlModal);
+ const glModal = wrapper.findComponent(GlModal);
glModal.vm.$emit('hidden');
expect(actions.hide).toHaveBeenCalledTimes(1);
diff --git a/spec/frontend/vue_shared/components/help_popover_spec.js b/spec/frontend/vue_shared/components/help_popover_spec.js
index 64dce194327..6fd5ae0e946 100644
--- a/spec/frontend/vue_shared/components/help_popover_spec.js
+++ b/spec/frontend/vue_shared/components/help_popover_spec.js
@@ -7,8 +7,8 @@ describe('HelpPopover', () => {
const title = 'popover <strong>title</strong>';
const content = 'popover <b>content</b>';
- const findQuestionButton = () => wrapper.find(GlButton);
- const findPopover = () => wrapper.find(GlPopover);
+ const findQuestionButton = () => wrapper.findComponent(GlButton);
+ const findPopover = () => wrapper.findComponent(GlPopover);
const createComponent = ({ props, ...opts } = {}) => {
wrapper = mount(HelpPopover, {
diff --git a/spec/frontend/vue_shared/components/integration_help_text_spec.js b/spec/frontend/vue_shared/components/integration_help_text_spec.js
index c0e8b719007..c63e46313b3 100644
--- a/spec/frontend/vue_shared/components/integration_help_text_spec.js
+++ b/spec/frontend/vue_shared/components/integration_help_text_spec.js
@@ -30,9 +30,9 @@ describe('IntegrationHelpText component', () => {
it('should use the gl components', () => {
wrapper = createComponent();
- expect(wrapper.find(GlSprintf).exists()).toBe(true);
- expect(wrapper.find(GlIcon).exists()).toBe(true);
- expect(wrapper.find(GlLink).exists()).toBe(true);
+ expect(wrapper.findComponent(GlSprintf).exists()).toBe(true);
+ expect(wrapper.findComponent(GlIcon).exists()).toBe(true);
+ expect(wrapper.findComponent(GlLink).exists()).toBe(true);
});
it('should render the help text', () => {
@@ -44,9 +44,9 @@ describe('IntegrationHelpText component', () => {
it('should not use the gl-link and gl-icon components', () => {
wrapper = createComponent({ message: 'Click nowhere!' });
- expect(wrapper.find(GlSprintf).exists()).toBe(true);
- expect(wrapper.find(GlIcon).exists()).toBe(false);
- expect(wrapper.find(GlLink).exists()).toBe(false);
+ expect(wrapper.findComponent(GlSprintf).exists()).toBe(true);
+ expect(wrapper.findComponent(GlIcon).exists()).toBe(false);
+ expect(wrapper.findComponent(GlLink).exists()).toBe(false);
});
it('should not render the link when start and end is not provided', () => {
diff --git a/spec/frontend/vue_shared/components/markdown/field_spec.js b/spec/frontend/vue_shared/components/markdown/field_spec.js
index 85a135d2b89..50864a4bf25 100644
--- a/spec/frontend/vue_shared/components/markdown/field_spec.js
+++ b/spec/frontend/vue_shared/components/markdown/field_spec.js
@@ -76,7 +76,7 @@ describe('Markdown field component', () => {
const getMarkdownButton = () => subject.find('.js-md');
const getListBulletedButton = () => subject.findAll('.js-md[title="Add a bullet list"]');
const getVideo = () => subject.find('video');
- const getAttachButton = () => subject.find('.button-attach-file');
+ const getAttachButton = () => subject.findByTestId('button-attach-file');
const clickAttachButton = () => getAttachButton().trigger('click');
const findDropzone = () => subject.find('.div-dropzone');
const findMarkdownHeader = () => subject.findComponent(MarkdownFieldHeader);
@@ -232,13 +232,10 @@ describe('Markdown field component', () => {
});
});
- it('should render attach a file button', () => {
- expect(getAttachButton().text()).toBe('Attach a file');
- });
-
it('should trigger dropzone when attach button is clicked', () => {
expect(dropzoneSpy).not.toHaveBeenCalled();
+ getAttachButton().trigger('click');
clickAttachButton();
expect(dropzoneSpy).toHaveBeenCalled();
diff --git a/spec/frontend/vue_shared/components/markdown/header_spec.js b/spec/frontend/vue_shared/components/markdown/header_spec.js
index 67222cab247..9831908f806 100644
--- a/spec/frontend/vue_shared/components/markdown/header_spec.js
+++ b/spec/frontend/vue_shared/components/markdown/header_spec.js
@@ -21,7 +21,7 @@ describe('Markdown field header component', () => {
const findWriteTab = () => wrapper.findByTestId('write-tab');
const findPreviewTab = () => wrapper.findByTestId('preview-tab');
const findToolbar = () => wrapper.findByTestId('md-header-toolbar');
- const findToolbarButtons = () => wrapper.findAll(ToolbarButton);
+ const findToolbarButtons = () => wrapper.findAllComponents(ToolbarButton);
const findToolbarButtonByProp = (prop, value) =>
findToolbarButtons()
.filter((button) => button.props(prop) === value)
@@ -44,16 +44,16 @@ describe('Markdown field header component', () => {
describe('markdown header buttons', () => {
it('renders the buttons with the correct title', () => {
const buttons = [
+ 'Insert suggestion',
'Add bold text (⌘B)',
'Add italic text (⌘I)',
'Add strikethrough text (⌘⇧X)',
'Insert a quote',
- 'Insert suggestion',
'Insert code',
'Add a link (⌘K)',
'Add a bullet list',
'Add a numbered list',
- 'Add a task list',
+ 'Add a checklist',
'Add a collapsible section',
'Add a table',
'Go full screen',
@@ -65,6 +65,13 @@ describe('Markdown field header component', () => {
});
});
+ it('renders "Attach a file or image" button using gl-button', () => {
+ const button = wrapper.findByTestId('button-attach-file');
+
+ expect(button.element.tagName).toBe('GL-BUTTON-STUB');
+ expect(button.attributes('title')).toBe('Attach a file or image');
+ });
+
describe('when the user is on a non-Mac', () => {
beforeEach(() => {
delete window.gl.client.isMac;
@@ -118,8 +125,8 @@ describe('Markdown field header component', () => {
),
]);
- expect(wrapper.emitted('preview-markdown')).toBeFalsy();
- expect(wrapper.emitted('write-markdown')).toBeFalsy();
+ expect(wrapper.emitted('preview-markdown')).toBeUndefined();
+ expect(wrapper.emitted('write-markdown')).toBeUndefined();
});
it('blurs preview link after click', () => {
diff --git a/spec/frontend/vue_shared/components/markdown/suggestion_diff_header_spec.js b/spec/frontend/vue_shared/components/markdown/suggestion_diff_header_spec.js
index 9944267cf24..9db1b779a04 100644
--- a/spec/frontend/vue_shared/components/markdown/suggestion_diff_header_spec.js
+++ b/spec/frontend/vue_shared/components/markdown/suggestion_diff_header_spec.js
@@ -38,13 +38,13 @@ describe('Suggestion Diff component', () => {
wrapper.destroy();
});
- const findApplyButton = () => wrapper.find(ApplySuggestion);
+ const findApplyButton = () => wrapper.findComponent(ApplySuggestion);
const findApplyBatchButton = () => wrapper.find('.js-apply-batch-btn');
const findAddToBatchButton = () => wrapper.find('.js-add-to-batch-btn');
const findRemoveFromBatchButton = () => wrapper.find('.js-remove-from-batch-btn');
const findHeader = () => wrapper.find('.js-suggestion-diff-header');
const findHelpButton = () => wrapper.find('.js-help-btn');
- const findLoading = () => wrapper.find(GlLoadingIcon);
+ const findLoading = () => wrapper.findComponent(GlLoadingIcon);
it('renders a suggestion header', () => {
createComponent();
diff --git a/spec/frontend/vue_shared/components/markdown/suggestion_diff_spec.js b/spec/frontend/vue_shared/components/markdown/suggestion_diff_spec.js
index af27e953776..d84483c1663 100644
--- a/spec/frontend/vue_shared/components/markdown/suggestion_diff_spec.js
+++ b/spec/frontend/vue_shared/components/markdown/suggestion_diff_spec.js
@@ -71,7 +71,7 @@ describe('Suggestion Diff component', () => {
});
it('renders a correct amount of suggestion diff rows', () => {
- expect(wrapper.findAll(SuggestionDiffRow)).toHaveLength(3);
+ expect(wrapper.findAllComponents(SuggestionDiffRow)).toHaveLength(3);
});
it.each`
@@ -81,14 +81,14 @@ describe('Suggestion Diff component', () => {
${'addToBatch'} | ${[]} | ${[suggestionId]}
${'removeFromBatch'} | ${[]} | ${[suggestionId]}
`('emits $event event on sugestion diff header $event', ({ event, childArgs, args }) => {
- wrapper.find(SuggestionDiffHeader).vm.$emit(event, ...childArgs);
+ wrapper.findComponent(SuggestionDiffHeader).vm.$emit(event, ...childArgs);
expect(wrapper.emitted(event)).toBeDefined();
expect(wrapper.emitted(event)).toEqual([args]);
});
it('passes suggestion batch props to suggestion diff header', () => {
- expect(wrapper.find(SuggestionDiffHeader).props()).toMatchObject({
+ expect(wrapper.findComponent(SuggestionDiffHeader).props()).toMatchObject({
batchSuggestionsCount: 1,
isBatched: true,
isApplyingBatch: MOCK_DATA.suggestion.is_applying_batch,
diff --git a/spec/frontend/vue_shared/components/markdown/toolbar_button_spec.js b/spec/frontend/vue_shared/components/markdown/toolbar_button_spec.js
index 19e4f2d8c92..82210e79799 100644
--- a/spec/frontend/vue_shared/components/markdown/toolbar_button_spec.js
+++ b/spec/frontend/vue_shared/components/markdown/toolbar_button_spec.js
@@ -26,7 +26,7 @@ describe('toolbar_button', () => {
});
const getButtonShortcutsAttr = () => {
- return wrapper.find(GlButton).attributes('data-md-shortcuts');
+ return wrapper.findComponent(GlButton).attributes('data-md-shortcuts');
};
describe('keyboard shortcuts', () => {
diff --git a/spec/frontend/vue_shared/components/memory_graph_spec.js b/spec/frontend/vue_shared/components/memory_graph_spec.js
index 53b96bd1b98..ae8d5ff78ba 100644
--- a/spec/frontend/vue_shared/components/memory_graph_spec.js
+++ b/spec/frontend/vue_shared/components/memory_graph_spec.js
@@ -47,7 +47,7 @@ describe('MemoryGraph', () => {
it('should draw container with chart', () => {
expect(wrapper.element).toMatchSnapshot();
expect(wrapper.find('.memory-graph-container').exists()).toBe(true);
- expect(wrapper.find(GlSparklineChart).exists()).toBe(true);
+ expect(wrapper.findComponent(GlSparklineChart).exists()).toBe(true);
});
});
});
diff --git a/spec/frontend/vue_shared/components/metric_images/metric_images_tab_spec.js b/spec/frontend/vue_shared/components/metric_images/metric_images_tab_spec.js
index 2cefa77b72d..1789610dba9 100644
--- a/spec/frontend/vue_shared/components/metric_images/metric_images_tab_spec.js
+++ b/spec/frontend/vue_shared/components/metric_images/metric_images_tab_spec.js
@@ -114,7 +114,7 @@ describe('Metric images tab', () => {
await waitForPromises();
- expect(findModal().attributes('visible')).toBeFalsy();
+ expect(findModal().attributes('visible')).toBeUndefined();
});
it('should add files and url when selected', async () => {
diff --git a/spec/frontend/vue_shared/components/namespace_select/namespace_select_spec.js b/spec/frontend/vue_shared/components/namespace_select/namespace_select_spec.js
index c11b20a692e..2c14d65186b 100644
--- a/spec/frontend/vue_shared/components/namespace_select/namespace_select_spec.js
+++ b/spec/frontend/vue_shared/components/namespace_select/namespace_select_spec.js
@@ -1,5 +1,12 @@
import { nextTick } from 'vue';
-import { GlDropdown, GlDropdownItem, GlDropdownSectionHeader, GlSearchBoxByType } from '@gitlab/ui';
+import {
+ GlDropdown,
+ GlDropdownItem,
+ GlDropdownSectionHeader,
+ GlSearchBoxByType,
+ GlIntersectionObserver,
+ GlLoadingIcon,
+} from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import NamespaceSelect, {
i18n,
@@ -7,7 +14,7 @@ import NamespaceSelect, {
} from '~/vue_shared/components/namespace_select/namespace_select.vue';
import { userNamespaces, groupNamespaces } from './mock_data';
-const FLAT_NAMESPACES = [...groupNamespaces, ...userNamespaces];
+const FLAT_NAMESPACES = [...userNamespaces, ...groupNamespaces];
const EMPTY_NAMESPACE_TITLE = 'Empty namespace TEST';
const EMPTY_NAMESPACE_ITEM = { id: EMPTY_NAMESPACE_ID, humanName: EMPTY_NAMESPACE_TITLE };
@@ -31,6 +38,8 @@ describe('Namespace Select', () => {
const findDropdown = () => wrapper.findComponent(GlDropdown);
const findDropdownText = () => findDropdown().props('text');
const findDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
+ const findGroupDropdownItems = () =>
+ wrapper.findByTestId('namespace-list-groups').findAllComponents(GlDropdownItem);
const findDropdownItemsTexts = () => findDropdownItems().wrappers.map((x) => x.text());
const findSectionHeaders = () => wrapper.findAllComponents(GlDropdownSectionHeader);
const findSearchBox = () => wrapper.findComponent(GlSearchBoxByType);
@@ -59,7 +68,7 @@ describe('Namespace Select', () => {
it('splits group and user namespaces', () => {
const headers = findSectionHeaders();
- expect(wrappersText(headers)).toEqual([i18n.GROUPS, i18n.USERS]);
+ expect(wrappersText(headers)).toEqual([i18n.USERS, i18n.GROUPS]);
});
it('does not render wrapper as full width', () => {
@@ -89,18 +98,20 @@ describe('Namespace Select', () => {
describe('with search', () => {
it.each`
- term | includeEmptyNamespace | expectedItems
- ${''} | ${false} | ${[...groupNamespaces, ...userNamespaces]}
- ${'sub'} | ${false} | ${[groupNamespaces[1]]}
- ${'User'} | ${false} | ${[...userNamespaces]}
- ${'User'} | ${true} | ${[...userNamespaces]}
- ${'namespace'} | ${true} | ${[EMPTY_NAMESPACE_ITEM, ...userNamespaces]}
+ term | includeEmptyNamespace | shouldFilterNamespaces | expectedItems
+ ${''} | ${false} | ${true} | ${[...userNamespaces, ...groupNamespaces]}
+ ${'sub'} | ${false} | ${true} | ${[groupNamespaces[1]]}
+ ${'User'} | ${false} | ${true} | ${[...userNamespaces]}
+ ${'User'} | ${true} | ${true} | ${[...userNamespaces]}
+ ${'namespace'} | ${true} | ${true} | ${[EMPTY_NAMESPACE_ITEM, ...userNamespaces]}
+ ${'sub'} | ${false} | ${false} | ${[...userNamespaces, ...groupNamespaces]}
`(
- 'with term=$term and includeEmptyNamespace=$includeEmptyNamespace, should show $expectedItems.length',
- async ({ term, includeEmptyNamespace, expectedItems }) => {
+ 'with term=$term, includeEmptyNamespace=$includeEmptyNamespace, and shouldFilterNamespaces=$shouldFilterNamespaces should show $expectedItems.length',
+ async ({ term, includeEmptyNamespace, shouldFilterNamespaces, expectedItems }) => {
wrapper = createComponent({
includeEmptyNamespace,
emptyNamespaceTitle: EMPTY_NAMESPACE_TITLE,
+ shouldFilterNamespaces,
});
search(term);
@@ -114,6 +125,18 @@ describe('Namespace Select', () => {
);
});
+ describe('when search is typed in', () => {
+ it('emits `search` event', async () => {
+ wrapper = createComponent();
+
+ wrapper.findComponent(GlSearchBoxByType).vm.$emit('input', 'foo');
+
+ await nextTick();
+
+ expect(wrapper.emitted('search')).toEqual([['foo']]);
+ });
+ });
+
describe('with a selected namespace', () => {
const selectedGroupIndex = 1;
const selectedItem = groupNamespaces[selectedGroupIndex];
@@ -121,7 +144,8 @@ describe('Namespace Select', () => {
beforeEach(() => {
wrapper = createComponent();
- findDropdownItems().at(selectedGroupIndex).vm.$emit('click');
+ wrapper.findComponent(GlSearchBoxByType).vm.$emit('input', 'foo');
+ findGroupDropdownItems().at(selectedGroupIndex).vm.$emit('click');
});
it('sets the dropdown text', () => {
@@ -132,6 +156,10 @@ describe('Namespace Select', () => {
const args = [selectedItem];
expect(wrapper.emitted('select')).toEqual([args]);
});
+
+ it('clears search', () => {
+ expect(wrapper.findComponent(GlSearchBoxByType).props('value')).toBe('');
+ });
});
describe('with an empty namespace option', () => {
@@ -166,4 +194,33 @@ describe('Namespace Select', () => {
expect(findDropdownItemsTexts().includes(EMPTY_NAMESPACE_TITLE)).toBe(shouldShow);
});
});
+
+ describe('when `hasNextPageOfGroups` prop is `true`', () => {
+ it('renders `GlIntersectionObserver` and emits `load-more-groups` event when bottom is reached', () => {
+ wrapper = createComponent({ hasNextPageOfGroups: true });
+
+ const intersectionObserver = wrapper.findComponent(GlIntersectionObserver);
+
+ intersectionObserver.vm.$emit('appear');
+
+ expect(intersectionObserver.exists()).toBe(true);
+ expect(wrapper.emitted('load-more-groups')).toEqual([[]]);
+ });
+
+ describe('when `isLoadingMoreGroups` prop is `true`', () => {
+ it('renders a loading icon', () => {
+ wrapper = createComponent({ hasNextPageOfGroups: true, isLoadingMoreGroups: true });
+
+ expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
+ });
+ });
+ });
+
+ describe('when `isSearchLoading` prop is `true`', () => {
+ it('sets `isLoading` prop to `true`', () => {
+ wrapper = createComponent({ isSearchLoading: true });
+
+ expect(wrapper.findComponent(GlSearchBoxByType).props('isLoading')).toBe(true);
+ });
+ });
});
diff --git a/spec/frontend/vue_shared/components/navigation_tabs_spec.js b/spec/frontend/vue_shared/components/navigation_tabs_spec.js
index 30a89fed12f..b1bec28bffb 100644
--- a/spec/frontend/vue_shared/components/navigation_tabs_spec.js
+++ b/spec/frontend/vue_shared/components/navigation_tabs_spec.js
@@ -44,7 +44,7 @@ describe('navigation tabs component', () => {
});
it('should render tabs', () => {
- expect(wrapper.findAll(GlTab)).toHaveLength(data.length);
+ expect(wrapper.findAllComponents(GlTab)).toHaveLength(data.length);
});
it('should render active tab', () => {
diff --git a/spec/frontend/vue_shared/components/notes/noteable_warning_spec.js b/spec/frontend/vue_shared/components/notes/noteable_warning_spec.js
index 99b65ca6937..17a62ae8a33 100644
--- a/spec/frontend/vue_shared/components/notes/noteable_warning_spec.js
+++ b/spec/frontend/vue_shared/components/notes/noteable_warning_spec.js
@@ -6,10 +6,11 @@ import NoteableWarning from '~/vue_shared/components/notes/noteable_warning.vue'
describe('Issue Warning Component', () => {
let wrapper;
- const findIcon = (w = wrapper) => w.find(GlIcon);
- const findLockedBlock = (w = wrapper) => w.find({ ref: 'locked' });
- const findConfidentialBlock = (w = wrapper) => w.find({ ref: 'confidential' });
- const findLockedAndConfidentialBlock = (w = wrapper) => w.find({ ref: 'lockedAndConfidential' });
+ const findIcon = (w = wrapper) => w.findComponent(GlIcon);
+ const findLockedBlock = (w = wrapper) => w.findComponent({ ref: 'locked' });
+ const findConfidentialBlock = (w = wrapper) => w.findComponent({ ref: 'confidential' });
+ const findLockedAndConfidentialBlock = (w = wrapper) =>
+ w.findComponent({ ref: 'lockedAndConfidential' });
const createComponent = (props) =>
shallowMount(NoteableWarning, {
@@ -73,7 +74,7 @@ describe('Issue Warning Component', () => {
});
it('renders warning icon', () => {
- expect(wrapper.find(GlIcon).exists()).toBe(true);
+ expect(wrapper.findComponent(GlIcon).exists()).toBe(true);
});
it('does not render information about locked noteable', () => {
@@ -99,7 +100,7 @@ describe('Issue Warning Component', () => {
});
it('does not render warning icon', () => {
- expect(wrapper.find(GlIcon).exists()).toBe(false);
+ expect(wrapper.findComponent(GlIcon).exists()).toBe(false);
});
it('does not render information about locked noteable', () => {
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 f951cfd5cd9..b86c8946e96 100644
--- a/spec/frontend/vue_shared/components/notes/placeholder_note_spec.js
+++ b/spec/frontend/vue_shared/components/notes/placeholder_note_spec.js
@@ -14,7 +14,7 @@ const getters = {
describe('Issue placeholder note component', () => {
let wrapper;
- const findNote = () => wrapper.find({ ref: 'note' });
+ const findNote = () => wrapper.findComponent({ ref: 'note' });
const createComponent = (isIndividual = false, propsData = {}) => {
wrapper = shallowMount(IssuePlaceholderNote, {
diff --git a/spec/frontend/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs_spec.js b/spec/frontend/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs_spec.js
index 51a936c0509..c0c3c4a9729 100644
--- a/spec/frontend/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs_spec.js
+++ b/spec/frontend/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs_spec.js
@@ -92,15 +92,15 @@ describe('AlertManagementEmptyState', () => {
const EmptyState = () => wrapper.find('.empty-state');
const ItemsTable = () => wrapper.find('.gl-table');
- const ErrorAlert = () => wrapper.find(GlAlert);
- const Pagination = () => wrapper.find(GlPagination);
- const Tabs = () => wrapper.find(GlTabs);
+ const ErrorAlert = () => wrapper.findComponent(GlAlert);
+ const Pagination = () => wrapper.findComponent(GlPagination);
+ const Tabs = () => wrapper.findComponent(GlTabs);
const ActionButton = () => wrapper.find('.header-actions > button');
- const Filters = () => wrapper.find(FilteredSearchBar);
- const findPagination = () => wrapper.find(GlPagination);
- const findStatusFilterTabs = () => wrapper.findAll(GlTab);
- const findStatusTabs = () => wrapper.find(GlTabs);
- const findStatusFilterBadge = () => wrapper.findAll(GlBadge);
+ const Filters = () => wrapper.findComponent(FilteredSearchBar);
+ const findPagination = () => wrapper.findComponent(GlPagination);
+ const findStatusFilterTabs = () => wrapper.findAllComponents(GlTab);
+ const findStatusTabs = () => wrapper.findComponent(GlTabs);
+ const findStatusFilterBadge = () => wrapper.findAllComponents(GlBadge);
describe('Snowplow tracking', () => {
beforeEach(() => {
@@ -213,7 +213,7 @@ describe('AlertManagementEmptyState', () => {
});
it('should render pagination', () => {
- expect(wrapper.find(GlPagination).exists()).toBe(true);
+ expect(wrapper.findComponent(GlPagination).exists()).toBe(true);
});
describe('prevPage', () => {
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 08119dee8af..b3be2f8a775 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
@@ -64,7 +64,7 @@ describe('Pagination bar', () => {
},
});
- expect(wrapper.find(GlDropdown).find('button').text()).toMatchInterpolatedText(
+ expect(wrapper.findComponent(GlDropdown).find('button').text()).toMatchInterpolatedText(
`${CURRENT_PAGE_SIZE} items per page`,
);
});
diff --git a/spec/frontend/vue_shared/components/pagination_links_spec.js b/spec/frontend/vue_shared/components/pagination_links_spec.js
index 83f1e2844f9..d444ad7a733 100644
--- a/spec/frontend/vue_shared/components/pagination_links_spec.js
+++ b/spec/frontend/vue_shared/components/pagination_links_spec.js
@@ -41,7 +41,7 @@ describe('Pagination links component', () => {
beforeEach(() => {
createComponent();
- glPagination = wrapper.find(GlPagination);
+ glPagination = wrapper.findComponent(GlPagination);
});
afterEach(() => {
diff --git a/spec/frontend/vue_shared/components/project_avatar_spec.js b/spec/frontend/vue_shared/components/project_avatar_spec.js
index d55f3127a74..af828fbca51 100644
--- a/spec/frontend/vue_shared/components/project_avatar_spec.js
+++ b/spec/frontend/vue_shared/components/project_avatar_spec.js
@@ -42,6 +42,42 @@ describe('ProjectAvatar', () => {
});
});
+ describe('with `projectId` prop', () => {
+ const validatorFunc = ProjectAvatar.props.projectId.validator;
+
+ it('prop validators return true for valid types', () => {
+ expect(validatorFunc(1)).toBe(true);
+ expect(validatorFunc('gid://gitlab/Project/1')).toBe(true);
+ });
+
+ it('prop validators return false for invalid types', () => {
+ expect(validatorFunc('1')).toBe(false);
+ });
+
+ it('renders GlAvatar with `entityId` 0 when `projectId` is not informed', () => {
+ createComponent({ props: { projectId: undefined } });
+
+ const avatar = findGlAvatar();
+ expect(avatar.props('entityId')).toBe(0);
+ });
+
+ it('renders GlAvatar with specified `entityId` when `projectId` is a Number', () => {
+ const mockProjectId = 1;
+ createComponent({ props: { projectId: mockProjectId } });
+
+ const avatar = findGlAvatar();
+ expect(avatar.props('entityId')).toBe(mockProjectId);
+ });
+
+ it('renders GlAvatar with specified `entityId` when `projectId` is a gid String', () => {
+ const mockProjectId = 'gid://gitlab/Project/1';
+ createComponent({ props: { projectId: mockProjectId } });
+
+ const avatar = findGlAvatar();
+ expect(avatar.props('entityId')).toBe(1);
+ });
+ });
+
describe('with `projectAvatarUrl` prop', () => {
it('renders GlAvatar with specified `src` prop', () => {
const mockProjectAvatarUrl = 'https://gitlab.com';
diff --git a/spec/frontend/vue_shared/components/project_selector/project_list_item_spec.js b/spec/frontend/vue_shared/components/project_selector/project_list_item_spec.js
index 397ab2254b9..4e0c318c84e 100644
--- a/spec/frontend/vue_shared/components/project_selector/project_list_item_spec.js
+++ b/spec/frontend/vue_shared/components/project_selector/project_list_item_spec.js
@@ -56,6 +56,7 @@ describe('ProjectListItem component', () => {
expect(avatar.exists()).toBe(true);
expect(avatar.props()).toMatchObject({
+ projectId: project.id,
projectAvatarUrl: '',
projectName: project.name_with_namespace,
});
diff --git a/spec/frontend/vue_shared/components/project_selector/project_selector_spec.js b/spec/frontend/vue_shared/components/project_selector/project_selector_spec.js
index 379e60c1b2d..a0832dd7030 100644
--- a/spec/frontend/vue_shared/components/project_selector/project_selector_spec.js
+++ b/spec/frontend/vue_shared/components/project_selector/project_selector_spec.js
@@ -15,7 +15,7 @@ describe('ProjectSelector component', () => {
let selected = [];
selected = selected.concat(allProjects.slice(0, 3)).concat(allProjects.slice(5, 8));
- const findSearchInput = () => wrapper.find(GlSearchBoxByType).find('input');
+ const findSearchInput = () => wrapper.findComponent(GlSearchBoxByType).find('input');
const findLegendText = () => wrapper.find('[data-testid="legend-text"]').text();
const search = (query) => {
const searchInput = findSearchInput();
@@ -65,14 +65,14 @@ describe('ProjectSelector component', () => {
it(`triggers a "bottomReached" event when user has scrolled to the bottom of the list`, () => {
jest.spyOn(vm, '$emit').mockImplementation(() => {});
- wrapper.find(GlInfiniteScroll).vm.$emit('bottomReached');
+ wrapper.findComponent(GlInfiniteScroll).vm.$emit('bottomReached');
expect(vm.$emit).toHaveBeenCalledWith('bottomReached');
});
it(`triggers a "projectClicked" event when a project is clicked`, () => {
jest.spyOn(vm, '$emit').mockImplementation(() => {});
- wrapper.find(ProjectListItem).vm.$emit('click', head(searchResults));
+ wrapper.findComponent(ProjectListItem).vm.$emit('click', head(searchResults));
expect(vm.$emit).toHaveBeenCalledWith('projectClicked', head(searchResults));
});
diff --git a/spec/frontend/vue_shared/components/registry/code_instruction_spec.js b/spec/frontend/vue_shared/components/registry/code_instruction_spec.js
index 3a2ea263a05..8f19f0ea14d 100644
--- a/spec/frontend/vue_shared/components/registry/code_instruction_spec.js
+++ b/spec/frontend/vue_shared/components/registry/code_instruction_spec.js
@@ -22,7 +22,7 @@ describe('Package code instruction', () => {
});
}
- const findCopyButton = () => wrapper.find(ClipboardButton);
+ const findCopyButton = () => wrapper.findComponent(ClipboardButton);
const findInputElement = () => wrapper.find('[data-testid="instruction-input"]');
const findMultilineInstruction = () => wrapper.find('[data-testid="multiline-instruction"]');
diff --git a/spec/frontend/vue_shared/components/registry/details_row_spec.js b/spec/frontend/vue_shared/components/registry/details_row_spec.js
index 3134e0d3e21..ebc9816f983 100644
--- a/spec/frontend/vue_shared/components/registry/details_row_spec.js
+++ b/spec/frontend/vue_shared/components/registry/details_row_spec.js
@@ -5,7 +5,7 @@ import component from '~/vue_shared/components/registry/details_row.vue';
describe('DetailsRow', () => {
let wrapper;
- const findIcon = () => wrapper.find(GlIcon);
+ const findIcon = () => wrapper.findComponent(GlIcon);
const findDefaultSlot = () => wrapper.find('[data-testid="default-slot"]');
const mountComponent = (props) => {
diff --git a/spec/frontend/vue_shared/components/registry/history_item_spec.js b/spec/frontend/vue_shared/components/registry/history_item_spec.js
index f146f87342f..947520567e6 100644
--- a/spec/frontend/vue_shared/components/registry/history_item_spec.js
+++ b/spec/frontend/vue_shared/components/registry/history_item_spec.js
@@ -27,8 +27,8 @@ describe('History Item', () => {
wrapper = null;
});
- const findTimelineEntry = () => wrapper.find(TimelineEntryItem);
- const findGlIcon = () => wrapper.find(GlIcon);
+ const findTimelineEntry = () => wrapper.findComponent(TimelineEntryItem);
+ const findGlIcon = () => wrapper.findComponent(GlIcon);
const findDefaultSlot = () => wrapper.find('[data-testid="default-slot"]');
const findBodySlot = () => wrapper.find('[data-testid="body-slot"]');
diff --git a/spec/frontend/vue_shared/components/registry/list_item_spec.js b/spec/frontend/vue_shared/components/registry/list_item_spec.js
index 6e9abb2bfb3..b941eb77c32 100644
--- a/spec/frontend/vue_shared/components/registry/list_item_spec.js
+++ b/spec/frontend/vue_shared/components/registry/list_item_spec.js
@@ -13,7 +13,7 @@ describe('list item', () => {
const findRightSecondarySlot = () => wrapper.find('[data-testid="right-secondary"]');
const findRightActionSlot = () => wrapper.find('[data-testid="right-action"]');
const findDetailsSlot = (name) => wrapper.find(`[data-testid="${name}"]`);
- const findToggleDetailsButton = () => wrapper.find(GlButton);
+ const findToggleDetailsButton = () => wrapper.findComponent(GlButton);
const mountComponent = (propsData, slots) => {
wrapper = shallowMount(component, {
diff --git a/spec/frontend/vue_shared/components/registry/metadata_item_spec.js b/spec/frontend/vue_shared/components/registry/metadata_item_spec.js
index e4abdc15fd5..a04e1e237d4 100644
--- a/spec/frontend/vue_shared/components/registry/metadata_item_spec.js
+++ b/spec/frontend/vue_shared/components/registry/metadata_item_spec.js
@@ -24,10 +24,10 @@ describe('Metadata Item', () => {
wrapper = null;
});
- const findIcon = () => wrapper.find(GlIcon);
- const findLink = (w = wrapper) => w.find(GlLink);
+ const findIcon = () => wrapper.findComponent(GlIcon);
+ const findLink = (w = wrapper) => w.findComponent(GlLink);
const findText = () => wrapper.find('[data-testid="metadata-item-text"]');
- const findTooltipOnTruncate = (w = wrapper) => w.find(TooltipOnTruncate);
+ const findTooltipOnTruncate = (w = wrapper) => w.findComponent(TooltipOnTruncate);
const findTextTooltip = () => wrapper.find('[data-testid="text-tooltip-container"]');
describe.each(['xs', 's', 'm', 'l', 'xl'])('size class', (size) => {
diff --git a/spec/frontend/vue_shared/components/registry/registry_search_spec.js b/spec/frontend/vue_shared/components/registry/registry_search_spec.js
index 20716e79a04..70f4693ae81 100644
--- a/spec/frontend/vue_shared/components/registry/registry_search_spec.js
+++ b/spec/frontend/vue_shared/components/registry/registry_search_spec.js
@@ -6,9 +6,9 @@ import component from '~/vue_shared/components/registry/registry_search.vue';
describe('Registry Search', () => {
let wrapper;
- const findPackageListSorting = () => wrapper.find(GlSorting);
- const findSortingItems = () => wrapper.findAll(GlSortingItem);
- const findFilteredSearch = () => wrapper.find(GlFilteredSearch);
+ const findPackageListSorting = () => wrapper.findComponent(GlSorting);
+ const findSortingItems = () => wrapper.findAllComponents(GlSortingItem);
+ const findFilteredSearch = () => wrapper.findComponent(GlFilteredSearch);
const defaultProps = {
filters: [],
diff --git a/spec/frontend/vue_shared/components/registry/title_area_spec.js b/spec/frontend/vue_shared/components/registry/title_area_spec.js
index b62676b35be..efb57ddd310 100644
--- a/spec/frontend/vue_shared/components/registry/title_area_spec.js
+++ b/spec/frontend/vue_shared/components/registry/title_area_spec.js
@@ -199,7 +199,7 @@ describe('title area', () => {
const message = findInfoMessages().at(0);
- expect(message.find(GlLink).attributes('href')).toBe('bar');
+ expect(message.findComponent(GlLink).attributes('href')).toBe('bar');
expect(message.text()).toBe('foo link');
});
diff --git a/spec/frontend/vue_shared/components/rich_timestamp_tooltip_spec.js b/spec/frontend/vue_shared/components/rich_timestamp_tooltip_spec.js
new file mode 100644
index 00000000000..5d96fe27676
--- /dev/null
+++ b/spec/frontend/vue_shared/components/rich_timestamp_tooltip_spec.js
@@ -0,0 +1,41 @@
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+
+import { formatDate } from '~/lib/utils/datetime_utility';
+import RichTimestampTooltip from '~/vue_shared/components/rich_timestamp_tooltip.vue';
+
+describe('RichTimestampTooltip', () => {
+ const currentDate = new Date();
+ const mockRawTimestamp = currentDate.toISOString();
+ const mockTimestamp = formatDate(currentDate);
+ let wrapper;
+
+ const createComponent = ({
+ target = 'some-element',
+ rawTimestamp = mockRawTimestamp,
+ timestampTypeText = 'Created',
+ } = {}) => {
+ wrapper = shallowMountExtended(RichTimestampTooltip, {
+ propsData: {
+ target,
+ rawTimestamp,
+ timestampTypeText,
+ },
+ });
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('renders the tooltip text header', () => {
+ expect(wrapper.findByTestId('header-text').text()).toBe('Created just now');
+ });
+
+ it('renders the tooltip text body', () => {
+ expect(wrapper.findByTestId('body-text').text()).toBe(mockTimestamp);
+ });
+});
diff --git a/spec/frontend/vue_shared/components/runner_instructions/runner_instructions_modal_spec.js b/spec/frontend/vue_shared/components/runner_instructions/runner_instructions_modal_spec.js
index a38dcd626f4..7c5fc63856a 100644
--- a/spec/frontend/vue_shared/components/runner_instructions/runner_instructions_modal_spec.js
+++ b/spec/frontend/vue_shared/components/runner_instructions/runner_instructions_modal_spec.js
@@ -166,7 +166,7 @@ describe('RunnerInstructionsModal component', () => {
});
it('sets the focus on the default selected platform', () => {
- const findOsxPlatformButton = () => wrapper.find({ ref: 'osx' });
+ const findOsxPlatformButton = () => wrapper.findComponent({ ref: 'osx' });
findOsxPlatformButton().element.focus = jest.fn();
@@ -234,14 +234,14 @@ describe('RunnerInstructionsModal component', () => {
MockResizeObserver.mockResize('xs');
await nextTick();
- expect(findPlatformButtonGroup().attributes('vertical')).toBeTruthy();
+ expect(findPlatformButtonGroup().attributes('vertical')).toEqual('true');
});
it('to a non-xs viewport', async () => {
MockResizeObserver.mockResize('sm');
await nextTick();
- expect(findPlatformButtonGroup().props('vertical')).toBeFalsy();
+ expect(findPlatformButtonGroup().props('vertical')).toBeUndefined();
});
});
});
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 71ebe561def..c5672bc28cc 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
@@ -50,7 +50,7 @@ describe('Merge request artifact Download', () => {
return createMockApollo(requestHandlers);
};
- const findDownloadDropdown = () => wrapper.find(SecurityReportDownloadDropdown);
+ const findDownloadDropdown = () => wrapper.findComponent(SecurityReportDownloadDropdown);
afterEach(() => {
wrapper.destroy();
diff --git a/spec/frontend/vue_shared/components/security_reports/help_icon_spec.js b/spec/frontend/vue_shared/components/security_reports/help_icon_spec.js
index ae86106d86e..08d3d5b19d4 100644
--- a/spec/frontend/vue_shared/components/security_reports/help_icon_spec.js
+++ b/spec/frontend/vue_shared/components/security_reports/help_icon_spec.js
@@ -17,9 +17,9 @@ describe('HelpIcon component', () => {
});
};
- const findLink = () => wrapper.find(GlLink);
- const findPopover = () => wrapper.find(GlPopover);
- const findPopoverTarget = () => wrapper.find({ ref: 'discoverProjectSecurity' });
+ const findLink = () => wrapper.findComponent(GlLink);
+ const findPopover = () => wrapper.findComponent(GlPopover);
+ const findPopoverTarget = () => wrapper.findComponent({ ref: 'discoverProjectSecurity' });
afterEach(() => {
wrapper.destroy();
diff --git a/spec/frontend/vue_shared/components/sidebar/issuable_move_dropdown_spec.js b/spec/frontend/vue_shared/components/sidebar/issuable_move_dropdown_spec.js
index f213e37cbc1..9b1316677d7 100644
--- a/spec/frontend/vue_shared/components/sidebar/issuable_move_dropdown_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/issuable_move_dropdown_spec.js
@@ -173,15 +173,15 @@ describe('IssuableMoveDropdown', () => {
});
describe('template', () => {
- const findDropdownEl = () => wrapper.find(GlDropdown);
+ const findDropdownEl = () => wrapper.findComponent(GlDropdown);
it('renders collapsed state element with icon', () => {
const collapsedEl = wrapper.find('[data-testid="move-collapsed"]');
expect(collapsedEl.exists()).toBe(true);
expect(collapsedEl.attributes('title')).toBe(mockProps.dropdownButtonTitle);
- expect(collapsedEl.find(GlIcon).exists()).toBe(true);
- expect(collapsedEl.find(GlIcon).props('name')).toBe('arrow-right');
+ expect(collapsedEl.findComponent(GlIcon).exists()).toBe(true);
+ expect(collapsedEl.findComponent(GlIcon).props('name')).toBe('arrow-right');
});
describe('gl-dropdown component', () => {
@@ -191,7 +191,7 @@ describe('IssuableMoveDropdown', () => {
});
it('renders gl-dropdown-form component', () => {
- expect(findDropdownEl().find(GlDropdownForm).exists()).toBe(true);
+ expect(findDropdownEl().findComponent(GlDropdownForm).exists()).toBe(true);
});
it('renders header element', () => {
@@ -199,11 +199,11 @@ describe('IssuableMoveDropdown', () => {
expect(headerEl.exists()).toBe(true);
expect(headerEl.find('span').text()).toBe(mockProps.dropdownHeaderTitle);
- expect(headerEl.find(GlButton).props('icon')).toBe('close');
+ expect(headerEl.findComponent(GlButton).props('icon')).toBe('close');
});
it('renders gl-search-box-by-type component', () => {
- const searchEl = findDropdownEl().find(GlSearchBoxByType);
+ const searchEl = findDropdownEl().findComponent(GlSearchBoxByType);
expect(searchEl.exists()).toBe(true);
expect(searchEl.attributes()).toMatchObject({
@@ -221,7 +221,7 @@ describe('IssuableMoveDropdown', () => {
await nextTick();
- expect(findDropdownEl().find(GlLoadingIcon).exists()).toBe(true);
+ expect(findDropdownEl().findComponent(GlLoadingIcon).exists()).toBe(true);
});
it('renders gl-dropdown-item components for available projects', async () => {
@@ -234,7 +234,7 @@ describe('IssuableMoveDropdown', () => {
await nextTick();
- const dropdownItems = wrapper.findAll(GlDropdownItem);
+ const dropdownItems = wrapper.findAllComponents(GlDropdownItem);
expect(dropdownItems).toHaveLength(mockProjects.length);
expect(dropdownItems.at(0).props()).toMatchObject({
@@ -285,7 +285,7 @@ describe('IssuableMoveDropdown', () => {
});
it('renders gl-button within footer', async () => {
- const moveButtonEl = wrapper.find('[data-testid="footer"]').find(GlButton);
+ const moveButtonEl = wrapper.find('[data-testid="footer"]').findComponent(GlButton);
expect(moveButtonEl.text()).toBe('Move');
expect(moveButtonEl.attributes('disabled')).toBe('true');
@@ -299,7 +299,7 @@ describe('IssuableMoveDropdown', () => {
await nextTick();
expect(
- wrapper.find('[data-testid="footer"]').find(GlButton).attributes('disabled'),
+ wrapper.find('[data-testid="footer"]').findComponent(GlButton).attributes('disabled'),
).not.toBeDefined();
});
});
@@ -308,7 +308,7 @@ describe('IssuableMoveDropdown', () => {
it('collapsed state element emits `toggle-collapse` event on component when clicked', () => {
wrapper.find('[data-testid="move-collapsed"]').trigger('click');
- expect(wrapper.emitted('toggle-collapse')).toBeTruthy();
+ expect(wrapper.emitted('toggle-collapse')).toHaveLength(1);
});
it('gl-dropdown component calls `fetchProjects` on `shown` event', () => {
@@ -337,11 +337,11 @@ describe('IssuableMoveDropdown', () => {
it('gl-dropdown component emits `dropdown-close` event on component from `hide` event', async () => {
findDropdownEl().vm.$emit('hide');
- expect(wrapper.emitted('dropdown-close')).toBeTruthy();
+ expect(wrapper.emitted('dropdown-close')).toHaveLength(1);
});
it('close icon in dropdown header closes the dropdown when clicked', () => {
- wrapper.find('[data-testid="header"]').find(GlButton).vm.$emit('click', mockEvent);
+ wrapper.find('[data-testid="header"]').findComponent(GlButton).vm.$emit('click', mockEvent);
expect(wrapper.vm.$refs.dropdown.hide).toHaveBeenCalled();
});
@@ -355,7 +355,7 @@ describe('IssuableMoveDropdown', () => {
await nextTick();
- wrapper.findAll(GlDropdownItem).at(0).vm.$emit('click', mockEvent);
+ wrapper.findAllComponents(GlDropdownItem).at(0).vm.$emit('click', mockEvent);
expect(wrapper.vm.selectedProject).toBe(mockProjects[0]);
});
@@ -369,10 +369,10 @@ describe('IssuableMoveDropdown', () => {
await nextTick();
- wrapper.find('[data-testid="footer"]').find(GlButton).vm.$emit('click');
+ wrapper.find('[data-testid="footer"]').findComponent(GlButton).vm.$emit('click');
expect(wrapper.vm.$refs.dropdown.hide).toHaveBeenCalled();
- expect(wrapper.emitted('move-issuable')).toBeTruthy();
+ expect(wrapper.emitted('move-issuable')).toHaveLength(1);
expect(wrapper.emitted('move-issuable')[0]).toEqual([mockProjects[0]]);
});
});
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_button_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_button_spec.js
index c05513a6d5f..c0e5408e1bd 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_button_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_button_spec.js
@@ -33,9 +33,9 @@ describe('DropdownButton', () => {
wrapper.destroy();
});
- const findDropdownButton = () => wrapper.find(GlButton);
+ const findDropdownButton = () => wrapper.findComponent(GlButton);
const findDropdownText = () => wrapper.find('.dropdown-toggle-text');
- const findDropdownIcon = () => wrapper.find(GlIcon);
+ const findDropdownIcon = () => wrapper.findComponent(GlIcon);
describe('methods', () => {
describe('handleButtonClick', () => {
@@ -61,7 +61,7 @@ describe('DropdownButton', () => {
describe('template', () => {
it('renders component container element', () => {
- expect(wrapper.find(GlButton).element).toBe(wrapper.element);
+ expect(wrapper.findComponent(GlButton).element).toBe(wrapper.element);
});
it('renders default button text element', () => {
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_create_view_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_create_view_spec.js
index 0673ffee22b..799e2c1d08e 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_create_view_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_create_view_spec.js
@@ -127,7 +127,7 @@ describe('DropdownContentsCreateView', () => {
});
it('renders dropdown back button element', () => {
- const backBtnEl = wrapper.find('.dropdown-title').findAll(GlButton).at(0);
+ const backBtnEl = wrapper.find('.dropdown-title').findAllComponents(GlButton).at(0);
expect(backBtnEl.exists()).toBe(true);
expect(backBtnEl.attributes('aria-label')).toBe('Go back');
@@ -142,7 +142,7 @@ describe('DropdownContentsCreateView', () => {
});
it('renders dropdown close button element', () => {
- const closeBtnEl = wrapper.find('.dropdown-title').findAll(GlButton).at(1);
+ const closeBtnEl = wrapper.find('.dropdown-title').findAllComponents(GlButton).at(1);
expect(closeBtnEl.exists()).toBe(true);
expect(closeBtnEl.attributes('aria-label')).toBe('Close');
@@ -150,7 +150,7 @@ describe('DropdownContentsCreateView', () => {
});
it('renders label title input element', () => {
- const titleInputEl = wrapper.find('.dropdown-input').find(GlFormInput);
+ const titleInputEl = wrapper.find('.dropdown-input').findComponent(GlFormInput);
expect(titleInputEl.exists()).toBe(true);
expect(titleInputEl.attributes('placeholder')).toBe('Name new label');
@@ -158,7 +158,7 @@ describe('DropdownContentsCreateView', () => {
});
it('renders color block element for all suggested colors', () => {
- const colorBlocksEl = wrapper.find('.dropdown-content').findAll(GlLink);
+ const colorBlocksEl = wrapper.find('.dropdown-content').findAllComponents(GlLink);
colorBlocksEl.wrappers.forEach((colorBlock, index) => {
expect(colorBlock.attributes('style')).toContain('background-color');
@@ -175,7 +175,7 @@ describe('DropdownContentsCreateView', () => {
await nextTick();
const colorPreviewEl = wrapper.find('.color-input-container > .dropdown-label-color-preview');
- const colorInputEl = wrapper.find('.color-input-container').find(GlFormInput);
+ const colorInputEl = wrapper.find('.color-input-container').findComponent(GlFormInput);
expect(colorPreviewEl.exists()).toBe(true);
expect(colorPreviewEl.attributes('style')).toContain('background-color');
@@ -185,7 +185,7 @@ describe('DropdownContentsCreateView', () => {
});
it('renders create button element', () => {
- const createBtnEl = wrapper.find('.dropdown-actions').findAll(GlButton).at(0);
+ const createBtnEl = wrapper.find('.dropdown-actions').findAllComponents(GlButton).at(0);
expect(createBtnEl.exists()).toBe(true);
expect(createBtnEl.text()).toContain('Create');
@@ -195,14 +195,14 @@ describe('DropdownContentsCreateView', () => {
wrapper.vm.$store.dispatch('requestCreateLabel');
await nextTick();
- const loadingIconEl = wrapper.find('.dropdown-actions').find(GlLoadingIcon);
+ const loadingIconEl = wrapper.find('.dropdown-actions').findComponent(GlLoadingIcon);
expect(loadingIconEl.exists()).toBe(true);
expect(loadingIconEl.isVisible()).toBe(true);
});
it('renders cancel button element', () => {
- const cancelBtnEl = wrapper.find('.dropdown-actions').findAll(GlButton).at(1);
+ const cancelBtnEl = wrapper.find('.dropdown-actions').findAllComponents(GlButton).at(1);
expect(cancelBtnEl.exists()).toBe(true);
expect(cancelBtnEl.text()).toContain('Cancel');
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view_spec.js
index 00c8e3a814a..cc9b9f393ce 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view_spec.js
@@ -58,7 +58,7 @@ describe('DropdownContentsLabelsView', () => {
const findDropdownContent = () => wrapper.find('[data-testid="dropdown-content"]');
const findDropdownTitle = () => wrapper.find('[data-testid="dropdown-title"]');
const findDropdownFooter = () => wrapper.find('[data-testid="dropdown-footer"]');
- const findLoadingIcon = () => wrapper.find(GlLoadingIcon);
+ const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
describe('computed', () => {
describe('visibleLabels', () => {
@@ -285,7 +285,7 @@ describe('DropdownContentsLabelsView', () => {
describe('template', () => {
it('renders gl-intersection-observer as component root', () => {
- expect(wrapper.find(GlIntersectionObserver).exists()).toBe(true);
+ expect(wrapper.findComponent(GlIntersectionObserver).exists()).toBe(true);
});
it('renders gl-loading-icon component when `labelsFetchInProgress` prop is true', async () => {
@@ -316,20 +316,20 @@ describe('DropdownContentsLabelsView', () => {
});
it('renders dropdown close button element', () => {
- const closeButtonEl = findDropdownTitle().find(GlButton);
+ const closeButtonEl = findDropdownTitle().findComponent(GlButton);
expect(closeButtonEl.exists()).toBe(true);
expect(closeButtonEl.props('icon')).toBe('close');
});
it('renders label search input element', () => {
- const searchInputEl = wrapper.find(GlSearchBoxByType);
+ const searchInputEl = wrapper.findComponent(GlSearchBoxByType);
expect(searchInputEl.exists()).toBe(true);
});
it('renders label elements for all labels', () => {
- expect(wrapper.findAll(LabelItem)).toHaveLength(mockLabels.length);
+ expect(wrapper.findAllComponents(LabelItem)).toHaveLength(mockLabels.length);
});
it('renders label element with `highlight` set to true when value of `currentHighlightItem` is more than -1', async () => {
@@ -340,7 +340,7 @@ describe('DropdownContentsLabelsView', () => {
});
await nextTick();
- const labelItemEl = findDropdownContent().find(LabelItem);
+ const labelItemEl = findDropdownContent().findComponent(LabelItem);
expect(labelItemEl.attributes('highlight')).toBe('true');
});
@@ -373,7 +373,7 @@ describe('DropdownContentsLabelsView', () => {
});
it('renders footer list items', () => {
- const footerLinks = findDropdownFooter().findAll(GlLink);
+ const footerLinks = findDropdownFooter().findAllComponents(GlLink);
const createLabelLink = footerLinks.at(0);
const manageLabelsLink = footerLinks.at(1);
@@ -387,7 +387,7 @@ describe('DropdownContentsLabelsView', () => {
wrapper.vm.$store.state.allowLabelCreate = false;
await nextTick();
- const createLabelLink = findDropdownFooter().findAll(GlLink).at(0);
+ const createLabelLink = findDropdownFooter().findAllComponents(GlLink).at(0);
expect(createLabelLink.text()).not.toBe('Create label');
});
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_title_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_title_spec.js
index 84e9f3f41c3..54804f85f81 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_title_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_title_spec.js
@@ -41,7 +41,7 @@ describe('DropdownTitle', () => {
});
it('renders edit link', () => {
- const editBtnEl = wrapper.find(GlButton);
+ const editBtnEl = wrapper.findComponent(GlButton);
expect(editBtnEl.exists()).toBe(true);
expect(editBtnEl.text()).toBe('Edit');
@@ -53,7 +53,7 @@ describe('DropdownTitle', () => {
});
await nextTick();
- expect(wrapper.find(GlLoadingIcon).isVisible()).toBe(true);
+ expect(wrapper.findComponent(GlLoadingIcon).isVisible()).toBe(true);
});
});
});
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/label_item_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/label_item_spec.js
index bedb6204088..bb0f1777de6 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/label_item_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/label_item_spec.js
@@ -32,7 +32,7 @@ describe('LabelItem', () => {
describe('template', () => {
it('renders gl-link component', () => {
- expect(wrapper.find(GlLink).exists()).toBe(true);
+ expect(wrapper.findComponent(GlLink).exists()).toBe(true);
});
it('renders component root with class `is-focused` when `highlight` prop is true', () => {
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 c150410ff8e..4c7ac6e9a6f 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
@@ -138,13 +138,13 @@ describe('LabelsSelectRoot', () => {
it('renders `dropdown-value-collapsed` component when `allowLabelCreate` prop is `true`', async () => {
createComponent();
await nextTick();
- expect(wrapper.find(DropdownValueCollapsed).exists()).toBe(true);
+ expect(wrapper.findComponent(DropdownValueCollapsed).exists()).toBe(true);
});
it('renders `dropdown-title` component', async () => {
createComponent();
await nextTick();
- expect(wrapper.find(DropdownTitle).exists()).toBe(true);
+ expect(wrapper.findComponent(DropdownTitle).exists()).toBe(true);
});
it('renders `dropdown-value` component', async () => {
@@ -153,7 +153,7 @@ describe('LabelsSelectRoot', () => {
});
await nextTick();
- const valueComp = wrapper.find(DropdownValue);
+ const valueComp = wrapper.findComponent(DropdownValue);
expect(valueComp.exists()).toBe(true);
expect(valueComp.text()).toBe('None');
@@ -163,14 +163,14 @@ describe('LabelsSelectRoot', () => {
createComponent();
wrapper.vm.$store.dispatch('toggleDropdownButton');
await nextTick();
- expect(wrapper.find(DropdownButton).exists()).toBe(true);
+ expect(wrapper.findComponent(DropdownButton).exists()).toBe(true);
});
it('renders `dropdown-contents` component when `showDropdownButton` & `showDropdownContents` prop is `true`', async () => {
createComponent();
wrapper.vm.$store.dispatch('toggleDropdownContents');
await nextTick();
- expect(wrapper.find(DropdownContents).exists()).toBe(true);
+ expect(wrapper.findComponent(DropdownContents).exists()).toBe(true);
});
describe('sets content direction based on viewport', () => {
@@ -187,7 +187,7 @@ describe('LabelsSelectRoot', () => {
wrapper.vm.setContentIsOnViewport(wrapper.vm.$store.state);
await nextTick();
- expect(wrapper.find(DropdownContents).props('renderOnTop')).toBe(true);
+ expect(wrapper.findComponent(DropdownContents).props('renderOnTop')).toBe(true);
});
it('does not set direction when inside of viewport', async () => {
@@ -195,7 +195,7 @@ describe('LabelsSelectRoot', () => {
wrapper.vm.setContentIsOnViewport(wrapper.vm.$store.state);
await nextTick();
- expect(wrapper.find(DropdownContents).props('renderOnTop')).toBe(false);
+ expect(wrapper.findComponent(DropdownContents).props('renderOnTop')).toBe(false);
});
},
);
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 1b27a294b90..cad401e0013 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
@@ -131,6 +131,7 @@ describe('LabelsSelectRoot', () => {
expect(findDropdownValue().exists()).toBe(true);
expect(findDropdownValue().props('selectedLabels')).toEqual([
{
+ __typename: 'Label',
color: '#330066',
description: null,
id: 'gid://gitlab/ProjectLabel/1',
diff --git a/spec/frontend/vue_shared/components/sidebar/todo_button_spec.js b/spec/frontend/vue_shared/components/sidebar/todo_button_spec.js
index de3e1ccfb03..01958a144ed 100644
--- a/spec/frontend/vue_shared/components/sidebar/todo_button_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/todo_button_spec.js
@@ -30,19 +30,19 @@ describe('Todo Button', () => {
it('renders GlButton', () => {
createComponent();
- expect(wrapper.find(GlButton).exists()).toBe(true);
+ expect(wrapper.findComponent(GlButton).exists()).toBe(true);
});
it('emits click event when clicked', () => {
createComponent({}, mount);
- wrapper.find(GlButton).trigger('click');
+ wrapper.findComponent(GlButton).trigger('click');
- expect(wrapper.emitted().click).toBeTruthy();
+ expect(wrapper.emitted().click).toHaveLength(1);
});
it('calls dispatchDocumentEvent to update global To-Do counter correctly', () => {
createComponent({}, mount);
- wrapper.find(GlButton).trigger('click');
+ wrapper.findComponent(GlButton).trigger('click');
const dispatchedEvent = dispatchEventSpy.mock.calls[0][0];
expect(dispatchEventSpy).toHaveBeenCalledTimes(1);
@@ -57,12 +57,12 @@ describe('Todo Button', () => {
`('sets correct label when isTodo is $isTodo', ({ label, isTodo }) => {
createComponent({ isTodo });
- expect(wrapper.find(GlButton).text()).toBe(label);
+ expect(wrapper.findComponent(GlButton).text()).toBe(label);
});
it('binds additional props to GlButton', () => {
createComponent({ loading: true });
- expect(wrapper.find(GlButton).props('loading')).toBe(true);
+ expect(wrapper.findComponent(GlButton).props('loading')).toBe(true);
});
});
diff --git a/spec/frontend/vue_shared/components/source_editor_spec.js b/spec/frontend/vue_shared/components/source_editor_spec.js
index dca4d60e23c..ca5b990bc29 100644
--- a/spec/frontend/vue_shared/components/source_editor_spec.js
+++ b/spec/frontend/vue_shared/components/source_editor_spec.js
@@ -3,6 +3,7 @@ import { nextTick } from 'vue';
import { EDITOR_READY_EVENT } from '~/editor/constants';
import Editor from '~/editor/source_editor';
import SourceEditor from '~/vue_shared/components/source_editor.vue';
+import * as helpers from 'jest/editor/helpers';
jest.mock('~/editor/source_editor');
@@ -13,6 +14,7 @@ describe('Source Editor component', () => {
const value = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.';
const fileName = 'lorem.txt';
const fileGlobalId = 'snippet_777';
+ const useSpy = jest.fn();
const createInstanceMock = jest.fn().mockImplementation(() => {
mockInstance = {
onDidChangeModelContent: jest.fn(),
@@ -20,6 +22,7 @@ describe('Source Editor component', () => {
getValue: jest.fn(),
setValue: jest.fn(),
dispose: jest.fn(),
+ use: useSpy,
};
return mockInstance;
});
@@ -77,16 +80,33 @@ describe('Source Editor component', () => {
});
it('initialises Source Editor instance', () => {
- const el = wrapper.find({ ref: 'editor' }).element;
+ const el = wrapper.findComponent({ ref: 'editor' }).element;
expect(createInstanceMock).toHaveBeenCalledWith({
el,
blobPath: fileName,
blobGlobalId: fileGlobalId,
blobContent: value,
- extensions: null,
});
});
+ it.each`
+ description | extensions | toBeCalled
+ ${'no extension when `undefined` is'} | ${undefined} | ${false}
+ ${'no extension when {} is'} | ${{}} | ${false}
+ ${'no extension when [] is'} | ${[]} | ${false}
+ ${'single extension'} | ${{ definition: helpers.SEClassExtension }} | ${true}
+ ${'single extension with options'} | ${{ definition: helpers.SEWithSetupExt, setupOptions: { foo: 'bar' } }} | ${true}
+ ${'multiple extensions'} | ${[{ definition: helpers.SEClassExtension }, { definition: helpers.SEWithSetupExt }]} | ${true}
+ ${'multiple extensions with options'} | ${[{ definition: helpers.SEClassExtension }, { definition: helpers.SEWithSetupExt, setupOptions: { foo: 'bar' } }]} | ${true}
+ `('installs $description passed as a prop', ({ extensions, toBeCalled }) => {
+ createComponent({ extensions });
+ if (toBeCalled) {
+ expect(useSpy).toHaveBeenCalledWith(extensions);
+ } else {
+ expect(useSpy).not.toHaveBeenCalled();
+ }
+ });
+
it('reacts to the changes in fileName', () => {
const newFileName = 'ipsum.txt';
@@ -112,7 +132,7 @@ describe('Source Editor component', () => {
});
it('emits EDITOR_READY_EVENT event when the Source Editor is ready', async () => {
- const el = wrapper.find({ ref: 'editor' }).element;
+ const el = wrapper.findComponent({ ref: 'editor' }).element;
expect(wrapper.emitted()[EDITOR_READY_EVENT]).toBeUndefined();
await el.dispatchEvent(new Event(EDITOR_READY_EVENT));
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 eb2eec92534..fd3ff9ce892 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,4 +1,3 @@
-import { GlLink } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import ChunkLine from '~/vue_shared/components/source_viewer/components/chunk_line.vue';
import {
@@ -11,16 +10,26 @@ const DEFAULT_PROPS = {
number: 2,
content: '// Line content',
language: 'javascript',
+ blamePath: 'blame/file.js',
};
describe('Chunk Line component', () => {
let wrapper;
+ const fileLineBlame = true;
const createComponent = (props = {}) => {
- wrapper = shallowMountExtended(ChunkLine, { propsData: { ...DEFAULT_PROPS, ...props } });
+ wrapper = shallowMountExtended(ChunkLine, {
+ propsData: { ...DEFAULT_PROPS, ...props },
+ provide: {
+ glFeatures: {
+ fileLineBlame,
+ },
+ },
+ });
};
- const findLink = () => wrapper.findComponent(GlLink);
+ 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');
@@ -47,14 +56,22 @@ describe('Chunk Line component', () => {
});
});
+ it('renders a blame link', () => {
+ expect(findBlameLink().attributes()).toMatchObject({
+ href: `${DEFAULT_PROPS.blamePath}#L${DEFAULT_PROPS.number}`,
+ });
+
+ expect(findBlameLink().text()).toBe('');
+ });
+
it('renders a line number', () => {
- expect(findLink().attributes()).toMatchObject({
+ expect(findLineLink().attributes()).toMatchObject({
'data-line-number': `${DEFAULT_PROPS.number}`,
- to: `#L${DEFAULT_PROPS.number}`,
+ href: `#L${DEFAULT_PROPS.number}`,
id: `L${DEFAULT_PROPS.number}`,
});
- expect(findLink().text()).toBe(DEFAULT_PROPS.number.toString());
+ expect(findLineLink().text()).toBe(DEFAULT_PROPS.number.toString());
});
it('renders content', () => {
diff --git a/spec/frontend/vue_shared/components/source_viewer/components/chunk_spec.js b/spec/frontend/vue_shared/components/source_viewer/components/chunk_spec.js
index 42c4f2eacb8..8dc3348acfa 100644
--- a/spec/frontend/vue_shared/components/source_viewer/components/chunk_spec.js
+++ b/spec/frontend/vue_shared/components/source_viewer/components/chunk_spec.js
@@ -10,6 +10,7 @@ const DEFAULT_PROPS = {
startingFrom: 140,
totalLines: 50,
language: 'javascript',
+ blamePath: 'blame/file.js',
};
describe('Chunk component', () => {
@@ -76,6 +77,7 @@ describe('Chunk component', () => {
number: DEFAULT_PROPS.startingFrom + 1,
content: splitContent[0],
language: DEFAULT_PROPS.language,
+ blamePath: DEFAULT_PROPS.blamePath,
});
});
});
diff --git a/spec/frontend/vue_shared/components/source_viewer/plugins/link_dependencies_spec.js b/spec/frontend/vue_shared/components/source_viewer/plugins/link_dependencies_spec.js
index 3036ce43888..375b1307616 100644
--- a/spec/frontend/vue_shared/components/source_viewer/plugins/link_dependencies_spec.js
+++ b/spec/frontend/vue_shared/components/source_viewer/plugins/link_dependencies_spec.js
@@ -1,8 +1,10 @@
import packageJsonLinker from '~/vue_shared/components/source_viewer/plugins/utils/package_json_linker';
+import gemspecLinker from '~/vue_shared/components/source_viewer/plugins/utils/gemspec_linker';
import linkDependencies from '~/vue_shared/components/source_viewer/plugins/link_dependencies';
-import { PACKAGE_JSON_FILE_TYPE, PACKAGE_JSON_CONTENT } from './mock_data';
+import { PACKAGE_JSON_FILE_TYPE, PACKAGE_JSON_CONTENT, GEMSPEC_FILE_TYPE } from './mock_data';
jest.mock('~/vue_shared/components/source_viewer/plugins/utils/package_json_linker');
+jest.mock('~/vue_shared/components/source_viewer/plugins/utils/gemspec_linker');
describe('Highlight.js plugin for linking dependencies', () => {
const hljsResultMock = { value: 'test' };
@@ -11,4 +13,9 @@ describe('Highlight.js plugin for linking dependencies', () => {
linkDependencies(hljsResultMock, PACKAGE_JSON_FILE_TYPE, PACKAGE_JSON_CONTENT);
expect(packageJsonLinker).toHaveBeenCalled();
});
+
+ it('calls gemspecLinker for gemspec file types', () => {
+ linkDependencies(hljsResultMock, GEMSPEC_FILE_TYPE);
+ expect(gemspecLinker).toHaveBeenCalled();
+ });
});
diff --git a/spec/frontend/vue_shared/components/source_viewer/plugins/mock_data.js b/spec/frontend/vue_shared/components/source_viewer/plugins/mock_data.js
index 75659770e2c..aa874c9c081 100644
--- a/spec/frontend/vue_shared/components/source_viewer/plugins/mock_data.js
+++ b/spec/frontend/vue_shared/components/source_viewer/plugins/mock_data.js
@@ -1,2 +1,4 @@
export const PACKAGE_JSON_FILE_TYPE = 'package_json';
export const PACKAGE_JSON_CONTENT = '{ "dependencies": { "@babel/core": "^7.18.5" } }';
+
+export const GEMSPEC_FILE_TYPE = 'gemspec';
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 ee200747af9..8079d5ad99a 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
@@ -14,10 +14,11 @@ describe('createLink', () => {
it('escapes the user-controlled content', () => {
const unescapedXSS = '<script>XSS</script>';
- const escapedXSS = '&amp;lt;script&amp;gt;XSS&amp;lt;/script&amp;gt;';
+ const escapedPackageName = '&lt;script&gt;XSS&lt;/script&gt;';
+ const escapedHref = '&amp;lt;script&amp;gt;XSS&amp;lt;/script&amp;gt;';
const href = `http://test.com/${unescapedXSS}`;
const innerText = `testing${unescapedXSS}`;
- const result = `<a href="http://test.com/${escapedXSS}" rel="nofollow noreferrer noopener">testing${escapedXSS}</a>`;
+ const result = `<a href="http://test.com/${escapedHref}" rel="nofollow noreferrer noopener">testing${escapedPackageName}</a>`;
expect(createLink(href, innerText)).toBe(result);
});
diff --git a/spec/frontend/vue_shared/components/source_viewer/plugins/utils/gemspec_linker_spec.js b/spec/frontend/vue_shared/components/source_viewer/plugins/utils/gemspec_linker_spec.js
new file mode 100644
index 00000000000..3f74bfa117f
--- /dev/null
+++ b/spec/frontend/vue_shared/components/source_viewer/plugins/utils/gemspec_linker_spec.js
@@ -0,0 +1,14 @@
+import gemspecLinker from '~/vue_shared/components/source_viewer/plugins/utils/gemspec_linker';
+
+describe('Highlight.js plugin for linking gemspec dependencies', () => {
+ it('mutates the input value by wrapping dependency names in anchors', () => {
+ const inputValue =
+ 's.add_dependency(<span class="hljs-string">&#x27;rugged&#x27;</span>, <span class="hljs-string">&#x27;~&gt; 0.24.0&#x27;</span>)';
+ const outputValue =
+ 's.add_dependency(<span class="hljs-string linked">&#x27;<a href="https://rubygems.org/gems/rugged" rel="nofollow noreferrer noopener">rugged</a>&#x27;</span>, <span class="hljs-string">&#x27;~&gt; 0.24.0&#x27;</span>)';
+ const hljsResultMock = { value: inputValue };
+
+ const output = gemspecLinker(hljsResultMock);
+ expect(output).toBe(outputValue);
+ });
+});
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 2c03b7aa7d3..4fbc907a813 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
@@ -40,8 +40,9 @@ describe('Source Viewer component', () => {
const chunk2 = generateContent('// Some source code 2', 70);
const content = chunk1 + chunk2;
const path = 'some/path.js';
+ const blamePath = 'some/blame/path.js';
const fileType = 'javascript';
- const DEFAULT_BLOB_DATA = { language, rawTextBlob: content, path, fileType };
+ const DEFAULT_BLOB_DATA = { language, rawTextBlob: content, path, blamePath, fileType };
const highlightedContent = `<span data-testid='test-highlighted' id='LC1'>${content}</span><span id='LC2'></span>`;
const createComponent = async (blob = {}) => {
diff --git a/spec/frontend/vue_shared/components/split_button_spec.js b/spec/frontend/vue_shared/components/split_button_spec.js
index 4965969bc3e..6b869db4058 100644
--- a/spec/frontend/vue_shared/components/split_button_spec.js
+++ b/spec/frontend/vue_shared/components/split_button_spec.js
@@ -26,8 +26,9 @@ describe('SplitButton', () => {
});
};
- const findDropdown = () => wrapper.find(GlDropdown);
- const findDropdownItem = (index = 0) => findDropdown().findAll(GlDropdownItem).at(index);
+ const findDropdown = () => wrapper.findComponent(GlDropdown);
+ const findDropdownItem = (index = 0) =>
+ findDropdown().findAllComponents(GlDropdownItem).at(index);
const selectItem = async (index) => {
findDropdownItem(index).vm.$emit('click');
diff --git a/spec/frontend/vue_shared/components/table_pagination_spec.js b/spec/frontend/vue_shared/components/table_pagination_spec.js
index ed23a47c328..99de26ce2ae 100644
--- a/spec/frontend/vue_shared/components/table_pagination_spec.js
+++ b/spec/frontend/vue_shared/components/table_pagination_spec.js
@@ -50,7 +50,7 @@ describe('Pagination component', () => {
change: spy,
});
- expect(wrapper.find(GlPagination).exists()).toBe(true);
+ expect(wrapper.findComponent(GlPagination).exists()).toBe(true);
});
it('renders if there is a prev page', () => {
@@ -66,7 +66,7 @@ describe('Pagination component', () => {
change: spy,
});
- expect(wrapper.find(GlPagination).exists()).toBe(true);
+ expect(wrapper.findComponent(GlPagination).exists()).toBe(true);
});
});
@@ -83,7 +83,7 @@ describe('Pagination component', () => {
},
change: spy,
});
- wrapper.find(GlPagination).vm.$emit('input', 3);
+ wrapper.findComponent(GlPagination).vm.$emit('input', 3);
expect(spy).toHaveBeenCalledWith(3);
});
});
diff --git a/spec/frontend/vue_shared/components/tooltip_on_truncate_spec.js b/spec/frontend/vue_shared/components/tooltip_on_truncate_spec.js
index 9e7e5c1263f..ca1f7996ad6 100644
--- a/spec/frontend/vue_shared/components/tooltip_on_truncate_spec.js
+++ b/spec/frontend/vue_shared/components/tooltip_on_truncate_spec.js
@@ -68,7 +68,7 @@ describe('TooltipOnTruncate component', () => {
},
);
- wrapper = parent.find(WrappedTooltipOnTruncate);
+ wrapper = parent.findComponent(WrappedTooltipOnTruncate);
};
const getTooltipValue = () => getBinding(wrapper.element, 'gl-tooltip')?.value;
diff --git a/spec/frontend/vue_shared/components/upload_dropzone/upload_dropzone_spec.js b/spec/frontend/vue_shared/components/upload_dropzone/upload_dropzone_spec.js
index 21e9b401215..a063a5591e3 100644
--- a/spec/frontend/vue_shared/components/upload_dropzone/upload_dropzone_spec.js
+++ b/spec/frontend/vue_shared/components/upload_dropzone/upload_dropzone_spec.js
@@ -14,7 +14,7 @@ describe('Upload dropzone component', () => {
const findDropzoneCard = () => wrapper.find('.upload-dropzone-card');
const findDropzoneArea = () => wrapper.find('[data-testid="dropzone-area"]');
- const findIcon = () => wrapper.find(GlIcon);
+ const findIcon = () => wrapper.findComponent(GlIcon);
const findUploadText = () => wrapper.find('[data-testid="upload-text"]').text();
const findFileInput = () => wrapper.find('input[type="file"]');
diff --git a/spec/frontend/vue_shared/components/user_access_role_badge_spec.js b/spec/frontend/vue_shared/components/user_access_role_badge_spec.js
index 7f25f7c08e7..cea6fcac8c8 100644
--- a/spec/frontend/vue_shared/components/user_access_role_badge_spec.js
+++ b/spec/frontend/vue_shared/components/user_access_role_badge_spec.js
@@ -18,7 +18,7 @@ describe('UserAccessRoleBadge', () => {
},
});
- const badge = wrapper.find(GlBadge);
+ const badge = wrapper.findComponent(GlBadge);
expect(badge.exists()).toBe(true);
expect(badge.html()).toContain('test slot content');
diff --git a/spec/frontend/vue_shared/components/user_avatar/user_avatar_image_new_spec.js b/spec/frontend/vue_shared/components/user_avatar/user_avatar_image_new_spec.js
index 5e05b54cb8c..f87737ca86a 100644
--- a/spec/frontend/vue_shared/components/user_avatar/user_avatar_image_new_spec.js
+++ b/spec/frontend/vue_shared/components/user_avatar/user_avatar_image_new_spec.js
@@ -18,6 +18,8 @@ const PROVIDED_PROPS = {
describe('User Avatar Image Component', () => {
let wrapper;
+ const findAvatar = () => wrapper.findComponent(GlAvatar);
+
afterEach(() => {
wrapper.destroy();
});
@@ -28,21 +30,14 @@ describe('User Avatar Image Component', () => {
propsData: {
...PROVIDED_PROPS,
},
- provide: {
- glFeatures: {
- glAvatarForAllUserAvatars: true,
- },
- },
});
});
it('should render `GlAvatar` and provide correct properties to it', () => {
- const avatar = wrapper.findComponent(GlAvatar);
-
- expect(avatar.attributes('data-src')).toBe(
+ expect(findAvatar().attributes('data-src')).toBe(
`${PROVIDED_PROPS.imgSrc}?width=${PROVIDED_PROPS.size}`,
);
- expect(avatar.props()).toMatchObject({
+ expect(findAvatar().props()).toMatchObject({
src: `${PROVIDED_PROPS.imgSrc}?width=${PROVIDED_PROPS.size}`,
alt: PROVIDED_PROPS.imgAlt,
size: PROVIDED_PROPS.size,
@@ -63,23 +58,28 @@ describe('User Avatar Image Component', () => {
...PROVIDED_PROPS,
lazy: true,
},
- provide: {
- glFeatures: {
- glAvatarForAllUserAvatars: true,
- },
- },
});
});
it('should add lazy attributes', () => {
- const avatar = wrapper.findComponent(GlAvatar);
-
- expect(avatar.classes()).toContain('lazy');
- expect(avatar.attributes()).toMatchObject({
+ 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', () => {
@@ -89,18 +89,11 @@ describe('User Avatar Image Component', () => {
...PROVIDED_PROPS,
imgSrc: null,
},
- provide: {
- glFeatures: {
- glAvatarForAllUserAvatars: true,
- },
- },
});
});
it('should have default avatar image', () => {
- const avatar = wrapper.findComponent(GlAvatar);
-
- expect(avatar.props('src')).toBe(`${defaultAvatarUrl}?width=${PROVIDED_PROPS.size}`);
+ expect(findAvatar().props('src')).toBe(`${defaultAvatarUrl}?width=${PROVIDED_PROPS.size}`);
});
});
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 75d2a936b34..6ad2ef226c2 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
@@ -15,47 +15,37 @@ const PROVIDED_PROPS = {
describe('User Avatar Image Component', () => {
let wrapper;
- afterEach(() => {
- wrapper.destroy();
- });
-
- describe('when `glAvatarForAllUserAvatars` feature flag enabled', () => {
- beforeEach(() => {
- wrapper = shallowMount(UserAvatarImage, {
- propsData: {
- ...PROVIDED_PROPS,
+ const createWrapper = (props = {}, { glAvatarForAllUserAvatars } = {}) => {
+ wrapper = shallowMount(UserAvatarImage, {
+ propsData: {
+ ...PROVIDED_PROPS,
+ ...props,
+ },
+ provide: {
+ glFeatures: {
+ glAvatarForAllUserAvatars,
},
- provide: {
- glFeatures: {
- glAvatarForAllUserAvatars: true,
- },
- },
- });
+ },
});
+ };
- it('should render `UserAvatarImageNew` component', () => {
- expect(wrapper.findComponent(UserAvatarImageNew).exists()).toBe(true);
- expect(wrapper.findComponent(UserAvatarImageOld).exists()).toBe(false);
- });
+ afterEach(() => {
+ wrapper.destroy();
});
- describe('when `glAvatarForAllUserAvatars` feature flag disabled', () => {
- beforeEach(() => {
- wrapper = shallowMount(UserAvatarImage, {
- propsData: {
- ...PROVIDED_PROPS,
- },
- provide: {
- glFeatures: {
- glAvatarForAllUserAvatars: false,
- },
- },
+ 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);
});
- });
-
- it('should render `UserAvatarImageOld` component', () => {
- expect(wrapper.findComponent(UserAvatarImageNew).exists()).toBe(false);
- expect(wrapper.findComponent(UserAvatarImageOld).exists()).toBe(true);
- });
- });
+ },
+ );
});
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
index 5ba80b31b99..f485a14cfea 100644
--- 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
@@ -54,6 +54,7 @@ describe('User Avatar Link Component', () => {
size: defaultProps.imgSize,
tooltipPlacement: defaultProps.tooltipPlacement,
tooltipText: '',
+ enforceGlAvatar: false,
});
});
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
index 2d513c46e77..cf7a1025dba 100644
--- 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
@@ -54,6 +54,7 @@ describe('User Avatar Link Component', () => {
size: defaultProps.imgSize,
tooltipPlacement: defaultProps.tooltipPlacement,
tooltipText: '',
+ enforceGlAvatar: false,
});
});
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 b36b83d1fea..fd3f59008ec 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
@@ -15,47 +15,37 @@ const PROVIDED_PROPS = {
describe('User Avatar Link Component', () => {
let wrapper;
- afterEach(() => {
- wrapper.destroy();
- });
-
- describe('when `glAvatarForAllUserAvatars` feature flag enabled', () => {
- beforeEach(() => {
- wrapper = shallowMount(UserAvatarLink, {
- propsData: {
- ...PROVIDED_PROPS,
+ const createWrapper = (props = {}, { glAvatarForAllUserAvatars } = {}) => {
+ wrapper = shallowMount(UserAvatarLink, {
+ propsData: {
+ ...PROVIDED_PROPS,
+ ...props,
+ },
+ provide: {
+ glFeatures: {
+ glAvatarForAllUserAvatars,
},
- provide: {
- glFeatures: {
- glAvatarForAllUserAvatars: true,
- },
- },
- });
+ },
});
+ };
- it('should render `UserAvatarLinkNew` component', () => {
- expect(wrapper.findComponent(UserAvatarLinkNew).exists()).toBe(true);
- expect(wrapper.findComponent(UserAvatarLinkOld).exists()).toBe(false);
- });
+ afterEach(() => {
+ wrapper.destroy();
});
- describe('when `glAvatarForAllUserAvatars` feature flag disabled', () => {
- beforeEach(() => {
- wrapper = shallowMount(UserAvatarLink, {
- propsData: {
- ...PROVIDED_PROPS,
- },
- provide: {
- glFeatures: {
- glAvatarForAllUserAvatars: false,
- },
- },
+ 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 `UserAvatarLinkOld` component', () => {
- expect(wrapper.findComponent(UserAvatarLinkNew).exists()).toBe(false);
- expect(wrapper.findComponent(UserAvatarLinkOld).exists()).toBe(true);
- });
- });
+ },
+ );
});
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 20ff0848cff..b9accbf0373 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
@@ -38,7 +38,7 @@ describe('UserAvatarList', () => {
};
const clickButton = () => {
- const button = wrapper.find(GlButton);
+ const button = wrapper.findComponent(GlButton);
button.vm.$emit('click');
};
@@ -79,7 +79,7 @@ describe('UserAvatarList', () => {
const items = createList(20);
factory({ propsData: { items } });
- const links = wrapper.findAll(UserAvatarLink);
+ const links = wrapper.findAllComponents(UserAvatarLink);
const linkProps = links.wrappers.map((x) => x.props());
expect(linkProps).toEqual(
@@ -105,7 +105,7 @@ describe('UserAvatarList', () => {
it('renders all avatars if length is <= breakpoint', () => {
factory();
- const links = wrapper.findAll(UserAvatarLink);
+ const links = wrapper.findAllComponents(UserAvatarLink);
expect(links.length).toEqual(props.items.length);
});
@@ -113,7 +113,7 @@ describe('UserAvatarList', () => {
it('does not show button', () => {
factory();
- expect(wrapper.find(GlButton).exists()).toBe(false);
+ expect(wrapper.findComponent(GlButton).exists()).toBe(false);
});
});
@@ -126,7 +126,7 @@ describe('UserAvatarList', () => {
it('renders avatars up to breakpoint', () => {
factory();
- const links = wrapper.findAll(UserAvatarLink);
+ const links = wrapper.findAllComponents(UserAvatarLink);
expect(links.length).toEqual(TEST_BREAKPOINT);
});
@@ -138,7 +138,7 @@ describe('UserAvatarList', () => {
});
it('renders all avatars', () => {
- const links = wrapper.findAll(UserAvatarLink);
+ const links = wrapper.findAllComponents(UserAvatarLink);
expect(links.length).toEqual(props.items.length);
});
@@ -147,7 +147,7 @@ describe('UserAvatarList', () => {
clickButton();
await nextTick();
- const links = wrapper.findAll(UserAvatarLink);
+ const links = wrapper.findAllComponents(UserAvatarLink);
expect(links.length).toEqual(TEST_BREAKPOINT);
});
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 9550368eefc..b7ce3e47cef 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
@@ -6,6 +6,7 @@ import UserPopover from '~/vue_shared/components/user_popover/user_popover.vue';
import axios from '~/lib/utils/axios_utils';
import createFlash from '~/flash';
import { followUser, unfollowUser } from '~/api/user_api';
+import { mockTracking } from 'helpers/tracking_helper';
jest.mock('~/flash');
jest.mock('~/api/user_api', () => ({
@@ -51,6 +52,18 @@ describe('User Popover Component', () => {
const findUserLocalTime = () => wrapper.findByTestId('user-popover-local-time');
const findToggleFollowButton = () => wrapper.findByTestId('toggle-follow-button');
+ const itTracksToggleFollowButtonClick = (expectedLabel) => {
+ it('tracks click', async () => {
+ const trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
+
+ await findToggleFollowButton().trigger('click');
+
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_button', {
+ label: expectedLabel,
+ });
+ });
+ };
+
const createWrapper = (props = {}) => {
wrapper = mountExtended(UserPopover, {
propsData: {
@@ -75,7 +88,7 @@ describe('User Popover Component', () => {
},
});
- expect(wrapper.find(GlSkeletonLoader).exists()).toBe(true);
+ expect(wrapper.findComponent(GlSkeletonLoader).exists()).toBe(true);
});
});
@@ -89,7 +102,7 @@ describe('User Popover Component', () => {
it('shows icon for location', () => {
createWrapper();
- const iconEl = wrapper.find(GlIcon);
+ const iconEl = wrapper.findComponent(GlIcon);
expect(iconEl.props('name')).toEqual('location');
});
@@ -102,8 +115,8 @@ describe('User Popover Component', () => {
});
describe('job data', () => {
- const findWorkInformation = () => wrapper.find({ ref: 'workInformation' });
- const findBio = () => wrapper.find({ ref: 'bio' });
+ const findWorkInformation = () => wrapper.findComponent({ ref: 'workInformation' });
+ const findBio = () => wrapper.findComponent({ ref: 'bio' });
const bio = 'My super interesting bio';
it('should show only bio if work information is not available', () => {
@@ -159,7 +172,7 @@ describe('User Popover Component', () => {
createWrapper({ user });
expect(
- wrapper.findAll(GlIcon).filter((icon) => icon.props('name') === 'profile').length,
+ wrapper.findAllComponents(GlIcon).filter((icon) => icon.props('name') === 'profile').length,
).toEqual(1);
});
@@ -172,7 +185,7 @@ describe('User Popover Component', () => {
createWrapper({ user });
expect(
- wrapper.findAll(GlIcon).filter((icon) => icon.props('name') === 'work').length,
+ wrapper.findAllComponents(GlIcon).filter((icon) => icon.props('name') === 'work').length,
).toEqual(1);
});
});
@@ -338,9 +351,11 @@ describe('User Popover Component', () => {
await axios.waitForAll();
expect(wrapper.emitted().follow.length).toBe(1);
- expect(wrapper.emitted().unfollow).toBeFalsy();
+ expect(wrapper.emitted().unfollow).toBeUndefined();
});
+ itTracksToggleFollowButtonClick('follow_from_user_popover');
+
describe('when an error occurs', () => {
beforeEach(() => {
followUser.mockRejectedValue({});
@@ -361,8 +376,8 @@ describe('User Popover Component', () => {
it('emits no events', async () => {
await axios.waitForAll();
- expect(wrapper.emitted().follow).toBe(undefined);
- expect(wrapper.emitted().unfollow).toBe(undefined);
+ expect(wrapper.emitted().follow).toBeUndefined();
+ expect(wrapper.emitted().unfollow).toBeUndefined();
});
});
});
@@ -388,6 +403,8 @@ describe('User Popover Component', () => {
expect(wrapper.emitted().unfollow.length).toBe(1);
});
+ itTracksToggleFollowButtonClick('unfollow_from_user_popover');
+
describe('when an error occurs', () => {
beforeEach(async () => {
unfollowUser.mockRejectedValue({});
@@ -406,8 +423,8 @@ describe('User Popover Component', () => {
});
it('emits no events', () => {
- expect(wrapper.emitted().follow).toBe(undefined);
- expect(wrapper.emitted().unfollow).toBe(undefined);
+ expect(wrapper.emitted().follow).toBeUndefined();
+ expect(wrapper.emitted().unfollow).toBeUndefined();
});
});
});
diff --git a/spec/frontend/vue_shared/components/user_select_spec.js b/spec/frontend/vue_shared/components/user_select_spec.js
index ec9128d5e38..4188adc72a1 100644
--- a/spec/frontend/vue_shared/components/user_select_spec.js
+++ b/spec/frontend/vue_shared/components/user_select_spec.js
@@ -9,6 +9,7 @@ import searchUsersQuery from '~/graphql_shared/queries/users_search.query.graphq
import searchUsersQueryOnMR from '~/graphql_shared/queries/users_search_with_mr_permissions.graphql';
import { IssuableType } from '~/issues/constants';
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
+import SidebarParticipant from '~/sidebar/components/assignees/sidebar_participant.vue';
import getIssueParticipantsQuery from '~/vue_shared/components/sidebar/queries/get_issue_participants.query.graphql';
import UserSelect from '~/vue_shared/components/user_select/user_select.vue';
import {
@@ -16,6 +17,8 @@ import {
searchResponseOnMR,
projectMembersResponse,
participantsQueryResponse,
+ mockUser1,
+ mockUser2,
} from 'jest/sidebar/mock_data';
const assignee = {
@@ -45,9 +48,14 @@ describe('User select dropdown', () => {
const findSearchField = () => wrapper.findComponent(GlSearchBoxByType);
const findParticipantsLoading = () => wrapper.find('[data-testid="loading-participants"]');
const findSelectedParticipants = () => wrapper.findAll('[data-testid="selected-participant"]');
+ const findSelectedParticipantByIndex = (index) =>
+ findSelectedParticipants().at(index).findComponent(SidebarParticipant);
const findUnselectedParticipants = () =>
wrapper.findAll('[data-testid="unselected-participant"]');
+ const findUnselectedParticipantByIndex = (index) =>
+ findUnselectedParticipants().at(index).findComponent(SidebarParticipant);
const findCurrentUser = () => wrapper.findAll('[data-testid="current-user"]');
+ const findIssuableAuthor = () => wrapper.findAll('[data-testid="issuable-author"]');
const findUnassignLink = () => wrapper.find('[data-testid="unassign"]');
const findEmptySearchResults = () => wrapper.find('[data-testid="empty-results"]');
@@ -136,6 +144,93 @@ describe('User select dropdown', () => {
expect(findCurrentUser().exists()).toBe(true);
});
+ it('does not render current user if user is not logged in', async () => {
+ createComponent({
+ props: {
+ currentUser: {},
+ },
+ });
+ await waitForPromises();
+
+ expect(findCurrentUser().exists()).toBe(false);
+ });
+
+ it('does not render issuable author if author is not passed as a prop', async () => {
+ createComponent();
+ await waitForPromises();
+
+ expect(findIssuableAuthor().exists()).toBe(false);
+ });
+
+ describe('when issuable author is passed as a prop', () => {
+ it('moves issuable author on top of assigned list, if author is assigned', async () => {
+ createComponent({
+ props: {
+ value: [assignee, mockUser2],
+ issuableAuthor: mockUser2,
+ },
+ });
+ await waitForPromises();
+
+ expect(findSelectedParticipantByIndex(0).props('user')).toEqual(mockUser2);
+ });
+
+ it('moves issuable author on top of assigned list after current user, if author and current user are assigned', async () => {
+ const currentUser = mockUser1;
+ const issuableAuthor = mockUser2;
+
+ createComponent({
+ props: {
+ value: [assignee, issuableAuthor, currentUser],
+ issuableAuthor,
+ currentUser,
+ },
+ });
+ await waitForPromises();
+
+ expect(findSelectedParticipantByIndex(0).props('user')).toEqual(currentUser);
+ expect(findSelectedParticipantByIndex(1).props('user')).toEqual(issuableAuthor);
+ });
+
+ it('moves issuable author on top of unassigned list, if author is unassigned project member', async () => {
+ createComponent({
+ props: {
+ issuableAuthor: mockUser2,
+ },
+ });
+ await waitForPromises();
+
+ expect(findUnselectedParticipantByIndex(0).props('user')).toEqual(mockUser2);
+ });
+
+ it('moves issuable author on top of unassigned list after current user, if author and current user are unassigned project members', async () => {
+ const currentUser = mockUser2;
+ const issuableAuthor = mockUser1;
+
+ createComponent({
+ props: {
+ issuableAuthor,
+ currentUser,
+ },
+ });
+ await waitForPromises();
+
+ expect(findUnselectedParticipantByIndex(0).props('user')).toEqual(currentUser);
+ expect(findUnselectedParticipantByIndex(1).props('user')).toMatchObject(issuableAuthor);
+ });
+
+ it('displays author in a designated position if author is not assigned and not a project member', async () => {
+ createComponent({
+ props: {
+ issuableAuthor: assignee,
+ },
+ });
+ await waitForPromises();
+
+ expect(findIssuableAuthor().exists()).toBe(true);
+ });
+ });
+
it('displays correct amount of selected users', async () => {
createComponent({
props: {
diff --git a/spec/frontend/vue_shared/components/web_ide_link_spec.js b/spec/frontend/vue_shared/components/web_ide_link_spec.js
index 040461f6be4..a0b868d1d52 100644
--- a/spec/frontend/vue_shared/components/web_ide_link_spec.js
+++ b/spec/frontend/vue_shared/components/web_ide_link_spec.js
@@ -3,7 +3,7 @@ import { nextTick } from 'vue';
import ActionsButton from '~/vue_shared/components/actions_button.vue';
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
-import WebIdeLink from '~/vue_shared/components/web_ide_link.vue';
+import WebIdeLink, { i18n } from '~/vue_shared/components/web_ide_link.vue';
import ConfirmForkModal from '~/vue_shared/components/confirm_fork_modal.vue';
import { stubComponent } from 'helpers/stub_component';
@@ -37,8 +37,8 @@ const ACTION_EDIT_CONFIRM_FORK = {
const ACTION_WEB_IDE = {
href: TEST_WEB_IDE_URL,
key: 'webide',
- secondaryText: 'Quickly and easily edit multiple files in your project.',
- tooltip: '',
+ secondaryText: i18n.webIdeText,
+ tooltip: i18n.webIdeTooltip,
text: 'Web IDE',
attrs: {
'data-qa-selector': 'web_ide_button',
@@ -108,8 +108,8 @@ describe('Web IDE link component', () => {
wrapper.destroy();
});
- const findActionsButton = () => wrapper.find(ActionsButton);
- const findLocalStorageSync = () => wrapper.find(LocalStorageSync);
+ const findActionsButton = () => wrapper.findComponent(ActionsButton);
+ const findLocalStorageSync = () => wrapper.findComponent(LocalStorageSync);
const findModal = () => wrapper.findComponent(GlModal);
const findForkConfirmModal = () => wrapper.findComponent(ConfirmForkModal);
diff --git a/spec/frontend/vue_shared/issuable/create/components/issuable_create_root_spec.js b/spec/frontend/vue_shared/issuable/create/components/issuable_create_root_spec.js
index 81362edaf37..7b0f0f7e344 100644
--- a/spec/frontend/vue_shared/issuable/create/components/issuable_create_root_spec.js
+++ b/spec/frontend/vue_shared/issuable/create/components/issuable_create_root_spec.js
@@ -51,11 +51,11 @@ describe('IssuableCreateRoot', () => {
});
it('renders issuable-form component', () => {
- expect(wrapper.find(IssuableForm).exists()).toBe(true);
+ expect(wrapper.findComponent(IssuableForm).exists()).toBe(true);
});
it('renders contents for slot "actions" within issuable-form component', () => {
- const buttonEl = wrapper.find(IssuableForm).find('button.js-issuable-save');
+ const buttonEl = wrapper.findComponent(IssuableForm).find('button.js-issuable-save');
expect(buttonEl.exists()).toBe(true);
expect(buttonEl.text()).toBe('Submit issuable');
diff --git a/spec/frontend/vue_shared/issuable/create/components/issuable_form_spec.js b/spec/frontend/vue_shared/issuable/create/components/issuable_form_spec.js
index cbfd05e7903..f98e7a678f4 100644
--- a/spec/frontend/vue_shared/issuable/create/components/issuable_form_spec.js
+++ b/spec/frontend/vue_shared/issuable/create/components/issuable_form_spec.js
@@ -65,9 +65,9 @@ describe('IssuableForm', () => {
expect(titleFieldEl.exists()).toBe(true);
expect(titleFieldEl.find('label').text()).toBe('Title');
- expect(titleFieldEl.find(GlFormInput).exists()).toBe(true);
- expect(titleFieldEl.find(GlFormInput).attributes('placeholder')).toBe('Title');
- expect(titleFieldEl.find(GlFormInput).attributes('autofocus')).toBe('true');
+ expect(titleFieldEl.findComponent(GlFormInput).exists()).toBe(true);
+ expect(titleFieldEl.findComponent(GlFormInput).attributes('placeholder')).toBe('Title');
+ expect(titleFieldEl.findComponent(GlFormInput).attributes('autofocus')).toBe('true');
});
it('renders issuable description input field', () => {
@@ -75,8 +75,8 @@ describe('IssuableForm', () => {
expect(descriptionFieldEl.exists()).toBe(true);
expect(descriptionFieldEl.find('label').text()).toBe('Description');
- expect(descriptionFieldEl.find(MarkdownField).exists()).toBe(true);
- expect(descriptionFieldEl.find(MarkdownField).props()).toMatchObject({
+ expect(descriptionFieldEl.findComponent(MarkdownField).exists()).toBe(true);
+ expect(descriptionFieldEl.findComponent(MarkdownField).props()).toMatchObject({
markdownPreviewPath: wrapper.vm.descriptionPreviewPath,
markdownDocsPath: wrapper.vm.descriptionHelpPath,
addSpacingClasses: false,
@@ -94,8 +94,8 @@ describe('IssuableForm', () => {
expect(labelsSelectEl.exists()).toBe(true);
expect(labelsSelectEl.find('label').text()).toBe('Labels');
- expect(labelsSelectEl.find(LabelsSelect).exists()).toBe(true);
- expect(labelsSelectEl.find(LabelsSelect).props()).toMatchObject({
+ expect(labelsSelectEl.findComponent(LabelsSelect).exists()).toBe(true);
+ expect(labelsSelectEl.findComponent(LabelsSelect).props()).toMatchObject({
allowLabelEdit: true,
allowLabelCreate: true,
allowMultiselect: true,
diff --git a/spec/frontend/vue_shared/issuable/list/components/issuable_item_spec.js b/spec/frontend/vue_shared/issuable/list/components/issuable_item_spec.js
index 80f14dffd08..f55d3156581 100644
--- a/spec/frontend/vue_shared/issuable/list/components/issuable_item_spec.js
+++ b/spec/frontend/vue_shared/issuable/list/components/issuable_item_spec.js
@@ -3,6 +3,7 @@ import { nextTick } from 'vue';
import { useFakeDate } from 'helpers/fake_date';
import { shallowMountExtended as shallowMount } from 'helpers/vue_test_utils_helper';
import IssuableItem from '~/vue_shared/issuable/list/components/issuable_item.vue';
+import WorkItemTypeIcon from '~/work_items/components/work_item_type_icon.vue';
import IssuableAssignees from '~/issuable/components/issue_assignees.vue';
import { mockIssuable, mockRegularLabel } from '../mock_data';
@@ -13,6 +14,7 @@ const createComponent = ({
issuable = mockIssuable,
showCheckbox = true,
slots = {},
+ showWorkItemTypeIcon = false,
} = {}) =>
shallowMount(IssuableItem, {
propsData: {
@@ -21,6 +23,7 @@ const createComponent = ({
issuable,
showDiscussions: true,
showCheckbox,
+ showWorkItemTypeIcon,
},
slots,
stubs: {
@@ -40,6 +43,7 @@ describe('IssuableItem', () => {
let wrapper;
const findTimestampWrapper = () => wrapper.find('[data-testid="issuable-timestamp"]');
+ const findWorkItemTypeIcon = () => wrapper.findComponent(WorkItemTypeIcon);
beforeEach(() => {
gon.gitlab_url = MOCK_GITLAB_URL;
@@ -273,9 +277,9 @@ describe('IssuableItem', () => {
const titleEl = wrapper.find('[data-testid="issuable-title"]');
expect(titleEl.exists()).toBe(true);
- expect(titleEl.find(GlLink).attributes('href')).toBe(expectedHref);
- expect(titleEl.find(GlLink).attributes('target')).toBe(expectedTarget);
- expect(titleEl.find(GlLink).text()).toBe(mockIssuable.title);
+ expect(titleEl.findComponent(GlLink).attributes('href')).toBe(expectedHref);
+ expect(titleEl.findComponent(GlLink).attributes('target')).toBe(expectedTarget);
+ expect(titleEl.findComponent(GlLink).text()).toBe(mockIssuable.title);
},
);
@@ -286,8 +290,8 @@ describe('IssuableItem', () => {
await nextTick();
- expect(wrapper.find(GlFormCheckbox).exists()).toBe(true);
- expect(wrapper.find(GlFormCheckbox).attributes('checked')).not.toBeDefined();
+ expect(wrapper.findComponent(GlFormCheckbox).exists()).toBe(true);
+ expect(wrapper.findComponent(GlFormCheckbox).attributes('checked')).not.toBeDefined();
wrapper.setProps({
checked: true,
@@ -295,7 +299,7 @@ describe('IssuableItem', () => {
await nextTick();
- expect(wrapper.find(GlFormCheckbox).attributes('checked')).toBe('true');
+ expect(wrapper.findComponent(GlFormCheckbox).attributes('checked')).toBe('true');
});
it('renders issuable title with `target` set as "_blank" when issuable.webUrl is external', async () => {
@@ -308,9 +312,9 @@ describe('IssuableItem', () => {
await nextTick();
- expect(wrapper.find('[data-testid="issuable-title"]').find(GlLink).attributes('target')).toBe(
- '_blank',
- );
+ expect(
+ wrapper.find('[data-testid="issuable-title"]').findComponent(GlLink).attributes('target'),
+ ).toBe('_blank');
});
it('renders issuable confidential icon when issuable is confidential', async () => {
@@ -323,7 +327,7 @@ describe('IssuableItem', () => {
await nextTick();
- const confidentialEl = wrapper.find('[data-testid="issuable-title"]').find(GlIcon);
+ const confidentialEl = wrapper.find('[data-testid="issuable-title"]').findComponent(GlIcon);
expect(confidentialEl.exists()).toBe(true);
expect(confidentialEl.props('name')).toBe('eye-slash');
@@ -349,11 +353,23 @@ describe('IssuableItem', () => {
wrapper = createComponent();
const taskStatus = wrapper.find('[data-testid="task-status"]');
- const expected = `${mockIssuable.taskCompletionStatus.completedCount} of ${mockIssuable.taskCompletionStatus.count} tasks completed`;
+ const expected = `${mockIssuable.taskCompletionStatus.completedCount} of ${mockIssuable.taskCompletionStatus.count} checklist items completed`;
expect(taskStatus.text()).toBe(expected);
});
+ it('does not renders work item type icon by default', () => {
+ wrapper = createComponent();
+
+ expect(findWorkItemTypeIcon().exists()).toBe(false);
+ });
+
+ it('renders work item type icon when props passed', () => {
+ wrapper = createComponent({ showWorkItemTypeIcon: true });
+
+ expect(findWorkItemTypeIcon().props('workItemType')).toBe(mockIssuable.type);
+ });
+
it('renders issuable reference', () => {
wrapper = createComponent();
@@ -440,7 +456,7 @@ describe('IssuableItem', () => {
it('renders gl-label component for each label present within `issuable` prop', () => {
wrapper = createComponent();
- const labelsEl = wrapper.findAll(GlLabel);
+ const labelsEl = wrapper.findAllComponents(GlLabel);
expect(labelsEl.exists()).toBe(true);
expect(labelsEl).toHaveLength(mockLabels.length);
@@ -476,18 +492,18 @@ describe('IssuableItem', () => {
const discussionsEl = wrapper.find('[data-testid="issuable-discussions"]');
expect(discussionsEl.exists()).toBe(true);
- expect(discussionsEl.find(GlLink).attributes()).toMatchObject({
+ expect(discussionsEl.findComponent(GlLink).attributes()).toMatchObject({
title: 'Comments',
href: `${mockIssuable.webUrl}#notes`,
});
- expect(discussionsEl.find(GlIcon).props('name')).toBe('comments');
- expect(discussionsEl.find(GlLink).text()).toContain('2');
+ expect(discussionsEl.findComponent(GlIcon).props('name')).toBe('comments');
+ expect(discussionsEl.findComponent(GlLink).text()).toContain('2');
});
it('renders issuable-assignees component', () => {
wrapper = createComponent();
- const assigneesEl = wrapper.find(IssuableAssignees);
+ const assigneesEl = wrapper.findComponent(IssuableAssignees);
expect(assigneesEl.exists()).toBe(true);
expect(assigneesEl.props()).toMatchObject({
diff --git a/spec/frontend/vue_shared/issuable/list/components/issuable_list_root_spec.js b/spec/frontend/vue_shared/issuable/list/components/issuable_list_root_spec.js
index 50e79dbe589..0c53f599d55 100644
--- a/spec/frontend/vue_shared/issuable/list/components/issuable_list_root_spec.js
+++ b/spec/frontend/vue_shared/issuable/list/components/issuable_list_root_spec.js
@@ -359,7 +359,7 @@ describe('IssuableListRoot', () => {
findIssuableTabs().vm.$emit('click');
- expect(wrapper.emitted('click-tab')).toBeTruthy();
+ expect(wrapper.emitted('click-tab')).toHaveLength(1);
});
it('sets all issuables as checked when filtered-search-bar component emits `checked-input` event', () => {
@@ -369,7 +369,7 @@ describe('IssuableListRoot', () => {
searchEl.vm.$emit('checked-input', true);
- expect(searchEl.emitted('checked-input')).toBeTruthy();
+ expect(searchEl.emitted('checked-input')).toHaveLength(1);
expect(searchEl.emitted('checked-input').length).toBe(1);
expect(wrapper.vm.checkedIssuables[mockIssuables[0].iid]).toEqual({
@@ -384,9 +384,9 @@ describe('IssuableListRoot', () => {
const searchEl = findFilteredSearchBar();
searchEl.vm.$emit('onFilter');
- expect(wrapper.emitted('filter')).toBeTruthy();
+ expect(wrapper.emitted('filter')).toHaveLength(1);
searchEl.vm.$emit('onSort');
- expect(wrapper.emitted('sort')).toBeTruthy();
+ expect(wrapper.emitted('sort')).toHaveLength(1);
});
it('sets an issuable as checked when issuable-item component emits `checked-input` event', () => {
@@ -396,7 +396,7 @@ describe('IssuableListRoot', () => {
issuableItem.vm.$emit('checked-input', true);
- expect(issuableItem.emitted('checked-input')).toBeTruthy();
+ expect(issuableItem.emitted('checked-input')).toHaveLength(1);
expect(issuableItem.emitted('checked-input').length).toBe(1);
expect(wrapper.vm.checkedIssuables[mockIssuables[0].iid]).toEqual({
@@ -425,7 +425,7 @@ describe('IssuableListRoot', () => {
wrapper = createComponent({ data, props: { showPaginationControls: true } });
findGlPagination().vm.$emit('input');
- expect(wrapper.emitted('page-change')).toBeTruthy();
+ expect(wrapper.emitted('page-change')).toHaveLength(1);
});
it.each`
diff --git a/spec/frontend/vue_shared/issuable/list/mock_data.js b/spec/frontend/vue_shared/issuable/list/mock_data.js
index 8640f4a2cd5..b67bd0f42fe 100644
--- a/spec/frontend/vue_shared/issuable/list/mock_data.js
+++ b/spec/frontend/vue_shared/issuable/list/mock_data.js
@@ -57,6 +57,7 @@ export const mockIssuable = {
count: 2,
completedCount: 1,
},
+ type: 'issue',
};
export const mockIssuables = [
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 7c582360637..39a76a51191 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
@@ -154,7 +154,7 @@ describe('IssuableBody', () => {
describe('template', () => {
it('renders issuable-title component', () => {
- const titleEl = wrapper.find(IssuableTitle);
+ const titleEl = wrapper.findComponent(IssuableTitle);
expect(titleEl.exists()).toBe(true);
expect(titleEl.props()).toMatchObject({
@@ -165,7 +165,7 @@ describe('IssuableBody', () => {
});
it('renders issuable-description component', () => {
- const descriptionEl = wrapper.find(IssuableDescription);
+ const descriptionEl = wrapper.findComponent(IssuableDescription);
expect(descriptionEl.exists()).toBe(true);
expect(descriptionEl.props('issuable')).toEqual(issuableBodyProps.issuable);
@@ -184,7 +184,7 @@ describe('IssuableBody', () => {
await nextTick();
- const editFormEl = wrapper.find(IssuableEditForm);
+ const editFormEl = wrapper.findComponent(IssuableEditForm);
expect(editFormEl.exists()).toBe(true);
expect(editFormEl.props()).toMatchObject({
issuable: issuableBodyProps.issuable,
@@ -198,7 +198,7 @@ describe('IssuableBody', () => {
describe('events', () => {
it('component emits `edit-issuable` event bubbled via issuable-title', () => {
- const issuableTitle = wrapper.find(IssuableTitle);
+ const issuableTitle = wrapper.findComponent(IssuableTitle);
issuableTitle.vm.$emit('edit-issuable');
@@ -223,7 +223,7 @@ describe('IssuableBody', () => {
await nextTick();
- const issuableEditForm = wrapper.find(IssuableEditForm);
+ const issuableEditForm = wrapper.findComponent(IssuableEditForm);
issuableEditForm.vm.$emit(eventName, eventObj, issuableMeta);
diff --git a/spec/frontend/vue_shared/issuable/show/components/issuable_edit_form_spec.js b/spec/frontend/vue_shared/issuable/show/components/issuable_edit_form_spec.js
index d3e484cf913..d843da4da5b 100644
--- a/spec/frontend/vue_shared/issuable/show/components/issuable_edit_form_spec.js
+++ b/spec/frontend/vue_shared/issuable/show/components/issuable_edit_form_spec.js
@@ -124,7 +124,7 @@ describe('IssuableEditForm', () => {
const titleInputEl = wrapper.find('[data-testid="title"]');
expect(titleInputEl.exists()).toBe(true);
- expect(titleInputEl.find(GlFormInput).attributes()).toMatchObject({
+ expect(titleInputEl.findComponent(GlFormInput).attributes()).toMatchObject({
'aria-label': 'Title',
placeholder: 'Title',
});
@@ -134,7 +134,7 @@ describe('IssuableEditForm', () => {
const descriptionEl = wrapper.find('[data-testid="description"]');
expect(descriptionEl.exists()).toBe(true);
- expect(descriptionEl.find(MarkdownField).props()).toMatchObject({
+ expect(descriptionEl.findComponent(MarkdownField).props()).toMatchObject({
markdownPreviewPath: issuableEditFormProps.descriptionPreviewPath,
markdownDocsPath: issuableEditFormProps.descriptionHelpPath,
enableAutocomplete: issuableEditFormProps.enableAutocomplete,
@@ -161,7 +161,7 @@ describe('IssuableEditForm', () => {
};
it('component emits `keydown-title` event with event object and issuableMeta params via gl-form-input', async () => {
- const titleInputEl = wrapper.find(GlFormInput);
+ const titleInputEl = wrapper.findComponent(GlFormInput);
titleInputEl.vm.$emit('keydown', eventObj, 'title');
diff --git a/spec/frontend/vue_shared/issuable/show/components/issuable_header_spec.js b/spec/frontend/vue_shared/issuable/show/components/issuable_header_spec.js
index e00bb184535..6a8b9ef77a9 100644
--- a/spec/frontend/vue_shared/issuable/show/components/issuable_header_spec.js
+++ b/spec/frontend/vue_shared/issuable/show/components/issuable_header_spec.js
@@ -86,7 +86,7 @@ describe('IssuableHeader', () => {
const blockedEl = wrapper.findByTestId('blocked');
expect(blockedEl.exists()).toBe(true);
- expect(blockedEl.find(GlIcon).props('name')).toBe('lock');
+ expect(blockedEl.findComponent(GlIcon).props('name')).toBe('lock');
});
it('renders confidential icon when issuable is confidential', async () => {
@@ -97,7 +97,7 @@ describe('IssuableHeader', () => {
const confidentialEl = wrapper.findByTestId('confidential');
expect(confidentialEl.exists()).toBe(true);
- expect(confidentialEl.find(GlIcon).props('name')).toBe('eye-slash');
+ expect(confidentialEl.findComponent(GlIcon).props('name')).toBe('eye-slash');
});
it('renders issuable author avatar', () => {
@@ -113,19 +113,19 @@ describe('IssuableHeader', () => {
const avatarEl = wrapper.findByTestId('avatar');
expect(avatarEl.exists()).toBe(true);
expect(avatarEl.attributes()).toMatchObject(avatarElAttrs);
- expect(avatarEl.find(GlAvatarLabeled).attributes()).toMatchObject({
+ expect(avatarEl.findComponent(GlAvatarLabeled).attributes()).toMatchObject({
size: '24',
src: avatarUrl,
label: name,
});
- expect(avatarEl.find(GlAvatarLabeled).find(GlIcon).exists()).toBe(false);
+ expect(avatarEl.findComponent(GlAvatarLabeled).findComponent(GlIcon).exists()).toBe(false);
});
it('renders task status text when `taskCompletionStatus` prop is defined', () => {
createComponent();
expect(findTaskStatusEl().exists()).toBe(true);
- expect(findTaskStatusEl().text()).toContain('0 of 5 tasks completed');
+ expect(findTaskStatusEl().text()).toContain('0 of 5 checklist items completed');
});
it('does not render task status text when tasks count is 0', () => {
@@ -172,7 +172,7 @@ describe('IssuableHeader', () => {
);
const avatarEl = wrapper.findComponent(GlAvatarLabeled);
- const icon = avatarEl.find(GlIcon);
+ const icon = avatarEl.findComponent(GlIcon);
expect(icon.exists()).toBe(true);
expect(icon.props('name')).toBe('external-link');
diff --git a/spec/frontend/vue_shared/issuable/show/components/issuable_show_root_spec.js b/spec/frontend/vue_shared/issuable/show/components/issuable_show_root_spec.js
index f56064ed8e1..edfd55c8bb4 100644
--- a/spec/frontend/vue_shared/issuable/show/components/issuable_show_root_spec.js
+++ b/spec/frontend/vue_shared/issuable/show/components/issuable_show_root_spec.js
@@ -63,7 +63,7 @@ describe('IssuableShowRoot', () => {
});
it('renders issuable-header component', () => {
- const issuableHeader = wrapper.find(IssuableHeader);
+ const issuableHeader = wrapper.findComponent(IssuableHeader);
expect(issuableHeader.exists()).toBe(true);
expect(issuableHeader.props()).toMatchObject({
@@ -84,7 +84,7 @@ describe('IssuableShowRoot', () => {
});
it('renders issuable-body component', () => {
- const issuableBody = wrapper.find(IssuableBody);
+ const issuableBody = wrapper.findComponent(IssuableBody);
expect(issuableBody.exists()).toBe(true);
expect(issuableBody.props()).toMatchObject({
@@ -99,38 +99,38 @@ describe('IssuableShowRoot', () => {
});
it('renders issuable-sidebar component', () => {
- const issuableSidebar = wrapper.find(IssuableSidebar);
+ const issuableSidebar = wrapper.findComponent(IssuableSidebar);
expect(issuableSidebar.exists()).toBe(true);
});
describe('events', () => {
it('component emits `edit-issuable` event bubbled via issuable-body', () => {
- const issuableBody = wrapper.find(IssuableBody);
+ const issuableBody = wrapper.findComponent(IssuableBody);
issuableBody.vm.$emit('edit-issuable');
- expect(wrapper.emitted('edit-issuable')).toBeTruthy();
+ expect(wrapper.emitted('edit-issuable')).toHaveLength(1);
});
it('component emits `task-list-update-success` event bubbled via issuable-body', () => {
- const issuableBody = wrapper.find(IssuableBody);
+ const issuableBody = wrapper.findComponent(IssuableBody);
const eventParam = {
foo: 'bar',
};
issuableBody.vm.$emit('task-list-update-success', eventParam);
- 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([eventParam]);
});
it('component emits `task-list-update-failure` event bubbled via issuable-body', () => {
- const issuableBody = wrapper.find(IssuableBody);
+ const issuableBody = wrapper.findComponent(IssuableBody);
issuableBody.vm.$emit('task-list-update-failure');
- expect(wrapper.emitted('task-list-update-failure')).toBeTruthy();
+ expect(wrapper.emitted('task-list-update-failure')).toHaveLength(1);
});
it.each(['keydown-title', 'keydown-description'])(
@@ -145,11 +145,11 @@ describe('IssuableShowRoot', () => {
issuableDescription: 'foobar',
};
- const issuableBody = wrapper.find(IssuableBody);
+ const issuableBody = wrapper.findComponent(IssuableBody);
issuableBody.vm.$emit(eventName, eventObj, issuableMeta);
- expect(wrapper.emitted(eventName)).toBeTruthy();
+ expect(wrapper.emitted()).toHaveProperty(eventName);
expect(wrapper.emitted(eventName)[0]).toMatchObject([eventObj, issuableMeta]);
},
);
diff --git a/spec/frontend/vue_shared/security_reports/components/security_report_download_dropdown_spec.js b/spec/frontend/vue_shared/security_reports/components/security_report_download_dropdown_spec.js
index 4b75da0b126..5f2b13a79c9 100644
--- a/spec/frontend/vue_shared/security_reports/components/security_report_download_dropdown_spec.js
+++ b/spec/frontend/vue_shared/security_reports/components/security_report_download_dropdown_spec.js
@@ -12,8 +12,8 @@ describe('SecurityReportDownloadDropdown component', () => {
});
};
- const findDropdown = () => wrapper.find(GlDropdown);
- const findDropdownItems = () => wrapper.findAll(GlDropdownItem);
+ const findDropdown = () => wrapper.findComponent(GlDropdown);
+ const findDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
afterEach(() => {
wrapper.destroy();
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 68a97103d3a..a9651cf8bac 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
@@ -70,8 +70,8 @@ describe('Security reports app', () => {
return createMockApollo(requestHandlers);
};
- const findDownloadDropdown = () => wrapper.find(SecurityReportDownloadDropdown);
- const findHelpIconComponent = () => wrapper.find(HelpIcon);
+ const findDownloadDropdown = () => wrapper.findComponent(SecurityReportDownloadDropdown);
+ const findHelpIconComponent = () => wrapper.findComponent(HelpIcon);
afterEach(() => {
wrapper.destroy();
diff --git a/spec/frontend/whats_new/components/app_spec.js b/spec/frontend/whats_new/components/app_spec.js
index 945727cd664..de5a814d3e7 100644
--- a/spec/frontend/whats_new/components/app_spec.js
+++ b/spec/frontend/whats_new/components/app_spec.js
@@ -64,7 +64,7 @@ describe('App', () => {
buildWrapper();
wrapper.vm.$store.state.features = [
- { title: 'Whats New Drawer', url: 'www.url.com', release: 3.11 },
+ { name: 'Whats New Drawer', documentation_link: 'www.url.com', release: 3.11 },
];
wrapper.vm.$store.state.drawerBodyHeight = MOCK_DRAWER_BODY_HEIGHT;
await nextTick();
@@ -115,7 +115,7 @@ describe('App', () => {
it('renders features when provided via ajax', () => {
expect(actions.fetchItems).toHaveBeenCalled();
- expect(wrapper.find('[data-test-id="feature-title"]').text()).toBe('Whats New Drawer');
+ expect(wrapper.find('[data-test-id="feature-name"]').text()).toBe('Whats New Drawer');
});
it('send an event when feature item is clicked', () => {
diff --git a/spec/frontend/whats_new/components/feature_spec.js b/spec/frontend/whats_new/components/feature_spec.js
index b6627c257ff..099054bf8ca 100644
--- a/spec/frontend/whats_new/components/feature_spec.js
+++ b/spec/frontend/whats_new/components/feature_spec.js
@@ -6,14 +6,15 @@ describe("What's new single feature", () => {
let wrapper;
const exampleFeature = {
- title: 'Compliance pipeline configurations',
- body:
+ name: 'Compliance pipeline configurations',
+ description:
'<p data-testid="body-content">We are thrilled to announce that it is now possible to define enforceable pipelines that will run for any project assigned a corresponding <a href="https://en.wikipedia.org/wiki/Compliance_(psychology)" target="_blank" rel="noopener noreferrer" onload="alert(xss)">compliance</a> framework.</p>',
stage: 'Manage',
'self-managed': true,
'gitlab-com': true,
- packages: ['Ultimate'],
- url: 'https://docs.gitlab.com/ee/user/project/settings/#compliance-pipeline-configuration',
+ available_in: ['Ultimate'],
+ documentation_link:
+ 'https://docs.gitlab.com/ee/user/project/settings/#compliance-pipeline-configuration',
image_url: 'https://img.youtube.com/vi/upLJ_equomw/hqdefault.jpg',
published_at: '2021-04-22T00:00:00.000Z',
release: '13.11',
diff --git a/spec/frontend/work_items/components/item_state_spec.js b/spec/frontend/work_items/components/item_state_spec.js
index 79b76f3c061..c3cc2fbc556 100644
--- a/spec/frontend/work_items/components/item_state_spec.js
+++ b/spec/frontend/work_items/components/item_state_spec.js
@@ -1,3 +1,4 @@
+import { GlFormSelect } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import { STATE_OPEN, STATE_CLOSED } from '~/work_items/constants';
import ItemState from '~/work_items/components/item_state.vue';
@@ -6,6 +7,7 @@ describe('ItemState', () => {
let wrapper;
const findLabel = () => wrapper.find('label').text();
+ const findFormSelect = () => wrapper.findComponent(GlFormSelect);
const selectedValue = () => wrapper.find('option:checked').element.value;
const clickOpen = () => wrapper.findAll('option').at(0).setSelected();
@@ -51,4 +53,18 @@ describe('ItemState', () => {
expect(wrapper.emitted('changed')).toBeUndefined();
});
+
+ describe('form select disabled prop', () => {
+ describe.each`
+ description | disabled | value
+ ${'when not disabled'} | ${false} | ${undefined}
+ ${'when disabled'} | ${true} | ${'disabled'}
+ `('$description', ({ disabled, value }) => {
+ it(`renders form select component with disabled=${value}`, () => {
+ createComponent({ disabled });
+
+ expect(findFormSelect().attributes('disabled')).toBe(value);
+ });
+ });
+ });
});
diff --git a/spec/frontend/work_items/components/item_title_spec.js b/spec/frontend/work_items/components/item_title_spec.js
index a55f448c9a2..de20369eb1b 100644
--- a/spec/frontend/work_items/components/item_title_spec.js
+++ b/spec/frontend/work_items/components/item_title_spec.js
@@ -37,7 +37,7 @@ describe('ItemTitle', () => {
disabled: true,
});
- expect(wrapper.classes()).toContain('gl-cursor-not-allowed');
+ expect(wrapper.classes()).toContain('gl-cursor-text');
expect(findInputEl().attributes('contenteditable')).toBe('false');
});
diff --git a/spec/frontend/work_items/components/work_item_actions_spec.js b/spec/frontend/work_items/components/work_item_actions_spec.js
index 137a0a7326d..a1f1d47ab90 100644
--- a/spec/frontend/work_items/components/work_item_actions_spec.js
+++ b/spec/frontend/work_items/components/work_item_actions_spec.js
@@ -1,5 +1,5 @@
-import { GlDropdownItem, GlModal } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
+import { GlModal } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import WorkItemActions from '~/work_items/components/work_item_actions.vue';
describe('WorkItemActions component', () => {
@@ -7,12 +7,19 @@ describe('WorkItemActions component', () => {
let glModalDirective;
const findModal = () => wrapper.findComponent(GlModal);
- const findDeleteButton = () => wrapper.findComponent(GlDropdownItem);
+ const findConfidentialityToggleButton = () =>
+ wrapper.findByTestId('confidentiality-toggle-action');
+ const findDeleteButton = () => wrapper.findByTestId('delete-action');
- const createComponent = ({ canDelete = true } = {}) => {
+ const createComponent = ({
+ canUpdate = true,
+ canDelete = true,
+ isConfidential = false,
+ isParentConfidential = false,
+ } = {}) => {
glModalDirective = jest.fn();
- wrapper = shallowMount(WorkItemActions, {
- propsData: { workItemId: '123', canDelete },
+ wrapper = shallowMountExtended(WorkItemActions, {
+ propsData: { workItemId: '123', canUpdate, canDelete, isConfidential, isParentConfidential },
directives: {
glModal: {
bind(_, { value }) {
@@ -34,27 +41,69 @@ describe('WorkItemActions component', () => {
expect(findModal().props('visible')).toBe(false);
});
- it('shows confirm modal when clicking Delete work item', () => {
+ it('renders dropdown actions', () => {
createComponent();
- findDeleteButton().vm.$emit('click');
-
- expect(glModalDirective).toHaveBeenCalled();
+ expect(findConfidentialityToggleButton().exists()).toBe(true);
+ expect(findDeleteButton().exists()).toBe(true);
});
- it('emits event when clicking OK button', () => {
- createComponent();
+ describe('toggle confidentiality action', () => {
+ it.each`
+ isConfidential | buttonText
+ ${true} | ${'Turn off confidentiality'}
+ ${false} | ${'Turn on confidentiality'}
+ `(
+ 'renders confidentiality toggle button with text "$buttonText"',
+ ({ isConfidential, buttonText }) => {
+ createComponent({ isConfidential });
+
+ expect(findConfidentialityToggleButton().text()).toBe(buttonText);
+ },
+ );
+
+ it('emits `toggleWorkItemConfidentiality` event when clicked', () => {
+ createComponent();
- findModal().vm.$emit('ok');
+ findConfidentialityToggleButton().vm.$emit('click');
- expect(wrapper.emitted('deleteWorkItem')).toEqual([[]]);
+ expect(wrapper.emitted('toggleWorkItemConfidentiality')[0]).toEqual([true]);
+ });
+
+ it.each`
+ props | propName | value
+ ${{ isParentConfidential: true }} | ${'isParentConfidential'} | ${true}
+ ${{ canUpdate: false }} | ${'canUpdate'} | ${false}
+ `('does not render when $propName is $value', ({ props }) => {
+ createComponent(props);
+
+ expect(findConfidentialityToggleButton().exists()).toBe(false);
+ });
});
- it('does not render when canDelete is false', () => {
- createComponent({
- canDelete: false,
+ describe('delete action', () => {
+ it('shows confirm modal when clicked', () => {
+ createComponent();
+
+ findDeleteButton().vm.$emit('click');
+
+ expect(glModalDirective).toHaveBeenCalled();
+ });
+
+ it('emits event when clicking OK button', () => {
+ createComponent();
+
+ findModal().vm.$emit('ok');
+
+ expect(wrapper.emitted('deleteWorkItem')).toEqual([[]]);
});
- expect(wrapper.html()).toBe('');
+ it('does not render when canDelete is false', () => {
+ createComponent({
+ canDelete: false,
+ });
+
+ expect(wrapper.findByTestId('delete-action').exists()).toBe(false);
+ });
});
});
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 299949a4baa..f0ef8aee7a9 100644
--- a/spec/frontend/work_items/components/work_item_assignees_spec.js
+++ b/spec/frontend/work_items/components/work_item_assignees_spec.js
@@ -5,14 +5,15 @@ import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import { mockTracking } from 'helpers/tracking_helper';
-import { stripTypenames } from 'helpers/graphql_helpers';
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
import userSearchQuery from '~/graphql_shared/queries/users_search.query.graphql';
import currentUserQuery from '~/graphql_shared/queries/current_user.query.graphql';
+import InviteMembersTrigger from '~/invite_members/components/invite_members_trigger.vue';
import workItemQuery from '~/work_items/graphql/work_item.query.graphql';
+import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
import WorkItemAssignees from '~/work_items/components/work_item_assignees.vue';
import { i18n, TASK_TYPE_NAME, TRACKING_CATEGORY_SHOW } from '~/work_items/constants';
-import { temporaryConfig, resolvers } from '~/work_items/graphql/provider';
+import { temporaryConfig } from '~/work_items/graphql/provider';
import {
projectMembersResponseWithCurrentUser,
mockAssignees,
@@ -20,6 +21,7 @@ import {
currentUserResponse,
currentUserNullResponse,
projectMembersResponseWithoutCurrentUser,
+ updateWorkItemMutationResponse,
} from '../mock_data';
Vue.use(VueApollo);
@@ -33,6 +35,7 @@ describe('WorkItemAssignees component', () => {
const findAssigneeLinks = () => wrapper.findAllComponents(GlLink);
const findTokenSelector = () => wrapper.findComponent(GlTokenSelector);
const findSkeletonLoader = () => wrapper.findComponent(GlSkeletonLoader);
+ const findInviteMembersTrigger = () => wrapper.findComponent(InviteMembersTrigger);
const findEmptyState = () => wrapper.findByTestId('empty-state');
const findAssignSelfButton = () => wrapper.findByTestId('assign-self');
@@ -43,6 +46,9 @@ describe('WorkItemAssignees component', () => {
.mockResolvedValue(projectMembersResponseWithCurrentUser);
const successCurrentUserQueryHandler = jest.fn().mockResolvedValue(currentUserResponse);
const noCurrentUserQueryHandler = jest.fn().mockResolvedValue(currentUserNullResponse);
+ const successUpdateWorkItemMutationHandler = jest
+ .fn()
+ .mockResolvedValue(updateWorkItemMutationResponse);
const errorHandler = jest.fn().mockRejectedValue('Houston, we have a problem');
@@ -50,15 +56,18 @@ describe('WorkItemAssignees component', () => {
assignees = mockAssignees,
searchQueryHandler = successSearchQueryHandler,
currentUserQueryHandler = successCurrentUserQueryHandler,
+ updateWorkItemMutationHandler = successUpdateWorkItemMutationHandler,
allowsMultipleAssignees = true,
+ canInviteMembers = false,
canUpdate = true,
} = {}) => {
const apolloProvider = createMockApollo(
[
[userSearchQuery, searchQueryHandler],
[currentUserQuery, currentUserQueryHandler],
+ [updateWorkItemMutation, updateWorkItemMutationHandler],
],
- resolvers,
+ {},
{
typePolicies: temporaryConfig.cacheConfig.typePolicies,
},
@@ -82,6 +91,7 @@ describe('WorkItemAssignees component', () => {
allowsMultipleAssignees,
workItemType: TASK_TYPE_NAME,
canUpdate,
+ canInviteMembers,
},
attachTo: document.body,
apolloProvider,
@@ -120,15 +130,6 @@ describe('WorkItemAssignees component', () => {
expect(findTokenSelector().element.contains(document.activeElement)).toBe(true);
});
- it('calls a mutation on clicking outside the token selector', async () => {
- createComponent();
- findTokenSelector().vm.$emit('input', [mockAssignees[0]]);
- findTokenSelector().vm.$emit('blur', new FocusEvent({ relatedTarget: null }));
- await waitForPromises();
-
- expect(findTokenSelector().props('selectedTokens')).toEqual([mockAssignees[0]]);
- });
-
it('passes `false` to `viewOnly` token selector prop if user can update assignees', () => {
createComponent();
@@ -141,6 +142,36 @@ describe('WorkItemAssignees component', () => {
expect(findTokenSelector().props('viewOnly')).toBe(true);
});
+ describe('when clicking outside the token selector', () => {
+ function arrange(args) {
+ createComponent(args);
+ findTokenSelector().vm.$emit('input', [mockAssignees[0]]);
+ findTokenSelector().vm.$emit('blur', new FocusEvent({ relatedTarget: null }));
+ }
+
+ it('calls a mutation with correct variables', () => {
+ arrange({ assignees: [] });
+
+ expect(successUpdateWorkItemMutationHandler).toHaveBeenCalledWith({
+ input: {
+ assigneesWidget: { assigneeIds: [mockAssignees[0].id] },
+ id: 'gid://gitlab/WorkItem/1',
+ },
+ });
+ });
+
+ it('emits an error and resets assignees if mutation was rejected', async () => {
+ arrange({ updateWorkItemMutationHandler: errorHandler, assignees: [mockAssignees[1]] });
+
+ await waitForPromises();
+
+ expect(wrapper.emitted('error')).toEqual([[i18n.updateError]]);
+ expect(findTokenSelector().props('selectedTokens')).toEqual([
+ { ...mockAssignees[1], class: expect.anything() },
+ ]);
+ });
+ });
+
describe('when searching for users', () => {
beforeEach(() => {
createComponent();
@@ -204,7 +235,7 @@ describe('WorkItemAssignees component', () => {
expect(findTokenSelector().props('dropdownItems')).toHaveLength(2);
});
- it('should search for users with correct key after text input', async () => {
+ it('searches for users with correct key after text input', async () => {
const searchKey = 'Hello';
findTokenSelector().vm.$emit('focus');
@@ -225,6 +256,18 @@ describe('WorkItemAssignees component', () => {
expect(wrapper.emitted('error')).toEqual([[i18n.fetchError]]);
});
+ it('updates localAssignees when assignees prop is updated', async () => {
+ createComponent({ assignees: [] });
+
+ expect(findTokenSelector().props('selectedTokens')).toEqual([]);
+
+ await wrapper.setProps({ assignees: [mockAssignees[0]] });
+
+ expect(findTokenSelector().props('selectedTokens')).toEqual([
+ { ...mockAssignees[0], class: expect.anything() },
+ ]);
+ });
+
describe('when assigning to current user', () => {
it('does not show `Assign myself` button if current user is loading', () => {
createComponent();
@@ -261,23 +304,21 @@ describe('WorkItemAssignees component', () => {
expect(findAssignSelfButton().exists()).toBe(true);
});
- it('calls update work item assignees mutation with current user as a variable on button click', () => {
- // TODO: replace this test as soon as we have a real mutation implemented
- jest.spyOn(wrapper.vm.$apollo, 'mutate').mockImplementation(jest.fn());
-
+ it('calls update work item assignees mutation with current user as a variable on button click', async () => {
+ const { currentUser } = currentUserResponse.data;
findTokenSelector().trigger('mouseover');
findAssignSelfButton().vm.$emit('click', new MouseEvent('click'));
+ await nextTick();
- expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith(
- expect.objectContaining({
- variables: {
- input: {
- assignees: [stripTypenames(currentUserResponse.data.currentUser)],
- id: workItemId,
- },
+ expect(findTokenSelector().props('selectedTokens')).toMatchObject([currentUser]);
+ expect(successUpdateWorkItemMutationHandler).toHaveBeenCalledWith({
+ input: {
+ id: workItemId,
+ assigneesWidget: {
+ assigneeIds: [currentUser.id],
},
- }),
- );
+ },
+ });
});
});
@@ -286,9 +327,7 @@ describe('WorkItemAssignees component', () => {
await waitForPromises();
expect(findTokenSelector().props('dropdownItems')[0]).toEqual(
- expect.objectContaining({
- ...stripTypenames(currentUserResponse.data.currentUser),
- }),
+ expect.objectContaining(currentUserResponse.data.currentUser),
);
});
@@ -303,9 +342,10 @@ describe('WorkItemAssignees component', () => {
});
it('adds current user to the top of dropdown items', () => {
- expect(findTokenSelector().props('dropdownItems')[0]).toEqual(
- stripTypenames(currentUserResponse.data.currentUser),
- );
+ expect(findTokenSelector().props('dropdownItems')[0]).toEqual({
+ ...currentUserResponse.data.currentUser,
+ class: expect.anything(),
+ });
});
it('does not add current user if search is not empty', async () => {
@@ -313,7 +353,7 @@ describe('WorkItemAssignees component', () => {
await waitForPromises();
expect(findTokenSelector().props('dropdownItems')[0]).not.toEqual(
- stripTypenames(currentUserResponse.data.currentUser),
+ currentUserResponse.data.currentUser,
);
});
});
@@ -405,4 +445,18 @@ describe('WorkItemAssignees component', () => {
});
});
});
+
+ describe('invite members', () => {
+ it('does not render `Invite members` link if user has no permission to invite members', () => {
+ createComponent();
+
+ expect(findInviteMembersTrigger().exists()).toBe(false);
+ });
+
+ it('renders `Invite members` link if user has a permission to invite members', () => {
+ createComponent({ canInviteMembers: true });
+
+ expect(findInviteMembersTrigger().exists()).toBe(true);
+ });
+ });
});
diff --git a/spec/frontend/work_items/components/work_item_detail_modal_spec.js b/spec/frontend/work_items/components/work_item_detail_modal_spec.js
index 70b1261bdb7..01891012f99 100644
--- a/spec/frontend/work_items/components/work_item_detail_modal_spec.js
+++ b/spec/frontend/work_items/components/work_item_detail_modal_spec.js
@@ -7,6 +7,13 @@ import createMockApollo from 'helpers/mock_apollo_helper';
import WorkItemDetail from '~/work_items/components/work_item_detail.vue';
import WorkItemDetailModal from '~/work_items/components/work_item_detail_modal.vue';
import deleteWorkItemFromTaskMutation from '~/work_items/graphql/delete_task_from_work_item.mutation.graphql';
+import deleteWorkItemMutation from '~/work_items/graphql/delete_work_item.mutation.graphql';
+import {
+ deleteWorkItemFromTaskMutationErrorResponse,
+ deleteWorkItemFromTaskMutationResponse,
+ deleteWorkItemMutationErrorResponse,
+ deleteWorkItemResponse,
+} from '../mock_data';
describe('WorkItemDetailModal component', () => {
let wrapper;
@@ -25,28 +32,38 @@ describe('WorkItemDetailModal component', () => {
},
};
+ const defaultPropsData = {
+ issueGid: 'gid://gitlab/WorkItem/1',
+ workItemId: 'gid://gitlab/WorkItem/2',
+ };
+
const findModal = () => wrapper.findComponent(GlModal);
const findAlert = () => wrapper.findComponent(GlAlert);
const findWorkItemDetail = () => wrapper.findComponent(WorkItemDetail);
- const createComponent = ({ workItemId = '1', issueGid = '2', error = false } = {}) => {
+ const createComponent = ({
+ lockVersion,
+ lineNumberStart,
+ lineNumberEnd,
+ error = false,
+ deleteWorkItemFromTaskMutationHandler = jest
+ .fn()
+ .mockResolvedValue(deleteWorkItemFromTaskMutationResponse),
+ deleteWorkItemMutationHandler = jest.fn().mockResolvedValue(deleteWorkItemResponse),
+ } = {}) => {
const apolloProvider = createMockApollo([
- [
- deleteWorkItemFromTaskMutation,
- jest.fn().mockResolvedValue({
- data: {
- workItemDeleteTask: {
- workItem: { id: 123, descriptionHtml: 'updated work item desc' },
- errors: [],
- },
- },
- }),
- ],
+ [deleteWorkItemFromTaskMutation, deleteWorkItemFromTaskMutationHandler],
+ [deleteWorkItemMutation, deleteWorkItemMutationHandler],
]);
wrapper = shallowMount(WorkItemDetailModal, {
apolloProvider,
- propsData: { workItemId, issueGid },
+ propsData: {
+ ...defaultPropsData,
+ lockVersion,
+ lineNumberStart,
+ lineNumberEnd,
+ },
data() {
return {
error,
@@ -67,8 +84,8 @@ describe('WorkItemDetailModal component', () => {
expect(findWorkItemDetail().props()).toEqual({
isModal: true,
- workItemId: '1',
- workItemParentId: '2',
+ workItemId: defaultPropsData.workItemId,
+ workItemParentId: defaultPropsData.issueGid,
});
});
@@ -109,16 +126,85 @@ describe('WorkItemDetailModal component', () => {
});
describe('delete work item', () => {
- it('emits workItemDeleted and closes modal', async () => {
- createComponent();
- const newDesc = 'updated work item desc';
-
- findWorkItemDetail().vm.$emit('deleteWorkItem');
-
- await waitForPromises();
+ describe('when there is task data', () => {
+ it('emits workItemDeleted and closes modal', async () => {
+ const mutationMock = jest.fn().mockResolvedValue(deleteWorkItemFromTaskMutationResponse);
+ createComponent({
+ lockVersion: 1,
+ lineNumberStart: '3',
+ lineNumberEnd: '3',
+ deleteWorkItemFromTaskMutationHandler: mutationMock,
+ });
+ const newDesc = 'updated work item desc';
+
+ findWorkItemDetail().vm.$emit('deleteWorkItem');
+ await waitForPromises();
+
+ expect(wrapper.emitted('workItemDeleted')).toEqual([[newDesc]]);
+ expect(hideModal).toHaveBeenCalled();
+ expect(mutationMock).toHaveBeenCalledWith({
+ input: {
+ id: defaultPropsData.issueGid,
+ lockVersion: 1,
+ taskData: { id: defaultPropsData.workItemId, lineNumberEnd: 3, lineNumberStart: 3 },
+ },
+ });
+ });
+
+ it.each`
+ errorType | mutationMock | errorMessage
+ ${'an error in the mutation response'} | ${jest.fn().mockResolvedValue(deleteWorkItemFromTaskMutationErrorResponse)} | ${'Error'}
+ ${'a network error'} | ${jest.fn().mockRejectedValue(new Error('GraphQL networkError'))} | ${'GraphQL networkError'}
+ `(
+ 'shows an error message when there is $errorType',
+ async ({ mutationMock, errorMessage }) => {
+ createComponent({
+ lockVersion: 1,
+ lineNumberStart: '3',
+ lineNumberEnd: '3',
+ deleteWorkItemFromTaskMutationHandler: mutationMock,
+ });
+
+ findWorkItemDetail().vm.$emit('deleteWorkItem');
+ await waitForPromises();
+
+ expect(wrapper.emitted('workItemDeleted')).toBeUndefined();
+ expect(hideModal).not.toHaveBeenCalled();
+ expect(findAlert().text()).toBe(errorMessage);
+ },
+ );
+ });
- expect(wrapper.emitted('workItemDeleted')).toEqual([[newDesc]]);
- expect(hideModal).toHaveBeenCalled();
+ describe('when there is no task data', () => {
+ it('emits workItemDeleted and closes modal', async () => {
+ const mutationMock = jest.fn().mockResolvedValue(deleteWorkItemResponse);
+ createComponent({ deleteWorkItemMutationHandler: mutationMock });
+
+ findWorkItemDetail().vm.$emit('deleteWorkItem');
+ await waitForPromises();
+
+ expect(wrapper.emitted('workItemDeleted')).toEqual([[defaultPropsData.workItemId]]);
+ expect(hideModal).toHaveBeenCalled();
+ expect(mutationMock).toHaveBeenCalledWith({ input: { id: defaultPropsData.workItemId } });
+ });
+
+ it.each`
+ errorType | mutationMock | errorMessage
+ ${'an error in the mutation response'} | ${jest.fn().mockResolvedValue(deleteWorkItemMutationErrorResponse)} | ${'Error'}
+ ${'a network error'} | ${jest.fn().mockRejectedValue(new Error('GraphQL networkError'))} | ${'GraphQL networkError'}
+ `(
+ 'shows an error message when there is $errorType',
+ async ({ mutationMock, errorMessage }) => {
+ createComponent({ deleteWorkItemMutationHandler: mutationMock });
+
+ findWorkItemDetail().vm.$emit('deleteWorkItem');
+ await waitForPromises();
+
+ expect(wrapper.emitted('workItemDeleted')).toBeUndefined();
+ expect(hideModal).not.toHaveBeenCalled();
+ expect(findAlert().text()).toBe(errorMessage);
+ },
+ );
});
});
});
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 93bf7286aa7..434c1db8a2c 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
@@ -1,13 +1,20 @@
import Vue from 'vue';
-import { GlForm, GlFormCombobox } from '@gitlab/ui';
+import { GlForm, GlFormInput, GlFormCombobox } from '@gitlab/ui';
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 WorkItemLinksForm from '~/work_items/components/work_item_links/work_item_links_form.vue';
import projectWorkItemsQuery from '~/work_items/graphql/project_work_items.query.graphql';
+import projectWorkItemTypesQuery from '~/work_items/graphql/project_work_item_types.query.graphql';
+import createWorkItemMutation from '~/work_items/graphql/create_work_item.mutation.graphql';
import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
-import { availableWorkItemsResponse, updateWorkItemMutationResponse } from '../../mock_data';
+import {
+ availableWorkItemsResponse,
+ projectWorkItemTypesQueryResponse,
+ createWorkItemMutationResponse,
+ updateWorkItemMutationResponse,
+} from '../../mock_data';
Vue.use(VueApollo);
@@ -15,14 +22,21 @@ describe('WorkItemLinksForm', () => {
let wrapper;
const updateMutationResolver = jest.fn().mockResolvedValue(updateWorkItemMutationResponse);
+ const createMutationResolver = jest.fn().mockResolvedValue(createWorkItemMutationResponse);
- const createComponent = async ({ listResponse = availableWorkItemsResponse } = {}) => {
+ const createComponent = async ({
+ listResponse = availableWorkItemsResponse,
+ typesResponse = projectWorkItemTypesQueryResponse,
+ parentConfidential = false,
+ } = {}) => {
wrapper = shallowMountExtended(WorkItemLinksForm, {
apolloProvider: createMockApollo([
[projectWorkItemsQuery, jest.fn().mockResolvedValue(listResponse)],
+ [projectWorkItemTypesQuery, jest.fn().mockResolvedValue(typesResponse)],
[updateWorkItemMutation, updateMutationResolver],
+ [createWorkItemMutation, createMutationResolver],
]),
- propsData: { issuableGid: 'gid://gitlab/WorkItem/1' },
+ propsData: { issuableGid: 'gid://gitlab/WorkItem/1', parentConfidential },
provide: {
projectPath: 'project/path',
},
@@ -33,6 +47,7 @@ describe('WorkItemLinksForm', () => {
const findForm = () => wrapper.findComponent(GlForm);
const findCombobox = () => wrapper.findComponent(GlFormCombobox);
+ const findInput = () => wrapper.findComponent(GlFormInput);
const findAddChildButton = () => wrapper.findByTestId('add-child-button');
beforeEach(async () => {
@@ -47,19 +62,73 @@ describe('WorkItemLinksForm', () => {
expect(findForm().exists()).toBe(true);
});
- it('passes available work items as prop when typing in combobox', async () => {
- findCombobox().vm.$emit('input', 'Task');
+ it('creates child task in non confidential parent', async () => {
+ findInput().vm.$emit('input', 'Create task test');
+
+ findForm().vm.$emit('submit', {
+ preventDefault: jest.fn(),
+ });
await waitForPromises();
+ expect(createMutationResolver).toHaveBeenCalledWith({
+ input: {
+ title: 'Create task test',
+ projectPath: 'project/path',
+ workItemTypeId: 'gid://gitlab/WorkItems::Type/3',
+ hierarchyWidget: {
+ parentId: 'gid://gitlab/WorkItem/1',
+ },
+ confidential: false,
+ },
+ });
+ });
+
+ it('creates child task in confidential parent', async () => {
+ await createComponent({ parentConfidential: true });
+
+ findInput().vm.$emit('input', 'Create confidential task');
- expect(findCombobox().exists()).toBe(true);
- expect(findCombobox().props('tokenList').length).toBe(2);
+ findForm().vm.$emit('submit', {
+ preventDefault: jest.fn(),
+ });
+ await waitForPromises();
+ expect(createMutationResolver).toHaveBeenCalledWith({
+ input: {
+ title: 'Create confidential task',
+ projectPath: 'project/path',
+ workItemTypeId: 'gid://gitlab/WorkItems::Type/3',
+ hierarchyWidget: {
+ parentId: 'gid://gitlab/WorkItem/1',
+ },
+ confidential: true,
+ },
+ });
});
- it('selects and add child', async () => {
+ // Follow up issue to turn this functionality back on https://gitlab.com/gitlab-org/gitlab/-/issues/368757
+ // eslint-disable-next-line jest/no-disabled-tests
+ it.skip('selects and add child', async () => {
findCombobox().vm.$emit('input', availableWorkItemsResponse.data.workspace.workItems.edges[0]);
findAddChildButton().vm.$emit('click');
await waitForPromises();
expect(updateMutationResolver).toHaveBeenCalled();
});
+
+ // eslint-disable-next-line jest/no-disabled-tests
+ describe.skip('when typing in combobox', () => {
+ beforeEach(async () => {
+ findCombobox().vm.$emit('input', 'Task');
+ await waitForPromises();
+ await jest.runOnlyPendingTimers();
+ });
+
+ it('passes available work items as prop', () => {
+ expect(findCombobox().exists()).toBe(true);
+ expect(findCombobox().props('tokenList').length).toBe(2);
+ });
+
+ it('passes action to create task', () => {
+ expect(findCombobox().props('actionList').length).toBe(1);
+ });
+ });
});
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 f8471b7f167..287ec022d3f 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
@@ -1,75 +1,24 @@
-import Vue from 'vue';
import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
-import { cloneDeep } from 'lodash';
-import VueApollo from 'vue-apollo';
-import createMockApollo from 'helpers/mock_apollo_helper';
+
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import waitForPromises from 'helpers/wait_for_promises';
import WorkItemLinksMenu from '~/work_items/components/work_item_links/work_item_links_menu.vue';
-import changeWorkItemParentMutation from '~/work_items/graphql/change_work_item_parent_link.mutation.graphql';
-import getWorkItemLinksQuery from '~/work_items/graphql/work_item_links.query.graphql';
-import { WIDGET_TYPE_HIERARCHY } from '~/work_items/constants';
-import { workItemHierarchyResponse, changeWorkItemParentMutationResponse } from '../../mock_data';
-
-Vue.use(VueApollo);
-
-const PARENT_ID = 'gid://gitlab/WorkItem/1';
-const WORK_ITEM_ID = 'gid://gitlab/WorkItem/3';
describe('WorkItemLinksMenu', () => {
let wrapper;
- let mockApollo;
-
- const $toast = {
- show: jest.fn(),
- };
-
- const createComponent = async ({
- data = {},
- mutationHandler = jest.fn().mockResolvedValue(changeWorkItemParentMutationResponse),
- } = {}) => {
- mockApollo = createMockApollo([
- [getWorkItemLinksQuery, jest.fn().mockResolvedValue(workItemHierarchyResponse)],
- [changeWorkItemParentMutation, mutationHandler],
- ]);
-
- mockApollo.clients.defaultClient.cache.writeQuery({
- query: getWorkItemLinksQuery,
- variables: {
- id: PARENT_ID,
- },
- data: workItemHierarchyResponse.data,
- });
- wrapper = shallowMountExtended(WorkItemLinksMenu, {
- data() {
- return {
- ...data,
- };
- },
- propsData: {
- workItemId: WORK_ITEM_ID,
- parentWorkItemId: PARENT_ID,
- },
- apolloProvider: mockApollo,
- mocks: {
- $toast,
- },
- });
-
- await waitForPromises();
+ const createComponent = () => {
+ wrapper = shallowMountExtended(WorkItemLinksMenu);
};
const findDropdown = () => wrapper.find(GlDropdown);
const findRemoveDropdownItem = () => wrapper.find(GlDropdownItem);
beforeEach(async () => {
- await createComponent();
+ createComponent();
});
afterEach(() => {
wrapper.destroy();
- mockApollo = null;
});
it('renders dropdown and dropdown items', () => {
@@ -77,65 +26,9 @@ describe('WorkItemLinksMenu', () => {
expect(findRemoveDropdownItem().exists()).toBe(true);
});
- it('calls correct mutation with correct variables', async () => {
- const mutationHandler = jest.fn().mockResolvedValue(changeWorkItemParentMutationResponse);
-
- createComponent({ mutationHandler });
-
- findRemoveDropdownItem().vm.$emit('click');
-
- await waitForPromises();
-
- expect(mutationHandler).toHaveBeenCalledWith({
- id: WORK_ITEM_ID,
- parentId: null,
- });
- });
-
- it('shows toast when mutation succeeds', async () => {
- const mutationHandler = jest.fn().mockResolvedValue(changeWorkItemParentMutationResponse);
-
- createComponent({ mutationHandler });
-
- findRemoveDropdownItem().vm.$emit('click');
-
- await waitForPromises();
-
- expect($toast.show).toHaveBeenCalledWith('Child removed', {
- action: { onClick: expect.anything(), text: 'Undo' },
- });
- });
-
- it('updates the cache when mutation succeeds', async () => {
- const mutationHandler = jest.fn().mockResolvedValue(changeWorkItemParentMutationResponse);
-
- createComponent({ mutationHandler });
-
- mockApollo.clients.defaultClient.cache.readQuery = jest.fn(
- () => workItemHierarchyResponse.data,
- );
-
- mockApollo.clients.defaultClient.cache.writeQuery = jest.fn();
-
+ it('emits removeChild event on click Remove', () => {
findRemoveDropdownItem().vm.$emit('click');
- await waitForPromises();
-
- // Remove the work item from parent's children
- const resp = cloneDeep(workItemHierarchyResponse);
- const index = resp.data.workItem.widgets
- .find((widget) => widget.type === WIDGET_TYPE_HIERARCHY)
- .children.nodes.findIndex((child) => child.id === WORK_ITEM_ID);
- resp.data.workItem.widgets
- .find((widget) => widget.type === WIDGET_TYPE_HIERARCHY)
- .children.nodes.splice(index, 1);
-
- expect(mockApollo.clients.defaultClient.cache.writeQuery).toHaveBeenCalledWith(
- expect.objectContaining({
- query: expect.anything(),
- variables: { id: PARENT_ID },
- data: resp.data,
- }),
- );
+ expect(wrapper.emitted('removeChild')).toHaveLength(1);
});
});
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 2ec9b1ec0ac..00f508f1548 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
@@ -1,34 +1,85 @@
import Vue, { nextTick } from 'vue';
-import { GlBadge } from '@gitlab/ui';
+import { GlButton, GlIcon, GlAlert } from '@gitlab/ui';
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 SidebarEventHub from '~/sidebar/event_hub';
+import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
import WorkItemLinks from '~/work_items/components/work_item_links/work_item_links.vue';
+import workItemQuery from '~/work_items/graphql/work_item.query.graphql';
+import changeWorkItemParentMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
import getWorkItemLinksQuery from '~/work_items/graphql/work_item_links.query.graphql';
-import { workItemHierarchyResponse, workItemHierarchyEmptyResponse } from '../../mock_data';
+import {
+ workItemHierarchyResponse,
+ workItemHierarchyEmptyResponse,
+ workItemHierarchyNoUpdatePermissionResponse,
+ changeWorkItemParentMutationResponse,
+ workItemQueryResponse,
+} from '../../mock_data';
Vue.use(VueApollo);
describe('WorkItemLinks', () => {
let wrapper;
+ let mockApollo;
+
+ const WORK_ITEM_ID = 'gid://gitlab/WorkItem/2';
+
+ const $toast = {
+ show: jest.fn(),
+ };
+
+ const mutationChangeParentHandler = jest
+ .fn()
+ .mockResolvedValue(changeWorkItemParentMutationResponse);
+
+ const childWorkItemQueryHandler = jest.fn().mockResolvedValue(workItemQueryResponse);
+
+ const findChildren = () => wrapper.findAll('[data-testid="links-child"]');
+
+ const createComponent = async ({
+ data = {},
+ fetchHandler = jest.fn().mockResolvedValue(workItemHierarchyResponse),
+ mutationHandler = mutationChangeParentHandler,
+ } = {}) => {
+ mockApollo = createMockApollo(
+ [
+ [getWorkItemLinksQuery, fetchHandler],
+ [changeWorkItemParentMutation, mutationHandler],
+ [workItemQuery, childWorkItemQueryHandler],
+ ],
+ {},
+ { addTypename: true },
+ );
- const createComponent = async ({ response = workItemHierarchyResponse } = {}) => {
wrapper = shallowMountExtended(WorkItemLinks, {
- apolloProvider: createMockApollo([
- [getWorkItemLinksQuery, jest.fn().mockResolvedValue(response)],
- ]),
+ data() {
+ return {
+ ...data,
+ };
+ },
+ provide: {
+ projectPath: 'project/path',
+ },
propsData: { issuableId: 1 },
+ apolloProvider: mockApollo,
+ mocks: {
+ $toast,
+ },
});
await waitForPromises();
};
+ const findAlert = () => wrapper.findComponent(GlAlert);
const findToggleButton = () => wrapper.findByTestId('toggle-links');
const findLinksBody = () => wrapper.findByTestId('links-body');
const findEmptyState = () => wrapper.findByTestId('links-empty');
const findToggleAddFormButton = () => wrapper.findByTestId('toggle-add-form');
const findAddLinksForm = () => wrapper.findByTestId('add-links-form');
+ const findFirstLinksMenu = () => wrapper.findByTestId('links-menu');
+ const findChildrenCount = () => wrapper.findByTestId('children-count');
beforeEach(async () => {
await createComponent();
@@ -36,6 +87,7 @@ describe('WorkItemLinks', () => {
afterEach(() => {
wrapper.destroy();
+ mockApollo = null;
});
it('is expanded by default', () => {
@@ -43,7 +95,7 @@ describe('WorkItemLinks', () => {
expect(findLinksBody().exists()).toBe(true);
});
- it('expands on click toggle button', async () => {
+ it('collapses on click toggle button', async () => {
findToggleButton().vm.$emit('click');
await nextTick();
@@ -67,7 +119,9 @@ describe('WorkItemLinks', () => {
describe('when no child links', () => {
beforeEach(async () => {
- await createComponent({ response: workItemHierarchyEmptyResponse });
+ await createComponent({
+ fetchHandler: jest.fn().mockResolvedValue(workItemHierarchyEmptyResponse),
+ });
});
it('displays empty state if there are no children', () => {
@@ -78,9 +132,140 @@ describe('WorkItemLinks', () => {
it('renders all hierarchy widget children', () => {
expect(findLinksBody().exists()).toBe(true);
+ expect(findChildren()).toHaveLength(4);
+ expect(findFirstLinksMenu().exists()).toBe(true);
+ });
+
+ it('shows alert when list loading fails', async () => {
+ const errorMessage = 'Some error';
+ await createComponent({
+ fetchHandler: jest.fn().mockRejectedValue(new Error(errorMessage)),
+ });
+
+ await nextTick();
+
+ expect(findAlert().exists()).toBe(true);
+ expect(findAlert().text()).toBe(errorMessage);
+ });
+
+ it('renders widget child icon and tooltip', () => {
+ expect(findChildren().at(0).findComponent(GlIcon).props('name')).toBe('issue-open-m');
+ expect(findChildren().at(1).findComponent(GlIcon).props('name')).toBe('issue-close');
+ });
+
+ it('renders confidentiality icon when child item is confidential', () => {
const children = wrapper.findAll('[data-testid="links-child"]');
+ const confidentialIcon = children.at(0).find('[data-testid="confidential-icon"]');
+
+ expect(confidentialIcon.exists()).toBe(true);
+ expect(confidentialIcon.props('name')).toBe('eye-slash');
+ });
+
+ it('displays number if children', () => {
+ expect(findChildrenCount().exists()).toBe(true);
+
+ expect(findChildrenCount().text()).toContain('4');
+ });
+
+ it('refetches child items when `confidentialityUpdated` event is emitted on SidebarEventhub', async () => {
+ const fetchHandler = jest.fn().mockResolvedValue(workItemHierarchyResponse);
+ await createComponent({
+ fetchHandler,
+ });
+ await waitForPromises();
+
+ SidebarEventHub.$emit('confidentialityUpdated');
+ await nextTick();
+
+ // First call is done on component mount.
+ // Second call is done on confidentialityUpdated event.
+ expect(fetchHandler).toHaveBeenCalledTimes(2);
+ });
+
+ describe('when no permission to update', () => {
+ beforeEach(async () => {
+ await createComponent({
+ fetchHandler: jest.fn().mockResolvedValue(workItemHierarchyNoUpdatePermissionResponse),
+ });
+ });
- expect(children).toHaveLength(4);
- expect(children.at(0).findComponent(GlBadge).text()).toBe('Open');
+ it('does not display button to toggle Add form', () => {
+ expect(findToggleAddFormButton().exists()).toBe(false);
+ });
+
+ it('does not display link menu on children', () => {
+ expect(findFirstLinksMenu().exists()).toBe(false);
+ });
+ });
+
+ describe('remove child', () => {
+ beforeEach(async () => {
+ await createComponent({ mutationHandler: mutationChangeParentHandler });
+ });
+
+ it('calls correct mutation with correct variables', async () => {
+ findFirstLinksMenu().vm.$emit('removeChild');
+
+ await waitForPromises();
+
+ expect(mutationChangeParentHandler).toHaveBeenCalledWith({
+ input: {
+ id: WORK_ITEM_ID,
+ hierarchyWidget: {
+ parentId: null,
+ },
+ },
+ });
+ });
+
+ it('shows toast when mutation succeeds', async () => {
+ findFirstLinksMenu().vm.$emit('removeChild');
+
+ await waitForPromises();
+
+ expect($toast.show).toHaveBeenCalledWith('Child removed', {
+ action: { onClick: expect.anything(), text: 'Undo' },
+ });
+ });
+
+ it('renders correct number of children after removal', async () => {
+ expect(findChildren()).toHaveLength(4);
+
+ findFirstLinksMenu().vm.$emit('removeChild');
+ await waitForPromises();
+
+ expect(findChildren()).toHaveLength(3);
+ });
+ });
+
+ describe('prefetching child items', () => {
+ beforeEach(async () => {
+ await createComponent();
+ });
+
+ const findChildLink = () => findChildren().at(0).findComponent(GlButton);
+
+ it('does not fetch the child work item before hovering work item links', () => {
+ expect(childWorkItemQueryHandler).not.toHaveBeenCalled();
+ });
+
+ it('fetches the child work item if link is hovered for 250+ ms', async () => {
+ findChildLink().vm.$emit('mouseover');
+ jest.advanceTimersByTime(DEFAULT_DEBOUNCE_AND_THROTTLE_MS);
+ await waitForPromises();
+
+ expect(childWorkItemQueryHandler).toHaveBeenCalledWith({
+ id: 'gid://gitlab/WorkItem/2',
+ });
+ });
+
+ it('does not fetch the child work item if link is hovered for less than 250 ms', async () => {
+ findChildLink().vm.$emit('mouseover');
+ jest.advanceTimersByTime(200);
+ findChildLink().vm.$emit('mouseout');
+ await waitForPromises();
+
+ expect(childWorkItemQueryHandler).not.toHaveBeenCalled();
+ });
});
});
diff --git a/spec/frontend/work_items/components/work_item_state_spec.js b/spec/frontend/work_items/components/work_item_state_spec.js
index b379d1fc846..6b23a6e4795 100644
--- a/spec/frontend/work_items/components/work_item_state_spec.js
+++ b/spec/frontend/work_items/components/work_item_state_spec.js
@@ -29,6 +29,7 @@ describe('WorkItemState component', () => {
const createComponent = ({
state = STATE_OPEN,
mutationHandler = mutationSuccessHandler,
+ canUpdate = true,
} = {}) => {
const { id, workItemType } = workItemQueryResponse.data.workItem;
wrapper = shallowMount(WorkItemState, {
@@ -39,6 +40,7 @@ describe('WorkItemState component', () => {
state,
workItemType,
},
+ canUpdate,
},
});
};
@@ -53,6 +55,20 @@ describe('WorkItemState component', () => {
expect(findItemState().props('state')).toBe(workItemQueryResponse.data.workItem.state);
});
+ describe('item state disabled prop', () => {
+ describe.each`
+ description | canUpdate | value
+ ${'when cannot update'} | ${false} | ${true}
+ ${'when can update'} | ${true} | ${false}
+ `('$description', ({ canUpdate, value }) => {
+ it(`renders item state component with disabled=${value}`, () => {
+ createComponent({ canUpdate });
+
+ expect(findItemState().props('disabled')).toBe(value);
+ });
+ });
+ });
+
describe('when updating the state', () => {
it('calls a mutation', () => {
createComponent();
diff --git a/spec/frontend/work_items/components/work_item_title_spec.js b/spec/frontend/work_items/components/work_item_title_spec.js
index a48449bb636..c0d966abab8 100644
--- a/spec/frontend/work_items/components/work_item_title_spec.js
+++ b/spec/frontend/work_items/components/work_item_title_spec.js
@@ -20,7 +20,11 @@ describe('WorkItemTitle component', () => {
const findItemTitle = () => wrapper.findComponent(ItemTitle);
- const createComponent = ({ workItemParentId, mutationHandler = mutationSuccessHandler } = {}) => {
+ const createComponent = ({
+ workItemParentId,
+ mutationHandler = mutationSuccessHandler,
+ canUpdate = true,
+ } = {}) => {
const { id, title, workItemType } = workItemQueryResponse.data.workItem;
wrapper = shallowMount(WorkItemTitle, {
apolloProvider: createMockApollo([
@@ -32,6 +36,7 @@ describe('WorkItemTitle component', () => {
workItemTitle: title,
workItemType: workItemType.name,
workItemParentId,
+ canUpdate,
},
});
};
@@ -46,6 +51,20 @@ describe('WorkItemTitle component', () => {
expect(findItemTitle().props('title')).toBe(workItemQueryResponse.data.workItem.title);
});
+ describe('item title disabled prop', () => {
+ describe.each`
+ description | canUpdate | value
+ ${'when cannot update'} | ${false} | ${true}
+ ${'when can update'} | ${true} | ${false}
+ `('$description', ({ canUpdate, value }) => {
+ it(`renders item title component with disabled=${value}`, () => {
+ createComponent({ canUpdate });
+
+ expect(findItemTitle().props('disabled')).toBe(value);
+ });
+ });
+ });
+
describe('when updating the title', () => {
it('calls a mutation', () => {
const title = 'new title!';
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
new file mode 100644
index 00000000000..85466578e18
--- /dev/null
+++ b/spec/frontend/work_items/components/work_item_type_icon_spec.js
@@ -0,0 +1,47 @@
+import { GlIcon } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import WorkItemTypeIcon from '~/work_items/components/work_item_type_icon.vue';
+
+let wrapper;
+
+function createComponent(propsData) {
+ wrapper = shallowMount(WorkItemTypeIcon, { propsData });
+}
+
+describe('Work Item type component', () => {
+ const findIcon = () => wrapper.findComponent(GlIcon);
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe.each`
+ workItemType | workItemIconName | iconName | text
+ ${'TASK'} | ${''} | ${'issue-type-task'} | ${'Task'}
+ ${''} | ${'issue-type-task'} | ${'issue-type-task'} | ${''}
+ ${'ISSUE'} | ${''} | ${'issue-type-issue'} | ${'Issue'}
+ ${''} | ${'issue-type-issue'} | ${'issue-type-issue'} | ${''}
+ ${'REQUIREMENTS'} | ${''} | ${'issue-type-requirements'} | ${'Requirements'}
+ ${'INCIDENT'} | ${''} | ${'issue-type-incident'} | ${'Incident'}
+ ${'TEST_CASE'} | ${''} | ${'issue-type-test-case'} | ${'Test case'}
+ ${'random-issue-type'} | ${''} | ${'issue-type-issue'} | ${''}
+ `(
+ 'with workItemType set to "$workItemType" and workItemIconName set to "$workItemIconName"',
+ ({ workItemType, workItemIconName, iconName, text }) => {
+ beforeEach(() => {
+ createComponent({
+ workItemType,
+ workItemIconName,
+ });
+ });
+
+ it(`renders icon with name '${iconName}'`, () => {
+ expect(findIcon().props('name')).toBe(iconName);
+ });
+
+ it(`renders correct text`, () => {
+ expect(wrapper.text()).toBe(text);
+ });
+ },
+ );
+});
diff --git a/spec/frontend/work_items/components/work_item_weight_spec.js b/spec/frontend/work_items/components/work_item_weight_spec.js
index c3bbea26cda..94bdb336deb 100644
--- a/spec/frontend/work_items/components/work_item_weight_spec.js
+++ b/spec/frontend/work_items/components/work_item_weight_spec.js
@@ -1,16 +1,21 @@
import { GlForm, GlFormInput } from '@gitlab/ui';
-import { nextTick } from 'vue';
+import Vue, { nextTick } from 'vue';
+import VueApollo from 'vue-apollo';
+import createMockApollo from 'helpers/mock_apollo_helper';
import { mockTracking } from 'helpers/tracking_helper';
import { mountExtended } from 'helpers/vue_test_utils_helper';
+import waitForPromises from 'helpers/wait_for_promises';
import { __ } from '~/locale';
import WorkItemWeight from '~/work_items/components/work_item_weight.vue';
-import { TRACKING_CATEGORY_SHOW } from '~/work_items/constants';
-import localUpdateWorkItemMutation from '~/work_items/graphql/local_update_work_item.mutation.graphql';
+import { i18n, TRACKING_CATEGORY_SHOW } from '~/work_items/constants';
+import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
+import { updateWorkItemMutationResponse } from 'jest/work_items/mock_data';
describe('WorkItemWeight component', () => {
+ Vue.use(VueApollo);
+
let wrapper;
- const mutateSpy = jest.fn();
const workItemId = 'gid://gitlab/WorkItem/1';
const workItemType = 'Task';
@@ -22,8 +27,10 @@ describe('WorkItemWeight component', () => {
hasIssueWeightsFeature = true,
isEditing = false,
weight,
+ mutationHandler = jest.fn().mockResolvedValue(updateWorkItemMutationResponse),
} = {}) => {
wrapper = mountExtended(WorkItemWeight, {
+ apolloProvider: createMockApollo([[updateWorkItemMutation, mutationHandler]]),
propsData: {
canUpdate,
weight,
@@ -33,11 +40,6 @@ describe('WorkItemWeight component', () => {
provide: {
hasIssueWeightsFeature,
},
- mocks: {
- $apollo: {
- mutate: mutateSpy,
- },
- },
});
if (isEditing) {
@@ -131,26 +133,73 @@ describe('WorkItemWeight component', () => {
});
describe('when blurred', () => {
- it('calls a mutation to update the weight', () => {
- const weight = 0;
- createComponent({ isEditing: true, weight });
+ it('calls a mutation to update the weight when the input value is different', () => {
+ const mutationSpy = jest.fn().mockResolvedValue(updateWorkItemMutationResponse);
+ createComponent({
+ isEditing: true,
+ weight: 0,
+ mutationHandler: mutationSpy,
+ canUpdate: true,
+ });
+
+ findInput().vm.$emit('blur', { target: { value: 1 } });
+
+ expect(mutationSpy).toHaveBeenCalledWith({
+ input: {
+ id: workItemId,
+ weightWidget: {
+ weight: 1,
+ },
+ },
+ });
+ });
+
+ it('does not call a mutation to update the weight when the input value is the same', () => {
+ const mutationSpy = jest.fn().mockResolvedValue(updateWorkItemMutationResponse);
+ createComponent({ isEditing: true, mutationHandler: mutationSpy, canUpdate: true });
findInput().trigger('blur');
- expect(mutateSpy).toHaveBeenCalledWith({
- mutation: localUpdateWorkItemMutation,
- variables: {
- input: {
- id: workItemId,
- weight,
+ expect(mutationSpy).not.toHaveBeenCalledWith();
+ });
+
+ it('emits an error when there is a GraphQL error', async () => {
+ const response = {
+ data: {
+ workItemUpdate: {
+ errors: ['Error!'],
+ workItem: {},
},
},
+ };
+ createComponent({
+ isEditing: true,
+ mutationHandler: jest.fn().mockResolvedValue(response),
+ canUpdate: true,
+ });
+
+ findInput().trigger('blur');
+ await waitForPromises();
+
+ expect(wrapper.emitted('error')).toEqual([[i18n.updateError]]);
+ });
+
+ it('emits an error when there is a network error', async () => {
+ createComponent({
+ isEditing: true,
+ mutationHandler: jest.fn().mockRejectedValue(new Error()),
+ canUpdate: true,
});
+
+ findInput().trigger('blur');
+ await waitForPromises();
+
+ expect(wrapper.emitted('error')).toEqual([[i18n.updateError]]);
});
it('tracks updating the weight', () => {
const trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
- createComponent();
+ createComponent({ canUpdate: true });
findInput().trigger('blur');
diff --git a/spec/frontend/work_items/mock_data.js b/spec/frontend/work_items/mock_data.js
index 0359caf7116..d24ac2a9f93 100644
--- a/spec/frontend/work_items/mock_data.js
+++ b/spec/frontend/work_items/mock_data.js
@@ -25,10 +25,14 @@ export const workItemQueryResponse = {
title: 'Test',
state: 'OPEN',
description: 'description',
+ confidential: false,
+ createdAt: '2022-08-03T12:41:54Z',
+ closedAt: null,
workItemType: {
__typename: 'WorkItemType',
id: 'gid://gitlab/WorkItems::Type/5',
name: 'Task',
+ iconName: 'issue-type-task',
},
userPermissions: {
deleteWorkItem: false,
@@ -46,6 +50,7 @@ export const workItemQueryResponse = {
__typename: 'WorkItemWidgetAssignees',
type: 'ASSIGNEES',
allowsMultipleAssignees: true,
+ canInviteMembers: true,
assignees: {
nodes: mockAssignees,
},
@@ -57,13 +62,14 @@ export const workItemQueryResponse = {
id: 'gid://gitlab/Issue/1',
iid: '5',
title: 'Parent title',
+ confidential: false,
},
children: {
- edges: [
+ nodes: [
{
- node: {
- id: 'gid://gitlab/WorkItem/444',
- },
+ id: 'gid://gitlab/WorkItem/444',
+ createdAt: '2022-08-03T12:41:54Z',
+ closedAt: null,
},
],
},
@@ -77,16 +83,21 @@ export const updateWorkItemMutationResponse = {
data: {
workItemUpdate: {
__typename: 'WorkItemUpdatePayload',
+ errors: [],
workItem: {
__typename: 'WorkItem',
id: 'gid://gitlab/WorkItem/1',
title: 'Updated title',
state: 'OPEN',
description: 'description',
+ confidential: false,
+ createdAt: '2022-08-03T12:41:54Z',
+ closedAt: null,
workItemType: {
__typename: 'WorkItemType',
id: 'gid://gitlab/WorkItems::Type/5',
name: 'Task',
+ iconName: 'issue-type-task',
},
userPermissions: {
deleteWorkItem: false,
@@ -95,24 +106,46 @@ export const updateWorkItemMutationResponse = {
widgets: [
{
children: {
- edges: [
+ nodes: [
{
- node: 'gid://gitlab/WorkItem/444',
+ id: 'gid://gitlab/WorkItem/444',
},
],
},
},
+ {
+ __typename: 'WorkItemWidgetAssignees',
+ type: 'ASSIGNEES',
+ allowsMultipleAssignees: true,
+ canInviteMembers: true,
+ assignees: {
+ nodes: [mockAssignees[0]],
+ },
+ },
],
},
},
},
};
+export const mockParent = {
+ parent: {
+ id: 'gid://gitlab/Issue/1',
+ iid: '5',
+ title: 'Parent title',
+ confidential: false,
+ },
+};
+
export const workItemResponseFactory = ({
canUpdate = false,
+ canDelete = false,
allowsMultipleAssignees = true,
assigneesWidgetPresent = true,
- parent = null,
+ weightWidgetPresent = true,
+ confidential = false,
+ canInviteMembers = false,
+ parent = mockParent.parent,
} = {}) => ({
data: {
workItem: {
@@ -121,13 +154,17 @@ export const workItemResponseFactory = ({
title: 'Updated title',
state: 'OPEN',
description: 'description',
+ confidential,
+ createdAt: '2022-08-03T12:41:54Z',
+ closedAt: null,
workItemType: {
__typename: 'WorkItemType',
id: 'gid://gitlab/WorkItems::Type/5',
name: 'Task',
+ iconName: 'issue-type-task',
},
userPermissions: {
- deleteWorkItem: false,
+ deleteWorkItem: canDelete,
updateWorkItem: canUpdate,
},
widgets: [
@@ -143,20 +180,28 @@ export const workItemResponseFactory = ({
__typename: 'WorkItemWidgetAssignees',
type: 'ASSIGNEES',
allowsMultipleAssignees,
+ canInviteMembers,
assignees: {
nodes: mockAssignees,
},
}
: { type: 'MOCK TYPE' },
+ weightWidgetPresent
+ ? {
+ __typename: 'WorkItemWidgetWeight',
+ type: 'WEIGHT',
+ weight: 0,
+ }
+ : { type: 'MOCK TYPE' },
{
__typename: 'WorkItemWidgetHierarchy',
type: 'HIERARCHY',
children: {
- edges: [
+ nodes: [
{
- node: {
- id: 'gid://gitlab/WorkItem/444',
- },
+ id: 'gid://gitlab/WorkItem/444',
+ createdAt: '2022-08-03T12:41:54Z',
+ closedAt: null,
},
],
},
@@ -203,10 +248,14 @@ export const createWorkItemMutationResponse = {
title: 'Updated title',
state: 'OPEN',
description: 'description',
+ confidential: false,
+ createdAt: '2022-08-03T12:41:54Z',
+ closedAt: null,
workItemType: {
__typename: 'WorkItemType',
id: 'gid://gitlab/WorkItems::Type/5',
name: 'Task',
+ iconName: 'issue-type-task',
},
userPermissions: {
deleteWorkItem: false,
@@ -214,6 +263,7 @@ export const createWorkItemMutationResponse = {
},
widgets: [],
},
+ errors: [],
},
},
};
@@ -229,10 +279,14 @@ export const createWorkItemFromTaskMutationResponse = {
id: 'gid://gitlab/WorkItem/1',
title: 'Updated title',
state: 'OPEN',
+ confidential: false,
+ createdAt: '2022-08-03T12:41:54Z',
+ closedAt: null,
workItemType: {
__typename: 'WorkItemType',
id: 'gid://gitlab/WorkItems::Type/5',
name: 'Task',
+ iconName: 'issue-type-task',
},
userPermissions: {
deleteWorkItem: false,
@@ -252,11 +306,15 @@ export const createWorkItemFromTaskMutationResponse = {
id: 'gid://gitlab/WorkItem/1000000',
title: 'Updated title',
state: 'OPEN',
+ createdAt: '2022-08-03T12:41:54Z',
+ closedAt: null,
description: '',
+ confidential: false,
workItemType: {
__typename: 'WorkItemType',
id: 'gid://gitlab/WorkItems::Type/5',
name: 'Task',
+ iconName: 'issue-type-task',
},
userPermissions: {
deleteWorkItem: false,
@@ -284,6 +342,32 @@ export const deleteWorkItemFailureResponse = {
],
};
+export const deleteWorkItemMutationErrorResponse = {
+ data: {
+ workItemDelete: {
+ errors: ['Error'],
+ },
+ },
+};
+
+export const deleteWorkItemFromTaskMutationResponse = {
+ data: {
+ workItemDeleteTask: {
+ workItem: { id: 123, descriptionHtml: 'updated work item desc' },
+ errors: [],
+ },
+ },
+};
+
+export const deleteWorkItemFromTaskMutationErrorResponse = {
+ data: {
+ workItemDeleteTask: {
+ workItem: { id: 123, descriptionHtml: 'updated work item desc' },
+ errors: ['Error'],
+ },
+ },
+};
+
export const workItemTitleSubscriptionResponse = {
data: {
issuableTitleUpdated: {
@@ -302,6 +386,13 @@ export const workItemHierarchyEmptyResponse = {
__typename: 'WorkItemType',
},
title: 'New title',
+ createdAt: '2022-08-03T12:41:54Z',
+ closedAt: null,
+ userPermissions: {
+ deleteWorkItem: false,
+ updateWorkItem: false,
+ },
+ confidential: false,
widgets: [
{
type: 'DESCRIPTION',
@@ -322,6 +413,54 @@ export const workItemHierarchyEmptyResponse = {
},
};
+export const workItemHierarchyNoUpdatePermissionResponse = {
+ data: {
+ workItem: {
+ id: 'gid://gitlab/WorkItem/1',
+ workItemType: {
+ id: 'gid://gitlab/WorkItems::Type/6',
+ __typename: 'WorkItemType',
+ },
+ title: 'New title',
+ userPermissions: {
+ deleteWorkItem: false,
+ updateWorkItem: false,
+ },
+ confidential: false,
+ widgets: [
+ {
+ type: 'DESCRIPTION',
+ __typename: 'WorkItemWidgetDescription',
+ },
+ {
+ type: 'HIERARCHY',
+ parent: null,
+ children: {
+ nodes: [
+ {
+ id: 'gid://gitlab/WorkItem/2',
+ workItemType: {
+ id: 'gid://gitlab/WorkItems::Type/5',
+ __typename: 'WorkItemType',
+ },
+ title: 'xyz',
+ state: 'OPEN',
+ confidential: false,
+ createdAt: '2022-08-03T12:41:54Z',
+ closedAt: null,
+ __typename: 'WorkItem',
+ },
+ ],
+ __typename: 'WorkItemConnection',
+ },
+ __typename: 'WorkItemWidgetHierarchy',
+ },
+ ],
+ __typename: 'WorkItem',
+ },
+ },
+};
+
export const workItemHierarchyResponse = {
data: {
workItem: {
@@ -331,6 +470,11 @@ export const workItemHierarchyResponse = {
__typename: 'WorkItemType',
},
title: 'New title',
+ userPermissions: {
+ deleteWorkItem: true,
+ updateWorkItem: true,
+ },
+ confidential: false,
widgets: [
{
type: 'DESCRIPTION',
@@ -349,6 +493,9 @@ export const workItemHierarchyResponse = {
},
title: 'xyz',
state: 'OPEN',
+ confidential: true,
+ createdAt: '2022-08-03T12:41:54Z',
+ closedAt: null,
__typename: 'WorkItem',
},
{
@@ -359,6 +506,9 @@ export const workItemHierarchyResponse = {
},
title: 'abc',
state: 'CLOSED',
+ confidential: false,
+ createdAt: '2022-08-03T12:41:54Z',
+ closedAt: '2022-08-12T13:07:52Z',
__typename: 'WorkItem',
},
{
@@ -369,6 +519,9 @@ export const workItemHierarchyResponse = {
},
title: 'bar',
state: 'OPEN',
+ confidential: false,
+ createdAt: '2022-08-03T12:41:54Z',
+ closedAt: null,
__typename: 'WorkItem',
},
{
@@ -379,6 +532,9 @@ export const workItemHierarchyResponse = {
},
title: 'foobar',
state: 'OPEN',
+ confidential: false,
+ createdAt: '2022-08-03T12:41:54Z',
+ closedAt: null,
__typename: 'WorkItem',
},
],
@@ -396,14 +552,34 @@ export const changeWorkItemParentMutationResponse = {
data: {
workItemUpdate: {
workItem: {
- id: 'gid://gitlab/WorkItem/2',
+ __typename: 'WorkItem',
workItemType: {
- id: 'gid://gitlab/WorkItems::Type/5',
__typename: 'WorkItemType',
+ id: 'gid://gitlab/WorkItems::Type/1',
+ name: 'Issue',
+ iconName: 'issue-type-issue',
},
- title: 'Foo',
+ userPermissions: {
+ deleteWorkItem: true,
+ updateWorkItem: true,
+ },
+ description: null,
+ id: 'gid://gitlab/WorkItem/2',
state: 'OPEN',
- __typename: 'WorkItem',
+ title: 'Foo',
+ confidential: false,
+ createdAt: '2022-08-03T12:41:54Z',
+ closedAt: null,
+ widgets: [
+ {
+ __typename: 'WorkItemWidgetHierarchy',
+ type: 'HIERARCHY',
+ parent: null,
+ children: {
+ nodes: [],
+ },
+ },
+ ],
},
errors: [],
__typename: 'WorkItemUpdatePayload',
@@ -423,6 +599,7 @@ export const availableWorkItemsResponse = {
id: 'gid://gitlab/WorkItem/458',
title: 'Task 1',
state: 'OPEN',
+ createdAt: '2022-08-03T12:41:54Z',
},
},
{
@@ -430,6 +607,7 @@ export const availableWorkItemsResponse = {
id: 'gid://gitlab/WorkItem/459',
title: 'Task 2',
state: 'OPEN',
+ createdAt: '2022-08-03T12:41:54Z',
},
},
],
@@ -551,11 +729,3 @@ export const projectLabelsResponse = {
},
},
};
-
-export const mockParent = {
- parent: {
- id: 'gid://gitlab/Issue/1',
- iid: '5',
- title: 'Parent title',
- },
-};
diff --git a/spec/frontend/work_items/pages/work_item_detail_spec.js b/spec/frontend/work_items/pages/work_item_detail_spec.js
index 43869468ad0..823981df880 100644
--- a/spec/frontend/work_items/pages/work_item_detail_spec.js
+++ b/spec/frontend/work_items/pages/work_item_detail_spec.js
@@ -1,11 +1,12 @@
-import { GlAlert, GlSkeletonLoader, GlButton } from '@gitlab/ui';
+import { GlAlert, GlBadge, GlLoadingIcon, GlSkeletonLoader, GlButton } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import Vue from 'vue';
+import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
import WorkItemDetail from '~/work_items/components/work_item_detail.vue';
+import WorkItemActions from '~/work_items/components/work_item_actions.vue';
import WorkItemDescription from '~/work_items/components/work_item_description.vue';
import WorkItemState from '~/work_items/components/work_item_state.vue';
import WorkItemTitle from '~/work_items/components/work_item_title.vue';
@@ -16,6 +17,8 @@ import WorkItemInformation from '~/work_items/components/work_item_information.v
import { i18n } from '~/work_items/constants';
import workItemQuery from '~/work_items/graphql/work_item.query.graphql';
import workItemTitleSubscription from '~/work_items/graphql/work_item_title.subscription.graphql';
+import 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 '~/work_items/graphql/provider';
import { useLocalStorageSpy } from 'helpers/local_storage_helper';
import {
@@ -30,12 +33,19 @@ describe('WorkItemDetail component', () => {
Vue.use(VueApollo);
- const workItemQueryResponse = workItemResponseFactory();
+ const workItemQueryResponse = workItemResponseFactory({ canUpdate: true, canDelete: true });
+ const workItemQueryResponseWithoutParent = workItemResponseFactory({
+ parent: null,
+ canUpdate: true,
+ canDelete: true,
+ });
const successHandler = jest.fn().mockResolvedValue(workItemQueryResponse);
const initialSubscriptionHandler = jest.fn().mockResolvedValue(workItemTitleSubscriptionResponse);
const findAlert = () => wrapper.findComponent(GlAlert);
const findSkeleton = () => wrapper.findComponent(GlSkeletonLoader);
+ const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
+ const findWorkItemActions = () => wrapper.findComponent(WorkItemActions);
const findWorkItemTitle = () => wrapper.findComponent(WorkItemTitle);
const findWorkItemState = () => wrapper.findComponent(WorkItemState);
const findWorkItemDescription = () => wrapper.findComponent(WorkItemDescription);
@@ -51,17 +61,21 @@ describe('WorkItemDetail component', () => {
const createComponent = ({
isModal = false,
+ updateInProgress = false,
workItemId = workItemQueryResponse.data.workItem.id,
handler = successHandler,
subscriptionHandler = initialSubscriptionHandler,
+ confidentialityMock = [updateWorkItemMutation, jest.fn()],
workItemsMvc2Enabled = false,
includeWidgets = false,
+ error = undefined,
} = {}) => {
wrapper = shallowMount(WorkItemDetail, {
apolloProvider: createMockApollo(
[
[workItemQuery, handler],
[workItemTitleSubscription, subscriptionHandler],
+ confidentialityMock,
],
{},
{
@@ -69,6 +83,12 @@ describe('WorkItemDetail component', () => {
},
),
propsData: { isModal, workItemId },
+ data() {
+ return {
+ updateInProgress,
+ error,
+ };
+ },
provide: {
glFeatures: {
workItemsMvc2: workItemsMvc2Enabled,
@@ -146,6 +166,148 @@ describe('WorkItemDetail component', () => {
});
});
+ describe('confidentiality', () => {
+ const errorMessage = 'Mutation failed';
+ const confidentialWorkItem = workItemResponseFactory({
+ confidential: true,
+ });
+
+ // Mocks for work item without parent
+ const withoutParentExpectedInputVars = {
+ id: workItemQueryResponse.data.workItem.id,
+ confidential: true,
+ };
+ const toggleConfidentialityWithoutParentHandler = jest.fn().mockResolvedValue({
+ data: {
+ workItemUpdate: {
+ workItem: confidentialWorkItem.data.workItem,
+ errors: [],
+ },
+ },
+ });
+ const withoutParentHandlerMock = jest
+ .fn()
+ .mockResolvedValue(workItemQueryResponseWithoutParent);
+ const confidentialityWithoutParentMock = [
+ updateWorkItemMutation,
+ toggleConfidentialityWithoutParentHandler,
+ ];
+ const confidentialityWithoutParentFailureMock = [
+ updateWorkItemMutation,
+ jest.fn().mockRejectedValue(new Error(errorMessage)),
+ ];
+
+ // Mocks for work item with parent
+ const withParentExpectedInputVars = {
+ id: mockParent.parent.id,
+ taskData: { id: workItemQueryResponse.data.workItem.id, confidential: true },
+ };
+ const toggleConfidentialityWithParentHandler = jest.fn().mockResolvedValue({
+ data: {
+ workItemUpdate: {
+ workItem: {
+ id: confidentialWorkItem.data.workItem.id,
+ descriptionHtml: confidentialWorkItem.data.workItem.description,
+ },
+ task: {
+ workItem: confidentialWorkItem.data.workItem,
+ confidential: true,
+ },
+ errors: [],
+ },
+ },
+ });
+ const confidentialityWithParentMock = [
+ updateWorkItemTaskMutation,
+ toggleConfidentialityWithParentHandler,
+ ];
+ const confidentialityWithParentFailureMock = [
+ updateWorkItemTaskMutation,
+ jest.fn().mockRejectedValue(new Error(errorMessage)),
+ ];
+
+ describe.each`
+ context | handlerMock | confidentialityMock | confidentialityFailureMock | inputVariables
+ ${'no parent'} | ${withoutParentHandlerMock} | ${confidentialityWithoutParentMock} | ${confidentialityWithoutParentFailureMock} | ${withoutParentExpectedInputVars}
+ ${'parent'} | ${successHandler} | ${confidentialityWithParentMock} | ${confidentialityWithParentFailureMock} | ${withParentExpectedInputVars}
+ `(
+ 'when work item has $context',
+ ({ handlerMock, confidentialityMock, confidentialityFailureMock, inputVariables }) => {
+ it('renders confidential badge when work item is confidential', async () => {
+ createComponent({
+ handler: jest.fn().mockResolvedValue(confidentialWorkItem),
+ confidentialityMock,
+ });
+
+ await waitForPromises();
+
+ const confidentialBadge = wrapper.findComponent(GlBadge);
+ expect(confidentialBadge.exists()).toBe(true);
+ expect(confidentialBadge.props()).toMatchObject({
+ variant: 'warning',
+ icon: 'eye-slash',
+ });
+ expect(confidentialBadge.attributes('title')).toBe(
+ 'Only project members with at least the Reporter role, the author, and assignees can view or be notified about this task.',
+ );
+ expect(confidentialBadge.text()).toBe('Confidential');
+ });
+
+ it('renders gl-loading-icon while update mutation is in progress', async () => {
+ createComponent({
+ handler: handlerMock,
+ confidentialityMock,
+ });
+
+ await waitForPromises();
+
+ findWorkItemActions().vm.$emit('toggleWorkItemConfidentiality', true);
+
+ await nextTick();
+
+ expect(findLoadingIcon().exists()).toBe(true);
+ });
+
+ it('emits workItemUpdated and shows confidentiality badge when mutation is successful', async () => {
+ createComponent({
+ handler: handlerMock,
+ confidentialityMock,
+ });
+
+ await waitForPromises();
+
+ findWorkItemActions().vm.$emit('toggleWorkItemConfidentiality', true);
+ await waitForPromises();
+
+ expect(wrapper.emitted('workItemUpdated')).toEqual([[{ confidential: true }]]);
+ expect(confidentialityMock[1]).toHaveBeenCalledWith({
+ input: inputVariables,
+ });
+ expect(findLoadingIcon().exists()).toBe(false);
+ });
+
+ it('shows alert message when mutation fails', async () => {
+ createComponent({
+ handler: handlerMock,
+ confidentialityMock: confidentialityFailureMock,
+ });
+
+ await waitForPromises();
+ findWorkItemActions().vm.$emit('toggleWorkItemConfidentiality', true);
+ await waitForPromises();
+
+ expect(wrapper.emitted('workItemUpdated')).toBeFalsy();
+
+ await nextTick();
+
+ expect(findAlert().exists()).toBe(true);
+ expect(findAlert().text()).toBe(errorMessage);
+ expect(findLoadingIcon().exists()).toBe(false);
+ });
+ },
+ );
+ });
+
describe('description', () => {
it('does not show description widget if loading description fails', () => {
createComponent();
@@ -169,7 +331,7 @@ describe('WorkItemDetail component', () => {
});
it('does not show secondary breadcrumbs if there is not a parent', async () => {
- createComponent();
+ createComponent({ handler: jest.fn().mockResolvedValue(workItemQueryResponseWithoutParent) });
await waitForPromises();
@@ -177,7 +339,7 @@ describe('WorkItemDetail component', () => {
});
it('shows work item type if there is not a parent', async () => {
- createComponent();
+ createComponent({ handler: jest.fn().mockResolvedValue(workItemQueryResponseWithoutParent) });
await waitForPromises();
expect(findWorkItemType().exists()).toBe(true);
@@ -276,34 +438,29 @@ describe('WorkItemDetail component', () => {
});
describe('weight widget', () => {
- describe('when work_items_mvc_2 feature flag is enabled', () => {
- describe.each`
- description | includeWidgets | exists
- ${'when widget is returned from API'} | ${true} | ${true}
- ${'when widget is not returned from API'} | ${false} | ${false}
- `('$description', ({ includeWidgets, exists }) => {
- it(`${includeWidgets ? 'renders' : 'does not render'} weight component`, async () => {
- createComponent({ includeWidgets, workItemsMvc2Enabled: true });
- await waitForPromises();
+ describe.each`
+ description | weightWidgetPresent | exists
+ ${'when widget is returned from API'} | ${true} | ${true}
+ ${'when widget is not returned from API'} | ${false} | ${false}
+ `('$description', ({ weightWidgetPresent, exists }) => {
+ it(`${weightWidgetPresent ? 'renders' : 'does not render'} weight component`, async () => {
+ const response = workItemResponseFactory({ weightWidgetPresent });
+ const handler = jest.fn().mockResolvedValue(response);
+ createComponent({ handler });
+ await waitForPromises();
- expect(findWorkItemWeight().exists()).toBe(exists);
- });
+ expect(findWorkItemWeight().exists()).toBe(exists);
});
});
- describe('when work_items_mvc_2 feature flag is disabled', () => {
- describe.each`
- description | includeWidgets | exists
- ${'when widget is returned from API'} | ${true} | ${false}
- ${'when widget is not returned from API'} | ${false} | ${false}
- `('$description', ({ includeWidgets, exists }) => {
- it(`${includeWidgets ? 'renders' : 'does not render'} weight component`, async () => {
- createComponent({ includeWidgets, workItemsMvc2Enabled: false });
- await waitForPromises();
+ it('shows an error message when it emits an `error` event', async () => {
+ createComponent({ workItemsMvc2Enabled: true });
+ await waitForPromises();
- expect(findWorkItemWeight().exists()).toBe(exists);
- });
- });
+ findWorkItemWeight().vm.$emit('error', i18n.updateError);
+ await waitForPromises();
+
+ expect(findAlert().text()).toBe(i18n.updateError);
});
});
diff --git a/spec/frontend/work_items_hierarchy/components/app_spec.js b/spec/frontend/work_items_hierarchy/components/app_spec.js
index 092e9c90553..1426fbfab80 100644
--- a/spec/frontend/work_items_hierarchy/components/app_spec.js
+++ b/spec/frontend/work_items_hierarchy/components/app_spec.js
@@ -1,19 +1,17 @@
-import { nextTick } from 'vue';
-import { createLocalVue, mount } from '@vue/test-utils';
+import Vue, { nextTick } from 'vue';
+import { mount } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
import { GlBanner } from '@gitlab/ui';
import App from '~/work_items_hierarchy/components/app.vue';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
-const localVue = createLocalVue();
-localVue.use(VueApollo);
+Vue.use(VueApollo);
describe('WorkItemsHierarchy App', () => {
let wrapper;
const createComponent = (props = {}, data = {}) => {
wrapper = extendedWrapper(
mount(App, {
- localVue,
provide: {
illustrationPath: '/foo.svg',
licensePlan: 'free',
diff --git a/spec/frontend/work_items_hierarchy/components/hierarchy_spec.js b/spec/frontend/work_items_hierarchy/components/hierarchy_spec.js
index 74774e38d6b..67420e7fc2a 100644
--- a/spec/frontend/work_items_hierarchy/components/hierarchy_spec.js
+++ b/spec/frontend/work_items_hierarchy/components/hierarchy_spec.js
@@ -1,4 +1,5 @@
-import { createLocalVue, mount } from '@vue/test-utils';
+import { mount } from '@vue/test-utils';
+import Vue from 'vue';
import VueApollo from 'vue-apollo';
import { GlBadge } from '@gitlab/ui';
import Hierarchy from '~/work_items_hierarchy/components/hierarchy.vue';
@@ -6,8 +7,7 @@ import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import RESPONSE from '~/work_items_hierarchy/static_response';
import { workItemTypes } from '~/work_items_hierarchy/constants';
-const localVue = createLocalVue();
-localVue.use(VueApollo);
+Vue.use(VueApollo);
describe('WorkItemsHierarchy Hierarchy', () => {
let wrapper;
@@ -32,7 +32,6 @@ describe('WorkItemsHierarchy Hierarchy', () => {
const createComponent = (props = {}) => {
wrapper = extendedWrapper(
mount(Hierarchy, {
- localVue,
propsData: {
workItemTypes: props.workItemTypes,
...props,